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

JuliaLang / julia / #37851

27 Jul 2024 03:03AM UTC coverage: 86.618% (-1.0%) from 87.596%
#37851

push

local

web-flow
Make `jl_*affinity` tests more portable (#55261)

Changes made:
- Use 0 for the thread ID to ensure it's always valid. The function
expects `0 <= tid < jl_n_threads` so 1 is incorrect if `jl_n_threads` is
1.
- After retrieving the affinity mask with `jl_getaffinity`, pass that
same mask back to `jl_setaffinity`. This ensures that the mask is always
valid. Using a mask of all ones results in `EINVAL` on FreeBSD. Based on
the discussion in #53402, this change may also fix Windows, so I've
tried reenabling it here.
- To check whether `jl_getaffinity` actually did something, we can check
that the mask is no longer all zeros after the call.

Fixes #54817

76775 of 88636 relevant lines covered (86.62%)

15433139.0 hits per line

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

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

3
"""
4
    AnnotatedString{S <: AbstractString} <: AbstractString
5

6
A string with metadata, in the form of annotated regions.
7

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

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

21
The above diagram represents a `AnnotatedString` where three ranges have been
22
annotated (labeled `A`, `B`, and `C`). Each annotation holds a label (`Symbol`)
23
and a value (`Any`), paired together as a `Pair{Symbol, <:Any}`.
24

25
Labels do not need to be unique, the same region can hold multiple annotations
26
with the same label.
27

28
Code written for `AnnotatedString`s in general should conserve the following
29
properties:
30
- Which characters an annotation is applied to
31
- The order in which annotations are applied to each character
32

33
Additional semantics may be introduced by specific uses of `AnnotatedString`s.
34

35
A corollary of these rules is that adjacent, consecutively placed, annotations
36
with identical labels and values are equivalent to a single annotation spanning
37
the combined range.
38

39
See also [`AnnotatedChar`](@ref), [`annotatedstring`](@ref),
40
[`annotations`](@ref), and [`annotate!`](@ref).
41

42
!!! warning
43
    While the constructors are part of the Base public API, the fields
44
    of `AnnotatedString` are not. This is to allow for potential future
45
    changes in the implementation of this type. Instead use the
46
    [`annotations`](@ref), and [`annotate!`](@ref) getter/setter
47
    functions.
48

49
# Constructors
50

51
```julia
52
AnnotatedString(s::S<:AbstractString) -> AnnotatedString{S}
53
AnnotatedString(s::S<:AbstractString, annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, <:Any}}})
54
```
55

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

59
# Examples
60

61
```julia-repl
62
julia> AnnotatedString("this is an example annotated string",
63
                    [(1:18, :A => 1), (12:28, :B => 2), (18:35, :C => 3)])
64
"this is an example annotated string"
65
```
66
"""
67
struct AnnotatedString{S <: AbstractString} <: AbstractString
68
    string::S
1,434✔
69
    annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}
70
end
71

72
"""
73
    AnnotatedChar{S <: AbstractChar} <: AbstractChar
74

75
A Char with annotations.
76

77
More specifically, this is a simple wrapper around any other
78
[`AbstractChar`](@ref), which holds a list of arbitrary labeled annotations
79
(`Pair{Symbol, <:Any}`) with the wrapped character.
80

81
See also: [`AnnotatedString`](@ref), [`annotatedstring`](@ref), `annotations`,
82
and `annotate!`.
83

84
!!! warning
85
    While the constructors are part of the Base public API, the fields
86
    of `AnnotatedChar` are not. This it to allow for potential future
87
    changes in the implementation of this type. Instead use the
88
    [`annotations`](@ref), and [`annotate!`](@ref) getter/setter
89
    functions.
90

91
# Constructors
92

93
```julia
94
AnnotatedChar(s::S) -> AnnotatedChar{S}
95
AnnotatedChar(s::S, annotations::Vector{Pair{Symbol, <:Any}})
96
```
97

98
# Examples
99

100
```julia-repl
101
julia> AnnotatedChar('j', :label => 1)
102
'j': ASCII/Unicode U+006A (category Ll: Letter, lowercase)
103
```
104
"""
105
struct AnnotatedChar{C <: AbstractChar} <: AbstractChar
106
    char::C
