• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #38039

31 Mar 2025 08:00AM UTC coverage: 20.268% (-0.02%) from 20.292%
#38039

push

local

web-flow
`_precompilepkgs`: interactive progress display: fix unintended capture (#57932)

The variable `str` also exists in one of the enclosing closures. Use a
new variable, as was surely intended, instead of capturing and mutating
the `str`.

Improves the sysimage's resistance to invalidation.

9938 of 49034 relevant lines covered (20.27%)

98941.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/base/strings/annotated.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
const Annotation = NamedTuple{(:label, :value), Tuple{Symbol, Any}}
4
const RegionAnnotation = NamedTuple{(:region, :label, :value), Tuple{UnitRange{Int}, Symbol, Any}}
5

6
"""
7
    AnnotatedString{S <: AbstractString} <: AbstractString
8

9
A string with metadata, in the form of annotated regions.
10

11
More specifically, this is a simple wrapper around any other
12
[`AbstractString`](@ref) that allows for regions of the wrapped string to be
13
annotated with labeled values.
14

15
```text
16
                           C
17
                    ┌──────┸─────────┐
18
  "this is an example annotated string"
19
  └──┰────────┼─────┘         │
20
     A        └─────┰─────────┘
21
                    B
22
```
23

24
The above diagram represents a `AnnotatedString` where three ranges have been
25
annotated (labeled `A`, `B`, and `C`). Each annotation holds a label (`Symbol`)
26
and a value (`Any`). These three pieces of information are held as a
27
`$RegionAnnotation`.
28

29
Labels do not need to be unique, the same region can hold multiple annotations
30
with the same label.
31

32
Code written for `AnnotatedString`s in general should conserve the following
33
properties:
34
- Which characters an annotation is applied to
35
- The order in which annotations are applied to each character
36

37
Additional semantics may be introduced by specific uses of `AnnotatedString`s.
38

39
A corollary of these rules is that adjacent, consecutively placed, annotations
40
with identical labels and values are equivalent to a single annotation spanning
41
the combined range.
42

43
See also [`AnnotatedChar`](@ref), [`annotatedstring`](@ref),
44
[`annotations`](@ref), and [`annotate!`](@ref).
45

46
# Constructors
47

48
```julia
49
AnnotatedString(s::S<:AbstractString) -> AnnotatedString{S}
50
AnnotatedString(s::S<:AbstractString, annotations::Vector{$RegionAnnotation})
51
```
52

53
A AnnotatedString can also be created with [`annotatedstring`](@ref), which acts much
54
like [`string`](@ref) but preserves any annotations present in the arguments.
55

56
# Examples
57

58
```jldoctest; setup=:(using Base: AnnotatedString)
59
julia> AnnotatedString("this is an example annotated string",
60
                    [(1:18, :A, 1), (12:28, :B, 2), (18:35, :C, 3)])
61
"this is an example annotated string"
62
```
63
"""
64
struct AnnotatedString{S <: AbstractString} <: AbstractString
65
    string::S
×
66
    annotations::Vector{RegionAnnotation}
67
end
68

69
"""
70
    AnnotatedChar{S <: AbstractChar} <: AbstractChar
71

72
A Char with annotations.
73

74
More specifically, this is a simple wrapper around any other
75
[`AbstractChar`](@ref), which holds a list of arbitrary labelled annotations
76
(`$Annotation`) with the wrapped character.
77

78
See also: [`AnnotatedString`](@ref), [`annotatedstring`](@ref), `annotations`,
79
and `annotate!`.
80

81
# Constructors
82

83
```julia
84
AnnotatedChar(s::S) -> AnnotatedChar{S}
85
AnnotatedChar(s::S, annotations::Vector{$Annotation})
86
```
87

88
# Examples
89

90
```jldoctest; setup=:(using Base: AnnotatedChar)
91
julia> AnnotatedChar('j', [(:label, 1)])
92
'j': ASCII/Unicode U+006A (category Ll: Letter, lowercase)
93
```
94
"""
95
struct AnnotatedChar{C <: AbstractChar} <: AbstractChar
96
    char::C
97
    annotations::Vector{Annotation}
98
end
99

100
## Constructors ##
101

102
# When called with overly-specialised arguments
103

104
AnnotatedString(s::AbstractString, annots::Vector) =
×
105
    AnnotatedString(s, Vector{RegionAnnotation}(annots))
106

107
AnnotatedString(s::AbstractString, annots) =
×
108
    AnnotatedString(s, collect(RegionAnnotation, annots))
109

110
AnnotatedChar(c::AbstractChar, annots::Vector) =
×
111
    AnnotatedChar(c, Vector{Annotation}(annots))
112

113
AnnotatedChar(c::AbstractChar, annots) =
×
114
    AnnotatedChar(c, collect(Annotation, annots))
115

116
# Constructors to avoid recursive wrapping
117

118
AnnotatedString(s::AnnotatedString, annots::Vector{RegionAnnotation}) =
×
119
    AnnotatedString(s.string, vcat(s.annotations, annots))
120

121
AnnotatedChar(c::AnnotatedChar, annots::Vector{Annotation}) =
×
122
    AnnotatedChar(c.char, vcat(c.annotations, Vector{Annotation}(annots)))
123

124
# To avoid pointless overhead
125
String(s::AnnotatedString{String}) = s.string
×
126

127
## Conversion/promotion ##
128

129
convert(::Type{AnnotatedString}, s::AnnotatedString) = s
×
130
convert(::Type{AnnotatedString{S}}, s::S) where {S <: AbstractString} =
×
131
    AnnotatedString(s, Vector{RegionAnnotation}())
132
convert(::Type{AnnotatedString}, s::S) where {S <: AbstractString} =
×
133
    convert(AnnotatedString{S}, s)
134
AnnotatedString(s::S) where {S <: AbstractString} = convert(AnnotatedString{S}, s)
×
135

136
convert(::Type{AnnotatedChar}, c::AnnotatedChar) = c
×
137
convert(::Type{AnnotatedChar{C}}, c::C) where { C <: AbstractChar } =
×
138
    AnnotatedChar{C}(c, Vector{Annotation}())
139
convert(::Type{AnnotatedChar}, c::C) where { C <: AbstractChar } =
×
140
    convert(AnnotatedChar{C}, c)
141

142
AnnotatedChar(c::AbstractChar) = convert(AnnotatedChar, c)
×
143
AnnotatedChar(c::UInt32) = convert(AnnotatedChar, Char(c))
×
144
AnnotatedChar{C}(c::UInt32) where {C <: AbstractChar} = convert(AnnotatedChar, C(c))
×
145

146
promote_rule(::Type{<:AnnotatedString}, ::Type{<:AbstractString}) = AnnotatedString
×
147

148
## AbstractString interface ##
149

150
ncodeunits(s::AnnotatedString) = ncodeunits(s.string)::Int
×
151
codeunits(s::AnnotatedString) = codeunits(s.string)
×
152
codeunit(s::AnnotatedString) = codeunit(s.string)
×
153
codeunit(s::AnnotatedString, i::Integer) = codeunit(s.string, i)
×
154
isvalid(s::AnnotatedString, i::Integer) = isvalid(s.string, i)::Bool
×
155
@propagate_inbounds iterate(s::AnnotatedString, i::Integer=firstindex(s)) =
×
156
    if i <= lastindex(s.string); (s[i], nextind(s, i)) end
×
157
eltype(::Type{<:AnnotatedString{S}}) where {S} = AnnotatedChar{eltype(S)}
×
158
firstindex(s::AnnotatedString) = firstindex(s.string)
×
159
lastindex(s::AnnotatedString) = lastindex(s.string)
×
160

161
function getindex(s::AnnotatedString, i::Integer)
×
162
    @boundscheck checkbounds(s, i)
×
163
    @inbounds if isvalid(s, i)
×
164
        AnnotatedChar(s.string[i], Annotation[(; label, value) for (; label, value) in annotations(s, i)])
×
165
    else
166
        string_index_err(s, i)
×
167
    end
168
end
169

170
# To make `AnnotatedString`s repr-evaluable, we need to override
171
# the generic `AbstractString` 2-arg show method.
172

173
function show(io::IO, s::A) where {A <: AnnotatedString}
×
174
    show(io, A)
×
175
    print(io, '(')
×
176
    show(io, s.string)
×
177
    print(io, ", ")
×
178
    tupanns = Vector{Tuple{UnitRange{Int}, Symbol, Any}}(map(values, s.annotations))
×
179
    show(IOContext(io, :typeinfo => typeof(tupanns)), tupanns)
×
180
    print(io, ')')
×
181
end
182

183
# But still use the generic `AbstractString` fallback for the 3-arg show.
184
show(io::IO, ::MIME"text/plain", s::AnnotatedString) =
×
185
    invoke(show, Tuple{IO, AbstractString}, io, s)
186

187
## AbstractChar interface ##
188

189
ncodeunits(c::AnnotatedChar) = ncodeunits(c.char)
×
190
codepoint(c::AnnotatedChar) = codepoint(c.char)
×
191

192
# Avoid the iteration fallback with comparison
193
cmp(a::AnnotatedString, b::AbstractString) = cmp(a.string, b)
×
194
cmp(a::AbstractString, b::AnnotatedString) = cmp(a, b.string)
×
195
# To avoid method ambiguity
196
cmp(a::AnnotatedString, b::AnnotatedString) = cmp(a.string, b.string)
×
197

198
==(a::AnnotatedString, b::AnnotatedString) =
×
199
    a.string == b.string && a.annotations == b.annotations
200

201
==(a::AnnotatedString, b::AbstractString) = isempty(a.annotations) && a.string == b
×
202
==(a::AbstractString, b::AnnotatedString) = isempty(b.annotations) && a == b.string
×
203

204
# To prevent substring equality from hitting the generic fallback
205

206
function ==(a::SubString{<:AnnotatedString}, b::SubString{<:AnnotatedString})
×
207
    SubString(a.string.string, a.offset, a.ncodeunits, Val(:noshift)) ==
×
208
        SubString(b.string.string, b.offset, b.ncodeunits, Val(:noshift)) &&
209
        annotations(a) == annotations(b)
210
end
211

212
==(a::SubString{<:AnnotatedString}, b::AnnotatedString) =
×
213
    annotations(a) == annotations(b) && SubString(a.string.string, a.offset, a.ncodeunits, Val(:noshift)) == b.string
214

215
==(a::SubString{<:AnnotatedString}, b::AbstractString) =
×
216
    isempty(annotations(a)) && SubString(a.string.string, a.offset, a.ncodeunits, Val(:noshift)) == b
217

218
==(a::AbstractString, b::SubString{<:AnnotatedString}) = b == a
×
219

220
==(a::AnnotatedString, b::SubString{<:AnnotatedString}) = b == a
×
221

222
"""
223
    annotatedstring(values...)
224

225
Create a `AnnotatedString` from any number of `values` using their
226
[`print`](@ref)ed representation.
227

228
This acts like [`string`](@ref), but takes care to preserve any annotations
229
present (in the form of [`AnnotatedString`](@ref) or [`AnnotatedChar`](@ref) values).
230

231
See also [`AnnotatedString`](@ref) and [`AnnotatedChar`](@ref).
232

233
## Examples
234

235
```jldoctest; setup=:(using Base: AnnotatedString, annotatedstring)
236
julia> annotatedstring("now a AnnotatedString")
237
"now a AnnotatedString"
238

239
julia> annotatedstring(AnnotatedString("annotated", [(1:9, :label, 1)]), ", and unannotated")
240
"annotated, and unannotated"
241
```
242
"""
243
function annotatedstring(xs...)
×
244
    isempty(xs) && return AnnotatedString("")
×
245
    size = mapreduce(_str_sizehint, +, xs)
×
246
    buf = IOBuffer(sizehint=size)
×
247
    s = IOContext(buf, :color => true)
×
248
    annotations = Vector{RegionAnnotation}()
×
249
    for x in xs
×
250
        size = filesize(s.io)
×
251
        if x isa AnnotatedString
×
252
            for annot in x.annotations
×
253
                push!(annotations, setindex(annot, annot.region .+ size, :region))
×
254
            end
×
255
            print(s, x.string)
×
256
        elseif x isa SubString{<:AnnotatedString}
×
257
            for annot in x.string.annotations
×
258
                start, stop = first(annot.region), last(annot.region)
×
259
                if start <= x.offset + x.ncodeunits && stop > x.offset
×
260
                    rstart = size + max(0, start - x.offset - 1) + 1
×
261
                    rstop = size + min(stop, x.offset + x.ncodeunits) - x.offset
×
262
                    push!(annotations, setindex(annot, rstart:rstop, :region))
×
263
                end
264
            end
×
265
            print(s, SubString(x.string.string, x.offset, x.ncodeunits, Val(:noshift)))
×
266
        elseif x isa AnnotatedChar
×
267
            for annot in x.annotations
×
268
                push!(annotations, (region=1+size:1+size, annot...))
×
269
            end
×
270
            print(s, x.char)
×
271
        else
272
            print(s, x)
×
273
        end
274
    end
×
275
    str = String(take!(buf))
×
276
    AnnotatedString(str, annotations)
×
277
end
278

279
annotatedstring(s::AnnotatedString) = s
×
280
annotatedstring(c::AnnotatedChar) =
×
281
    AnnotatedString(string(c.char), [(region=1:ncodeunits(c), annot...) for annot in c.annotations])
282

283
AnnotatedString(s::SubString{<:AnnotatedString}) = annotatedstring(s)
×
284

285
function repeat(str::AnnotatedString, r::Integer)
×
286
    r == 0 && return one(AnnotatedString)
×
287
    r == 1 && return str
×
288
    unannot = repeat(str.string, r)
×
289
    annotations = Vector{RegionAnnotation}()
×
290
    len = ncodeunits(str)
×
291
    fullregion = firstindex(str):lastindex(str)
×
292
    if isempty(str.annotations)
×
293
    elseif allequal(a -> a.region, str.annotations) && first(str.annotations).region == fullregion
×
294
        newfullregion = firstindex(unannot):lastindex(unannot)
×
295
        for annot in str.annotations
×
296
            push!(annotations, setindex(annot, newfullregion, :region))
×
297
        end
×
298
    else
299
        for offset in 0:len:(r-1)*len
×
300
            for annot in str.annotations
×
301
                push!(annotations, setindex(annot, annot.region .+ offset, :region))
×
302
            end
×
303
        end
×
304
    end
305
    AnnotatedString(unannot, annotations)
×
306
end
307

308
repeat(str::SubString{<:AnnotatedString}, r::Integer) =
×
309
    repeat(AnnotatedString(str), r)
310

311
function repeat(c::AnnotatedChar, r::Integer)
×
312
    str = repeat(c.char, r)
×
313
    fullregion = firstindex(str):lastindex(str)
×
314
    AnnotatedString(str, [(region=fullregion, annot...) for annot in c.annotations])
×
315
end
316

317
function reverse(s::AnnotatedString)
×
318
    lastind = lastindex(s)
×
319
    AnnotatedString(
×
320
        reverse(s.string),
321
        [setindex(annot,
322
                  UnitRange(1 + lastind - last(annot.region),
323
                            1 + lastind - first(annot.region)),
324
                  :region)
325
         for annot in s.annotations])
326
end
327

328
# TODO optimise?
329
reverse(s::SubString{<:AnnotatedString}) = reverse(AnnotatedString(s))
×
330

331
# TODO implement `replace(::AnnotatedString, ...)`
332

333
## End AbstractString interface ##
334

335
function _annotate!(annlist::Vector{RegionAnnotation}, region::UnitRange{Int}, label::Symbol, @nospecialize(value::Any))
336
    if value === nothing
×
337
        deleteat!(annlist, findall(ann -> ann.region == region && ann.label === label, annlist))
×
338
    else
339
        push!(annlist, RegionAnnotation((; region, label, value)))
×
340
    end
341
end
342

343
"""
344
    annotate!(str::AnnotatedString, [range::UnitRange{Int}], label::Symbol, value)
345
    annotate!(str::SubString{AnnotatedString}, [range::UnitRange{Int}], label::Symbol, value)
346

347
Annotate a `range` of `str` (or the entire string) with a labeled value `(label, value)`.
348
To remove existing `label` annotations, use a value of `nothing`.
349

350
The order in which annotations are applied to `str` is semantically meaningful,
351
as described in [`AnnotatedString`](@ref).
352
"""
353
annotate!(s::AnnotatedString, range::UnitRange{Int}, label::Symbol, @nospecialize(val::Any)) =
×
354
    (_annotate!(s.annotations, range, label, val); s)
×
355

356
annotate!(ss::AnnotatedString, label::Symbol, @nospecialize(val::Any)) =
×
357
    annotate!(ss, firstindex(ss):lastindex(ss), label, val)
358

359
annotate!(s::SubString{<:AnnotatedString}, range::UnitRange{Int}, label::Symbol, @nospecialize(val::Any)) =
×
360
    (annotate!(s.string, s.offset .+ (range), label, val); s)
×
361

362
annotate!(s::SubString{<:AnnotatedString}, label::Symbol, @nospecialize(val::Any)) =
×
363
    (annotate!(s.string, s.offset .+ (1:s.ncodeunits), label, val); s)
×
364

365
"""
366
    annotate!(char::AnnotatedChar, label::Symbol, value::Any)
367

368
Annotate `char` with the labeled value `(label, value)`.
369
"""
370
annotate!(c::AnnotatedChar, label::Symbol, @nospecialize(val::Any)) =
×
371
    (push!(c.annotations, Annotation((; label, val))); c)
×
372

373
"""
374
    annotations(str::Union{AnnotatedString, SubString{AnnotatedString}},
375
                [position::Union{Integer, UnitRange}]) ->
376
        Vector{$RegionAnnotation}
377

378
Get all annotations that apply to `str`. Should `position` be provided, only
379
annotations that overlap with `position` will be returned.
380

381
Annotations are provided together with the regions they apply to, in the form of
382
a vector of region–annotation tuples.
383

384
In accordance with the semantics documented in [`AnnotatedString`](@ref), the
385
order of annotations returned matches the order in which they were applied.
386

387
See also: [`annotate!`](@ref).
388
"""
389
annotations(s::AnnotatedString) = s.annotations
×
390

391
function annotations(s::SubString{<:AnnotatedString})
×
392
    RegionAnnotation[
×
393
        setindex(ann, first(ann.region)-s.offset:last(ann.region)-s.offset, :region)
394
        for ann in annotations(s.string, s.offset+1:s.offset+s.ncodeunits)]
395
end
396

397
function annotations(s::AnnotatedString, pos::UnitRange{<:Integer})
×
398
    # TODO optimise
399
    RegionAnnotation[
×
400
        setindex(ann, max(first(pos), first(ann.region)):min(last(pos), last(ann.region)), :region)
401
        for ann in s.annotations if !isempty(intersect(pos, ann.region))]
402
end
403

404
annotations(s::AnnotatedString, pos::Integer) = annotations(s, pos:pos)
×
405

406
annotations(s::SubString{<:AnnotatedString}, pos::Integer) =
×
407
    annotations(s.string, s.offset + pos)
408

409
annotations(s::SubString{<:AnnotatedString}, pos::UnitRange{<:Integer}) =
×
410
    annotations(s.string, first(pos)+s.offset:last(pos)+s.offset)
411

412
"""
413
    annotations(chr::AnnotatedChar)::Vector{$Annotation}
414

415
Get all annotations of `chr`, in the form of a vector of annotation pairs.
416
"""
417
annotations(c::AnnotatedChar) = c.annotations
×
418

419
## Character transformation helper function, c.f. `unicode.jl`.
420

421
"""
422
    annotated_chartransform(f::Function, str::AnnotatedString, state=nothing)
423

424
Transform every character in `str` with `f`, adjusting annotation regions as
425
appropriate. `f` must take one of two forms, either:
426
- `f(c::Char) -> Char`, or
427
- `f(c::Char, state) -> (Char, state)`.
428

429
This works by comparing the number of code units of each character before and
430
after transforming with `f`, recording and aggregating any differences, then
431
applying them to the annotation regions.
432

433
Returns an `AnnotatedString{String}` (regardless of the original underling
434
string type of `str`).
435
"""
436
function annotated_chartransform(f::Function, str::AnnotatedString, state=nothing)
×
437
    outstr = IOBuffer()
×
438
    annots = RegionAnnotation[]
×
439
    bytepos = firstindex(str) - 1
×
440
    offsets = [bytepos => 0]
×
441
    for c in str.string
×
442
        oldnb = ncodeunits(c)
×
443
        bytepos += oldnb
×
444
        if isnothing(state)
×
445
            c = f(c)
×
446
        else
447
            c, state = f(c, state)
×
448
        end
449
        nb = write(outstr, c)
×
450
        if nb != oldnb
×
451
            push!(offsets, bytepos => last(last(offsets)) + nb - oldnb)
×
452
        end
453
    end
×
454
    for annot in str.annotations
×
455
        start, stop = first(annot.region), last(annot.region)
×
456
        start_offset = last(offsets[findlast(<=(start) ∘ first, offsets)::Int])
×
457
        stop_offset  = last(offsets[findlast(<=(stop) ∘ first, offsets)::Int])
×
458
        push!(annots, setindex(annot, (start + start_offset):(stop + stop_offset), :region))
×
459
    end
×
460
    AnnotatedString(String(take!(outstr)), annots)
×
461
end
462

463
struct RegionIterator{S <: AbstractString}
464
    str::S
465
    regions::Vector{UnitRange{Int}}
466
    annotations::Vector{Vector{Annotation}}
467
end
468

469
Base.length(si::RegionIterator) = length(si.regions)
×
470

471
Base.@propagate_inbounds function Base.iterate(si::RegionIterator, i::Integer=1)
×
472
    if i <= length(si.regions)
×
473
        @inbounds ((SubString(si.str, si.regions[i]), si.annotations[i]), i+1)
×
474
    end
475
end
476

477
Base.eltype(::RegionIterator{S}) where { S <: AbstractString} =
×
478
    Tuple{SubString{S}, Vector{Annotation}}
479

480
"""
481
    eachregion(s::AnnotatedString{S})
482
    eachregion(s::SubString{AnnotatedString{S}})
483

484
Identify the contiguous substrings of `s` with a constant annotations, and return
485
an iterator which provides each substring and the applicable annotations as a
486
`Tuple{SubString{S}, Vector{$Annotation}}`.
487

488
# Examples
489

490
```jldoctest; setup=:(using Base: AnnotatedString, eachregion)
491
julia> collect(eachregion(AnnotatedString(
492
           "hey there", [(1:3, :face, :bold),
493
                         (5:9, :face, :italic)])))
494
3-element Vector{Tuple{SubString{String}, Vector{$Annotation}}}:
495
 ("hey", [$Annotation((:face, :bold))])
496
 (" ", [])
497
 ("there", [$Annotation((:face, :italic))])
498
```
499
"""
500
function eachregion(s::AnnotatedString, subregion::UnitRange{Int}=firstindex(s):lastindex(s))
×
501
    isempty(s) || isempty(subregion) &&
×
502
        return RegionIterator(s.string, UnitRange{Int}[], Vector{Annotation}[])
503
    events = annotation_events(s, subregion)
×
504
    isempty(events) && return RegionIterator(s.string, [subregion], [Annotation[]])
×
505
    annotvals = Annotation[
×
506
        (; label, value) for (; label, value) in annotations(s)]
507
    regions = Vector{UnitRange{Int}}()
×
508
    annots = Vector{Vector{Annotation}}()
×
509
    pos = first(events).pos
×
510
    if pos > first(subregion)
×
511
        push!(regions, thisind(s, first(subregion)):prevind(s, pos))
×
512
        push!(annots, [])
×
513
    end
514
    activelist = Int[]
×
515
    for event in events
×
516
        if event.pos != pos
×
517
            push!(regions, pos:prevind(s, event.pos))
×
518
            push!(annots, annotvals[activelist])
×
519
            pos = event.pos
×
520
        end
521
        if event.active
×
522
            insert!(activelist, searchsortedfirst(activelist, event.index), event.index)
×
523
        else
524
            deleteat!(activelist, searchsortedfirst(activelist, event.index))
×
525
        end
526
    end
×
527
    if last(events).pos < nextind(s, last(subregion))
×
528
        push!(regions, last(events).pos:thisind(s, last(subregion)))
×
529
        push!(annots, [])
×
530
    end
531
    RegionIterator(s.string, regions, annots)
×
532
end
533

534
function eachregion(s::SubString{<:AnnotatedString}, pos::UnitRange{Int}=firstindex(s):lastindex(s))
×
535
    if isempty(s)
×
536
        RegionIterator(s.string, Vector{UnitRange{Int}}(), Vector{Vector{Annotation}}())
×
537
    else
538
        eachregion(s.string, first(pos)+s.offset:last(pos)+s.offset)
×
539
    end
540
end
541

542
"""
543
    annotation_events(string::AbstractString, annots::Vector{$RegionAnnotation}, subregion::UnitRange{Int})
544
    annotation_events(string::AnnotatedString, subregion::UnitRange{Int})
545

546
Find all annotation "change events" that occur within a `subregion` of `annots`,
547
with respect to `string`. When `string` is styled, `annots` is inferred.
548

549
Each change event is given in the form of a `@NamedTuple{pos::Int, active::Bool,
550
index::Int}` where `pos` is the position of the event, `active` is a boolean
551
indicating whether the annotation is being activated or deactivated, and `index`
552
is the index of the annotation in question.
553
"""
554
function annotation_events(s::AbstractString, annots::Vector{RegionAnnotation}, subregion::UnitRange{Int})
×
555
    events = Vector{NamedTuple{(:pos, :active, :index), Tuple{Int, Bool, Int}}}() # Position, Active?, Annotation index
×
556
    for (i, (; region)) in enumerate(annots)
×
557
        if !isempty(intersect(subregion, region))
×
558
            start, stop = max(first(subregion), first(region)), min(last(subregion), last(region))
×
559
            start <= stop || continue # Currently can't handle empty regions
×
560
            push!(events, (pos=thisind(s, start), active=true, index=i))
×
561
            push!(events, (pos=nextind(s, stop), active=false, index=i))
×
562
        end
563
    end
×
564
    sort(events, by=e -> e.pos)
×
565
end
566

567
annotation_events(s::AnnotatedString, subregion::UnitRange{Int}) =
×
568
    annotation_events(s.string, annotations(s), subregion)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc