1extern crate hi_doc_jumprope as jumprope;
2
3use std::{
4 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
5 ops::RangeInclusive,
6};
7
8mod segment;
9use annotation::{Annotation, AnnotationId, Opts};
10use anomaly_fixer::{apply_fixup, fixup_byte_to_char, fixup_char_to_display};
11pub use formatting::Text;
12use rand::{rngs::SmallRng, Rng, SeedableRng};
13use random_color::{
14 options::{Gamut, Luminosity},
15 RandomColor,
16};
17use range_map::{Range, RangeSet};
18use segment::SegmentBuffer;
19use single_line::LineAnnotation;
20
21pub use crate::formatting::Formatting;
22
23mod annotation;
24mod anomaly_fixer;
25pub(crate) mod associated_data;
26mod chars;
27mod formatting;
28mod inline;
29mod single_line;
30
31#[derive(Clone, Debug)]
32struct RawLine {
33 data: Text,
34}
35
36#[derive(Debug)]
37struct AnnotationLine {
38 prefix: Text,
39 line: Text,
40 annotation: Option<AnnotationId>,
42}
43
44#[derive(Debug)]
45struct GapLine {
46 prefix: Text,
47 line: Text,
48}
49
50#[derive(Debug)]
51struct TextLine {
52 prefix: Text,
53 line_num: usize,
54 line: Text,
55 fold: bool,
57 annotation: Option<AnnotationId>,
58 annotations: Vec<LineAnnotation>,
59 top_annotations: Vec<(Option<AnnotationId>, Text)>,
60 bottom_annotations: Vec<(Option<AnnotationId>, Text)>,
61}
62impl TextLine {
63 #[allow(dead_code)]
64 fn add_prefix(&mut self, this: Text, annotations: Text) {
65 self.prefix.extend([this]);
66 for (_, ele) in self.bottom_annotations.iter_mut() {
67 ele.splice(0..0, Some(annotations.clone()));
68 }
69 }
70 fn len(&self) -> usize {
71 self.line.len()
72 }
73 fn is_empty(&self) -> bool {
74 self.line.is_empty()
75 }
76 }
80
81fn cons_slices<T>(mut slice: &mut [T], test: impl Fn(&T) -> bool) -> Vec<&mut [T]> {
82 let mut out = Vec::new();
83
84 while !slice.is_empty() {
85 let mut skip = 0;
86 while !slice.get(skip).map(&test).unwrap_or(true) {
87 skip += 1;
88 }
89 let mut take = 0;
90 while slice.get(skip + take).map(&test).unwrap_or(false) {
91 take += 1;
92 }
93 let (_skipped, rest) = slice.split_at_mut(skip);
94 let (taken, rest) = rest.split_at_mut(take);
95 if !taken.is_empty() {
96 out.push(taken);
97 }
98 slice = rest;
99 }
100
101 out
102}
103
104#[derive(Debug)]
105enum Line {
106 Text(TextLine),
107 Annotation(AnnotationLine),
108 Raw(RawLine),
109 Nop,
110 Gap(GapLine),
111}
112impl Line {
113 fn text_mut(&mut self) -> Option<&mut Text> {
114 Some(match self {
115 Line::Text(t) => &mut t.line,
116 Line::Gap(t) => &mut t.line,
117 Line::Annotation(t) => &mut t.line,
118 _ => return None,
119 })
120 }
121 fn is_text(&self) -> bool {
122 matches!(self, Self::Text(_))
123 }
124 fn is_annotation(&self) -> bool {
125 matches!(self, Self::Annotation(_))
126 }
127 fn as_annotation(&self) -> Option<&AnnotationLine> {
128 match self {
129 Self::Annotation(a) => Some(a),
130 _ => None,
131 }
132 }
133 fn is_gap(&self) -> bool {
134 matches!(self, Self::Gap(_))
135 }
136 fn as_text_mut(&mut self) -> Option<&mut TextLine> {
137 match self {
138 Line::Text(t) => Some(t),
139 _ => None,
140 }
141 }
142 #[allow(dead_code)]
143 fn as_gap_mut(&mut self) -> Option<&mut GapLine> {
144 match self {
145 Line::Gap(t) => Some(t),
146 _ => None,
147 }
148 }
149 fn as_text(&self) -> Option<&TextLine> {
150 match self {
151 Line::Text(t) => Some(t),
152 _ => None,
153 }
154 }
155 fn as_raw(&self) -> Option<&RawLine> {
156 match self {
157 Line::Raw(r) => Some(r),
158 _ => None,
159 }
160 }
161 fn is_nop(&self) -> bool {
162 matches!(self, Self::Nop)
163 }
164}
165
166#[derive(Debug)]
167pub struct Source {
168 lines: Vec<Line>,
169}
170
171fn cleanup_nops(source: &mut Source) {
172 let mut i = 0;
173 while i < source.lines.len() {
174 if source.lines[i].is_nop() {
175 source.lines.remove(i);
176 } else {
177 i += 1;
178 }
179 }
180}
181
182fn cleanup(source: &mut Source) {
184 for slice in cons_slices(&mut source.lines, Line::is_text) {
185 for line in slice
186 .iter_mut()
187 .take_while(|l| l.as_text().unwrap().is_empty())
188 {
189 *line = Line::Nop;
190 }
191 for line in slice
192 .iter_mut()
193 .rev()
194 .take_while(|l| l.as_text().unwrap().is_empty())
195 {
196 *line = Line::Nop;
197 }
198 }
199 cleanup_nops(source);
200 for slice in cons_slices(&mut source.lines, Line::is_gap) {
201 if slice.len() == 1 {
202 continue;
203 }
204 for ele in slice.iter_mut().skip(1) {
205 *ele = Line::Nop;
206 }
207 }
208 cleanup_nops(source);
209}
210
211fn fold(source: &mut Source, opts: &Opts) {
212 for slice in cons_slices(&mut source.lines, Line::is_text) {
213 'line: for i in 0..slice.len() {
214 for j in i.saturating_sub(opts.context_lines)..=(i + opts.context_lines) {
215 let Some(ctx) = slice.get(j) else {
216 continue;
217 };
218 let Line::Text(t) = ctx else {
219 continue;
220 };
221 if t.fold {
222 continue;
223 }
224 continue 'line;
225 }
226 slice[i] = Line::Gap(GapLine {
227 prefix: Text::new(),
228 line: Text::new(),
229 });
230 }
231 }
232 cleanup(source);
233}
234
235fn draw_line_numbers(source: &mut Source) {
236 for lines in &mut cons_slices(&mut source.lines, |l| {
237 l.is_annotation() || l.is_text() || l.is_gap()
238 }) {
239 let max_num = lines
240 .iter()
241 .filter_map(|l| match l {
242 Line::Text(t) => Some(t.line_num),
243 _ => None,
244 })
245 .max()
246 .unwrap_or(0);
247 let max_len = max_num.to_string().len();
248 let prefix_segment =
249 SegmentBuffer::segment(" ".repeat(max_len - 1), Formatting::line_number());
250 for line in lines.iter_mut() {
251 match line {
252 Line::Text(t) => t.prefix.extend([SegmentBuffer::segment(
253 format!("{:>width$} ", t.line_num, width = max_len),
254 Formatting::line_number(),
255 )]),
256 Line::Annotation(a) => a.prefix.extend([
257 prefix_segment.clone(),
258 SegmentBuffer::segment("· ", Formatting::line_number()),
259 ]),
260 Line::Gap(a) => a.prefix.extend([
261 prefix_segment.clone(),
262 SegmentBuffer::segment("⋮ ", Formatting::line_number()),
263 ]),
264 _ => unreachable!(),
265 }
266 }
267 }
268}
269
270fn draw_line_connections(
271 source: &mut Source,
272 annotation_formats: HashMap<AnnotationId, Formatting>,
273) {
274 for lines in &mut cons_slices(&mut source.lines, |l| {
275 l.is_annotation() || l.is_text() || l.is_gap()
276 }) {
277 #[derive(Debug)]
278 struct Connection {
279 range: Range<usize>,
280 connected: Vec<usize>,
281 }
282
283 let mut connected_annotations = HashMap::new();
284 for (i, line) in lines.iter().enumerate() {
285 let annotation = if let Some(annotation) = line.as_annotation() {
286 annotation.annotation
287 } else if let Some(text) = line.as_text() {
288 text.annotation
289 } else {
290 None
291 };
292 if let Some(annotation) = annotation {
293 let conn = connected_annotations
294 .entry(annotation)
295 .or_insert(Connection {
296 range: Range::new(i, i),
297 connected: Vec::new(),
298 });
299 conn.range.start = conn.range.start.min(i);
300 conn.range.end = conn.range.end.max(i);
301 conn.connected.push(i);
302 }
303 }
304 let mut grouped = connected_annotations
305 .iter()
306 .map(|(k, v)| (*k, vec![v.range].into_iter().collect::<RangeSet<usize>>()))
307 .collect::<Vec<_>>();
308
309 grouped.sort_by_key(|a| a.1.num_elements());
310 let grouped = single_line::group_nonconflicting(&grouped, &HashSet::new());
311
312 for group in grouped {
313 for annotation in group {
314 let annotation_fmt = annotation_formats
315 .get(&annotation)
316 .expect("id is used in string but not defined")
317 .clone()
318 .decoration();
319 let conn = connected_annotations.get(&annotation).expect("exists");
320 let range = conn.range;
321 let mut max_index = usize::MAX;
322 for line in range.start..=range.end {
323 match &lines[line] {
324 Line::Text(t) if t.line.chars().all(|c| c.is_whitespace()) => {}
325 Line::Text(t) => {
326 let whitespaces =
327 t.line.chars().take_while(|i| i.is_whitespace()).count();
328 max_index = max_index.min(whitespaces)
329 }
330 Line::Annotation(t) if t.line.chars().all(|c| c.is_whitespace()) => {}
331 Line::Annotation(t) => {
332 let whitespaces =
333 t.line.chars().take_while(|i| i.is_whitespace()).count();
334 max_index = max_index.min(whitespaces)
335 }
336 Line::Gap(_) => {}
337 _ => unreachable!(),
338 }
339 }
340 while max_index < 2 {
341 let seg = Some(SegmentBuffer::segment(
342 " ".repeat(2 - max_index),
343 annotation_fmt.clone(),
344 ));
345 for line in lines.iter_mut() {
346 match line {
347 Line::Text(t) => t.line.splice(0..0, seg.clone()),
348 Line::Annotation(t) => t.line.splice(0..0, seg.clone()),
349 Line::Gap(t) => t.line.splice(0..0, seg.clone()),
350 _ => unreachable!(),
351 }
352 }
353 max_index = 2;
354 }
355 if max_index >= 2 {
356 let offset = max_index - 2;
357
358 for line in range.start..=range.end {
359 use chars::line::*;
360 let char = if range.start == range.end {
361 RANGE_EMPTY
362 } else if line == range.start {
363 RANGE_START
364 } else if line == range.end {
365 RANGE_END
366 } else if conn.connected.contains(&line) {
367 RANGE_CONNECTION
368 } else {
369 RANGE_CONTINUE
370 };
371 let text = lines[line].text_mut().expect("only with text reachable");
372 if text.len() <= offset {
373 text.resize(offset + 1, ' ', annotation_fmt.clone());
374 }
375 text.splice(
376 offset..offset + 1,
377 Some(SegmentBuffer::segment(
378 char.to_string(),
379 annotation_fmt.clone(),
380 )),
381 );
382
383 if conn.connected.contains(&line) {
384 for i in offset + 1..text.len() {
385 let (char, fmt) = text.get(i).expect("in bounds");
386 if !text.get(i).expect("in bounds").0.is_whitespace()
387 && !fmt.decoration
388 {
389 break;
390 }
391 if let Some((keep_style, replacement)) = cross(char) {
392 text.splice(
393 i..=i,
394 Some(SegmentBuffer::segment(
395 replacement.to_string(),
396 if keep_style {
397 fmt.clone()
398 } else {
399 annotation_fmt.clone()
400 },
401 )),
402 )
403 }
404 }
405 }
406 }
407 }
408 }
409 }
410 }
411}
412
413fn generate_annotations(source: &mut Source, opts: &Opts) {
414 for line in source
415 .lines
416 .iter_mut()
417 .flat_map(Line::as_text_mut)
418 .filter(|t| !t.annotations.is_empty())
419 {
420 let hide_ranges_for = if opts.apply_to_orig {
421 let parsed = inline::group_singleline(&line.annotations);
422 assert!(line.annotation.is_none());
423 line.annotation = parsed.annotation;
424 inline::apply_inline_annotations(&mut line.line, &parsed.inline, parsed.right);
425
426 line.annotations
427 .retain(|a| !parsed.processed.contains(&a.id));
428 line.fold = false;
429
430 parsed.hide_ranges_for
431 } else {
432 HashSet::new()
433 };
434
435 let char_to_display_fixup = fixup_char_to_display(line.line.chars());
436 let mut extra = single_line::generate_range_annotations(
437 line.annotations.clone(),
438 &char_to_display_fixup,
439 &hide_ranges_for,
440 false,
441 );
442 extra.reverse();
443 line.top_annotations = extra;
445 line.annotations.truncate(0);
446 }
447}
448
449fn apply_annotations(source: &mut Source) {
450 {
452 let mut insertions = vec![];
453 for (i, line) in source
454 .lines
455 .iter_mut()
456 .enumerate()
457 .flat_map(|(i, l)| l.as_text_mut().map(|t| (i, t)))
458 {
459 for buf in line.top_annotations.drain(..) {
460 insertions.push((i + 1, buf))
461 }
462 }
463 insertions.reverse();
464 for (i, (annotation, line)) in insertions {
465 source.lines.insert(
466 i - 1,
467 Line::Annotation(AnnotationLine {
468 line,
469 annotation,
470 prefix: SegmentBuffer::new(),
471 }),
472 );
473 }
474 }
475 {
477 let mut insertions = vec![];
478 for (i, line) in source
479 .lines
480 .iter_mut()
481 .enumerate()
482 .flat_map(|(i, l)| l.as_text_mut().map(|t| (i, t)))
483 {
484 for buf in line.bottom_annotations.drain(..) {
485 insertions.push((i + 1, buf))
486 }
487 }
488 insertions.reverse();
489 for (i, (annotation, line)) in insertions {
490 source.lines.insert(
491 i,
492 Line::Annotation(AnnotationLine {
493 line,
494 annotation,
495 prefix: SegmentBuffer::new(),
496 }),
497 );
498 }
499 }
500}
501
502fn process(
503 source: &mut Source,
504 annotation_formats: HashMap<AnnotationId, Formatting>,
505 opts: &Opts,
506) {
507 cleanup(source);
508 generate_annotations(source, opts);
510 if opts.fold {
512 fold(source, opts)
513 }
514 apply_annotations(source);
516 draw_line_connections(source, annotation_formats);
518 draw_line_numbers(source);
520 {
522 for line in &mut source.lines {
523 match line {
524 Line::Text(t) => {
525 let mut buf = SegmentBuffer::new();
526 buf.extend([t.prefix.clone(), t.line.clone()]);
527 *line = Line::Raw(RawLine { data: buf });
528 }
529 Line::Annotation(t) => {
530 let mut buf = SegmentBuffer::new();
531 buf.extend([t.prefix.clone(), t.line.clone()]);
532 *line = Line::Raw(RawLine { data: buf })
533 }
534 Line::Gap(t) => {
535 let mut buf = SegmentBuffer::new();
536 buf.extend([t.prefix.clone(), t.line.clone()]);
537 *line = Line::Raw(RawLine { data: buf })
538 }
539 Line::Raw(_) | Line::Nop => {}
540 }
541 }
542 }
543 cleanup(source);
544}
545
546fn linestarts(str: &str) -> BTreeSet<usize> {
547 let mut linestarts = BTreeSet::new();
548 for (i, c) in str.chars().enumerate() {
549 if c == '\n' {
550 linestarts.insert(i + 1);
551 }
552 }
553 linestarts
554}
555struct LineCol {
556 line: usize,
557 column: usize,
558}
559fn offset_to_linecol(mut offset: usize, linestarts: &BTreeSet<usize>) -> LineCol {
560 let mut line = 0;
561 let last_offset = linestarts
562 .range(..=offset)
563 .inspect(|_| line += 1)
564 .last()
565 .copied()
566 .unwrap_or(0);
567 offset -= last_offset;
568 LineCol {
569 line,
570 column: offset,
571 }
572}
573
574fn parse(txt: &str, annotations: &[Annotation], opts: &Opts) -> Source {
575 let (txt, byte_to_char_fixup) = fixup_byte_to_char(txt, opts.tab_width);
576 let mut annotations = annotations.to_vec();
577
578 for annotation in annotations.iter_mut() {
580 let ranges: RangeSet<usize> = annotation
581 .ranges
582 .ranges()
583 .map(|r| {
584 let mut start = r.start;
585 let mut end = r.end;
586 apply_fixup(&mut start, &byte_to_char_fixup);
587 apply_fixup(&mut end, &byte_to_char_fixup);
588 Range::new(start, end)
589 })
590 .collect();
591 annotation.ranges = ranges;
592 }
593 let linestarts = linestarts(&txt);
594
595 let mut lines: Vec<Line> = txt
596 .split('\n')
597 .map(|s| s.to_string())
598 .enumerate()
599 .map(|(num, line)| TextLine {
600 line_num: num + 1,
601 line: SegmentBuffer::segment(
602 line.chars().chain([' ']).collect::<String>(),
604 Formatting::default(),
605 ),
606 annotation: None,
607 prefix: SegmentBuffer::new(),
608 annotations: Vec::new(),
609 bottom_annotations: Vec::new(),
610 top_annotations: Vec::new(),
611 fold: true,
612 })
613 .map(Line::Text)
614 .collect();
615
616 for (aid, annotation) in annotations.iter().enumerate() {
617 let mut line_ranges: BTreeMap<usize, RangeSet<usize>> = BTreeMap::new();
618 for range in annotation.ranges.ranges() {
619 let start = offset_to_linecol(range.start, &linestarts);
620 let end = offset_to_linecol(range.end, &linestarts);
621
622 if start.line == end.line {
623 let set = line_ranges.entry(start.line).or_insert_with(RangeSet::new);
624 *set = set.union(&[Range::new(start.column, end.column)].into_iter().collect());
625 } else {
626 {
627 let set = line_ranges.entry(start.line).or_insert_with(RangeSet::new);
628 let line = lines[start.line].as_text().expect("annotation OOB");
629 *set = set.union(
630 &[Range::new(start.column, line.len() - 1)]
631 .into_iter()
632 .collect(),
633 );
634 }
635 {
636 let set = line_ranges.entry(end.line).or_insert_with(RangeSet::new);
637 *set = set.union(&[Range::new(0, end.column)].into_iter().collect());
638 }
639 }
640 }
641 let left = line_ranges.len() > 1;
642 let line_ranges_len = line_ranges.len();
643
644 for (i, (line, ranges)) in line_ranges.into_iter().enumerate() {
645 let last = i == line_ranges_len - 1;
646 let line = lines[line].as_text_mut().expect("annotation OOB");
647 line.annotations.push(LineAnnotation {
648 id: AnnotationId(aid),
649 priority: annotation.priority,
650 ranges,
651 formatting: annotation.formatting.clone(),
652 left,
653 right: if last {
654 annotation.text.clone()
655 } else {
656 Text::new()
657 },
658 });
659 line.fold = false;
660 }
661 }
662
663 let mut source = Source { lines };
664
665 let annotation_formats = annotations
666 .iter()
667 .enumerate()
668 .map(|(aid, a)| (AnnotationId(aid), a.formatting.clone()))
669 .collect();
670
671 process(&mut source, annotation_formats, opts);
672
673 source
674}
675
676pub fn source_to_ansi(source: &Source) -> String {
677 let mut out = String::new();
678 for line in &source.lines {
679 let line = line
680 .as_raw()
681 .expect("after processing all lines should turn raw");
682 let data = line.data.clone();
683 formatting::text_to_ansi(&data, &mut out);
684 out.push('\n');
685 }
686 out
687}
688
689pub struct FormattingGenerator {
690 rand: SmallRng,
691}
692impl FormattingGenerator {
693 pub fn new(src: &[u8]) -> Self {
694 let mut rng_seed = [0; 8];
695 for chunk in src.chunks(8) {
697 for (s, c) in rng_seed.iter_mut().zip(chunk.iter()) {
698 *s ^= *c;
699 }
700 }
701
702 Self {
703 rand: SmallRng::seed_from_u64(u64::from_be_bytes(rng_seed)),
704 }
705 }
706 fn next(&mut self) -> RandomColor {
707 let mut color = RandomColor::new();
708 color.seed(self.rand.gen::<u64>());
709 color.luminosity(Luminosity::Bright);
710 color
711 }
712}
713
714pub struct SnippetBuilder {
715 src: String,
716 generator: FormattingGenerator,
717 annotations: Vec<Annotation>,
718}
719impl SnippetBuilder {
720 pub fn new(src: impl AsRef<str>) -> Self {
721 Self {
722 src: src.as_ref().to_string(),
723 generator: FormattingGenerator::new(src.as_ref().as_bytes()),
724 annotations: Vec::new(),
725 }
726 }
727 fn custom(&mut self, custom_color: Gamut, text: Text) -> AnnotationBuilder<'_> {
728 let mut color = self.generator.next();
729 color.hue(custom_color);
730 let formatting = Formatting::rgb(color.to_rgb_array());
731 AnnotationBuilder {
738 snippet: self,
739 priority: 0,
740 formatting,
741 ranges: Vec::new(),
742 text,
743 }
744 }
745 pub fn error(&mut self, text: Text) -> AnnotationBuilder<'_> {
746 self.custom(Gamut::Red, text)
747 }
748 pub fn warning(&mut self, text: Text) -> AnnotationBuilder<'_> {
749 self.custom(Gamut::Orange, text)
750 }
751 pub fn note(&mut self, text: Text) -> AnnotationBuilder<'_> {
752 self.custom(Gamut::Green, text)
753 }
754 pub fn info(&mut self, text: Text) -> AnnotationBuilder<'_> {
755 self.custom(Gamut::Blue, text)
756 }
757 pub fn build(self) -> Source {
758 parse(
759 &self.src,
760 &self.annotations,
761 &Opts {
762 apply_to_orig: true,
763 fold: true,
764 tab_width: 4,
765 context_lines: 2,
766 },
767 )
768 }
769}
770
771#[must_use]
772pub struct AnnotationBuilder<'s> {
773 snippet: &'s mut SnippetBuilder,
774 priority: usize,
775 formatting: Formatting,
776 ranges: Vec<Range<usize>>,
777 text: Text,
778}
779
780impl AnnotationBuilder<'_> {
781 pub fn range(mut self, range: RangeInclusive<usize>) -> Self {
782 assert!(
783 *range.end() < self.snippet.src.len(),
784 "out of bounds annotation"
785 );
786 self.ranges.push(Range::new(*range.start(), *range.end()));
787 self
788 }
789 pub fn ranges(mut self, ranges: impl IntoIterator<Item = RangeInclusive<usize>>) -> Self {
790 for range in ranges {
791 self = self.range(range);
792 }
793 self
794 }
795 pub fn build(self) {
796 self.snippet.annotations.push(Annotation {
797 priority: self.priority,
798 formatting: self.formatting,
799 ranges: self.ranges.into_iter().collect(),
800 text: self.text,
801 });
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808
809 fn default<T: Default>() -> T {
810 Default::default()
811 }
812
813 #[test]
814 fn readme() {
815 let mut snippet = SnippetBuilder::new(include_str!("../../../fixtures/std.jsonnet"));
816 snippet
817 .error(Text::segment("Local defs", default()))
818 .ranges([4..=8, 3142..=3146])
819 .build();
820 snippet
821 .warning(Text::segment("Local name", default()))
822 .range(10..=12)
823 .build();
824 snippet
825 .info(Text::segment("Equals", default()))
826 .range(14..=14)
827 .build();
828 snippet
829 .note(Text::segment("Connected definition", default()))
830 .ranges([3133..=3135, 6155..=6157])
831 .build();
832 snippet
833 .note(Text::segment("Another connected definition", default()))
834 .ranges([5909..=5913, 6062..=6066, 6242..=6244])
835 .build();
836 let s = snippet.build();
837 println!("{}", source_to_ansi(&s))
838 }
839
840 #[test]
841 fn test_fmt() {
842 let mut snippet = SnippetBuilder::new(include_str!("../../../fixtures/std.jsonnet"));
843 snippet
844 .info(Text::segment("Hello world", default()))
845 .range(2832..=3135)
846 .build();
847 snippet
848 .warning(Text::segment("Conflict", default()))
849 .range(2838..=2847)
850 .build();
851 snippet
852 .error(Text::segment("Still has text", default()))
853 .range(2839..=2846)
854 .build();
855 let s = snippet.build();
856 println!("{}", source_to_ansi(&s))
857 }
858
859 #[test]
860 fn fullwidth_marker() {
861 let mut snippet = SnippetBuilder::new("ABC");
862 snippet
863 .info(Text::segment("a", default()))
864 .range(0..=2)
865 .build();
866 snippet
867 .info(Text::segment("b", default()))
868 .range(3..=5)
869 .build();
870 snippet
871 .info(Text::segment("c", default()))
872 .range(6..=8)
873 .build();
874 let s = snippet.build();
875 println!("{}", source_to_ansi(&s))
876 }
877
878 #[test]
879 fn fullwidth_marker_apply() {
880 let s = parse(
881 "ABC",
882 &[
883 Annotation {
884 priority: 0,
885 formatting: Formatting::color(0xff000000),
886 ranges: [Range::new(0, 2)].into_iter().collect(),
887 text: Text::segment("a", default()),
888 },
889 Annotation {
890 priority: 0,
891 formatting: Formatting::color(0x00ff0000),
892 ranges: [Range::new(3, 5)].into_iter().collect(),
893 text: Text::segment("b", default()),
894 },
895 Annotation {
896 priority: 0,
897 formatting: Formatting::color(0x0000ff00),
898 ranges: [Range::new(6, 8)].into_iter().collect(),
899 text: Text::segment("c", default()),
900 },
901 ],
902 &Opts {
903 apply_to_orig: true,
904 fold: true,
905 tab_width: 4,
906 context_lines: 2,
907 },
908 );
909 println!("{}", source_to_ansi(&s))
910 }
911
912 #[test]
913 fn tab_in_normal_and_fullwidth() {
914 let s = parse(
915 "A\tB\n\tB\na\tb\n\tb",
916 &[
917 Annotation {
918 priority: 0,
919 formatting: Formatting::color(0xff000000),
920 ranges: [Range::new(17, 17)].into_iter().collect(),
921 text: Text::segment("Line start", default()),
922 },
923 Annotation {
924 priority: 0,
925 formatting: Formatting::color(0x00ff0000),
926 ranges: [Range::new(18, 18)].into_iter().collect(),
927 text: Text::segment("Aligned", default()),
928 },
929 ],
930 &Opts {
931 apply_to_orig: false,
932 fold: false,
933 tab_width: 4,
934 context_lines: 2,
935 },
936 );
937 dbg!(&s);
938 println!("{}", source_to_ansi(&s))
939 }
940
941 #[test]
942 fn example_from_annotate_snippets() {
943 let src = r#") -> Option<String> {
944 for ann in annotations {
945 match (ann.range.0, ann.range.1) {
946 (None, None) => continue,
947 (Some(start), Some(end)) if start > end_index => continue,
948 (Some(start), Some(end)) if start >= start_index => {
949 let label = if let Some(ref label) = ann.label {
950 format!(" {}", label)
951 } else {
952 String::from("")
953 };
954 return Some(format!(
955 "{}{}{}",
956 " ".repeat(start - start_index),
957 "^".repeat(end - start),
958 label
959 ));
960 }
961 _ => continue,
962 }
963 }"#;
964 let mut snippet = SnippetBuilder::new(src);
965 snippet
966 .error(Text::segment(
967 "expected `Option<String>` because of return type",
968 default(),
969 ))
970 .range(5..=18)
971 .build();
972 snippet
973 .note(Text::segment(
974 "expected enum `std::option::Option`",
975 default(),
976 ))
977 .range(22..=510)
978 .build();
979 let s = snippet.build();
980 println!("{}", source_to_ansi(&s))
981 }
982}