Skip to content

Commit 485723e

Browse files
committed
Compute width excluding ansi delimiters
1 parent df8a6ab commit 485723e

File tree

2 files changed

+46
-19
lines changed

2 files changed

+46
-19
lines changed

base/show.jl

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ show(io::IO, ::MIME"text/plain", s::Splat) = show(io, s)
5050

5151
const possible_ansi_regex = r"\x1B(?:[@-Z\\-_\[])"
5252
const start_ansi_regex = r"\G(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"
53+
const next_ansi_regexes = r".(?:\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))+"
5354

5455
function _find_lastidx(str, width, chars, truncwidth, start)
5556
idx = start
@@ -76,7 +77,7 @@ function _find_lastidx(str, width, chars, truncwidth, start)
7677
if !isnothing(m)
7778
firstmatch == 0 && (firstmatch = lastidx)
7879
noansi = m.match == "[0m"
79-
s = sizeof(m.match)
80+
s = ncodeunits(m.match)
8081
idx += s
8182
lastidx = idx - 1 # the last character of an ansi delimiter is always of size 1.
8283
continue
@@ -85,27 +86,48 @@ function _find_lastidx(str, width, chars, truncwidth, start)
8586
truncidx == 0 && wid > (width - truncwidth) && (truncidx = last_lastidx)
8687
stop = (wid >= width || c in chars)
8788
end
88-
return lastidx, truncidx, noansi || (lastidx < lastindex(str) && truncidx < firstmatch)
89+
return lastidx, truncidx, noansi || (lastidx < lastindex(str) && truncidx < firstmatch), wid
8990
end
9091

91-
function _truncate_at_width_or_chars(ignore_ansi::Bool, str, width, chars="", truncmark="")
92+
function _truncate_at_width_or_chars(ignore_ansi::Bool, str, width, rpad=false, chars="\r\n", truncmark="")
9293
truncwidth = textwidth(truncmark)
93-
(width <= 0 || width < truncwidth) && return ""
94+
(width <= 0 || width < truncwidth) && return rpad ? ' '^width : ""
9495
# possible_substring is a subset of str at least as large as the returned string
9596
possible_substring = SubString(str, 1, thisind(str, min(ncodeunits(str), width+1)))
9697
m = ignore_ansi ? match(possible_ansi_regex, possible_substring) : nothing
9798
start_offset = isnothing(m) ? thisind(str, min(ncodeunits(str), width-truncwidth)) : m.offset
98-
lastidx, truncidx, noansi = _find_lastidx(str, width, chars, truncwidth, start_offset)
99-
lastidx == 0 && return ""
99+
lastidx, truncidx, noansi, wid = _find_lastidx(str, width, chars, truncwidth, start_offset)
100+
lastidx == 0 && return rpad ? ' '^width : ""
100101
str[lastidx] in chars && (lastidx = prevind(str, lastidx))
101102
truncidx == 0 && (truncidx = lastidx)
103+
endansi = noansi ? "" : "\033[0m"
104+
pad = rpad ? repeat(' ', max(0, width-wid)) : ""
102105
if lastidx < lastindex(str)
103-
return string(SubString(str, 1, truncidx), noansi ? "" : "\033[0m", truncmark)
106+
return string(SubString(str, 1, truncidx), endansi, truncmark, pad)
104107
else
105-
return noansi ? String(str) : string(str, "\033[0m")
108+
return string(str, endansi, pad)
106109
end
107110
end
108111

112+
function _width_excluding_color(hascolor, str)
113+
(!hascolor || length(str) < 2) && return textwidth(str)
114+
i = 1
115+
n = lastindex(str)
116+
while i n && str[i] == '\033'
117+
m = match(start_ansi_regex, str, i+1)
118+
m isa RegexMatch || break
119+
i = m.offset + ncodeunits(m.match)
120+
end
121+
m = match(next_ansi_regexes, str, i)
122+
wid = 0
123+
while m isa RegexMatch
124+
wid += textwidth(SubString(str, i, m.offset))
125+
i = m.offset + ncodeunits(m.match)
126+
m = match(next_ansi_regexes, str, i)
127+
end
128+
return wid + textwidth(SubString(str, i, n))
129+
end
130+
109131
function show(io::IO, ::MIME"text/plain", iter::Union{KeySet,ValueIterator})
110132
isempty(iter) && get(io, :compact, false) && return show(io, iter)
111133
summary(io, iter)
@@ -129,7 +151,7 @@ function show(io::IO, ::MIME"text/plain", iter::Union{KeySet,ValueIterator})
129151

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

