diff --git a/benches/simple.rs b/benches/simple.rs index 35da8d6..e11e9e9 100644 --- a/benches/simple.rs +++ b/benches/simple.rs @@ -1,73 +1,126 @@ #[macro_use] extern crate criterion; -use criterion::black_box; -use criterion::Criterion; +use criterion::{BatchSize, Criterion}; -use annotate_snippets::DisplayList; -use annotate_snippets::{Annotation, AnnotationType, SourceAnnotation}; -use annotate_snippets::{Slice, Snippet}; - -use annotate_snippets::renderers::ascii_default::get_renderer; -use annotate_snippets::renderers::Renderer; +use annotate_snippets::*; +use std::ops::Range; const SOURCE: &'static str = r#") -> Option<String> { -for ann in annotations { - match (ann.range.0, ann.range.1) { - (None, None) => continue, - (Some(start), Some(end)) if start > end_index => continue, - (Some(start), Some(end)) if start >= start_index => { - let label = if let Some(ref label) = ann.label { - format!(" {}", label) - } else { - String::from("") - }; + for ann in annotations { + match (ann.range.0, ann.range.1) { + (None, None) => continue, + (Some(start), Some(end)) if start > end_index => continue, + (Some(start), Some(end)) if start >= start_index => { + let label = if let Some(ref label) = ann.label { + format!(" {}", label) + } else { + String::from("") + }; - return Some(format!( - "{}{}{}", - " ".repeat(start - start_index), - "^".repeat(end - start), - label - )); + return Some(format!( + "{}{}{}", + " ".repeat(start - start_index), + "^".repeat(end - start), + label + )); + } + _ => continue, } - _ => continue, + }"#; + +fn source_snippet() -> Snippet<'static, WithLineNumber<&'static str>> { + Snippet { + title: Some(Title { + code: Some(&"E0308"), + message: Message { + text: &"mismatched types", + level: Level::Error, + }, + }), + slices: &[Slice { + span: WithLineNumber { + line_num: 51, + data: SOURCE, + }, + origin: Some(&"src/format.rs"), + annotations: &[ + Annotation { + span: 5..19, + message: Some(Message { + text: &"expected `Option<String>` because of return type", + level: Level::Warning, + }), + }, + Annotation { + span: 26..725, + message: Some(Message { + text: &"expected enum `std::option::Option`", + level: Level::Error, + }), + }, + ], + footer: &[], + }], } -}"#; +} -fn create_snippet() { - let snippet = Snippet { - title: Some(Annotation { - id: Some("E0308"), - label: Some("mismatched types"), - annotation_type: AnnotationType::Error, +fn range_snippet() -> Snippet<'static, Range<usize>> { + Snippet { + title: Some(Title { + code: Some(&"E0308"), + message: Message { + text: &"mismatched types", + level: Level::Error, + }, }), - footer: &[], slices: &[Slice { - source: SOURCE, - line_start: Some(51), - origin: Some("src/format.rs"), + span: 0..725, + origin: Some(&"src/format.rs"), annotations: &[ - SourceAnnotation { - label: "expected `Option<String>` because of return type", - annotation_type: AnnotationType::Warning, - range: 5..19, + Annotation { + span: 5..19, + message: Some(Message { + text: &"expected `Option<String>` because of return type", + level: Level::Warning, + }), }, - SourceAnnotation { - label: "expected enum `std::option::Option`", - annotation_type: AnnotationType::Error, - range: 23..725, + Annotation { + span: 26..725, + message: Some(Message { + text: &"expected enum `std::option::Option`", + level: Level::Error, + }), }, ], + footer: &[], }], - }; - let r = get_renderer(); - let dl: DisplayList = (&snippet).into(); - let mut result: Vec<u8> = Vec::new(); - r.fmt(&mut result, &dl).unwrap(); + } } pub fn criterion_benchmark(c: &mut Criterion) { - c.bench_function("format", |b| b.iter(|| black_box(create_snippet()))); + c.bench_function("format [&str]", |b| { + b.iter_batched_ref( + || Vec::<u8>::with_capacity(1100), + |out| { + let snippet = source_snippet(); + let formatted = format(&snippet, &()); + renderer::Ascii::new().render(&formatted, &(), out) + }, + BatchSize::SmallInput, + ) + }); + c.bench_function("format [Range]", |b| { + b.iter_batched_ref( + || Vec::<u8>::with_capacity(1100), + |out| { + let snippet = range_snippet(); + let formatted = format(&snippet, &SOURCE); + renderer::Ascii::new().render(&formatted, &SOURCE, out) + }, + BatchSize::SmallInput, + ) + }); } criterion_group!(benches, criterion_benchmark); diff --git a/examples/format.rs b/examples/format.rs index c59bb09..4519750 100644 --- a/examples/format.rs +++ b/examples/format.rs @@ -1,9 +1,5 @@ -use annotate_snippets::DisplayList; -use annotate_snippets::{Annotation, AnnotationType, SourceAnnotation}; -use annotate_snippets::{Slice, Snippet}; - -use annotate_snippets::renderers::get_renderer; -use annotate_snippets::renderers::Renderer; +use annotate_snippets::*; +use std::io; fn main() { let source = r#") -> Option<String> { @@ -30,34 +26,43 @@ fn main() { }"#; let snippet = Snippet { - title: Some(Annotation { - id: Some("E0308"), - label: Some("mismatched types"), - annotation_type: AnnotationType::Error, + title: Some(Title { + code: Some(&"E0308"), + message: Message { + text: &"mismatched types", + level: Level::Error, + }, }), - footer: &[], slices: &[Slice { - source, - line_start: Some(51), - origin: Some("src/format.rs"), + span: WithLineNumber { + line_num: 51, + data: source, + }, + origin: Some(&"src/format.rs"), annotations: &[ - SourceAnnotation { - label: "expected `Option<String>` because of return type", - annotation_type: AnnotationType::Warning, - range: 5..19, + Annotation { + span: 5..19, + message: Some(Message { + text: &"expected `Option<String>` because of return type", + level: Level::Warning, + }), }, - SourceAnnotation { - label: "expected enum `std::option::Option`", - annotation_type: AnnotationType::Error, - range: 23..725, + Annotation { + span: 26..725, + message: Some(Message { + text: &"expected enum `std::option::Option`", + level: Level::Error, + }), }, ], + footer: &[], }], }; - let dl = DisplayList::from(&snippet); - let r = get_renderer(); - let mut s: Vec<u8> = Vec::new(); - r.fmt(&mut s, &dl).unwrap(); - println!("{}", std::str::from_utf8(&s).unwrap()); + let formatted = format(&snippet, &()); + renderer::Ascii::new() + .ansi(true) + .box_drawing(true) + .render(&formatted, &(), &mut io::stdout().lock()) + .unwrap(); } diff --git a/src/annotation.rs b/src/annotation.rs deleted file mode 100644 index b1ac52d..0000000 --- a/src/annotation.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::ops::Range; - -#[derive(Debug, Clone)] -pub struct Annotation<'s> { - pub id: Option<&'s str>, - pub label: Option<&'s str>, - pub annotation_type: AnnotationType, -} - -#[derive(Debug, Clone)] -pub enum AnnotationType { - None, - Error, - Warning, - Info, - Note, - Help, -} - -#[derive(Debug, Clone)] -pub struct SourceAnnotation<'s> { - pub range: Range<usize>, - pub label: &'s str, - pub annotation_type: AnnotationType, -} diff --git a/src/display_list/annotation.rs b/src/display_list/annotation.rs deleted file mode 100644 index a9da08c..0000000 --- a/src/display_list/annotation.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::annotation::AnnotationType; - -#[derive(Debug, Clone)] -pub struct Annotation<'d> { - pub annotation_type: AnnotationType, - pub id: Option<&'d str>, - pub label: &'d str, -} diff --git a/src/display_list/line.rs b/src/display_list/line.rs deleted file mode 100644 index f0774b7..0000000 --- a/src/display_list/line.rs +++ /dev/null @@ -1,51 +0,0 @@ -use super::annotation::Annotation; -use crate::annotation::AnnotationType; -use std::ops::Range; - -#[derive(Debug, Clone)] -pub enum DisplayLine<'d> { - Source { - lineno: Option<usize>, - inline_marks: Vec<DisplayMark>, - line: DisplaySourceLine<'d>, - }, - Raw(DisplayRawLine<'d>), -} - -#[derive(Debug, Clone)] -pub enum DisplaySourceLine<'d> { - Content { - text: &'d str, - }, - Annotation { - annotation: Annotation<'d>, - range: Range<usize>, - }, - Empty, -} - -#[derive(Debug, Clone)] -pub enum DisplayRawLine<'d> { - Origin { - path: &'d str, - pos: (Option<usize>, Option<usize>), - }, - Annotation { - annotation: Annotation<'d>, - source_aligned: bool, - continuation: bool, - }, -} - -#[derive(Debug, Clone)] -pub struct DisplayMark { - pub mark_type: DisplayMarkType, - pub annotation_type: AnnotationType, -} - -#[derive(Debug, Clone)] -pub enum DisplayMarkType { - AnnotationThrough, - AnnotationStart, - AnnotationEnd, -} diff --git a/src/display_list/list.rs b/src/display_list/list.rs deleted file mode 100644 index 3b18248..0000000 --- a/src/display_list/list.rs +++ /dev/null @@ -1,147 +0,0 @@ -use super::annotation::Annotation; -use super::line::{DisplayLine, DisplayMark, DisplayMarkType, DisplayRawLine, DisplaySourceLine}; -use crate::{Slice, Snippet, SourceAnnotation}; - -#[derive(Debug, Clone)] -pub struct DisplayList<'d> { - pub body: Vec<DisplayLine<'d>>, -} - -fn get_header_pos(slice: &Slice) -> (Option<usize>, Option<usize>) { - let line = slice.line_start; - (line, None) -} - -impl<'d> From<&Snippet<'d>> for DisplayList<'d> { - fn from(snippet: &Snippet<'d>) -> Self { - let mut body = vec![]; - - if let Some(annotation) = &snippet.title { - let label = annotation.label.unwrap_or_default(); - body.push(DisplayLine::Raw(DisplayRawLine::Annotation { - annotation: Annotation { - annotation_type: annotation.annotation_type.clone(), - id: annotation.id, - label: &label, - }, - source_aligned: false, - continuation: false, - })); - } - - for slice in snippet.slices { - let slice_dl: DisplayList = slice.into(); - body.extend(slice_dl.body); - } - DisplayList { body } - } -} - -impl<'d> From<&Slice<'d>> for DisplayList<'d> { - fn from(slice: &Slice<'d>) -> Self { - let mut body = vec![]; - - if let Some(path) = slice.origin { - body.push(DisplayLine::Raw(DisplayRawLine::Origin { - path, - pos: get_header_pos(slice), - })); - } - - body.push(DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - }); - - let mut annotations: Vec<&SourceAnnotation> = slice.annotations.iter().collect(); - - // let mut current_annotation = annotations.next(); - let mut line_start_pos = 0; - - let mut i = slice.line_start.unwrap_or(1); - for line in slice.source.lines() { - let line_range = line_start_pos..(line_start_pos + line.chars().count() + 1); - - let mut current_annotations = vec![]; - let mut inline_marks = vec![]; - - annotations.retain(|ann| { - if line_range.contains(&ann.range.start) && line_range.contains(&ann.range.end) { - // Annotation in this line - current_annotations.push(*ann); - false - } else if line_range.contains(&ann.range.start) - && !line_range.contains(&ann.range.end) - { - // Annotation starts in this line - inline_marks.push(DisplayMark { - mark_type: DisplayMarkType::AnnotationStart, - annotation_type: ann.annotation_type.clone(), - }); - true - } else if ann.range.start < line_range.start && ann.range.end > line_range.end { - // Annotation goes through this line - inline_marks.push(DisplayMark { - mark_type: DisplayMarkType::AnnotationThrough, - annotation_type: ann.annotation_type.clone(), - }); - true - } else if line_range.contains(&ann.range.end) { - // Annotation ends on this line - inline_marks.push(DisplayMark { - mark_type: DisplayMarkType::AnnotationThrough, - annotation_type: ann.annotation_type.clone(), - }); - current_annotations.push(*ann); - false - } else { - true - } - }); - - body.push(DisplayLine::Source { - lineno: Some(i), - inline_marks, - line: DisplaySourceLine::Content { text: line }, - }); - for ann in current_annotations { - let start = if ann.range.start >= line_start_pos { - ann.range.start - line_start_pos - } else { - 0 - }; - let inline_marks = if ann.range.start < line_start_pos { - vec![DisplayMark { - mark_type: DisplayMarkType::AnnotationEnd, - annotation_type: ann.annotation_type.clone(), - }] - } else { - vec![] - }; - body.push(DisplayLine::Source { - lineno: None, - inline_marks, - line: DisplaySourceLine::Annotation { - annotation: Annotation { - annotation_type: ann.annotation_type.clone(), - id: None, - label: ann.label, - }, - range: start..(ann.range.end - line_start_pos), - }, - }); - } - line_start_pos += line_range.len(); - i += 1; - } - - body.push(DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - }); - - DisplayList { body } - } -} diff --git a/src/display_list/mod.rs b/src/display_list/mod.rs deleted file mode 100644 index 49ff81a..0000000 --- a/src/display_list/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod annotation; -pub mod line; -pub mod list; - -pub use list::DisplayList; diff --git a/src/formatter/mod.rs b/src/formatter/mod.rs new file mode 100644 index 0000000..693185d --- /dev/null +++ b/src/formatter/mod.rs @@ -0,0 +1,151 @@ +use crate::{Annotation, Level, Slice, Snippet, Span as _, SpanFormatter, WithLineNumber}; +use std::cmp; + +pub fn format<'d, Span: crate::Span>( + snippet: &'d Snippet<'d, Span>, + f: &dyn SpanFormatter<Span>, +) -> FormattedSnippet<'d, Span> { + let mut lines = vec![]; + + if let Some(title) = snippet.title { + lines.push(DisplayLine::Raw(RawLine::Title { title })) + } + + for slice in snippet.slices { + format_into(&mut lines, slice, f); + } + + FormattedSnippet { lines } +} + +fn format_into<'d, Span: crate::Span>( + lines: &mut Vec<DisplayLine<'d, Span>>, + slice: &'d Slice<'d, Span>, + f: &dyn SpanFormatter<Span>, +) { + let mut this_line = f.first_line(&slice.span); + + if let Some(origin) = slice.origin { + lines.push(DisplayLine::Raw(RawLine::Origin { + path: origin, + pos: Some(( + this_line.line_num, + f.count_columns( + &slice.span, + &slice.span.slice(this_line.data.start()..slice.span.start()), + ), + )), + })); + + // spacing line iff origin line present + lines.push(DisplayLine::Source { + lineno: None, + inline_marks: vec![], + line: SourceLine::Empty, + }) + } + + // TODO: benchmark whether `retain` here benefits more than the allocation overhead + let mut annotations: Vec<&Annotation<'_, _>> = slice.annotations.iter().collect(); + + let mut process_line = |line: &WithLineNumber<Span::Subspan>| { + let WithLineNumber { + data: line, + line_num, + } = line; + + let mut annotations_here = vec![]; + let mut marks_here = vec![]; + + annotations.retain(|&ann| { + let level = ann.message.map(|m| m.level).unwrap_or(Level::Info); + + if line.start() <= ann.span.start() && ann.span.end() <= line.end() { + // Annotation in this line + annotations_here.push(ann); + false + } else if line.start() <= ann.span.start() && ann.span.start() <= line.end() { + // Annotation starts in this line + marks_here.push(Mark { + kind: MarkKind::Start, + level, + }); + true + } else if ann.span.start() < line.start() && line.end() < ann.span.end() { + // Annotation goes through this line + marks_here.push(Mark { + kind: MarkKind::Continue, + level, + }); + true + } else if ann.span.start() < line.start() && ann.span.end() <= line.end() { + // Annotation ends on this line + marks_here.push(Mark { + kind: MarkKind::Continue, + level, + }); + annotations_here.push(ann); + false + } else { + // Annotation starts on later line + true + } + }); + + lines.push(DisplayLine::Source { + lineno: Some(*line_num), + inline_marks: marks_here, + line: SourceLine::Content { span: &slice.span, subspan: line.clone() }, + }); + + for ann in annotations_here { + let level = ann.message.map(|m| m.level).unwrap_or(Level::Info); + + let start_pos = cmp::max(ann.span.start(), line.start()); + let start = f.count_columns(&slice.span, &slice.span.slice(line.start()..start_pos)); + let len = f.count_columns(&slice.span, &slice.span.slice(start_pos..ann.span.end())); + + let marks_here = if ann.span.start() < line.start() { + vec![Mark { + kind: MarkKind::Here, + level, + }] + } else { + vec![] + }; + + lines.push(DisplayLine::Source { + lineno: None, + inline_marks: marks_here, + line: SourceLine::Annotation { + message: ann.message, + underline: (start, len), + }, + }) + } + }; + + process_line(&this_line); + while let Some(line) = f.next_line(&slice.span, &this_line) { + this_line = line; + process_line(&this_line); + } + + if !slice.footer.is_empty() { + // spacing line iff footer lines follow + lines.push(DisplayLine::Source { + lineno: None, + inline_marks: vec![], + line: SourceLine::Empty, + }); + + for &message in slice.footer { + lines.push(DisplayLine::Raw(RawLine::Message { message })) + } + } +} + +mod types; +pub use types::{ + DisplayLine, Mark, MarkKind, RawLine, SourceLine, FormattedSnippet, +}; diff --git a/src/formatter/types.rs b/src/formatter/types.rs new file mode 100644 index 0000000..5d69b6e --- /dev/null +++ b/src/formatter/types.rs @@ -0,0 +1,170 @@ +use crate::{DebugAndDisplay, Level, Message, Title}; +use std::fmt; + +// Cannot derive Debug, Clone because we need to bound Span::Subspan +// so #[derive(Debug, Clone)] is manually expanded here (ugh) + +pub struct FormattedSnippet<'d, Span: crate::Span> { + pub lines: Vec<DisplayLine<'d, Span>>, +} + +impl<Span: crate::Span> fmt::Debug for FormattedSnippet<'_, Span> +where + Span: fmt::Debug, + Span::Subspan: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("FormattedSnippet") + .field("inner", &self.lines) + .finish() + } +} + +impl<Span: crate::Span> Clone for FormattedSnippet<'_, Span> +where + Span::Subspan: Clone, +{ + fn clone(&self) -> Self { + FormattedSnippet { + lines: self.lines.clone(), + } + } +} + +pub enum DisplayLine<'d, Span: crate::Span> { + Source { + lineno: Option<usize>, + inline_marks: Vec<Mark>, + line: SourceLine<'d, Span>, + }, + Raw(RawLine<'d>), +} + +// #[derive(Debug)] +impl<Span: crate::Span> fmt::Debug for DisplayLine<'_, Span> +where + Span: fmt::Debug, + Span::Subspan: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DisplayLine::Source { + lineno, + inline_marks, + line, + } => f + .debug_struct("Source") + .field("lineno", lineno) + .field("inline_marks", inline_marks) + .field("line", line) + .finish(), + DisplayLine::Raw(raw) => f.debug_tuple("Raw").field(raw).finish(), + } + } +} + +// #[derive(Clone)] +impl<Span: crate::Span> Clone for DisplayLine<'_, Span> +where + Span::Subspan: Clone, +{ + fn clone(&self) -> Self { + match self { + DisplayLine::Source { + lineno, + inline_marks, + line, + } => DisplayLine::Source { + lineno: *lineno, + inline_marks: inline_marks.clone(), + line: (*line).clone(), + }, + DisplayLine::Raw(raw) => DisplayLine::Raw(*raw), + } + } +} + +pub enum SourceLine<'d, Span: crate::Span> { + Content { + span: &'d Span, + subspan: Span::Subspan, + }, + Annotation { + message: Option<Message<'d>>, + underline: (usize, usize), + }, + Empty, +} + +// #[derive(Debug)] +impl<Span: crate::Span> fmt::Debug for SourceLine<'_, Span> +where + Span: fmt::Debug, + Span::Subspan: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SourceLine::Content { span, subspan } => f + .debug_struct("Content") + .field("span", span) + .field("subspan", subspan) + .finish(), + SourceLine::Annotation { message, underline } => f + .debug_struct("Annotation") + .field("message", message) + .field("underline", underline) + .finish(), + SourceLine::Empty => f.debug_struct("Empty").finish(), + } + } +} + +// #[derive(Copy)] +impl<Span: crate::Span> Copy for SourceLine<'_, Span> where Span::Subspan: Copy {} + +// #[derive(Clone)] +impl<Span: crate::Span> Clone for SourceLine<'_, Span> +where + Span::Subspan: Clone, +{ + fn clone(&self) -> Self { + match self { + SourceLine::Content { span, subspan } => SourceLine::Content { + span: *span, + subspan: subspan.clone(), + }, + SourceLine::Annotation { message, underline } => SourceLine::Annotation { + message: *message, + underline: *underline, + }, + SourceLine::Empty => SourceLine::Empty, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub enum RawLine<'d> { + Origin { + path: &'d dyn DebugAndDisplay, + pos: Option<(usize, usize)>, + }, + Title { + title: Title<'d>, + }, + Message { + message: Message<'d>, + }, +} + +#[derive(Debug, Copy, Clone)] +pub struct Mark { + pub kind: MarkKind, + pub level: Level, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum MarkKind { + Start, + Continue, + Here, +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..a855838 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,227 @@ +use std::fmt; + +pub trait DebugAndDisplay: fmt::Debug + fmt::Display {} +impl<T: ?Sized + fmt::Debug + fmt::Display> DebugAndDisplay for T {} + +// Cannot derive Debug because we need to bound Span::Subspan +// so #[derive(Debug)] is manually expanded here (ugh) + +/// Primary structure for annotation formatting. +/// +/// # Examples +/// +/// To produce the error annotation +/// +/// ```text +/// error[E0277]: `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely +/// --> examples/nonsend_future.rs:23:5 +/// | +/// 5 | fn is_send<T: Send>(t: T) { +/// | ------- ---- required by this bound in `is_send` +/// ... +/// 23 | is_send(foo()); +/// | ^^^^^^^ `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely +/// | +/// = help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, u32>` +/// note: future does not implement `std::marker::Send` as this value is used across an await +/// --> examples/nonsend_future.rs:15:3 +/// | +/// 14 | let g = x.lock().unwrap(); +/// | - has type `std::sync::MutexGuard<'_, u32>` +/// 15 | baz().await; +/// | ^^^^^^^^^^^ await occurs here, with `g` maybe used later +/// 16 | } +/// | - `g` is later dropped here +/// ``` +/// +/// two snippets are used: +/// +/// ```rust +/// # use annotate_snippets::*; +/// let first_snippet = Snippet { +/// title: Some(Title { +/// code: Some(&"E0277"), +/// message: Message { +/// text: &"`std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely", +/// level: Level::Error, +/// }, +/// }), +/// slices: &[Slice { +/// span: WithLineNumber { +/// data: "fn is_send<T: Send>(t: T) {\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n is_send(foo());", +/// line_num: 5, +/// }, +/// origin: Some(&"examples/nonsend_future.rs"), +/// annotations: &[ +/// Annotation { +/// span: 4..11, +/// message: None, +/// }, +/// Annotation { +/// span: 14..18, +/// message: Some(Message { +/// text: &"required by this bound in `is_send`", +/// level: Level::Info, +/// }) +/// }, +/// Annotation { +/// span: 67..74, +/// message: Some(Message { +/// text: &"`std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely", +/// level: Level::Error, +/// }) +/// }, +/// ], +/// footer: &[Message { +/// text: &"within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, u32>`", +/// level: Level::Help, +/// }], +/// }], +/// }; +/// let second_snippet = Snippet { +/// title: Some(Title { +/// code: None, +/// message: Message { +/// text: &"future does not implement `std::marker::Send` as this value is used across an await", +/// level: Level::Note, +/// }, +/// }), +/// slices: &[Slice { +/// span: WithLineNumber { +/// data: " let g = x.lock().unwrap();\n baz().await;\n}", +/// line_num: 14, +/// }, +/// origin: Some(&"examples/nonsend_future.rs"), +/// annotations: &[ +/// Annotation { +/// span: 8..9, +/// message: Some(Message { +/// text: &"has type `std::sync::MutexGuard<'_, u32>`", +/// level: Level::Info, +/// }), +/// }, +/// Annotation { +/// span: 36..47, +/// message: Some(Message { +/// text: &"await occurs here, with `g` maybe used later", +/// level: Level::Error, +/// }) +/// }, +/// Annotation { +/// span: 50..51, +/// message: Some(Message { +/// text: &"`g` is later dropped here", +/// level: Level::Info, +/// }) +/// }, +/// ], +/// footer: &[], +/// }], +/// }; +/// ``` +#[derive(Copy, Clone)] +pub struct Snippet<'s, Span: crate::Span> { + pub title: Option<Title<'s>>, + pub slices: &'s [Slice<'s, Span>], +} + +// #[derive(Debug)] +impl<Span: crate::Span> fmt::Debug for Snippet<'_, Span> +where + Span: fmt::Debug, + Span::Subspan: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Snippet") + .field("title", &self.title) + .field("slices", &self.slices) + .finish() + } +} + +/// Title line for an annotation snippet. +#[derive(Debug, Copy, Clone)] +pub struct Title<'s> { + pub code: Option<&'s dyn DebugAndDisplay>, + pub message: Message<'s>, +} + +/// A slice of text with annotations. +#[derive(Copy, Clone)] +pub struct Slice<'s, Span: crate::Span> { + pub span: Span, + pub origin: Option<&'s dyn DebugAndDisplay>, + pub annotations: &'s [Annotation<'s, Span>], + pub footer: &'s [Message<'s>], +} + +// #[derive(Debug)] +impl<Span: crate::Span> fmt::Debug for Slice<'_, Span> +where + Span: fmt::Debug, + Span::Subspan: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Slice") + .field("span", &self.span) + .field("origin", &self.origin) + .field("annotations", &self.annotations) + .field("footer", &self.footer) + .finish() + } +} + +/// An annotation for some span. +pub struct Annotation<'s, Span: crate::Span> { + pub span: Span::Subspan, + pub message: Option<Message<'s>>, +} + +// #[derive(Debug)] +impl<Span: crate::Span> fmt::Debug for Annotation<'_, Span> +where + Span::Subspan: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Annotation") + .field("span", &self.span) + .field("message", &self.message) + .finish() + } +} + +// #[derive(Copy)] +impl<Span: crate::Span> Copy for Annotation<'_, Span> where Span::Subspan: Copy {} + +// #[derive(Clone)] +impl<Span: crate::Span> Clone for Annotation<'_, Span> +where + Span::Subspan: Clone, +{ + fn clone(&self) -> Self { + Annotation { + span: self.span.clone(), + message: self.message, + } + } +} + +/// A message with an associated level. +#[derive(Debug, Copy, Clone)] +pub struct Message<'s> { + pub text: &'s dyn DebugAndDisplay, + pub level: Level, +} + +/// A level of severity for an annotation message. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Level { + /// Typically displayed using a red color. + Error, + /// Typically displayed using a yellow color. + Warning, + /// Typically displayed using a blue color. + Info, + Note, + Help, +} diff --git a/src/lib.rs b/src/lib.rs index 9848c43..da57daf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,9 @@ -pub mod annotation; -mod display_list; -pub mod renderers; -pub mod slice; -pub mod snippet; +pub mod formatter; +mod input; +pub mod renderer; +mod span; -pub use annotation::{Annotation, AnnotationType, SourceAnnotation}; -pub use display_list::DisplayList; -pub use slice::Slice; -pub use snippet::Snippet; +pub use formatter::format; +pub use input::{Annotation, DebugAndDisplay, Level, Message, Slice, Snippet, Title}; +pub use renderer::Renderer; +pub use span::{Span, SpanFormatter, SpanWriter, WithLineNumber}; diff --git a/src/renderer/default.rs b/src/renderer/default.rs new file mode 100644 index 0000000..675e6d7 --- /dev/null +++ b/src/renderer/default.rs @@ -0,0 +1,273 @@ +use crate::{ + formatter::{DisplayLine, FormattedSnippet, Mark, MarkKind, RawLine, SourceLine}, + renderer::{log10usize, max_line_num, max_marks_width, Renderer}, + DebugAndDisplay, Level, SpanWriter, +}; +use std::io; + +#[derive(Debug, Copy, Clone, Default)] +pub struct Ascii { + pub ansi: bool, + #[allow(unused)] // TODO + pub fold: bool, + pub box_drawing: bool, + #[doc(hidden)] // to allow structural creation with `Ascii { ..Default::default() }` + pub __non_exhaustive: (), +} + +impl Ascii { + pub fn new() -> Self { + Default::default() + } + + pub fn ansi(&mut self, b: bool) -> &mut Self { + self.ansi = b; + self + } + + pub fn box_drawing(&mut self, b: bool) -> &mut Self { + self.box_drawing = b; + self + } +} + +impl Ascii { + #[inline(always)] + fn reset(self, w: &mut dyn io::Write) -> io::Result<()> { + if self.ansi { + write!(w, "\x1B[0m") + } else { + Ok(()) + } + } + + #[inline(always)] + fn bold(self, w: &mut dyn io::Write) -> io::Result<()> { + if self.ansi { + write!(w, "\x1B[0;1m") + } else { + Ok(()) + } + } + + // bold + fg(Fixed(12)) + #[inline(always)] + fn bold_bright_blue(self, w: &mut dyn io::Write) -> io::Result<()> { + if self.ansi { + write!(w, "\x1B[1;34;1m") + } else { + Ok(()) + } + } + + // FIXME: emitted ANSI codes are highly redundant when repeated + #[inline(always)] + fn style_for(self, level: Level, w: &mut dyn io::Write) -> io::Result<()> { + if self.ansi { + match level { + Level::Error => write!(w, "\x1B[0;31;1m"), + Level::Warning => write!(w, "\x1B[0;33;1m"), + Level::Info => write!(w, "\x1B[0;34;1m"), + Level::Note => self.reset(w), + Level::Help => write!(w, "\x1B[0;36;1m"), + } + } else { + Ok(()) + } + } + + // FIXME: emitted ANSI codes are highly redundant when repeated + #[inline(always)] + fn style_bold_for(self, level: Level, w: &mut dyn io::Write) -> io::Result<()> { + if self.ansi { + match level { + Level::Error => write!(w, "\x1B[1;31;1m"), + Level::Warning => write!(w, "\x1B[1;33;1m"), + Level::Info => write!(w, "\x1B[1;34;1m"), + Level::Note => self.reset(w), + Level::Help => write!(w, "\x1B[1;36;1m"), + } + } else { + Ok(()) + } + } +} + +impl Ascii { + fn render_marks(self, marks: &[Mark], w: &mut dyn io::Write) -> io::Result<()> { + for mark in marks { + self.style_for(mark.level, w)?; + let c = if self.box_drawing { + match mark.kind { + MarkKind::Start => '┌', + MarkKind::Continue => '│', + MarkKind::Here => '└', + } + } else { + match mark.kind { + MarkKind::Start => '/', + MarkKind::Continue => '|', + MarkKind::Here => '\\', + } + }; + write!(w, "{}", c)?; + } + self.reset(w) + } + + fn render_source_line<Span: crate::Span>( + self, + line: &SourceLine<'_, Span>, + is_long: bool, + f: &dyn SpanWriter<Span>, + w: &mut dyn io::Write, + ) -> io::Result<()> { + match line { + SourceLine::Content { span, subspan } => { + write!(w, " ")?; + f.write(w, span, subspan) + } + SourceLine::Annotation { message, underline } => { + let (indent, len) = if is_long { + (0, underline.0 + underline.1 + 1) + } else { + (underline.0 + 1, underline.1) + }; + write!(w, "{:>width$}", "", width = indent)?; + let level = message.map_or(Level::Info, |message| message.level); + self.style_bold_for(level, w)?; + if is_long { + if self.box_drawing { + write!(w, "{:─>width$} ", "┘", width = len)?; + } else { + write!(w, "{:_>width$} ", "^", width = len)?; + } + } else { + match level { + Level::Error => write!(w, "{:^>width$} ", "", width = len)?, + Level::Warning => write!(w, "{:~>width$} ", "", width = len)?, + Level::Info | Level::Help | Level::Note => { + write!(w, "{:->width$} ", "", width = len)? + } + } + } + write!( + w, + "{}", + message.map_or(&"" as &dyn DebugAndDisplay, |message| message.text) + ) + } + SourceLine::Empty => Ok(()), + } + } + + fn render_raw_line( + self, + line: &RawLine<'_>, + line_num_width: usize, + w: &mut dyn io::Write, + ) -> io::Result<()> { + match line { + &RawLine::Origin { path, pos } => { + write!(w, "{:>width$}", "", width = line_num_width)?; + self.bold_bright_blue(w)?; + if self.box_drawing { + write!(w, "═╦═")?; + } else { + write!(w, "-->")?; + } + self.reset(w)?; + write!(w, " {}", path)?; + if let Some((line, column)) = pos { + write!(w, ":{}:{}", line, column)?; + } + writeln!(w) + } + RawLine::Message { message } => { + self.style_for(message.level, w)?; + let cta = match message.level { + Level::Error => "error", + Level::Warning => "warning", + Level::Info => "info", + Level::Note => "note", + Level::Help => "help", + }; + write!(w, "{:>width$} = {}", "", cta, width = line_num_width)?; + writeln!(w, ": {}", message.text) + } + RawLine::Title { title } => { + self.style_bold_for(title.message.level, w)?; + let cta = match title.message.level { + Level::Error => "error", + Level::Warning => "warning", + Level::Info => "info", + Level::Note => "note", + Level::Help => "help", + }; + write!(w, "{}", cta)?; + if let Some(code) = title.code { + write!(w, "[{}]", code)?; + } + self.bold(w)?; + writeln!(w, ": {}", title.message.text) + } + } + } +} + +impl Renderer for Ascii { + fn render<'a, Span: crate::Span>( + &self, + snippet: &FormattedSnippet<'a, Span>, + f: &dyn SpanWriter<Span>, + w: &mut dyn io::Write, + ) -> io::Result<()> { + let max_line_num = max_line_num(snippet).unwrap_or(0); + let marks_width = max_marks_width(snippet); + + for line in &snippet.lines { + self.render_line(line, log10usize(max_line_num), marks_width, f, w)?; + } + + self.reset(w) + } + + fn render_line<Span: crate::Span>( + &self, + line: &DisplayLine<'_, Span>, + line_num_width: usize, + marks_width: usize, + f: &dyn SpanWriter<Span>, + w: &mut dyn io::Write, + ) -> io::Result<()> { + match line { + DisplayLine::Source { + lineno, + inline_marks, + line, + } => { + self.bold_bright_blue(w)?; + let sep = if self.box_drawing { '║' } else { '|' }; + if let Some(lineno) = lineno { + write!(w, "{:>width$} {} ", lineno, sep, width = line_num_width)?; + } else { + write!(w, "{:>width$} {} ", "", sep, width = line_num_width)?; + } + self.reset(w)?; + write!( + w, + "{:>width$}", + "", + width = marks_width - inline_marks.len() + )?; + self.render_marks(inline_marks, w)?; + let is_long = inline_marks + .last() + .map_or(false, |mark| mark.kind == MarkKind::Here); + self.render_source_line(line, is_long, f, w)?; + writeln!(w) + } + DisplayLine::Raw(line) => self.render_raw_line(line, line_num_width, w), + } + } +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs new file mode 100644 index 0000000..0042a19 --- /dev/null +++ b/src/renderer/mod.rs @@ -0,0 +1,68 @@ +use crate::{ + formatter::{DisplayLine, FormattedSnippet}, + SpanWriter, +}; +use std::io; + +pub fn max_line_num<Span: crate::Span>(snippet: &FormattedSnippet<'_, Span>) -> Option<usize> { + // note that the line numbers of multiple slices might not be in order + snippet + .lines + .iter() + .filter_map(|line| match *line { + DisplayLine::Source { lineno, .. } => lineno, + _ => None, + }) + .max() +} + +pub fn max_marks_width<Span: crate::Span>(snippet: &FormattedSnippet<'_, Span>) -> usize { + snippet + .lines + .iter() + .filter_map(|line| match line { + DisplayLine::Source { inline_marks, .. } => Some(inline_marks.len()), + _ => None, + }) + .max() + .unwrap_or(0) +} + +pub trait Renderer { + fn render<'a, Span: crate::Span>( + &self, + snippet: &FormattedSnippet<'a, Span>, + f: &dyn SpanWriter<Span>, + w: &mut dyn io::Write, + ) -> io::Result<()> { + let max_line_num = max_line_num(snippet).unwrap_or(0); + let marks_width = max_marks_width(snippet); + + for line in &snippet.lines { + self.render_line(line, log10usize(max_line_num), marks_width, f, w)?; + } + + Ok(()) + } + + fn render_line<Span: crate::Span>( + &self, + line: &DisplayLine<'_, Span>, + line_num_width: usize, + marks_width: usize, + f: &dyn SpanWriter<Span>, + w: &mut dyn io::Write, + ) -> io::Result<()>; +} + +fn log10usize(mut n: usize) -> usize { + let mut sum = 0; + while n != 0 { + n /= 10; + sum += 1; + } + sum +} + +mod default; +pub use default::Ascii; diff --git a/src/renderers/ascii_default/mod.rs b/src/renderers/ascii_default/mod.rs deleted file mode 100644 index deb3d76..0000000 --- a/src/renderers/ascii_default/mod.rs +++ /dev/null @@ -1,226 +0,0 @@ -mod marks; -mod styles; - -#[cfg(feature = "ansi_term")] -use crate::renderers::ascii_default::styles::color::Style; -#[cfg(feature = "termcolor")] -use crate::renderers::ascii_default::styles::color2::Style; -#[cfg(all(not(feature = "ansi_term"), not(feature = "termcolor")))] -use crate::renderers::ascii_default::styles::plain::Style; - -use super::Renderer as RendererTrait; -use crate::annotation::AnnotationType; -use crate::display_list::line::DisplayLine; -use crate::display_list::line::DisplayMark; -use crate::display_list::line::DisplayMarkType; -use crate::display_list::line::DisplayRawLine; -use crate::display_list::line::DisplaySourceLine; -use crate::DisplayList; -use marks::MarkKind; -use std::cmp; -use std::io::Write; -use std::iter::repeat; -use std::marker::PhantomData; -use styles::Style as StyleTrait; -use styles::StyleType; - -fn digits(n: usize) -> usize { - let mut n = n; - let mut sum = 0; - while n != 0 { - n /= 10; - sum += 1; - } - sum -} - -pub struct Renderer<S: StyleTrait> { - style: PhantomData<S>, -} - -pub fn get_renderer() -> impl RendererTrait { - Renderer::<Style>::new() -} - -impl<S: StyleTrait> Renderer<S> { - pub fn new() -> Self { - Renderer { style: PhantomData } - } - - pub fn fmt(&self, w: &mut impl Write, dl: &DisplayList) -> std::io::Result<()> { - let lineno_max = dl.body.iter().rev().find_map(|line| { - if let DisplayLine::Source { - lineno: Some(lineno), - .. - } = line - { - Some(digits(*lineno)) - } else { - None - } - }); - let inline_marks_width = dl.body.iter().fold(0, |max, line| match line { - DisplayLine::Source { inline_marks, .. } => cmp::max(inline_marks.len(), max), - _ => max, - }); - for line in &dl.body { - self.fmt_line(w, line, lineno_max, inline_marks_width)?; - } - Ok(()) - } - - fn fmt_line( - &self, - w: &mut impl Write, - line: &DisplayLine, - lineno_max: Option<usize>, - inline_marks_width: usize, - ) -> std::io::Result<()> { - let lineno_max = lineno_max.unwrap_or(1); - match line { - DisplayLine::Source { - lineno, - inline_marks, - line, - } => { - let style = &[StyleType::LineNo, StyleType::Emphasis]; - let vertical_mark = MarkKind::get(MarkKind::Vertical); - if let Some(lineno) = lineno { - S::fmt( - w, - format_args!("{:>width$} {} ", lineno, vertical_mark, width = lineno_max), - style, - )?; - } else { - S::fmt( - w, - format_args!("{:>width$} {} ", "", vertical_mark, width = lineno_max), - style, - )?; - } - write!( - w, - "{:>width$}", - "", - width = inline_marks_width - inline_marks.len() - )?; - for mark in inline_marks { - self.fmt_display_mark(w, mark)?; - } - self.fmt_source_line(w, line)?; - writeln!(w) - } - DisplayLine::Raw(l) => self.fmt_raw_line(w, l, lineno_max), - } - } - - fn fmt_source_line( - &self, - w: &mut impl std::io::Write, - line: &DisplaySourceLine, - ) -> std::io::Result<()> { - match line { - DisplaySourceLine::Content { text } => write!(w, " {}", text), - DisplaySourceLine::Annotation { annotation, range } => { - let (_, style) = self.get_annotation_type_style(&annotation.annotation_type); - let styles = [StyleType::Emphasis, style]; - let indent = if range.start == 0 { 0 } else { range.start + 1 }; - write!(w, "{:>width$}", "", width = indent)?; - if range.start == 0 { - let horizontal_mark = MarkKind::get(MarkKind::Horizontal); - S::fmt( - w, - format_args!( - "{}{} {}", - repeat(horizontal_mark).take(5).collect::<String>(), - MarkKind::get(MarkKind::UpLeft), - annotation.label, - ), - &styles, - ) - } else { - S::fmt( - w, - format_args!("{:->width$} {}", "", annotation.label, width = range.len()), - &styles, - ) - } - } - DisplaySourceLine::Empty => Ok(()), - } - } - - fn fmt_raw_line( - &self, - w: &mut impl std::io::Write, - line: &DisplayRawLine, - lineno_max: usize, - ) -> std::io::Result<()> { - match line { - DisplayRawLine::Origin { path, pos } => { - write!(w, "{:>width$}", "", width = lineno_max)?; - S::fmt( - w, - format_args!( - "{}{}>", - MarkKind::get(MarkKind::Horizontal), - MarkKind::get(MarkKind::Horizontal), - ), - &[StyleType::Emphasis, StyleType::LineNo], - )?; - write!(w, " {}", path)?; - if let Some(line) = pos.0 { - write!(w, ":{}", line)?; - } - writeln!(w) - } - DisplayRawLine::Annotation { annotation, .. } => { - let (desc, style) = self.get_annotation_type_style(&annotation.annotation_type); - let s = [StyleType::Emphasis, style]; - S::fmt(w, desc, &s)?; - if let Some(id) = annotation.id { - S::fmt(w, format_args!("[{}]", id), &s)?; - } - S::fmt( - w, - format_args!(": {}\n", annotation.label), - &[StyleType::Emphasis], - ) - } - } - } - - fn get_annotation_type_style( - &self, - annotation_type: &AnnotationType, - ) -> (&'static str, StyleType) { - match annotation_type { - AnnotationType::Error => ("error", StyleType::Error), - AnnotationType::Warning => ("warning", StyleType::Warning), - AnnotationType::Info => ("info", StyleType::Info), - AnnotationType::Note => ("note", StyleType::Note), - AnnotationType::Help => ("help", StyleType::Help), - AnnotationType::None => ("", StyleType::None), - } - } - - fn fmt_display_mark( - &self, - w: &mut impl std::io::Write, - display_mark: &DisplayMark, - ) -> std::io::Result<()> { - let (_, style) = self.get_annotation_type_style(&display_mark.annotation_type); - let ch = match display_mark.mark_type { - DisplayMarkType::AnnotationStart => MarkKind::get(MarkKind::DownRight), - DisplayMarkType::AnnotationEnd => MarkKind::get(MarkKind::UpRight), - DisplayMarkType::AnnotationThrough => MarkKind::get(MarkKind::Vertical), - }; - S::fmt(w, ch, &[StyleType::Emphasis, style]) - } -} - -impl<S: StyleTrait> RendererTrait for Renderer<S> { - fn fmt(&self, w: &mut impl Write, dl: &DisplayList) -> std::io::Result<()> { - Renderer::fmt(self, w, dl) - } -} diff --git a/src/renderers/ascii_default/styles/color.rs b/src/renderers/ascii_default/styles/color.rs deleted file mode 100644 index 58a81ce..0000000 --- a/src/renderers/ascii_default/styles/color.rs +++ /dev/null @@ -1,34 +0,0 @@ -use ansi_term::Color::Fixed; -use ansi_term::Style as AnsiTermStyle; - -use super::Style as StyleTrait; -use super::StyleType; - -use std::fmt; - -pub struct Style {} - -impl StyleTrait for Style { - fn fmt( - w: &mut dyn std::io::Write, - pattern: impl fmt::Display, - styles: &[StyleType], - ) -> std::io::Result<()> { - let mut style = AnsiTermStyle::new(); - for style_type in styles { - match style_type { - StyleType::Emphasis => { - style = style.bold(); - } - StyleType::Error => style = style.fg(Fixed(9)), - StyleType::Warning => style = style.fg(Fixed(11)), - StyleType::Info => style = style.fg(Fixed(12)), - StyleType::Note => {} - StyleType::Help => style = style.fg(Fixed(14)), - StyleType::LineNo => style = style.fg(Fixed(12)), - StyleType::None => {} - } - } - write!(w, "{}", style.paint(pattern.to_string())) - } -} diff --git a/src/renderers/ascii_default/styles/color2.rs b/src/renderers/ascii_default/styles/color2.rs deleted file mode 100644 index a07eae9..0000000 --- a/src/renderers/ascii_default/styles/color2.rs +++ /dev/null @@ -1,42 +0,0 @@ -use termcolor::{Ansi, Color, ColorSpec, WriteColor}; - -use super::Style as StyleTrait; -use super::StyleType; - -use std::fmt; -use std::io::Write; - -pub struct Style {} - -impl StyleTrait for Style { - fn fmt( - w: &mut dyn std::io::Write, - pattern: impl fmt::Display, - styles: &[StyleType], - ) -> std::io::Result<()> { - let mut color = ColorSpec::new(); - for style_type in styles { - match style_type { - StyleType::Emphasis => { - color.set_bold(true); - } - StyleType::Error => { - color.set_fg(Some(Color::Red)); - } - StyleType::Warning => { - color.set_fg(Some(Color::Yellow)); - } - StyleType::LineNo => { - color.set_fg(Some(Color::Ansi256(12))); - } - _ => {} - } - } - let mut ansi = Ansi::new(w); - ansi.set_color(&color).unwrap(); - //ansi.set_color(ColorSpec::new().set_bold(true)).unwrap(); - write!(ansi, "{}", pattern)?; - ansi.reset().unwrap(); - Ok(()) - } -} diff --git a/src/renderers/ascii_default/styles/mod.rs b/src/renderers/ascii_default/styles/mod.rs deleted file mode 100644 index 1381179..0000000 --- a/src/renderers/ascii_default/styles/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -#[cfg(feature = "ansi_term")] -pub mod color; -#[cfg(feature = "termcolor")] -pub mod color2; -#[cfg(all(not(feature = "ansi_term"), not(feature = "termcolor")))] -pub mod plain; - -use std::fmt; - -pub trait Style { - fn fmt( - w: &mut dyn std::io::Write, - pattern: impl fmt::Display, - styles: &[StyleType], - ) -> std::io::Result<()>; -} - -#[derive(Debug)] -pub enum StyleType { - Emphasis, - - Error, - Warning, - Info, - Note, - Help, - LineNo, - None, -} diff --git a/src/renderers/ascii_default/styles/plain.rs b/src/renderers/ascii_default/styles/plain.rs deleted file mode 100644 index 889d2e5..0000000 --- a/src/renderers/ascii_default/styles/plain.rs +++ /dev/null @@ -1,16 +0,0 @@ -use super::Style as StyleTrait; -use super::StyleType; - -use std::fmt; - -pub struct Style {} - -impl StyleTrait for Style { - fn fmt( - w: &mut dyn std::io::Write, - pattern: impl fmt::Display, - _styles: &[StyleType], - ) -> std::io::Result<()> { - write!(w, "{}", pattern) - } -} diff --git a/src/renderers/mod.rs b/src/renderers/mod.rs deleted file mode 100644 index b37cd72..0000000 --- a/src/renderers/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[cfg(not(feature = "html"))] -pub mod ascii_default; -#[cfg(feature = "html")] -pub mod html; - -#[cfg(feature = "html")] -use html::get_renderer as get_type_renderer; - -#[cfg(not(feature = "html"))] -use ascii_default::get_renderer as get_type_renderer; - -use crate::DisplayList; -use std::io::Write; - -pub trait Renderer { - fn fmt(&self, w: &mut impl Write, dl: &DisplayList) -> std::io::Result<()>; -} - -pub fn get_renderer() -> impl Renderer { - get_type_renderer() -} diff --git a/src/slice.rs b/src/slice.rs deleted file mode 100644 index 23a58d0..0000000 --- a/src/slice.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::annotation::SourceAnnotation; - -#[derive(Debug, Clone, Default)] -pub struct Slice<'s> { - pub source: &'s str, - pub line_start: Option<usize>, - pub origin: Option<&'s str>, - pub annotations: &'s [SourceAnnotation<'s>], -} diff --git a/src/snippet.rs b/src/snippet.rs deleted file mode 100644 index 3be7c34..0000000 --- a/src/snippet.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::annotation::Annotation; -use crate::slice::Slice; - -#[derive(Debug, Clone)] -pub struct Snippet<'s> { - pub title: Option<Annotation<'s>>, - pub footer: &'s [Annotation<'s>], - pub slices: &'s [Slice<'s>], -} diff --git a/src/span.rs b/src/span.rs new file mode 100644 index 0000000..1896ac4 --- /dev/null +++ b/src/span.rs @@ -0,0 +1,233 @@ +use std::{io, ops::Range}; + +pub trait Span: Clone { + type Subspan: Span<Pos = Self::Pos>; + type Pos: Ord + Copy; + + fn start(&self) -> Self::Pos; + fn end(&self) -> Self::Pos; + fn slice(&self, range: Range<Self::Pos>) -> Self::Subspan; +} + +pub trait SpanFormatter<Span: self::Span> { + fn first_line(&self, span: &Span) -> WithLineNumber<Span::Subspan>; + fn next_line( + &self, + span: &Span, + subspan: &WithLineNumber<Span::Subspan>, + ) -> Option<WithLineNumber<Span::Subspan>>; + fn count_columns(&self, span: &Span, subspan: &Span::Subspan) -> usize; +} + +pub trait SpanWriter<Span: crate::Span> { + fn write(&self, w: &mut dyn io::Write, span: &Span, subspan: &Span::Subspan) -> io::Result<()>; +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct WithLineNumber<T> { + pub line_num: usize, + pub data: T, +} + +impl Span for &str { + /// Byte index range into this string. + type Subspan = Range<usize>; + /// Byte index into this string. + type Pos = usize; + + fn start(&self) -> Self::Pos { + 0 + } + + fn end(&self) -> Self::Pos { + self.len() + } + + fn slice(&self, range: Range<Self::Pos>) -> Range<usize> { + range + } +} + +impl SpanFormatter<&str> for () { + fn first_line(&self, span: &&str) -> WithLineNumber<Range<usize>> { + let start = 0; + let end = span + .as_bytes() + .iter() + .enumerate() + .find(|(_, &b)| b == b'\n') + .map_or_else(|| span.len(), |(i, _)| i); + WithLineNumber { + data: start..end, + line_num: 1, + } + } + + fn next_line( + &self, + span: &&str, + subspan: &WithLineNumber<Range<usize>>, + ) -> Option<WithLineNumber<Range<usize>>> { + let start = subspan.data.end + 1; + let end = span + .get(start..)? + .as_bytes() + .iter() + .enumerate() + .find(|(_, &b)| b == b'\n') + .map_or_else(|| span.len(), |(i, _)| i + start); + Some(WithLineNumber { + data: start..end, + line_num: subspan.line_num + 1, + }) + } + + fn count_columns(&self, span: &&str, subspan: &Range<usize>) -> usize { + span[subspan.start..subspan.end].chars().count() + } +} + +impl SpanWriter<&str> for () { + fn write( + &self, + w: &mut dyn io::Write, + span: &&str, + subspan: &Range<usize>, + ) -> io::Result<()> { + w.write_all(span[subspan.start..subspan.end].as_bytes()) + } +} + +impl<S: Span> Span for WithLineNumber<S> { + type Subspan = S::Subspan; + type Pos = S::Pos; + + fn start(&self) -> S::Pos { + self.data.start() + } + + fn end(&self) -> S::Pos { + self.data.end() + } + + fn slice(&self, range: Range<S::Pos>) -> S::Subspan { + self.data.slice(range) + } +} + +impl<S: Span, SF: SpanFormatter<S>> SpanFormatter<WithLineNumber<S>> for SF { + fn first_line(&self, span: &WithLineNumber<S>) -> WithLineNumber<S::Subspan> { + let wln = self.first_line(&span.data); + WithLineNumber { + data: wln.data, + line_num: wln.line_num + span.line_num - 1, + } + } + + fn next_line( + &self, + span: &WithLineNumber<S>, + subspan: &WithLineNumber<S::Subspan>, + ) -> Option<WithLineNumber<S::Subspan>> { + self.next_line(&span.data, subspan) + } + + fn count_columns(&self, span: &WithLineNumber<S>, subspan: &S::Subspan) -> usize { + self.count_columns(&span.data, subspan) + } +} + +impl<S: Span, SW: SpanWriter<S>> SpanWriter<WithLineNumber<S>> for SW { + fn write( + &self, + w: &mut dyn io::Write, + span: &WithLineNumber<S>, + subspan: &S::Subspan, + ) -> io::Result<()> { + self.write(w, &span.data, subspan) + } +} + +impl Span for Range<usize> { + /// Byte index into the source, _not_ this range. + /// This is a "sibling" subspan. + type Subspan = Range<usize>; + /// Byte index into the source. + type Pos = usize; + + fn start(&self) -> Self::Pos { + self.start + } + + fn end(&self) -> Self::Pos { + self.end + } + + fn slice(&self, range: Range<Self::Pos>) -> Range<usize> { + range + } +} + +impl SpanFormatter<Range<usize>> for &str { + fn first_line(&self, span: &Range<usize>) -> WithLineNumber<Range<usize>> { + let start = self[..span.start] + .as_bytes() + .iter() + .enumerate() + .rfind(|(_, &b)| b == b'\n') + .map_or_else(|| 0, |(i, _)| i + 1); + let end = self[start..] + .as_bytes() + .iter() + .enumerate() + .find(|(_, &b)| b == b'\n') + .map_or_else(|| span.len(), |(i, _)| i + start); + #[allow(clippy::naive_bytecount)] + WithLineNumber { + data: start..end, + line_num: self[..start] + .as_bytes() + .iter() + .filter(|&&b| b == b'\n') + .count(), + } + } + + fn next_line( + &self, + span: &Range<usize>, + subspan: &WithLineNumber<Range<usize>>, + ) -> Option<WithLineNumber<Range<usize>>> { + let start = subspan.data.end + 1; + let end = self + .get(start..)? + .as_bytes() + .iter() + .enumerate() + .find(|(_, &b)| b == b'\n') + .map_or_else(|| span.end, |(i, _)| i + start); + if start <= span.end { + Some(WithLineNumber { + data: start..end, + line_num: subspan.line_num + 1, + }) + } else { + None + } + } + + fn count_columns(&self, _span: &Range<usize>, subspan: &Range<usize>) -> usize { + self[subspan.start..subspan.end].chars().count() + } +} + +impl SpanWriter<Range<usize>> for &str { + fn write( + &self, + w: &mut dyn io::Write, + _span: &Range<usize>, + subspan: &Range<usize>, + ) -> io::Result<()> { + w.write_all(self[subspan.start..subspan.end].as_bytes()) + } +}