diff --git a/base/strings/annotated.jl b/base/strings/annotated.jl index a85cdf1b08bbb..47a7bdbb36946 100644 --- a/base/strings/annotated.jl +++ b/base/strings/annotated.jl @@ -108,8 +108,18 @@ AnnotatedChar(c::AbstractChar, annots::Vector{<:Pair{Symbol, <:Any}}) = # Constructors to avoid recursive wrapping -AnnotatedString(s::AnnotatedString, annots::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}) = - AnnotatedString(s.string, vcat(s.annotations, annots)) +function AnnotatedString(s::AnnotatedString, annots::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}) + if isempty(s.annotations) + AnnotatedString{typeof(s)}(s, annots) # Add type to avoid infinite recursion + else # Apply `annots` with a lower priority than existing annotations. + snew = deepcopy(s) + for annot in annots + sortedindex = searchsortedfirst(snew.annotations, annot, by=_annot_sortkey) + insert!(snew.annotations, sortedindex, annot) + end + snew + end +end AnnotatedChar(c::AnnotatedChar, annots::Vector{Pair{Symbol, Any}}) = AnnotatedChar(c.char, vcat(c.annotations, annots)) @@ -316,16 +326,27 @@ reverse(s::SubString{<:AnnotatedString}) = reverse(AnnotatedString(s)) ## End AbstractString interface ## +""" + _annot_sortkey(annot::Tuple{UnitRange{Int}, Any}) -> Tuple{Int, Int} +Produce a sortable `Tuple` according to the estimated priority of `annot`. + +We want to maintain a logical, consistent order for annotations. +Bearing in mind that the last annotation affecting a given region "wins", +we will try to prioritise the more "specific" annotations by sorting annotations +that start later and affect a narrower region later. +""" +const _annot_sortkey = (r -> (first(r), -last(r))) ∘ first + function _annotate!(annlist::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}, range::UnitRange{Int}, @nospecialize(labelval::Pair{Symbol, <:Any})) label, val = labelval if val === nothing - indices = searchsorted(annlist, (range,), by=first) + indices = searchsorted(annlist, (range,), by=_annot_sortkey) labelindex = filter(i -> first(annlist[i][2]) === label, indices) for index in Iterators.reverse(labelindex) deleteat!(annlist, index) end else - sortedindex = searchsortedlast(annlist, (range,), by=first) + 1 + sortedindex = searchsortedlast(annlist, (range,), by=_annot_sortkey) + 1 insert!(annlist, sortedindex, (range, Pair{Symbol, Any}(label, val))) end end @@ -475,7 +496,7 @@ function _clear_annotations_in_region!(annotations::Vector{Tuple{UnitRange{Int}, end # Insert any extra entries in the appropriate position for entry in extras - sortedindex = searchsortedlast(annotations, (first(entry),), by=first) + 1 + sortedindex = searchsortedlast(annotations, (first(entry),), by=_annot_sortkey) + 1 insert!(annotations, sortedindex, entry) end end @@ -486,7 +507,7 @@ function _insert_annotations!(io::AnnotatedIOBuffer, annotations::Vector{Tuple{U if !eof(io) for (region, annot) in annotations region = first(region)+offset:last(region)+offset - sortedindex = searchsortedlast(io.annotations, (region,), by=first) + 1 + sortedindex = searchsortedlast(io.annotations, (region,), by=_annot_sortkey) + 1 insert!(io.annotations, sortedindex, (region, annot)) end else diff --git a/test/strings/annotated.jl b/test/strings/annotated.jl index fda583bf7f778..19fb9139597c8 100644 --- a/test/strings/annotated.jl +++ b/test/strings/annotated.jl @@ -22,14 +22,14 @@ # :all @test str[3:4] == SubString(str, 3, 4) @test Base.AnnotatedString(str[3:4]) == - Base.AnnotatedString("me", [(1:2, :thing => 0x01), (1:2, :all => 0x03)]) + Base.AnnotatedString("me", [(1:2, :all => 0x03), (1:2, :thing => 0x01)]) @test Base.AnnotatedString(str[3:6]) == - Base.AnnotatedString("me s", [(1:2, :thing => 0x01), (1:4, :all => 0x03), (4:4, :other => 0x02)]) - @test str == Base.AnnotatedString("some string", [(1:4, :thing => 0x01), (1:11, :all => 0x03), (6:11, :other => 0x02)]) + Base.AnnotatedString("me s", [(1:4, :all => 0x03), (1:2, :thing => 0x01), (4:4, :other => 0x02)]) + @test str == Base.AnnotatedString("some string", [(1:11, :all => 0x03), (1:4, :thing => 0x01), (6:11, :other => 0x02)]) @test str != Base.AnnotatedString("some string") - @test str != Base.AnnotatedString("some string", [(1:1, :thing => 0x01), (6:6, :other => 0x02), (11:11, :all => 0x03)]) - @test str != Base.AnnotatedString("some string", [(1:4, :thing => 0x11), (1:11, :all => 0x13), (6:11, :other => 0x12)]) - @test str != Base.AnnotatedString("some thingg", [(1:4, :thing => 0x01), (1:11, :all => 0x03), (6:11, :other => 0x02)]) + @test str != Base.AnnotatedString("some string", [(1:1, :all => 0x03), (1:4, :thing => 0x01), (6:11, :other => 0x02)]) + @test str != Base.AnnotatedString("some string", [(1:11, :all => 0x43), (1:4, :thing => 0x21), (6:11, :other => 0x32)]) + @test str != Base.AnnotatedString("some thingg", [(1:11, :all => 0x03), (1:4, :thing => 0x01), (6:11, :other => 0x02)]) @test Base.AnnotatedString([Base.AnnotatedChar('a', [:a => 1]), Base.AnnotatedChar('b', [:b => 2])]) == Base.AnnotatedString("ab", [(1:1, :a => 1), (2:2, :b => 2)]) let allstrings =