1,984✔
107
    annotations::Vector{Pair{Symbol, Any}}
108
end
109

110
## Constructors ##
111

112
# When called with overly-specialised arguments
113

114
AnnotatedString(s::AbstractString, annots::Vector{<:Tuple{UnitRange{Int}, <:Pair{Symbol, <:Any}}}) =
181✔
115
    AnnotatedString(s, Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}(annots))
116

117
AnnotatedChar(c::AbstractChar, annots::Vector{<:Pair{Symbol, <:Any}}) =
15✔
118
    AnnotatedChar(c, Vector{Pair{Symbol, Any}}(annots))
119

120
# Constructors to avoid recursive wrapping
121

122
AnnotatedString(s::AnnotatedString, annots::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}) =
110✔
123
    AnnotatedString(s.string, vcat(s.annotations, annots))
124

125
AnnotatedChar(c::AnnotatedChar, annots::Vector{Pair{Symbol, Any}}) =
×
126
    AnnotatedChar(c.char, vcat(c.annotations, annots))
127

128
String(s::AnnotatedString{String}) = s.string # To avoid pointless overhead
402✔
129

130
## Conversion/promotion ##
131

132
convert(::Type{AnnotatedString}, s::AnnotatedString) = s
×
133
convert(::Type{AnnotatedString{S}}, s::S) where {S <: AbstractString} =
123✔
134
    AnnotatedString(s, Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}())
135
convert(::Type{AnnotatedString}, s::S) where {S <: AbstractString} =
×
136
    convert(AnnotatedString{S}, s)
137
AnnotatedString(s::S) where {S <: AbstractString} = convert(AnnotatedString{S}, s)
123✔
138

139
convert(::Type{AnnotatedChar}, c::AnnotatedChar) = c
×
140
convert(::Type{AnnotatedChar{C}}, c::C) where { C <: AbstractChar } =
4✔
141
    AnnotatedChar{C}(c, Vector{Pair{Symbol, Any}}())
142
convert(::Type{AnnotatedChar}, c::C) where { C <: AbstractChar } =
4✔
143
    convert(AnnotatedChar{C}, c)
144

145
AnnotatedChar(c::AbstractChar) = convert(AnnotatedChar, c)
2✔
146
AnnotatedChar(c::UInt32) = convert(AnnotatedChar, Char(c))
×
147
AnnotatedChar{C}(c::UInt32) where {C <: AbstractChar} = convert(AnnotatedChar, C(c))
2✔
148

149
promote_rule(::Type{<:AnnotatedString}, ::Type{<:AbstractString}) = AnnotatedString
×
150

151
## AbstractString interface ##
152

153
ncodeunits(s::AnnotatedString) = ncodeunits(s.string)
3,897✔
154
codeunits(s::AnnotatedString) = codeunits(s.string)
×
155
codeunit(s::AnnotatedString) = codeunit(s.string)
×
156
codeunit(s::AnnotatedString, i::Integer) = codeunit(s.string, i)
×
157
isvalid(s::AnnotatedString, i::Integer) = isvalid(s.string, i)
9,811✔
158
@propagate_inbounds iterate(s::AnnotatedString, i::Integer=firstindex(s)) =
304✔
159
    if i <= lastindex(s.string); (s[i], nextind(s, i)) end
150✔
160
eltype(::Type{<:AnnotatedString{S}}) where {S} = AnnotatedChar{eltype(S)}
3✔
161
firstindex(s::AnnotatedString) = firstindex(s.string)
×
162
lastindex(s::AnnotatedString) = lastindex(s.string)
275✔
163

