Skip to content

Commit

Permalink
use textwidth for string display truncation (#55442)
Browse files Browse the repository at this point in the history
It makes a big difference when displaying strings that have width-2 or
width-0 characters.
  • Loading branch information
stevengj authored Oct 24, 2024
1 parent 5cdf378 commit bc66047
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 35 deletions.
32 changes: 13 additions & 19 deletions base/strings/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -214,35 +214,29 @@ function show(
# one line in collection, seven otherwise
get(io, :typeinfo, nothing) === nothing && (limit *= 7)
end
limit = max(0, limit-2) # quote chars

# early out for short strings
len = ncodeunits(str)
len limit - 2 && # quote chars
return show(io, str)
check_textwidth(str, limit) && return show(io, str)

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

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

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

# threshold: min chars skipped to make elision worthwhile
t = short + ndigits(len - chars) - 1
n = tail - head # skipped code units
if 4t n || t n && t length(str, head, tail-1)
skip = skip_text(n)
show(io, SubString(str, 1:prevind(str, head)))
printstyled(io, skip; color=:light_yellow, bold=true)
show(io, SubString(str, tail))
afterhead = nextind(str, head)
n = tail - afterhead # skipped code units
replacement = skip_text(n)
t = ncodeunits(replacement) # length of replacement (textwidth == ncodeunits here)
@views if 4t n || t n && t textwidth(str[afterhead:prevind(str,tail)])
show(io, str[begin:head])
printstyled(io, replacement; color=:light_yellow, bold=true)
show(io, str[tail:end])
else
show(io, str)
end
Expand Down
21 changes: 12 additions & 9 deletions base/strings/util.jl
Original file line number Diff line number Diff line change
Expand Up @@ -613,22 +613,25 @@ function ctruncate(str::AbstractString, maxwidth::Integer, replacement::Union{Ab
end
end

# return whether textwidth(str) <= maxwidth
function check_textwidth(str::AbstractString, maxwidth::Integer)
# check efficiently for early return if str is wider than maxwidth
total_width = 0
for c in str
total_width += textwidth(c)
total_width > maxwidth && return false
end
return true
end

function string_truncate_boundaries(
str::AbstractString,
maxwidth::Integer,
replacement::Union{AbstractString,AbstractChar},
::Val{mode},
prefer_left::Bool = true) where {mode}

maxwidth >= 0 || throw(ArgumentError("maxwidth $maxwidth should be non-negative"))

# check efficiently for early return if str is less wide than maxwidth
total_width = 0
for c in str
total_width += textwidth(c)
total_width > maxwidth && break
end
total_width <= maxwidth && return nothing
check_textwidth(str, maxwidth) && return nothing

l0, _ = left, right = firstindex(str), lastindex(str)
width = textwidth(replacement)
Expand Down
14 changes: 7 additions & 7 deletions test/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -928,19 +928,19 @@ end
# string show with elision
@testset "string show with elision" begin
@testset "elision logic" begin
strs = ["A", "", "∀A", "A∀", "😃"]
strs = ["A", "", "∀A", "A∀", "😃", ""]
for limit = 0:100, len = 0:100, str in strs
str = str^len
str = str[1:nextind(str, 0, len)]
out = sprint() do io
show(io, MIME"text/plain"(), str; limit)
end
lower = length("\"\"$(ncodeunits(str)) bytes ⋯ \"\"")
lower = textwidth("\"\"$(ncodeunits(str)) bytes ⋯ \"\"")
limit = max(limit, lower)
if length(str) + 2 limit
if textwidth(str) + 2 limit+1 && !contains(out, '')
@test eval(Meta.parse(out)) == str
else
@test limit-!isascii(str) <= length(out) <= limit
@test limit-2 <= textwidth(out) <= limit
re = r"(\"[^\"]*\") ⋯ (\d+) bytes ⋯ (\"[^\"]*\")"
m = match(re, out)
head = eval(Meta.parse(m.captures[1]))
Expand All @@ -956,11 +956,11 @@ end

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

Expand Down

0 comments on commit bc66047

Please sign in to comment.