use crate::app::{FileMerge, SideState};
use crate::merge::{ChunkKind, MergeChunk};
pub(crate) const FOLD_THRESHOLD: usize = 8;
pub(crate) const FOLD_KEEP: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ChangeType {
None,
Modified,
Added,
Deleted,
Conflict,
}
fn change_type(chunk: &MergeChunk) -> ChangeType {
let of = |side: &[String]| {
if chunk.base.is_empty() {
ChangeType::Added
} else if side.is_empty() {
ChangeType::Deleted
} else {
ChangeType::Modified
}
};
match chunk.kind {
ChunkKind::Stable => ChangeType::None,
ChunkKind::Conflict => ChangeType::Conflict,
ChunkKind::Ours => of(&chunk.ours),
ChunkKind::Theirs => of(&chunk.theirs),
ChunkKind::Agree => of(&chunk.ours),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Cell {
Line {
no: usize,
offset: usize,
text: String,
},
Empty,
Placeholder,
}
pub(crate) struct Row {
pub(crate) chunk: usize,
pub(crate) change: ChangeType,
pub(crate) resolved: bool,
pub(crate) current: bool,
pub(crate) fold: Option<usize>,
pub(crate) ours_state: Option<SideState>,
pub(crate) theirs_state: Option<SideState>,
pub(crate) ours: Cell,
pub(crate) result: Cell,
pub(crate) theirs: Cell,
}
pub(crate) fn build_rows(merge: &FileMerge, folded: bool) -> (Vec<Row>, Vec<usize>) {
let mut rows: Vec<Row> = Vec::new();
let mut chunk_starts: Vec<usize> = Vec::new();
let mut result_no = 1usize;
for (idx, chunk) in merge.chunks.iter().enumerate() {
chunk_starts.push(rows.len());
let resolved = merge.chunk_resolved(idx);
let current = idx == merge.cursor && chunk.kind != ChunkKind::Stable;
let change = change_type(chunk);
let result_lines = merge.current_content(idx);
let st = &merge.states[idx];
let (ours_state, theirs_state) = match chunk.kind {
ChunkKind::Ours => (Some(st.ours), None),
ChunkKind::Theirs => (None, Some(st.theirs)),
ChunkKind::Agree | ChunkKind::Conflict => (Some(st.ours), Some(st.theirs)),
ChunkKind::Stable => (None, None),
};
let height = chunk
.ours
.len()
.max(result_lines.len())
.max(chunk.theirs.len())
.max(1);
let placeholder_at = (chunk.kind == ChunkKind::Conflict && !resolved).then_some(height / 2);
let push_slice = |rows: &mut Vec<Row>, range: std::ops::Range<usize>| {
for i in range {
let cell = |lines: &[String], start: usize| match lines.get(i) {
Some(t) => Cell::Line {
no: start + i,
offset: i,
text: t.clone(),
},
None => Cell::Empty,
};
rows.push(Row {
chunk: idx,
change,
resolved,
current,
fold: None,
ours_state,
theirs_state,
ours: cell(&chunk.ours, chunk.ours_start),
result: match placeholder_at {
Some(at) if i == at => Cell::Placeholder,
Some(_) => Cell::Empty,
None => cell(&result_lines, result_no),
},
theirs: cell(&chunk.theirs, chunk.theirs_start),
});
}
};
let len = chunk.base.len();
if chunk.kind == ChunkKind::Stable && folded && len > FOLD_THRESHOLD {
push_slice(&mut rows, 0..FOLD_KEEP);
rows.push(Row {
chunk: idx,
change,
resolved,
current: false,
fold: Some(len - FOLD_KEEP * 2),
ours_state,
theirs_state,
ours: Cell::Empty,
result: Cell::Empty,
theirs: Cell::Empty,
});
push_slice(&mut rows, len - FOLD_KEEP..len);
} else {
push_slice(&mut rows, 0..height);
}
result_no += result_lines.len();
}
(rows, chunk_starts)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::Side;
#[test]
fn build_rows_folds_long_stable_chunks() {
let base: String = (1..=20).map(|i| format!("line{i}\n")).collect();
let merge = FileMerge::from_three_way("demo.txt".to_owned(), &base, &base, &base);
let (rows, starts) = build_rows(&merge, true);
assert_eq!(rows.len(), 7);
assert_eq!(rows[3].fold, Some(14));
assert_eq!(starts, vec![0]);
let (unfolded, _) = build_rows(&merge, false);
assert_eq!(unfolded.len(), 20);
}
#[test]
fn change_type_maps_idea_semantics() {
let v = |s: &[&str]| s.iter().map(|t| (*t).to_owned()).collect::<Vec<_>>();
let mk = |kind, base: &[&str], ours: &[&str], theirs: &[&str]| MergeChunk {
id: 0,
kind,
base: v(base),
ours: v(ours),
theirs: v(theirs),
base_start: 1,
ours_start: 1,
theirs_start: 1,
};
let c = mk(ChunkKind::Ours, &[], &["new"], &[]);
assert_eq!(change_type(&c), ChangeType::Added);
let c = mk(ChunkKind::Theirs, &["old"], &["old"], &[]);
assert_eq!(change_type(&c), ChangeType::Deleted);
let c = mk(ChunkKind::Ours, &["a"], &["b"], &["a"]);
assert_eq!(change_type(&c), ChangeType::Modified);
let c = mk(ChunkKind::Conflict, &[], &["x"], &["y"]);
assert_eq!(change_type(&c), ChangeType::Conflict);
let c = mk(ChunkKind::Agree, &[], &["n"], &["n"]);
assert_eq!(change_type(&c), ChangeType::Added);
}
#[test]
fn result_shows_placeholder_until_conflict_resolved() {
let mut merge =
FileMerge::from_three_way("demo.txt".to_owned(), "a\nb\nc\n", "a\nX\nc\n", "a\nY\nc\n");
let (rows, _) = build_rows(&merge, false);
assert!(rows.iter().any(|r| r.result == Cell::Placeholder));
merge.apply(Side::Ours);
merge.ignore(Side::Theirs);
let (rows, _) = build_rows(&merge, false);
assert!(rows.iter().all(|r| r.result != Cell::Placeholder));
assert!(rows.iter().any(|r| matches!(
&r.result,
Cell::Line { text, .. } if text == "X"
)));
}
}