Skip to content
Open
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
27 changes: 19 additions & 8 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,25 @@ fn process_replace(old_range: &[Block], new_range: &[Block], results: &mut Vec<D
}

// 3. Emit results in document order.
// First: unmatched old blocks as Deleted.
for (oi, old_b) in old_range.iter().enumerate() {
if matched_old[oi].is_none() {
results.push(DiffResult::Deleted(old_b.clone()));
}
}

// Then: new blocks in order — Modified if matched, Added if unmatched.
// An unmatched old block goes out as Deleted just before the first new
// block that follows it: the next one matched to a later old block, or an
// added block in between.
let deleted_between = |from: usize, to: usize| {
(from..to)
.filter(|&oi| matched_old[oi].is_none())
.map(|oi| DiffResult::Deleted(old_range[oi].clone()))
};
let mut next_old = 0;

// New blocks in order — Modified if matched, Added if unmatched.
for (ni, new_b) in new_range.iter().enumerate() {
let next_match = matched_new[ni..]
.iter()
.find_map(|m| *m)
.unwrap_or(old_range.len());
results.extend(deleted_between(next_old, next_match));
next_old = next_old.max(next_match);

if let Some(oi) = matched_new[ni] {
let old_b = &old_range[oi];
let old_text = old_b.diff_text().unwrap();
Expand All @@ -130,6 +140,7 @@ fn process_replace(old_range: &[Block], new_range: &[Block], results: &mut Vec<D
results.push(DiffResult::Added(new_b.clone()));
}
}
results.extend(deleted_between(next_old, old_range.len()));
}

/// Tokenize text for diffing: ASCII alphanumeric runs form word tokens,
Expand Down
14 changes: 14 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,17 @@ fn test_indented_line_comment_does_not_split_paragraph() {
// A whitespace-only line would be treated as a paragraph break by Typst.
assert!(!output.contains("\n \n"));
}

#[test]
fn test_deleted_equation_stays_after_preceding_paragraph() {
let old = "The model is defined below.\n\n$ x = 1 $\n";
let new = "The model is defined here.\n";
let output = run_diff(old, new);

let paragraph = output.find("#diff-added[here]").unwrap();
let equation = output.find("#diff-deleted[$ x = 1 $]").unwrap();
assert!(
paragraph < equation,
"deleted equation moved above the paragraph before it: {output}"
);
}