1use crate::events::HighlightEvent;
4use std::collections::{BTreeMap, BTreeSet};
5use std::error::Error;
6use std::fmt;
7use std::ops::Range;
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
11pub struct Position {
12 line: usize,
13 column: usize,
14}
15
16impl Position {
17 pub const fn new(line: usize, column: usize) -> Self {
19 Self { line, column }
20 }
21
22 pub const fn line(self) -> usize {
24 self.line
25 }
26
27 pub const fn column(self) -> usize {
29 self.column
30 }
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, Hash)]
35#[non_exhaustive]
36pub enum AnnotationRange {
37 Offset(Range<usize>),
39 Position(Range<Position>),
41}
42
43impl From<Range<usize>> for AnnotationRange {
44 fn from(range: Range<usize>) -> Self {
45 Self::Offset(range)
46 }
47}
48
49impl From<Range<Position>> for AnnotationRange {
50 fn from(range: Range<Position>) -> Self {
51 Self::Position(range)
52 }
53}
54
55#[derive(Clone, Debug, PartialEq, Eq, Hash)]
75pub struct Annotation<T = ()> {
76 range: AnnotationRange,
78 data: T,
80}
81
82impl<T> Annotation<T> {
83 pub fn new(range: impl Into<AnnotationRange>, data: T) -> Result<Self, AnnotationError> {
97 let range = range.into();
98 let is_reversed = match &range {
99 AnnotationRange::Offset(range) => range.start > range.end,
100 AnnotationRange::Position(range) => range.start > range.end,
101 };
102
103 if is_reversed {
104 return Err(AnnotationError::InvalidRange { range });
105 }
106
107 Ok(Self { range, data })
108 }
109
110 pub fn range(&self) -> &AnnotationRange {
112 &self.range
113 }
114
115 pub fn data(&self) -> &T {
117 &self.data
118 }
119
120 pub fn into_data(self) -> T {
122 self.data
123 }
124}
125
126#[derive(Debug, PartialEq, Eq, Hash)]
127pub(crate) struct ResolvedAnnotation<'a, T = ()> {
128 pub(crate) range: Range<usize>,
129 pub(crate) data: &'a T,
130}
131
132impl<T> Clone for ResolvedAnnotation<'_, T> {
133 fn clone(&self) -> Self {
134 Self {
135 range: self.range.clone(),
136 data: self.data,
137 }
138 }
139}
140
141#[derive(Clone, Debug, PartialEq, Eq)]
143#[non_exhaustive]
144pub enum AnnotationError {
145 InvalidRange {
146 range: AnnotationRange,
147 },
148 OutOfBounds {
149 index: usize,
150 end: usize,
151 source_len: usize,
152 },
153 NotCharBoundary {
154 index: usize,
155 offset: usize,
156 },
157 LineOutOfBounds {
158 index: usize,
159 line: usize,
160 line_count: usize,
161 },
162 ColumnOutOfBounds {
163 index: usize,
164 position: Position,
165 line_len: usize,
166 },
167}
168
169impl fmt::Display for AnnotationError {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 match self {
172 Self::InvalidRange { range } => {
173 write!(
174 f,
175 "annotation range start must not be after its end: {range:?}"
176 )
177 }
178 Self::OutOfBounds {
179 index,
180 end,
181 source_len,
182 } => write!(
183 f,
184 "annotation {index} ends at byte {end}, beyond source length {source_len}"
185 ),
186 Self::NotCharBoundary { index, offset } => write!(
187 f,
188 "annotation {index} offset {offset} is not a UTF-8 character boundary"
189 ),
190 Self::LineOutOfBounds {
191 index,
192 line,
193 line_count,
194 } => write!(
195 f,
196 "annotation {index} line {line} is outside the source's {line_count} lines"
197 ),
198 Self::ColumnOutOfBounds {
199 index,
200 position,
201 line_len,
202 } => write!(
203 f,
204 "annotation {index} column {} is beyond line {} byte length {line_len}",
205 position.column, position.line
206 ),
207 }
208 }
209}
210
211impl Error for AnnotationError {}
212
213#[derive(Clone, Copy, PartialEq, Eq)]
214enum InputLayer<'a> {
215 Syntax {
216 id: usize,
217 scope_index: usize,
218 language: &'a str,
219 },
220 Decoration {
221 id: usize,
222 decoration: crate::decorations::Decoration,
223 },
224}
225
226#[derive(Clone, Copy, PartialEq, Eq)]
227enum ActiveLayer<'s> {
228 Input(InputLayer<'s>),
229 Annotation { index: usize },
230}
231
232impl InputLayer<'_> {
233 fn open_event<'a, T>(self) -> HighlightEvent<'a, T> {
234 match self {
235 Self::Syntax {
236 scope_index,
237 language,
238 ..
239 } => HighlightEvent::Start {
240 scope_index,
241 language: language.to_string(),
242 },
243 Self::Decoration { decoration, .. } => HighlightEvent::DecorationStart { decoration },
244 }
245 }
246
247 fn close_event<'a, T>(self) -> HighlightEvent<'a, T> {
248 match self {
249 Self::Syntax { .. } => HighlightEvent::End,
250 Self::Decoration { .. } => HighlightEvent::DecorationEnd,
251 }
252 }
253}
254
255#[derive(Default)]
256struct AnnotationBoundary {
257 starts: Vec<usize>,
258 ends: Vec<usize>,
259 points: Vec<usize>,
263}
264
265#[doc(hidden)]
271pub fn compose_annotations<'a, T>(
272 source: &str,
273 events: &[HighlightEvent<'_, ()>],
274 annotations: &'a [Annotation<T>],
275) -> Result<Vec<HighlightEvent<'a, T>>, AnnotationError> {
276 compose_annotations_with_placement(source, events, annotations, AnnotationPlacement::Outermost)
277}
278
279pub(crate) fn compose_annotations_innermost<'a, T>(
284 source: &str,
285 events: &[HighlightEvent<'_, ()>],
286 annotations: &'a [Annotation<T>],
287) -> Result<Vec<HighlightEvent<'a, T>>, AnnotationError> {
288 compose_annotations_with_placement(source, events, annotations, AnnotationPlacement::Innermost)
289}
290
291#[derive(Clone, Copy)]
292enum AnnotationPlacement {
293 Outermost,
294 Innermost,
295}
296
297fn compose_annotations_with_placement<'a, T>(
298 source: &str,
299 events: &[HighlightEvent<'_, ()>],
300 annotations: &'a [Annotation<T>],
301 placement: AnnotationPlacement,
302) -> Result<Vec<HighlightEvent<'a, T>>, AnnotationError> {
303 if annotations.is_empty() {
304 return Ok(copy_syntax_events(events));
305 }
306
307 let annotations = resolve_annotations(source, annotations)?;
308
309 let boundaries = annotation_boundaries(&annotations);
310 let mut boundary_positions = boundaries.keys().peekable();
311 let mut pending_points: BTreeSet<usize> = boundaries
312 .iter()
313 .filter(|(_, boundary)| !boundary.points.is_empty())
314 .map(|(offset, _)| *offset)
315 .collect();
316 let mut active_annotations = BTreeSet::new();
317 let mut input_layers = Vec::new();
318 let mut next_input_id = 0usize;
319 let mut active_layers = Vec::new();
320 let mut desired_layers = Vec::new();
321 let mut output = Vec::new();
322
323 for event in events {
324 let HighlightEvent::Source { start, end } = event else {
325 update_input_layers(event, &mut input_layers, &mut next_input_id);
326 continue;
327 };
328
329 while boundary_positions
330 .peek()
331 .is_some_and(|&&position| position <= *start)
332 {
333 let &position = boundary_positions
334 .next()
335 .expect("peeked annotation boundary exists");
336 apply_boundary(&boundaries[&position], &mut active_annotations);
337 }
338
339 let mut cursor = *start;
340 while cursor < *end {
341 let next = boundary_positions
342 .peek()
343 .map(|&&position| position)
344 .filter(|position| *position < *end)
345 .unwrap_or(*end);
346
347 set_desired_layers(
348 &active_annotations,
349 &input_layers,
350 placement,
351 &mut desired_layers,
352 );
353 transition_layers(
354 &mut output,
355 &mut active_layers,
356 &mut desired_layers,
357 &annotations,
358 );
359 emit_points(
360 &mut output,
361 &boundaries,
362 &annotations,
363 &mut pending_points,
364 cursor,
365 );
366
367 if cursor < next {
368 output.push(HighlightEvent::Source {
369 start: cursor,
370 end: next,
371 });
372 }
373 cursor = next;
374
375 while boundary_positions
376 .peek()
377 .is_some_and(|&&position| position == cursor)
378 {
379 let &position = boundary_positions
380 .next()
381 .expect("peeked annotation boundary exists");
382 apply_boundary(&boundaries[&position], &mut active_annotations);
383 }
384 }
385 }
386
387 desired_layers.clear();
388 transition_layers(
389 &mut output,
390 &mut active_layers,
391 &mut desired_layers,
392 &annotations,
393 );
394
395 for offset in pending_points.clone() {
399 emit_points(
400 &mut output,
401 &boundaries,
402 &annotations,
403 &mut pending_points,
404 offset,
405 );
406 }
407
408 Ok(output)
409}
410
411fn update_input_layers<'s>(
412 event: &'s HighlightEvent<'_, ()>,
413 layers: &mut Vec<InputLayer<'s>>,
414 next_id: &mut usize,
415) {
416 match event {
417 HighlightEvent::Start {
418 scope_index,
419 language,
420 } => {
421 layers.push(InputLayer::Syntax {
422 id: *next_id,
423 scope_index: *scope_index,
424 language,
425 });
426 *next_id += 1;
427 }
428 HighlightEvent::End if matches!(layers.last(), Some(InputLayer::Syntax { .. })) => {
429 layers.pop();
430 }
431 HighlightEvent::DecorationStart { decoration } => {
432 layers.push(InputLayer::Decoration {
433 id: *next_id,
434 decoration: *decoration,
435 });
436 *next_id += 1;
437 }
438 HighlightEvent::DecorationEnd
439 if matches!(layers.last(), Some(InputLayer::Decoration { .. })) =>
440 {
441 layers.pop();
442 }
443 HighlightEvent::Source { .. }
444 | HighlightEvent::End
445 | HighlightEvent::AnnotationStart { .. }
446 | HighlightEvent::AnnotationEnd
447 | HighlightEvent::DecorationEnd => {}
448 }
449}
450
451fn copy_syntax_events<'a, T>(events: &[HighlightEvent<'_, ()>]) -> Vec<HighlightEvent<'a, T>> {
452 let mut output = Vec::with_capacity(events.len());
453
454 for event in events {
455 output.push(match event {
456 HighlightEvent::Start {
457 scope_index,
458 language,
459 } => HighlightEvent::Start {
460 scope_index: *scope_index,
461 language: language.clone(),
462 },
463 HighlightEvent::Source { start, end } => HighlightEvent::Source {
464 start: *start,
465 end: *end,
466 },
467 HighlightEvent::End => HighlightEvent::End,
468 HighlightEvent::DecorationStart { decoration } => HighlightEvent::DecorationStart {
469 decoration: *decoration,
470 },
471 HighlightEvent::DecorationEnd => HighlightEvent::DecorationEnd,
472 HighlightEvent::AnnotationStart { .. } | HighlightEvent::AnnotationEnd => continue,
473 });
474 }
475
476 output
477}
478
479fn resolve_annotations<'a, T>(
480 source: &str,
481 annotations: &'a [Annotation<T>],
482) -> Result<Vec<ResolvedAnnotation<'a, T>>, AnnotationError> {
483 let line_starts = annotations
484 .iter()
485 .any(|annotation| matches!(annotation.range, AnnotationRange::Position(_)))
486 .then(|| source_line_starts(source));
487 let mut resolved = Vec::with_capacity(annotations.len());
488
489 for (index, annotation) in annotations.iter().enumerate() {
490 let range = match &annotation.range {
491 AnnotationRange::Offset(range) => range.clone(),
492 AnnotationRange::Position(range) => {
493 let line_starts = line_starts
494 .as_deref()
495 .expect("line starts exist when position ranges are present");
496 resolve_position(source, line_starts, index, range.start)?
497 ..resolve_position(source, line_starts, index, range.end)?
498 }
499 };
500
501 if range.end > source.len() {
502 return Err(AnnotationError::OutOfBounds {
503 index,
504 end: range.end,
505 source_len: source.len(),
506 });
507 }
508 for offset in [range.start, range.end] {
509 if !source.is_char_boundary(offset) {
510 return Err(AnnotationError::NotCharBoundary { index, offset });
511 }
512 }
513
514 resolved.push(ResolvedAnnotation {
515 range,
516 data: &annotation.data,
517 });
518 }
519
520 Ok(resolved)
521}
522
523fn source_line_starts(source: &str) -> Vec<usize> {
524 let mut starts = vec![0];
525 starts.extend(
526 source
527 .bytes()
528 .enumerate()
529 .filter_map(|(index, byte)| (byte == b'\n').then_some(index + 1)),
530 );
531 starts
532}
533
534fn resolve_position(
535 source: &str,
536 line_starts: &[usize],
537 index: usize,
538 position: Position,
539) -> Result<usize, AnnotationError> {
540 let Some(&line_start) = line_starts.get(position.line) else {
541 return Err(AnnotationError::LineOutOfBounds {
542 index,
543 line: position.line,
544 line_count: line_starts.len(),
545 });
546 };
547 let line_end = line_starts
548 .get(position.line + 1)
549 .map_or(source.len(), |next_start| next_start - 1);
550 let line_len = line_end - line_start;
551
552 if position.column > line_len {
553 return Err(AnnotationError::ColumnOutOfBounds {
554 index,
555 position,
556 line_len,
557 });
558 }
559
560 Ok(line_start + position.column)
561}
562
563fn annotation_boundaries<T>(
564 annotations: &[ResolvedAnnotation<'_, T>],
565) -> BTreeMap<usize, AnnotationBoundary> {
566 let mut boundaries = BTreeMap::<usize, AnnotationBoundary>::new();
567 for (index, annotation) in annotations.iter().enumerate() {
568 if annotation.range.start == annotation.range.end {
569 boundaries
570 .entry(annotation.range.start)
571 .or_default()
572 .points
573 .push(index);
574 continue;
575 }
576 boundaries
577 .entry(annotation.range.start)
578 .or_default()
579 .starts
580 .push(index);
581 boundaries
582 .entry(annotation.range.end)
583 .or_default()
584 .ends
585 .push(index);
586 }
587 boundaries
588}
589
590fn emit_points<'a, T>(
596 output: &mut Vec<HighlightEvent<'a, T>>,
597 boundaries: &BTreeMap<usize, AnnotationBoundary>,
598 annotations: &[ResolvedAnnotation<'a, T>],
599 pending: &mut BTreeSet<usize>,
600 offset: usize,
601) {
602 if !pending.remove(&offset) {
603 return;
604 }
605 for index in &boundaries[&offset].points {
606 let annotation = &annotations[*index];
607 output.push(HighlightEvent::AnnotationStart {
608 range: annotation.range.clone(),
609 data: annotation.data,
610 });
611 output.push(HighlightEvent::AnnotationEnd);
612 }
613}
614
615fn apply_boundary(boundary: &AnnotationBoundary, active: &mut BTreeSet<usize>) {
616 for index in &boundary.ends {
617 active.remove(index);
618 }
619 for index in &boundary.starts {
620 active.insert(*index);
621 }
622}
623
624fn set_desired_layers<'s>(
625 active_annotations: &BTreeSet<usize>,
626 input_layers: &[InputLayer<'s>],
627 placement: AnnotationPlacement,
628 layers: &mut Vec<ActiveLayer<'s>>,
629) {
630 layers.clear();
631 layers.reserve(active_annotations.len() + input_layers.len());
632
633 let push_annotations = |layers: &mut Vec<_>| {
634 for index in active_annotations {
635 layers.push(ActiveLayer::Annotation { index: *index });
636 }
637 };
638 let push_inputs = |layers: &mut Vec<_>| {
639 for layer in input_layers {
640 layers.push(ActiveLayer::Input(*layer));
641 }
642 };
643
644 match placement {
645 AnnotationPlacement::Outermost => {
646 push_annotations(layers);
647 push_inputs(layers);
648 }
649 AnnotationPlacement::Innermost => {
650 push_inputs(layers);
651 push_annotations(layers);
652 }
653 }
654}
655
656fn transition_layers<'s, 'a, T>(
657 output: &mut Vec<HighlightEvent<'a, T>>,
658 current: &mut Vec<ActiveLayer<'s>>,
659 desired: &mut Vec<ActiveLayer<'s>>,
660 annotations: &[ResolvedAnnotation<'a, T>],
661) {
662 let common = current
663 .iter()
664 .zip(desired.iter())
665 .take_while(|(left, right)| left == right)
666 .count();
667
668 for layer in current[common..].iter().rev() {
669 output.push(match layer {
670 ActiveLayer::Input(layer) => layer.close_event(),
671 ActiveLayer::Annotation { .. } => HighlightEvent::AnnotationEnd,
672 });
673 }
674
675 for layer in &desired[common..] {
676 output.push(match layer {
677 ActiveLayer::Input(layer) => layer.open_event(),
678 ActiveLayer::Annotation { index } => {
679 let annotation = &annotations[*index];
680 HighlightEvent::AnnotationStart {
681 range: annotation.range.clone(),
682 data: annotation.data,
683 }
684 }
685 });
686 }
687
688 std::mem::swap(current, desired);
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694
695 #[test]
696 #[allow(clippy::reversed_empty_ranges)]
698 fn rejects_reversed_ranges_at_construction() {
699 assert_eq!(
700 Annotation::new(3..2, ()).unwrap_err(),
701 AnnotationError::InvalidRange {
702 range: AnnotationRange::Offset(3..2),
703 }
704 );
705 assert_eq!(
706 Annotation::new(Position::new(1, 0)..Position::new(0, 0), ()).unwrap_err(),
707 AnnotationError::InvalidRange {
708 range: AnnotationRange::Position(Position::new(1, 0)..Position::new(0, 0)),
709 }
710 );
711 }
712
713 #[test]
714 fn accepts_empty_ranges_as_points() {
715 assert!(Annotation::new(2..2, ()).is_ok());
716 assert!(Annotation::new(Position::new(1, 0)..Position::new(1, 0), ()).is_ok());
717 }
718
719 #[test]
720 fn composes_an_empty_annotation_as_a_start_end_pair() {
721 let source = "a\n\nb";
723 let syntax = vec![HighlightEvent::Source { start: 0, end: 4 }];
724 let annotations = vec![Annotation::new(2..2, "blank-line").unwrap()];
725
726 let events = compose_annotations(source, &syntax, &annotations).unwrap();
727
728 assert_eq!(
729 events,
730 vec![
731 HighlightEvent::Source { start: 0, end: 2 },
732 HighlightEvent::AnnotationStart {
733 range: 2..2,
734 data: &"blank-line",
735 },
736 HighlightEvent::AnnotationEnd,
737 HighlightEvent::Source { start: 2, end: 4 },
738 ]
739 );
740 }
741
742 #[test]
743 fn composes_a_point_at_the_end_of_the_source() {
744 let source = "a\n";
747 let syntax = vec![HighlightEvent::Source { start: 0, end: 2 }];
748 let annotations = vec![Annotation::new(2..2, "trailing").unwrap()];
749
750 let events = compose_annotations(source, &syntax, &annotations).unwrap();
751
752 assert_eq!(
753 events,
754 vec![
755 HighlightEvent::Source { start: 0, end: 2 },
756 HighlightEvent::AnnotationStart {
757 range: 2..2,
758 data: &"trailing",
759 },
760 HighlightEvent::AnnotationEnd,
761 ]
762 );
763 }
764
765 #[test]
766 fn a_point_does_not_leak_into_later_layers() {
767 let source = "abcd";
770 let syntax = vec![HighlightEvent::Source { start: 0, end: 4 }];
771 let annotations = vec![Annotation::new(1..1, "point").unwrap()];
772
773 let events = compose_annotations(source, &syntax, &annotations).unwrap();
774
775 let starts = events
776 .iter()
777 .filter(|event| matches!(event, HighlightEvent::AnnotationStart { .. }))
778 .count();
779 let ends = events
780 .iter()
781 .filter(|event| matches!(event, HighlightEvent::AnnotationEnd))
782 .count();
783
784 assert_eq!((starts, ends), (1, 1));
785 assert!(matches!(
786 events.last(),
787 Some(HighlightEvent::Source { start: 1, end: 4 })
788 ));
789 }
790
791 #[test]
792 fn a_point_nests_inside_a_span_starting_at_the_same_offset() {
793 let source = "abcd";
794 let syntax = vec![HighlightEvent::Source { start: 0, end: 4 }];
795 let annotations = vec![
796 Annotation::new(1..3, "span").unwrap(),
797 Annotation::new(1..1, "point").unwrap(),
798 ];
799
800 let events = compose_annotations(source, &syntax, &annotations).unwrap();
801 let order: Vec<&str> = events
802 .iter()
803 .filter_map(|event| match event {
804 HighlightEvent::AnnotationStart { data, .. } => Some(**data),
805 _ => None,
806 })
807 .collect();
808
809 assert_eq!(order, vec!["span", "point"]);
810 }
811
812 #[test]
813 fn composition_without_annotations_preserves_syntax_events() {
814 let source = "ab";
815 let syntax = vec![
816 HighlightEvent::Start {
817 scope_index: 1,
818 language: "rust".to_string(),
819 },
820 HighlightEvent::Source { start: 0, end: 1 },
821 HighlightEvent::End,
822 HighlightEvent::Start {
823 scope_index: 1,
824 language: "rust".to_string(),
825 },
826 HighlightEvent::Source { start: 1, end: 2 },
827 HighlightEvent::End,
828 ];
829 let annotations: [Annotation; 0] = [];
830
831 let events = compose_annotations(source, &syntax, &annotations).unwrap();
832
833 assert_eq!(events, syntax);
834 }
835
836 #[test]
837 fn composition_keeps_distinct_adjacent_syntax_layers() {
838 let source = "ab";
839 let syntax = vec![
840 HighlightEvent::Start {
841 scope_index: 1,
842 language: "rust".to_string(),
843 },
844 HighlightEvent::Source { start: 0, end: 1 },
845 HighlightEvent::End,
846 HighlightEvent::Start {
847 scope_index: 1,
848 language: "rust".to_string(),
849 },
850 HighlightEvent::Source { start: 1, end: 2 },
851 HighlightEvent::End,
852 ];
853 let annotations = [Annotation::new(0..2, ()).unwrap()];
854
855 let events = compose_annotations(source, &syntax, &annotations).unwrap();
856 let syntax_starts = events
857 .iter()
858 .filter(|event| matches!(event, HighlightEvent::Start { .. }))
859 .count();
860
861 assert_eq!(syntax_starts, 2);
862 }
863
864 #[test]
865 fn composition_preserves_source_and_balances_flat_events() {
866 let source = "let value";
867 let syntax = vec![
868 HighlightEvent::Start {
869 scope_index: 1,
870 language: "rust".to_string(),
871 },
872 HighlightEvent::Source { start: 0, end: 3 },
873 HighlightEvent::End,
874 HighlightEvent::Source { start: 3, end: 4 },
875 HighlightEvent::Start {
876 scope_index: 2,
877 language: "rust".to_string(),
878 },
879 HighlightEvent::Source { start: 4, end: 9 },
880 HighlightEvent::End,
881 ];
882 let annotations = [
883 Annotation::new(1..7, ()).unwrap(),
884 Annotation::new(4..9, ()).unwrap(),
885 ];
886
887 let events = compose_annotations(source, &syntax, &annotations).unwrap();
888 let mut rendered = String::new();
889 let mut depth = 0usize;
890
891 for event in events {
892 match event {
893 HighlightEvent::Start { .. } | HighlightEvent::AnnotationStart { .. } => {
894 depth += 1;
895 }
896 HighlightEvent::End | HighlightEvent::AnnotationEnd => {
897 depth = depth.checked_sub(1).expect("unbalanced closing event");
898 }
899 HighlightEvent::Source { start, end } => rendered.push_str(&source[start..end]),
900 _ => unreachable!("annotation composition emits no other event"),
901 }
902 }
903
904 assert_eq!(rendered, source);
905 assert_eq!(depth, 0);
906 }
907
908 #[test]
909 fn rejects_non_utf8_boundaries() {
910 let source = "aéz";
911 let syntax = vec![HighlightEvent::Source {
912 start: 0,
913 end: source.len(),
914 }];
915 let annotations = [Annotation::new(1..2, ()).unwrap()];
916
917 assert_eq!(
918 compose_annotations(source, &syntax, &annotations).unwrap_err(),
919 AnnotationError::NotCharBoundary {
920 index: 0,
921 offset: 2,
922 }
923 );
924 }
925
926 #[test]
927 fn position_ranges_resolve_to_utf8_byte_ranges() {
928 let source = "π\r\ncafé";
929 let syntax = vec![HighlightEvent::Source {
930 start: 0,
931 end: source.len(),
932 }];
933 let annotations = [Annotation::new(Position::new(1, 0)..Position::new(1, 5), 7).unwrap()];
934
935 let events = compose_annotations(source, &syntax, &annotations).unwrap();
936 let (range, data) = events
937 .iter()
938 .find_map(|event| match event {
939 HighlightEvent::AnnotationStart { range, data } => Some((range, data)),
940 _ => None,
941 })
942 .unwrap();
943
944 assert_eq!(range, &(4..9));
945 assert_eq!(**data, 7);
946 }
947
948 #[test]
949 fn position_columns_must_be_utf8_boundaries() {
950 let source = "π\ncafé";
951 let syntax = vec![HighlightEvent::Source {
952 start: 0,
953 end: source.len(),
954 }];
955 let annotations = [Annotation::new(Position::new(1, 0)..Position::new(1, 4), ()).unwrap()];
956
957 assert_eq!(
958 compose_annotations(source, &syntax, &annotations).unwrap_err(),
959 AnnotationError::NotCharBoundary {
960 index: 0,
961 offset: 7,
962 }
963 );
964 }
965}