Skip to content

Commit bc66047

Browse files
authored
use textwidth for string display truncation (#55442)
It makes a big difference when displaying strings that have width-2 or width-0 characters.
1 parent 5cdf378 commit bc66047

File tree

3 files changed

+32
-35
lines changed

3 files changed

+32
-35
lines changed

base/strings/io.jl

+13-19
Original file line numberDiff line numberDiff line change
@@ -214,35 +214,29 @@ function show(
214214
# one line in collection, seven otherwise
215215
get(io, :typeinfo, nothing) === nothing && (limit *= 7)
216216
end
217+
limit = max(0, limit-2) # quote chars
217218

218219
# early out for short strings
219-
len = ncodeunits(str)
220-
len limit - 2 && # quote chars
221-
return show(io, str)
220+
check_textwidth(str, limit) && return show(io, str)
222221

223222
# these don't depend on string data
224223
units = codeunit(str) == UInt8 ? "bytes" : "code units"
225224
skip_text(skip) = "$skip $units"
226-
short = length(skip_text("")) + 4 # quote chars
227-
chars = max(limit, short + 1) - short # at least 1 digit
228225

229-
# figure out how many characters to print in elided case
230-
chars -= d = ndigits(len - chars) # first adjustment
231-
chars += d - ndigits(len - chars) # second if needed
232-
chars = max(0, chars)
226+
# longest possible replacement string for omitted chars
227+
max_replacement = skip_text(ncodeunits(str) * 100) # *100 for 2 inner quote chars
233228

234-
# find head & tail, avoiding O(length(str)) computation
235-
head = nextind(str, 0, 1 + (chars + 1) ÷ 2)
236-
tail = prevind(str, len + 1, chars ÷ 2)
229+
head, tail = string_truncate_boundaries(str, limit, max_replacement, Val(:center))
237230

238231
# threshold: min chars skipped to make elision worthwhile
239-
t = short + ndigits(len - chars) - 1
240-
n = tail - head # skipped code units
241-
if 4t n || t n && t length(str, head, tail-1)
242-
skip = skip_text(n)
243-
show(io, SubString(str, 1:prevind(str, head)))
244-
printstyled(io, skip; color=:light_yellow, bold=true)
245-
show(io, SubString(str, tail))
232+
afterhead = nextind(str, head)
233+
n = tail - afterhead # skipped code units
234+
replacement = skip_text(n)
235+
t = ncodeunits(replacement) # length of replacement (textwidth == ncodeunits here)
236+
@views if 4t n || t n && t textwidth(str[afterhead:prevind(str,tail)])
237+
show(io, str[begin:head])
238+
printstyled(io, replacement; color=:light_yellow, bold=true)
239+
show(io, str[tail:end])
246240
else
247241
show(io, str)
248242
end

base/strings/util.jl

+12-9
Original file line numberDiff line numberDiff line change
@@ -613,22 +613,25 @@ function ctruncate(str::AbstractString, maxwidth::Integer, replacement::Union{Ab
613613
end
614614
end
615615

616+
# return whether textwidth(str) <= maxwidth
617+
function check_textwidth(str::AbstractString, maxwidth::Integer)
618+
# check efficiently for early return if str is wider than maxwidth
619+
total_width = 0
620+
for c in str
621+
total_width += textwidth(c)
622+
total_width > maxwidth && return false
623+
end
624+
return true
625+
end
626+
616627
function string_truncate_boundaries(
617628
str::AbstractString,
618629
maxwidth::Integer,
619630
replacement::Union{AbstractString,AbstractChar},
620631
::Val{mode},
621632
prefer_left::Bool = true) where {mode}
622-
623633
maxwidth >= 0 || throw(ArgumentError("maxwidth $maxwidth should be non-negative"))
624-
625-
# check efficiently for early return if str is less wide than maxwidth
626-
total_width = 0
627-
for c in str
628-
total_width += textwidth(c)
629-
total_width > maxwidth && break
630-
end
631-
total_width <= maxwidth && return nothing
634+
check_textwidth(str, maxwidth) && return nothing
632635

633636
l0, _ = left, right = firstindex(str), lastindex(str)
634637
width = textwidth(replacement)

test/show.jl

+7-7
Original file line numberDiff line numberDiff line change
@@ -928,19 +928,19 @@ end
928928
# string show with elision
929929
@testset "string show with elision" begin
930930
@testset "elision logic" begin
931-
strs = ["A", "", "∀A", "A∀", "😃"]
931+
strs = ["A", "", "∀A", "A∀", "😃", ""]
932932
for limit = 0:100, len = 0:100, str in strs
933933
str = str^len
934934
str = str[1:nextind(str, 0, len)]
935935
out = sprint() do io
936936
show(io, MIME"text/plain"(), str; limit)
937937
end
938-
lower = length("\"\"$(ncodeunits(str)) bytes ⋯ \"\"")
938+
lower = textwidth("\"\"$(ncodeunits(str)) bytes ⋯ \"\"")
939939
limit = max(limit, lower)
940-
if length(str) + 2 limit
940+
if textwidth(str) + 2 limit+1 && !contains(out, '')
941941
@test eval(Meta.parse(out)) == str
942942
else
943-
@test limit-!isascii(str) <= length(out) <= limit
943+
@test limit-2 <= textwidth(out) <= limit
944944
re = r"(\"[^\"]*\") ⋯ (\d+) bytes ⋯ (\"[^\"]*\")"
945945
m = match(re, out)
946946
head = eval(Meta.parse(m.captures[1]))
@@ -956,11 +956,11 @@ end
956956

957957
@testset "default elision limit" begin
958958
r = replstr("x"^1000)
959-
@test length(r) == 7*80
960-
@test r == repr("x"^271) * "459 bytes ⋯ " * repr("x"^270)
959+
@test length(r) == 7*80-1
960+
@test r == repr("x"^270) * "460 bytes ⋯ " * repr("x"^270)
961961
r = replstr(["x"^1000])
962962
@test length(r) < 120
963-
@test r == "1-element Vector{String}:\n " * repr("x"^31) * "939 bytes ⋯ " * repr("x"^30)
963+
@test r == "1-element Vector{String}:\n " * repr("x"^30) * "940 bytes ⋯ " * repr("x"^30)
964964
end
965965
end
966966

0 commit comments

Comments
 (0)