diff --git a/base/strings/io.jl b/base/strings/io.jl index acbd945c8e137..bb0b7ae5854f4 100644 --- a/base/strings/io.jl +++ b/base/strings/io.jl @@ -210,35 +210,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 diff --git a/base/strings/util.jl b/base/strings/util.jl index 0ba76e1c76fa0..04d451a4fd288 100644 --- a/base/strings/util.jl +++ b/base/strings/util.jl @@ -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) diff --git a/test/show.jl b/test/show.jl index 63663152d9d91..15fa9be679b56 100644 --- a/test/show.jl +++ b/test/show.jl @@ -865,19 +865,19 @@ end # string show with elision @testset "string show with elision" begin @testset "elision logic" begin - strs = ["A", "∀", "∀A", "A∀", "😃"] + strs = ["A", "∀", "∀A", "A∀", "😃", "x̂"] 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])) @@ -893,11 +893,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