1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4mod display;
5mod draw;
6mod source;
7mod write;
8
9pub use crate::{
10 draw::{ColorGenerator, Fmt},
11 source::{sources, Cache, FileCache, FnCache, Line, Source},
12};
13pub use yansi::Color;
14
15#[cfg(any(feature = "concolor", doc))]
16pub use crate::draw::StdoutFmt;
17
18use crate::display::*;
19use std::{
20 cmp::{Eq, PartialEq},
21 fmt,
22 hash::Hash,
23 io::{self, Write},
24 ops::Range,
25 ops::RangeInclusive,
26};
27use unicode_width::UnicodeWidthChar;
28
29pub trait Span {
31 type SourceId: PartialEq + ToOwned + ?Sized;
33
34 fn source(&self) -> &Self::SourceId;
36
37 fn start(&self) -> usize;
41
42 fn end(&self) -> usize;
48
49 fn len(&self) -> usize {
51 self.end().saturating_sub(self.start())
52 }
53
54 fn is_empty(&self) -> bool {
56 self.len() == 0
57 }
58
59 fn contains(&self, offset: usize) -> bool {
61 (self.start()..self.end()).contains(&offset)
62 }
63}
64
65impl Span for Range<usize> {
66 type SourceId = ();
67
68 fn source(&self) -> &Self::SourceId {
69 &()
70 }
71 fn start(&self) -> usize {
72 self.start
73 }
74 fn end(&self) -> usize {
75 self.end
76 }
77}
78
79impl<Id: fmt::Debug + Hash + PartialEq + Eq + ToOwned> Span for (Id, Range<usize>) {
80 type SourceId = Id;
81
82 fn source(&self) -> &Self::SourceId {
83 &self.0
84 }
85 fn start(&self) -> usize {
86 self.1.start
87 }
88 fn end(&self) -> usize {
89 self.1.end
90 }
91}
92
93impl Span for RangeInclusive<usize> {
94 type SourceId = ();
95
96 fn source(&self) -> &Self::SourceId {
97 &()
98 }
99 fn start(&self) -> usize {
100 *self.start()
101 }
102 fn end(&self) -> usize {
103 *self.end() + 1
104 }
105}
106
107impl<Id: fmt::Debug + Hash + PartialEq + Eq + ToOwned> Span for (Id, RangeInclusive<usize>) {
108 type SourceId = Id;
109
110 fn source(&self) -> &Self::SourceId {
111 &self.0
112 }
113 fn start(&self) -> usize {
114 *self.1.start()
115 }
116 fn end(&self) -> usize {
117 *self.1.end() + 1
118 }
119}
120
121#[derive(Clone, Debug, Hash, PartialEq, Eq)]
123struct LabelDisplay {
124 msg: Option<String>,
125 color: Option<Color>,
126 order: i32,
127 priority: i32,
128}
129
130#[derive(Clone, Debug, Hash, PartialEq, Eq)]
132pub struct Label<S = Range<usize>> {
133 span: S,
134 display_info: LabelDisplay,
135}
136
137impl<S: Span> Label<S> {
138 pub fn new(span: S) -> Self {
145 assert!(span.start() <= span.end(), "Label start is after its end");
146
147 Self {
148 span,
149 display_info: LabelDisplay {
150 msg: None,
151 color: None,
152 order: 0,
153 priority: 0,
154 },
155 }
156 }
157
158 pub fn with_message<M: ToString>(mut self, msg: M) -> Self {
160 self.display_info.msg = Some(msg.to_string());
161 self
162 }
163
164 pub fn with_color(mut self, color: Color) -> Self {
166 self.display_info.color = Some(color);
167 self
168 }
169
170 pub fn with_order(mut self, order: i32) -> Self {
181 self.display_info.order = order;
182 self
183 }
184
185 pub fn with_priority(mut self, priority: i32) -> Self {
195 self.display_info.priority = priority;
196 self
197 }
198}
199
200pub struct Report<'a, S: Span = Range<usize>> {
202 kind: ReportKind<'a>,
203 code: Option<String>,
204 msg: Option<String>,
205 notes: Vec<String>,
206 help: Vec<String>,
207 span: S,
208 labels: Vec<Label<S>>,
209 config: Config,
210}
211
212impl<S: Span> Report<'_, S> {
213 pub fn build(kind: ReportKind, span: S) -> ReportBuilder<S> {
217 ReportBuilder {
218 kind,
219 code: None,
220 msg: None,
221 notes: vec![],
222 help: vec![],
223 span,
224 labels: Vec::new(),
225 config: Config::default(),
226 }
227 }
228
229 pub fn eprint<C: Cache<S::SourceId>>(&self, cache: C) -> io::Result<()> {
231 self.write(cache, io::stderr())
232 }
233
234 pub fn print<C: Cache<S::SourceId>>(&self, cache: C) -> io::Result<()> {
239 self.write_for_stdout(cache, io::stdout())
240 }
241}
242
243impl<'a, S: Span> fmt::Debug for Report<'a, S> {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 f.debug_struct("Report")
246 .field("kind", &self.kind)
247 .field("code", &self.code)
248 .field("msg", &self.msg)
249 .field("notes", &self.notes)
250 .field("help", &self.help)
251 .field("config", &self.config)
252 .finish()
253 }
254}
255#[derive(Copy, Clone, Debug, PartialEq, Eq)]
257pub enum ReportKind<'a> {
258 Error,
261 Warning,
264 Advice,
266 Custom(&'a str, Color),
268}
269
270impl fmt::Display for ReportKind<'_> {
271 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
272 match self {
273 ReportKind::Error => write!(f, "Error"),
274 ReportKind::Warning => write!(f, "Warning"),
275 ReportKind::Advice => write!(f, "Advice"),
276 ReportKind::Custom(s, _) => write!(f, "{}", s),
277 }
278 }
279}
280
281pub struct ReportBuilder<'a, S: Span> {
283 kind: ReportKind<'a>,
284 code: Option<String>,
285 msg: Option<String>,
286 notes: Vec<String>,
287 help: Vec<String>,
288 span: S,
289 labels: Vec<Label<S>>,
290 config: Config,
291}
292
293impl<'a, S: Span> ReportBuilder<'a, S> {
294 pub fn with_code<C: fmt::Display>(mut self, code: C) -> Self {
296 self.code = Some(format!("{:02}", code));
297 self
298 }
299
300 pub fn set_message<M: ToString>(&mut self, msg: M) {
302 self.msg = Some(msg.to_string());
303 }
304
305 pub fn with_message<M: ToString>(mut self, msg: M) -> Self {
307 self.msg = Some(msg.to_string());
308 self
309 }
310
311 pub fn set_note<N: ToString>(&mut self, note: N) {
313 self.notes = vec![note.to_string()];
314 }
315
316 pub fn add_note<N: ToString>(&mut self, note: N) {
318 self.notes.push(note.to_string());
319 }
320
321 pub fn with_notes<N: IntoIterator<Item = impl ToString>>(&mut self, notes: N) {
323 for note in notes {
324 self.add_note(note)
325 }
326 }
327
328 pub fn with_note<N: ToString>(mut self, note: N) -> Self {
330 self.add_note(note);
331 self
332 }
333
334 pub fn set_help<N: ToString>(&mut self, note: N) {
336 self.help = vec![note.to_string()];
337 }
338
339 pub fn add_help<N: ToString>(&mut self, note: N) {
341 self.help.push(note.to_string());
342 }
343
344 pub fn with_helps<N: IntoIterator<Item = impl ToString>>(&mut self, helps: N) {
346 for help in helps {
347 self.add_help(help)
348 }
349 }
350
351 pub fn with_help<N: ToString>(mut self, note: N) -> Self {
353 self.add_help(note);
354 self
355 }
356
357 pub fn add_label(&mut self, label: Label<S>) {
359 self.add_labels(std::iter::once(label));
360 }
361
362 pub fn add_labels<L: IntoIterator<Item = Label<S>>>(&mut self, labels: L) {
364 let config = &self.config; self.labels.extend(labels.into_iter().map(|mut label| {
366 label.display_info.color = config.filter_color(label.display_info.color);
367 label
368 }));
369 }
370
371 pub fn with_label(mut self, label: Label<S>) -> Self {
373 self.add_label(label);
374 self
375 }
376
377 pub fn with_labels<L: IntoIterator<Item = Label<S>>>(mut self, labels: L) -> Self {
379 self.add_labels(labels);
380 self
381 }
382
383 pub fn with_config(mut self, config: Config) -> Self {
385 self.config = config;
386 self
387 }
388
389 pub fn finish(self) -> Report<'a, S> {
391 Report {
392 kind: self.kind,
393 code: self.code,
394 msg: self.msg,
395 notes: self.notes,
396 help: self.help,
397 span: self.span,
398 labels: self.labels,
399 config: self.config,
400 }
401 }
402}
403
404impl<'a, S: Span> fmt::Debug for ReportBuilder<'a, S> {
405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406 f.debug_struct("ReportBuilder")
407 .field("kind", &self.kind)
408 .field("code", &self.code)
409 .field("msg", &self.msg)
410 .field("notes", &self.notes)
411 .field("help", &self.help)
412 .field("config", &self.config)
413 .finish()
414 }
415}
416
417#[derive(Copy, Clone, Debug, PartialEq, Eq)]
419pub enum LabelAttach {
420 Start,
422 Middle,
424 End,
426}
427
428#[derive(Copy, Clone, Debug, PartialEq, Eq)]
430pub enum CharSet {
431 Unicode,
433 Ascii,
435}
436
437#[derive(Copy, Clone, Debug, PartialEq, Eq)]
439pub enum IndexType {
440 Byte,
442 Char,
444}
445
446#[derive(Copy, Clone, Debug, PartialEq, Eq)]
448pub struct Config {
449 cross_gap: bool,
450 label_attach: LabelAttach,
451 compact: bool,
452 underlines: bool,
453 multiline_arrows: bool,
454 color: bool,
455 tab_width: usize,
456 char_set: CharSet,
457 index_type: IndexType,
458}
459
460impl Config {
461 pub const fn with_cross_gap(mut self, cross_gap: bool) -> Self {
467 self.cross_gap = cross_gap;
468 self
469 }
470 pub const fn with_label_attach(mut self, label_attach: LabelAttach) -> Self {
474 self.label_attach = label_attach;
475 self
476 }
477 pub const fn with_compact(mut self, compact: bool) -> Self {
481 self.compact = compact;
482 self
483 }
484 pub const fn with_underlines(mut self, underlines: bool) -> Self {
488 self.underlines = underlines;
489 self
490 }
491 pub const fn with_multiline_arrows(mut self, multiline_arrows: bool) -> Self {
495 self.multiline_arrows = multiline_arrows;
496 self
497 }
498 pub const fn with_color(mut self, color: bool) -> Self {
502 self.color = color;
503 self
504 }
505 pub const fn with_tab_width(mut self, tab_width: usize) -> Self {
509 self.tab_width = tab_width;
510 self
511 }
512 pub const fn with_char_set(mut self, char_set: CharSet) -> Self {
516 self.char_set = char_set;
517 self
518 }
519 pub const fn with_index_type(mut self, index_type: IndexType) -> Self {
523 self.index_type = index_type;
524 self
525 }
526
527 fn error_color(&self) -> Option<Color> {
528 Some(Color::Red).filter(|_| self.color)
529 }
530 fn warning_color(&self) -> Option<Color> {
531 Some(Color::Yellow).filter(|_| self.color)
532 }
533 fn advice_color(&self) -> Option<Color> {
534 Some(Color::Fixed(147)).filter(|_| self.color)
535 }
536 fn margin_color(&self) -> Option<Color> {
537 Some(Color::Fixed(246)).filter(|_| self.color)
538 }
539 fn skipped_margin_color(&self) -> Option<Color> {
540 Some(Color::Fixed(240)).filter(|_| self.color)
541 }
542 fn unimportant_color(&self) -> Option<Color> {
543 Some(Color::Fixed(249)).filter(|_| self.color)
544 }
545 fn note_color(&self) -> Option<Color> {
546 Some(Color::Fixed(115)).filter(|_| self.color)
547 }
548 fn filter_color(&self, color: Option<Color>) -> Option<Color> {
549 color.filter(|_| self.color)
550 }
551
552 fn char_width(&self, c: char, col: usize) -> (char, usize) {
554 match c {
555 '\t' => {
556 let tab_end = (col / self.tab_width + 1) * self.tab_width;
558 (' ', tab_end - col)
559 }
560 c if c.is_whitespace() => (' ', 1),
561 _ => (c, c.width().unwrap_or(1)),
562 }
563 }
564
565 pub const fn new() -> Self {
567 Self {
568 cross_gap: true,
569 label_attach: LabelAttach::Middle,
570 compact: false,
571 underlines: true,
572 multiline_arrows: true,
573 color: true,
574 tab_width: 4,
575 char_set: CharSet::Unicode,
576 index_type: IndexType::Char,
577 }
578 }
579}
580
581impl Default for Config {
582 fn default() -> Self {
583 Self::new()
584 }
585}
586
587#[test]
588#[should_panic]
589#[allow(clippy::reversed_empty_ranges)]
590fn backwards_label_should_panic() {
591 Label::new(1..0);
592}