feat: track checkout identity for pinned diff editability

This commit is contained in:
2026-10-13 17:04:15 +02:00
parent e3e087f16f
commit 99fa5a2aff
14 changed files with 705 additions and 170 deletions
+156 -30
View File
@@ -2588,7 +2588,7 @@ impl Editor {
self.diff_display_options = (view_mode, split_axis);
}
pub(crate) fn queue_working_refreshes(&mut self) {
pub(crate) fn queue_checkout_refreshes(&mut self, follow_working: bool) {
for index in 0..self.tab_order.len() {
let tab = self.tab_order[index];
let owner = if tab == self.tab_id {
@@ -2599,7 +2599,7 @@ impl Editor {
if let Some(owner) = owner {
self.with_pane(owner, |editor| {
if let Some(diff) = &mut editor.diff {
diff.queue_working_refresh();
diff.queue_checkout_refresh(follow_working);
}
});
}
@@ -2638,7 +2638,19 @@ impl Editor {
{
Some(buffers)
}
Operation::RefreshWorking { buffers, .. } => Some(buffers),
Operation::RefreshCheckout {
follow_working: true,
buffers,
..
} if self
.diff
.as_ref()
.or(self.parked_diff.as_ref())
.and_then(|diff| diff.after.as_ref())
.is_some_and(|after| after.target == RevisionTarget::Working) =>
{
Some(buffers)
}
_ => None,
};
if let Some(buffers) = buffers {
@@ -2694,6 +2706,25 @@ impl Editor {
return self.with_pane(owner, |editor| editor.apply_diff_result(id, result));
}
self.outstanding_diff_request = None;
if self.diff.as_ref().is_some_and(DiffState::refresh_pending)
&& !matches!(&result, Ok(Outcome::CheckoutRefresh(None)))
{
self.finish_completion();
self.snippet = None;
self.clear_hover();
if self.active.view.mode == Mode::Insert {
self.finish_insert();
self.leave_insert();
}
}
let preserved_view = (self.diff.as_ref().is_some_and(|diff| {
diff.refresh_pending()
&& diff
.after
.as_ref()
.is_some_and(|after| after.target != RevisionTarget::Working)
}) && !matches!(&result, Ok(Outcome::CheckoutRefresh(None))))
.then(|| self.active.view.inherit());
let Some(diff) = &mut self.diff else {
return false;
};
@@ -2701,15 +2732,10 @@ impl Editor {
return false;
}
let refresh = diff.refresh_pending();
let previous = diff.comparison_id;
if !diff.apply_result(id, result) {
return false;
}
let replaced = previous != diff.comparison_id;
if refresh {
if !replaced {
return true;
}
self.pending_lsp_navigation = None;
self.diff_bound = None;
} else if let Some(files) = &mut diff.files {
@@ -2719,7 +2745,34 @@ impl Editor {
self.diff.as_ref().and_then(|diff| diff.comparison_id) != Some(role.0)
});
self.bind_diff_buffer();
let pinned_refresh = preserved_view.is_some();
if self.active.diff_snapshot.is_some()
&& let Some(mut view) = preserved_view
{
view.clamp(&self.active.lines);
self.active.view = view;
}
let reviewed = self.diff.as_mut().and_then(|diff| {
(refresh
&& diff
.after
.as_ref()
.is_some_and(|after| after.target != RevisionTarget::Working))
.then(|| std::mem::take(&mut diff.reviewed_files))
});
self.refresh_live_diff();
if let Some(reviewed) = reviewed {
self.diff
.as_mut()
.expect("comparison remains open")
.reviewed_files = reviewed;
}
if pinned_refresh {
self.diff
.as_mut()
.expect("comparison remains open")
.follow_caret = false;
}
if refresh || obsolete_snapshot {
self.prune_diff_snapshots();
}
@@ -2976,8 +3029,15 @@ impl Editor {
diff.focus
};
let key = (comparison_id, diff.file, focus);
let writable = focus == Focus::After && diff.after_editable();
if self.diff_bound.is_some_and(|(id, file, side, buffer)| {
(id, file, side) == key && buffer == self.active.id
(id, file, side) == key
&& buffer == self.active.id
&& if writable {
self.active.diff_snapshot.is_none() && diff.live_buffer == Some(self.active.id)
} else {
self.active.diff_snapshot == Some(key)
}
}) {
return;
}
@@ -2996,7 +3056,6 @@ impl Editor {
diff.corresponding_row(from, focus, target.active.row)
.map(|row| (row, column))
});
let writable = focus == Focus::After && diff.after_editable();
// Binding a comparison side is not a user departure from its palette.
let palette = self.palette.take();
if writable {
@@ -3139,7 +3198,7 @@ impl Editor {
}
});
if let Some(buffer) = buffer
&& diff.working_after_available()
&& diff.live_after_available()
&& diff.live_revision != Some(buffer.text_revision)
{
diff.refresh_after(&buffer.lines, buffer.text_revision);
@@ -3833,10 +3892,6 @@ impl Editor {
return Err("Definition target is binary or non-UTF-8".into());
}
let writable = focus == Focus::After
&& diff
.after
.as_ref()
.is_some_and(|revision| revision.target == RevisionTarget::Working)
&& comparison.editable_paths.contains(&file.path)
&& file.status != crate::diff::FileStatus::Removed
&& file
@@ -3872,13 +3927,7 @@ impl Editor {
let file = &comparison.files[file_index];
let live = if writable {
Some(target)
} else if focus == Focus::Before
&& diff
.after
.as_ref()
.is_some_and(|revision| revision.target == RevisionTarget::Working)
&& comparison.editable_paths.contains(&file.path)
{
} else if focus == Focus::Before && comparison.editable_paths.contains(&file.path) {
let identity = path_identity(&comparison.root.join(&file.path)).ok();
std::iter::once(&self.active)
.chain(self.parked.values())
@@ -12780,14 +12829,14 @@ mod tests {
let visible_tab = editor.tab_id();
let visible_pane = editor.pane_id();
let cursor = editor.cursor();
editor.queue_working_refreshes();
editor.queue_checkout_refreshes(true);
let (tab, request, operation) = editor.take_next_diff_request().unwrap();
assert_eq!(tab, hidden_tab);
assert!(
matches!(operation, Operation::RefreshWorking { comparison_id, .. }
matches!(operation, Operation::RefreshCheckout { comparison_id, .. }
if comparison_id == old_comparison)
);
assert!(!editor.apply_diff_result(request, Ok(Outcome::WorkingRefresh(None))));
assert!(!editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(None))));
assert_eq!(
editor.tab_diff(hidden_tab).unwrap().comparison_id,
Some(old_comparison)
@@ -12797,12 +12846,12 @@ mod tests {
assert_eq!(editor.buffer_id(), old_snapshot);
assert_eq!(editor.cursor(), cursor);
editor.queue_working_refreshes();
editor.queue_checkout_refreshes(true);
let (_, request, _) = editor.take_next_diff_request().unwrap();
let replacement = editor.with_pane(owner, |editor| {
editor.diff.as_mut().unwrap().comparison.take().unwrap()
});
assert!(editor.apply_diff_result(request, Ok(Outcome::WorkingRefresh(Some(replacement)))));
assert!(editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(Some(replacement)))));
assert_eq!(
editor.tab_diff(hidden_tab).unwrap().comparison_id,
Some(request)
@@ -12816,7 +12865,7 @@ mod tests {
.apply(vec![Effect::Insert("rejected".into())])
.is_err()
);
assert!(!editor.apply_diff_result(request, Ok(Outcome::WorkingRefresh(None))));
assert!(!editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(None))));
editor.with_pane(owner, |editor| {
assert_ne!(editor.buffer_id(), old_snapshot);
@@ -12846,7 +12895,7 @@ mod tests {
let mut target = lazy_lsp_target(&mut editor);
let old_comparison = editor.diff().unwrap().comparison_id.unwrap();
let virtual_root = editor.diff().unwrap().snapshot_roots[0].clone().unwrap();
editor.queue_working_refreshes();
editor.queue_checkout_refreshes(true);
let (_, refresh, _) = editor.take_next_diff_request().unwrap();
let loaded_before = target.before.clone();
let loaded_after = target.after.clone();
@@ -12878,7 +12927,7 @@ mod tests {
let changed = request_kind != "noop_navigation";
let outcome = if changed { Some(replacement) } else { None };
assert_eq!(
editor.apply_diff_result(refresh, Ok(Outcome::WorkingRefresh(outcome))),
editor.apply_diff_result(refresh, Ok(Outcome::CheckoutRefresh(outcome))),
changed
);
if changed {
@@ -13832,6 +13881,83 @@ mod tests {
assert!(editor.dirty());
editor.handle(Input::Char('u'));
assert_eq!(editor.lines(), ["beta"]);
for refresh_error in [false, true] {
let path = directory.path("file.txt");
fs::write(&path, "original").unwrap();
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
historical_diff(
&mut editor,
&directory.0,
vec!["before".into()],
vec!["original".into()],
);
let endpoints = (
editor.diff().unwrap().before.clone(),
editor.diff().unwrap().after.clone(),
);
editor.queue_checkout_refreshes(false);
let (_, request, _) = editor.take_next_diff_request().unwrap();
let mut promoted = editor.diff.as_mut().unwrap().comparison.take().unwrap();
promoted.editable_paths.insert("file.txt".into());
editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(Some(promoted))));
editor.diff.as_mut().unwrap().focus = Focus::After;
editor.bind_diff_buffer();
assert!(editor.diff_editing());
let live = editor.buffer_id();
type_text(&mut editor, "ix");
assert_eq!(editor.mode(), Mode::Insert);
let comparison_id = editor.diff().unwrap().comparison_id;
editor.queue_checkout_refreshes(false);
let (_, request, _) = editor.take_next_diff_request().unwrap();
if refresh_error {
assert!(editor.apply_diff_result(request, Err("checkout unavailable".into())));
assert_eq!(editor.diff().unwrap().comparison_id, comparison_id);
} else {
let mut demoted = editor.diff.as_mut().unwrap().comparison.take().unwrap();
demoted.editable_paths.clear();
demoted.files[0].after = vec!["original".into()];
demoted.files[0].after_eof = false;
demoted.files[0].hunks = crate::diff::line_hunks(
&demoted.files[0].before,
false,
&demoted.files[0].after,
false,
);
assert!(
editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(Some(demoted))),)
);
}
assert!(!editor.diff_editable());
assert_eq!(editor.lines(), ["original"]);
assert_eq!(editor.buffer(live).unwrap().lines, ["xoriginal"]);
assert!(editor.buffer(live).unwrap().dirty());
assert_eq!(editor.buffer(live).unwrap().view.mode, Mode::Normal);
assert!(
editor
.apply(vec![Effect::Insert("blocked".into())])
.is_err()
);
editor.execute("w");
assert_eq!(fs::read_to_string(&path).unwrap(), "original");
editor.queue_checkout_refreshes(false);
let (_, request, _) = editor.take_next_diff_request().unwrap();
let mut promoted = editor.diff.as_mut().unwrap().comparison.take().unwrap();
promoted.editable_paths.insert("file.txt".into());
editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(Some(promoted))));
assert!(editor.diff_editing());
assert!(editor.diff().unwrap().error.is_none());
assert_eq!(editor.buffer_id(), live);
assert_eq!(editor.lines(), ["xoriginal"]);
assert_eq!(
(
editor.diff().unwrap().before.clone(),
editor.diff().unwrap().after.clone(),
),
endpoints,
);
editor.handle(Input::Char('u'));
assert_eq!(editor.lines(), ["original"]);
}
}
#[test]