1use std::fmt::{self, Write};
45
46use thiserror::Error;
47use typst::diag::Severity;
48use typst::syntax::Span;
49use typst::World;
50
51pub use typst::diag::{Severity as DiagnosticSeverity, SourceDiagnostic};
53
54#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
60pub enum DisplayStyle {
61 #[default]
63 Rich,
64 Short,
66}
67
68#[derive(Debug, Clone)]
91pub struct DiagnosticOptions {
92 pub colored: bool,
94 pub style: DisplayStyle,
96 pub snippets: bool,
98 pub hints: bool,
100 pub traces: bool,
102 pub tab_width: usize,
104}
105
106impl Default for DiagnosticOptions {
107 fn default() -> Self {
108 Self {
109 colored: true,
110 style: DisplayStyle::Rich,
111 snippets: true,
112 hints: true,
113 traces: true,
114 tab_width: 2,
115 }
116 }
117}
118
119impl DiagnosticOptions {
120 pub fn new() -> Self {
122 Self::default()
123 }
124
125 pub fn colored() -> Self {
127 Self::default()
128 }
129
130 pub fn plain() -> Self {
132 Self {
133 colored: false,
134 ..Self::default()
135 }
136 }
137
138 pub fn short() -> Self {
140 Self {
141 style: DisplayStyle::Short,
142 snippets: false,
143 traces: false,
144 ..Self::default()
145 }
146 }
147
148 pub fn with_colored(mut self, colored: bool) -> Self {
150 self.colored = colored;
151 self
152 }
153
154 pub fn with_style(mut self, style: DisplayStyle) -> Self {
156 self.style = style;
157 self
158 }
159
160 pub fn with_snippets(mut self, snippets: bool) -> Self {
162 self.snippets = snippets;
163 self
164 }
165
166 pub fn with_hints(mut self, hints: bool) -> Self {
168 self.hints = hints;
169 self
170 }
171
172 pub fn with_traces(mut self, traces: bool) -> Self {
174 self.traces = traces;
175 self
176 }
177
178 pub fn with_tab_width(mut self, width: usize) -> Self {
180 self.tab_width = width;
181 self
182 }
183}
184
185#[derive(Debug, Error)]
214pub enum CompileError {
215 #[error("Typst compilation failed:\n{formatted}")]
217 Compilation {
218 diagnostics: Vec<SourceDiagnostic>,
220 formatted: String,
222 },
223
224 #[error("HTML export failed: {message}")]
226 HtmlExport {
227 message: String,
229 },
230
231 #[error("I/O error: {0}")]
233 Io(#[from] std::io::Error),
234}
235
236impl CompileError {
237 pub fn compilation<W: World>(world: &W, diagnostics: Vec<SourceDiagnostic>) -> Self {
239 let formatted = format_diagnostics(world, &diagnostics);
240 Self::Compilation {
241 diagnostics,
242 formatted,
243 }
244 }
245
246 pub fn compilation_with_options<W: World>(
248 world: &W,
249 diagnostics: Vec<SourceDiagnostic>,
250 options: &DiagnosticOptions,
251 ) -> Self {
252 let formatted = format_diagnostics_with_options(world, &diagnostics, options);
253 Self::Compilation {
254 diagnostics,
255 formatted,
256 }
257 }
258
259 pub fn html_export(message: impl Into<String>) -> Self {
261 Self::HtmlExport {
262 message: message.into(),
263 }
264 }
265
266 pub fn has_fatal_errors(&self) -> bool {
268 match self {
269 Self::Compilation { diagnostics, .. } => has_errors(diagnostics),
270 _ => true,
271 }
272 }
273
274 pub fn diagnostics(&self) -> Option<&[SourceDiagnostic]> {
276 match self {
277 Self::Compilation { diagnostics, .. } => Some(diagnostics),
278 _ => None,
279 }
280 }
281}
282
283#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
289pub struct DiagnosticSummary {
290 pub errors: usize,
292 pub warnings: usize,
294}
295
296impl DiagnosticSummary {
297 pub fn from_diagnostics(diagnostics: &[SourceDiagnostic]) -> Self {
299 let (errors, warnings) = count_diagnostics(diagnostics);
300 Self { errors, warnings }
301 }
302
303 pub fn total(&self) -> usize {
305 self.errors + self.warnings
306 }
307
308 pub fn has_errors(&self) -> bool {
310 self.errors > 0
311 }
312
313 pub fn is_empty(&self) -> bool {
315 self.total() == 0
316 }
317}
318
319impl fmt::Display for DiagnosticSummary {
320 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321 match (self.errors, self.warnings) {
322 (0, 0) => write!(f, "no diagnostics"),
323 (e, 0) => write!(f, "{e} error{}", if e == 1 { "" } else { "s" }),
324 (0, w) => write!(f, "{w} warning{}", if w == 1 { "" } else { "s" }),
325 (e, w) => write!(
326 f,
327 "{e} error{}, {w} warning{}",
328 if e == 1 { "" } else { "s" },
329 if w == 1 { "" } else { "s" }
330 ),
331 }
332 }
333}
334
335pub trait DiagnosticsExt {
365 fn has_errors(&self) -> bool;
367
368 fn has_warnings(&self) -> bool;
370
371 fn is_empty(&self) -> bool;
373
374 fn len(&self) -> usize;
376
377 fn error_count(&self) -> usize;
379
380 fn warning_count(&self) -> usize;
382
383 fn counts(&self) -> (usize, usize);
385
386 fn summary(&self) -> DiagnosticSummary;
388
389 fn filter_out(&self, filters: &[DiagnosticFilter]) -> Vec<SourceDiagnostic>;
403
404 fn filter_html_warnings(&self) -> Vec<SourceDiagnostic> {
408 self.filter_out(&[DiagnosticFilter::HtmlExport])
409 }
410
411 fn format<W: World>(&self, world: &W) -> String;
415
416 fn format_with<W: World>(&self, world: &W, options: &DiagnosticOptions) -> String;
418
419 fn resolve<W: World>(&self, world: &W) -> Vec<DiagnosticInfo>;
423}
424
425#[derive(Debug, Clone, PartialEq, Eq)]
429pub enum DiagnosticFilter {
430 HtmlExport,
434
435 ExternalPackages,
439
440 AllWarnings,
442
443 MessageContains(String),
445}
446
447impl DiagnosticFilter {
448 fn matches(&self, diag: &SourceDiagnostic) -> bool {
450 match self {
451 DiagnosticFilter::HtmlExport => {
452 diag.severity == Severity::Warning
453 && diag.message.contains("html export is under active development")
454 }
455 DiagnosticFilter::ExternalPackages => {
456 diag.severity == Severity::Warning && is_external_package(diag)
457 }
458 DiagnosticFilter::AllWarnings => diag.severity == Severity::Warning,
459 DiagnosticFilter::MessageContains(text) => diag.message.contains(text.as_str()),
460 }
461 }
462}
463
464fn is_external_package(diag: &SourceDiagnostic) -> bool {
466 if let Some(id) = diag.span.id() {
468 let path = id.vpath().as_rootless_path();
469 path.to_string_lossy().starts_with('@')
471 } else {
472 false
473 }
474}
475
476impl DiagnosticsExt for [SourceDiagnostic] {
477 fn has_errors(&self) -> bool {
478 self.iter().any(|d| d.severity == Severity::Error)
479 }
480
481 fn has_warnings(&self) -> bool {
482 self.iter().any(|d| d.severity == Severity::Warning)
483 }
484
485 fn is_empty(&self) -> bool {
486 <[SourceDiagnostic]>::is_empty(self)
487 }
488
489 fn len(&self) -> usize {
490 <[SourceDiagnostic]>::len(self)
491 }
492
493 fn error_count(&self) -> usize {
494 self.iter()
495 .filter(|d| d.severity == Severity::Error)
496 .count()
497 }
498
499 fn warning_count(&self) -> usize {
500 self.iter()
501 .filter(|d| d.severity == Severity::Warning)
502 .count()
503 }
504
505 fn counts(&self) -> (usize, usize) {
506 self.iter().fold((0, 0), |(errors, warnings), d| match d.severity {
507 Severity::Error => (errors + 1, warnings),
508 Severity::Warning => (errors, warnings + 1),
509 })
510 }
511
512 fn summary(&self) -> DiagnosticSummary {
513 let (errors, warnings) = self.counts();
514 DiagnosticSummary { errors, warnings }
515 }
516
517 fn filter_out(&self, filters: &[DiagnosticFilter]) -> Vec<SourceDiagnostic> {
518 self.iter()
519 .filter(|d| !filters.iter().any(|f| f.matches(d)))
520 .cloned()
521 .collect()
522 }
523
524 fn format<W: World>(&self, world: &W) -> String {
525 format_diagnostics(world, self)
526 }
527
528 fn format_with<W: World>(&self, world: &W, options: &DiagnosticOptions) -> String {
529 format_diagnostics_with_options(world, self, options)
530 }
531
532 fn resolve<W: World>(&self, world: &W) -> Vec<DiagnosticInfo> {
533 self.iter().map(|d| resolve_diagnostic(world, d)).collect()
534 }
535}
536
537impl DiagnosticsExt for Vec<SourceDiagnostic> {
538 fn has_errors(&self) -> bool {
539 self.as_slice().has_errors()
540 }
541
542 fn has_warnings(&self) -> bool {
543 self.as_slice().has_warnings()
544 }
545
546 fn is_empty(&self) -> bool {
547 Vec::is_empty(self)
548 }
549
550 fn len(&self) -> usize {
551 Vec::len(self)
552 }
553
554 fn error_count(&self) -> usize {
555 self.as_slice().error_count()
556 }
557
558 fn warning_count(&self) -> usize {
559 self.as_slice().warning_count()
560 }
561
562 fn counts(&self) -> (usize, usize) {
563 self.as_slice().counts()
564 }
565
566 fn summary(&self) -> DiagnosticSummary {
567 self.as_slice().summary()
568 }
569
570 fn filter_out(&self, filters: &[DiagnosticFilter]) -> Vec<SourceDiagnostic> {
571 self.as_slice().filter_out(filters)
572 }
573
574 fn format<W: World>(&self, world: &W) -> String {
575 self.as_slice().format(world)
576 }
577
578 fn format_with<W: World>(&self, world: &W, options: &DiagnosticOptions) -> String {
579 self.as_slice().format_with(world, options)
580 }
581
582 fn resolve<W: World>(&self, world: &W) -> Vec<DiagnosticInfo> {
583 self.as_slice().resolve(world)
584 }
585}
586
587mod gutter {
593 pub const HEADER: &str = "┌─";
594 pub const BAR: &str = "│";
595 pub const SPAN_START: &str = "╭";
596 pub const SPAN_END: &str = "╰";
597 pub const DASH: &str = "─";
598 pub const MARKER: &str = "^";
599}
600
601#[derive(Debug, Clone)]
624pub struct DiagnosticInfo {
625 pub severity: Severity,
627 pub message: String,
629 pub path: Option<String>,
631 pub line: Option<usize>,
633 pub column: Option<usize>,
635 pub source_lines: Vec<SourceLine>,
637 pub hints: Vec<String>,
639 pub traces: Vec<TraceInfo>,
641}
642
643#[derive(Debug, Clone)]
645pub struct SourceLine {
646 pub line_num: usize,
648 pub text: String,
650 pub highlight: Option<(usize, usize)>,
652}
653
654#[derive(Debug, Clone)]
656pub struct TraceInfo {
657 pub message: String,
659 pub path: Option<String>,
661 pub line: Option<usize>,
663 pub column: Option<usize>,
665 pub source_lines: Vec<SourceLine>,
667}
668
669pub(crate) fn resolve_diagnostic<W: World>(world: &W, diag: &SourceDiagnostic) -> DiagnosticInfo {
672 let location = SpanLocation::from_span(world, diag.span);
673
674 let (path, line, column, source_lines) = match &location {
675 Some(loc) => (
676 Some(loc.path.clone()),
677 Some(loc.start_line),
678 Some(loc.start_col + 1), loc.to_source_lines(),
680 ),
681 None => (None, None, None, vec![]),
682 };
683
684 let hints = diag.hints.iter().map(|h| h.to_string()).collect();
685
686 let traces = diag
687 .trace
688 .iter()
689 .filter_map(|t| {
690 use typst::diag::Tracepoint;
691
692 if matches!(t.v, Tracepoint::Import) {
694 return None;
695 }
696
697 let message = t.v.to_string();
699
700 let loc = SpanLocation::from_span(world, t.span);
701 Some(TraceInfo {
702 message,
703 path: loc.as_ref().map(|l| l.path.clone()),
704 line: loc.as_ref().map(|l| l.start_line),
705 column: loc.as_ref().map(|l| l.start_col + 1),
706 source_lines: loc.as_ref().map(|l| l.to_source_lines()).unwrap_or_default(),
707 })
708 })
709 .collect();
710
711 DiagnosticInfo {
712 severity: diag.severity,
713 message: diag.message.to_string(),
714 path,
715 line,
716 column,
717 source_lines,
718 hints,
719 traces,
720 }
721}
722
723#[allow(dead_code)]
726pub(crate) fn resolve_diagnostics<W: World>(
727 world: &W,
728 diagnostics: &[SourceDiagnostic],
729) -> Vec<DiagnosticInfo> {
730 diagnostics
731 .iter()
732 .map(|d| resolve_diagnostic(world, d))
733 .collect()
734}
735
736#[cfg(feature = "colored-diagnostics")]
742fn colorize(text: &str, severity: Severity) -> String {
743 use colored::Colorize;
744 match severity {
745 Severity::Error => text.red().to_string(),
746 Severity::Warning => text.yellow().to_string(),
747 }
748}
749
750#[cfg(feature = "colored-diagnostics")]
751fn colorize_help(text: &str) -> String {
752 use colored::Colorize;
753 text.cyan().to_string()
754}
755
756#[cfg(not(feature = "colored-diagnostics"))]
757fn colorize(text: &str, _severity: Severity) -> String {
758 text.to_owned()
759}
760
761#[cfg(not(feature = "colored-diagnostics"))]
762fn colorize_help(text: &str) -> String {
763 text.to_owned()
764}
765
766fn get_paint_fn(options: &DiagnosticOptions, severity: Severity) -> Box<dyn Fn(&str) -> String> {
768 if options.colored {
769 Box::new(move |s| colorize(s, severity))
770 } else {
771 Box::new(|s: &str| s.to_owned())
772 }
773}
774
775fn get_help_paint_fn(options: &DiagnosticOptions) -> Box<dyn Fn(&str) -> String> {
776 if options.colored {
777 Box::new(colorize_help)
778 } else {
779 Box::new(|s: &str| s.to_owned())
780 }
781}
782
783struct SpanLocation {
792 path: String,
794 start_line: usize,
796 start_col: usize,
798 lines: Vec<String>,
800 highlight_start_col: usize,
802 highlight_end_col: usize,
804}
805
806impl SpanLocation {
807 fn from_span<W: World>(world: &W, span: Span) -> Option<Self> {
809 let id = span.id()?;
810 let source = world.source(id).ok()?;
811 let range = source.range(span)?;
812 let text = source.text();
813
814 let start_line_start = text[..range.start].rfind('\n').map_or(0, |i| i + 1);
816 let end_line_end = text[range.end..]
817 .find('\n')
818 .map_or(text.len(), |i| range.end + i);
819 let end_line_start = text[..range.end].rfind('\n').map_or(0, |i| i + 1);
820
821 let start_line = text[..range.start].matches('\n').count() + 1;
824 let start_col = text[start_line_start..range.start].chars().count();
825 let end_col = text[end_line_start..range.end].chars().count();
826
827 let lines = text[start_line_start..end_line_end]
829 .lines()
830 .map(String::from)
831 .collect();
832
833 let path = id.vpath().as_rootless_path().to_string_lossy().into_owned();
835
836 Some(Self {
837 path,
838 start_line,
839 start_col,
840 lines,
841 highlight_start_col: start_col,
842 highlight_end_col: end_col,
843 })
844 }
845
846 #[inline]
848 const fn is_multiline(&self) -> bool {
849 self.lines.len() > 1
850 }
851
852 #[inline]
854 const fn end_line(&self) -> usize {
855 self.start_line + self.lines.len() - 1
856 }
857
858 #[inline]
860 fn line_num_width(&self) -> usize {
861 self.end_line().to_string().len().max(1)
862 }
863
864 fn to_source_lines(&self) -> Vec<SourceLine> {
866 self.lines
867 .iter()
868 .enumerate()
869 .map(|(i, text)| {
870 let line_num = self.start_line + i;
871 let highlight = if i == 0 {
872 Some((self.highlight_start_col, self.highlight_end_col))
873 } else if self.lines.len() > 1 {
874 Some((0, text.chars().count()))
876 } else {
877 None
878 };
879 SourceLine {
880 line_num,
881 text: text.clone(),
882 highlight,
883 }
884 })
885 .collect()
886 }
887}
888
889struct SnippetWriter<'a, F>
898where
899 F: Fn(&str) -> String,
900{
901 output: &'a mut String,
902 paint: F,
903 line_num_width: usize,
904}
905
906impl<'a, F> SnippetWriter<'a, F>
907where
908 F: Fn(&str) -> String,
909{
910 fn new(output: &'a mut String, paint: F, line_num_width: usize) -> Self {
911 Self {
912 output,
913 paint,
914 line_num_width,
915 }
916 }
917
918 fn write_header(&mut self, path: &str, line: usize, col: usize) {
920 _ = writeln!(
921 self.output,
922 "{:>width$} {} {}:{}:{}",
923 "",
924 (self.paint)(gutter::HEADER),
925 path,
926 line,
927 col,
928 width = self.line_num_width
929 );
930 }
931
932 fn write_empty_gutter(&mut self) {
934 _ = writeln!(
935 self.output,
936 "{:>width$} {}",
937 "",
938 (self.paint)(gutter::BAR),
939 width = self.line_num_width
940 );
941 }
942
943 fn write_source_line(
945 &mut self,
946 line_num: usize,
947 line_text: &str,
948 box_char: Option<&str>,
949 highlight_range: Option<(usize, usize)>,
950 ) {
951 let line_num_str = format!("{:>width$}", line_num, width = self.line_num_width);
952
953 let formatted_line = match (box_char, highlight_range) {
954 (Some(bc), Some((start, end))) => {
955 let (before, highlighted, after) = split_line(line_text, start, end);
956 format!(
957 "{} {} {} {}{}{}",
958 (self.paint)(&line_num_str),
959 (self.paint)(gutter::BAR),
960 (self.paint)(bc),
961 before,
962 (self.paint)(&highlighted),
963 after
964 )
965 }
966 (None, Some((start, end))) => {
967 let (before, highlighted, after) = split_line(line_text, start, end);
968 format!(
969 "{} {} {}{}{}",
970 (self.paint)(&line_num_str),
971 (self.paint)(gutter::BAR),
972 before,
973 (self.paint)(&highlighted),
974 after
975 )
976 }
977 _ => {
978 format!(
979 "{} {} {}",
980 (self.paint)(&line_num_str),
981 (self.paint)(gutter::BAR),
982 line_text
983 )
984 }
985 };
986
987 _ = writeln!(self.output, "{formatted_line}");
988 }
989
990 fn write_single_line_marker(&mut self, start_col: usize, span_len: usize) {
992 let spaces = " ".repeat(start_col);
993 let markers = gutter::MARKER.repeat(span_len.max(1));
994 _ = writeln!(
995 self.output,
996 "{:>width$} {} {}{}",
997 "",
998 (self.paint)(gutter::BAR),
999 spaces,
1000 (self.paint)(&markers),
1001 width = self.line_num_width
1002 );
1003 }
1004
1005 fn write_multiline_end_marker(&mut self, end_col: usize) {
1007 let dashes = gutter::DASH.repeat(end_col);
1008 _ = writeln!(
1009 self.output,
1010 "{:>width$} {} {}{}{}",
1011 "",
1012 (self.paint)(gutter::BAR),
1013 (self.paint)(gutter::SPAN_END),
1014 (self.paint)(&dashes),
1015 (self.paint)(gutter::MARKER),
1016 width = self.line_num_width
1017 );
1018 }
1019}
1020
1021fn split_line(line: &str, start_col: usize, end_col: usize) -> (String, String, String) {
1024 let chars: Vec<char> = line.chars().collect();
1025 let start_idx = start_col.min(chars.len());
1026 let end_idx = end_col.min(chars.len());
1027
1028 let before: String = chars[..start_idx].iter().collect();
1029 let highlighted: String = chars[start_idx..end_idx].iter().collect();
1030 let after: String = chars[end_idx..].iter().collect();
1031
1032 (before, highlighted, after)
1033}
1034
1035pub(crate) fn format_diagnostics<W: World>(world: &W, diagnostics: &[SourceDiagnostic]) -> String {
1042 format_diagnostics_with_options(world, diagnostics, &DiagnosticOptions::default())
1043}
1044
1045pub(crate) fn format_diagnostics_with_options<W: World>(
1048 world: &W,
1049 diagnostics: &[SourceDiagnostic],
1050 options: &DiagnosticOptions,
1051) -> String {
1052 let mut output = String::new();
1053
1054 let (errors, warnings): (Vec<_>, Vec<_>) = diagnostics
1056 .iter()
1057 .partition(|d| d.severity == Severity::Error);
1058
1059 let all_diags: Vec<_> = errors.iter().chain(warnings.iter()).collect();
1060 for (i, diag) in all_diags.iter().enumerate() {
1061 format_diagnostic_internal(&mut output, world, diag, options);
1062 if i < all_diags.len() - 1 {
1064 output.push('\n');
1065 }
1066 }
1067
1068 output
1069}
1070
1071pub fn count_diagnostics(diagnostics: &[SourceDiagnostic]) -> (usize, usize) {
1073 diagnostics.iter().fold((0, 0), |(errors, warnings), d| {
1074 match d.severity {
1075 Severity::Error => (errors + 1, warnings),
1076 Severity::Warning => (errors, warnings + 1),
1077 }
1078 })
1079}
1080
1081pub fn has_errors(diagnostics: &[SourceDiagnostic]) -> bool {
1083 diagnostics.iter().any(|d| d.severity == Severity::Error)
1084}
1085
1086pub fn filter_html_warnings(diagnostics: &[SourceDiagnostic]) -> Vec<SourceDiagnostic> {
1091 diagnostics
1092 .iter()
1093 .filter(|d| {
1094 if d.severity == Severity::Error {
1096 return true;
1097 }
1098 !d.message.contains("html export is under active development")
1100 })
1101 .cloned()
1102 .collect()
1103}
1104
1105#[cfg(all(test, feature = "colored-diagnostics"))]
1107pub fn disable_colors() {
1108 colored::control::set_override(false);
1109}
1110
1111fn format_diagnostic_internal<W: World>(
1117 output: &mut String,
1118 world: &W,
1119 diag: &SourceDiagnostic,
1120 options: &DiagnosticOptions,
1121) {
1122 let label = match diag.severity {
1123 Severity::Error => "error",
1124 Severity::Warning => "warning",
1125 };
1126 let paint = get_paint_fn(options, diag.severity);
1127
1128 match options.style {
1129 DisplayStyle::Short => {
1130 format_diagnostic_short(output, world, diag, label, &paint);
1131 }
1132 DisplayStyle::Rich => {
1133 format_diagnostic_rich(output, world, diag, label, &paint, options);
1134 }
1135 }
1136}
1137
1138fn format_diagnostic_short<W: World>(
1140 output: &mut String,
1141 world: &W,
1142 diag: &SourceDiagnostic,
1143 label: &str,
1144 paint: &dyn Fn(&str) -> String,
1145) {
1146 if let Some(loc) = SpanLocation::from_span(world, diag.span) {
1147 _ = writeln!(
1148 output,
1149 "{}:{}:{}: {}: {}",
1150 loc.path,
1151 loc.start_line,
1152 loc.start_col + 1, paint(label),
1154 diag.message
1155 );
1156 } else {
1157 _ = writeln!(output, "{}: {}", paint(label), diag.message);
1158 }
1159}
1160
1161fn format_diagnostic_rich<W: World>(
1163 output: &mut String,
1164 world: &W,
1165 diag: &SourceDiagnostic,
1166 label: &str,
1167 paint: &dyn Fn(&str) -> String,
1168 options: &DiagnosticOptions,
1169) {
1170 _ = writeln!(output, "{}: {}", paint(label), diag.message);
1172
1173 if options.snippets
1175 && let Some(location) = SpanLocation::from_span(world, diag.span)
1176 {
1177 write_snippet(output, &location, paint);
1178 }
1179
1180 if options.traces {
1182 let help_paint = get_help_paint_fn(options);
1183 for trace in &diag.trace {
1184 write_trace(output, world, &trace.v, trace.span, &help_paint);
1185 }
1186 }
1187
1188 if options.hints {
1190 let help_paint = get_help_paint_fn(options);
1191 for hint in &diag.hints {
1192 _ = writeln!(
1193 output,
1194 " {} hint: {}",
1195 help_paint("="),
1196 hint
1197 );
1198 }
1199 }
1200}
1201
1202fn write_snippet(output: &mut String, location: &SpanLocation, paint: &dyn Fn(&str) -> String) {
1204 let mut writer = SnippetWriter::new(output, |s| paint(s), location.line_num_width());
1205
1206 writer.write_header(&location.path, location.start_line, location.start_col);
1207 writer.write_empty_gutter();
1208
1209 if location.is_multiline() {
1210 write_multiline_snippet(&mut writer, location);
1211 } else {
1212 write_singleline_snippet(&mut writer, location);
1213 }
1214}
1215
1216fn write_singleline_snippet<F>(writer: &mut SnippetWriter<F>, location: &SpanLocation)
1218where
1219 F: Fn(&str) -> String,
1220{
1221 let line_text = location.lines.first().map_or("", String::as_str);
1222
1223 let span_len = location
1224 .highlight_end_col
1225 .saturating_sub(location.highlight_start_col)
1226 .max(1);
1227
1228 writer.write_source_line(
1229 location.start_line,
1230 line_text,
1231 None,
1232 Some((location.highlight_start_col, location.highlight_end_col)),
1233 );
1234 writer.write_single_line_marker(location.highlight_start_col, span_len);
1235}
1236
1237fn write_multiline_snippet<F>(writer: &mut SnippetWriter<F>, location: &SpanLocation)
1239where
1240 F: Fn(&str) -> String,
1241{
1242 for (i, line_text) in location.lines.iter().enumerate() {
1243 let line_num = location.start_line + i;
1244 let is_first = i == 0;
1245 let line_len = line_text.chars().count();
1246
1247 let (box_char, highlight_range) = if is_first {
1248 (
1249 gutter::SPAN_START,
1250 (location.highlight_start_col, line_len + 1),
1251 )
1252 } else {
1253 (gutter::BAR, (1, line_len + 1))
1254 };
1255
1256 writer.write_source_line(line_num, line_text, Some(box_char), Some(highlight_range));
1257 }
1258
1259 writer.write_multiline_end_marker(location.highlight_end_col);
1260}
1261
1262fn write_trace<W: World>(
1267 output: &mut String,
1268 world: &W,
1269 tracepoint: &typst::diag::Tracepoint,
1270 span: Span,
1271 help_paint: &dyn Fn(&str) -> String,
1272) {
1273 use typst::diag::Tracepoint;
1274
1275 if matches!(tracepoint, Tracepoint::Import) {
1277 return;
1278 }
1279
1280 let message = match tracepoint {
1281 Tracepoint::Call(Some(name)) => {
1282 format!("error occurred in this call of function `{name}`")
1283 }
1284 Tracepoint::Call(None) => "error occurred in this function call".into(),
1285 Tracepoint::Show(name) => format!("error occurred in this show rule for `{name}`"),
1286 Tracepoint::Import => unreachable!(), };
1288
1289 _ = writeln!(
1290 output,
1291 "{}: {}",
1292 help_paint("help"),
1293 message
1294 );
1295
1296 if let Some(location) = SpanLocation::from_span(world, span) {
1297 write_snippet(output, &location, help_paint);
1298 }
1299}
1300
1301#[cfg(test)]
1306mod tests {
1307 use super::*;
1308
1309 #[test]
1310 fn test_count_diagnostics() {
1311 let diags = vec![
1312 SourceDiagnostic::error(Span::detached(), "error 1"),
1313 SourceDiagnostic::error(Span::detached(), "error 2"),
1314 SourceDiagnostic::warning(Span::detached(), "warning 1"),
1315 SourceDiagnostic::warning(Span::detached(), "warning 2"),
1316 ];
1317
1318 let (errors, warnings) = count_diagnostics(&diags);
1319 assert_eq!(errors, 2);
1320 assert_eq!(warnings, 2);
1321 }
1322
1323 #[test]
1324 fn test_has_errors() {
1325 let warnings_only = vec![
1326 SourceDiagnostic::warning(Span::detached(), "warning 1"),
1327 SourceDiagnostic::warning(Span::detached(), "warning 2"),
1328 ];
1329 assert!(!has_errors(&warnings_only));
1330
1331 let with_errors = vec![
1332 SourceDiagnostic::warning(Span::detached(), "warning 1"),
1333 SourceDiagnostic::error(Span::detached(), "error 1"),
1334 ];
1335 assert!(has_errors(&with_errors));
1336
1337 let empty: Vec<SourceDiagnostic> = vec![];
1338 assert!(!has_errors(&empty));
1339 }
1340
1341 #[test]
1342 fn test_split_line_helper() {
1343 let (before, highlighted, after) = split_line("hello world", 6, 11);
1345 assert_eq!(before, "hello ");
1346 assert_eq!(highlighted, "world");
1347 assert_eq!(after, "");
1348
1349 let (before, highlighted, after) = split_line("abc", 0, 1);
1351 assert_eq!(before, "");
1352 assert_eq!(highlighted, "a");
1353 assert_eq!(after, "bc");
1354
1355 let (before, highlighted, after) = split_line("test", 0, 4);
1357 assert_eq!(before, "");
1358 assert_eq!(highlighted, "test");
1359 assert_eq!(after, "");
1360
1361 let (before, highlighted, after) = split_line("你好世界", 0, 2);
1363 assert_eq!(before, "");
1364 assert_eq!(highlighted, "你好");
1365 assert_eq!(after, "世界");
1366 }
1367
1368 #[test]
1369 fn test_span_location_methods() {
1370 let location = SpanLocation {
1371 path: "test.typ".to_string(),
1372 start_line: 10,
1373 start_col: 0, lines: vec!["line1".into(), "line2".into(), "line3".into()],
1375 highlight_start_col: 0,
1376 highlight_end_col: 5,
1377 };
1378
1379 assert!(location.is_multiline());
1380 assert_eq!(location.end_line(), 12);
1381 assert_eq!(location.line_num_width(), 2);
1382
1383 let single_line = SpanLocation {
1384 path: "test.typ".to_string(),
1385 start_line: 5,
1386 start_col: 0, lines: vec!["single".into()],
1388 highlight_start_col: 0,
1389 highlight_end_col: 6,
1390 };
1391
1392 assert!(!single_line.is_multiline());
1393 assert_eq!(single_line.end_line(), 5);
1394 assert_eq!(single_line.line_num_width(), 1);
1395 }
1396
1397 #[test]
1398 fn test_filter_html_warnings() {
1399 let diags = vec![
1400 SourceDiagnostic::error(Span::detached(), "error 1"),
1401 SourceDiagnostic::warning(
1402 Span::detached(),
1403 "html export is under active development",
1404 ),
1405 SourceDiagnostic::warning(Span::detached(), "other warning"),
1406 ];
1407
1408 let filtered = filter_html_warnings(&diags);
1409 assert_eq!(filtered.len(), 2);
1410 assert!(filtered.iter().any(|d| d.message == "error 1"));
1411 assert!(filtered.iter().any(|d| d.message == "other warning"));
1412 }
1413}