Compute an edit script transforming `old` into `new` at the token level. Returns a sequence of [`EditOp`]s. The algorithm uses dynamic-programming LCS to align the two sequences and then walks the DP table to emit keep / delete / insert / replace operations.
(old: &[MergeToken], new: &[MergeToken])
| 174 | /// LCS to align the two sequences and then walks the DP table to emit |
| 175 | /// keep / delete / insert / replace operations. |
| 176 | fn diff_tokens(old: &[MergeToken], new: &[MergeToken]) -> Vec<EditOp> { |
| 177 | let n = old.len(); |
| 178 | let m = new.len(); |
| 179 | |
| 180 | if n == 0 && m == 0 { |
| 181 | return Vec::new(); |
| 182 | } |
| 183 | if n == 0 { |
| 184 | // Everything in `new` is an insertion before the start. |
| 185 | return new |
| 186 | .iter() |
| 187 | .map(|t| EditOp::Insert(usize::MAX, t.content.clone())) |
| 188 | .collect(); |
| 189 | } |
| 190 | if m == 0 { |
| 191 | return (0..n).map(EditOp::Delete).collect(); |
| 192 | } |
| 193 | |
| 194 | // DP table for LCS lengths: dp[i][j] = LCS(old[0..i], new[0..j]) |
| 195 | let mut dp = vec![vec![0u32; m + 1]; n + 1]; |
| 196 | for i in 1..=n { |
| 197 | for j in 1..=m { |
| 198 | if old[i - 1] == new[j - 1] { |
| 199 | dp[i][j] = dp[i - 1][j - 1] + 1; |
| 200 | } else { |
| 201 | dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]); |
| 202 | } |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | // Back-track to produce edit operations. |
| 207 | let mut ops = Vec::new(); |
| 208 | let mut i = n; |
| 209 | let mut j = m; |
| 210 | |
| 211 | // We collect in reverse order, then reverse at the end. |
| 212 | while i > 0 || j > 0 { |
| 213 | if i > 0 && j > 0 && old[i - 1] == new[j - 1] { |
| 214 | ops.push(EditOp::Keep(i - 1)); |
| 215 | i -= 1; |
| 216 | j -= 1; |
| 217 | } else if j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j]) { |
| 218 | // Insertion from `new` — record the base index *after which* it sits. |
| 219 | let after = if i > 0 { i - 1 } else { usize::MAX }; |
| 220 | ops.push(EditOp::Insert(after, new[j - 1].content.clone())); |
| 221 | j -= 1; |
| 222 | } else { |
| 223 | // Deletion from `old`. |
| 224 | ops.push(EditOp::Delete(i - 1)); |
| 225 | i -= 1; |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | ops.reverse(); |
| 230 | |
| 231 | // Coalesce adjacent Delete(i) + Insert(i, _) into Replace(i, _). |
| 232 | coalesce_replace(&mut ops); |
| 233 |