1#![cfg(feature = "emit-diagnostics")]
4
5use std::collections::HashMap;
6use std::io;
7use std::ops::Range;
8
9use codespan_reporting::diagnostic::{Diagnostic, Label};
10use codespan_reporting::files::Files;
11use codespan_reporting::term;
12use termcolor::{Color, ColorSpec, WriteColor};
13use typst_library::World;
14use typst_library::diag::{FileError, Severity, SourceDiagnostic, Tracepoint};
15use typst_syntax::{DiagSpan, DiagSpanKind, FileId, Lines, Source, Spanned};
16
17type CodespanResult<T> = Result<T, CodespanError>;
18type CodespanError = codespan_reporting::files::Error;
19
20pub use term::termcolor;
21
22pub trait DiagnosticWorld: World {
24 fn name(&self, id: FileId) -> String;
28}
29
30#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
32pub enum DiagnosticFormat {
33 #[default]
35 Human,
36 Short,
38}
39
40pub fn emit<'a>(
42 dest: &mut dyn WriteColor,
43 world: &dyn DiagnosticWorld,
44 diagnostics: impl IntoIterator<Item = &'a SourceDiagnostic>,
45 format: DiagnosticFormat,
46) -> Result<(), codespan_reporting::files::Error> {
47 let mut files = WorldFiles { world, sources: HashMap::new() };
48
49 let mut config = term::Config { tab_width: 2, ..Default::default() };
50 if format == DiagnosticFormat::Short {
51 config.display_style = term::DisplayStyle::Short;
52 }
53
54 for diagnostic in diagnostics {
55 let diag = match diagnostic.severity {
56 Severity::Error => Diagnostic::error(),
57 Severity::Warning => Diagnostic::warning(),
58 }
59 .with_message(diagnostic.message.clone())
60 .with_notes(
61 diagnostic
62 .hints
63 .iter()
64 .filter(|s| s.span.is_detached())
65 .map(|s| format!("hint: {}", s.v))
66 .collect(),
67 )
68 .with_labels(
69 diagnostic
70 .span
71 .id()
72 .and_then(|id| {
73 let range = files.range(diagnostic.span)?;
74 Some(Label::primary(id, range))
75 })
76 .into_iter()
77 .chain(diagnostic.hints.iter().filter_map(|hint| {
78 let id = hint.span.id()?;
79 let range = files.range(hint.span)?;
80 Some(Label::secondary(id, range).with_message(&hint.v))
81 }))
82 .collect(),
83 );
84
85 term::emit(dest, &config, &files, &diag)?;
86
87 if format == DiagnosticFormat::Human {
89 let mut traced = false;
90 for point in &diagnostic.trace {
91 emit_trace(dest, &mut files, point)?;
92 traced = true;
93 }
94
95 if traced {
96 writeln!(dest)?;
97 }
98 }
99 }
100
101 Ok(())
102}
103
104fn emit_trace(
106 dest: &mut dyn WriteColor,
107 files: &mut WorldFiles,
108 point: &Spanned<Tracepoint>,
109) -> Result<(), codespan_reporting::files::Error> {
110 let Some(id) = point.span.id() else { return Ok(()) };
111 let Some(range) = files.range(point.span) else { return Ok(()) };
112 let lines = files.lines(id)?;
113
114 let name = files.name(id)?;
115 let line_index = files.line_index(id, range.start)?;
116 let line = files.line_number(id, line_index)?;
117 let column = files.column_number(id, line_index, range.start)?;
118 let text = &lines.text()[range];
119
120 write!(dest, " {} at ", point.v)?;
122 dest.set_color(ColorSpec::new().set_underline(true))?;
123 write!(dest, "{name}:{line}:{column}")?;
124 dest.reset()?;
125 writeln!(dest)?;
126
127 let mut lines = text.lines();
129 write!(dest, " ")?;
130 dest.set_color(ColorSpec::new().set_fg(Some(Color::Ansi256(248))))?;
131 if let Some(first) = lines.next() {
132 write!(dest, "{first}")?;
133 }
134 if let Some(last) = lines.next_back()
135 && let Some(last_char) = last.chars().next_back()
136 && !last_char.is_whitespace()
137 {
138 write!(dest, "…{last_char}")?;
141 }
142 dest.reset()?;
143 writeln!(dest)?;
144
145 Ok(())
146}
147
148struct WorldFiles<'a> {
150 world: &'a dyn DiagnosticWorld,
151 sources: HashMap<FileId, Source>,
152}
153
154impl WorldFiles<'_> {
155 fn range(&mut self, span: impl Into<DiagSpan>) -> Option<Range<usize>> {
158 match span.into().get() {
159 DiagSpanKind::Detached => None,
160 DiagSpanKind::Number { id, num, sub_range } => {
161 let source = self.world.source(id).ok()?;
162 let range = source.range(num, sub_range);
163 self.sources.entry(id).or_insert(source);
164 range
165 }
166 DiagSpanKind::Range { id: _, range } => Some(range),
167 }
168 }
169
170 fn lines(&self, id: FileId) -> CodespanResult<Lines<String>> {
173 match self.sources.get(&id) {
174 Some(source) => Ok(source.lines().clone()),
175 None => self
176 .world
177 .file(id)
178 .and_then(|file| file.lines().map_err(Into::into))
179 .map_err(|err| match err {
180 FileError::NotFound(_) => CodespanError::FileMissing,
181 other => CodespanError::Io(io::Error::other(other)),
182 }),
183 }
184 }
185}
186
187impl<'a> Files<'a> for WorldFiles<'_> {
188 type FileId = FileId;
189 type Name = String;
190 type Source = Lines<String>;
191
192 fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> {
193 Ok(self.world.name(id))
194 }
195
196 fn source(&'a self, id: FileId) -> CodespanResult<Self::Source> {
197 self.lines(id)
198 }
199
200 fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult<usize> {
201 let lines = self.lines(id)?;
202 lines
203 .byte_to_line(given)
204 .ok_or_else(|| CodespanError::IndexTooLarge { given, max: lines.len_bytes() })
205 }
206
207 fn line_range(
208 &'a self,
209 id: FileId,
210 given: usize,
211 ) -> CodespanResult<std::ops::Range<usize>> {
212 let lines = self.lines(id)?;
213 lines
214 .line_to_range(given)
215 .ok_or_else(|| CodespanError::LineTooLarge { given, max: lines.len_lines() })
216 }
217
218 fn column_number(
219 &'a self,
220 id: FileId,
221 _: usize,
222 given: usize,
223 ) -> CodespanResult<usize> {
224 let lines = self.lines(id)?;
225 lines.byte_to_column(given).ok_or_else(|| {
226 let max = lines.len_bytes();
227 if given <= max {
228 CodespanError::InvalidCharBoundary { given }
229 } else {
230 CodespanError::IndexTooLarge { given, max }
231 }
232 })
233 }
234}