1mod line_wrap;
2
3use std::borrow::Cow;
4use std::error::Error;
5use std::fmt;
6use std::iter;
7
8use owo_colors::{AnsiColors, DynColor, OwoColorize};
9
10use line_wrap::{get_wrap_width, wrap_text};
11
12pub trait Hinted {
18 fn hints(&self) -> Hints<'_> {
20 Hints::none()
21 }
22}
23
24#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
26pub enum HintOrdering {
27 First,
29 #[default]
31 Any,
32 Last,
34}
35
36pub struct Hint<'a> {
38 message: Cow<'a, str>,
39 ordering: HintOrdering,
40}
41
42impl<'a> Hint<'a> {
43 pub fn new(message: impl Into<Cow<'a, str>>) -> Self {
45 Self {
46 message: message.into(),
47 ordering: HintOrdering::default(),
48 }
49 }
50
51 #[must_use]
53 pub fn with_ordering(mut self, ordering: HintOrdering) -> Self {
54 self.ordering = ordering;
55 self
56 }
57
58 fn into_owned(self) -> Hint<'static> {
60 Hint {
61 message: Cow::Owned(self.message.into_owned()),
62 ordering: self.ordering,
63 }
64 }
65}
66
67impl<'a> From<&'a str> for Hint<'a> {
68 fn from(message: &'a str) -> Self {
69 Self::new(message)
70 }
71}
72
73impl From<String> for Hint<'_> {
74 fn from(message: String) -> Self {
75 Self::new(message)
76 }
77}
78
79pub struct Hints<'a>(Vec<Hint<'a>>);
84
85impl<'a> Hints<'a> {
86 pub fn none() -> Self {
88 Self(Vec::new())
89 }
90
91 pub fn push(&mut self, hint: impl Into<Hint<'a>>) {
93 self.0.push(hint.into());
94 }
95
96 #[must_use]
98 pub fn with_ordering(mut self, ordering: HintOrdering) -> Self {
99 for hint in &mut self.0 {
100 hint.ordering = ordering;
101 }
102 self
103 }
104
105 pub fn into_owned(self) -> Hints<'static> {
107 Hints(self.0.into_iter().map(Hint::into_owned).collect())
108 }
109
110 fn is_empty(&self) -> bool {
112 self.0.is_empty()
113 }
114
115 pub fn iter(&self) -> HintsIter<'_, 'a> {
117 HintsIter {
118 hints: &self.0,
119 current: self.0.iter(),
120 ordering: HintOrdering::First,
121 remaining: [HintOrdering::Any, HintOrdering::Last].into_iter(),
122 }
123 }
124
125 pub fn extend(&mut self, other: Hints<'_>) {
129 for hint in other.0 {
130 if let Some(existing) = self
131 .0
132 .iter_mut()
133 .find(|existing| existing.message == hint.message)
134 {
135 existing.ordering = existing.ordering.min(hint.ordering);
136 } else {
137 self.0.push(hint.into_owned());
138 }
139 }
140 }
141}
142
143pub struct HintsIter<'h, 'a> {
145 hints: &'h [Hint<'a>],
146 current: std::slice::Iter<'h, Hint<'a>>,
147 ordering: HintOrdering,
148 remaining: std::array::IntoIter<HintOrdering, 2>,
149}
150
151impl<'h> Iterator for HintsIter<'h, '_> {
152 type Item = &'h str;
153
154 fn next(&mut self) -> Option<Self::Item> {
155 loop {
156 if let Some(hint) = self.current.find(|hint| hint.ordering == self.ordering) {
157 return Some(hint.message.as_ref());
158 }
159 self.ordering = self.remaining.next()?;
160 self.current = self.hints.iter();
161 }
162 }
163}
164
165impl<'h, 'a> IntoIterator for &'h Hints<'a> {
166 type Item = &'h str;
167 type IntoIter = HintsIter<'h, 'a>;
168
169 fn into_iter(self) -> Self::IntoIter {
170 self.iter()
171 }
172}
173
174impl<'a> IntoIterator for Hints<'a> {
175 type Item = Cow<'a, str>;
176 type IntoIter = std::iter::Map<std::vec::IntoIter<Hint<'a>>, fn(Hint<'a>) -> Cow<'a, str>>;
177
178 fn into_iter(mut self) -> Self::IntoIter {
179 self.0.sort_by_key(|hint| hint.ordering);
180 self.0.into_iter().map(|hint| hint.message)
181 }
182}
183
184pub struct ErrorWithHints<'a, E> {
189 error: E,
190 hints: Hints<'a>,
191}
192
193impl<'a, E> ErrorWithHints<'a, E> {
194 pub fn new(error: E, hints: Hints<'a>) -> Self {
196 Self { error, hints }
197 }
198}
199
200impl<E: fmt::Display> fmt::Display for ErrorWithHints<'_, E> {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(f, "{}", self.error)?;
203 if !self.hints.is_empty() {
204 writeln!(f)?;
205 write!(f, "{}", self.hints)?;
206 }
207 Ok(())
208 }
209}
210
211impl<'a> From<&'a str> for Hints<'a> {
212 fn from(hint: &'a str) -> Self {
213 Self::from(Hint::from(hint))
214 }
215}
216
217impl From<String> for Hints<'_> {
218 fn from(hint: String) -> Self {
219 Self::from(Hint::from(hint))
220 }
221}
222
223impl<'a> From<Hint<'a>> for Hints<'a> {
224 fn from(hint: Hint<'a>) -> Self {
225 Self(vec![hint])
226 }
227}
228
229impl<'a, T: Into<Hint<'a>>> FromIterator<T> for Hints<'a> {
230 fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
231 Self(iter.into_iter().map(Into::into).collect())
232 }
233}
234
235impl fmt::Display for Hints<'_> {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 for hint in self {
238 write!(f, "\n{HintPrefix} {hint}")?;
239 }
240 Ok(())
241 }
242}
243
244pub struct HintPrefix;
246
247impl fmt::Display for HintPrefix {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 write!(f, "{}{}", "hint".bold().cyan(), ":".bold())
250 }
251}
252
253#[must_use]
255pub struct ErrorOptions<'a, C = AnsiColors, W = Stderr> {
256 level: Cow<'a, str>,
257 color: C,
258 width_override: Option<usize>,
259 stream: W,
260}
261
262#[derive(Debug, Clone, Copy, Default)]
264pub struct Stderr;
265
266impl fmt::Write for Stderr {
267 fn write_str(&mut self, output: &str) -> fmt::Result {
268 anstream::eprint!("{output}");
269 Ok(())
270 }
271}
272
273impl Default for ErrorOptions<'_, AnsiColors, Stderr> {
274 fn default() -> Self {
275 Self {
276 level: Cow::Borrowed("error"),
277 color: AnsiColors::Red,
278 width_override: None,
279 stream: Stderr,
280 }
281 }
282}
283
284impl<'a, C, W> ErrorOptions<'a, C, W> {
285 pub fn with_level(mut self, level: impl Into<Cow<'a, str>>) -> Self {
287 self.level = level.into();
288 self
289 }
290
291 pub fn with_color<D>(self, color: D) -> ErrorOptions<'a, D, W> {
293 ErrorOptions {
294 level: self.level,
295 color,
296 width_override: self.width_override,
297 stream: self.stream,
298 }
299 }
300
301 #[cfg(test)]
305 fn with_width_override(mut self, width_override: usize) -> Self {
306 self.width_override = Some(width_override);
307 self
308 }
309
310 pub fn with_stream<D>(self, stream: D) -> ErrorOptions<'a, C, D> {
312 ErrorOptions {
313 level: self.level,
314 color: self.color,
315 width_override: self.width_override,
316 stream,
317 }
318 }
319}
320
321pub fn write_error_chain(err: &dyn Error, hints: &Hints<'_>) -> fmt::Result {
324 write_error_chain_with_options(err, hints, ErrorOptions::default())
325}
326
327pub fn debug_error_chain(err: &dyn Error) -> impl fmt::Display + '_ {
329 DebugErrorChain(err)
330}
331
332struct DebugErrorChain<'a>(&'a dyn Error);
333
334impl fmt::Display for DebugErrorChain<'_> {
335 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
336 for (index, error) in iter::successors(Some(self.0), |&error| error.source()).enumerate() {
337 if index > 0 {
338 formatter.write_str("\n")?;
339 }
340 write!(formatter, "{index}: {error:?}")?;
341 }
342 Ok(())
343 }
344}
345
346pub fn write_error_chain_with_options<C: DynColor + Copy, W: fmt::Write>(
350 err: &dyn Error,
351 hints: &Hints<'_>,
352 options: ErrorOptions<'_, C, W>,
353) -> fmt::Result {
354 let ErrorOptions {
355 level,
356 color,
357 width_override,
358 mut stream,
359 } = options;
360 let width = get_wrap_width(width_override);
361
362 let main_msg = err.to_string();
363 let main_padding = " ".repeat(level.len() + 2);
364 let wrapped_main = wrap_text(&main_msg, width, &main_padding, &main_padding, "");
365 writeln!(
366 &mut stream,
367 "{}{} {}",
368 level.as_ref().color(color).bold(),
369 ":".bold(),
370 wrapped_main.trim()
371 )?;
372
373 for source in iter::successors(err.source(), |&err| err.source()) {
374 let msg = source.to_string();
375 let wrapped = wrap_text(&msg, width.map(|width| width.saturating_sub(9)), "", "", "");
378
379 let mut lines = wrapped.lines();
380 if let Some(first) = lines.next() {
381 writeln!(
382 &mut stream,
383 " {}{} {}",
384 "cause".color(color).bold(),
385 ":".bold(),
386 first.trim()
387 )?;
388 for line in lines {
389 if line.trim().is_empty() {
390 writeln!(&mut stream)?;
391 } else {
392 writeln!(&mut stream, " {line}")?;
393 }
394 }
395 }
396 }
397
398 for hint in hints {
399 writeln!(&mut stream, "\n{HintPrefix} {hint}")?;
400 }
401
402 Ok(())
403}
404
405#[cfg(test)]
406mod tests {
407 use anyhow::anyhow;
408 use indoc::indoc;
409 use insta::{assert_debug_snapshot, assert_snapshot};
410 use owo_colors::AnsiColors;
411
412 use super::{
413 ErrorOptions, ErrorWithHints, Hint, HintOrdering, Hints, debug_error_chain,
414 write_error_chain_with_options,
415 };
416
417 #[test]
418 fn extend_deduplicates_matching_hints() {
419 let mut hints = Hints::from("same");
420 hints.extend(Hints::from("same"));
421 hints.extend(Hints::from("other"));
422
423 let hints = hints.iter().collect::<Vec<_>>();
424 assert_debug_snapshot!(hints, @r#"
425 [
426 "same",
427 "other",
428 ]
429 "#);
430 }
431
432 #[test]
433 fn hint_ordering_retains_insertion_order() {
434 let mut hints = Hints::from("any 1");
435 hints.push(Hint::new("last 1").with_ordering(HintOrdering::Last));
436 hints.push(Hint::new("first 1").with_ordering(HintOrdering::First));
437 hints.push("any 2".to_string());
438 hints.extend(Hints::from("last 2").with_ordering(HintOrdering::Last));
439 hints.extend(Hints::from("first 2").with_ordering(HintOrdering::First));
440
441 assert_snapshot!(anstream::adapter::strip_str(&hints.to_string()), @"
442 hint: first 1
443 hint: first 2
444 hint: any 1
445 hint: any 2
446 hint: last 1
447 hint: last 2
448 ");
449 assert_debug_snapshot!((&hints).into_iter().collect::<Vec<_>>(), @r#"
450 [
451 "first 1",
452 "first 2",
453 "any 1",
454 "any 2",
455 "last 1",
456 "last 2",
457 ]
458 "#);
459 assert_debug_snapshot!(hints.into_iter().collect::<Vec<_>>(), @r#"
460 [
461 "first 1",
462 "first 2",
463 "any 1",
464 "any 2",
465 "last 1",
466 "last 2",
467 ]
468 "#);
469 }
470
471 #[test]
472 fn changing_ordering_retains_insertion_order() {
473 let hints = [
474 Hint::new("last").with_ordering(HintOrdering::Last),
475 Hint::new("first").with_ordering(HintOrdering::First),
476 Hint::new("any"),
477 ]
478 .into_iter()
479 .collect::<Hints<'_>>()
480 .with_ordering(HintOrdering::Any);
481
482 assert_debug_snapshot!(hints.iter().collect::<Vec<_>>(), @r#"
483 [
484 "last",
485 "first",
486 "any",
487 ]
488 "#);
489 }
490
491 #[test]
492 fn duplicate_hints_retain_the_earliest_ordering() {
493 let message = String::from("shared");
494 let mut hints = Hints::from(message.as_str())
495 .with_ordering(HintOrdering::Last)
496 .into_owned();
497 drop(message);
498
499 hints.extend(Hints::from("first").with_ordering(HintOrdering::First));
500 hints.extend(Hints::from("shared").with_ordering(HintOrdering::First));
501 hints.extend(Hints::from("shared").with_ordering(HintOrdering::Last));
502 hints.extend(Hints::from("any"));
503
504 assert_snapshot!(anstream::adapter::strip_str(&hints.to_string()), @"
505 hint: shared
506 hint: first
507 hint: any
508 ");
509 }
510
511 #[test]
512 fn error_with_hints_separates_hints_from_error() {
513 let output = ErrorWithHints::new("error", Hints::from("fix it")).to_string();
514 assert_snapshot!(anstream::adapter::strip_str(&output), @"
515 error
516
517 hint: fix it
518 ");
519 assert_snapshot!(ErrorWithHints::new("error", Hints::none()), @"error");
520 }
521
522 #[test]
523 fn test_error_wrapping_with_columns() {
524 #[derive(Debug, thiserror::Error)]
525 #[error(
526 "Because fiasobfhuasbf was not found in the package registry and you require fiasobfhuasbf, we can conclude that your requirements are unsatisfiable."
527 )]
528 struct Inner;
529
530 #[derive(Debug, thiserror::Error)]
531 #[error("No solution found when resolving dependencies")]
532 struct Outer {
533 #[source]
534 source: Inner,
535 }
536
537 let error = Outer { source: Inner };
538 let mut output = String::new();
539 write_error_chain_with_options(
540 &error,
541 &Hints::none(),
542 ErrorOptions::default()
543 .with_width_override(80)
544 .with_stream(&mut output),
545 )
546 .unwrap();
547 let output = anstream::adapter::strip_str(&output);
548
549 assert_snapshot!(output, @"
550 error: No solution found when resolving dependencies
551 cause: Because fiasobfhuasbf was not found in the package registry and you
552 require fiasobfhuasbf, we can conclude that your requirements are
553 unsatisfiable.
554 ");
555 }
556
557 #[test]
558 fn test_error_chain_with_cause() {
559 #[derive(Debug, thiserror::Error)]
560 #[error("Permission denied")]
561 struct Inner;
562
563 #[derive(Debug, thiserror::Error)]
564 #[error("Failed to write file")]
565 struct Outer {
566 #[source]
567 source: Inner,
568 }
569
570 let error = Outer { source: Inner };
571 let mut output = String::new();
572 write_error_chain_with_options(
573 &error,
574 &Hints::none(),
575 ErrorOptions::default().with_stream(&mut output),
576 )
577 .unwrap();
578 assert_snapshot!(format!("{output:?}"), @r#""\u{1b}[1m\u{1b}[31merror\u{1b}[39m\u{1b}[0m\u{1b}[1m:\u{1b}[0m Failed to write file\n \u{1b}[1m\u{1b}[31mcause\u{1b}[39m\u{1b}[0m\u{1b}[1m:\u{1b}[0m Permission denied\n""#);
579 let output = anstream::adapter::strip_str(&output);
580
581 assert_snapshot!(output, @"
582 error: Failed to write file
583 cause: Permission denied
584 ");
585 }
586
587 #[test]
588 fn formats_debug_error_chain() {
589 #[derive(Debug, thiserror::Error)]
590 #[error("inner error")]
591 struct InnerError {
592 code: u8,
593 }
594
595 #[derive(Debug, thiserror::Error)]
596 #[error("outer error")]
597 struct OuterError {
598 #[source]
599 source: InnerError,
600 }
601
602 let error = OuterError {
603 source: InnerError { code: 42 },
604 };
605
606 assert_eq!(
607 debug_error_chain(&error).to_string(),
608 indoc! {"
609 0: OuterError { source: InnerError { code: 42 } }
610 1: InnerError { code: 42 }"
611 }
612 );
613 }
614
615 #[test]
616 fn format_with_custom_level() {
617 let error = anyhow!("Failed to create registry entry");
618 let mut output = String::new();
619 write_error_chain_with_options(
620 error.as_ref(),
621 &Hints::none(),
622 ErrorOptions::default()
623 .with_level("warning")
624 .with_color(AnsiColors::Yellow)
625 .with_stream(&mut output),
626 )
627 .unwrap();
628 let output = anstream::adapter::strip_str(&output);
629
630 assert_snapshot!(output, @"warning: Failed to create registry entry
631");
632 }
633
634 #[test]
635 fn test_no_hyphenation() {
636 #[derive(Debug, thiserror::Error)]
637 #[error(
638 "Failed to download package from https://files.pythonhosted.org/packages/verylongpackagename"
639 )]
640 struct LongWord;
641
642 let error = LongWord;
643 let mut output = String::new();
644 write_error_chain_with_options(
645 &error,
646 &Hints::none(),
647 ErrorOptions::default()
648 .with_width_override(50)
649 .with_stream(&mut output),
650 )
651 .unwrap();
652 let output = anstream::adapter::strip_str(&output);
653 assert_snapshot!(output, @r"
654 error: Failed to download package from
655 https://files.pythonhosted.org/packages/verylongpackagename
656 ");
657 }
658
659 #[test]
660 fn test_long_words_not_broken() {
661 #[derive(Debug, thiserror::Error)]
662 #[error(
663 "The package supercalifragilisticexpialidocious-extraordinarily-long-name was not found"
664 )]
665 struct VeryLongWord;
666
667 let error = VeryLongWord;
668 let mut output = String::new();
669 write_error_chain_with_options(
670 &error,
671 &Hints::none(),
672 ErrorOptions::default()
673 .with_width_override(40)
674 .with_stream(&mut output),
675 )
676 .unwrap();
677 let output = anstream::adapter::strip_str(&output);
678 assert_snapshot!(output, @r"
679 error: The package
680 supercalifragilisticexpialidocious-extraordinarily-long-name
681 was not found
682 ");
683 }
684
685 #[test]
686 fn test_multiple_error_sources() {
687 #[derive(Debug, thiserror::Error)]
688 #[error("Network connection timeout after multiple retry attempts")]
689 struct DeepError;
690
691 #[derive(Debug, thiserror::Error)]
692 #[error("Failed to fetch package metadata from registry")]
693 struct MiddleError {
694 #[source]
695 source: DeepError,
696 }
697
698 #[derive(Debug, thiserror::Error)]
699 #[error("Unable to resolve package dependencies")]
700 struct TopError {
701 #[source]
702 source: MiddleError,
703 }
704
705 let error = TopError {
706 source: MiddleError { source: DeepError },
707 };
708 let mut output = String::new();
709 write_error_chain_with_options(
710 &error,
711 &Hints::none(),
712 ErrorOptions::default()
713 .with_width_override(40)
714 .with_stream(&mut output),
715 )
716 .unwrap();
717 let output = anstream::adapter::strip_str(&output);
718 assert_snapshot!(output, @"
719 error: Unable to resolve package
720 dependencies
721 cause: Failed to fetch package
722 metadata from registry
723 cause: Network connection timeout
724 after multiple retry attempts
725 ");
726 }
727
728 #[test]
729 fn format_cause_with_narrow_width() {
730 let error = anyhow!("one two").context("root");
731 let mut output = String::new();
732 write_error_chain_with_options(
733 error.as_ref(),
734 &Hints::none(),
735 ErrorOptions::default()
736 .with_width_override(4)
737 .with_stream(&mut output),
738 )
739 .unwrap();
740 let output = anstream::adapter::strip_str(&output);
741
742 assert_snapshot!(output, @"
743 error: root
744 cause: one
745 two
746 ");
747 }
748
749 #[test]
750 fn test_multiline_main_message_wraps_each_line() {
751 #[derive(Debug, thiserror::Error)]
752 #[error("{}", indoc! {"
753 There is no command `foobar` for `uv`. Did you mean one of:
754 auth
755 run
756 init"
757 })]
758 struct Suggestions;
759
760 let error = Suggestions;
761 let mut output = String::new();
762 write_error_chain_with_options(
763 &error,
764 &Hints::none(),
765 ErrorOptions::default()
766 .with_width_override(50)
767 .with_stream(&mut output),
768 )
769 .unwrap();
770 let output = anstream::adapter::strip_str(&output);
771
772 assert_snapshot!(output, @r"
773 error: There is no command `foobar` for `uv`. Did
774 you mean one of:
775 auth
776 run
777 init
778 ");
779 }
780
781 #[test]
782 fn test_wrap_only_on_ascii_space() {
783 #[derive(Debug, thiserror::Error)]
784 #[error("Path /usr/local/lib/python3.12/site-packages not found in filesystem hierarchy")]
785 struct SpecialChars;
786
787 let error = SpecialChars;
788 let mut output = String::new();
789 write_error_chain_with_options(
790 &error,
791 &Hints::none(),
792 ErrorOptions::default()
793 .with_width_override(50)
794 .with_stream(&mut output),
795 )
796 .unwrap();
797 let output = anstream::adapter::strip_str(&output);
798 assert_snapshot!(output, @r"
799 error: Path /usr/local/lib/python3.12/site-packages
800 not found in filesystem hierarchy
801 ");
802 }
803
804 #[test]
805 fn format_with_hints() {
806 let err = anyhow!("Permission denied").context("Failed to fetch package");
807
808 let hints = [
809 "Try running with `--verbose` for more information.".to_string(),
810 "Try running without --offline.".to_string(),
811 ]
812 .into_iter()
813 .collect();
814
815 let mut rendered = String::new();
816 write_error_chain_with_options(
817 err.as_ref(),
818 &hints,
819 ErrorOptions::default().with_stream(&mut rendered),
820 )
821 .unwrap();
822 let rendered = anstream::adapter::strip_str(&rendered);
823
824 assert_snapshot!(rendered, @"
825 error: Failed to fetch package
826 cause: Permission denied
827
828 hint: Try running with `--verbose` for more information.
829
830 hint: Try running without --offline.
831 ");
832 }
833
834 #[test]
835 fn format_multiline_message() {
836 let err_middle = indoc! {"Failed to fetch https://example.com/upload/python3.13.tar.zst
837 Server says: This endpoint only support POST requests.
838
839 For downloads, please refer to https://example.com/download/python3.13.tar.zst"};
840 let err = anyhow!("Caused By: HTTP Error 400")
841 .context(err_middle)
842 .context("Failed to download Python 3.12");
843
844 let mut rendered = String::new();
845 write_error_chain_with_options(
846 err.as_ref(),
847 &Hints::none(),
848 ErrorOptions::default().with_stream(&mut rendered),
849 )
850 .unwrap();
851 let rendered = anstream::adapter::strip_str(&rendered);
852
853 assert_snapshot!(rendered, @"
854 error: Failed to download Python 3.12
855 cause: Failed to fetch https://example.com/upload/python3.13.tar.zst
856 Server says: This endpoint only support POST requests.
857
858 For downloads, please refer to https://example.com/download/python3.13.tar.zst
859 cause: Caused By: HTTP Error 400
860 ");
861 }
862}