feat: follow forward working-checkout changes in diffs

This commit is contained in:
Valdemar Lorenzen
2026-10-13 13:38:46 +02:00
parent b16232aa1d
commit bfdb7c544f
12 changed files with 1118 additions and 62 deletions
+262 -12
View File
@@ -2542,8 +2542,13 @@ impl Editor {
self.diff.as_ref().or(self.parked_diff.as_ref())
}
pub(crate) fn diff_root(&mut self) -> PathBuf {
let owner = self.diff_owner.unwrap_or(self.pane_id);
pub(crate) fn diff_root(&mut self, tab: usize) -> PathBuf {
let owner = if tab == self.tab_id {
self.diff_owner.unwrap_or(self.pane_id)
} else {
let tab = &self.tabs[&tab];
tab.diff_owner.unwrap_or(tab.pane_id)
};
self.with_pane(owner, |editor| {
if let Some(root) = &editor.active.snapshot_root {
return root.clone();
@@ -2575,15 +2580,60 @@ impl Editor {
self.diff_display_options = (view_mode, split_axis);
}
pub(crate) fn queue_working_refreshes(&mut self) {
for index in 0..self.tab_order.len() {
let tab = self.tab_order[index];
let owner = if tab == self.tab_id {
self.diff_owner
} else {
self.tabs.get(&tab).and_then(|tab| tab.diff_owner)
};
if let Some(owner) = owner {
self.with_pane(owner, |editor| {
if let Some(diff) = &mut editor.diff {
diff.queue_working_refresh();
}
});
}
}
}
pub(crate) fn take_next_diff_request(&mut self) -> Option<(usize, u64, Operation)> {
let tab = std::iter::once(self.tab_id)
.chain(
self.tab_order
.iter()
.copied()
.filter(|tab| *tab != self.tab_id),
)
.find(|tab| self.tab_diff(*tab).is_some_and(DiffState::request_ready))?;
let owner = if tab == self.tab_id {
self.diff_owner
} else {
self.tabs.get(&tab)?.diff_owner
}?;
self.with_pane(owner, |editor| {
let (id, operation) = editor.take_diff_request()?;
Some((tab, id, operation))
})
}
pub(crate) fn take_diff_request(&mut self) -> Option<(u64, Operation)> {
let mut operation = self
.diff
.as_mut()
.or(self.parked_diff.as_mut())?
.take_request()?;
if let Operation::Compare { after, buffers, .. } = &mut operation
&& after.target == RevisionTarget::Working
{
let buffers = match &mut operation {
Operation::Compare { after, buffers, .. }
if after.target == RevisionTarget::Working =>
{
Some(buffers)
}
Operation::RefreshWorking { buffers, .. } => Some(buffers),
_ => None,
};
if let Some(buffers) = buffers {
*buffers = std::iter::once(&self.active)
.chain(self.parked.values())
.filter(|buffer| buffer.dirty())
@@ -2639,15 +2689,32 @@ impl Editor {
let Some(diff) = &mut self.diff else {
return false;
};
if diff.request_queued() {
if diff.request_queued() && !diff.refresh_pending() {
return false;
}
diff.apply_result(id, result);
if let Some(files) = &mut diff.files {
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 {
files.set_view(self.file_explorer_view);
}
let obsolete_snapshot = self.active.diff_snapshot.is_some_and(|role| {
self.diff.as_ref().and_then(|diff| diff.comparison_id) != Some(role.0)
});
self.bind_diff_buffer();
self.refresh_live_diff();
if refresh || obsolete_snapshot {
self.prune_diff_snapshots();
}
let completed_navigation = self
.pending_lsp_navigation
.as_ref()
@@ -2792,6 +2859,17 @@ impl Editor {
.is_some_and(|bound| bound.3 != self.active.id)
{
if let Some(role) = self.active.diff_snapshot {
if self
.diff
.as_ref()
.is_none_or(|diff| diff.comparison_id != Some(role.0))
{
self.diff_bound = None;
self.diff = None;
self.diff_owner = None;
self.outstanding_diff_request = None;
return;
}
self.diff_bound = Some((role.0, role.1, role.2, self.active.id));
if let Some(diff) = &mut self.diff {
if diff.file != role.1
@@ -2821,9 +2899,18 @@ impl Editor {
{
self.activate(buffer);
}
self.prune_diff_snapshots();
self.diff_bound = None;
self.diff_return_buffer = None;
}
fn prune_diff_snapshots(&mut self) {
self.parked.retain(|id, buffer| {
buffer.diff_snapshot.is_none()
|| self.panes.values().any(|pane| pane.buffer_id == *id)
|| self
.panes
.iter()
.any(|(pane_id, pane)| *pane_id != self.pane_id && pane.buffer_id == *id)
|| buffer.diff_snapshot.is_some_and(|role| {
self.tabs.values().any(|tab| {
tab.diff()
@@ -2835,8 +2922,6 @@ impl Editor {
pane.views
.retain(|id, _| *id == self.active.id || self.parked.contains_key(id));
}
self.diff_bound = None;
self.diff_return_buffer = None;
}
fn bind_diff_buffer(&mut self) {
@@ -12638,6 +12723,171 @@ mod tests {
assert!(editor.diff_interacting());
}
#[test]
fn working_refresh_routes_hidden_owners_and_keeps_obsolete_shared_snapshots_readonly() {
let directory = Directory::new();
fs::write(directory.path("file.txt"), "working").unwrap();
let mut editor = editor();
working_diff(&mut editor, &directory.0, &["file.txt"]);
editor.diff.as_mut().unwrap().focus = Focus::Before;
editor.bind_diff_buffer();
let old_snapshot = editor.buffer_id();
let old_comparison = editor.diff().unwrap().comparison_id.unwrap();
let owner = editor.pane_id();
let hidden_tab = editor.tab_id();
editor.execute("tab.new");
let visible_tab = editor.tab_id();
let visible_pane = editor.pane_id();
let cursor = editor.cursor();
editor.queue_working_refreshes();
let (tab, request, operation) = editor.take_next_diff_request().unwrap();
assert_eq!(tab, hidden_tab);
assert!(
matches!(operation, Operation::RefreshWorking { comparison_id, .. }
if comparison_id == old_comparison)
);
assert!(!editor.apply_diff_result(request, Ok(Outcome::WorkingRefresh(None))));
assert_eq!(
editor.tab_diff(hidden_tab).unwrap().comparison_id,
Some(old_comparison)
);
assert_eq!(editor.tab_id(), visible_tab);
assert_eq!(editor.pane_id(), visible_pane);
assert_eq!(editor.buffer_id(), old_snapshot);
assert_eq!(editor.cursor(), cursor);
editor.queue_working_refreshes();
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_eq!(
editor.tab_diff(hidden_tab).unwrap().comparison_id,
Some(request)
);
assert_eq!(editor.tab_id(), visible_tab);
assert_eq!(editor.buffer_id(), old_snapshot);
assert_eq!(editor.cursor(), cursor);
assert!(editor.active.diff_snapshot.is_some());
assert!(
editor
.apply(vec![Effect::Insert("rejected".into())])
.is_err()
);
assert!(!editor.apply_diff_result(request, Ok(Outcome::WorkingRefresh(None))));
editor.with_pane(owner, |editor| {
assert_ne!(editor.buffer_id(), old_snapshot);
editor.switch_buffer(old_snapshot);
editor.leave_diff_on_navigation();
assert!(editor.diff().is_none());
assert!(editor.active.diff_snapshot.is_some());
assert!(editor.mutation_blocked());
});
}
#[test]
fn completed_working_refresh_serializes_lazy_selection_and_invalidates_pending_navigation() {
for request_kind in [
"selection",
"after_selection",
"navigation",
"noop_navigation",
] {
let directory = Directory::new();
fs::write(directory.path("file.txt"), "working").unwrap();
let mut editor = editor();
working_diff(&mut editor, &directory.0, &["file.txt"]);
editor.diff.as_mut().unwrap().focus = Focus::Before;
editor.bind_diff_buffer();
let old_snapshot = editor.buffer_id();
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();
let (_, refresh, _) = editor.take_next_diff_request().unwrap();
let loaded_before = target.before.clone();
let loaded_after = target.after.clone();
target.loaded = false;
let replacement = crate::diff::Comparison {
root: directory.0.clone(),
before: "base".into(),
after: "ahead".into(),
editable_paths: Default::default(),
files: vec![target],
};
let selection = request_kind.ends_with("selection");
let focus = if request_kind == "after_selection" {
Focus::After
} else {
Focus::Before
};
if selection {
assert!(editor.diff.as_mut().unwrap().select_file_index(1));
editor.diff.as_mut().unwrap().focus = focus;
} else {
editor
.navigate_lsp_to(virtual_root.join("nested/old.txt"), lsp_point(1, 4), None)
.unwrap();
assert!(editor.pending_lsp_navigation.is_some());
assert_eq!(editor.diff().unwrap().file, 0);
}
assert!(editor.take_next_diff_request().is_none());
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))),
changed
);
if changed {
assert!(editor.pending_lsp_navigation.is_none());
assert_eq!(editor.diff().unwrap().comparison_id, Some(refresh));
assert_eq!(editor.diff().unwrap().file, 0);
assert_eq!(editor.diff().unwrap().focus, focus);
} else {
assert!(editor.pending_lsp_navigation.is_some());
assert_eq!(editor.diff().unwrap().comparison_id, Some(old_comparison));
}
let (_, load, operation) = editor.take_next_diff_request().unwrap();
assert!(
matches!(operation, Operation::LoadFile { comparison_id, file_index }
if comparison_id == if changed { refresh } else { old_comparison }
&& file_index == if changed { 0 } else { 1 })
);
if !changed {
assert_eq!(
editor.pending_lsp_navigation.as_ref().unwrap().request,
Some(load)
);
}
if changed {
let file = crate::diff::ChangedFile {
path: "nested/new.txt".into(),
old_path: Some("nested/old.txt".into()),
status: crate::diff::FileStatus::Renamed,
loaded: true,
hunks: crate::diff::line_hunks(&loaded_before, false, &loaded_after, false),
before: loaded_before,
after: loaded_after,
before_eof: false,
after_eof: false,
notice: None,
};
assert!(editor.apply_diff_result(
load,
Ok(Outcome::File {
comparison_id: refresh,
file_index: 0,
file,
})
));
assert_eq!(editor.diff().unwrap().focus, focus);
assert!(!editor.parked.contains_key(&old_snapshot));
}
}
}
fn lsp_point(row: usize, column: usize) -> Utf16Range {
Utf16Range {
start: (row, column),
@@ -13010,7 +13260,7 @@ mod tests {
editor.interaction_document_root(editor.buffer_id()),
directory.0.as_path()
);
assert_eq!(editor.diff_root(), directory.0);
assert_eq!(editor.diff_root(editor.tab_id()), directory.0);
editor.handle(Input::Char('\t'));
assert_eq!(editor.buffer_id(), live_id);
assert!(editor.diff_editing());