1use crate::{Parsed, Position, Schema, Span};
7use std::borrow::Cow;
8use std::fmt;
9use std::path::Path;
10use unicode_segmentation::UnicodeSegmentation;
11use unicode_width::UnicodeWidthStr;
12
13pub trait Diagnostic {
15 fn kind(&self) -> DiagnosticKind;
17
18 fn schema_name(&self) -> &str;
22
23 fn format<'a>(&'a self, parsed: &'a Parsed) -> Formatted<'a>;
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub enum DiagnosticKind {
30 Error,
32
33 Warning,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub enum Style {
43 Regular,
45
46 Error,
48
49 Warning,
51
52 Info,
54
55 Emphasized,
57
58 Separator,
60
61 LineNumber,
63}
64
65#[derive(Debug, Clone)]
102pub struct Formatted<'a> {
103 kind: DiagnosticKind,
104 intro: Line<'a>,
105 lines: Vec<Line<'a>>,
106}
107
108impl<'a> Formatted<'a> {
109 pub fn kind(&self) -> DiagnosticKind {
111 self.kind
112 }
113
114 pub fn summary(&self) -> &str {
128 &self.intro.chunks[2].0
129 }
130
131 #[allow(clippy::len_without_is_empty)]
133 pub fn len(&self) -> usize {
134 self.lines.len() + 1
135 }
136
137 pub fn lines(&'a self) -> Lines<'a> {
139 Lines {
140 intro: &self.intro,
141 lines: &self.lines,
142 line: 0,
143 }
144 }
145}
146
147impl<'a> IntoIterator for &'a Formatted<'a> {
148 type Item = &'a Line<'a>;
149 type IntoIter = Lines<'a>;
150
151 fn into_iter(self) -> Self::IntoIter {
152 self.lines()
153 }
154}
155
156impl fmt::Display for Formatted<'_> {
157 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
158 for line in self {
159 line.fmt(f)?;
160 }
161
162 Ok(())
163 }
164}
165
166#[derive(Debug)]
168pub struct Lines<'a> {
169 intro: &'a Line<'a>,
170 lines: &'a [Line<'a>],
171 line: usize,
172}
173
174impl<'a> Iterator for Lines<'a> {
175 type Item = &'a Line<'a>;
176
177 fn next(&mut self) -> Option<Self::Item> {
178 if self.line == 0 {
179 self.line += 1;
180 Some(self.intro)
181 } else if self.line <= self.lines.len() {
182 let line = &self.lines[self.line - 1];
183 self.line += 1;
184 Some(line)
185 } else {
186 None
187 }
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct Line<'a> {
194 padding: Cow<'a, str>,
195 chunks: Vec<(Cow<'a, str>, Style)>,
196}
197
198impl<'a> Line<'a> {
199 #[allow(clippy::len_without_is_empty)]
201 pub fn len(&self) -> usize {
202 self.chunks.len() + 1
203 }
204
205 pub fn chunks(&'a self) -> Chunks<'a> {
207 Chunks {
208 line: self,
209 chunk: 0,
210 }
211 }
212}
213
214impl<'a> IntoIterator for &'a Line<'a> {
215 type Item = (&'a str, Style);
216 type IntoIter = Chunks<'a>;
217
218 fn into_iter(self) -> Self::IntoIter {
219 self.chunks()
220 }
221}
222
223impl fmt::Display for Line<'_> {
224 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
225 for (chunk, _) in self {
226 f.write_str(chunk)?;
227 }
228
229 writeln!(f)
230 }
231}
232
233#[derive(Debug, Clone)]
235pub struct Chunks<'a> {
236 line: &'a Line<'a>,
237 chunk: usize,
238}
239
240impl<'a> Iterator for Chunks<'a> {
241 type Item = (&'a str, Style);
242
243 fn next(&mut self) -> Option<Self::Item> {
244 if self.chunk == 0 {
245 self.chunk += 1;
246 Some((&self.line.padding, Style::Regular))
247 } else if self.chunk <= self.line.chunks.len() {
248 let chunk = &self.line.chunks[self.chunk - 1];
249 self.chunk += 1;
250 Some((&chunk.0, chunk.1))
251 } else {
252 None
253 }
254 }
255}
256
257pub(crate) struct Formatter<'a> {
258 kind: DiagnosticKind,
259 intro: Line<'a>,
260 lines: Vec<UnpaddedLine<'a>>,
261 padding: usize,
262}
263
264impl<'a> Formatter<'a> {
265 pub fn new<D, S>(diagnostic: &'a D, summary: S) -> Self
266 where
267 D: Diagnostic,
268 S: Into<Cow<'a, str>>,
269 {
270 let (kind, kind_style) = match diagnostic.kind() {
271 DiagnosticKind::Error => ("error", Style::Error),
272 DiagnosticKind::Warning => ("warning", Style::Warning),
273 };
274 let summary = summary.into();
275 let sep = if summary.is_empty() { ":" } else { ": " };
276
277 let intro = Line {
278 padding: "".into(),
279 chunks: vec![
280 (kind.into(), kind_style),
281 (sep.into(), Style::Emphasized),
282 (summary, Style::Emphasized),
284 ],
285 };
286
287 Formatter {
288 kind: diagnostic.kind(),
289 intro,
290 lines: Vec::new(),
291 padding: 0,
292 }
293 }
294
295 pub fn format(self) -> Formatted<'a> {
296 let mut lines = Vec::with_capacity(self.lines.len());
297 for line in self.lines {
298 lines.push(Line {
299 padding: gen_padding(self.padding - line.padding),
300 chunks: line.chunks,
301 });
302 }
303
304 Formatted {
305 kind: self.kind,
306 intro: self.intro,
307 lines,
308 }
309 }
310
311 pub fn block<S>(
312 &mut self,
313 schema: &'a Schema,
314 location: Position,
315 indicator: Span,
316 text: S,
317 is_main_block: bool,
318 ) -> &mut Self
319 where
320 S: Into<Cow<'a, str>>,
321 {
322 self.location(schema.path(), location, is_main_block);
323
324 let source = match schema.source() {
325 Some(source) => source,
326 None => return self,
327 };
328
329 #[derive(PartialEq, Eq)]
330 enum State {
331 Normal,
332 Skip,
333 }
334
335 self.empty_context();
336 let mut state = State::Normal;
337 let mut lines = indicator.lines(source).peekable();
338
339 while let Some((line, span)) = lines.next() {
340 let line = line.trim_end();
341
342 if line.trim().is_empty() {
343 if state == State::Skip {
344 continue;
345 }
346
347 if let Some((next, _)) = lines.peek() {
348 if next.trim().is_empty() {
349 state = State::Skip;
350 self.skipped_context();
351 self.empty_context();
352 continue;
353 }
354 } else {
355 continue;
356 }
357 }
358
359 state = State::Normal;
360
361 let trimmed = line.trim_start();
362 let diff = line.len() - trimmed.len();
363 let (source, from) = if diff >= 8 {
364 self.trimmed_context(span.from.line_col.line, trimmed);
365 let from = span.from.line_col.column.saturating_sub(diff + 1);
366 let to = span.to.line_col.column - diff - 1;
367 let trimmed = &trimmed[from..to];
368 (trimmed, from + 4)
369 } else {
370 self.context(span.from.line_col.line, line);
371 let from = span.from.line_col.column - 1;
372 let to = span.to.line_col.column - 1;
373 (&line[from..to], from)
374 };
375
376 let to: usize = source.graphemes(true).map(UnicodeWidthStr::width).sum();
377
378 if lines.peek().is_some() {
379 self.indicator(from, from + to, "", is_main_block);
380 } else {
381 self.indicator(from, from + to, text, is_main_block);
382 self.empty_context();
383 break;
384 }
385 }
386
387 self
388 }
389
390 pub fn main_block<S>(
391 &mut self,
392 schema: &'a Schema,
393 location: Position,
394 span: Span,
395 text: S,
396 ) -> &mut Self
397 where
398 S: Into<Cow<'a, str>>,
399 {
400 self.block(schema, location, span, text, true)
401 }
402
403 pub fn info_block<S>(
404 &mut self,
405 schema: &'a Schema,
406 location: Position,
407 span: Span,
408 text: S,
409 ) -> &mut Self
410 where
411 S: Into<Cow<'a, str>>,
412 {
413 self.block(schema, location, span, text, false)
414 }
415
416 pub fn location<P>(&mut self, path: P, location: Position, is_main_location: bool) -> &mut Self
417 where
418 P: AsRef<Path>,
419 {
420 if is_main_location {
421 self.main_location(path, location)
422 } else {
423 self.info_location(path, location)
424 }
425 }
426
427 pub fn main_location<P>(&mut self, path: P, location: Position) -> &mut Self
428 where
429 P: AsRef<Path>,
430 {
431 self.location_impl(path, location, "-->")
432 }
433
434 pub fn info_location<P>(&mut self, path: P, location: Position) -> &mut Self
435 where
436 P: AsRef<Path>,
437 {
438 self.location_impl(path, location, ":::")
439 }
440
441 fn location_impl<P, S>(&mut self, path: P, location: Position, sep: S) -> &mut Self
442 where
443 P: AsRef<Path>,
444 S: Into<Cow<'a, str>>,
445 {
446 let location = format!(
447 " {}:{}:{}",
448 path.as_ref().display(),
449 location.line_col.line,
450 location.line_col.column
451 );
452
453 self.lines.push(UnpaddedLine::new(vec![
454 (" ".into(), Style::Regular),
455 (sep.into(), Style::Separator),
456 (location.into(), Style::Regular),
457 ]));
458
459 self
460 }
461
462 pub fn empty_context(&mut self) -> &mut Self {
463 self.lines.push(UnpaddedLine::new(vec![
464 (" ".into(), Style::Regular),
465 ("|".into(), Style::Separator),
466 ]));
467
468 self
469 }
470
471 pub fn context<S>(&mut self, line: usize, source: S) -> &mut Self
472 where
473 S: Into<Cow<'a, str>>,
474 {
475 let line = line.to_string();
476 if line.len() > self.padding {
477 self.padding = line.len();
478 }
479
480 self.lines.push(UnpaddedLine::with_padding(
481 line.len(),
482 vec![
483 (" ".into(), Style::Regular),
484 (line.into(), Style::LineNumber),
485 (" ".into(), Style::Regular),
486 ("|".into(), Style::Separator),
487 (" ".into(), Style::Regular),
488 (source.into(), Style::Regular),
489 ],
490 ));
491
492 self
493 }
494
495 pub fn trimmed_context<S>(&mut self, line: usize, source: S) -> &mut Self
496 where
497 S: Into<Cow<'a, str>>,
498 {
499 let line = line.to_string();
500 if line.len() > self.padding {
501 self.padding = line.len();
502 }
503
504 self.lines.push(UnpaddedLine::with_padding(
505 line.len(),
506 vec![
507 (" ".into(), Style::Regular),
508 (line.into(), Style::LineNumber),
509 (" ".into(), Style::Regular),
510 ("|".into(), Style::Separator),
511 (" ... ".into(), Style::Regular),
512 (source.into(), Style::Regular),
513 ],
514 ));
515
516 self
517 }
518
519 pub fn skipped_context(&mut self) -> &mut Self {
520 let skip = "..";
521 if skip.len() > self.padding {
522 self.padding = skip.len();
523 }
524
525 self.lines.push(UnpaddedLine::with_padding(
526 skip.len(),
527 vec![
528 (" ".into(), Style::Regular),
529 (skip.into(), Style::LineNumber),
530 (" ".into(), Style::Regular),
531 ("|".into(), Style::Separator),
532 ],
533 ));
534
535 self
536 }
537
538 pub fn indicator<S>(
539 &mut self,
540 from: usize,
541 to: usize,
542 text: S,
543 is_main_indicator: bool,
544 ) -> &mut Self
545 where
546 S: Into<Cow<'a, str>>,
547 {
548 if is_main_indicator {
549 self.main_indicator(from, to, text)
550 } else {
551 self.info_indicator(from, to, text)
552 }
553 }
554
555 pub fn main_indicator<S>(&mut self, from: usize, to: usize, text: S) -> &mut Self
556 where
557 S: Into<Cow<'a, str>>,
558 {
559 let style = match self.kind {
560 DiagnosticKind::Error => Style::Error,
561 DiagnosticKind::Warning => Style::Warning,
562 };
563
564 self.indicator_impl(from, text, gen_main_indicator(to - from), style)
565 }
566
567 pub fn info_indicator<S>(&mut self, from: usize, to: usize, text: S) -> &mut Self
568 where
569 S: Into<Cow<'a, str>>,
570 {
571 self.indicator_impl(from, text, gen_info_indicator(to - from), Style::Info)
572 }
573
574 fn indicator_impl<S, I>(&mut self, from: usize, text: S, ind: I, style: Style) -> &mut Self
575 where
576 S: Into<Cow<'a, str>>,
577 I: Into<Cow<'a, str>>,
578 {
579 let mut line = UnpaddedLine::new(vec![
580 (" ".into(), Style::Regular),
581 ("|".into(), Style::Separator),
582 (gen_padding(from + 1), Style::Regular),
583 (ind.into(), style),
584 ]);
585
586 let text = text.into();
587 if !text.is_empty() {
588 line.chunks.push((" ".into(), Style::Regular));
589 line.chunks.push((text, style));
590 }
591
592 self.lines.push(line);
593 self
594 }
595
596 pub fn note<S>(&mut self, text: S) -> &mut Self
597 where
598 S: Into<Cow<'a, str>>,
599 {
600 self.info_impl("note", text)
601 }
602
603 pub fn help<S>(&mut self, text: S) -> &mut Self
604 where
605 S: Into<Cow<'a, str>>,
606 {
607 self.info_impl("help", text)
608 }
609
610 fn info_impl<K, S>(&mut self, kind: K, text: S) -> &mut Self
611 where
612 K: Into<Cow<'a, str>>,
613 S: Into<Cow<'a, str>>,
614 {
615 self.lines.push(UnpaddedLine::new(vec![
616 (" ".into(), Style::Regular),
617 ("=".into(), Style::Separator),
618 (" ".into(), Style::Regular),
619 (kind.into(), Style::Emphasized),
620 (":".into(), Style::Emphasized),
621 (" ".into(), Style::Regular),
622 (text.into(), Style::Regular),
623 ]));
624
625 self
626 }
627}
628
629struct UnpaddedLine<'a> {
630 padding: usize,
631 chunks: Vec<(Cow<'a, str>, Style)>,
632}
633
634impl<'a> UnpaddedLine<'a> {
635 fn new(chunks: Vec<(Cow<'a, str>, Style)>) -> Self {
636 UnpaddedLine { padding: 0, chunks }
637 }
638
639 fn with_padding(padding: usize, chunks: Vec<(Cow<'a, str>, Style)>) -> Self {
640 UnpaddedLine { padding, chunks }
641 }
642}
643
644fn gen_padding(size: usize) -> Cow<'static, str> {
645 const PADDING: &str = " ";
646 if size < PADDING.len() {
647 PADDING[..size].into()
648 } else {
649 " ".repeat(size).into()
650 }
651}
652
653fn gen_main_indicator(size: usize) -> Cow<'static, str> {
654 const INDICATOR: &str = "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^";
655 if size < INDICATOR.len() {
656 INDICATOR[..size].into()
657 } else {
658 "^".repeat(size).into()
659 }
660}
661
662fn gen_info_indicator(size: usize) -> Cow<'static, str> {
663 const INDICATOR: &str = "----------------------------------------------------------------";
664 if size < INDICATOR.len() {
665 INDICATOR[..size].into()
666 } else {
667 "-".repeat(size).into()
668 }
669}