164
function getindex(s::AnnotatedString, i::Integer)
1,957✔
165
    @boundscheck checkbounds(s, i)
1,958✔
166
    @inbounds if isvalid(s, i)
3,915✔
167
        AnnotatedChar(s.string[i], Pair{Symbol, Any}[last(x) for x in annotations(s, i)])
1,958✔
168
    else
169
        string_index_err(s, i)
×
170
    end
171
end
172

173
# To make `AnnotatedString`s repr-evaluable, we need to override
174
# the generic `AbstractString` 2-arg show method.
175

176
function show(io::IO, s::A) where {A <: AnnotatedString}
2✔
177
    show(io, A)
2✔
178
    print(io, '(')
2✔
179
    show(io, s.string)
2✔
180
    print(io, ", ")
2✔
181
    show(IOContext(io, :typeinfo => typeof(annotations(s))), annotations(s))
2✔
182
    print(io, ')')
2✔
183
end
184

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

189
## AbstractChar interface ##
190

191
ncodeunits(c::AnnotatedChar) = ncodeunits(c.char)
72✔
192
codepoint(c::AnnotatedChar) = codepoint(c.char)
2,409✔
193

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

200
==(a::AnnotatedString, b::AnnotatedString) =
432✔
201
    a.string == b.string && a.annotations == b.annotations
202

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

206
# To prevent substring equality from hitting the generic fallback
207

208
function ==(a::SubString{<:AnnotatedString}, b::SubString{<:AnnotatedString})
5✔
209
    SubString(a.string.string, a.offset, a.ncodeunits, Val(:noshift)) ==
5✔
210
        SubString(b.string.string, b.offset, b.ncodeunits, Val(:noshift)) &&
211
        annotations(a) == annotations(b)
212
end
213

214
==(a::SubString{<:AnnotatedString}, b::AnnotatedString) =
1✔
215
    annotations(a) == annotations(b) && SubString(a.string.string, a.offset, a.ncodeunits, Val(:noshift)) == b.string
216

217
==(a::SubString{<:AnnotatedString}, b::AbstractString) =
18✔
218
    isempty(annotations(a)) && SubString(a.string.string, a.offset, a.ncodeunits, Val(:noshift)) == b
219

220
==(a::AbstractString, b::SubString{<:AnnotatedString}) = b == a
2✔
221

222
==(a::AnnotatedString, b::SubString{<:AnnotatedString}) = b == a
×
223

224
"""
225
    annotatedstring(values...)
226

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

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

233
See also [`AnnotatedString`](@ref) and [`AnnotatedChar`](@ref).
234

235
## Examples
236

237
```julia-repl
238
julia> annotatedstring("now a AnnotatedString")
239
"now a AnnotatedString"
240

241
julia> annotatedstring(AnnotatedString("annotated", [(1:9, :label => 1)]), ", and unannotated")
242
"annotated, and unannotated"
243
```
244
"""
245
function annotatedstring(xs...)
1,006✔
246
    isempty(xs) && return AnnotatedString("")
892✔
247
    size = mapreduce(_str_sizehint, +, xs)
1,006✔
248
    buf = IOBuffer(sizehint=size)
1,006✔
249
    s = IOContext(buf, :color => true)
892✔
250
    annotations = Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}()
1,006✔
251
    for x in xs
1,006✔
252
        size = filesize(s.io)
1,721✔
253
        if x isa AnnotatedString
1,697✔
254
            for (region, annot) in x.annotations
763✔
255
                push!(annotations, (size .+ (region), annot))
363✔
256
            end
363✔
257
            print(s, x.string)
763✔
258
        elseif x isa SubString{<:AnnotatedString}
1,038✔
259
            for (region, annot) in x.string.annotations
261✔
260
                start, stop = first(region), last(region)
231✔
261
                if start <= x.offset + x.ncodeunits && stop > x.offset
231✔
262
                    rstart = size + max(0, start - x.offset - 1) + 1
