spanner 0.2.0

map source code positions to easy-to-use structs
Documentation
#![doc = include_str!("../README.md")]
#![warn(
	clippy::pedantic,
	clippy::allow_attributes_without_reason,
	missing_docs
)]
#![feature(round_char_boundary)]

mod buffer;
mod line_col;
mod loc;

use {
	::core::{cmp::Ordering, fmt::Debug},
	::std::sync::Arc,
};
pub use {buffer::*, line_col::*, loc::*};

/// handles handing out [`Span`]s that point into [`Buffer`]s
///
/// ```rust
/// use ::spanner::{BufferSource, Spanner, Span};
///
/// struct MySource {
///     filename: &'static str,
///     source: &'static str,
/// }
///
/// impl BufferSource for MySource {
///     fn source(&self) -> &str {
///         &self.source
///     }
///
///     fn name(&self) -> &str {
///         &self.filename
///     }
/// }
///
/// let spanner = Spanner::new();
///
/// let buf = spanner.add(|_| MySource {
///     filename: "file.rs",
///     source: r#"fn main() { println!("hello world!") }"#,
/// });
/// ``````
#[derive(Debug)]
pub struct Spanner<Src: BufferSource = String> {
	buffers: Vec<Arc<Buffer<Src>>>,
	end_linear_index: usize,
}

impl<Src: BufferSource> Spanner<Src> {
	/// construct a new empty spanner
	#[must_use]
	pub fn new() -> Self {
		Self {
			buffers: vec![],
			end_linear_index: 0,
		}
	}

	/// add a buffer to the spanner
	///
	/// the function passed should take the starting [`Loc`]ation of the [`Buffer`], and return the Src of it
	#[must_use]
	#[allow(clippy::cast_possible_truncation, reason = "documented")]
	pub fn add(&mut self, src: impl FnOnce(Loc) -> Src) -> Arc<Buffer<Src>> {
		let bufn = self.buffers.len() as u16;
		let src = src(Loc { pos: 0, buf: bufn });
		let src_code = src.source();
		let line_beginnings = line_col::calc_line_beginnings(src_code);

		let linear_index_range = self.end_linear_index..self.end_linear_index + src_code.len();

		let buf = Arc::new(Buffer {
			index: bufn,
			linear_span: linear_index_range.clone(),
			src,
			line_beginnings,
		});

		self.buffers.push(Arc::clone(&buf));
		self.end_linear_index = linear_index_range.end + 1; // a gap of 1 byte between buffers to allow buffers to refer to their end and be in the same range

		buf
	}

	/// find the buffer that contains this [`Loc`]
	#[must_use]
	pub fn lookup_buf(&self, loc: Loc) -> &Arc<Buffer<Src>> {
		&self.buffers[loc.buf as usize]
	}

	/// convert this [`Span`] into one that refers to its source code
	#[must_use]
	pub fn lookup_span(&self, span: Span) -> SrcSpan<Src> {
		SrcSpan {
			start: span.start,
			end: span.end,
			buf: self.lookup_buf(span.start()).clone(),
		}
	}

	/// find the source [`str`] for this [`Span`]
	#[must_use]
	pub fn lookup_src(&self, span: Span) -> &str {
		self.buffers[span.buf as usize].src_slice(span)
	}

	/// find a linear index from a [`Loc`]
	///
	/// all [`Buffer`]s have non-overlapping ranges, meaning a linear index contains both buffer and location information
	#[must_use]
	pub fn lookup_linear_index(&self, loc: Loc) -> usize {
		self.lookup_buf(loc).linear_span.start + loc.pos as usize
	}

