use super::cancellation::{
collect_canceled_committed, collect_canceled_simple, collect_canceled_staged,
insert_canceled_lines,
};
use super::output::{build_working_line_output, determine_deletion_source};
use super::provenance::{build_modification_map, build_provenance_map};
use super::{DiffLine, FileDiff, LineSource};
#[derive(Debug, Default)]
pub struct DiffInput<'a> {
pub path: &'a str,
pub base: Option<&'a str>,
pub head: Option<&'a str>,
pub index: Option<&'a str>,
pub working: Option<&'a str>,
pub old_path: Option<&'a str>,
}
fn build_deletion_diff(path: &str, content: &str, source: LineSource) -> FileDiff {
let mut lines = vec![DiffLine::deleted_file_header(path)];
for (i, line) in content.lines().enumerate() {
lines.push(
DiffLine::new(source, line.to_string(), '-', Some(i + 1)).with_file_path(path),
);
}
FileDiff::new(lines)
}
fn check_file_deletion(input: &DiffInput<'_>) -> Option<FileDiff> {
if input.working.is_none()
&& let Some(content) = input.index
{
return Some(build_deletion_diff(input.path, content, LineSource::DeletedStaged));
}
if input.index.is_none()
&& input.working.is_none()
&& let Some(content) = input.head
{
return Some(build_deletion_diff(input.path, content, LineSource::DeletedCommitted));
}
if input.head.is_none()
&& input.index.is_none()
&& input.working.is_none()
&& let Some(content) = input.base
{
return Some(build_deletion_diff(input.path, content, LineSource::DeletedBase));
}
None
}
pub fn compute_four_way_diff(input: DiffInput<'_>) -> FileDiff {
if let Some(deletion_diff) = check_file_deletion(&input) {
return deletion_diff;
}
let path = input.path;
let header = match input.old_path {
Some(old) => DiffLine::renamed_file_header(old, path),
None => DiffLine::file_header(path),
};
let mut lines = vec![header];
let base = input.base.unwrap_or("");
let head = input.head.unwrap_or(base);
let index = input.index.unwrap_or(head);
let working = input.working.unwrap_or(index);
let base_lines: Vec<&str> = base.lines().collect();
let head_lines: Vec<&str> = head.lines().collect();
let index_lines: Vec<&str> = index.lines().collect();
let working_lines: Vec<&str> = working.lines().collect();
if base == working {
let head_from_base = build_provenance_map(&base_lines, &head_lines);
let index_from_head = build_provenance_map(&head_lines, &index_lines);
let working_from_index = build_provenance_map(&index_lines, &working_lines);
lines.extend(collect_canceled_simple(
&head_lines,
&index_lines,
&head_from_base,
&index_from_head,
&working_from_index,
path,
));
return FileDiff::new(lines);
}
let head_from_base = build_provenance_map(&base_lines, &head_lines);
let index_from_head = build_provenance_map(&head_lines, &index_lines);
let working_from_index = build_provenance_map(&index_lines, &working_lines);
let base_head_mods = build_modification_map(&base_lines, &head_lines, LineSource::Committed);
let head_index_mods = build_modification_map(&head_lines, &index_lines, LineSource::Staged);
let index_working_mods = build_modification_map(&index_lines, &working_lines, LineSource::Unstaged);
let mut base_to_working: Vec<Option<usize>> = vec![None; base_lines.len()];
for working_idx in 0..working_lines.len() {
if let Some(index_idx) = working_from_index.get(working_idx).copied().flatten()
&& let Some(head_idx) = index_from_head.get(index_idx).copied().flatten()
&& let Some(base_idx) = head_from_base.get(head_idx).copied().flatten()
{
base_to_working[base_idx] = Some(working_idx);
}
}
for (head_idx, (base_idx, _)) in &base_head_mods {
for working_idx in 0..working_lines.len() {
if let Some(index_idx) = working_from_index.get(working_idx).copied().flatten()
&& let Some(h_idx) = index_from_head.get(index_idx).copied().flatten()
&& h_idx == *head_idx
{
base_to_working[*base_idx] = Some(working_idx);
break;
}
}
}
for (index_idx, (head_idx, _)) in &head_index_mods {
if let Some(base_idx) = head_from_base.get(*head_idx).copied().flatten() {
for working_idx in 0..working_lines.len() {
if working_from_index.get(working_idx).copied().flatten() == Some(*index_idx) {
base_to_working[base_idx] = Some(working_idx);
break;
}
}
}
}
for (working_idx, (index_idx, _)) in &index_working_mods {
if let Some(head_idx) = index_from_head.get(*index_idx).copied().flatten()
&& let Some(base_idx) = head_from_base.get(head_idx).copied().flatten()
{
base_to_working[base_idx] = Some(*working_idx);
}
}
let trace_source = |working_idx: usize| -> LineSource {
if let Some(index_idx) = working_from_index.get(working_idx).copied().flatten() {
if let Some(head_idx) = index_from_head.get(index_idx).copied().flatten() {
if head_from_base.get(head_idx).copied().flatten().is_some() {
LineSource::Base
} else {
LineSource::Committed
}
} else {
LineSource::Staged
}
} else {
LineSource::Unstaged
}
};
let trace_index_source = |index_idx: usize| -> LineSource {
if let Some(head_idx) = index_from_head.get(index_idx).copied().flatten() {
if head_from_base.get(head_idx).copied().flatten().is_some() {
LineSource::Base
} else {
LineSource::Committed
}
} else {
LineSource::Staged
}
};
let trace_head_source = |head_idx: usize| -> LineSource {
if head_from_base.get(head_idx).copied().flatten().is_some() {
LineSource::Base
} else {
LineSource::Committed
}
};
let get_working_base_pos = |working_idx: usize| -> Option<usize> {
if let Some(index_idx) = working_from_index.get(working_idx).copied().flatten()
&& let Some(head_idx) = index_from_head.get(index_idx).copied().flatten()
&& let Some(base_idx) = head_from_base.get(head_idx).copied().flatten()
{
return Some(base_idx);
}
if let Some((index_idx, _)) = index_working_mods.get(&working_idx)
&& let Some(head_idx) = index_from_head.get(*index_idx).copied().flatten()
&& let Some(base_idx) = head_from_base.get(head_idx).copied().flatten()
{
return Some(base_idx);
}
None
};
let get_working_head_idx = |working_idx: usize| -> Option<usize> {
if let Some(index_idx) = working_from_index.get(working_idx).copied().flatten()
&& let Some(head_idx) = index_from_head.get(index_idx).copied().flatten()
{
return Some(head_idx);
}
if let Some((index_idx, _)) = index_working_mods.get(&working_idx)
&& let Some(head_idx) = index_from_head.get(*index_idx).copied().flatten()
{
return Some(head_idx);
}
None
};
let mut line_num = 1usize;
let mut next_base_deletion = 0usize;
let mut output_head_positions: Vec<Option<usize>> = Vec::new();
for working_idx in 0..working_lines.len() {
let working_content = working_lines[working_idx].trim_end();
let working_base_pos = get_working_base_pos(working_idx);
let deletion_boundary = if let Some(pos) = working_base_pos {
Some(pos)
} else {
let mut next_base = None;
for future_idx in (working_idx + 1)..working_lines.len() {
if let Some(pos) = get_working_base_pos(future_idx) {
next_base = Some(pos);
break;
}
}
next_base
};
if let Some(boundary) = deletion_boundary {
while next_base_deletion < boundary {
if base_to_working[next_base_deletion].is_none() {
let base_content = base_lines[next_base_deletion].trim_end();
let delete_source = determine_deletion_source(
next_base_deletion,
&base_lines,
&head_lines,
&index_lines,
&head_from_base,
&index_from_head,
);
lines.push(DiffLine::new(
delete_source,
base_content.to_string(),
'-',
None,
).with_file_path(path));
let head_idx_for_deletion = head_from_base.iter()
.position(|&h| h == Some(next_base_deletion));
output_head_positions.push(head_idx_for_deletion);
}
next_base_deletion += 1;
}
}
let source = trace_source(working_idx);
let working_head_idx = get_working_head_idx(working_idx);
output_head_positions.push(working_head_idx);
let output_line = build_working_line_output(
working_idx,
working_content,
source,
line_num,
path,
&working_from_index,
&index_from_head,
&head_from_base,
&index_working_mods,
&base_head_mods,
&head_index_mods,
&index_lines,
&head_lines,
&trace_index_source,
&trace_head_source,
);
lines.push(output_line);
line_num += 1;
if let Some(base_pos) = working_base_pos {
next_base_deletion = next_base_deletion.max(base_pos + 1);
}
}
while next_base_deletion < base_lines.len() {
if base_to_working[next_base_deletion].is_none() {
let base_content = base_lines[next_base_deletion].trim_end();
let delete_source = determine_deletion_source(
next_base_deletion,
&base_lines,
&head_lines,
&index_lines,
&head_from_base,
&index_from_head,
);
lines.push(DiffLine::new(
delete_source,
base_content.to_string(),
'-',
None,
).with_file_path(path));
let head_idx_for_deletion = head_from_base.iter()
.position(|&h| h == Some(next_base_deletion));
output_head_positions.push(head_idx_for_deletion);
}
next_base_deletion += 1;
}
let canceled_committed = collect_canceled_committed(
&head_lines,
&head_from_base,
&index_from_head,
&working_from_index,
&head_index_mods,
&index_working_mods,
);
insert_canceled_lines(
&mut lines,
canceled_committed,
LineSource::CanceledCommitted,
path,
&mut output_head_positions,
);
let canceled_staged = collect_canceled_staged(
&index_lines,
&index_from_head,
&working_from_index,
&index_working_mods,
);
let mut output_index_positions: Vec<Option<usize>> = lines
.iter()
.map(|line| index_lines.iter().position(|h| h.trim_end() == line.content))
.collect();
insert_canceled_lines(
&mut lines,
canceled_staged,
LineSource::CanceledStaged,
path,
&mut output_index_positions,
);
FileDiff::new(lines)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_file_deletion_unstaged() {
let input = DiffInput {
path: "deleted.rs",
base: Some("base content"),
head: Some("head content"),
index: Some("index content\nline 2"),
working: None, old_path: None,
};
let result = check_file_deletion(&input);
assert!(result.is_some(), "Should detect unstaged deletion");
let diff = result.unwrap();
assert_eq!(diff.lines[0].source, LineSource::FileHeader);
assert!(diff.lines[0].content.contains("deleted.rs"));
assert!(diff.lines[0].content.contains("(deleted)"));
assert_eq!(diff.lines[1].source, LineSource::DeletedStaged);
assert_eq!(diff.lines[1].content, "index content");
assert_eq!(diff.lines[1].prefix, '-');
assert_eq!(diff.lines[1].line_number, Some(1));
assert_eq!(diff.lines[2].source, LineSource::DeletedStaged);
assert_eq!(diff.lines[2].content, "line 2");
assert_eq!(diff.lines[2].line_number, Some(2));
}
#[test]
fn test_check_file_deletion_staged() {
let input = DiffInput {
path: "staged_delete.rs",
base: Some("base content"),
head: Some("head content\nhead line 2"),
index: None, working: None, old_path: None,
};
let result = check_file_deletion(&input);
assert!(result.is_some(), "Should detect staged deletion");
let diff = result.unwrap();
assert_eq!(diff.lines[1].source, LineSource::DeletedCommitted);
assert_eq!(diff.lines[1].content, "head content");
}
#[test]
fn test_check_file_deletion_committed() {
let input = DiffInput {
path: "committed_delete.rs",
base: Some("base content\nbase line 2\nbase line 3"),
head: None,
index: None,
working: None,
old_path: None,
};
let result = check_file_deletion(&input);
assert!(result.is_some(), "Should detect committed deletion");
let diff = result.unwrap();
assert_eq!(diff.lines[1].source, LineSource::DeletedBase);
assert_eq!(diff.lines[1].content, "base content");
assert_eq!(diff.lines.len(), 4); }
#[test]
fn test_check_file_deletion_no_deletion() {
let input = DiffInput {
path: "exists.rs",
base: Some("base"),
head: Some("head"),
index: Some("index"),
working: Some("working"),
old_path: None,
};
let result = check_file_deletion(&input);
assert!(result.is_none(), "Should not detect deletion when file exists");
}
#[test]
fn test_check_file_deletion_new_file() {
let input = DiffInput {
path: "new.rs",
base: None,
head: None,
index: None,
working: Some("new content"),
old_path: None,
};
let result = check_file_deletion(&input);
assert!(result.is_none(), "New file should not be detected as deletion");
}
#[test]
fn test_build_deletion_diff_preserves_content() {
let content = "line 1\nline 2\nline 3";
let diff = build_deletion_diff("test.rs", content, LineSource::DeletedBase);
assert_eq!(diff.lines.len(), 4);
assert_eq!(diff.lines[1].content, "line 1");
assert_eq!(diff.lines[2].content, "line 2");
assert_eq!(diff.lines[3].content, "line 3");
}
#[test]
fn test_build_deletion_diff_correct_source() {
let content = "content";
let diff_base = build_deletion_diff("a.rs", content, LineSource::DeletedBase);
assert_eq!(diff_base.lines[1].source, LineSource::DeletedBase);
let diff_committed = build_deletion_diff("b.rs", content, LineSource::DeletedCommitted);
assert_eq!(diff_committed.lines[1].source, LineSource::DeletedCommitted);
let diff_staged = build_deletion_diff("c.rs", content, LineSource::DeletedStaged);
assert_eq!(diff_staged.lines[1].source, LineSource::DeletedStaged);
}
#[test]
fn test_build_deletion_diff_line_numbers() {
let content = "a\nb\nc\nd\ne";
let diff = build_deletion_diff("test.rs", content, LineSource::DeletedBase);
for (i, line) in diff.lines.iter().skip(1).enumerate() {
assert_eq!(line.line_number, Some(i + 1));
}
}
#[test]
fn test_build_deletion_diff_file_path() {
let diff = build_deletion_diff("path/to/file.rs", "content", LineSource::DeletedBase);
for line in diff.lines.iter().skip(1) {
assert_eq!(line.file_path, Some("path/to/file.rs".to_string()));
}
}
#[test]
fn test_build_deletion_diff_empty_file() {
let diff = build_deletion_diff("empty.rs", "", LineSource::DeletedBase);
assert_eq!(diff.lines.len(), 1);
assert_eq!(diff.lines[0].source, LineSource::FileHeader);
}
#[test]
fn test_four_way_diff_base_equals_working() {
let base = "line1\nline2";
let head = "line1\ninserted\nline2"; let index = "line1\nline2";
let diff = compute_four_way_diff(DiffInput {
path: "test.rs",
base: Some(base),
head: Some(head),
index: Some(index),
working: Some(base), old_path: None,
});
let canceled: Vec<_> = diff.lines.iter()
.filter(|l| l.source == LineSource::CanceledCommitted)
.collect();
assert_eq!(canceled.len(), 1);
assert_eq!(canceled[0].content, "inserted");
}
#[test]
fn test_four_way_diff_simple_addition() {
let base = "line1\nline2";
let working = "line1\nline2\nline3";
let diff = compute_four_way_diff(DiffInput {
path: "test.rs",
base: Some(base),
head: Some(base),
index: Some(base),
working: Some(working),
old_path: None,
});
let additions: Vec<_> = diff.lines.iter()
.filter(|l| l.source == LineSource::Unstaged)
.collect();
assert_eq!(additions.len(), 1);
assert_eq!(additions[0].content, "line3");
assert_eq!(additions[0].prefix, '+');
}
#[test]
fn test_four_way_diff_simple_deletion() {
let base = "line1\nline2\nline3";
let working = "line1\nline3";
let diff = compute_four_way_diff(DiffInput {
path: "test.rs",
base: Some(base),
head: Some(base),
index: Some(base),
working: Some(working),
old_path: None,
});
let deletions: Vec<_> = diff.lines.iter()
.filter(|l| l.source.is_deletion())
.collect();
assert_eq!(deletions.len(), 1);
assert_eq!(deletions[0].content, "line2");
assert_eq!(deletions[0].prefix, '-');
}
#[test]
fn test_four_way_diff_committed_change() {
let base = "old_function_name()";
let head = "new_function_name()";
let diff = compute_four_way_diff(DiffInput {
path: "test.rs",
base: Some(base),
head: Some(head),
index: Some(head),
working: Some(head),
old_path: None,
});
let modified_line = diff.lines.iter()
.find(|l| l.content == "new_function_name()")
.expect("Should have the current content");
assert_eq!(modified_line.old_content, Some("old_function_name()".to_string()),
"Should have old content for inline diff");
assert_eq!(modified_line.change_source, Some(LineSource::Committed),
"Should indicate change came from commit");
assert_eq!(modified_line.source, LineSource::Base,
"Source should be Base since line traces back to base");
assert_eq!(modified_line.prefix, ' ',
"Prefix should be space (not deletion marker)");
}
#[test]
fn test_four_way_diff_committed_complete_replacement() {
let base = "func foo() { return 42; }";
let head = "struct Bar { x: i32, y: i32 }";
let diff = compute_four_way_diff(DiffInput {
path: "test.rs",
base: Some(base),
head: Some(head),
index: Some(head),
working: Some(head),
old_path: None,
});
let has_deletion = diff.lines.iter()
.any(|l| l.source.is_deletion() && l.content == "func foo() { return 42; }");
assert!(has_deletion, "Should show deletion when lines are too different");
let has_new = diff.lines.iter()
.any(|l| l.content == "struct Bar { x: i32, y: i32 }");
assert!(has_new, "Should show new content");
}
#[test]
fn test_four_way_diff_staged_change() {
let base = "base";
let index = "staged";
let diff = compute_four_way_diff(DiffInput {
path: "test.rs",
base: Some(base),
head: Some(base),
index: Some(index),
working: Some(index),
old_path: None,
});
let staged: Vec<_> = diff.lines.iter()
.filter(|l| l.source == LineSource::Staged)
.collect();
assert!(!staged.is_empty(), "Should have staged content");
}
#[test]
fn test_four_way_diff_empty_files() {
let diff = compute_four_way_diff(DiffInput {
path: "empty.rs",
base: Some(""),
head: Some(""),
index: Some(""),
working: Some(""),
old_path: None,
});
assert_eq!(diff.lines.len(), 1);
assert_eq!(diff.lines[0].source, LineSource::FileHeader);
}
#[test]
fn test_four_way_diff_identical_nonempty_content() {
let content = "line1\nline2\nline3";
let diff = compute_four_way_diff(DiffInput {
path: "unchanged.rs",
base: Some(content),
head: Some(content),
index: Some(content),
working: Some(content),
old_path: None,
});
assert_eq!(diff.lines.len(), 1);
assert_eq!(diff.lines[0].source, LineSource::FileHeader);
}
#[test]
fn test_four_way_diff_new_file() {
let diff = compute_four_way_diff(DiffInput {
path: "new.rs",
base: None,
head: None,
index: None,
working: Some("new content"),
old_path: None,
});
let content_lines: Vec<_> = diff.lines.iter()
.filter(|l| l.source != LineSource::FileHeader)
.collect();
assert_eq!(content_lines.len(), 1);
assert_eq!(content_lines[0].source, LineSource::Unstaged);
assert_eq!(content_lines[0].content, "new content");
}
#[test]
fn test_four_way_diff_renamed_file() {
let diff = compute_four_way_diff(DiffInput {
path: "new_name.rs",
base: Some("content"),
head: Some("content"),
index: Some("content"),
working: Some("content"),
old_path: Some("old_name.rs"),
});
assert_eq!(diff.lines[0].source, LineSource::FileHeader);
assert!(diff.lines[0].content.contains("old_name.rs"));
assert!(diff.lines[0].content.contains("new_name.rs"));
}
#[test]
fn test_four_way_diff_multiple_changes() {
let base = "a\nb\nc\nd\ne";
let working = "a\nB\nc\nD\ne\nf";
let diff = compute_four_way_diff(DiffInput {
path: "test.rs",
base: Some(base),
head: Some(base),
index: Some(base),
working: Some(working),
old_path: None,
});
let additions: Vec<_> = diff.lines.iter()
.filter(|l| l.source == LineSource::Unstaged && l.prefix == '+')
.collect();
assert!(!additions.is_empty(), "Should have additions");
let has_f = diff.lines.iter().any(|l| l.content == "f");
assert!(has_f, "Should have 'f' as addition");
}
#[test]
fn test_four_way_diff_preserves_line_numbers() {
let working = "line1\nline2\nline3";
let diff = compute_four_way_diff(DiffInput {
path: "test.rs",
base: Some(""),
head: Some(""),
index: Some(""),
working: Some(working),
old_path: None,
});
let content_lines: Vec<_> = diff.lines.iter()
.filter(|l| l.line_number.is_some())
.collect();
for (i, line) in content_lines.iter().enumerate() {
assert_eq!(line.line_number, Some(i + 1));
}
}
#[test]
fn test_four_way_diff_file_path_propagation() {
let diff = compute_four_way_diff(DiffInput {
path: "path/to/file.rs",
base: Some("content"),
head: Some("content"),
index: Some("content"),
working: Some("content"),
old_path: None,
});
for line in &diff.lines {
assert_eq!(line.file_path, Some("path/to/file.rs".to_string()));
}
}
#[test]
fn test_diff_input_default() {
let input = DiffInput::default();
assert_eq!(input.path, "");
assert!(input.base.is_none());
assert!(input.head.is_none());
assert!(input.index.is_none());
assert!(input.working.is_none());
assert!(input.old_path.is_none());
}
fn diff_base_to_working(base: &str, working: &str) -> FileDiff {
compute_four_way_diff(DiffInput {
path: "test.rs",
base: Some(base),
head: Some(working),
index: Some(working),
working: Some(working),
old_path: None,
})
}
fn line_pairs(diff: &FileDiff) -> Vec<(char, &str)> {
diff.lines.iter()
.filter(|l| l.source != LineSource::FileHeader)
.map(|l| (l.prefix, l.content.as_str()))
.collect()
}
#[test]
fn test_deleted_function_has_clean_boundary() {
let base = "\
fn three() {\n println!(\"three\");\n}\n\n\
fn four() {\n println!(\"four\");\n}\n\n\
fn five() {\n println!(\"five\");\n}";
let working = "\
fn three() {\n println!(\"three\");\n}\n\n\
fn five() {\n println!(\"five\");\n}";
let diff = diff_base_to_working(base, working);
let deletions: Vec<&str> = diff.lines.iter()
.filter(|l| l.prefix == '-')
.map(|l| l.content.as_str())
.collect();
assert_eq!(deletions[0], "fn four() {",
"first deleted line should be 'fn four() {{', got: {deletions:?}");
assert!(deletions.contains(&"}"),
"deletion should include the closing '}}', got: {deletions:?}");
}
#[test]
fn test_added_function_has_clean_boundary() {
let base = "\
fn six() {\n println!(\"six\");\n}\n\n\
fn seven() {\n println!(\"seven\");\n}";
let working = "\
fn six() {\n println!(\"six\");\n}\n\n\
fn new_func() {\n println!(\"new\");\n}\n\n\
fn seven() {\n println!(\"seven\");\n}";
let diff = diff_base_to_working(base, working);
let additions: Vec<&str> = diff.lines.iter()
.filter(|l| l.prefix == '+')
.map(|l| l.content.as_str())
.collect();
assert_eq!(additions[0], "fn new_func() {",
"first added line should be 'fn new_func() {{', got: {additions:?}");
assert!(additions.contains(&"}"),
"addition should include the closing '}}', got: {additions:?}");
}
#[test]
fn test_multiple_deleted_functions_each_have_clean_boundaries() {
let base = "\
fn one() {\n println!(\"one\");\n}\n\n\
fn two() {\n println!(\"two\");\n}\n\n\
fn three() {\n println!(\"three\");\n}\n\n\
fn four() {\n println!(\"four\");\n println!(\"more\");\n}\n\n\
fn five() {\n println!(\"five\");\n}";
let working = "\
fn one() {\n println!(\"one\");\n}\n\n\
fn three() {\n println!(\"three\");\n}\n\n\
fn five() {\n println!(\"five\");\n}";
let diff = diff_base_to_working(base, working);
let pairs = line_pairs(&diff);
let mut deletion_runs: Vec<Vec<&str>> = Vec::new();
let mut current_run: Vec<&str> = Vec::new();
for (prefix, content) in &pairs {
if *prefix == '-' {
current_run.push(content);
} else if !current_run.is_empty() {
deletion_runs.push(current_run.clone());
current_run.clear();
}
}
if !current_run.is_empty() {
deletion_runs.push(current_run);
}
assert_eq!(deletion_runs.len(), 2,
"should have 2 deletion runs, got {}: {deletion_runs:?}", deletion_runs.len());
assert_eq!(deletion_runs[0][0], "fn two() {",
"first deletion should start with 'fn two() {{', got: {:?}", deletion_runs[0]);
assert_eq!(deletion_runs[1][0], "fn four() {",
"second deletion should start with 'fn four() {{', got: {:?}", deletion_runs[1]);
}
#[test]
fn test_deletion_with_adjacent_addition_has_clean_boundary() {
let base = "\
fn one() {\n println!(\"one\");\n}\n\n\
fn two() {\n println!(\"two\");\n}\n\n\
fn three() {\n println!(\"three\");\n}";
let working = "\
fn one() {\n println!(\"one\");\n}\n\n\
fn three() {\n println!(\"three\");\n}\n\n\
fn brand_new() {\n println!(\"new\");\n}";
let diff = diff_base_to_working(base, working);
let deletions: Vec<&str> = diff.lines.iter()
.filter(|l| l.prefix == '-')
.map(|l| l.content.as_str())
.collect();
let additions: Vec<&str> = diff.lines.iter()
.filter(|l| l.prefix == '+')
.map(|l| l.content.as_str())
.collect();
assert_eq!(deletions[0], "fn two() {",
"deletion should start with 'fn two() {{', got: {deletions:?}");
let first_nonblank_add = additions.iter()
.find(|l| !l.trim().is_empty())
.expect("should have non-blank additions");
assert_eq!(*first_nonblank_add, "fn brand_new() {",
"first non-blank addition should be 'fn brand_new() {{', got: {additions:?}");
}
}