230✔
263
                    rstop = size + min(stop, x.offset + x.ncodeunits) - x.offset
230✔
264
                    push!(annotations, (rstart:rstop, annot))
230✔
265
                end
266
            end
231✔
267
            print(s, SubString(x.string.string, x.offset, x.ncodeunits, Val(:noshift)))
261✔
268
        elseif x isa AnnotatedChar
659✔
269
            for annot in x.annotations
336✔
270
                push!(annotations, (1+size:1+size, annot))
168✔
271
            end
168✔
272
            print(s, x.char)
336✔
273
        else
274
            print(s, x)
449✔
275
        end
276
    end
2,514✔
277
    str = String(take!(buf))
2,012✔
278
    AnnotatedString(str, annotations)
1,006✔
279
end
280

281
annotatedstring(s::AnnotatedString) = s
324✔
282
annotatedstring(c::AnnotatedChar) =
112✔
283
    AnnotatedString(string(c.char), [(1:ncodeunits(c), annot) for annot in c.annotations])
284

285
AnnotatedString(s::SubString{<:AnnotatedString}) = annotatedstring(s)
37✔
286

287
function repeat(str::AnnotatedString, r::Integer)
2✔
288
    r == 0 && return one(AnnotatedString)
2✔
289
    r == 1 && return str
2✔
290
    unannot = repeat(str.string, r)
2✔
291
    annotations = Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}()
2✔
292
    len = ncodeunits(str)
2✔
293
    fullregion = firstindex(str):lastindex(str)
4✔
294
    if allequal(first, str.annotations) && first(first(str.annotations)) == fullregion
2✔
295
        newfullregion = firstindex(unannot):lastindex(unannot)
2✔
296
        for (_, annot) in str.annotations
1✔
297
            push!(annotations, (newfullregion, annot))
1✔
298
        end
1✔
299
    else
300
        for offset in 0:len:(r-1)*len
2✔
301
            for (region, annot) in str.annotations
2✔
302
                push!(annotations, (region .+ offset, annot))
2✔
303
            end
2✔
304
        end
2✔
305
    end
306
    AnnotatedString(unannot, annotations)
2✔
307
end
308

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

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

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

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

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

332
## End AbstractString interface ##
333

334
function _annotate!(annlist::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}, range::UnitRange{Int}, @nospecialize(labelval::Pair{Symbol, <:Any}))
335
    label, val = labelval
118✔
336
    if val === nothing
118✔
337
        deleteat!(annlist, findall(ann -> ann[1] == range && first(ann[2]) === label, annlist))
×
338
    else
339
        push!(annlist, (range, Pair{Symbol, Any}(label, val)))
118✔
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}, @nospecialize(labelval::Pair{Symbol, <:Any})) =
215✔
354
    (_annotate!(s.annotations, range, labelval); s)
17✔
355

356
annotate!(ss::AnnotatedString, @nospecialize(labelval::Pair{Symbol, <:Any})) =
×
357
    annotate!(ss, firstindex(ss):lastindex(ss), labelval)
358

359
annotate!(s::SubString{<:AnnotatedString}, range::UnitRange{Int}, @nospecialize(labelval::Pair{Symbol, <:Any})) =
200✔
360
    (annotate!(s.string, s.offset .+ (range), labelval); s)
14✔
361

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

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

368
Annotate `char` with the pair `label => value`.
369
"""
370
annotate!(c::AnnotatedChar, @nospecialize(labelval::Pair{Symbol, <:Any})) =
×
371
    (push!(c.annotations, labelval); c)
×
372

373
"""
374
    annotations(str::Union{AnnotatedString, SubString{AnnotatedString}},
375
                [position::Union{Integer, UnitRange}]) ->
