1use miette::{Diagnostic, LabeledSpan, Severity};
2use owo_colors::OwoColorize;
3use std::fmt;
4use std::sync::Arc;
5
6use syntax::ParseError;
7use syntax::ast::Span;
8
9#[derive(Clone, Debug)]
11pub struct IndexedSource {
12 source: Arc<str>,
13 line_starts: Arc<[usize]>,
14}
15
16impl IndexedSource {
17 pub fn new(s: &str) -> Self {
18 let mut line_starts = vec![0usize];
19 for (i, byte) in s.bytes().enumerate() {
20 if byte == b'\n' {
21 line_starts.push(i + 1);
22 }
23 }
24 Self {
25 source: Arc::from(s),
26 line_starts: Arc::from(line_starts),
27 }
28 }
29}
30
31impl miette::SourceCode for IndexedSource {
32 fn read_span<'a>(
33 &'a self,
34 span: &miette::SourceSpan,
35 context_lines_before: usize,
36 context_lines_after: usize,
37 ) -> Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
38 let src = self.source.as_ref();
39 let offset = span.offset();
40 let len = span.len();
41
42 if offset + len > src.len() {
43 return Err(miette::MietteError::OutOfBounds);
44 }
45
46 let span_line = match self.line_starts.binary_search(&offset) {
47 Ok(exact) => exact,
48 Err(idx) => idx.saturating_sub(1),
49 };
50
51 let start_line = span_line.saturating_sub(context_lines_before);
52 let start_offset = self.line_starts[start_line];
53 let start_column = if context_lines_before == 0 {
54 offset - self.line_starts[span_line]
55 } else {
56 0
57 };
58
59 let span_end = offset + len.saturating_sub(1);
60 let end_line = match self.line_starts.binary_search(&span_end) {
61 Ok(exact) => exact,
62 Err(idx) => idx.saturating_sub(1),
63 };
64
65 let last_line = (end_line + context_lines_after).min(self.line_starts.len() - 1);
66 let end_offset = if last_line + 1 < self.line_starts.len() {
67 self.line_starts[last_line + 1].min(src.len())
68 } else {
69 src.len()
70 };
71
72 Ok(Box::new(miette::MietteSpanContents::new(
73 &src.as_bytes()[start_offset..end_offset],
74 (start_offset, end_offset - start_offset).into(),
75 start_line,
76 start_column,
77 last_line + 1,
78 )))
79 }
80}
81
82fn strip_period(s: &str, strip: bool) -> &str {
83 if strip {
84 s.strip_suffix('.').unwrap_or(s)
85 } else {
86 s
87 }
88}
89
90fn span_to_labeled(span: &Span, text: String, primary: bool) -> LabeledSpan {
91 let source_span = miette::SourceSpan::new(
92 (span.byte_offset as usize).into(),
93 span.byte_length as usize,
94 );
95 if primary {
96 LabeledSpan::new_primary_with_span(Some(text), source_span)
97 } else {
98 LabeledSpan::new_with_span(Some(text), source_span)
99 }
100}
101
102pub use miette::Report;
103
104impl From<ParseError> for LisetteDiagnostic {
105 fn from(err: ParseError) -> Self {
106 let mut diagnostic = LisetteDiagnostic::error(&err.message);
107
108 for (span, label) in &err.labels {
109 diagnostic = diagnostic.with_span_label(span, label);
110 }
111
112 if let Some(help) = err.help {
113 diagnostic = diagnostic.with_help(help);
114 }
115
116 if let Some(note) = err.note {
117 diagnostic = diagnostic.with_note(note);
118 }
119
120 if !err.code.is_empty() {
121 diagnostic = diagnostic.with_code(err.code);
122 }
123
124 diagnostic
125 }
126}
127
128fn format_with_backticks<F>(text: &str, use_color: bool, base_style: F) -> String
129where
130 F: Fn(&str) -> String,
131{
132 if !use_color {
133 return text.to_string();
134 }
135
136 let mut result = String::new();
137 let mut chars = text.char_indices().peekable();
138 let mut segment_start = 0;
139
140 while let Some((i, ch)) = chars.next() {
141 if ch == '`' {
142 if i > segment_start {
143 result.push_str(&base_style(&text[segment_start..i]));
144 }
145
146 let mut found_closing = false;
147 for (j, inner_ch) in chars.by_ref() {
148 if inner_ch == '`' {
149 let quoted = &text[i + 1..j];
150 result.push_str(&format!("{}", quoted.bright_magenta()));
151 segment_start = j + 1;
152 found_closing = true;
153 break;
154 }
155 }
156
157 if !found_closing {
158 result.push_str(&base_style(&text[i..]));
159 segment_start = text.len();
160 }
161 }
162 }
163
164 if segment_start < text.len() {
165 result.push_str(&base_style(&text[segment_start..]));
166 }
167
168 result
169}
170
171#[derive(Debug, Clone)]
172#[must_use]
173pub struct LisetteDiagnostic {
174 message: String,
175 labels: Vec<LabeledSpan>,
176 help: Option<String>,
177 note: Option<String>,
178 severity: Severity,
179 code: Option<String>,
180 file_id: Option<u32>,
181 use_color: bool,
182}
183
184impl fmt::Display for LisetteDiagnostic {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 if self.use_color {
187 let styled_message = match self.severity {
188 Severity::Error => {
189 format_with_backticks(&self.message, true, |s| format!("{}", s.red().bold()))
190 }
191 Severity::Warning => {
192 format_with_backticks(&self.message, true, |s| format!("{}", s.yellow().bold()))
193 }
194 Severity::Advice => {
195 format_with_backticks(&self.message, true, |s| format!("{}", s.cyan().bold()))
196 }
197 };
198 write!(f, "{}", styled_message)?;
199 } else {
200 self.message.fmt(f)?;
201 }
202 Ok(())
203 }
204}
205
206impl std::error::Error for LisetteDiagnostic {}
207
208struct HelpText<'a> {
209 help: Option<&'a str>,
210 note: Option<&'a str>,
211 diagnostic_code: Option<&'a str>,
212 use_color: bool,
213}
214
215impl fmt::Display for HelpText<'_> {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 let use_color = self.use_color;
218 let has_code = self.diagnostic_code.is_some();
219
220 let combined = match (self.help, self.note) {
221 (Some(h), Some(n)) => format!("{} {}", h, strip_period(n, has_code)),
222 (Some(h), None) => strip_period(h, has_code).to_string(),
223 (None, Some(n)) => strip_period(n, has_code).to_string(),
224 (None, None) => String::new(),
225 };
226
227 if !combined.is_empty() {
228 if use_color {
229 let styled = format_with_backticks(&combined, true, |s| format!("{}", s.dimmed()));
230 write!(f, "{}", styled)?;
231 } else {
232 write!(f, "{}", combined)?;
233 }
234 }
235
236 if let Some(code) = self.diagnostic_code {
237 let is_listing = self
238 .help
239 .is_some_and(|h| h.lines().skip(1).any(|line| line.starts_with(" ")));
240 let prefix = if is_listing { "\ncode: " } else { " ยท code: " };
241 if use_color {
242 write!(f, "{}{}", prefix.dimmed(), format!("[{}]", code).dimmed())?;
243 } else {
244 write!(f, "{}[{}]", prefix, code)?;
245 }
246 }
247
248 Ok(())
249 }
250}
251
252impl Diagnostic for LisetteDiagnostic {
253 fn severity(&self) -> Option<Severity> {
254 Some(self.severity)
255 }
256
257 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
258 let diagnostic_code = self.code.as_deref();
259
260 if self.help.is_none() && self.note.is_none() && diagnostic_code.is_none() {
261 return None;
262 }
263 Some(Box::new(HelpText {
264 help: self.help.as_deref(),
265 note: self.note.as_deref(),
266 diagnostic_code,
267 use_color: self.use_color,
268 }))
269 }
270
271 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
272 let use_color = self.use_color;
273 let severity = self.severity;
274
275 let formatted_labels = self.labels.iter().map(move |span| {
276 if let Some(label) = span.label() {
277 let formatted = if use_color {
278 let base_style = match severity {
279 Severity::Error => |s: &str| format!("{}", s.red()),
280 Severity::Warning => |s: &str| format!("{}", s.yellow()),
281 Severity::Advice => |s: &str| format!("{}", s.cyan()),
282 };
283 format_with_backticks(label, true, base_style)
284 } else {
285 label.to_string()
286 };
287 if span.primary() {
288 LabeledSpan::new_primary_with_span(Some(formatted), *span.inner())
289 } else {
290 LabeledSpan::new_with_span(Some(formatted), *span.inner())
291 }
292 } else {
293 span.clone()
294 }
295 });
296
297 Some(Box::new(formatted_labels))
298 }
299
300 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
301 None }
303}
304
305impl LisetteDiagnostic {
306 pub fn plain_message(&self) -> &str {
307 &self.message
308 }
309
310 pub fn plain_help(&self) -> Option<&str> {
311 self.help.as_deref()
312 }
313
314 pub fn plain_note(&self) -> Option<&str> {
315 self.note.as_deref()
316 }
317
318 pub fn error(message: impl Into<String>) -> Self {
319 Self {
320 message: message.into(),
321 labels: Vec::new(),
322 help: None,
323 note: None,
324 severity: Severity::Error,
325 code: None,
326 file_id: None,
327 use_color: false,
328 }
329 }
330
331 pub fn warn(message: impl Into<String>) -> Self {
332 Self {
333 message: message.into(),
334 labels: Vec::new(),
335 help: None,
336 note: None,
337 severity: Severity::Warning,
338 code: None,
339 file_id: None,
340 use_color: false,
341 }
342 }
343
344 pub fn with_color(mut self, use_color: bool) -> Self {
345 self.use_color = use_color;
346 self
347 }
348
349 pub fn with_span_label(mut self, span: &Span, text: impl Into<String>) -> Self {
350 if self.file_id.is_none() {
351 self.file_id = Some(span.file_id);
352 }
353 self.labels.push(span_to_labeled(span, text.into(), false));
354 self
355 }
356
357 pub fn with_span_primary_label(mut self, span: &Span, text: impl Into<String>) -> Self {
358 if self.file_id.is_none() {
359 self.file_id = Some(span.file_id);
360 }
361 self.labels.push(span_to_labeled(span, text.into(), true));
362 self
363 }
364
365 pub fn with_labels(mut self, labels: Vec<LabeledSpan>) -> Self {
366 self.labels.extend(labels);
367 self
368 }
369
370 pub fn with_help(mut self, help: impl Into<String>) -> Self {
371 self.help = Some(help.into());
372 self
373 }
374
375 pub fn with_note(mut self, note: impl Into<String>) -> Self {
376 self.note = Some(note.into());
377 self
378 }
379
380 pub fn with_lex_code(mut self, code: &str) -> Self {
381 self.code = Some(format!("lex.{}", code));
382 self
383 }
384
385 pub fn with_parse_code(mut self, code: &str) -> Self {
386 self.code = Some(format!("parse.{}", code));
387 self
388 }
389
390 pub fn with_resolve_code(mut self, code: &str) -> Self {
391 self.code = Some(format!("resolve.{}", code));
392 self
393 }
394
395 pub fn with_infer_code(mut self, code: &str) -> Self {
396 self.code = Some(format!("infer.{}", code));
397 self
398 }
399
400 pub fn with_lint_code(mut self, code: &str) -> Self {
401 self.code = Some(format!("lint.{}", code));
402 self
403 }
404
405 pub fn with_code(mut self, code: impl Into<String>) -> Self {
406 self.code = Some(code.into());
407 self
408 }
409
410 pub fn with_source_code(self, source: IndexedSource, filename: String) -> miette::Report {
411 miette::Report::new(self).with_source_code(miette::NamedSource::new(filename, source))
412 }
413
414 pub fn code_str(&self) -> Option<&str> {
415 self.code.as_deref()
416 }
417
418 pub fn primary_offset(&self) -> usize {
419 self.labels.first().map(|l| l.offset()).unwrap_or(0)
420 }
421
422 pub fn file_id(&self) -> Option<u32> {
423 self.file_id
424 }
425
426 pub fn is_error(&self) -> bool {
427 self.severity == Severity::Error
428 }
429
430 pub fn is_warning(&self) -> bool {
431 self.severity == Severity::Warning
432 }
433}