#![forbid(unsafe_code)]
use std::ops::Range;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Op {
Same,
Removed,
Added,
Locked,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Segment {
pub op: Op,
pub text: String,
}
pub fn diff_words(before: &str, after: &str) -> Vec<Segment> {
diff_words_locked(before, after, &[], &[])
}
pub fn diff_words_locked(
before: &str,
after: &str,
before_locked: &[Range<usize>],
after_locked: &[Range<usize>],
) -> Vec<Segment> {
let a = tokenize(before, before_locked);
let b = tokenize(after, after_locked);
let a_keys: Vec<&str> = a.iter().map(|t| t.core).collect();
let b_keys: Vec<&str> = b.iter().map(|t| t.core).collect();
let mut edits = Vec::with_capacity(a.len().max(b.len()));
Myers::new(&a_keys, &b_keys).run(&mut edits);
let mut ordered = Vec::with_capacity(edits.len());
let mut added = Vec::new();
for edit in edits {
match edit {
Edit::Added(_) => added.push(edit),
Edit::Removed(_) => ordered.push(edit),
Edit::Same(..) => {
ordered.append(&mut added);
ordered.push(edit);
}
}
}
ordered.append(&mut added);
let mut out: Vec<Segment> = Vec::new();
for edit in ordered {
let (op, text) = match edit {
Edit::Same(i, j) => {
let op = if a[i].locked && b[j].locked {
Op::Locked
} else {
Op::Same
};
(op, b[j].text())
}
Edit::Removed(i) => (Op::Removed, a[i].text()),
Edit::Added(j) => (Op::Added, b[j].text()),
};
match out.last_mut() {
Some(last) if last.op == op && op != Op::Locked => last.text.push_str(text),
_ => out.push(Segment {
op,
text: text.to_owned(),
}),
}
}
out
}
#[derive(Debug)]
struct Token<'a> {
full: &'a str,
core: &'a str,
locked: bool,
}
impl<'a> Token<'a> {
fn text(&self) -> &'a str {
self.full
}
}
fn is_joiner(c: char) -> bool {
matches!(c, '\'' | '’' | '-' | '.' | ',' | '%')
}
fn tokenize<'a>(text: &'a str, locked: &[Range<usize>]) -> Vec<Token<'a>> {
let mut ranges: Vec<Range<usize>> = locked
.iter()
.filter(|r| {
r.start < r.end
&& r.end <= text.len()
&& text.is_char_boundary(r.start)
&& text.is_char_boundary(r.end)
})
.cloned()
.collect();
ranges.sort_by_key(|r| r.start);
let mut kept: Vec<Range<usize>> = Vec::with_capacity(ranges.len());
for r in ranges {
if kept.last().is_none_or(|k| k.end <= r.start) {
kept.push(r);
}
}
let mut locked = kept.into_iter().peekable();
let mut tokens = Vec::new();
let mut i = 0;
let lead = text.len() - text.trim_start().len();
if lead > 0 {
tokens.push(Token {
full: &text[..lead],
core: "",
locked: false,
});
i = lead;
}
while i < text.len() {
let (end, is_locked) = if let Some(r) = locked.next_if(|r| r.start <= i) {
(r.end.max(i + 1).min(text.len()), true)
} else {
let next_lock = locked.peek().map_or(text.len(), |r| r.start);
(word_end(text, i, next_lock), false)
};
let end = end.max(i + text[i..].chars().next().map_or(1, char::len_utf8));
let ws = text[end..].len() - text[end..].trim_start().len();
let ws_end = locked
.peek()
.map_or(end + ws, |r| (end + ws).min(r.start.max(end)));
tokens.push(Token {
full: &text[i..ws_end],
core: &text[i..end],
locked: is_locked,
});
i = ws_end;
}
tokens
}
fn word_end(text: &str, start: usize, limit: usize) -> usize {
let mut chars = text[start..limit].char_indices().peekable();
let Some((_, first)) = chars.next() else {
return start;
};
if !first.is_alphanumeric() {
return start + first.len_utf8();
}
let mut end = start + first.len_utf8();
while let Some((at, c)) = chars.next() {
if c.is_alphanumeric() {
end = start + at + c.len_utf8();
} else if is_joiner(c) && end == start + at {
let followed = chars.peek().is_some_and(|(_, n)| n.is_alphanumeric());
if followed || c == '%' {
end = start + at + c.len_utf8();
} else {
break;
}
} else {
break;
}
}
end
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Edit {
Same(usize, usize),
Removed(usize),
Added(usize),
}
struct Myers<'k> {
a: &'k [&'k str],
b: &'k [&'k str],
forward: Vec<usize>,
backward: Vec<usize>,
}
struct Snake {
x: usize,
y: usize,
u: usize,
v: usize,
}
impl<'k> Myers<'k> {
fn new(a: &'k [&'k str], b: &'k [&'k str]) -> Self {
let size = 2 * (a.len() + b.len()) + 4;
Self {
a,
b,
forward: vec![0; size],
backward: vec![0; size],
}
}
fn run(mut self, out: &mut Vec<Edit>) {
self.compare(0..self.a.len(), 0..self.b.len(), out);
}
fn compare(&mut self, mut a: Range<usize>, mut b: Range<usize>, out: &mut Vec<Edit>) {
while a.start < a.end && b.start < b.end && self.a[a.start] == self.b[b.start] {
out.push(Edit::Same(a.start, b.start));
a.start += 1;
b.start += 1;
}
let mut suffix = 0;
while a.start < a.end - suffix
&& b.start < b.end - suffix
&& self.a[a.end - suffix - 1] == self.b[b.end - suffix - 1]
{
suffix += 1;
}
a.end -= suffix;
b.end -= suffix;
if a.is_empty() {
out.extend(b.clone().map(Edit::Added));
} else if b.is_empty() {
out.extend(a.clone().map(Edit::Removed));
} else {
let snake = self.middle_snake(a.clone(), b.clone());
self.compare(a.start..a.start + snake.x, b.start..b.start + snake.y, out);
for k in 0..snake.u - snake.x {
out.push(Edit::Same(a.start + snake.x + k, b.start + snake.y + k));
}
self.compare(a.start + snake.u..a.end, b.start + snake.v..b.end, out);
}
for k in (1..=suffix).rev() {
out.push(Edit::Same(a.end + suffix - k, b.end + suffix - k));
}
}
#[allow(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::many_single_char_names
)]
fn middle_snake(&mut self, a: Range<usize>, b: Range<usize>) -> Snake {
let n = a.len() as isize;
let m = b.len() as isize;
let delta = n - m;
let odd = delta % 2 != 0;
let max = (n + m + 1) / 2;
let offset = max + 1;
let idx = |k: isize| (k + offset) as usize;
self.forward[idx(1)] = 0;
self.backward[idx(1)] = 0;
let fwd_eq =
|s: &Self, x: isize, y: isize| s.a[a.start + x as usize] == s.b[b.start + y as usize];
let bwd_eq = |s: &Self, x: isize, y: isize| {
s.a[a.end - 1 - x as usize] == s.b[b.end - 1 - y as usize]
};
for d in 0..=max {
let mut k = -d;
while k <= d {
let mut x =
if k == -d || (k != d && self.forward[idx(k - 1)] < self.forward[idx(k + 1)]) {
self.forward[idx(k + 1)] as isize
} else {
self.forward[idx(k - 1)] as isize + 1
};
let mut y = x - k;
let (x0, y0) = (x, y);
while x < n && y < m && fwd_eq(self, x, y) {
x += 1;
y += 1;
}
self.forward[idx(k)] = x as usize;
let c = delta - k;
if odd && c > -d && c < d && x + self.backward[idx(c)] as isize >= n {
return Snake {
x: x0 as usize,
y: y0 as usize,
u: x as usize,
v: y as usize,
};
}
k += 2;
}
let mut k = -d;
while k <= d {
let mut x = if k == -d
|| (k != d && self.backward[idx(k - 1)] < self.backward[idx(k + 1)])
{
self.backward[idx(k + 1)] as isize
} else {
self.backward[idx(k - 1)] as isize + 1
};
let mut y = x - k;
let (x0, y0) = (x, y);
while x < n && y < m && bwd_eq(self, x, y) {
x += 1;
y += 1;
}
self.backward[idx(k)] = x as usize;
let c = delta - k;
if !odd && c >= -d && c <= d && x + self.forward[idx(c)] as isize >= n {
return Snake {
x: (n - x) as usize,
y: (m - y) as usize,
u: (n - x0) as usize,
v: (m - y0) as usize,
};
}
k += 2;
}
}
unreachable!("two non-empty sequences always have a middle snake within (n + m + 1) / 2")
}
}
#[cfg(test)]
mod tests;