/*! Default trait implementations for [`SourceCode`]. */ use std::{ borrow::{Cow, ToOwned}, collections::VecDeque, fmt::Debug, sync::Arc, }; use crate::{MietteError, MietteSpanContents, SourceCode, SourceSpan, SpanContents}; fn context_info<'a>( input: &'a [u8], span: &SourceSpan, context_lines_before: usize, context_lines_after: usize, ) -> Result, MietteError> { let mut offset = 0usize; let mut line_count = 0usize; let mut start_line = 0usize; let mut start_column = 0usize; let mut before_lines_starts = VecDeque::new(); let mut current_line_start = 0usize; let mut end_lines = 0usize; let mut post_span = false; let mut post_span_got_newline = false; let mut iter = input.iter().copied().peekable(); while let Some(char) = iter.next() { if matches!(char, b'\r' | b'\n') { line_count += 1; if char == b'\r' && iter.next_if_eq(&b'\n').is_some() { offset += 1; } if offset < span.offset() { // We're before the start of the span. start_column = 0; before_lines_starts.push_back(current_line_start); if before_lines_starts.len() > context_lines_before { start_line += 1; before_lines_starts.pop_front(); } } else if offset >= span.offset() + span.len().saturating_sub(1) { // We're after the end of the span, but haven't necessarily // started collecting end lines yet (we might still be // collecting context lines). if post_span { start_column = 0; if post_span_got_newline { end_lines += 1; } else { post_span_got_newline = true; } if end_lines >= context_lines_after { offset += 1; break; } } } current_line_start = offset + 1; } else if offset < span.offset() { start_column += 1; } if offset >= (span.offset() + span.len()).saturating_sub(1) { post_span = true; if end_lines >= context_lines_after { offset += 1; break; } } offset += 1; } if offset >= (span.offset() + span.len()).saturating_sub(1) { let starting_offset = before_lines_starts.front().copied().unwrap_or_else(|| { if context_lines_before == 0 { span.offset() } else { 0 } }); Ok(MietteSpanContents::new( &input[starting_offset..offset], (starting_offset, offset - starting_offset).into(), start_line, if context_lines_before == 0 { start_column } else { 0 }, line_count, )) } else { Err(MietteError::OutOfBounds) } } impl SourceCode for [u8] { fn read_span<'a>( &'a self, span: &SourceSpan, context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError> { let contents = context_info(self, span, context_lines_before, context_lines_after)?; Ok(Box::new(contents)) } } impl<'src> SourceCode for &'src [u8] { fn read_span<'a>( &'a self, span: &SourceSpan, context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError> { <[u8] as SourceCode>::read_span(self, span, context_lines_before, context_lines_after) } } impl SourceCode for Vec { fn read_span<'a>( &'a self, span: &SourceSpan, context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError> { <[u8] as SourceCode>::read_span(self, span, context_lines_before, context_lines_after) } } impl SourceCode for str { fn read_span<'a>( &'a self, span: &SourceSpan, context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError> { <[u8] as SourceCode>::read_span( self.as_bytes(), span, context_lines_before, context_lines_after, ) } } /// Makes `src: &'static str` or `struct S<'a> { src: &'a str }` usable. impl<'s> SourceCode for &'s str { fn read_span<'a>( &'a self, span: &SourceSpan, context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError> { ::read_span(self, span, context_lines_before, context_lines_after) } } impl SourceCode for String { fn read_span<'a>( &'a self, span: &SourceSpan, context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError> { ::read_span(self, span, context_lines_before, context_lines_after) } } impl SourceCode for Arc { fn read_span<'a>( &'a self, span: &SourceSpan, context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError> { self.as_ref() .read_span(span, context_lines_before, context_lines_after) } } impl SourceCode for Cow<'_, T> where // The minimal bounds are used here. // `T::Owned` need not be // `SourceCode`, because `&T` // can always be obtained from // `Cow<'_, T>`. T::Owned: Debug + Send + Sync, { fn read_span<'a>( &'a self, span: &SourceSpan, context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError> { self.as_ref() .read_span(span, context_lines_before, context_lines_after) } } #[cfg(test)] mod tests { use super::*; #[test] fn basic() -> Result<(), MietteError> { let src = String::from("foo\n"); let contents = src.read_span(&(0, 4).into(), 0, 0)?; assert_eq!("foo\n", std::str::from_utf8(contents.data()).unwrap()); assert_eq!(0, contents.line()); assert_eq!(0, contents.column()); Ok(()) } #[test] fn shifted() -> Result<(), MietteError> { let src = String::from("foobar"); let contents = src.read_span(&(3, 3).into(), 1, 1)?; assert_eq!("foobar", std::str::from_utf8(contents.data()).unwrap()); assert_eq!(0, contents.line()); assert_eq!(0, contents.column()); Ok(()) } #[test] fn middle() -> Result<(), MietteError> { let src = String::from("foo\nbar\nbaz\n"); let contents = src.read_span(&(4, 4).into(), 0, 0)?; assert_eq!("bar\n", std::str::from_utf8(contents.data()).unwrap()); assert_eq!(1, contents.line()); assert_eq!(0, contents.column()); Ok(()) } #[test] fn middle_of_line() -> Result<(), MietteError> { let src = String::from("foo\nbarbar\nbaz\n"); let contents = src.read_span(&(7, 4).into(), 0, 0)?; assert_eq!("bar\n", std::str::from_utf8(contents.data()).unwrap()); assert_eq!(1, contents.line()); assert_eq!(3, contents.column()); Ok(()) } #[test] fn with_crlf() -> Result<(), MietteError> { let src = String::from("foo\r\nbar\r\nbaz\r\n"); let contents = src.read_span(&(5, 5).into(), 0, 0)?; assert_eq!("bar\r\n", std::str::from_utf8(contents.data()).unwrap()); assert_eq!(1, contents.line()); assert_eq!(0, contents.column()); Ok(()) } #[test] fn with_context() -> Result<(), MietteError> { let src = String::from("xxx\nfoo\nbar\nbaz\n\nyyy\n"); let contents = src.read_span(&(8, 3).into(), 1, 1)?; assert_eq!( "foo\nbar\nbaz\n", std::str::from_utf8(contents.data()).unwrap() ); assert_eq!(1, contents.line()); assert_eq!(0, contents.column()); Ok(()) } #[test] fn multiline_with_context() -> Result<(), MietteError> { let src = String::from("aaa\nxxx\n\nfoo\nbar\nbaz\n\nyyy\nbbb\n"); let contents = src.read_span(&(9, 11).into(), 1, 1)?; assert_eq!( "\nfoo\nbar\nbaz\n\n", std::str::from_utf8(contents.data()).unwrap() ); assert_eq!(2, contents.line()); assert_eq!(0, contents.column()); let span: SourceSpan = (8, 14).into(); assert_eq!(&span, contents.span()); Ok(()) } #[test] fn multiline_with_context_line_start() -> Result<(), MietteError> { let src = String::from("one\ntwo\n\nthree\nfour\nfive\n\nsix\nseven\n"); let contents = src.read_span(&(2, 0).into(), 2, 2)?; assert_eq!( "one\ntwo\n\n", std::str::from_utf8(contents.data()).unwrap() ); assert_eq!(0, contents.line()); assert_eq!(0, contents.column()); let span: SourceSpan = (0, 9).into(); assert_eq!(&span, contents.span()); Ok(()) } }