Skip to content

Fix Dict limit printing of small values ending with color #45521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 57 additions & 49 deletions base/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,54 +48,61 @@ show(io::IO, ::MIME"text/plain", c::ComposedFunction) = show(io, c)
show(io::IO, ::MIME"text/plain", c::Returns) = show(io, c)
show(io::IO, ::MIME"text/plain", s::Splat) = show(io, s)

const ansi_regex = r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"
# An iterator similar to `pairs` but skips over "tokens" corresponding to
# ansi sequences
struct IgnoreAnsiIterator
captures::Base.RegexMatchIterator
const ansi_regex = r"(?s)(?:\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))|."

# Pseudo-character representing an ANSI delimiter
struct ANSIDelimiter
del::SubString{String}
end
IgnoreAnsiIterator(s::AbstractString) =
IgnoreAnsiIterator(eachmatch(ansi_regex, s))
ncodeunits(c::ANSIDelimiter) = ncodeunits(c.del)
textwidth(::ANSIDelimiter) = 0

Base.IteratorSize(::Type{IgnoreAnsiIterator}) = Base.SizeUnknown()
function iterate(I::IgnoreAnsiIterator, (i, m_st)=(1, iterate(I.captures)))
# Advance until the next non ansi sequence
if m_st !== nothing
m, j = m_st
if m.offset == i
i += sizeof(m.match)
return iterate(I, (i, iterate(I.captures, j)))
end
end
ci = iterate(I.captures.string, i)
ci === nothing && return nothing
i_prev = i
(c, i) = ci
return (i_prev => c), (i, m_st)
# An iterator similar to `pairs(::String)` but whose values are Char or ANSIDelimiter
struct ANSIIterator
captures::RegexMatchIterator
end
ANSIIterator(s::AbstractString) = ANSIIterator(eachmatch(ansi_regex, s))

IteratorSize(::Type{ANSIIterator}) = SizeUnknown()
eltype(::Type{ANSIIterator}) = Pair{Int, Union{Char,ANSIDelimiter}}
function iterate(I::ANSIIterator, (i, m_st)=(1, iterate(I.captures)))
m_st === nothing && return nothing
m, (j, new_m_st) = m_st
c = lastindex(m.match) == 1 ? only(m.match) : ANSIDelimiter(m.match)
return (i => c, (j, iterate(I.captures, (j, new_m_st))))
end
textwidth(I::ANSIIterator) = mapreduce(textwidth∘last, +, I; init=0)

function _truncate_at_width_or_chars(ignore_ansi::Bool, str, width, chars="", truncmark="…")
function _truncate_at_width_or_chars(ignore_ANSI::Bool, str, width, rpad=false, chars="\r\n", truncmark="…")
truncwidth = textwidth(truncmark)
(width <= 0 || width < truncwidth) && return ""
wid = truncidx = lastidx = 0
ignore_ansi &= match(ansi_regex, str) !== nothing
I = ignore_ansi ? IgnoreAnsiIterator(str) : pairs(str)
for (_lastidx, c) in I
lastidx = _lastidx
wid += textwidth(c)
if wid >= (width - truncwidth) && truncidx == 0
truncidx = lastidx
end
(wid >= width || c in chars) && break
end
if lastidx != 0 && str[lastidx] in chars
lastidx = prevind(str, lastidx)
end
# if str needs to be truncated, truncidx is the index of truncation.
stop = false # once set, only ANSI delimiters will be kept as new characters.
needANSIend = false # set if the last ANSI delimiter before truncidx is not "\033[0m".
I = ignore_ANSI ? ANSIIterator(str) : pairs(str)
for (i, c) in I
if c isa ANSIDelimiter
truncidx == 0 && (needANSIend = c != "\033[0m")
lastidx = i + ncodeunits(c) - 1
else
stop && break
wid += textwidth(c)
truncidx == 0 && wid > (width - truncwidth) && (truncidx = lastidx)
lastidx = i
c in chars && break
stop = wid >= width
end
end
lastidx == 0 && return rpad ? ' '^width : ""
str[lastidx] in chars && (lastidx = prevind(str, lastidx))
ANSIend = needANSIend ? "\033[0m" : ""
pad = rpad ? repeat(' ', max(0, width-wid)) : ""
truncidx == 0 && (truncidx = lastidx)
if lastidx < lastindex(str)
return String(SubString(str, 1, truncidx) * truncmark)
return string(SubString(str, 1, truncidx), ANSIend, truncmark, pad)
else
return String(str)
return string(str, ANSIend, pad)
end
end

Expand All @@ -122,7 +129,7 @@ function show(io::IO, ::MIME"text/plain", iter::Union{KeySet,ValueIterator})

