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 Hint {
18 fn hints(&self) -> Hints<'_> {
20 Hints::none()
21 }
22}
23
24pub struct Hints<'a>(Vec<Cow<'a, str>>);
28
29impl Hints<'_> {
30 pub fn none() -> Self {
32 Self(Vec::new())
33 }
34
35 pub fn push(&mut self, hint: String) {
37 self.0.push(Cow::Owned(hint));
38 }
39
40 pub fn into_owned(self) -> Hints<'static> {
42 Hints(
43 self.0
44 .into_iter()
45 .map(|cow| Cow::Owned(cow.into_owned()))
46 .collect(),
47 )
48 }
49
50 pub fn is_empty(&self) -> bool {
52 self.0.is_empty()
53 }
54
55 pub fn extend(&mut self, other: Hints<'_>) {
57 for hint in other.0 {
58 let hint = Cow::Owned(hint.into_owned());
59 if !self.0.iter().any(|existing| existing == &hint) {
60 self.0.push(hint);
61 }
62 }
63 }
64}
65
66pub struct ErrorWithHints<'a, E> {
71 error: E,
72 hints: Hints<'a>,
73}
74
75impl<'a, E> ErrorWithHints<'a, E> {
76 pub fn new(error: E, hints: Hints<'a>) -> Self {
78 Self { error, hints }
79 }
80}
81
82impl<E: fmt::Display> fmt::Display for ErrorWithHints<'_, E> {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 write!(f, "{}", self.error)?;
85 if !self.hints.is_empty() {
86 writeln!(f)?;
87 write!(f, "{}", self.hints)?;
88 }
89 Ok(())
90 }
91}
92
93impl<'a> From<&'a str> for Hints<'a> {
94 fn from(hint: &'a str) -> Self {
95 Self(vec![Cow::Borrowed(hint)])
96 }
97}
98
99impl From<String> for Hints<'_> {
100 fn from(hint: String) -> Self {
101 Self(vec![Cow::Owned(hint)])
102 }
103}
104
105impl FromIterator<String> for Hints<'_> {
106 fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
107 Self(iter.into_iter().map(Cow::Owned).collect())
108 }
109}
110
111impl fmt::Display for Hints<'_> {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 for hint in &self.0 {
114 write!(f, "\n{HintPrefix} {hint}")?;
115 }
116 Ok(())
117 }
118}
119
120impl<'a> IntoIterator for Hints<'a> {
121 type Item = Cow<'a, str>;
122 type IntoIter = std::vec::IntoIter<Cow<'a, str>>;
123
124 fn into_iter(self) -> Self::IntoIter {
125 self.0.into_iter()
126 }
127}
128
129pub struct HintPrefix;
131
132impl fmt::Display for HintPrefix {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 write!(f, "{}{}", "hint".bold().cyan(), ":".bold())
135 }
136}
137
138#[must_use]
140pub struct ErrorOptions<'a, C = AnsiColors, W = Stderr> {
141 level: Cow<'a, str>,
142 color: C,
143 width_override: Option<usize>,
144 stream: W,
145}
146
147#[derive(Debug, Clone, Copy, Default)]
149pub struct Stderr;
150
151impl fmt::Write for Stderr {
152 fn write_str(&mut self, output: &str) -> fmt::Result {
153 anstream::eprint!("{output}");
154 Ok(())
155 }
156}
157
158impl Default for ErrorOptions<'_, AnsiColors, Stderr> {
159 fn default() -> Self {
160 Self {
161 level: Cow::Borrowed("error"),
162 color: AnsiColors::Red,
163 width_override: None,
164 stream: Stderr,
165 }
166 }
167}
168
169impl<'a, C, W> ErrorOptions<'a, C, W> {
170 pub fn with_level(mut self, level: impl Into<Cow<'a, str>>) -> Self {
172 self.level = level.into();
173 self
174 }
175
176 pub fn with_color<D>(self, color: D) -> ErrorOptions<'a, D, W> {
178 ErrorOptions {
179 level: self.level,
180 color,
181 width_override: self.width_override,
182 stream: self.stream,
183 }
184 }
185
186 pub fn with_width_override(mut self, width_override: usize) -> Self {
190 self.width_override = Some(width_override);
191 self
192 }
193
194 pub fn with_stream<D>(self, stream: D) -> ErrorOptions<'a, C, D> {
196 ErrorOptions {
197 level: self.level,
198 color: self.color,
199 width_override: self.width_override,
200 stream,
201 }
202 }
203}
204
205pub fn write_error_chain(err: &dyn Error, hints: Hints<'_>) -> fmt::Result {
208 write_error_chain_with_options(err, hints, ErrorOptions::default())
209}
210
211pub fn write_error_chain_with_options<C: DynColor + Copy, W: fmt::Write>(
215 err: &dyn Error,
216 hints: Hints<'_>,
217 options: ErrorOptions<'_, C, W>,
218) -> fmt::Result {
219 let ErrorOptions {
220 level,
221 color,
222 width_override,
223 mut stream,
224 } = options;
225 let width = get_wrap_width(width_override);
226
227 let main_msg = err.to_string();
228 let main_padding = " ".repeat(level.len() + 2);
229 let wrapped_main = wrap_text(&main_msg, width, &main_padding, &main_padding);
230 writeln!(
231 &mut stream,
232 "{}{} {}",
233 level.as_ref().color(color).bold(),
234 ":".bold(),
235 wrapped_main.trim()
236 )?;
237
238 for source in iter::successors(err.source(), |&err| err.source()) {
239 let msg = source.to_string();
240 let padding = " ";
241 let cause = "Caused by";
242 let child_padding = " ".repeat(padding.len() + cause.len() + 2);
243
244 let wrapped = wrap_text(&msg, width, "", &child_padding);
245
246 let mut lines = wrapped.lines();
247 if let Some(first) = lines.next() {
248 writeln!(
249 &mut stream,
250 "{}{}: {}",
251 padding,
252 cause.color(color).bold(),
253 first.trim()
254 )?;
255 for line in lines {
256 if line.trim().is_empty() {
257 writeln!(&mut stream)?;
258 } else {
259 writeln!(&mut stream, "{line}")?;
260 }
261 }
262 }
263 }
264
265 for hint in hints {
266 writeln!(&mut stream, "\n{HintPrefix} {hint}")?;
267 }
268
269 Ok(())
270}
271
272#[cfg(test)]
273mod tests {
274 use anyhow::anyhow;
275 use indoc::indoc;
276 use insta::assert_snapshot;
277 use owo_colors::AnsiColors;
278
279 use super::{ErrorOptions, ErrorWithHints, HintPrefix, Hints, write_error_chain_with_options};
280
281 #[test]
282 fn extend_deduplicates_matching_hints() {
283 let mut hints = Hints::from("same");
284 hints.extend(Hints::from("same"));
285 hints.extend(Hints::from("other"));
286
287 let hints = hints
288 .into_iter()
289 .map(std::borrow::Cow::into_owned)
290 .collect::<Vec<_>>();
291 assert_eq!(hints, vec!["same".to_string(), "other".to_string()]);
292 }
293
294 #[test]
295 fn error_with_hints_separates_hints_from_error() {
296 assert_eq!(
297 ErrorWithHints::new("error", Hints::from("fix it")).to_string(),
298 format!("error\n\n{HintPrefix} fix it")
299 );
300 assert_eq!(
301 ErrorWithHints::new("error", Hints::none()).to_string(),
302 "error"
303 );
304 }
305
306 #[test]
307 fn test_error_wrapping_with_columns() {
308 #[derive(Debug, thiserror::Error)]
309 #[error(
310 "Because fiasobfhuasbf was not found in the package registry and you require fiasobfhuasbf, we can conclude that your requirements are unsatisfiable."
311 )]
312 struct Inner;
313
314 #[derive(Debug, thiserror::Error)]
315 #[error("No solution found when resolving dependencies")]
316 struct Outer {
317 #[source]
318 source: Inner,
319 }
320
321 let error = Outer { source: Inner };
322 let mut output = String::new();
323 write_error_chain_with_options(
324 &error,
325 Hints::none(),
326 ErrorOptions::default()
327 .with_width_override(80)
328 .with_stream(&mut output),
329 )
330 .unwrap();
331 let output = anstream::adapter::strip_str(&output);
332
333 assert_snapshot!(output, @r"
334 error: No solution found when resolving dependencies
335 Caused by: Because fiasobfhuasbf was not found in the package registry and you require
336 fiasobfhuasbf, we can conclude that your requirements are
337 unsatisfiable.
338 ");
339 }
340
341 #[test]
342 fn test_error_chain_with_cause() {
343 #[derive(Debug, thiserror::Error)]
344 #[error("Permission denied")]
345 struct Inner;
346
347 #[derive(Debug, thiserror::Error)]
348 #[error("Failed to write file")]
349 struct Outer {
350 #[source]
351 source: Inner,
352 }
353
354 let error = Outer { source: Inner };
355 let mut output = String::new();
356 write_error_chain_with_options(
357 &error,
358 Hints::none(),
359 ErrorOptions::default().with_stream(&mut output),
360 )
361 .unwrap();
362 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}[31mCaused by\u{1b}[39m\u{1b}[0m: Permission denied\n""#);
363 let output = anstream::adapter::strip_str(&output);
364
365 assert_snapshot!(output, @r"
366 error: Failed to write file
367 Caused by: Permission denied
368 ");
369 }
370
371 #[test]
372 fn format_with_custom_level() {
373 let error = anyhow!("Failed to create registry entry");
374 let mut output = String::new();
375 write_error_chain_with_options(
376 error.as_ref(),
377 Hints::none(),
378 ErrorOptions::default()
379 .with_level("warning")
380 .with_color(AnsiColors::Yellow)
381 .with_stream(&mut output),
382 )
383 .unwrap();
384 let output = anstream::adapter::strip_str(&output);
385
386 assert_snapshot!(output, @"warning: Failed to create registry entry
387");
388 }
389
390 #[test]
391 fn test_no_hyphenation() {
392 #[derive(Debug, thiserror::Error)]
393 #[error(
394 "Failed to download package from https://files.pythonhosted.org/packages/verylongpackagename"
395 )]
396 struct LongWord;
397
398 let error = LongWord;
399 let mut output = String::new();
400 write_error_chain_with_options(
401 &error,
402 Hints::none(),
403 ErrorOptions::default()
404 .with_width_override(50)
405 .with_stream(&mut output),
406 )
407 .unwrap();
408 let output = anstream::adapter::strip_str(&output);
409 assert_snapshot!(output, @r"
410 error: Failed to download package from
411 https://files.pythonhosted.org/packages/verylongpackagename
412 ");
413 }
414
415 #[test]
416 fn test_long_words_not_broken() {
417 #[derive(Debug, thiserror::Error)]
418 #[error(
419 "The package supercalifragilisticexpialidocious-extraordinarily-long-name was not found"
420 )]
421 struct VeryLongWord;
422
423 let error = VeryLongWord;
424 let mut output = String::new();
425 write_error_chain_with_options(
426 &error,
427 Hints::none(),
428 ErrorOptions::default()
429 .with_width_override(40)
430 .with_stream(&mut output),
431 )
432 .unwrap();
433 let output = anstream::adapter::strip_str(&output);
434 assert_snapshot!(output, @r"
435 error: The package
436 supercalifragilisticexpialidocious-extraordinarily-long-name
437 was not found
438 ");
439 }
440
441 #[test]
442 fn test_multiple_error_sources() {
443 #[derive(Debug, thiserror::Error)]
444 #[error("Network connection timeout after multiple retry attempts")]
445 struct DeepError;
446
447 #[derive(Debug, thiserror::Error)]
448 #[error("Failed to fetch package metadata from registry")]
449 struct MiddleError {
450 #[source]
451 source: DeepError,
452 }
453
454 #[derive(Debug, thiserror::Error)]
455 #[error("Unable to resolve package dependencies")]
456 struct TopError {
457 #[source]
458 source: MiddleError,
459 }
460
461 let error = TopError {
462 source: MiddleError { source: DeepError },
463 };
464 let mut output = String::new();
465 write_error_chain_with_options(
466 &error,
467 Hints::none(),
468 ErrorOptions::default()
469 .with_width_override(60)
470 .with_stream(&mut output),
471 )
472 .unwrap();
473 let output = anstream::adapter::strip_str(&output);
474 assert_snapshot!(output, @r"
475 error: Unable to resolve package dependencies
476 Caused by: Failed to fetch package metadata from registry
477 Caused by: Network connection timeout after multiple retry attempts
478 ");
479 }
480
481 #[test]
482 fn test_multiline_main_message_wraps_each_line() {
483 #[derive(Debug, thiserror::Error)]
484 #[error(
485 "There is no command `foobar` for `uv`. Did you mean one of:\n auth\n run\n init"
486 )]
487 struct Suggestions;
488
489 let error = Suggestions;
490 let mut output = String::new();
491 write_error_chain_with_options(
492 &error,
493 Hints::none(),
494 ErrorOptions::default()
495 .with_width_override(50)
496 .with_stream(&mut output),
497 )
498 .unwrap();
499 let output = anstream::adapter::strip_str(&output);
500
501 assert_snapshot!(output, @r"
502 error: There is no command `foobar` for `uv`. Did
503 you mean one of:
504 auth
505 run
506 init
507 ");
508 }
509
510 #[test]
511 fn test_wrap_only_on_ascii_space() {
512 #[derive(Debug, thiserror::Error)]
513 #[error("Path /usr/local/lib/python3.12/site-packages not found in filesystem hierarchy")]
514 struct SpecialChars;
515
516 let error = SpecialChars;
517 let mut output = String::new();
518 write_error_chain_with_options(
519 &error,
520 Hints::none(),
521 ErrorOptions::default()
522 .with_width_override(50)
523 .with_stream(&mut output),
524 )
525 .unwrap();
526 let output = anstream::adapter::strip_str(&output);
527 assert_snapshot!(output, @r"
528 error: Path /usr/local/lib/python3.12/site-packages
529 not found in filesystem hierarchy
530 ");
531 }
532
533 #[test]
534 fn format_with_hints() {
535 let err = anyhow!("Permission denied").context("Failed to fetch package");
536
537 let hints = [
538 "Try running with `--verbose` for more information.".to_string(),
539 "Try running without --offline.".to_string(),
540 ]
541 .into_iter()
542 .collect();
543
544 let mut rendered = String::new();
545 write_error_chain_with_options(
546 err.as_ref(),
547 hints,
548 ErrorOptions::default().with_stream(&mut rendered),
549 )
550 .unwrap();
551 let rendered = anstream::adapter::strip_str(&rendered);
552
553 assert_snapshot!(rendered, @r"
554 error: Failed to fetch package
555 Caused by: Permission denied
556
557 hint: Try running with `--verbose` for more information.
558
559 hint: Try running without --offline.
560 ");
561 }
562
563 #[test]
564 fn format_multiline_message() {
565 let err_middle = indoc! {"Failed to fetch https://example.com/upload/python3.13.tar.zst
566 Server says: This endpoint only support POST requests.
567
568 For downloads, please refer to https://example.com/download/python3.13.tar.zst"};
569 let err = anyhow!("Caused By: HTTP Error 400")
570 .context(err_middle)
571 .context("Failed to download Python 3.12");
572
573 let mut rendered = String::new();
574 write_error_chain_with_options(
575 err.as_ref(),
576 Hints::none(),
577 ErrorOptions::default().with_stream(&mut rendered),
578 )
579 .unwrap();
580 let rendered = anstream::adapter::strip_str(&rendered);
581
582 assert_snapshot!(rendered, @r"
583 error: Failed to download Python 3.12
584 Caused by: Failed to fetch https://example.com/upload/python3.13.tar.zst
585 Server says: This endpoint only support POST requests.
586
587 For downloads, please refer to https://example.com/download/python3.13.tar.zst
588 Caused by: Caused By: HTTP Error 400
589 ");
590 }
591}