use crate::myers::{DiffChunk, DiffTag};
#[must_use]
pub fn merge_change_indices(
left_chunks: &[DiffChunk],
right_chunks: &[DiffChunk],
) -> Vec<(usize, bool)> {
let mut indices: Vec<(usize, bool, usize)> = Vec::new();
for (i, c) in left_chunks.iter().enumerate() {
if c.tag != DiffTag::Equal {
indices.push((i, false, c.start_b));
}
}
for (i, c) in right_chunks.iter().enumerate() {
if c.tag != DiffTag::Equal {
indices.push((i, true, c.start_a));
}
}
indices.sort_by_key(|&(_, _, line)| line);
indices
.into_iter()
.map(|(i, is_right, _)| (i, is_right))
.collect()
}
#[must_use]
pub fn find_conflict_markers_in_text(text: &str) -> Vec<usize> {
text.lines()
.enumerate()
.filter_map(|(i, line)| {
if line.starts_with("<<<<<<<") {
Some(i)
} else {
None
}
})
.collect()
}
#[must_use]
pub fn find_conflict_blocks(text: &str) -> Vec<(usize, usize)> {
let mut blocks = Vec::new();
let mut open: Option<usize> = None;
for (i, line) in text.lines().enumerate() {
if line.starts_with("<<<<<<<") {
open = Some(i);
} else if line.starts_with(">>>>>>>")
&& let Some(start) = open.take()
{
blocks.push((start, i));
}
}
blocks
}
#[must_use]
pub fn conflict_at_cursor_fast(blocks: &[(usize, usize)], cursor_line: usize) -> Option<usize> {
let idx = blocks.partition_point(|&(open, _)| open <= cursor_line);
if idx == 0 {
return None;
}
let (open, close) = blocks[idx - 1];
if cursor_line <= close {
Some(open)
} else {
None
}
}
#[must_use]
pub fn conflict_at_cursor(text: &str, cursor_line: usize) -> Option<usize> {
let blocks = find_conflict_blocks(text);
conflict_at_cursor_fast(&blocks, cursor_line)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn change_indices_empty() {
let result = merge_change_indices(&[], &[]);
assert!(result.is_empty());
}
#[test]
fn change_indices_only_equal() {
let left = vec![DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 5,
start_b: 0,
end_b: 5,
}];
let right = left.clone();
let result = merge_change_indices(&left, &right);
assert!(result.is_empty());
}
#[test]
fn change_indices_left_only() {
let left = vec![
DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 2,
start_b: 0,
end_b: 2,
},
DiffChunk {
tag: DiffTag::Replace,
start_a: 2,
end_a: 4,
start_b: 2,
end_b: 4,
},
];
let result = merge_change_indices(&left, &[]);
assert_eq!(result, vec![(1, false)]);
}
#[test]
fn change_indices_right_only() {
let right = vec![
DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 3,
start_b: 0,
end_b: 3,
},
DiffChunk {
tag: DiffTag::Delete,
start_a: 3,
end_a: 5,
start_b: 3,
end_b: 3,
},
];
let result = merge_change_indices(&[], &right);
assert_eq!(result, vec![(1, true)]);
}
#[test]
fn change_indices_sorted_by_middle_line() {
let left = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 0,
end_a: 2,
start_b: 10, end_b: 12,
}];
let right = vec![DiffChunk {
tag: DiffTag::Insert,
start_a: 5, end_a: 5,
start_b: 0,
end_b: 2,
}];
let result = merge_change_indices(&left, &right);
assert_eq!(result, vec![(0, true), (0, false)]);
}
#[test]
fn change_indices_mixed_tags() {
let left = vec![
DiffChunk {
tag: DiffTag::Insert,
start_a: 0,
end_a: 0,
start_b: 0, end_b: 2,
},
DiffChunk {
tag: DiffTag::Equal,
start_a: 0,
end_a: 5,
start_b: 2,
end_b: 7,
},
DiffChunk {
tag: DiffTag::Delete,
start_a: 5,
end_a: 8,
start_b: 7, end_b: 7,
},
];
let right = vec![DiffChunk {
tag: DiffTag::Replace,
start_a: 3, end_a: 5,
start_b: 3,
end_b: 6,
}];
let result = merge_change_indices(&left, &right);
assert_eq!(result, vec![(0, false), (0, true), (2, false)]);
}
#[test]
fn conflict_markers_empty_text() {
let result = find_conflict_markers_in_text("");
assert!(result.is_empty());
}
#[test]
fn conflict_markers_none_present() {
let text = "line one\nline two\nline three\n";
let result = find_conflict_markers_in_text(text);
assert!(result.is_empty());
}
#[test]
fn conflict_markers_single() {
let text =
"before\n<<<<<<< HEAD\nconflict content\n=======\nother side\n>>>>>>> branch\nafter\n";
let result = find_conflict_markers_in_text(text);
assert_eq!(result, vec![1]);
}
#[test]
fn conflict_markers_multiple() {
let text = "<<<<<<< HEAD\nfoo\n=======\nbar\n>>>>>>> b\n<<<<<<< HEAD\nbaz\n=======\nqux\n>>>>>>> b\n";
let result = find_conflict_markers_in_text(text);
assert_eq!(result, vec![0, 5]);
}
#[test]
fn conflict_markers_not_at_line_start() {
let text = "some text <<<<<<< HEAD\nanother line\n";
let result = find_conflict_markers_in_text(text);
assert!(result.is_empty());
}
#[test]
fn conflict_markers_bare_marker() {
let text = "hello\n<<<<<<<\nworld\n";
let result = find_conflict_markers_in_text(text);
assert_eq!(result, vec![1]);
}
#[test]
fn conflict_at_cursor_outside() {
let text = "before\n<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> b\nafter\n";
assert_eq!(conflict_at_cursor(text, 0), None); assert_eq!(conflict_at_cursor(text, 6), None); }
#[test]
fn conflict_at_cursor_on_open_marker() {
let text = "before\n<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> b\nafter\n";
assert_eq!(conflict_at_cursor(text, 1), Some(1));
}
#[test]
fn conflict_at_cursor_inside() {
let text = "before\n<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> b\nafter\n";
assert_eq!(conflict_at_cursor(text, 2), Some(1)); assert_eq!(conflict_at_cursor(text, 3), Some(1)); assert_eq!(conflict_at_cursor(text, 4), Some(1)); }
#[test]
fn conflict_at_cursor_on_close_marker() {
let text = "before\n<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> b\nafter\n";
assert_eq!(conflict_at_cursor(text, 5), Some(1)); }
#[test]
fn conflict_at_cursor_multiple_blocks() {
let text = "<<<<<<< HEAD\na\n>>>>>>> b\nmiddle\n<<<<<<< HEAD\nb\n>>>>>>> b\n";
assert_eq!(conflict_at_cursor(text, 0), Some(0)); assert_eq!(conflict_at_cursor(text, 1), Some(0)); assert_eq!(conflict_at_cursor(text, 3), None); assert_eq!(conflict_at_cursor(text, 4), Some(4)); assert_eq!(conflict_at_cursor(text, 5), Some(4)); }
#[test]
fn conflict_at_cursor_past_end() {
let text = "hello\nworld\n";
assert_eq!(conflict_at_cursor(text, 99), None);
}
#[test]
fn conflict_at_cursor_empty() {
assert_eq!(conflict_at_cursor("", 0), None);
}
#[test]
fn conflict_blocks_unmatched_open() {
let text = "before\n<<<<<<< HEAD\nours\n=======\ntheirs\n";
let blocks = find_conflict_blocks(text);
assert!(blocks.is_empty());
}
#[test]
fn conflict_blocks_unmatched_close() {
let text = "before\nsome text\n>>>>>>> branch\nafter\n";
let blocks = find_conflict_blocks(text);
assert!(blocks.is_empty());
}
#[test]
fn conflict_blocks_adjacent() {
let text = "<<<<<<< a\nours\n>>>>>>> b\n<<<<<<< c\ntheirs\n>>>>>>> d\n";
let blocks = find_conflict_blocks(text);
assert_eq!(blocks, vec![(0, 2), (3, 5)]);
}
#[test]
fn conflict_blocks_nested_open_takes_last() {
let text = "<<<<<<< first\nstuff\n<<<<<<< second\nmore\n>>>>>>> end\n";
let blocks = find_conflict_blocks(text);
assert_eq!(blocks, vec![(2, 4)]);
}
#[test]
fn conflict_at_cursor_fast_empty_blocks() {
assert_eq!(conflict_at_cursor_fast(&[], 0), None);
assert_eq!(conflict_at_cursor_fast(&[], 100), None);
}
#[test]
fn conflict_at_cursor_fast_exactly_on_boundaries() {
let blocks = vec![(5, 10), (20, 25)];
assert_eq!(conflict_at_cursor_fast(&blocks, 4), None);
assert_eq!(conflict_at_cursor_fast(&blocks, 5), Some(5)); assert_eq!(conflict_at_cursor_fast(&blocks, 10), Some(5)); assert_eq!(conflict_at_cursor_fast(&blocks, 11), None); assert_eq!(conflict_at_cursor_fast(&blocks, 20), Some(20));
assert_eq!(conflict_at_cursor_fast(&blocks, 25), Some(20));
assert_eq!(conflict_at_cursor_fast(&blocks, 26), None);
}
#[test]
fn conflict_at_cursor_fast_between_blocks() {
let blocks = vec![(0, 3), (10, 15)];
assert_eq!(conflict_at_cursor_fast(&blocks, 5), None);
assert_eq!(conflict_at_cursor_fast(&blocks, 9), None);
}
}