use std::hash::{Hash, Hasher};
use std::ops::Index;
use std::sync::Arc;
pub type Offset = usize;
#[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)
}
}
#[derive(Debug, Clone)]
struct SourceSlice {
text: Arc<str>,
origin: Offset,
}
impl PartialEq for SourceSlice {
fn eq(&self, other: &Self) -> bool {
self.origin == other.origin && Arc::ptr_eq(&self.text, &other.text)
}
}
impl Eq for SourceSlice {}
impl Hash for SourceSlice {
fn hash<H: Hasher>(&self, state: &mut H) {
self.origin.hash(state);
std::ptr::hash(Arc::as_ptr(&self.text), state);
}
}
impl SourceSlice {
fn covers(&self, start: Offset, end: Offset) -> bool {
self.origin <= start && end <= self.origin.saturating_add(self.text.len())
}
}
fn common_source(
left: Option<&SourceSlice>,
right: Option<&SourceSlice>,
start: Offset,
end: Offset,
) -> Option<SourceSlice> {
match (left, right) {
(Some(left), Some(right)) if left == right => Some(left.clone()),
(Some(source), None) | (None, Some(source)) if source.covers(start, end) => {
Some(source.clone())
}
_ => None,
}
}
#[derive(Debug, Clone, Default)]
pub struct Span {
bounds: Option<(Offset, Offset)>,
source: Option<SourceSlice>,
}
impl PartialEq for Span {
fn eq(&self, other: &Self) -> bool {
self.bounds == other.bounds && self.source == other.source
}
}
impl Eq for Span {}
impl Hash for Span {
fn hash<H: Hasher>(&self, state: &mut H) {
self.bounds.hash(state);
self.source.hash(state);
}
}
impl Span {
#[allow(non_upper_case_globals)]
pub const Detached: Self = Self {
bounds: None,
source: None,
};
pub fn detached() -> Self {
Self::Detached
}
pub fn new(start: Offset, end: Offset) -> Self {
if start <= end {
Self {
bounds: Some((start, end)),
source: None,
}
} else {
Self::Detached
}
}
pub fn from_offset(offset: Offset) -> Self {
Self {
bounds: Some((offset, offset)),
source: None,
}
}
pub fn with_text(self, text: impl Into<Arc<str>>) -> Self {
if self.source.is_some() {
return self;
}
let text = text.into();
let origin = match self.bounds {
Some((start, end)) => {
assert_eq!(
end - start,
text.len(),
"source fragment width must equal its span width"
);
start
}
None => 0,
};
Self {
source: Some(SourceSlice { text, origin }),
..self
}
}
pub fn replaced_text(&self, text: impl Into<Arc<str>>) -> Self {
let text = text.into();
let bounds = self
.start()
.map(|start| (start, start.saturating_add(text.len())));
let origin = bounds.map_or(0, |(start, _)| start);
Self {
bounds,
source: Some(SourceSlice { text, origin }),
}
}
pub fn source(text: impl Into<Arc<str>>) -> Self {
let text = text.into();
let end = text.len();
Self {
bounds: Some((0, end)),
source: Some(SourceSlice { text, origin: 0 }),
}
}
pub fn subspan(&self, start: Offset, end: Offset) -> Self {
let Some((outer_start, outer_end)) = self.bounds else {
return Self::Detached;
};
let Some(start) = outer_start.checked_add(start) else {
return Self::Detached;
};
let Some(end) = outer_start.checked_add(end) else {
return Self::Detached;
};
if start > end || end > outer_end {
return Self::Detached;
}
Self {
bounds: Some((start, end)),
source: self.source.clone(),
}
}
pub fn text(&self) -> Option<&str> {
let source = self.source.as_ref()?;
match self.bounds {
Some((start, end)) => source
.text
.get(start.checked_sub(source.origin)?..end.checked_sub(source.origin)?),
None => Some(&source.text),
}
}
pub fn shares_source(&self, other: &Self) -> bool {
match (&self.source, &other.source) {
(Some(left), Some(right)) => Arc::ptr_eq(&left.text, &right.text),
_ => false,
}
}
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.bounds
}
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 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::Detached,
Some((start, end)) => Self::new(edit.shift(start), edit.shift(end)),
}
}
pub fn join(&self, other: Self) -> Self {
match (self.bounds(), other.bounds()) {
(None, _) => other,
(_, None) => self.clone(),
(Some((start, end)), Some((start2, end2))) => {
let start = start.min(start2);
let end = end.max(end2);
let bounds = Some((start, end));
let source = common_source(self.source.as_ref(), other.source.as_ref(), start, end);
Self { bounds, source }
}
}
}
pub fn meet(&self, other: Self) -> Self {
match (self.bounds(), other.bounds()) {
(Some((start, end)), Some((start2, end2))) => {
let start = start.max(start2);
let end = end.min(end2);
if start < end {
let source =
common_source(self.source.as_ref(), other.source.as_ref(), start, end);
Self {
bounds: Some((start, end)),
source,
}
} else {
Self::Detached
}
}
_ => Self::Detached,
}
}
pub fn join_all(spans: impl IntoIterator<Item = Span>) -> Self {
spans
.into_iter()
.reduce(|left, right| left.join(right))
.unwrap_or_default()
}
}
impl Index<Span> for str {
type Output = str;
fn index(&self, span: Span) -> &str {
span.bounds()
.and_then(|(start, end)| self.get(start..end))
.unwrap_or_else(|| panic!("{span:?} does not name text in this buffer"))
}
}
impl Index<&Span> for str {
type Output = str;
fn index(&self, span: &Span) -> &str {
span.bounds()
.and_then(|(start, end)| self.get(start..end))
.unwrap_or_else(|| panic!("{span:?} does not name text in this buffer"))
}
}
impl Index<Span> for String {
type Output = str;
fn index(&self, span: Span) -> &str {
self.as_str().index(span)
}
}
impl Index<&Span> for String {
type Output = str;
fn index(&self, span: &Span) -> &str {
self.as_str().index(span)
}
}
pub trait Spanned {
fn 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()
}
}
impl<T: Spanned + ?Sized> Spanned for Box<T> {
fn span(&self) -> Span {
self.as_ref().span()
}
}
impl<T: Spanned> Spanned for Option<T> {
fn span(&self) -> Span {
self.as_ref().map_or_else(Span::detached, Spanned::span)
}
}
impl<T: Spanned> Spanned for Vec<T> {
fn span(&self) -> Span {
Span::join_all(self.iter().map(Spanned::span))
}
}
impl<A: Spanned, B: Spanned> Spanned for (A, B) {
fn span(&self) -> Span {
self.0.span().join(self.1.span())
}
}
impl<T: Spanned> Spanned for &[T] {
fn span(&self) -> Span {
Span::join_all(self.iter().map(Spanned::span))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn span_of(start: Offset, text: &str) -> Span {
Span::new(start, start + text.len())
}
#[test]
fn boundaries_are_gaps_so_adjacent_spans_never_own_the_same_position() {
let a = span_of(0, "a");
let plus = span_of(1, "+");
assert!(!a.contains(1));
assert!(plus.contains(1));
assert!(a.touches(1));
assert!(plus.touches(1));
}
#[test]
fn a_marker_token_contains_nothing_but_touches_its_own_position() {
let eof = Span::from_offset(3);
assert!(eof.is_empty());
assert!(!eof.contains(3));
assert!(eof.touches(3));
}
#[test]
fn an_insertion_moves_exactly_the_offsets_at_or_after_it() {
let insert = TextEdit::insertion(3, "xy");
assert_eq!(insert.shift(2), 2);
assert_eq!(insert.shift(3), 5);
assert_eq!(insert.shift(4), 6);
}
#[test]
fn shifting_never_inverts_a_span() {
let edits = [TextEdit::insertion(3, "xy"), TextEdit::deletion(2, 5)];
for edit in &edits {
for start in 0..8 {
for end in start..8 {
let shifted = Span::new(start, end).shifted(edit);
assert_ne!(shifted, Span::Detached, "{start}..{end} under {edit:?}");
}
}
assert!(Span::from_offset(3).shifted(edit).is_empty());
}
}
#[test]
fn insertion_at_a_span_boundary_moves_it_or_extends_the_span() {
let span = span_of(2, "xy");
assert_eq!(
span.shifted(&TextEdit::insertion(2, "AB")),
span_of(4, "xy")
);
assert_eq!(span.shifted(&TextEdit::insertion(4, "AB")), Span::new(2, 6));
}
#[test]
fn insertion_strictly_inside_a_span_extends_it() {
let span = span_of(0, "abcd");
assert_eq!(
span.shifted(&TextEdit::insertion(2, "efg")),
span_of(0, "abcdefg")
);
}
#[test]
fn text_after_a_deletion_moves_back_and_covered_text_collapses() {
let deleted = TextEdit::deletion(1, 3);
assert_eq!(span_of(4, "z").shifted(&deleted), span_of(2, "z"));
assert_eq!(span_of(0, "a").shifted(&deleted), span_of(0, "a"));
let covered = span_of(1, "xy").shifted(&deleted);
assert_eq!(covered, Span::from_offset(1));
assert!(covered.is_empty());
}
#[test]
fn a_detached_span_is_unaffected_by_edits() {
assert_eq!(
Span::detached().shifted(&TextEdit::insertion(0, "abc")),
Span::detached()
);
}
#[test]
fn shifting_late_offsets_does_not_overflow() {
let span = Span::new(usize::MAX - 4, usize::MAX - 3);
let insert = TextEdit::insertion(1, "ab");
assert_eq!(
span.shifted(&insert),
Span::new(usize::MAX - 2, usize::MAX - 1)
);
}
#[test]
fn text_before_an_edit_never_moves() {
let edits = [TextEdit::insertion(8, "AB"), TextEdit::deletion(8, 10)];
let earlier = [
span_of(5, "ab"),
Span::new(0, 2),
Span::new(6, 7),
Span::from_offset(1),
];
for edit in &edits {
for span in &earlier {
assert_eq!(span.shifted(edit), *span, "{span:?} moved under {edit:?}");
}
}
}
#[test]
fn inserting_at_a_span_start_moves_both_endpoints() {
let span = Span::new(2, 6);
let shifted = span.shifted(&TextEdit::insertion(2, "xyz"));
assert_eq!(shifted, Span::new(5, 9));
assert_eq!(shifted.len(), span.len());
}
#[test]
fn an_inverted_pair_collapses_rather_than_becoming_a_range() {
assert_eq!(Span::new(5, 2), Span::Detached);
}
#[test]
#[should_panic(expected = "deletion start must not exceed its end")]
fn an_inverted_deletion_is_rejected_at_construction() {
TextEdit::deletion(3, 1);
}
#[test]
fn adjacent_spans_do_not_have_an_overlap() {
assert_eq!(Span::new(0, 1).meet(Span::new(1, 2)), Span::Detached);
}
#[test]
fn a_span_indexes_the_buffer_it_came_from() {
let source = "quotient ring";
assert_eq!(&source[span_of(9, "ring")], "ring");
assert_eq!(&source[Span::from_offset(9)], "");
assert_eq!(Span::new(0, source.len()), Span::new(0, 13));
}
#[test]
#[should_panic(expected = "does not name text in this buffer")]
fn indexing_with_a_detached_span_is_a_bug_rather_than_an_empty_slice() {
let _ = &"ring"[Span::detached()];
}
#[test]
fn an_edit_acts_on_buffer_and_span_alike() {
let source = "quotient ring R";
let ring = span_of(9, "ring");
let disjoint = [
TextEdit::insertion(0, "the "),
TextEdit::deletion(0, 9),
TextEdit::insertion(9, "graded "),
TextEdit::insertion(15, " = QQ"),
TextEdit::deletion(13, 15),
TextEdit::insertion(0, "ℤ "),
];
for edit in &disjoint {
let edited = edit.apply(source).expect("edit lands inside the buffer");
assert_eq!(
&edited[ring.shifted(edit)],
&source[&ring],
"{edit:?} broke the span"
);
}
}
#[test]
fn an_edit_inside_a_span_acts_on_the_text_the_span_names() {
let source = "quotient ring";
let ring = span_of(9, "ring");
let edit = TextEdit::insertion(11, "AB");
let edited = edit.apply(source).expect("edit lands inside the buffer");
assert_eq!(&edited[ring.shifted(&edit)], "riABng");
let inside = TextEdit::insertion(11 - ring.start().unwrap(), "AB");
assert_eq!(inside.apply(&source[ring]).unwrap(), "riABng");
}
#[test]
fn a_span_absorbs_text_appended_at_its_closing_boundary() {
let source = "f(x";
let group = Span::new(1, source.len());
assert_eq!(&source[&group], "(x");
let typed = TextEdit::insertion(3, ", y");
let edited = typed.apply(source).expect("edit lands inside the buffer");
assert_eq!(&edited[group.shifted(&typed)], "(x, y");
assert_eq!(group.shifted(&typed), Span::new(1, edited.len()));
}
}