@@ -460,201 +460,109 @@ function annotated_chartransform(f::Function, str::AnnotatedString, state=nothin
460460    AnnotatedString (String (take! (outstr)), annots)
461461end 
462462
463- # # AnnotatedIOBuffer
464- 
465- struct  AnnotatedIOBuffer <:  AbstractPipe 
466-     io:: IOBuffer 
467-     annotations:: Vector{RegionAnnotation} 
468- end 
469- 
470- AnnotatedIOBuffer (io:: IOBuffer ) =  AnnotatedIOBuffer (io, Vector {RegionAnnotation} ())
471- AnnotatedIOBuffer () =  AnnotatedIOBuffer (IOBuffer ())
472- 
473- function  show (io:: IO , aio:: AnnotatedIOBuffer )
474-     show (io, AnnotatedIOBuffer)
475-     size =  filesize (aio. io)
476-     print (io, ' (' "  byte" ifelse (size ==  1 , " " " s" " , " 
477-           length (aio. annotations), "  annotation" ifelse (length (aio. annotations) ==  1 , " " " s" " )" 
463+ struct  RegionIterator{S <:  AbstractString }
464+     str:: S 
465+     regions:: Vector{UnitRange{Int}} 
466+     annotations:: Vector{Vector{Annotation}} 
478467end 
479468
480- pipe_reader (io:: AnnotatedIOBuffer ) =  io. io
481- pipe_writer (io:: AnnotatedIOBuffer ) =  io. io
482- 
483- #  Useful `IOBuffer` methods that we don't get from `AbstractPipe`
484- position (io:: AnnotatedIOBuffer ) =  position (io. io)
485- seek (io:: AnnotatedIOBuffer , n:: Integer ) =  (seek (io. io, n); io)
486- seekend (io:: AnnotatedIOBuffer ) =  (seekend (io. io); io)
487- skip (io:: AnnotatedIOBuffer , n:: Integer ) =  (skip (io. io, n); io)
488- copy (io:: AnnotatedIOBuffer ) =  AnnotatedIOBuffer (copy (io. io), copy (io. annotations))
489- 
490- annotations (io:: AnnotatedIOBuffer ) =  io. annotations
491- 
492- annotate! (io:: AnnotatedIOBuffer , range:: UnitRange{Int} , label:: Symbol , @nospecialize (val:: Any )) = 
493-     (_annotate! (io. annotations, range, label, val); io)
494- 
495- function  write (io:: AnnotatedIOBuffer , astr:: Union{AnnotatedString, SubString{<:AnnotatedString}} )
496-     astr =  AnnotatedString (astr)
497-     offset =  position (io. io)
498-     eof (io) ||  _clear_annotations_in_region! (io. annotations, offset+ 1 : offset+ ncodeunits (astr))
499-     _insert_annotations! (io, astr. annotations)
500-     write (io. io, String (astr))
501- end 
469+ Base. length (si:: RegionIterator ) =  length (si. regions)
502470
503- write (io:: AnnotatedIOBuffer , c:: AnnotatedChar ) = 
504-     write (io, AnnotatedString (string (c), [(region= 1 : ncodeunits (c), a... ) for  a in  c. annotations]))
505- write (io:: AnnotatedIOBuffer , x:: AbstractString ) =  write (io. io, x)
506- write (io:: AnnotatedIOBuffer , s:: Union{SubString{String}, String} ) =  write (io. io, s)
507- write (io:: AnnotatedIOBuffer , b:: UInt8 ) =  write (io. io, b)
508- 
509- function  write (dest:: AnnotatedIOBuffer , src:: AnnotatedIOBuffer )
510-     destpos =  position (dest)
511-     isappending =  eof (dest)
512-     srcpos =  position (src)
513-     nb =  write (dest. io, src. io)
514-     isappending ||  _clear_annotations_in_region! (dest. annotations, destpos: destpos+ nb)
515-     srcannots =  [setindex (annot, max (1  +  srcpos, first (annot. region)): last (annot. region), :region )
516-                  for  annot in  src. annotations if  first (annot. region) >=  srcpos]
517-     _insert_annotations! (dest, srcannots, destpos -  srcpos)
518-     nb
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 
519475end 
520476
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}} )
524-     if  pipe_writer (io) isa  AnnotatedIOBuffer
525-         write (pipe_writer (io), s)
526-     else 
527-         invoke (write, Tuple{IO, typeof (s)}, io, s)
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 )
532-     if  pipe_writer (io) isa  AnnotatedIOBuffer
533-         write (pipe_writer (io), c)
534-     else 
535-         invoke (write, Tuple{IO, typeof (c)}, io, c)
536-     end :: Int 
537- end 
477+ Base. eltype (:: RegionIterator{S} ) where  { S <:  AbstractString } = 
478+     Tuple{SubString{S}, Vector{Annotation}}
538479
539480""" 
540-     _clear_annotations_in_region!(annotations::Vector{$RegionAnnotation }, span::UnitRange{Int}) 
481+     eachregion(s::AnnotatedString{S}) 
482+     eachregion(s::SubString{AnnotatedString{S}}) 
541483
542- Erase the presence of `annotations` within a certain `span`. 
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 }}`. 
543487
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`. 
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+ ``` 
547499""" 
548- function  _clear_annotations_in_region! (annotations:: Vector{RegionAnnotation} , span:: UnitRange{Int} )
549-     #  Clear out any overlapping pre-existing annotations.
550-     filter! (ann ->  first (ann. region) <  first (span) ||  last (ann. region) >  last (span), annotations)
551-     extras =  Tuple{Int, RegionAnnotation}[]
552-     for  i in  eachindex (annotations)
553-         annot =  annotations[i]
554-         region =  annot. region
555-         #  Test for partial overlap
556-         if  first (region) <=  first (span) <=  last (region) ||  first (region) <=  last (span) <=  last (region)
557-             annotations[i] = 
558-                 setindex (annot,
559-                          if  first (region) <  first (span)
560-                              first (region): first (span)- 1 
561-                          else 
562-                              last (span)+ 1 : last (region)
563-                          end ,
564-                          :region )
565-             #  If `span` fits exactly within `region`, then we've only copied over
566-             #  the beginning overhang, but also need to conserve the end overhang.
567-             if  first (region) <  first (span) &&  last (span) <  last (region)
568-                 push! (extras, (i, setindex (annot, last (span)+ 1 : last (region), :region )))
569-             end 
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))
570525        end 
571526    end 
572-     #  Insert any extra entries in the appropriate position 
573-     for  (offset, (i, entry))  in   enumerate (extras )
574-         insert! (annotations, i  +  offset, entry )
527+     if   last (events) . pos  <   nextind (s,  last (subregion)) 
528+          push! (regions,  last (events) . pos : thisind (s,  last (subregion)) )
529+         push! (annots, [] )
575530    end 
576-     annotations 
531+     RegionIterator (s . string, regions, annots) 
577532end 
578533
579- """ 
580-     _insert_annotations!(io::AnnotatedIOBuffer, annotations::Vector{$RegionAnnotation }, offset::Int = position(io)) 
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 
581541
582- Register new `annotations` in `io`, applying an `offset` to their regions. 
542+ """ 
543+     annotation_events(string::AbstractString, annots::Vector{$RegionAnnotation }, subregion::UnitRange{Int}) 
544+     annotation_events(string::AnnotatedString, subregion::UnitRange{Int}) 
583545
584- The largely consists of simply shifting the regions of `annotations` by `offset` 
585- and pushing them onto `io`'s annotations. However, when it is possible to merge 
586- the new annotations with recent annotations in accordance with the semantics 
587- outlined in [`AnnotatedString`](@ref), we do so. More specifically, when there 
588- is a run of the most recent annotations that are also present as the first 
589- `annotations`, with the same value and adjacent regions, the new annotations are 
590- merged into the existing recent annotations by simply extending their range. 
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. 
591548
592- This is implemented so that one can say write an `AnnotatedString` to an 
593- `AnnotatedIOBuffer` one character at a time without needlessly producing a 
594- new annotation for each character. 
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. 
595553""" 
596- function  _insert_annotations! (io:: AnnotatedIOBuffer , annotations:: Vector{RegionAnnotation} , offset:: Int  =  position (io))
597-     run =  0 
598-     if  ! isempty (io. annotations) &&  last (last (io. annotations). region) ==  offset
599-         for  i in  reverse (axes (annotations, 1 ))
600-             annot =  annotations[i]
601-             first (annot. region) ==  1  ||  continue 
602-             i <=  length (io. annotations) ||  continue 
603-             if  annot. label ==  last (io. annotations). label &&  annot. value ==  last (io. annotations). value
604-                 valid_run =  true 
605-                 for  runlen in  1 : i
606-                     new =  annotations[begin + runlen- 1 ]
607-                     old =  io. annotations[end - i+ runlen]
608-                     if  last (old. region) !=  offset ||  first (new. region) !=  1  ||  old. label !=  new. label ||  old. value !=  new. value
609-                         valid_run =  false 
610-                         break 
611-                     end 
612-                 end 
613-                 if  valid_run
614-                     run =  i
615-                     break 
616-                 end 
617-             end 
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))
618562        end 
619563    end 
620-     for  runindex in  0 : run- 1 
621-         old_index =  lastindex (io. annotations) -  run +  1  +  runindex
622-         old =  io. annotations[old_index]
623-         new =  annotations[begin + runindex]
624-         io. annotations[old_index] =  setindex (old, first (old. region): last (new. region)+ offset, :region )
625-     end 
626-     for  index in  run+ 1 : lastindex (annotations)
627-         annot =  annotations[index]
628-         start, stop =  first (annot. region), last (annot. region)
629-         push! (io. annotations, setindex (annotations[index], start+ offset: stop+ offset, :region ))
630-     end 
564+     sort (events, by= e ->  e. pos)
631565end 
632566
633- function  read (io:: AnnotatedIOBuffer , :: Type{AnnotatedString{T}} ) where  {T <:  AbstractString }
634-     if  (start =  position (io)) ==  0 
635-         AnnotatedString (read (io. io, T), copy (io. annotations))
636-     else 
637-         annots =  [setindex (annot, UnitRange {Int} (max (1 , first (annot. region) -  start), last (annot. region)- start), :region )
638-                   for  annot in  io. annotations if  last (annot. region) >  start]
639-         AnnotatedString (read (io. io, T), annots)
640-     end 
641- end 
642- read (io:: AnnotatedIOBuffer , :: Type{AnnotatedString{AbstractString}} ) =  read (io, AnnotatedString{String})
643- read (io:: AnnotatedIOBuffer , :: Type{AnnotatedString} ) =  read (io, AnnotatedString{String})
644- 
645- function  read (io:: AnnotatedIOBuffer , :: Type{AnnotatedChar{T}} ) where  {T <:  AbstractChar }
646-     pos =  position (io)
647-     char =  read (io. io, T)
648-     annots =  [NamedTuple {(:label, :value)} (annot) for  annot in  io. annotations if  pos+ 1  in  annot. region]
649-     AnnotatedChar (char, annots)
650- end 
651- read (io:: AnnotatedIOBuffer , :: Type{AnnotatedChar{AbstractChar}} ) =  read (io, AnnotatedChar{Char})
652- read (io:: AnnotatedIOBuffer , :: Type{AnnotatedChar} ) =  read (io, AnnotatedChar{Char})
653- 
654- function  truncate (io:: AnnotatedIOBuffer , size:: Integer )
655-     truncate (io. io, size)
656-     filter! (ann ->  first (ann. region) <=  size, io. annotations)
657-     map! (ann ->  setindex (ann, first (ann. region): min (size, last (ann. region)), :region ),
658-          io. annotations, io. annotations)
659-     io
660- end 
567+ annotation_events (s:: AnnotatedString , subregion:: UnitRange{Int} ) = 
568+     annotation_events (s. string, annotations (s), subregion)
0 commit comments