From 69f3a68448a77a31b44a3b078f07cfa4ba61e348 Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 14 Mar 2026 18:28:09 +0100 Subject: [PATCH 1/3] adds print elision logic also for similar consecutive nodes. --- src/printing.jl | 83 ++++++++++++++++++++++++++++++++++++++++-------- test/printing.jl | 33 +++++++++++++++++++ 2 files changed, 103 insertions(+), 13 deletions(-) diff --git a/src/printing.jl b/src/printing.jl index 0215ce3..c08b3c6 100644 --- a/src/printing.jl +++ b/src/printing.jl @@ -15,6 +15,10 @@ Print a text representation of `tree` to the given `io` object. * `io::IO` - IO stream to write to. * `tree` - tree to print. * `maxdepth::Integer = 5` - truncate printing of subtrees at this depth. +* `maxsibling::Union{Nothing,Integer} = nothing` - if set, print at most this many + consecutive similar siblings and then elide the rest of that run. +* `sibling_similarity_threshold::Real = 0` - threshold for considering sibling node values + similar. Non-numeric node values currently only support exact matching. * `indicate_truncation::Bool = true` - print a vertical ellipsis character beneath truncated nodes. * `charset::TreeCharSet` - [`TreeCharSet`](@ref) to use to print branches. @@ -191,8 +195,38 @@ print_child_key(io::IO, key::CartesianIndex) = show(io, Tuple(key)) branchwidth(cs::TreeCharSet) = sum(textwidth.((cs.mid, cs.dash))) + +""" + nodevalue_distance(v1, v2; kw...)::Real + +Compute the distance between the values of two child nodes. By default, equivalent to isequal. +It must return a Real + +**OPTIONAL**: This can be extended for custom types and controls how nodes are shown +in [`print_tree`](@ref). +""" +nodevalue_distance(v1, v2) = ifelse(isequal(v1, v2), zero(Float64), Inf) + +function print_and_child_prefix(io::IO, prefix::AbstractString, is_last::Bool, charset::TreeCharSet) + print(io, prefix) + + if is_last + print(io, charset.terminator) + child_prefix = prefix * " " ^ (textwidth(charset.skip) + textwidth(charset.dash) + 1) + else + print(io, charset.mid) + child_prefix = prefix * charset.skip * " " ^ (textwidth(charset.dash) + 1) + end + + print(io, charset.dash, ' ') + + return child_prefix +end + function print_tree(printnode::Function, print_child_key::Function, io::IO, node; maxdepth::Integer=5, + maxsibling::Union{Nothing,Integer}=nothing, + sibling_similarity_threshold::Real=0, indicate_truncation::Bool=true, charset::TreeCharSet=TreeCharSet(), printkeys::Union{Bool,Nothing}=nothing, @@ -200,6 +234,7 @@ function print_tree(printnode::Function, print_child_key::Function, io::IO, node prefix::AbstractString="", printnode_kw=(;), ) + # Get node representation as string buf = IOBuffer() printnode(IOContext(buf, io), node; printnode_kw...) @@ -233,9 +268,13 @@ function print_tree(printnode::Function, print_child_key::Function, io::IO, node # Print children s = Iterators.Stateful(this_printkeys ? pairs(c) : c) - while !isempty(s) - child_prefix = prefix + use_sibling_elision = !isnothing(maxsibling) + prev_child_value = nothing + seen_child = false + similar_run_length = 0 + elided_count = 0 + while !isempty(s) if this_printkeys child_key, child = popfirst!(s) else @@ -243,18 +282,29 @@ function print_tree(printnode::Function, print_child_key::Function, io::IO, node child_key = nothing end - print(io, prefix) + child_value = nodevalue(child) + if use_sibling_elision + if seen_child && nodevalue_distance(prev_child_value, child_value) <= sibling_similarity_threshold + similar_run_length += 1 + else + if elided_count > 0 + print_and_child_prefix(io, prefix, false, charset) + println(io, charset.trunc, " (", elided_count, " siblings elided)") + elided_count = 0 + end + similar_run_length = 1 + end + end - # Last child? - if isempty(s) - print(io, charset.terminator) - child_prefix *= " " ^ (textwidth(charset.skip) + textwidth(charset.dash) + 1) - else - print(io, charset.mid) - child_prefix *= charset.skip * " " ^ (textwidth(charset.dash) + 1) + prev_child_value = child_value + seen_child = true + + if use_sibling_elision && similar_run_length > maxsibling + elided_count += 1 + continue end - print(io, charset.dash, ' ') + child_prefix = print_and_child_prefix(io, prefix, isempty(s) && elided_count == 0, charset) # Print key if this_printkeys @@ -267,10 +317,17 @@ function print_tree(printnode::Function, print_child_key::Function, io::IO, node end print_tree(printnode, print_child_key, io, child; - maxdepth=maxdepth, indicate_truncation=indicate_truncation, charset=charset, - printkeys=printkeys, depth=depth+1, prefix=child_prefix, printnode_kw=printnode_kw, + maxdepth=maxdepth, maxsibling=maxsibling, + sibling_similarity_threshold=sibling_similarity_threshold, + indicate_truncation=indicate_truncation, charset=charset, printkeys=printkeys, + depth=depth+1, prefix=child_prefix, printnode_kw=printnode_kw, ) end + + if elided_count > 0 + print_and_child_prefix(io, prefix, true, charset) + println(io, charset.trunc, " (", elided_count, " siblings elided)") + end end print_tree(printnode::Function, io::IO, node; kw...) = print_tree(printnode, print_child_key, io, node; kw...) diff --git a/test/printing.jl b/test/printing.jl index 3056923..edee6e7 100644 --- a/test/printing.jl +++ b/test/printing.jl @@ -94,6 +94,39 @@ AbstractTrees.printnode(io::IO, u::UnindexableChildren) = AbstractTrees.printnod @test numlines == 4 # 1 (head node) + 3 (depth) end +@testset "Sibling elision" begin + truncation_str = TreeCharSet().trunc + + # Start eliding only after the first N similar siblings. + ptxt = repr_tree([1, 1, 1, 2, 2, 2, 3], maxsibling=2) + @test endswith(ptxt, """ + ├─ 1 + ├─ 1 + ├─ $(truncation_str) (1 siblings elided) + ├─ 2 + ├─ 2 + ├─ $(truncation_str) (1 siblings elided) + └─ 3 + """) + + # If an elided run ends the sibling list, the summary line is the terminator. + ptxt = repr_tree((1, 1, 1), maxsibling=2, printkeys=true) + @test endswith(ptxt, """ + ├─ 1 ⇒ 1 + ├─ 2 ⇒ 1 + └─ $(truncation_str) (1 siblings elided) + """) + + tree = UnindexableChildren([1, 1, 1, 2]) + @test repr_tree(tree, maxsibling=2) == repr_tree(tree.node, maxsibling=2) + + # Elided siblings are not recursed into, so they do not contribute maxdepth truncation lines. + ptxt = repr_tree([[1, 2], [1, 2], [1, 2], [3, 4]], maxdepth=1, maxsibling=2) + lines = [strip(line) for line in split(ptxt, '\n') if !isempty(strip(line))] + @test count(==(truncation_str), lines) == 3 + @test any(line -> occursin(" (1 siblings elided)", line), lines) +end + @testset "Child keys" begin @testset "AbstractVector" begin From e263244104b7fffe638789f8614efb4feb6b03f1 Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 14 Mar 2026 18:54:08 +0100 Subject: [PATCH 2/3] fix tests --- test/printing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/printing.jl b/test/printing.jl index edee6e7..eb6ea25 100644 --- a/test/printing.jl +++ b/test/printing.jl @@ -123,7 +123,7 @@ end # Elided siblings are not recursed into, so they do not contribute maxdepth truncation lines. ptxt = repr_tree([[1, 2], [1, 2], [1, 2], [3, 4]], maxdepth=1, maxsibling=2) lines = [strip(line) for line in split(ptxt, '\n') if !isempty(strip(line))] - @test count(==(truncation_str), lines) == 3 + @test count(line -> endswith(line, truncation_str), lines) == 3 @test any(line -> occursin(" (1 siblings elided)", line), lines) end From f06f3f2bbc168882fcd72f98f574bad2bec7f0e3 Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 14 Mar 2026 18:58:41 +0100 Subject: [PATCH 3/3] add nodevalue_distance to docs block --- docs/src/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/index.md b/docs/src/index.md index a0b4855..b73d2b0 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -188,6 +188,7 @@ descendleft getroot print_tree printnode +nodevalue_distance print_child_key TreeCharSet shouldprintkeys