376
        Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}
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!`.
388
"""
389
annotations(s::AnnotatedString) = s.annotations
27✔
390

391
function annotations(s::SubString{<:AnnotatedString})
392
    map(((region, annot),) -> (first(region)-s.offset:last(region)-s.offset, annot),
48✔
393
        annotations(s.string, s.offset+1:s.offset+s.ncodeunits))
394
end
395

396
function annotations(s::AnnotatedString, pos::UnitRange{<:Integer})
397
    # TODO optimise
398
    Tuple{UnitRange{Int64}, Pair{Symbol, Any}}[
2,006✔
399
        (max(first(pos), first(region)):min(last(pos), last(region)), annot)
400
        for (region, annot) in s.annotations if !isempty(intersect(pos, region))]
401
end
402

403
annotations(s::AnnotatedString, pos::Integer) = annotations(s, pos:pos)
1,977✔
404

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

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

411
"""
412
    annotations(chr::AnnotatedChar) -> Vector{Pair{Symbol, Any}}
413

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

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

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

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

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

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

463
## AnnotatedIOBuffer
464

465
struct AnnotatedIOBuffer <: AbstractPipe
466
    io::IOBuffer
2,840✔
467
    annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}
468
end
469

470
AnnotatedIOBuffer(io::IOBuffer) = AnnotatedIOBuffer(io, Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}())
2,835✔
471
AnnotatedIOBuffer() = AnnotatedIOBuffer(IOBuffer())
2,835✔
472

473
function show(io::IO, aio::AnnotatedIOBuffer)
×
474
    show(io, AnnotatedIOBuffer)
×
475
    size = filesize(aio.io)
×
476
    print(io, '(', size, " byte", ifelse(size == 1, "", "s"), ", ",
×
477
          length(aio.annotations), " annotation", ifelse(length(aio.annotations) == 1, "", "s"), ")")
478
end
479

480
pipe_reader(io::AnnotatedIOBuffer) = io.io
156✔
481
pipe_writer(io::AnnotatedIOBuffer) = io.io
91✔
482

483
# Useful `IOBuffer` methods that we don't get from `AbstractPipe`
484
position(io::AnnotatedIOBuffer) = position(io.io)
340✔
485
seek(io::AnnotatedIOBuffer, n::Integer) = (seek(io.io, n); io)
135✔
486
seekend(io::AnnotatedIOBuffer) = (seekend(io.io); io)
1✔
487
skip(io::AnnotatedIOBuffer, n::Integer) = (skip(io.io, n); io)
1✔
488
copy(io::AnnotatedIOBuffer) = AnnotatedIOBuffer(copy(io.io), copy(io.annotations))
5✔
489

490
annotations(io::AnnotatedIOBuffer) = io.annotations
20✔
491

492
annotate!(io::AnnotatedIOBuffer, range::UnitRange{Int}, @nospecialize(labelval::Pair{Symbol, <:Any})) =
4✔
493
    (_annotate!(io.annotations, range, labelval); io)
2✔
494

495
function write(io::AnnotatedIOBuffer, astr::Union{AnnotatedString, SubString{<:AnnotatedString}})
110✔
496
    astr = AnnotatedString(astr)
145✔
497
    offset = position(io.io)
145✔
498
    eof(io) || _clear_annotations_in_region!(io.annotations, offset+1:offset+ncodeunits(astr))
149✔
499
    _insert_annotations!(io, astr.annotations)
145✔
500
    write(io.io, String(astr))
145✔
501
end
502

503
write(io::AnnotatedIOBuffer, c::AnnotatedChar) =
8✔
504
    write(io, AnnotatedString(string(c), map(a -> (1:ncodeunits(c), a), annotations(c))))
14✔
505
write(io::AnnotatedIOBuffer, x::AbstractString) = write(io.io, x)
×
506
write(io::AnnotatedIOBuffer, s::Union{SubString{String}, String}) = write(io.io, s)
125,383✔
507
write(io::AnnotatedIOBuffer, b::UInt8) = write(io.io, b)
8,852✔
508

509
function write(dest::AnnotatedIOBuffer, src::AnnotatedIOBuffer)
3✔
510
    destpos = position(dest)
3✔
511
    isappending = eof(dest)
3✔
512
    srcpos = position(src)
3✔
513
    nb = write(dest.io, src.io)
6✔
514
    isappending || _clear_annotations_in_region!(dest.annotations, destpos:destpos+nb)
4✔
515
    srcannots = [(max(1 + srcpos, first(region)):last(region), annot)
3✔
516
                 for (region, annot) in src.annotations if first(region) >= srcpos]
517
    _insert_annotations!(dest, srcannots, destpos - srcpos)
3✔
518
    nb
3✔
519
end
520

521
# So that read/writes with `IOContext` (and any similar `AbstractPipe` wrappers)
522
# work as expected.
523
function write(io::AbstractPipe, s::Union{AnnotatedString, SubString{<:AnnotatedString}})
1✔
524
    if pipe_writer(io) isa AnnotatedIOBuffer
113✔
525
        write(pipe_writer(io), s)
8✔
526
    else
527
        invoke(write, Tuple{IO, typeof(s)}, io, s)
106✔
528
    end::Int
529
end
530
# Can't be part of the `Union` above because it introduces method ambiguities
531
function write(io::AbstractPipe, c::AnnotatedChar)
1✔
532
    if pipe_writer(io) isa AnnotatedIOBuffer
1✔
533
        write(pipe_writer(io), c)
1✔
534
    else
535
        invoke(write, Tuple{IO, typeof(c)}, io, c)
×
536
    end::Int
537
end
538

539
"""
540
    _clear_annotations_in_region!(annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}, span::UnitRange{Int})
541

542
Erase the presence of `annotations` within a certain `span`.
543

544
This operates by removing all elements of `annotations` that are entirely
545
contained in `span`, truncating ranges that partially overlap, and splitting
546
annotations that subsume `span` to just exist either side of `span`.
547
"""
548
function _clear_annotations_in_region!(annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}, span::UnitRange{Int})
5✔
549
    # Clear out any overlapping pre-existing annotations.
550
    filter!(((region, _),) -> first(region) < first(span) || last(region) > last(span), annotations)
33✔
551
    extras = Tuple{Int, Tuple{UnitRange{Int}, Pair{Symbol, Any}}}[]
5✔
552
    for i in eachindex(annotations)
5✔
553
        region, annot = annotations[i]
12✔
554
        # Test for partial overlap
555
        if first(region) <= first(span) <= last(region) || first(region) <= last(span) <= last(region)
21✔
556
            annotations[i] = (if first(region) < first(span)
3✔
557
                                        first(region):first(span)-1
2✔
558
                                    else last(span)+1:last(region) end, annot)
4✔
559
            # If `span` fits exactly within `region`, then we've only copied over
560
            # the beginning overhang, but also need to conserve the end overhang.
561
            if first(region) < first(span) && last(span) < last(region)
3✔
562
                push!(extras, (i, (last(span)+1:last(region), annot)))
1✔
563
            end
564
        end
565
    end
19✔
566
    # Insert any extra entries in the appropriate position
567
    for (offset, (i, entry)) in enumerate(extras)
5✔
568
        insert!(annotations, i + offset, entry)
1✔
569
    end
1✔
570
    annotations
×
571
end
572

573
"""
574
    _insert_annotations!(io::AnnotatedIOBuffer, annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}, offset::Int = position(io))
575

576
Register new `annotations` in `io`, applying an `offset` to their regions.
577

578
The largely consists of simply shifting the regions of `annotations` by `offset`
579
and pushing them onto `io`'s annotations. However, when it is possible to merge
580
the new annotations with recent annotations in accordance with the semantics
581
outlined in [`AnnotatedString`](@ref), we do so. More specifically, when there
582
is a run of the most recent annotations that are also present as the first
583
`annotations`, with the same value and adjacent regions, the new annotations are
584
merged into the existing recent annotations by simply extending their range.
585

586
This is implemented so that one can say write an `AnnotatedString` to an
587
`AnnotatedIOBuffer` one character at a time without needlessly producing a
588
new annotation for each character.
589
"""
590
function _insert_annotations!(io::AnnotatedIOBuffer, annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}, offset::Int = position(io))
148✔
591
    run = 0
145✔
592
    if !isempty(io.annotations) && last(first(last(io.annotations))) == offset
148✔
593
        for i in reverse(axes(annotations, 1))
12✔
594
            annot = annotations[i]
12✔
595
            first(first(annot)) == 1 || continue
12✔
596
            i <= length(io.annotations) || continue
14✔
597
            if last(annot) == last(last(io.annotations))
10✔
598
                valid_run = true
×
599
                for runlen in 1:i
4✔
600
                    new_range, new_annot = annotations[begin+runlen-1]
5✔
601
                    old_range, old_annot = io.annotations[end-i+runlen]
5✔
602
                    if last(old_range) != offset || first(new_range) != 1 || old_annot != new_annot
10✔
603
                        valid_run = false
×
604
                        break
×
605
                    end
606
                end
4✔
607
                if valid_run
4✔
608
                    run = i
×
609
                    break
3✔
610
                end
611
            end
612
        end
9✔
613
    end
614
    for runindex in 0:run-1
148✔
615
        old_index = lastindex(io.annotations) - run + 1 + runindex
4✔
616
        old_region, annot = io.annotations[old_index]
4✔
617
        new_region, _ = annotations[begin+runindex]
4✔
618
        io.annotations[old_index] = (first(old_region):last(new_region)+offset, annot)
4✔
619
    end
5✔
620
    for index in run+1:lastindex(annotations)
188✔
621
        region, annot = annotations[index]
119✔
622
        start, stop = first(region), last(region)
119✔
623
        push!(io.annotations, (start+offset:stop+offset, annot))
119✔
624
    end
119✔
625
end
626

627
function read(io::AnnotatedIOBuffer, ::Type{AnnotatedString{T}}) where {T <: AbstractString}
115✔
628
    if (start = position(io)) == 0
115✔
629
        AnnotatedString(read(io.io, T), copy(io.annotations))
111✔
630
    else
631
        annots = [(UnitRange{Int}(max(1, first(region) - start), last(region)-start), val)
4✔
632
                  for (region, val) in io.annotations if last(region) > start]
633
        AnnotatedString(read(io.io, T), annots)
4✔
634
    end
635
end
636
read(io::AnnotatedIOBuffer, ::Type{AnnotatedString{AbstractString}}) = read(io, AnnotatedString{String})
×
637
read(io::AnnotatedIOBuffer, ::Type{AnnotatedString}) = read(io, AnnotatedString{String})
100✔
638

639
function read(io::AnnotatedIOBuffer, ::Type{AnnotatedChar{T}}) where {T <: AbstractChar}
640
    pos = position(io)
3✔
641
    char = read(io.io, T)
3✔
642
    annots = Pair{Symbol, Any}[annot for (range, annot) in io.annotations if pos+1 in range]
3✔
643
    AnnotatedChar(char, annots)
3✔
644
end
645
read(io::AnnotatedIOBuffer, ::Type{AnnotatedChar{AbstractChar}}) = read(io, AnnotatedChar{Char})
×
646
read(io::AnnotatedIOBuffer, ::Type{AnnotatedChar}) = read(io, AnnotatedChar{Char})
3✔
647

648
function truncate(io::AnnotatedIOBuffer, size::Integer)
4✔
649
    truncate(io.io, size)
4✔
650
    filter!(((range, _),) -> first(range) <= size, io.annotations)
12✔
651
    map!(((range, val),) -> (first(range):min(size, last(range)), val),
9✔
652
         io.annotations, io.annotations)
653
    io
4✔
654
end
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