use super::span::Span;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextEdit {
pub span: Span,
pub replacement: String,
}
impl TextEdit {
pub fn replace(span: Span, replacement: impl Into<String>) -> Self {
Self {
span,
replacement: replacement.into(),
}
}
pub fn insert(offset: usize, text: impl Into<String>) -> Self {
Self::replace(Span::new(offset, offset), text)
}
pub fn delete(span: Span) -> Self {
Self::replace(span, "")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditError {
Overlap { first: Span, second: Span },
OutOfBounds(Span),
NotCharBoundary(usize),
}
impl fmt::Display for EditError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EditError::Overlap { first, second } => {
write!(f, "overlapping edits at {first} and {second}")
}
EditError::OutOfBounds(span) => {
write!(f, "edit at {span} is out of bounds")
}
EditError::NotCharBoundary(offset) => {
write!(f, "edit boundary at byte {offset} splits a character")
}
}
}
}
impl std::error::Error for EditError {}
pub fn apply_edits(src: &str, edits: &[TextEdit]) -> Result<String, EditError> {
let mut order: Vec<usize> = (0..edits.len()).collect();
order.sort_by_key(|&i| (edits[i].span.start, edits[i].span.end));
for &i in &order {
let span = edits[i].span;
if span.end > src.len() {
return Err(EditError::OutOfBounds(span));
}
for offset in [span.start, span.end] {
if !src.is_char_boundary(offset) {
return Err(EditError::NotCharBoundary(offset));
}
}
}
for pair in order.windows(2) {
let (a, b) = (edits[pair[0]].span, edits[pair[1]].span);
if a.end > b.start {
return Err(EditError::Overlap {
first: a,
second: b,
});
}
}
let extra: usize = edits.iter().map(|e| e.replacement.len()).sum();
let mut out = String::with_capacity(src.len() + extra);
let mut cursor = 0;
for &i in &order {
let edit = &edits[i];
out.push_str(&src[cursor..edit.span.start]);
out.push_str(&edit.replacement);
cursor = edit.span.end;
}
out.push_str(&src[cursor..]);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replace_insert_delete() {
let src = "Get-WmiObject Win32_BIOS # legacy";
let edits = [
TextEdit::replace(Span::new(0, 13), "Get-CimInstance"),
TextEdit::insert(src.len(), "\n"),
];
assert_eq!(
apply_edits(src, &edits).unwrap(),
"Get-CimInstance Win32_BIOS # legacy\n"
);
let edits = [TextEdit::delete(Span::new(3, 7))];
assert_eq!(apply_edits("abcdefgh", &edits).unwrap(), "abch");
}
#[test]
fn edits_apply_regardless_of_given_order() {
let src = "a b c";
let edits = [
TextEdit::replace(Span::new(4, 5), "C"),
TextEdit::replace(Span::new(0, 1), "A"),
];
assert_eq!(apply_edits(src, &edits).unwrap(), "A b C");
}
#[test]
fn touching_spans_are_fine_overlap_is_not() {
let src = "abcdef";
let touching = [
TextEdit::replace(Span::new(0, 3), "X"),
TextEdit::replace(Span::new(3, 6), "Y"),
];
assert_eq!(apply_edits(src, &touching).unwrap(), "XY");
let overlapping = [
TextEdit::replace(Span::new(0, 4), "X"),
TextEdit::replace(Span::new(3, 6), "Y"),
];
assert!(matches!(
apply_edits(src, &overlapping),
Err(EditError::Overlap { .. })
));
}
#[test]
fn bounds_and_char_boundaries_are_checked() {
assert!(matches!(
apply_edits("ab", &[TextEdit::delete(Span::new(1, 5))]),
Err(EditError::OutOfBounds(_))
));
assert!(matches!(
apply_edits("ż", &[TextEdit::insert(1, "x")]),
Err(EditError::NotCharBoundary(1))
));
}
#[test]
fn insertion_at_a_replacement_boundary_is_order_independent() {
let src = "0123456789";
let want = "PREX";
let insert_first = [
TextEdit::insert(0, "PRE"),
TextEdit::replace(Span::new(0, 10), "X"),
];
let replace_first = [
TextEdit::replace(Span::new(0, 10), "X"),
TextEdit::insert(0, "PRE"),
];
assert_eq!(apply_edits(src, &insert_first).unwrap(), want);
assert_eq!(apply_edits(src, &replace_first).unwrap(), want);
let inside = [
TextEdit::replace(Span::new(0, 10), "X"),
TextEdit::insert(5, "Y"),
];
assert!(matches!(
apply_edits(src, &inside),
Err(EditError::Overlap { .. })
));
}
#[test]
fn empty_edit_list_is_identity() {
assert_eq!(apply_edits("unchanged", &[]).unwrap(), "unchanged");
}
}