feat: resolve conflicts through focused two-way drafts
Replace the marker-filled three-pane resolver with a marker-free native Draft versus frozen Alternative projected from current working text. Preserve preexisting outside edits, native undo, dirty-save guards, all sides/bases, and draft caret/viewports across separate History views. Reuse DiffState alignment/highlighting/excerpts for Resolution and merge its base-aligned rows for readonly three-way History. Hide explorer and source-selector chrome by default. Verified 926 tests plus actual terminal projection, History highlighting, hunk choice, optional explorer, and guarded jj-confirmed save workflows. Clippy completes with pre-existing warnings only. Smoke fixtures stay unresolved in this ancestry; remove that ancestry before publication. Size exception: approximately 1650 implementation added/deleted lines, excluding tests/docs, kept coherent because native draft lifecycle and aligned rendering must change together without parallel legacy modes.
This commit is contained in:
+103
@@ -350,6 +350,87 @@ impl DiffState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_resolver_text(&mut self, draft: &[String], alternative: &[String]) {
|
||||
fn content(lines: &[String]) -> (&[String], bool) {
|
||||
let eof = lines.len() > 1 && lines.last().is_some_and(String::is_empty);
|
||||
let content = if eof {
|
||||
&lines[..lines.len() - 1]
|
||||
} else {
|
||||
lines
|
||||
};
|
||||
(
|
||||
if !eof && content.len() == 1 && content[0].is_empty() {
|
||||
&[]
|
||||
} else {
|
||||
content
|
||||
},
|
||||
eof,
|
||||
)
|
||||
}
|
||||
let (before, before_eof) = content(draft);
|
||||
let (after, after_eof) = content(alternative);
|
||||
if self.comparison.as_ref().is_some_and(|comparison| {
|
||||
let file = &comparison.files[0];
|
||||
file.before == before
|
||||
&& file.after == after
|
||||
&& file.before_eof == before_eof
|
||||
&& file.after_eof == after_eof
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
let viewport = (self.top, self.left_before, self.left_after);
|
||||
self.phase = Phase::View;
|
||||
self.loading = false;
|
||||
self.pending = None;
|
||||
self.request_kind = None;
|
||||
self.files_visible = false;
|
||||
self.comparison_id = Some(self.comparison_id.unwrap_or(0).wrapping_add(1));
|
||||
self.file = 0;
|
||||
self.live_files.clear();
|
||||
self.comparison = Some(Comparison {
|
||||
before: "Draft".into(),
|
||||
after: "Alternative".into(),
|
||||
root: PathBuf::new(),
|
||||
editable_paths: BTreeSet::new(),
|
||||
files: vec![ChangedFile {
|
||||
path: PathBuf::new(),
|
||||
old_path: None,
|
||||
status: FileStatus::Modified,
|
||||
loaded: true,
|
||||
before: before.to_vec(),
|
||||
after: after.to_vec(),
|
||||
before_eof,
|
||||
after_eof,
|
||||
hunks: line_hunks(before, before_eof, after, after_eof),
|
||||
notice: None,
|
||||
}],
|
||||
});
|
||||
self.focus = if self.focus == Focus::After {
|
||||
Focus::After
|
||||
} else {
|
||||
Focus::Before
|
||||
};
|
||||
self.align_file();
|
||||
let before_terminal = (draft.len() > before.len()).then_some(before.len());
|
||||
let after_terminal = (alternative.len() > after.len()).then_some(after.len());
|
||||
if before_terminal.is_some() || after_terminal.is_some() {
|
||||
self.rows.push(AlignedRow {
|
||||
before: before_terminal,
|
||||
after: after_terminal,
|
||||
kind: RowKind::Equal,
|
||||
changes: Vec::new(),
|
||||
});
|
||||
self.reset_excerpts();
|
||||
}
|
||||
if self.comparison.as_ref().unwrap().files[0].hunks.is_empty() {
|
||||
self.excerpt_ranges = vec![(0, self.rows.len())];
|
||||
self.rebuild_display_rows();
|
||||
}
|
||||
self.top = viewport.0.min(self.display_rows.len().saturating_sub(1));
|
||||
self.left_before = viewport.1.min(self.max_before);
|
||||
self.left_after = viewport.2.min(self.max_after);
|
||||
}
|
||||
|
||||
pub(crate) fn pull_requests(remote: String) -> Self {
|
||||
Self {
|
||||
pull_request_remote: Some(remote.clone()),
|
||||
@@ -2291,6 +2372,28 @@ mod tests {
|
||||
pub(super) static WIDTH_SCANS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolver_equal_text_retains_native_rows_and_cached_viewport() {
|
||||
for text in ["", "\n", "same\n", "same"] {
|
||||
let lines: Vec<_> = text.split('\n').map(str::to_owned).collect();
|
||||
let mut state = DiffState::new();
|
||||
state.set_resolver_text(&lines, &lines);
|
||||
assert_eq!(state.pane_layout(), PaneLayout::Split);
|
||||
assert!(state.comparison.as_ref().unwrap().files[0].hunks.is_empty());
|
||||
for row in 0..lines.len() {
|
||||
assert!(state.source_display_row(Focus::Before, row).is_some());
|
||||
assert!(state.source_display_row(Focus::After, row).is_some());
|
||||
}
|
||||
let id = state.comparison_id;
|
||||
state.top = 1;
|
||||
state.left_before = 3;
|
||||
state.left_after = 4;
|
||||
state.set_resolver_text(&lines, &lines);
|
||||
assert_eq!(state.comparison_id, id);
|
||||
assert_eq!((state.top, state.left_before, state.left_after), (1, 3, 4));
|
||||
}
|
||||
}
|
||||
|
||||
fn excerpt_state(len: usize, changed: &[usize]) -> DiffState {
|
||||
let mut state = DiffState::new();
|
||||
state.phase = Phase::View;
|
||||
|
||||
Reference in New Issue
Block a user