if limit
str = sprint(show, v, context=io, sizehint=0)
str = _truncate_at_width_or_chars(get(io, :color, false), str, cols, "\r\n")
str = _truncate_at_width_or_chars(get(io, :color, false), str, cols)
print(io, str)
else
show(io, v)
Expand Down Expand Up @@ -154,19 +161,20 @@ function show(io::IO, ::MIME"text/plain", t::AbstractDict{K,V}) where {K,V}
rows -= 1 # Subtract the summary

# determine max key width to align the output, caching the strings
hascolor = get(recur_io, :color, false)
ks = Vector{String}(undef, min(rows, length(t)))
vs = Vector{String}(undef, min(rows, length(t)))
keylen = 0
vallen = 0
keywidth = 0
valwidth = 0
for (i, (k, v)) in enumerate(t)
i > rows && break
ks[i] = sprint(show, k, context=recur_io_k, sizehint=0)
vs[i] = sprint(show, v, context=recur_io_v, sizehint=0)
keylen = clamp(length(ks[i]), keylen, cols)
vallen = clamp(length(vs[i]), vallen, cols)
keywidth = clamp(hascolor ? textwidth(ANSIIterator(ks[i])) : textwidth(ks[i]), keywidth, cols)
valwidth = clamp(hascolor ? textwidth(ANSIIterator(vs[i])) : textwidth(vs[i]), valwidth, cols)
end
if keylen > max(div(cols, 2), cols - vallen)
keylen = max(cld(cols, 3), cols - vallen)
if keywidth > max(div(cols, 2), cols - valwidth)
keywidth = max(cld(cols, 3), cols - valwidth)
end
else
rows = cols = typemax(Int)
Expand All @@ -175,20 +183,20 @@ function show(io::IO, ::MIME"text/plain", t::AbstractDict{K,V}) where {K,V}
for (i, (k, v)) in enumerate(t)
print(io, "\n ")
if i == rows < length(t)
print(io, rpad("⋮", keylen), " => ⋮")
print(io, rpad("⋮", keywidth), " => ⋮")
break
end

if limit
key = rpad(_truncate_at_width_or_chars(get(recur_io, :color, false), ks[i], keylen, "\r\n"), keylen)
key = _truncate_at_width_or_chars(hascolor, ks[i], keywidth, true)
else
key = sprint(show, k, context=recur_io_k, sizehint=0)
end
print(recur_io, key)
print(io, " => ")

if limit
val = _truncate_at_width_or_chars(get(recur_io, :color, false), vs[i], cols - keylen, "\r\n")
val = _truncate_at_width_or_chars(hascolor, vs[i], cols - keywidth)
print(io, val)
else
show(recur_io_v, v)
Expand Down Expand Up @@ -232,7 +240,7 @@ function show(io::IO, ::MIME"text/plain", t::AbstractSet{T}) where T

if limit
str = sprint(show, v, context=recur_io, sizehint=0)
print(io, _truncate_at_width_or_chars(get(io, :color, false), str, cols, "\r\n"))
print(io, _truncate_at_width_or_chars(get(io, :color, false), str, cols))
else
show(recur_io, v)
end
Expand Down
102 changes: 93 additions & 9 deletions test/dict.jl
Original file line number Diff line number Diff line change
Expand Up @@ -369,23 +369,107 @@ end
end


struct RainBowString
struct RainbowString
s::String
end

function Base.show(io::IO, rbs::RainBowString)
for s in rbs.s
_, color = rand(Base.text_colors)
print(io, color, s, "\e[0m")
bold::Bool
other::Bool
valid::Bool
offset::Int
end
RainbowString(s, bold=false, other=false, valid=true) = RainbowString(s, bold, other, valid, 0)

function Base.show(io::IO, rbs::RainbowString)
for (i, s) in enumerate(rbs.s)
if i ≤ rbs.offset
print(io, s)
continue
end
color = rbs.other ? string("\033[4", rand(1:7), 'm') : Base.text_colors[rand(0:255)]
if rbs.bold
printstyled(io, color, s; bold=true)
else
print(io, color, s)
end
if rbs.valid
print(io, '\033', '[', rbs.other ? "0" : "39", 'm') # end of color marker
end
end
end

@testset "Display with colors" begin
d = Dict([randstring(8) => [RainBowString(randstring(8)) for i in 1:10] for j in 1:5]...)
d = Dict([randstring(8) => [RainbowString(randstring(8)) for i in 1:10] for j in 1:5]...)
str = sprint(io -> show(io, MIME("text/plain"), d); context = (:displaysize=>(30,80), :color=>true, :limit=>true))
lines = split(str, '\n')
@test all(endswith('…'), lines[2:end])
@test all(endswith("\033[0m…"), lines[2:end])
@test all(x -> length(x) > 100, lines[2:end])

