spanner/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(
3	clippy::pedantic,
4	clippy::allow_attributes_without_reason,
5	missing_docs
6)]
7#![feature(round_char_boundary)]
8
9mod buffer;
10mod line_col;
11mod loc;
12
13use {
14	::core::{cmp::Ordering, fmt::Debug},
15	::std::sync::Arc,
16};
17pub use {buffer::*, line_col::*, loc::*};
18
19/// handles handing out [`Span`]s that point into [`Buffer`]s
20///
21/// ```rust
22/// use ::spanner::{BufferSource, Spanner, Span};
23///
24/// struct MySource {
25///     filename: &'static str,
26///     source: &'static str,
27/// }
28///
29/// impl BufferSource for MySource {
30///     fn source(&self) -> &str {
31///         &self.source
32///     }
33///
34///     fn name(&self) -> &str {
35///         &self.filename
36///     }
37/// }
38///
39/// let spanner = Spanner::new();
40///
41/// let buf = spanner.add(|_| MySource {
42///     filename: "file.rs",
43///     source: r#"fn main() { println!("hello world!") }"#,
44/// });
45/// ``````
46#[derive(Debug)]
47pub struct Spanner<Src: BufferSource = String> {
48	buffers: Vec<Arc<Buffer<Src>>>,
49	end_linear_index: usize,
50}
51
52impl<Src: BufferSource> Spanner<Src> {
53	/// construct a new empty spanner
54	#[must_use]
55	pub fn new() -> Self {
56		Self {
57			buffers: vec![],
58			end_linear_index: 0,
59		}
60	}
61
62	/// add a buffer to the spanner
63	///
64	/// the function passed should take the starting [`Loc`]ation of the [`Buffer`], and return the Src of it
65	#[must_use]
66	#[allow(clippy::cast_possible_truncation, reason = "documented")]
67	pub fn add(&mut self, src: impl FnOnce(Loc) -> Src) -> Arc<Buffer<Src>> {
68		let bufn = self.buffers.len() as u16;
69		let src = src(Loc { pos: 0, buf: bufn });
70		let src_code = src.source();
71		let line_beginnings = line_col::calc_line_beginnings(src_code);
72
73		let linear_index_range = self.end_linear_index..self.end_linear_index + src_code.len();
74
75		let buf = Arc::new(Buffer {
76			index: bufn,
77			linear_span: linear_index_range.clone(),
78			src,
79			line_beginnings,
80		});
81
82		self.buffers.push(Arc::clone(&buf));
83		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
84
85		buf
86	}
87
88	/// find the buffer that contains this [`Loc`]
89	#[must_use]
90	pub fn lookup_buf(&self, loc: Loc) -> &Arc<Buffer<Src>> {
91		&self.buffers[loc.buf as usize]
92	}
93
94	/// convert this [`Span`] into one that refers to its source code
95	#[must_use]
96	pub fn lookup_span(&self, span: Span) -> SrcSpan<Src> {
97		SrcSpan {
98			start: span.start,
99			end: span.end,
100			buf: self.lookup_buf(span.start()).clone(),
101		}
102	}
103
104	/// find the source [`str`] for this [`Span`]
105	#[must_use]
106	pub fn lookup_src(&self, span: Span) -> &str {
107		self.buffers[span.buf as usize].src_slice(span)
108	}
109
110	/// find a linear index from a [`Loc`]
111	///
112	/// all [`Buffer`]s have non-overlapping ranges, meaning a linear index contains both buffer and location information
113	#[must_use]
114	pub fn lookup_linear_index(&self, loc: Loc) -> usize {
115		self.lookup_buf(loc).linear_span.start + loc.pos as usize
116	}
117
118	/// find a [`Loc`] from a linear index
119	///
120	/// all [`Buffer`]s have non-overlapping ranges, meaning a linear index contains both buffer and location information
121	///
122	/// # Panics
123	///
124	/// if linear index is out of range
125	#[must_use]
126	#[allow(clippy::cast_possible_truncation, reason = "documented")]
127	pub fn lookup_loc(&self, linear_index: usize) -> Loc {
128		assert!(
129			linear_index < self.end_linear_index,
130			"linear index out of range"
131		);
132		let buf = self
133			.buffers
134			.binary_search_by(|buf| {
135				if linear_index > buf.linear_span.end + 1 {
136					Ordering::Less
137				} else if linear_index < buf.linear_span.start {
138					Ordering::Greater
139				} else {
140					Ordering::Equal
141				}
142			})
143			.unwrap();
144		Loc {
145			pos: (linear_index - self.buffers[buf].linear_span.start) as u32,
146			buf: buf as u16,
147		}
148	}
149}
150
151impl<Src: BufferSource> Default for Spanner<Src> {
152	fn default() -> Self {
153		Self::new()
154	}
155}
156
157#[cfg(feature = "miette")]
158mod miette_impls {
159	use {
160		super::*,
161		::miette::{MietteError, SourceCode, SourceSpan, SpanContents},
162		::tracing::{error, info, instrument, trace, warn},
163	};
164
165	impl<Src: BufferSource + Send + Sync> SourceCode for Spanner<Src> {
166		#[instrument(skip(self))]
167		fn read_span<'a>(
168			&'a self,
169			miette_span: &SourceSpan,
170			context_lines_before: usize,
171			context_lines_after: usize,
172		) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError> {
173			if miette_span.offset() == 0 {
174				return Err(MietteError::OutOfBounds);
175			}
176
177			let (mut start, mut end) = (
178				self.lookup_loc(miette_span.offset()),
179				self.lookup_loc(miette_span.offset() + miette_span.len()),
180			);
181
182			if !end.same_buf_as(&start) {
183				error!(?start, ?end, "span crosses buffers");
184				return Err(MietteError::OutOfBounds);
185			}
186
187			let buf = self.lookup_buf(start);
188			if start == buf.end() {
189				info!("labeling end of buffer");
190				start = buf.end() - 2;
191				end = start + 2;
192			}
193
194			let src_span = self.lookup_span(Span::new(start, end));
195			let source = buf.src.source();
196			let new_miette_span = {
197				let nms_start = source.ceil_char_boundary(src_span.start as usize);
198				let nms_end = source.floor_char_boundary(src_span.end as usize);
199				SourceSpan::new(nms_start.into(), nms_end - nms_start)
200			};
201			trace!(?start, ?end, ?new_miette_span, %src_span);
202
203			let contents =
204				source.read_span(&new_miette_span, context_lines_before, context_lines_after)?;
205
206			struct ContentsOverride<'a>(
207				Box<dyn SpanContents<'a> + 'a>,
208				SourceSpan,
209				Option<&'a str>,
210			);
211
212			impl<'a> SpanContents<'a> for ContentsOverride<'a> {
213				fn data(&self) -> &'a [u8] {
214					self.0.data()
215				}
216
217				fn span(&self) -> &SourceSpan {
218					&self.1
219				}
220
221				fn name(&self) -> Option<&str> {
222					self.2.or(self.0.name())
223				}
224
225				fn line(&self) -> usize {
226					self.0.line()
227				}
228
229				fn column(&self) -> usize {
230					self.0.column()
231				}
232
233				fn line_count(&self) -> usize {
234					self.0.line_count()
235				}
236
237				fn language(&self) -> Option<&str> {
238					None
239				}
240			}
241
242			let out_span = *contents.span();
243			let contents = Box::new(ContentsOverride(
244				contents,
245				SourceSpan::new(
246					(out_span.offset() + buf.linear_span.start).into(),
247					out_span.len(),
248				),
249				buf.src.name(),
250			));
251
252			trace!(contents = ?DebugSpanContents(&*contents));
253
254			Ok(contents)
255		}
256	}
257
258	struct DebugSpanContents<'a, 'b>(&'a dyn SpanContents<'b>);
259
260	impl ::core::fmt::Debug for DebugSpanContents<'_, '_> {
261		fn fmt(&self, fmt: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
262			fmt.debug_struct("SpanContents")
263				.field("data", &::core::str::from_utf8(self.0.data()).unwrap())
264				.field("span", &self.0.span())
265				.field("name", &self.0.name())
266				.field("line", &self.0.line())
267				.field("column", &self.0.column())
268				.field("line_count", &self.0.line_count())
269				.field("language", &self.0.language())
270				.finish()
271		}
272	}
273}