Skip to content

Commit 67186f4

Browse files
show(::String): elide long strings (close #40724) (#40736)
1 parent e4f79b7 commit 67186f4

File tree

3 files changed

+99
-4
lines changed

3 files changed

+99
-4
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ New library features
5555
Standard library changes
5656
------------------------
5757

58+
* Long strings are now elided using the syntax `"head" ⋯ 12345 bytes ⋯ "tail"` when displayed in the REPL ([#40736]).
5859
* `count` and `findall` now accept an `AbstractChar` argument to search for a character in a string ([#38675]).
5960
* `range` now supports the `range(start, stop)` and `range(start, stop, length)` methods ([#39228]).
6061
* `range` now supports `start` as an optional keyword argument ([#38041]).

base/strings/io.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,54 @@ print(io::IO, s::AbstractString) = for c in s; print(io, c); end
190190
write(io::IO, s::AbstractString) = (len = 0; for c in s; len += Int(write(io, c))::Int; end; len)
191191
show(io::IO, s::AbstractString) = print_quoted(io, s)
192192

193+
# show elided string if more than `limit` characters
194+
function show(
195+
io :: IO,
196+
mime :: MIME"text/plain",
197+
str :: AbstractString;
198+
limit :: Union{Int, Nothing} = nothing,
199+
)
200+
# compute limit in default case
201+
if limit === nothing
202+
get(io, :limit, false) || return show(io, str)
203+
limit = max(20, displaysize(io)[2])
204+
# one line in collection, seven otherwise
205+
get(io, :typeinfo, nothing) === nothing && (limit *= 7)
206+
end
207+
208+
# early out for short strings
209+
len = ncodeunits(str)
210+
len  limit - 2 && # quote chars
211+
return show(io, str)
212+
213+
# these don't depend on string data
214+
units = codeunit(str) == UInt8 ? "bytes" : "code units"
215+
skip_text(skip) = "$skip $units"
216+
short = length(skip_text("")) + 4 # quote chars
217+
chars = max(limit, short + 1) - short # at least 1 digit
218+
219+
# figure out how many characters to print in elided case
220+
chars -= d = ndigits(len - chars) # first adjustment
221+
chars += d - ndigits(len - chars) # second if needed
222+
chars = max(0, chars)
223+
224+
# find head & tail, avoiding O(length(str)) computation
225+
head = nextind(str, 0, 1 + (chars + 1) ÷ 2)
226+
tail = prevind(str, len + 1, chars ÷ 2)
227+
228+
# threshold: min chars skipped to make elision worthwhile
229+
t = short + ndigits(len - chars) - 1
230+
n = tail - head # skipped code units
231+
if 4t n || t n && t length(str, head, tail-1)
232+
skip = skip_text(n)
233+
show(io, SubString(str, 1:prevind(str, head)))
234+
print(io, skip) # TODO: bold styled
235+
show(io, SubString(str, tail))
236+
else
237+
show(io, str)
238+
end
239+
end
240+
193241
# optimized methods to avoid iterating over chars
194242
write(io::IO, s::Union{String,SubString{String}}) =
195243
GC.@preserve s Int(unsafe_write(io, pointer(s), reinterpret(UInt, sizeof(s))))::Int

test/show.jl

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -808,13 +808,20 @@ Base.methodloc_callback[] = nothing
808808
# test that no spurious visual lines are added when one element spans multiple lines
809809
v = fill!(Array{Any}(undef, 9), 0)
810810
v[1] = "look I'm wide! --- " ^ 9
811-
@test replstr(v) == "9-element Vector{Any}:\n \"look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- \"\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n 0"
812-
@test replstr([fill(0, 9) v]) == "9×2 Matrix{Any}:\n 0 … \"look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- \"\n 0 0\n 0 0\n 0 0\n 0 0\n 0 … 0\n 0 0\n 0 0\n 0 0"
811+
r = replstr(v)
812+
@test startswith(r, "9-element Vector{Any}:\n \"look I'm wide! ---")
813+
@test endswith(r, "look I'm wide! --- \"\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n 0")
814+
813815
# test vertical/diagonal ellipsis
814816
v = fill!(Array{Any}(undef, 50), 0)
815817
v[1] = "look I'm wide! --- " ^ 9
816-
@test replstr(v) == "50-element Vector{Any}:\n \"look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- \"\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n 0"
817-
@test replstr([fill(0, 50) v]) == "50×2 Matrix{Any}:\n 0 … \"look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- look I'm wide! --- \"\n 0 0\n 0 0\n 0 0\n 0 0\n 0 … 0\n 0 0\n 0 0\n 0 0\n 0 0\n ⋮ ⋱ \n 0 0\n 0 0\n 0 0\n 0 0\n 0 … 0\n 0 0\n 0 0\n 0 0\n 0 0"
818+
r = replstr(v)
819+
@test startswith(r, "50-element Vector{Any}:\n \"look I'm wide! ---")
820+
@test endswith(r, "look I'm wide! --- \"\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n 0\n 0")
821+
822+
r = replstr([fill(0, 50) v])
823+
@test startswith(r, "50×2 Matrix{Any}:\n 0 … \"look I'm wide! ---")
824+
@test endswith(r, "look I'm wide! --- \"\n 0 0\n 0 0\n 0 0\n 0 0\n 0 … 0\n 0 0\n 0 0\n 0 0\n 0 0\n ⋮ ⋱ \n 0 0\n 0 0\n 0 0\n 0 0\n 0 … 0\n 0 0\n 0 0\n 0 0\n 0 0")
818825

819826
# issue #34659
820827
@test replstr(Int32[]) == "Int32[]"
@@ -825,6 +832,45 @@ Base.methodloc_callback[] = nothing
825832
@test replstr([zeros(3,0),zeros(2,0)]) == "2-element Vector{Matrix{Float64}}:\n 3×0 Matrix{Float64}\n 2×0 Matrix{Float64}"
826833
end
827834

835+
# string show with elision
836+
@testset "string show with elision" begin
837+
@testset "elision logic" begin
838+
strs = ["A", "", "∀A", "A∀", "😃"]
839+
for limit = 0:100, len = 0:100, str in strs
840+
str = str^len
841+
str = str[1:nextind(str, 0, len)]
842+
out = sprint() do io
843+
show(io, MIME"text/plain"(), str; limit)
844+
end
845+
lower = length("\"\"$(ncodeunits(str)) bytes ⋯ \"\"")
846+
limit = max(limit, lower)
847+
if length(str) + 2  limit
848+
@test eval(Meta.parse(out)) == str
849+
else
850+
@test limit-!isascii(str) <= length(out) <= limit
851+
re = r"(\"[^\"]*\") ⋯ (\d+) bytes ⋯ (\"[^\"]*\")"
852+
m = match(re, out)
853+
head = eval(Meta.parse(m.captures[1]))
854+
tail = eval(Meta.parse(m.captures[3]))
855+
skip = parse(Int, m.captures[2])
856+
@test startswith(str, head)
857+
@test endswith(str, tail)
858+
@test ncodeunits(str) ==
859+
ncodeunits(head) + skip + ncodeunits(tail)
860+
end
861+
end
862+
end
863+
864+
@testset "default elision limit" begin
865+
r = replstr("x"^1000)
866+
@test length(r) == 7*80
867+
@test r == repr("x"^271) * " ⋯ 459 bytes ⋯ " * repr("x"^270)
868+
r = replstr(["x"^1000])
869+
@test length(r) < 120
870+
@test r == "1-element Vector{String}:\n " * repr("x"^31) * " ⋯ 939 bytes ⋯ " * repr("x"^30)
871+
end
872+
end
873+
828874
# Issue 14121
829875
@test_repr "(A'x)'"
830876

0 commit comments

Comments
 (0)