pub struct Edit {
pub at: usize,
pub removed: usize,
pub inserted: String,
}
#[must_use]
pub fn taken(before: &str, after: &str) -> Option<Edit> {
if before == after {
return None;
}
let before: Vec<char> = before.chars().collect();
let after: Vec<char> = after.chars().collect();
let prefix = before
.iter()
.zip(&after)
.take_while(|(old, new)| old == new)
.count();
let rest = before.len().min(after.len()) - prefix;
let suffix = before
.iter()
.rev()
.zip(after.iter().rev())
.take_while(|(old, new)| old == new)
.count()
.min(rest);
Some(Edit {
at: prefix,
removed: before.len() - prefix - suffix,
inserted: after
.iter()
.take(after.len() - suffix)
.skip(prefix)
.collect(),
})
}
pub fn spread(
text: &mut String,
edit: &Edit,
before: usize,
primary: usize,
others: &mut [usize],
) -> usize {
let left = before.saturating_sub(edit.at).min(edit.removed);
let right = edit.removed - left;
let width = edit.inserted.chars().count();
let delta = width.cast_signed() - edit.removed.cast_signed();
for caret in others.iter_mut() {
if *caret >= edit.at + edit.removed {
*caret = shifted(*caret, delta);
} else if *caret > edit.at {
*caret = edit.at;
}
}
others.sort_unstable();
let mut characters: Vec<char> = text.chars().collect();
let mut moved = primary;
let mut shift = 0isize;
for caret in others.iter_mut() {
let at = shifted(*caret, shift);
let start = at.saturating_sub(left);
let end = (at + right).min(characters.len());
characters.splice(start..end, edit.inserted.chars());
*caret = start + width;
if start < moved {
moved = shifted(moved, delta);
}
shift += delta;
}
*text = characters.into_iter().collect();
moved
}
fn shifted(index: usize, delta: isize) -> usize {
usize::try_from(index.cast_signed() + delta).unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::{spread, taken};
#[test]
fn a_typed_character_is_the_span_it_added() {
let edit = taken("ab", "axb").expect("the text changed");
assert_eq!(edit.at, 1);
assert_eq!(edit.removed, 0);
assert_eq!(edit.inserted, "x");
}
#[test]
fn a_backspace_is_the_span_it_took() {
let edit = taken("axb", "ab").expect("the text changed");
assert_eq!(edit.at, 1);
assert_eq!(edit.removed, 1);
assert_eq!(edit.inserted, "");
}
#[test]
fn an_unchanged_buffer_is_no_edit() {
assert!(taken("same", "same").is_none());
}
#[test]
fn typing_reaches_every_other_caret() {
let mut text = "aXbb".to_owned();
let edit = taken("abb", "aXbb").expect("the text changed");
let mut others = vec![2, 3];
let primary = spread(&mut text, &edit, 1, 2, &mut others);
assert_eq!(text, "aXbXbX");
assert_eq!(others, vec![4, 6]);
assert_eq!(primary, 2);
}
#[test]
fn a_backspace_reaches_every_other_caret() {
let mut text = "bcc".to_owned();
let edit = taken("abcc", "bcc").expect("the text changed");
let mut others = vec![3, 4];
let primary = spread(&mut text, &edit, 1, 0, &mut others);
assert_eq!(text, "b");
assert_eq!(others, vec![1, 1]);
assert_eq!(primary, 0);
}
#[test]
fn carets_before_the_typing_one_push_it_along() {
let mut text = "aXb".to_owned();
let edit = taken("ab", "aXb").expect("the text changed");
let mut others = vec![0];
let primary = spread(&mut text, &edit, 1, 2, &mut others);
assert_eq!(text, "XaXb");
assert_eq!(others, vec![1]);
assert_eq!(primary, 3);
}
}