d2 = Dict(:foo => RainbowString("bar"))
str2 = sprint(io -> show(io, MIME("text/plain"), d2); context = (:displaysize=>(30,80), :color=>true, :limit=>true))
@test !occursin('…', str2)
@test endswith(str2, "\033[0m")

d3 = Dict(:foo => RainbowString("bar", true))
str3 = sprint(io -> show(io, MIME("text/plain"), d3); context = (:displaysize=>(30,80), :color=>true, :limit=>true))
@test !occursin('…', str3)
@test endswith(str3, "\033[0m")

d4 = Dict(RainbowString(randstring(8), true) => nothing)
str4 = sprint(io -> show(io, MIME("text/plain"), d4); context = (:displaysize=>(30,20), :color=>true, :limit=>true))
@test endswith(str4, "\033[0m… => nothing")

d5 = Dict(RainbowString(randstring(30), false, true, false) => nothing)
str5 = sprint(io -> show(io, MIME("text/plain"), d5); context = (:displaysize=>(30,30), :color=>true, :limit=>true))
@test endswith(str5, "\033[0m… => nothing")

d6 = Dict(randstring(8) => RainbowString(randstring(30), true, true, false) for _ in 1:3)
str6 = sprint(io -> show(io, MIME("text/plain"), d6); context = (:displaysize=>(30,30), :color=>true, :limit=>true))
lines6 = split(str6, '\n')
@test all(endswith("\033[0m…"), lines6[2:end])
@test all(x -> length(x) > 100, lines6[2:end])
str6_long = sprint(io -> show(io, MIME("text/plain"), d6); context = (:displaysize=>(30,80), :color=>true, :limit=>true))
lines6_long = split(str6_long, '\n')
@test all(endswith("\033[0m"), lines6_long[2:end])

d7 = Dict(randstring(8) => RainbowString(randstring(30)))
str7 = sprint(io -> show(io, MIME("text/plain"), d7); context = (:displaysize=>(30,20), :color=>true, :limit=>true))
line7 = split(str7, '\n')[2]
@test endswith(line7, "\033[0m…")
@test length(line7) > 100

d8 = Dict(:x => RainbowString(randstring(10), false, false, false, 6))
str8 = sprint(io -> show(io, MIME("text/plain"), d8); context = (:displaysize=>(30,14), :color=>true, :limit=>true))
line8 = split(str8, '\n')[2]
@test !occursin("\033[", line8)
@test length(line8) == 14
str8_long = sprint(io -> show(io, MIME("text/plain"), d8); context = (:displaysize=>(30,16), :color=>true, :limit=>true))
line8_long = split(str8_long, '\n')[2]
@test endswith(line8_long, "\033[0m…")
@test length(line8_long) > 20

d9 = Dict(:x => RainbowString(repeat('苹', 5), false, true, false))
str9 = sprint(io -> show(io, MIME("text/plain"), d9); context = (:displaysize=>(30,15), :color=>true, :limit=>true))
@test endswith(str9, "\033[0m…")
@test count('苹', str9) == 3

d10 = Dict(:xy => RainbowString(repeat('苹', 5), false, true, false))
str10 = sprint(io -> show(io, MIME("text/plain"), d10); context = (:displaysize=>(30,15), :color=>true, :limit=>true))
@test endswith(str10, "\033[0m…")
@test count('苹', str10) == 2

d11 = Dict(RainbowString("abcdefgh", false, true, false) => 0, "123456" => 1)
str11 = sprint(io -> show(io, MIME("text/plain"), d11); context = (:displaysize=>(30,80), :color=>true, :limit=>true))
_, line11_a, line11_b = split(str11, '\n')
@test endswith(line11_a, "h\033[0m => 0") || endswith(line11_b, "h\033[0m => 0")
@test endswith(line11_a, "6\" => 1") || endswith(line11_b, "6\" => 1")

d12 = Dict(RainbowString(repeat(Char(48+i), 4), (i&1)==1, (i&2)==2, (i&4)==4) => i for i in 1:8)
str12 = sprint(io -> show(io, MIME("text/plain"), d12); context = (:displaysize=>(30,80), :color=>true, :limit=>true))
@test !occursin('…', str12)

d13 = Dict(RainbowString("foo\nbar") => 74)
str13 = sprint(io -> show(io, MIME("text/plain"), d13); context = (:displaysize=>(30,80), :color=>true, :limit=>true))
@test count('\n', str13) == 1
@test occursin('…', str13)
end

@testset "Issue #15739" begin # Compact REPL printouts of an `AbstractDict` use brackets when appropriate
Expand Down