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