Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
target
.idea
13 changes: 12 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion crates/dmp/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "diff_match_patch"
version = "0.3.0"
version = "0.3.1"
authors = [
"chandan <chandanmahi1998@gmail.com>",
"ajit <ajit@distill.io>",
Expand Down Expand Up @@ -36,6 +36,12 @@ grapheme = ["dep:unicode-segmentation"]
percent-encoding = "2"
unicode-segmentation = { version = "1", optional = true }

# std::time::Instant::now() panics on wasm32-unknown-unknown ("time not implemented");
# web-time backs it with performance.now() there and re-exports std::time verbatim on
# every other target, so the deadline machinery works identically everywhere.
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-time = "1"

[dev-dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
9 changes: 6 additions & 3 deletions crates/dmp/src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

use crate::engine;
use crate::types::{Diff, DiffToken, Dmp, TDiff};
use std::time::{Duration, Instant};
use std::time::Duration;

#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
use std::time::Instant;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use web_time::Instant;

// The historic public API takes &Vec/&mut Vec/&String; frozen by the
// drop-in compatibility contract.
Expand Down Expand Up @@ -441,8 +446,6 @@ fn compute<T: DiffToken>(
}
}

// Check to see if the problem can be split in two (only when a deadline is
// set: half-match trades optimality for speed).
if deadline.is_some() {
if let Some(hm) = engine::half_match(old, new) {
// A half-match was found, send both pairs off for separate processing.
Expand Down
8 changes: 4 additions & 4 deletions crates/dmp/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
// functions over `&[T: Eq]` — no Dmp state, no text; orchestration and
// text materialization live in diff.rs.

#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
use std::time::Instant;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use web_time::Instant;

// Chunk width for the common-run scans. Runs are scanned token by token
// first — most probes (e.g. the bisect snake walk) mismatch within a couple
Expand Down Expand Up @@ -158,10 +161,7 @@ pub(crate) fn find_sub<T: Eq>(hay: &[T], needle: &[T], from: usize) -> Option<us
// Classic KMP single-steps through every position that cannot
// start a match; hunt the next candidate with the chunked scan
// instead (identical positions skipped, O(n+m) preserved).
match skip_to(hay, i + 1, &needle[0]) {
Some(j) => i = j,
None => return None,
}
i = skip_to(hay, i + 1, &needle[0])?;
} else {
len = kmp.get_or_insert_with(|| Kmp::new(needle)).fail(len);
}
Expand Down
39 changes: 36 additions & 3 deletions crates/dmp/src/tokenize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ impl Dmp {
for diff in diffs.iter_mut() {
let mut text: String = "".to_string();
for ch in diff.text.chars() {
text += line_array[ch as usize].as_str();
text += line_array[id_to_slot(ch as u32)].as_str();
}
diff.text = text;
}
Expand Down Expand Up @@ -243,6 +243,18 @@ fn slot_to_id(slot: usize) -> u32 {
}
}

/// The inverse of [`slot_to_id`]: ids at or past the surrogate gap (U+E000)
/// map back to their slot by subtracting the gap width. Every decoder must go
/// through this — indexing by raw id reads 2048 slots past the real line once
/// a document exceeds 55,295 unique lines.
fn id_to_slot(id: u32) -> usize {
if id >= 57344 {
id as usize - 2048
} else {
id as usize
}
}

/// True when `munge_arena` would pack `text` into a single line token: lines
/// carry their trailing '\n', so that means the only '\n', if any, is the
/// final char. Lets line mode skip the packing for one-line inputs, whose
Expand Down Expand Up @@ -407,12 +419,12 @@ pub(crate) fn chars_tolines_arena(diffs: &mut [TDiff], store: &LineArena) {
for diff in diffs.iter_mut() {
let mut total = 0;
for &ch in &diff.data {
let (s, e) = store.spans[ch as usize];
let (s, e) = store.spans[id_to_slot(ch as u32)];
total += e - s;
}
let mut data: Vec<char> = Vec::with_capacity(total);
for &ch in &diff.data {
let (s, e) = store.spans[ch as usize];
let (s, e) = store.spans[id_to_slot(ch as u32)];
data.extend(store.arena[s..e].chars());
}
diff.data = data;
Expand Down Expand Up @@ -561,6 +573,27 @@ impl GraphemePacker {
mod tests {
use super::*;

#[test]
fn arena_roundtrip_survives_the_surrogate_gap() {
// Line ids skip U+D800..U+DFFF on the encode side (slot 55296 becomes
// scalar 57344), so the decode must subtract the gap back. With 57,500
// unique lines the high ids sit past the gap; a decoder indexing by raw
// id either panics or rehydrates the wrong line.
let mut text1 = String::new();
for i in 0..57_500 {
text1.push_str(&format!("line {i}\n"));
}
let mut text2 = text1.clone();
text2.push_str("one more line\n");
let c1: Vec<char> = text1.chars().collect();
let c2: Vec<char> = text2.chars().collect();
let (p1, _p2, store) = lines_tochars_arena(&c1, &c2);
let mut diffs = vec![TDiff::new(0, p1.chars().collect::<Vec<char>>())];
chars_tolines_arena(&mut diffs, &store);
let rehydrated: String = diffs[0].data.iter().collect();
assert_eq!(rehydrated, text1);
}

#[test]
fn packs_to_one_line_agrees_with_the_arena() {
// The single-line shortcut in line mode is only sound if this
Expand Down
2 changes: 1 addition & 1 deletion crates/dmp/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ impl Dmp {
pub fn new() -> Self {
// it will give a new dmp object.
Dmp {
diff_timeout: None,
diff_timeout: Some(1.0),
patch_delete_threshold: 0.5,
edit_cost: 0,
match_distance: 1000,
Expand Down
31 changes: 21 additions & 10 deletions crates/dmp/tests/golden.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
//! Golden-reference tests: validate the crate against vectors emitted by the vendored
//! googlediff oracle (oracle/vendor/, regenerate with `node oracle/generate.mjs`).
//!
//! Config parity: `Dmp::new()` defaults (diff_timeout None, edit_cost 0, margin 4,
//! maxbits 32, thresholds 0.5, distance 1000) equal the generator's oracle settings,
//! so plain `Dmp::new()` is the correct configuration for every test here.
//! Config parity: the oracle generates with `Diff_Timeout = 0` (no deadline, no
//! half-match) for determinism, while the crate DEFAULT is `Some(1.0)` to match
//! upstream's default. Every test here overrides to `diff_timeout: None` — the
//! faithful mapping of the oracle's `Diff_Timeout = 0` — via [`oracle_dmp`]. All
//! other defaults (edit_cost 0, margin 4, maxbits 32, thresholds 0.5, distance
//! 1000) equal the generator's settings.
//!
//! Known deviations from the oracle are pinned in tests/characterization.rs, not here.

Expand Down Expand Up @@ -48,6 +51,14 @@ fn corpus() -> Vec<Case> {
c.cases
}

/// A `Dmp` configured like the oracle: no deadline, half-match disabled.
fn oracle_dmp() -> Dmp {
Dmp {
diff_timeout: None,
..Dmp::new()
}
}

fn to_tuples(diffs: &[Diff]) -> Vec<(i32, String)> {
diffs
.iter()
Expand All @@ -64,7 +75,7 @@ fn from_tuples(rows: &[(i32, String)]) -> Vec<Diff> {
#[test]
fn golden_diff_main() {
for c in corpus() {
let got = to_tuples(&Dmp::new().diff_main(&c.text1, &c.text2, true));
let got = to_tuples(&oracle_dmp().diff_main(&c.text1, &c.text2, true));
assert_eq!(got, c.diff, "diff mismatch in case '{}'", c.name);
}
}
Expand All @@ -73,7 +84,7 @@ fn golden_diff_main() {
fn golden_diff_cleanup_semantic() {
for c in corpus() {
let mut diffs = from_tuples(&c.diff);
Dmp::new().diff_cleanup_semantic(&mut diffs);
oracle_dmp().diff_cleanup_semantic(&mut diffs);
assert_eq!(
to_tuples(&diffs),
c.diff_semantic,
Expand All @@ -88,7 +99,7 @@ fn golden_delta_encode() {
for c in corpus() {
let mut diffs = from_tuples(&c.diff);
assert_eq!(
Dmp::new().diff_todelta(&mut diffs),
oracle_dmp().diff_todelta(&mut diffs),
c.delta,
"delta mismatch in case '{}'",
c.name
Expand All @@ -99,7 +110,7 @@ fn golden_delta_encode() {
#[test]
fn golden_delta_decode() {
for c in corpus() {
let diffs = Dmp::new().diff_from_delta(&c.text1, &c.delta);
let diffs = oracle_dmp().diff_from_delta(&c.text1, &c.delta);
assert_eq!(
to_tuples(&diffs),
c.diff,
Expand All @@ -112,7 +123,7 @@ fn golden_delta_decode() {
#[test]
fn golden_patch_make() {
for c in corpus() {
let mut d = Dmp::new();
let mut d = oracle_dmp();
let mut patches = d.patch_make1(&c.text1, &c.text2);
assert_eq!(
d.patch_to_text(&mut patches),
Expand All @@ -126,7 +137,7 @@ fn golden_patch_make() {
#[test]
fn golden_patch_text_roundtrip() {
for c in corpus() {
let mut d = Dmp::new();
let mut d = oracle_dmp();
let mut patches = d.patch_from_text(c.patch_text.clone());
assert_eq!(
d.patch_to_text(&mut patches),
Expand All @@ -140,7 +151,7 @@ fn golden_patch_text_roundtrip() {
#[test]
fn golden_patch_apply() {
for c in corpus() {
let mut d = Dmp::new();
let mut d = oracle_dmp();
let mut patches = d.patch_from_text(c.patch_text.clone());
let (applied, results) = d.patch_apply(&mut patches, &c.apply_to);
let applied: String = applied.into_iter().collect();
Expand Down
2 changes: 1 addition & 1 deletion crates/dmp/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ pub fn test_diff_words_tochars() {
let (chars1, chars2, hash_arr) = dmp.diff_words_tochars(&old_string, &new_string);
let mut res_diffs = dmp.diff_main(&chars1, &chars2, true);
dmp.diff_chars_tolines(&mut res_diffs, &hash_arr);
println!("{:?}", &res_diffs);
println!("{:?}", res_diffs);
assert_eq!(diff_arr, res_diffs);
}

Expand Down
Loading
Loading