#![cfg_attr(all(target_arch = "wasm32", feature = "bare", not(test)), no_std)]
extern crate alloc;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use core::cmp::min;
#[cfg(all(target_arch = "wasm32", feature = "bare", not(test)))]
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
core::arch::wasm32::unreachable()
}
#[cfg(all(target_arch = "wasm32", feature = "bare", not(test)))]
#[global_allocator]
static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffKind {
Equal,
Add,
Remove,
}
impl DiffKind {
pub fn as_str(self) -> &'static str {
match self {
DiffKind::Equal => "equal",
DiffKind::Add => "add",
DiffKind::Remove => "remove",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffLine {
pub kind: DiffKind,
pub text: String,
pub old_line: Option<u32>,
pub new_line: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiffStats {
pub added: u32,
pub removed: u32,
pub identical: bool,
}
#[derive(Debug, Clone, Copy)]
struct Step {
kind: DiffKind,
a_index: usize,
b_index: usize,
}
pub fn split_lines(text: &str) -> Vec<&str> {
let bytes = text.as_bytes();
let mut lines = Vec::new();
let mut start = 0;
let mut i = 0;
while i < bytes.len() {
let end = i;
match bytes[i] {
b'\n' => i += 1,
b'\r' if bytes.get(i + 1) == Some(&b'\n') => i += 2,
b'\r' => i += 1,
_ => {
i += 1;
continue;
}
}
lines.push(text.get(start..end).unwrap_or(""));
start = i;
}
if start < bytes.len() {
lines.push(text.get(start..).unwrap_or(""));
}
lines
}
pub fn diff_lines(before: &str, after: &str) -> Vec<DiffLine> {
let a = split_lines(before);
let b = split_lines(after);
let steps = myers(&a, &b);
let mut out = Vec::with_capacity(steps.len());
let mut old_no: u32 = 1;
let mut new_no: u32 = 1;
for step in steps {
let (old_line, new_line) = match step.kind {
DiffKind::Equal => {
let pair = (Some(old_no), Some(new_no));
old_no += 1;
new_no += 1;
pair
}
DiffKind::Remove => {
let pair = (Some(old_no), None);
old_no += 1;
pair
}
DiffKind::Add => {
let pair = (None, Some(new_no));
new_no += 1;
pair
}
};
out.push(DiffLine {
kind: step.kind,
text: step_text(&a, &b, &step).to_string(),
old_line,
new_line,
});
}
out
}
fn step_text<'a>(a: &[&'a str], b: &[&'a str], step: &Step) -> &'a str {
match step.kind {
DiffKind::Equal | DiffKind::Remove => a[step.a_index],
DiffKind::Add => b[step.b_index],
}
}
pub fn diff_stats(lines: &[DiffLine]) -> DiffStats {
let mut added = 0;
let mut removed = 0;
for line in lines {
match line.kind {
DiffKind::Add => added += 1,
DiffKind::Remove => removed += 1,
DiffKind::Equal => {}
}
}
DiffStats {
added,
removed,
identical: added == 0 && removed == 0,
}
}
fn myers(a: &[&str], b: &[&str]) -> Vec<Step> {
let n = a.len();
let m = b.len();
if n == 0 {
return (0..m)
.map(|j| Step { kind: DiffKind::Add, a_index: 0, b_index: j })
.collect();
}
if m == 0 {
return (0..n)
.map(|i| Step { kind: DiffKind::Remove, a_index: i, b_index: 0 })
.collect();
}
let max = n + m;
let offset = max as i32;
let mut v: Vec<i32> = vec![0i32; 2 * max + 1];
let mut trace: Vec<Vec<i32>> = Vec::with_capacity(min(max, 4096));
for d in 0..=max as i32 {
trace.push(v.clone());
let mut k = -d;
while k <= d {
let idx = (k + offset) as usize;
let down = (k + 1 + offset) as usize;
let right = (k - 1 + offset) as usize;
let mut x = if k == -d || (k != d && v[right] < v[down]) {
v[down]
} else {
v[right] + 1
};
let mut y = x - k;
while (x as usize) < n && (y as usize) < m && a[x as usize] == b[y as usize] {
x += 1;
y += 1;
}
v[idx] = x;
if x as usize >= n && y as usize >= m {
return backtrack(&trace, d, offset, x, y);
}
k += 2;
}
}
Vec::new()
}
fn backtrack(trace: &[Vec<i32>], d: i32, offset: i32, end_x: i32, end_y: i32) -> Vec<Step> {
let mut steps = Vec::new();
let mut x = end_x;
let mut y = end_y;
for step in (1..=d).rev() {
let v = &trace[step as usize];
let k = x - y;
let prev_k = if k == -step
|| (k != step && v[(k - 1 + offset) as usize] < v[(k + 1 + offset) as usize])
{
k + 1
} else {
k - 1
};
let prev_x = v[(prev_k + offset) as usize];
let prev_y = prev_x - prev_k;
while x > prev_x && y > prev_y {
steps.push(Step {
kind: DiffKind::Equal,
a_index: (x - 1) as usize,
b_index: (y - 1) as usize,
});
x -= 1;
y -= 1;
}
if x == prev_x {
steps.push(Step { kind: DiffKind::Add, a_index: x as usize, b_index: (y - 1) as usize });
y -= 1;
} else {
steps.push(Step {
kind: DiffKind::Remove,
a_index: (x - 1) as usize,
b_index: y as usize,
});
x -= 1;
}
}
while x > 0 && y > 0 {
steps.push(Step {
kind: DiffKind::Equal,
a_index: (x - 1) as usize,
b_index: (y - 1) as usize,
});
x -= 1;
y -= 1;
}
steps.reverse();
steps
}
#[cfg(feature = "bindgen")]
use wasm_bindgen::prelude::*;
const KIND_EQUAL: i32 = 0;
const KIND_ADD: i32 = 1;
const KIND_REMOVE: i32 = 2;
#[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = lineCount))]
pub fn js_line_count(text: &str) -> usize {
split_lines(text).len()
}
#[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = diffKinds))]
pub fn js_diff_kinds(before: &str, after: &str) -> Vec<i32> {
diff_lines(before, after)
.iter()
.map(|l| match l.kind {
DiffKind::Equal => KIND_EQUAL,
DiffKind::Add => KIND_ADD,
DiffKind::Remove => KIND_REMOVE,
})
.collect()
}
#[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = diffOldLines))]
pub fn js_diff_old_lines(before: &str, after: &str) -> Vec<i32> {
diff_lines(before, after)
.iter()
.map(|l| l.old_line.map_or(-1, |n| n as i32))
.collect()
}
#[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = diffNewLines))]
pub fn js_diff_new_lines(before: &str, after: &str) -> Vec<i32> {
diff_lines(before, after)
.iter()
.map(|l| l.new_line.map_or(-1, |n| n as i32))
.collect()
}
#[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = diffTextAt))]
pub fn js_diff_text_at(before: &str, after: &str, index: usize) -> String {
diff_lines(before, after)
.get(index)
.map(|l| l.text.clone())
.unwrap_or_default()
}
#[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = diffStats))]
pub fn js_diff_stats(before: &str, after: &str) -> Vec<i32> {
let stats = diff_stats(&diff_lines(before, after));
vec![stats.added as i32, stats.removed as i32]
}
#[cfg_attr(feature = "bindgen", wasm_bindgen(js_name = diffIdentical))]
pub fn js_diff_identical(before: &str, after: &str) -> bool {
diff_stats(&diff_lines(before, after)).identical
}
#[cfg(test)]
mod tests {
use super::*;
fn reconstruct(lines: &[DiffLine]) -> (String, String) {
let before = lines
.iter()
.filter(|l| l.kind != DiffKind::Add)
.map(|l| l.text.as_str())
.collect::<Vec<_>>()
.join("\n");
let after = lines
.iter()
.filter(|l| l.kind != DiffKind::Remove)
.map(|l| l.text.as_str())
.collect::<Vec<_>>()
.join("\n");
(before, after)
}
fn cases() -> Vec<(&'static str, &'static str, &'static str)> {
vec![
("single change", "a\nb\nc", "a\nx\nc"),
("from empty", "", "new"),
("to empty", "gone", ""),
("both empty", "", ""),
("identical", "same", "same"),
("middle edit", "a\nb\nc\nd\ne\nf\ng\nh", "a\nb\nc\nX\nY\ne\nf\ng\nh"),
("reversed", "1\n2\n3\n4\n5", "5\n4\n3\n2\n1"),
("blank lines removed", "a\n\n\nb", "a\nb"),
("duplicated", "x", "x\nx\nx"),
("full replace", "l1\nl2", "totally\ndifferent\ncontent\nhere"),
]
}
#[test]
fn reconstructs_both_sides() {
for (name, before, after) in cases() {
let lines = diff_lines(before, after);
let (rebuilt_before, rebuilt_after) = reconstruct(&lines);
assert_eq!(rebuilt_before, split_lines(before).join("\n"), "{name}: before");
assert_eq!(rebuilt_after, split_lines(after).join("\n"), "{name}: after");
}
}
#[test]
fn empty_input_is_not_one_empty_line() {
assert!(split_lines("").is_empty());
let stats = diff_stats(&diff_lines("", "new"));
assert_eq!(stats.added, 1);
assert_eq!(stats.removed, 0);
}
#[test]
fn trailing_newline_is_a_terminator() {
assert_eq!(split_lines("a\n").len(), 1);
}
#[test]
fn every_terminator_ends_one_line() {
assert_eq!(split_lines("a\r\nb\rc\nd"), ["a", "b", "c", "d"]);
assert_eq!(split_lines("a\r\r\nb"), ["a", "", "b"]);
assert_eq!(split_lines("\n"), [""]);
assert_eq!(split_lines("a\n\n"), ["a", ""]);
assert_eq!(split_lines("é\r😀"), ["é", "😀"]);
}
#[test]
fn normalizes_crlf() {
let lines = diff_lines("a\r\nb", "a\r\nc");
assert!(!lines.iter().any(|l| l.text.contains('\r')));
assert_eq!(
lines.iter().map(|l| l.text.as_str()).collect::<Vec<_>>().join(","),
"a,b,c"
);
}
#[test]
fn reports_accurate_counts() {
let stats = diff_stats(&diff_lines("a\nb\nc", "a\nc\nd\ne"));
assert_eq!(stats.added, 2);
assert_eq!(stats.removed, 1);
assert!(!stats.identical);
assert!(diff_stats(&diff_lines("a", "a")).identical);
}
#[test]
fn line_numbers_are_one_based_and_absent_on_the_missing_side() {
let lines = diff_lines("a", "a\nb");
let added = lines.iter().find(|l| l.kind == DiffKind::Add).unwrap();
assert_eq!(added.old_line, None, "never invent a number for a line that was never there");
assert_eq!(added.new_line, Some(2));
}
#[test]
fn handles_a_large_file_with_a_small_edit() {
let big: String = (0..20_000).map(|i| format!("line {i}\n")).collect();
let edited = big.replacen("line 10000", "line 10000 CHANGED", 1);
let stats = diff_stats(&diff_lines(&big, &edited));
assert_eq!(stats.added, 1);
assert_eq!(stats.removed, 1);
}
}