use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::Range;
use std::sync::{
Arc, OnceLock,
atomic::{AtomicU16, Ordering},
};
pub type Offset = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct LineColumn {
pub line: usize,
pub column: usize,
}
impl Display for LineColumn {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}:{}", self.line, self.column)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LineColumnRange {
pub start: LineColumn,
pub end: LineColumn,
}
impl Display for LineColumnRange {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
if self.start == self.end {
Display::fmt(&self.start, formatter)
} else {
write!(formatter, "{}-{}", self.start, self.end)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TextEdit {
Insertion {
at: Offset,
text: String,
},
Deletion {
start: Offset,
end: Offset,
},
}
impl TextEdit {
pub fn insertion(at: Offset, text: impl Into<String>) -> Self {
Self::Insertion {
at,
text: text.into(),
}
}
pub fn deletion(start: Offset, end: Offset) -> Self {
assert!(start <= end, "deletion start must not exceed its end");
Self::Deletion { start, end }
}
fn shift(&self, offset: Offset) -> Offset {
match self {
Self::Insertion { at, text } => {
if offset >= *at {
offset + text.len()
} else {
offset
}
}
Self::Deletion { start, end } => {
if offset <= *start {
offset
} else if offset <= *end {
*start
} else {
offset - (end - start)
}
}
}
}
pub fn apply(&self, source: &str) -> Option<String> {
let (cut, resumes) = match self {
Self::Insertion { at, .. } => (*at, *at),
Self::Deletion { start, end } => (*start, *end),
};
let mut edited = String::from(source.get(..cut)?);
if let Self::Insertion { text, .. } = self {
edited.push_str(text);
}
edited.push_str(source.get(resumes..)?);
Some(edited)
}
}
static NEXT_FILE_ID: AtomicU16 = AtomicU16::new(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum File {
FileId(u16),
Synthetic(u16),
}
impl File {
pub fn fresh() -> Self {
let id = NEXT_FILE_ID.fetch_add(1, Ordering::Relaxed);
Self::FileId(id)
}
pub const fn from_raw(raw: u16) -> Self {
Self::FileId(raw)
}
pub const fn into_raw(self) -> u16 {
match self {
Self::FileId(id) | Self::Synthetic(id) => id,
}
}
}
pub type SourceId = File;
pub struct Source {
file: File,
text: Arc<str>,
line_starts: OnceLock<Box<[Offset]>>,
}
impl Source {
pub fn new(text: impl Into<Arc<str>>) -> Arc<Self> {
Self::with_file(File::fresh(), text)
}
pub fn synthetic(text: impl Into<Arc<str>>) -> Arc<Self> {
let id = NEXT_FILE_ID.fetch_add(1, Ordering::Relaxed);
Self::with_file(File::Synthetic(id), text)
}
pub fn with_file(file: File, text: impl Into<Arc<str>>) -> Arc<Self> {
let text = text.into();
Arc::new(Self {
file,
text,
line_starts: OnceLock::new(),
})
}
pub fn file(&self) -> File {
self.file
}
pub fn text(&self) -> &str {
&self.text
}
pub fn line_column(&self, offset: Offset) -> Option<LineColumn> {
self.text.get(..offset)?;
let line_starts = self.line_starts.get_or_init(|| {
std::iter::once(0)
.chain(
self.text
.bytes()
.enumerate()
.filter_map(|(offset, byte)| (byte == b'\n').then_some(offset + 1)),
)
.collect::<Vec<_>>()
.into_boxed_slice()
});
let line_index = line_starts
.partition_point(|line_start| *line_start <= offset)
.checked_sub(1)?;
let line_start = line_starts[line_index];
let column = self.text.get(line_start..offset)?.chars().count() + 1;
Some(LineColumn {
line: line_index + 1,
column,
})
}
}
impl Debug for Source {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Source")
.field("file", &self.file)
.field("bytes", &self.text.len())
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct Span {
source: Arc<Source>,
range: Option<Range<Offset>>,
}
impl Span {
pub fn new(source: Arc<Source>, range: Option<Range<Offset>>) -> Self {
Self {
source,
range: range.filter(|range| range.start <= range.end),
}
}
pub fn ranged(source: Arc<Source>, range: Range<Offset>) -> Self {
Self::new(source, Some(range))
}
pub fn unlocated(source: Arc<Source>) -> Self {
Self {
source,
range: None,
}
}
pub fn for_text(text: impl Into<Arc<str>>) -> Self {
let source = Source::synthetic(text);
Self::ranged(Arc::clone(&source), 0..source.text().len())
}
pub fn file(&self) -> File {
self.source.file()
}
pub fn source(&self) -> &Arc<Source> {
&self.source
}
pub fn source_id(&self) -> File {
self.file()
}
pub fn range(&self) -> Option<Range<Offset>> {
self.range.clone()
}
pub fn with_source(mut self, source: Arc<Source>) -> Self {
self.source = source;
self
}
pub fn with_text(self, text: impl Into<Arc<str>>) -> Self {
let text = text.into();
if self.text() == Some(text.as_ref()) {
return self;
}
let source = Source::with_file(self.file(), text);
Self::ranged(Arc::clone(&source), 0..source.text().len())
}
pub fn text(&self) -> Option<&str> {
self.source.text().get(self.range.as_ref()?.clone())
}
pub fn at(&self, offset: Offset) -> Self {
if self.range.is_none() {
return Self::unlocated(Arc::clone(&self.source));
}
Self {
source: Arc::clone(&self.source),
range: Some(offset..offset),
}
}
pub fn subspan(&self, start: Offset, end: Offset) -> Self {
let Some(outer) = &self.range else {
return Self::unlocated(Arc::clone(&self.source));
};
let Some(start) = outer.start.checked_add(start) else {
return Self::unlocated(Arc::clone(&self.source));
};
let Some(end) = outer.start.checked_add(end) else {
return Self::unlocated(Arc::clone(&self.source));
};
if start > end || end > outer.end {
return Self::unlocated(Arc::clone(&self.source));
}
Self {
source: Arc::clone(&self.source),
range: Some(start..end),
}
}
pub fn start(&self) -> Option<Offset> {
self.bounds().map(|(start, _)| start)
}
pub fn end(&self) -> Option<Offset> {
self.bounds().map(|(_, end)| end)
}
pub fn bounds(&self) -> Option<(Offset, Offset)> {
self.range.as_ref().map(|range| (range.start, range.end))
}
pub fn line_column_range(&self) -> Option<LineColumnRange> {
let (start, end) = self.bounds()?;
Some(LineColumnRange {
start: self.source.line_column(start)?,
end: self.source.line_column(end)?,
})
}
pub fn len(&self) -> Option<usize> {
self.bounds().map(|(start, end)| end - start)
}
pub fn is_empty(&self) -> bool {
self.len().unwrap_or(0) == 0
}
pub fn shares_source(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.source, &other.source)
}
pub fn contains(&self, offset: Offset) -> bool {
self.bounds()
.is_some_and(|(start, end)| start <= offset && offset < end)
}
pub fn touches(&self, offset: Offset) -> bool {
self.bounds()
.is_some_and(|(start, end)| start <= offset && offset <= end)
}
pub fn shifted(&self, edit: &TextEdit) -> Self {
match self.bounds() {
None => Self::unlocated(Arc::clone(&self.source)),
Some((start, end)) => Self {
source: Arc::clone(&self.source),
range: Some(edit.shift(start)..edit.shift(end)),
},
}
}
pub fn resized(&self, len: usize) -> Self {
let Some(start) = self.start() else {
return Self::unlocated(Arc::clone(&self.source));
};
Self {
source: Arc::clone(&self.source),
range: Some(start..start.saturating_add(len)),
}
}
}
impl Span {
pub fn join(&self, other: Self) -> Option<Self> {
if !self.shares_source(&other) {
return None;
}
match (self.bounds(), other.bounds()) {
(None, _) => Some(other),
(_, None) => Some(self.clone()),
(Some((start, end)), Some((start2, end2))) => {
let start = start.min(start2);
let end = end.max(end2);
Some(Self {
source: Arc::clone(&self.source),
range: Some(start..end),
})
}
}
}
pub fn meet(&self, other: Self) -> Option<Self> {
if !self.shares_source(&other) {
return None;
}
match (self.bounds(), other.bounds()) {
(Some((start, end)), Some((start2, end2))) => {
let start = start.max(start2);
let end = end.min(end2);
if start < end {
Some(Self {
source: Arc::clone(&self.source),
range: Some(start..end),
})
} else {
None
}
}
_ => None,
}
}
pub fn join_all<I, S>(spans: I) -> Option<Self>
where
I: IntoIterator<Item = S>,
S: Into<Option<Self>>,
{
let mut spans = spans.into_iter().filter_map(Into::into);
let first = spans.next()?;
spans.try_fold(first, |left, right| left.join(right))
}
}
impl PartialEq for Span {
fn eq(&self, other: &Self) -> bool {
self.shares_source(other) && self.range == other.range
}
}
impl Eq for Span {}
impl Hash for Span {
fn hash<H: Hasher>(&self, state: &mut H) {
Arc::as_ptr(&self.source).hash(state);
self.range.hash(state);
}
}
impl Debug for Span {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let mut span = formatter.debug_struct("Span");
span.field("file", &self.file())
.field("range", &self.line_column_range())
.field("bytes", &self.range);
if let Some(text) = self.text() {
span.field("text", &text);
}
span.finish()
}
}
impl Display for Span {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self.line_column_range() {
Some(range) => Display::fmt(&range, formatter),
None => formatter.write_str("unlocated"),
}
}
}
pub trait Spanned {
fn span(&self) -> Span {
self.try_span()
.expect("syntax must have one shared source snapshot")
}
fn try_span(&self) -> Option<Span> {
Some(self.span())
}
fn expect_span(&self) -> Span {
self.span()
}
}
impl Spanned for Span {
fn span(&self) -> Span {
self.clone()
}
}
impl<T: Spanned + ?Sized> Spanned for &T {
fn span(&self) -> Span {
(*self).span()
}
fn try_span(&self) -> Option<Span> {
(*self).try_span()
}
}
impl<T: Spanned + ?Sized> Spanned for Box<T> {
fn span(&self) -> Span {
self.as_ref().span()
}
fn try_span(&self) -> Option<Span> {
self.as_ref().try_span()
}
}
impl<T: Spanned> Spanned for Option<T> {
fn try_span(&self) -> Option<Span> {
self.as_ref().and_then(Spanned::try_span)
}
}
impl<T: Spanned> Spanned for Vec<T> {
fn try_span(&self) -> Option<Span> {
Span::join_all(self.iter().map(Spanned::try_span))
}
}
impl<A: Spanned, B: Spanned> Spanned for (A, B) {
fn try_span(&self) -> Option<Span> {
self.0.try_span()?.join(self.1.try_span()?)
}
}
impl<T: Spanned> Spanned for &[T] {
fn try_span(&self) -> Option<Span> {
Span::join_all(self.iter().map(Spanned::try_span))
}
}
#[cfg(test)]
mod source_tests {
use std::sync::Arc;
use super::*;
#[test]
fn spans_resolve_text_from_one_shared_source() {
let source = Source::new("quotient ring");
let quotient = Span::ranged(Arc::clone(&source), 0..8);
let ring = Span::ranged(Arc::clone(&source), 9..13);
assert_eq!(quotient.text(), Some("quotient"));
assert_eq!(ring.text(), Some("ring"));
assert!(Arc::ptr_eq(quotient.source(), ring.source()));
}
#[test]
fn joining_different_sources_returns_none() {
let left = Span::for_text("x");
let right = Span::for_text("y");
assert_eq!(left.join(right), None);
}
#[test]
fn an_unlocated_span_still_knows_its_source() {
let source = Source::new("x + y");
let span = Span::unlocated(Arc::clone(&source));
assert_eq!(span.file(), source.file());
assert_eq!(span.range(), None);
assert_eq!(span.text(), None);
}
#[test]
fn source_snapshots_with_the_same_file_do_not_alias() {
let file = File::from_raw(7);
let before = Span::ranged(Source::with_file(file, "x"), 0..1);
let after = Span::ranged(Source::with_file(file, "y"), 0..1);
assert_ne!(before, after);
assert_eq!(before.join(after), None);
}
#[test]
fn shifting_preserves_source_identity() {
let source = Source::new("ring");
let span = Span::ranged(Arc::clone(&source), 0..4);
let shifted = span.shifted(&TextEdit::insertion(0, "graded "));
assert!(Arc::ptr_eq(&source, shifted.source()));
assert_eq!(shifted.range(), Some(7..11));
}
#[test]
fn line_columns_are_one_based_and_count_characters() {
let source = Source::new("aα\nβc");
assert_eq!(
source.line_column(0),
Some(LineColumn { line: 1, column: 1 }),
);
assert_eq!(
source.line_column("aα\n".len()),
Some(LineColumn { line: 2, column: 1 }),
);
assert_eq!(
source.line_column("aα\nβ".len()),
Some(LineColumn { line: 2, column: 2 }),
);
assert_eq!(source.line_column(2), None);
}
#[test]
fn spans_format_as_half_open_line_column_ranges() {
let source = Source::new("one\ntwo");
let range = Span::ranged(Arc::clone(&source), 2..6);
let point = range.at(4);
assert_eq!(range.to_string(), "1:3-2:3");
assert_eq!(point.to_string(), "2:1");
}
}