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 &note.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}