geo_aid_script/cli.rs
1//! Utilities for console printing of diagnostics.
2
3use std::{collections::BTreeMap, fmt::Display, path::Path};
4
5use crossterm::style::{Color, Stylize};
6
7use crate::token::{Position, Span};
8
9/// The kind of a diagnostic
10#[derive(Debug, Clone, Copy)]
11pub enum DiagnosticKind {
12 /// A language error
13 Error,
14 /// Some information
15 Note,
16 /// A suggestion
17 Help,
18}
19
20/// Get the terminal color for the given diagnostic kind.
21#[must_use]
22pub fn get_color(kind: DiagnosticKind) -> Color {
23 match kind {
24 DiagnosticKind::Error => Color::Red,
25 DiagnosticKind::Note => Color::Blue,
26 DiagnosticKind::Help => Color::Yellow,
27 // UnderscoreType::Warning => Color::Yellow,
28 // UnderscoreType::Info => Color::Cyan,
29 }
30}
31
32/// The kind of an annotation
33#[derive(Debug, Clone, Copy)]
34pub enum AnnotationKind {
35 /// Additional context
36 Note,
37 /// A helpful suggestion, e.g. a fix
38 Help,
39}
40
41/// A set of annotations typically attached to a main diagnostic
42#[derive(Debug)]
43struct AnnotationSet {
44 span: Span,
45 annotations: Vec<AnnotationPrivate>,
46}
47
48/// Annotation data
49#[derive(Debug)]
50struct AnnotationPrivate {
51 /// The annotation message
52 message: String,
53 /// The annotation kind
54 kind: AnnotationKind,
55}
56
57/// An annotation providing additional information or help
58/// regarding a diagnostic
59pub struct Annotation {
60 /// What kind of a diagnostic this is
61 pub kind: AnnotationKind,
62 /// The annotation's message
63 pub message: String,
64 /// The span this diagnostic relates to
65 pub at: Span,
66}
67
68/// A suggested change in code
69#[derive(Debug, Clone)]
70pub struct Change {
71 /// The span to be replaced
72 pub span: Span,
73 /// The new contents.
74 ///
75 /// Instead of a simple string, we use a vector of lines to make the
76 /// rendering process more reliable.
77 pub new_content: Vec<String>,
78}
79
80/// A suggested fix for a diagnostic
81#[derive(Debug)]
82pub struct Fix {
83 /// Changes that this fix introduces
84 pub changes: Vec<Change>,
85 /// A message describing the suggested fix
86 pub message: String,
87}
88
89/// A diagnostic
90pub struct Diagnostic<'r> {
91 /// The kind of this diagnostic
92 kind: DiagnosticKind,
93 /// Annotations provided along the diagnostic
94 annotations: Vec<AnnotationSet>,
95 /// Notes (spanless annotations) provided along the diagnostic
96 notes: Vec<(AnnotationKind, String)>,
97 /// The main diagnostic message.
98 message: String,
99 /// Notes with a span. They're the same as annotations, but
100 /// show up separately from the main diagnostic view.
101 /// This field cannot be manually constructed and is
102 /// automatically filled when two annotations have overlapping spans.
103 annotated_notes: BTreeMap<Position, Diagnostic<'r>>,
104 /// Suggested fixes
105 fixes: Vec<Fix>,
106 /// The source of this code.
107 file: &'r Path,
108 /// The code itself.
109 script: &'r str,
110}
111
112/// The data used to construct a diagnostic.
113pub struct DiagnosticData {
114 /// The main diagnostic message.
115 pub message: String,
116 /// Diagnostic's spans
117 pub spans: Vec<Span>,
118 /// Annotations provided along the diagnostics
119 pub annotations: Vec<Annotation>,
120 /// Spanless notes provided along the diagnostic
121 pub notes: Vec<(AnnotationKind, String)>,
122 /// Suggested fixes for the diagnostic
123 pub fixes: Vec<Fix>,
124}
125
126impl DiagnosticData {
127 /// Creates an empty diagnostic with just a message.
128 #[must_use]
129 pub fn new<S: ToString + ?Sized>(message: &S) -> Self {
130 Self {
131 message: message.to_string(),
132 spans: Vec::new(),
133 annotations: Vec::new(),
134 notes: Vec::new(),
135 fixes: Vec::new(),
136 }
137 }
138
139 /// Add a note (spanless annotation) to the diagnostic.
140 #[must_use]
141 pub fn add_note(mut self, kind: AnnotationKind, message: String) -> Self {
142 self.notes.push((kind, message));
143 self
144 }
145
146 /// Add a span for the diagnostic
147 #[must_use]
148 pub fn add_span(mut self, sp: Span) -> Self {
149 self.spans.push(sp);
150 self
151 }
152
153 /// Add an annotation (message with a span).
154 #[must_use]
155 pub fn add_annotation<S: ToString + ?Sized>(
156 mut self,
157 at: Span,
158 kind: AnnotationKind,
159 message: &S,
160 ) -> Self {
161 self.annotations.push(Annotation {
162 kind,
163 message: message.to_string(),
164 at,
165 });
166 self
167 }
168
169 /// Add an annotation if `message` is `Some`
170 #[must_use]
171 pub fn add_annotation_opt_msg<S: ToString + ?Sized>(
172 mut self,
173 at: Span,
174 kind: AnnotationKind,
175 message: Option<&S>,
176 ) -> Self {
177 if let Some(message) = message {
178 self.annotations.push(Annotation {
179 kind,
180 message: message.to_string(),
181 at,
182 });
183 }
184 self
185 }
186
187 /// Add an annotation if `span` is `Some`
188 #[must_use]
189 pub fn add_annotation_opt_span<S: ToString + ?Sized>(
190 mut self,
191 at: Option<Span>,
192 kind: AnnotationKind,
193 message: &S,
194 ) -> Self {
195 if let Some(at) = at {
196 self.annotations.push(Annotation {
197 kind,
198 message: message.to_string(),
199 at,
200 });
201 }
202 self
203 }
204
205 /// Suggest a fix
206 #[must_use]
207 pub fn add_fix(mut self, fix: Fix) -> Self {
208 self.fixes.push(fix);
209 self
210 }
211}
212
213impl<'r> Diagnostic<'r> {
214 /// Prepare all of the diagnostic's spans
215 fn find_spans(
216 spans: Vec<Span>,
217 kind: DiagnosticKind,
218 message: &str,
219 file: &'r Path,
220 script: &'r str,
221 ) -> (Vec<AnnotationSet>, BTreeMap<Position, Diagnostic<'r>>) {
222 let mut annotation_sets: Vec<AnnotationSet> = Vec::new();
223 let mut annotated_notes = BTreeMap::new();
224
225 for at in spans {
226 let mut index = 0;
227 let mut insert_at: u8 = 2;
228
229 for (i, set) in annotation_sets.iter_mut().enumerate() {
230 if set.span.overlaps(at) {
231 set.span = set.span.join(at);
232 insert_at = 0;
233 break;
234 }
235
236 if !at.is_single_line()
237 && set.span.is_single_line()
238 && at.end.line == set.span.start.line
239 {
240 // Banish the span
241 annotated_notes.insert(
242 at.start,
243 Diagnostic::new(
244 kind,
245 DiagnosticData {
246 message: String::from(message),
247 spans: vec![at],
248 annotations: Vec::new(),
249 notes: Vec::new(),
250 fixes: Vec::new(),
251 },
252 file,
253 script,
254 ),
255 );
256 insert_at = 0;
257 break;
258 }
259
260 if at.start > set.span.start {
261 index = i;
262 } else {
263 insert_at = 1;
264 break;
265 }
266 }
267
268 match insert_at {
269 0 => (),
270 1 => annotation_sets.insert(
271 index,
272 AnnotationSet {
273 span: at,
274 annotations: Vec::new(),
275 },
276 ),
277 2 => annotation_sets.push(AnnotationSet {
278 span: at,
279 annotations: Vec::new(),
280 }),
281 _ => unreachable!(),
282 }
283 }
284
285 (annotation_sets, annotated_notes)
286 }
287
288 /// Create a new diagnostic based on diagnostic data, a source and a piece of code.
289 #[must_use]
290 pub fn new(
291 kind: DiagnosticKind,
292 data: DiagnosticData,
293 file: &'r Path,
294 script: &'r str,
295 ) -> Self {
296 let (mut annotation_sets, mut annotated_notes) =
297 Diagnostic::find_spans(data.spans, kind, &data.message, file, script);
298
299 for ann in data.annotations {
300 let mut index = 0;
301 let mut insert_at: u8 = 2;
302
303 for (i, annotation) in annotation_sets.iter_mut().enumerate() {
304 if annotation.span == ann.at {
305 // If there's a common span, attach both messages to one span.
306 annotation.annotations.push(AnnotationPrivate {
307 message: ann.message.clone(),
308 kind: ann.kind,
309 });
310
311 insert_at = 0;
312 break;
313 } else if annotation.span.overlaps(ann.at) {
314 // If there is an overlap, banish the note to annotated_notes.
315 annotated_notes.insert(
316 ann.at.start,
317 Diagnostic::new(
318 match ann.kind {
319 AnnotationKind::Help => DiagnosticKind::Help,
320 AnnotationKind::Note => DiagnosticKind::Note,
321 },
322 DiagnosticData {
323 message: ann.message.clone(),
324 spans: vec![ann.at],
325 annotations: Vec::new(),
326 notes: Vec::new(),
327 fixes: Vec::new(),
328 },
329 file,
330 script,
331 ),
332 );
333
334 insert_at = 0;
335 break;
336 }
337
338 if ann.at.start > annotation.span.start {
339 index = i;
340 } else {
341 insert_at = 1;
342 break;
343 }
344 }
345
346 // If the function has not broken before, the annotation should be inserted at the given index.
347 match insert_at {
348 0 => (),
349 1 => annotation_sets.insert(
350 index,
351 AnnotationSet {
352 span: ann.at,
353 annotations: vec![AnnotationPrivate {
354 message: ann.message.clone(),
355 kind: ann.kind,
356 }],
357 },
358 ),
359 2 => annotation_sets.push(AnnotationSet {
360 span: ann.at,
361 annotations: vec![AnnotationPrivate {
362 message: ann.message.clone(),
363 kind: ann.kind,
364 }],
365 }),
366 _ => unreachable!(),
367 }
368 }
369
370 // Sort by span start position in reverse (first span is the last in the vector)
371 let fixes = data
372 .fixes
373 .into_iter()
374 .map(|mut v| {
375 v.changes.sort_unstable_by_key(|c| c.span.start);
376 v.changes.reverse();
377 v
378 })
379 .collect();
380
381 Diagnostic {
382 kind,
383 annotations: annotation_sets,
384 notes: Vec::new(),
385 message: data.message,
386 annotated_notes,
387 fixes,
388 file,
389 script,
390 }
391 }
392}
393
394impl Display for Diagnostic<'_> {
395 #[allow(clippy::too_many_lines)]
396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397 // Display the main message
398 writeln!(
399 f,
400 "{}{} {}",
401 match self.kind {
402 DiagnosticKind::Error => "error".red().bold(),
403 DiagnosticKind::Note => "note".blue().bold(),
404 DiagnosticKind::Help => "help".yellow().bold(),
405 },
406 ":".white().bold(),
407 self.message.clone().white().bold()
408 )?;
409 let vertical = "|".blue().bold();
410
411 // Only execute the rest if there are any annotations.
412 let note_indent = if let Some(last) = self.annotations.last() {
413 let first = self.annotations.first().unwrap();
414 // Calculate the necessary indent (how much space to leave for line numbers).
415 let indent = last.span.end.line.to_string().len();
416 // Write the file path
417 writeln!(
418 f,
419 "{:indent$}{} {}:{}:{}",
420 "",
421 "-->".blue().bold(),
422 self.file.display(),
423 first.span.start.line,
424 first.span.start.column
425 )?;
426
427 let mut lines = self.script.lines().skip(first.span.start.line - 1);
428
429 // First " | "
430 writeln!(f, "{:indent$} {vertical}", "")?;
431
432 let mut annotations = self.annotations.iter().peekable();
433
434 // Do for each annotation
435 while let Some(ann) = annotations.next() {
436 if ann.span.is_single_line() {
437 // If single line, only render the line of code
438 writeln!(
439 f,
440 "{:indent$} {vertical} {}",
441 ann.span.start.line.to_string().blue().bold(),
442 lines.next().unwrap()
443 )?;
444 // And simple squiggles
445 write!(
446 f,
447 "{:<indent$} {vertical} {:squiggle_offset$}{}",
448 "",
449 "",
450 "^".repeat(ann.span.end.column - ann.span.start.column)
451 .with(get_color(self.kind))
452 .bold(),
453 squiggle_offset = ann.span.start.column - 1
454 )?;
455 } else {
456 // Otherwise
457 if ann.span.start.column == 1 {
458 // If range starts at column 1, start with '/'
459 writeln!(
460 f,
461 "{:<indent$} {vertical} {} {}",
462 ann.span.start.line.to_string().blue().bold(),
463 "/".with(get_color(self.kind)).bold(),
464 lines.next().unwrap()
465 )?;
466 } else {
467 // Otherwise with an indicator of where the span starts
468 writeln!(
469 f,
470 "{:indent$} {vertical} {}",
471 ann.span.start.line.to_string().blue().bold(),
472 lines.next().unwrap()
473 )?;
474 writeln!(
475 f,
476 "{:indent$} {vertical} {}",
477 "",
478 (String::from(" ") + &"_".repeat(ann.span.start.column - 2) + "^")
479 .with(get_color(self.kind))
480 .bold()
481 )?;
482 }
483
484 // Then add '|' for each line in span.
485 for i in ann.span.start.line..ann.span.end.line {
486 writeln!(
487 f,
488 "{:<indent$} {vertical} {} {}",
489 (i + 1).to_string().blue().bold(),
490 "|".with(get_color(self.kind)).bold(),
491 lines.next().unwrap()
492 )?;
493 }
494
495 // And end with ending indicator.
496 write!(
497 f,
498 "{:indent$} {vertical} {}",
499 "",
500 (String::from("|") + &"_".repeat(ann.span.end.column - 1) + "^")
501 .with(get_color(self.kind))
502 .bold()
503 )?;
504 }
505
506 let mut messages: Vec<(usize, Vec<&AnnotationPrivate>)> = vec![(
507 ann.span.end.column,
508 ann.annotations
509 .iter()
510 .filter(|x| !x.message.is_empty())
511 .collect(),
512 )];
513
514 // While we can, we put squiggles in the same line.
515 let mut previous = ann;
516 let mut next_ann = annotations.peek().copied();
517 while let Some(next) = next_ann {
518 if next.span.start.line == previous.span.end.line {
519 annotations.next();
520 write!(
521 f,
522 "{:squiggle_offset$}{}",
523 "",
524 "^".repeat(next.span.end.column - next.span.start.column)
525 .with(get_color(self.kind))
526 .bold(),
527 squiggle_offset = next.span.start.column - previous.span.end.column
528 )?;
529
530 messages.push((
531 next.span.end.column,
532 next.annotations
533 .iter()
534 .filter(|x| !x.message.is_empty())
535 .collect(),
536 ));
537
538 previous = next;
539 next_ann = annotations.peek().copied();
540 } else {
541 break;
542 }
543 }
544
545 let l = messages.len();
546 let mut messages: Vec<(usize, Vec<&AnnotationPrivate>)> = messages
547 .into_iter()
548 .enumerate()
549 .filter(|(i, (_, x))| !x.is_empty() || *i == l - 1)
550 .map(|x| x.1)
551 .collect();
552
553 // We first dispatch the last message, since it can follow the squiggles immediately.
554 if let Some(v) = messages.last().unwrap().1.first() {
555 write!(
556 f,
557 "{}",
558 format!(
559 " {}: {}",
560 match v.kind {
561 AnnotationKind::Note => "note",
562 AnnotationKind::Help => "help",
563 },
564 v.message
565 )
566 .with(get_color(self.kind))
567 .bold()
568 )?;
569 }
570
571 if !messages.last_mut().unwrap().1.is_empty() {
572 messages.last_mut().unwrap().1.remove(0);
573 }
574 writeln!(f)?;
575
576 let mut offsets: Vec<usize> = messages.iter().map(|x| x.0).collect();
577 offsets.pop();
578
579 // And then for each annotation
580 for (offset, msg) in messages.into_iter().rev() {
581 // For each message.
582 for ann in msg {
583 write!(f, "{:indent$} {vertical} ", "")?;
584 let mut last = 1;
585
586 // We write the `|` for other messages.
587 for off in &offsets {
588 write!(
589 f,
590 "{:ind$}{}",
591 "",
592 "|".with(get_color(self.kind)).bold(),
593 ind = *off - last - 1
594 )?;
595 last = *off;
596 }
597
598 // And then write the message itself.
599 writeln!(
600 f,
601 "{:ind$} {}",
602 "",
603 format!(
604 " {}: {}",
605 match ann.kind {
606 AnnotationKind::Note => "note",
607 AnnotationKind::Help => "help",
608 },
609 ann.message
610 )
611 .with(get_color(self.kind))
612 .bold(),
613 ind = offset - last - 1
614 )?;
615 }
616
617 offsets.pop();
618 }
619
620 // Advance the source display
621 if let Some(next) = next_ann {
622 match next.span.start.line - previous.span.end.line {
623 1 => (),
624 2 => writeln!(
625 f,
626 "{:<indent$} {vertical} {}",
627 (previous.span.start.line - 1).to_string().blue().bold(),
628 lines.next().unwrap()
629 )?,
630 diff => {
631 for _ in 0..(diff - 1) {
632 lines.next();
633 }
634
635 writeln!(f, "{:indent$}{}", "", "...".blue().bold())?;
636 }
637 }
638 }
639 }
640
641 indent
642 } else {
643 0
644 };
645
646 // Display notes.
647 for note in &self.notes {
648 writeln!(
649 f,
650 "{:note_indent$} = {}",
651 "",
652 format!(
653 "{}: {}",
654 match ¬e.0 {
655 AnnotationKind::Note => "note",
656 AnnotationKind::Help => "help",
657 },
658 note.1
659 )
660 .bold()
661 )?;
662 }
663
664 // Display annotated notes
665 if !self.annotated_notes.is_empty() {
666 writeln!(f, "{:note_indent$} {vertical}", "")?;
667 }
668
669 for diagnostic in self.annotated_notes.values() {
670 write!(f, "{diagnostic}")?;
671 }
672
673 for fix in &self.fixes {
674 writeln!(
675 f,
676 "{} {}",
677 "Fix:".green().bold(),
678 fix.message.clone().bold(),
679 )?;
680
681 if let Some(last) = fix.changes.last() {
682 // Calculate the necessary indent (how much space to leave for line numbers).
683 let indent = last.span.end.line.to_string().len();
684 let first = fix.changes.first().unwrap();
685 // Write the file path
686 writeln!(
687 f,
688 "{:indent$}{} {}:{}:{}",
689 "",
690 "-->".blue().bold(),
691 self.file.display(),
692 first.span.start.line,
693 first.span.start.column
694 )?;
695
696 let mut lines = self.script.lines().skip(first.span.start.line - 1);
697
698 // First " | "
699 writeln!(f, "{:note_indent$} {vertical}", "")?;
700
701 // A fix is a message with vector of changes ordered in descending span start.
702 // So we will create a stack for those spans.
703 // In each iteration we will consider the top entry.
704 // If the entry's span is single-line, we simply print it
705 // Otherwise, we divide the span and push the two parts to the top of the stack,
706 // so that it is considered in the next iteration.
707 let mut changes = fix.changes.clone();
708
709 let mut current_line = lines.next().map(|s| s.chars().enumerate());
710 let mut signs = Vec::new();
711
712 // Until there is a top of the stack.
713 while let Some(change) = changes.pop() {
714 if !change.span.is_single_line() {
715 // If not single line, divide in two.
716 changes.push(Change {
717 span: Span {
718 start: Position {
719 line: change.span.start.line + 1,
720 column: 1,
721 },
722 end: change.span.end,
723 },
724 new_content: change.new_content,
725 });
726
727 changes.push(Change {
728 span: Span {
729 start: change.span.start,
730 end: Position {
731 line: change.span.start.line,
732 column: usize::MAX,
733 },
734 },
735 new_content: Vec::new(),
736 });
737 } else if let Some(chars) = current_line.as_mut() {
738 // Render the line and fill in `signs`.
739 write!(
740 f,
741 "{:<indent$} {vertical} ",
742 change.span.start.line.to_string().blue().bold()
743 )?;
744
745 // Render the line pre-change
746 if change.span.start.column > 1 {
747 for (i, c) in chars.by_ref() {
748 write!(f, "{c}")?;
749 signs.push(' ');
750
751 if i + 2 >= change.span.start.column {
752 // next character is too far, break
753 break;
754 }
755 }
756 }
757
758 // Render the deleted span as red.
759 if !change.span.is_empty() {
760 for (i, c) in chars.by_ref() {
761 write!(f, "{}", c.red())?;
762 signs.push('-');
763
764 if i + 2 >= change.span.end.column {
765 break;
766 }
767 }
768 }
769
770 let mut first = true;
771
772 // Render the added code.
773 for ln in &change.new_content {
774 // If it's not the first line of the added content, print a newline and signs
775 if !first {
776 writeln!(f)?;
777 write!(f, "{:indent$} {vertical} ", "")?;
778
779 for sign in &signs {
780 match sign {
781 '+' => write!(f, "{}", "+".green())?,
782 '-' => write!(f, "{}", "-".red())?,
783 _ => write!(f, " ")?,
784 }
785 }
786
787 writeln!(f)?;
788 write!(f, "{:indent$} {vertical} ", "")?;
789 }
790
791 // Print the line
792 write!(f, "{}", ln.as_str().green())?;
793
794 // And add signs
795 signs.resize(signs.len() + ln.chars().count(), '+');
796
797 first = false;
798 }
799
800 // Render the rest of the line and fill in `signs`.
801 for (_, c) in chars {
802 write!(f, "{c}")?;
803 signs.push(' ');
804 }
805 }
806
807 // Advance the source display
808 if let Some(next) = changes.last() {
809 if next.span.start.line != change.span.end.line {
810 // Render signs
811 writeln!(f)?; // Finish the line.
812 write!(f, "{:note_indent$} {vertical} ", "")?;
813
814 for sign in &signs {
815 match sign {
816 '+' => write!(f, "{}", "+".green())?,
817 '-' => write!(f, "{}", "-".red())?,
818 _ => write!(f, " ")?,
819 }
820 }
821 writeln!(f)?;
822 signs.clear();
823
824 match next.span.start.line - change.span.end.line {
825 1 => (),
826 2 => writeln!(
827 f,
828 "{:<note_indent$} {vertical} {}",
829 (change.span.start.line + 1).to_string().blue().bold(),
830 lines.next().unwrap()
831 )?,
832 diff => {
833 for _ in 0..(diff - 1) {
834 lines.next();
835 }
836
837 writeln!(f, "{:indent$}{}", "", "...".blue().bold())?;
838 }
839 }
840
841 current_line = lines.next().map(|s| s.chars().enumerate());
842 }
843 }
844 }
845 }
846 }
847
848 Ok(())
849 }
850}