163185
# determine max key width to align the output, caching the strings
186+
hascolor = get(recur_io, :color, false)
164187
ks = Vector{String}(undef, min(rows, length(t)))
165188
vs = Vector{String}(undef, min(rows, length(t)))
166-
keylen = 0
167-
vallen = 0
189+
keywidth = 0
190+
valwidth = 0
168191
for (i, (k, v)) in enumerate(t)
169192
i > rows && break
170193
ks[i] = sprint(show, k, context=recur_io_k, sizehint=0)
171194
vs[i] = sprint(show, v, context=recur_io_v, sizehint=0)
172-
keylen = clamp(length(ks[i]), keylen, cols)
173-
vallen = clamp(length(vs[i]), vallen, cols)
195+
keywidth = clamp(_width_excluding_color(hascolor, ks[i]), keywidth, cols)
196+
valwidth = clamp(_width_excluding_color(hascolor, vs[i]), valwidth, cols)
174197
end
175-
if keylen > max(div(cols, 2), cols - vallen)
176-
keylen = max(cld(cols, 3), cols - vallen)
198+
if keywidth > max(div(cols, 2), cols - valwidth)
199+
keywidth = max(cld(cols, 3), cols - valwidth)
177200
end
178201
else
179202
rows = cols = typemax(Int)
@@ -182,20 +205,20 @@ function show(io::IO, ::MIME"text/plain", t::AbstractDict{K,V}) where {K,V}
182205
for (i, (k, v)) in enumerate(t)
183206
print(io, "\n ")
184207
if i == rows < length(t)
185-
print(io, rpad("", keylen), " => ⋮")
208+
print(io, rpad("", keywidth), " => ⋮")
186209
break
187210
end
188211

189212
if limit
190-
key = rpad(_truncate_at_width_or_chars(get(recur_io, :color, false), ks[i], keylen, "\r\n"), keylen)
213+
key = _truncate_at_width_or_chars(hascolor, ks[i], keywidth, true)
191214
else
192215
key = sprint(show, k, context=recur_io_k, sizehint=0)
193216
end
194217
print(recur_io, key)
195218
print(io, " => ")
196219

197220
if limit
198-
val = _truncate_at_width_or_chars(get(recur_io, :color, false), vs[i], cols - keylen, "\r\n")
221+
val = _truncate_at_width_or_chars(hascolor, vs[i], cols - keywidth)
199222
print(io, val)
200223
else
201224
show(recur_io_v, v)
@@ -239,7 +262,7 @@ function show(io::IO, ::MIME"text/plain", t::AbstractSet{T}) where T
239262

240263
if limit
241264
str = sprint(show, v, context=recur_io, sizehint=0)
242-
print(io, _truncate_at_width_or_chars(get(io, :color, false), str, cols, "\r\n"))
265+
print(io, _truncate_at_width_or_chars(get(io, :color, false), str, cols))
243266
else
244267
show(recur_io, v)
245268
end

test/dict.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,10 @@ end
452452
str10 = sprint(io -> show(io, MIME("text/plain"), d10); context = (:displaysize=>(30,15), :color=>true, :limit=>true))
453453
@test endswith(str10, "\033[0m…")
454454
@test count('', str10) == 2
455+
456+
d11 = Dict(RainbowString("abcdefgh", false, true, false) => 0, "123456" => 1)
457+
str11 = sprint(io -> show(io, MIME("text/plain"), dict); context = (:displaysize=>(30,80), :color=>true, :limit=>true))
458+
@test endswith(str11, "\033[0m => 0")
455459
end
456460

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

0 commit comments

Comments
 (0)