texlang/error/
display.rs

1use crate::token::trace::{self, SourceCodeTrace};
2use crate::{error, token};
3use std::collections::HashMap;
4use texcraft_stdext::color::{self, Colorize};
5
6pub fn format_error(
7    f: &mut std::fmt::Formatter<'_>,
8    err: &error::TracedTexError,
9) -> std::fmt::Result {
10    let root = err.error.as_ref();
11    let stack = &err.stack_trace;
12
13    let (error_line, immediate_command) = match root.kind() {
14        error::Kind::Token(s) => (err.token_traces.get(&s).unwrap(), stack.last()),
15        error::Kind::EndOfInput => (err.end_of_input_trace.as_ref().unwrap(), stack.last()),
16        error::Kind::FailedPrecondition => (
17            match err.error.source_code_trace_override() {
18                None => &stack.last().unwrap().trace,
19                Some(trace) => trace,
20            },
21            None,
22        ),
23    };
24
25    let line = PrimaryLine {
26        kind: PrimaryLineKind::Error,
27        source: error_line,
28        title: root.title(),
29        token_annotation: root.source_annotation(),
30        notes: root
31            .notes()
32            .iter()
33            .map(|n| n.trace(&err.token_traces))
34            .map(|n| format!("{n}"))
35            .collect(),
36    };
37    write!(f, "{line}")?;
38
39    if let Some(err) = immediate_command {
40        let line = PrimaryLine {
41            kind: PrimaryLineKind::Context,
42            source: &err.trace,
43            title: format!["this error occurred while {}:", err.context.action()],
44            token_annotation: "".into(),
45            notes: vec![format![
46                "this is the full stack trace of the error:\n\n{}",
47                ErrorStack(stack)
48            ]],
49        };
50        write!(f, "\n{line}")?;
51    }
52
53    Ok(())
54}
55
56#[derive(Debug)]
57struct PrimaryLine<'a> {
58    kind: PrimaryLineKind,
59    source: &'a trace::SourceCodeTrace,
60    title: String,
61    token_annotation: String,
62    notes: Vec<String>,
63}
64
65#[derive(Debug, Clone, Copy)]
66enum PrimaryLineKind {
67    Error,
68    Context,
69}
70
71impl PrimaryLineKind {
72    fn color(&self, s: &str) -> color::ColoredString {
73        match self {
74            PrimaryLineKind::Error => s.bright_red(),
75            PrimaryLineKind::Context => s.bright_yellow(),
76        }
77    }
78}
79
80impl std::fmt::Display for PrimaryLineKind {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        write!(
83            f,
84            "{}",
85            match self {
86                PrimaryLineKind::Error => "Error".bright_red(),
87                PrimaryLineKind::Context => "Context".bright_yellow(),
88            }
89            .bold()
90        )
91    }
92}
93
94impl<'a> std::fmt::Display for PrimaryLine<'a> {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        let margin_width = self.source.line_number.to_string().len() + 1;
97        let printer = Printer {
98            indent: margin_width,
99        };
100        writeln!(f, "{}: {}", self.kind, self.title.bold())?;
101        fmt_source_code_trace(&printer, f, self.source, &self.token_annotation, self.kind)?;
102
103        for (i, note) in self.notes.iter().enumerate() {
104            let mut note_lines = note.trim_end().lines();
105            let first_note_line = match note_lines.next() {
106                None => continue,
107                Some(s) => s,
108            };
109            printer.new_line().print(f)?;
110            printer
111                .new_line()
112                .with_separator('=')
113                .with_content(format!["{} {}", "note:".bold(), first_note_line])
114                .print(f)?;
115            for line in note_lines {
116                let mut l = printer.new_line().with_content(format!["      {line}"]);
117                if i == self.notes.len() - 1 {
118                    l = l.with_separator(' ');
119                }
120                l.print(f)?;
121            }
122        }
123
124        Ok(())
125    }
126}
127
128struct ErrorStack<'a>(&'a Vec<error::StackTraceElement>);
129
130impl<'a> std::fmt::Display for ErrorStack<'a> {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        for (u, propagated) in self.0.iter().rev().enumerate() {
133            if u > 0 {
134                writeln!(f)?;
135            }
136            let (s, p) = (&propagated.trace, &propagated.context);
137            fmt_source_code_trace_light(f, s, 2, PrimaryLineKind::Context, p.action())?;
138        }
139        Ok(())
140    }
141}
142
143struct Printer {
144    indent: usize,
145}
146
147struct PrintLineBuilder<'a> {
148    printer: &'a Printer,
149    margin_content: String,
150    separator: Option<char>,
151    content: String,
152    indent_adjustment: usize,
153}
154
155impl Printer {
156    fn new_line(&self) -> PrintLineBuilder<'_> {
157        PrintLineBuilder {
158            printer: self,
159            margin_content: "".into(),
160            separator: Some('|'),
161            content: "".into(),
162            indent_adjustment: 0,
163        }
164    }
165}
166
167impl<'a> PrintLineBuilder<'a> {
168    fn with_content<T: Into<String>>(mut self, content: T) -> Self {
169        self.content = content.into();
170        self
171    }
172    fn with_margin_content<T: Into<String>>(mut self, content: T) -> Self {
173        self.margin_content = content.into();
174        self
175    }
176    fn with_indent_adjustment(mut self, u: usize) -> Self {
177        self.indent_adjustment = u;
178        self
179    }
180    fn with_separator(mut self, c: char) -> Self {
181        self.separator = Some(c);
182        self
183    }
184    fn without_separator(mut self) -> Self {
185        self.separator = None;
186        self
187    }
188
189    fn print(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        let indent: usize = self
191            .printer
192            .indent
193            .saturating_sub(self.indent_adjustment)
194            .saturating_sub(self.margin_content.len() + 1);
195        let margin_content = format!["{}{} ", " ".repeat(indent), self.margin_content];
196        let separator = match self.separator {
197            None => "".to_string(),
198            Some(c) => format!["{c} "],
199        };
200        writeln!(
201            f,
202            "{}{}{}",
203            margin_content.bright_cyan(),
204            separator.bright_cyan(),
205            self.content
206        )
207    }
208}
209
210fn fmt_source_code_trace(
211    printer: &Printer,
212    f: &mut std::fmt::Formatter<'_>,
213    s: &trace::SourceCodeTrace,
214    annotation: &str,
215    line_kind: PrimaryLineKind,
216) -> std::fmt::Result {
217    printer
218        .new_line()
219        .without_separator()
220        .with_indent_adjustment(1)
221        .with_content(format!(
222            "{} {}:{}:{}",
223            ">>>".bright_cyan().bold(),
224            match &s.origin {
225                trace::Origin::File(file_name) => format!["{}", file_name.display()],
226                trace::Origin::Terminal => "<input from terminal>".to_string(),
227            },
228            s.line_number,
229            s.index + 1
230        ))
231        .print(f)?;
232    printer.new_line().print(f)?;
233    printer
234        .new_line()
235        .with_margin_content(format!["{}", s.line_number])
236        .with_content(highlight_substring(&s.line_content, s.index, s.value.len()))
237        .print(f)?;
238    printer
239        .new_line()
240        .with_content(format![
241            "{}{} {}",
242            " ".repeat(s.index),
243            line_kind.color(&"^".repeat(s.value.len())).bold(),
244            line_kind.color(annotation).bold(),
245        ])
246        .print(f)?;
247    Ok(())
248}
249
250fn highlight_substring(line: &str, start: usize, length: usize) -> String {
251    if line.len() < start + length {
252        return line.into();
253    }
254    format![
255        "{}{}{}",
256        &line[..start],
257        (&line[start..start + length]).bold(),
258        line[start + length..].trim_end(),
259    ]
260}
261
262fn fmt_source_code_trace_light(
263    f: &mut std::fmt::Formatter<'_>,
264    s: &SourceCodeTrace,
265    indent: usize,
266    line_kind: PrimaryLineKind,
267    annotation: &str,
268) -> std::fmt::Result {
269    let prefix = format!(
270        "{}{}:{}:{}",
271        " ".repeat(indent),
272        match &s.origin {
273            trace::Origin::File(file_name) => format!["{}", file_name.display()],
274            trace::Origin::Terminal => "<input from terminal>".to_string(),
275        },
276        s.line_number,
277        s.index + 1
278    );
279    writeln!(
280        f,
281        "{}  {}",
282        prefix,
283        highlight_substring(&s.line_content, s.index, s.value.len())
284    )?;
285    writeln!(
286        f,
287        "{}  {} {}",
288        " ".repeat(prefix.len() + s.index),
289        line_kind.color(&"^".repeat(s.value.len())).bold(),
290        annotation,
291    )?;
292    Ok(())
293}
294
295#[derive(Clone, Debug)]
296#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
297pub enum Note {
298    Text(String),
299    SourceCodeTrace(String, token::Token),
300}
301
302impl Note {
303    fn trace<'a>(&'a self, m: &'a HashMap<token::Token, trace::SourceCodeTrace>) -> TracedNote<'a> {
304        match self {
305            Note::Text(t) => TracedNote::Text(t),
306            Note::SourceCodeTrace(s, token) => TracedNote::SourceCodeTrace(
307                s,
308                m.get(token)
309                    .expect("all tokens in the error have been traced"),
310            ),
311        }
312    }
313}
314
315impl<T: Into<String>> From<T> for Note {
316    fn from(value: T) -> Self {
317        Note::Text(value.into())
318    }
319}
320
321#[derive(Debug)]
322enum TracedNote<'a> {
323    Text(&'a str),
324    SourceCodeTrace(&'a str, &'a trace::SourceCodeTrace),
325}
326
327impl<'a> std::fmt::Display for TracedNote<'a> {
328    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329        match self {
330            TracedNote::Text(s) => write!(f, "{s}"),
331            TracedNote::SourceCodeTrace(s, trace) => {
332                write!(f, "{s}\n\n")?;
333                fmt_source_code_trace_light(f, trace, 2, PrimaryLineKind::Error, "")
334            }
335        }
336    }
337}