	/// find a [`Loc`] from a linear index
	///
	/// all [`Buffer`]s have non-overlapping ranges, meaning a linear index contains both buffer and location information
	///
	/// # Panics
	///
	/// if linear index is out of range
	#[must_use]
	#[allow(clippy::cast_possible_truncation, reason = "documented")]
	pub fn lookup_loc(&self, linear_index: usize) -> Loc {
		assert!(
			linear_index < self.end_linear_index,
			"linear index out of range"
		);
		let buf = self
			.buffers
			.binary_search_by(|buf| {
				if linear_index > buf.linear_span.end + 1 {
					Ordering::Less
				} else if linear_index < buf.linear_span.start {
					Ordering::Greater
				} else {
					Ordering::Equal
				}
			})
			.unwrap();
		Loc {
			pos: (linear_index - self.buffers[buf].linear_span.start) as u32,
			buf: buf as u16,
		}
	}
}

impl<Src: BufferSource> Default for Spanner<Src> {
	fn default() -> Self {
		Self::new()
	}
}

#[cfg(feature = "miette")]
mod miette_impls {
	use {
		super::*,
		::miette::{MietteError, SourceCode, SourceSpan, SpanContents},
		::tracing::{error, info, instrument, trace, warn},
	};

	impl<Src: BufferSource + Send + Sync> SourceCode for Spanner<Src> {
		#[instrument(skip(self))]
		fn read_span<'a>(
			&'a self,
			miette_span: &SourceSpan,
			context_lines_before: usize,
			context_lines_after: usize,
		) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError> {
			if miette_span.offset() == 0 {
				return Err(MietteError::OutOfBounds);
			}

			let (mut start, mut end) = (
				self.lookup_loc(miette_span.offset()),
				self.lookup_loc(miette_span.offset() + miette_span.len()),
			);

			if !end.same_buf_as(&start) {
				error!(?start, ?end, "span crosses buffers");
				return Err(MietteError::OutOfBounds);
			}

			let buf = self.lookup_buf(start);
			if start == buf.end() {
				info!("labeling end of buffer");
				start = buf.end() - 2;
				end = start + 2;
			}

			let src_span = self.lookup_span(Span::new(start, end));
			let source = buf.src.source();
			let new_miette_span = {
				let nms_start = source.ceil_char_boundary(src_span.start as usize);
				let nms_end = source.floor_char_boundary(src_span.end as usize);
				SourceSpan::new(nms_start.into(), nms_end - nms_start)
			};
			trace!(?start, ?end, ?new_miette_span, %src_span);

			let contents =
				source.read_span(&new_miette_span, context_lines_before, context_lines_after)?;

			struct ContentsOverride<'a>(
				Box<dyn SpanContents<'a> + 'a>,
				SourceSpan,
				Option<&'a str>,
			);

			impl<'a> SpanContents<'a> for ContentsOverride<'a> {
				fn data(&self) -> &'a [u8] {
					self.0.data()
				}

				fn span(&self) -> &SourceSpan {
					&self.1
				}

				fn name(&self) -> Option<&str> {
					self.2.or(self.0.name())
				}

				fn line(&self) -> usize {
					self.0.line()
				}

				fn column(&self) -> usize {
					self.0.column()
				}

				fn line_count(&self) -> usize {
					self.0.line_count()
				}

				fn language(&self) -> Option<&str> {
					None
				}
			}

			let out_span = *contents.span();
			let contents = Box::new(ContentsOverride(
				contents,
				SourceSpan::new(
					(out_span.offset() + buf.linear_span.start).into(),
					out_span.len(),
				),
				buf.src.name(),
			));

			trace!(contents = ?DebugSpanContents(&*contents));

			Ok(contents)
		}
	}

	struct DebugSpanContents<'a, 'b>(&'a dyn SpanContents<'b>);

	impl ::core::fmt::Debug for DebugSpanContents<'_, '_> {
		fn fmt(&self, fmt: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
			fmt.debug_struct("SpanContents")
				.field("data", &::core::str::from_utf8(self.0.data()).unwrap())
				.field("span", &self.0.span())
				.field("name", &self.0.name())
				.field("line", &self.0.line())
				.field("column", &self.0.column())
				.field("line_count", &self.0.line_count())
				.field("language", &self.0.language())
				.finish()
		}
	}
}