feat: refresh diffs when selected jj bookmarks move

This commit is contained in:
Valdemar Lorenzen
2026-10-15 15:18:13 +02:00
parent 1a02502488
commit c32d4a7590
8 changed files with 991 additions and 62 deletions
+174 -16
View File
@@ -4188,16 +4188,13 @@ impl Editor {
{
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) =>
Operation::RefreshCheckout { 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)
}
@@ -4275,11 +4272,14 @@ impl Editor {
}
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))))
&& (matches!(&result, Ok(Outcome::BookmarkRefresh(_)))
|| diff
.after
.as_ref()
.is_some_and(|after| after.target != RevisionTarget::Working))
&& !matches!(&result, Ok(Outcome::CheckoutRefresh(None)))
|| matches!(&result, Ok(Outcome::File { .. })) && diff.refresh_view.is_some()
}))
.then(|| self.active.view.inherit());
let Some(diff) = &mut self.diff else {
return false;
@@ -4288,11 +4288,14 @@ impl Editor {
return false;
}
let refresh = diff.refresh_pending();
let previous_comparison = diff.comparison_id;
if !diff.apply_result(id, result) {
return false;
}
if refresh {
self.pending_lsp_navigation = None;
if diff.comparison_id != previous_comparison {
self.pending_lsp_navigation = None;
}
self.diff_bound = None;
} else if let Some(files) = &mut diff.files {
files.set_view(self.file_explorer_view);
@@ -15573,6 +15576,161 @@ mod tests {
});
}
#[test]
fn bookmark_refresh_reloads_paths_and_preserves_view_until_lazy_content_arrives() {
use crate::diff::{ChangedFile, Comparison, FileStatus};
let directory = Directory::new();
let mut editor = editor();
historical_diff(
&mut editor,
&directory.0,
vec!["old before".into(); 4],
vec!["old after".into(); 4],
);
editor.diff.as_mut().unwrap().after.as_mut().unwrap().target = RevisionTarget::Bookmark {
name: "topic".into(),
remote: None,
};
editor.diff.as_mut().unwrap().focus = Focus::After;
editor.bind_diff_buffer();
editor.handle(Input::Char('j'));
editor.diff.as_mut().unwrap().top = 2;
editor.diff.as_mut().unwrap().left_after = 3;
editor.diff.as_mut().unwrap().reviewed_files.insert(0);
editor.diff.as_mut().unwrap().view_mode = ViewMode::Unified;
let old_comparison = editor.diff().unwrap().comparison_id.unwrap();
let old_snapshot = editor.buffer_id();
let cursor = editor.cursor();
let file = |path: &str, loaded| ChangedFile {
path: path.into(),
old_path: None,
status: FileStatus::Modified,
loaded,
before: if loaded {
vec!["new before".into(); 3]
} else {
vec![]
},
after: if loaded {
vec!["new after".into(); 3]
} else {
vec![]
},
before_eof: false,
after_eof: false,
hunks: if loaded {
crate::diff::line_hunks(
&vec!["new before".into(); 3],
false,
&vec!["new after".into(); 3],
false,
)
} else {
vec![]
},
notice: None,
};
editor.queue_checkout_refreshes(false);
let (_, refresh, _) = editor.take_next_diff_request().unwrap();
assert!(editor.apply_diff_result(
refresh,
Ok(Outcome::BookmarkRefresh(Comparison {
root: directory.0.clone(),
before: "base".into(),
after: "topic".into(),
editable_paths: Default::default(),
files: vec![file("a-new.txt", false), file("file.txt", false)],
}))
));
let diff = editor.diff().unwrap();
assert_eq!(diff.file, 1);
assert_eq!(diff.focus, Focus::After);
assert_eq!(diff.view_mode, ViewMode::Unified);
assert!(diff.reviewed_files.is_empty());
assert!(!diff.selected_file_loaded());
assert!(!diff.comparison.as_ref().unwrap().files[0].loaded);
assert_eq!(editor.buffer_id(), old_snapshot);
let (_, load, operation) = editor.take_next_diff_request().unwrap();
assert!(
matches!(operation, Operation::LoadFile { comparison_id, file_index: 1 } if comparison_id == refresh)
);
assert!(editor.apply_diff_result(
load,
Ok(Outcome::File {
comparison_id: refresh,
file_index: 1,
file: file("file.txt", true),
})
));
assert_eq!(editor.lines(), ["new after"; 3]);
assert_eq!(editor.cursor(), cursor);
assert_eq!(editor.diff().unwrap().top, 2);
assert_eq!(editor.diff().unwrap().left_after, 3);
assert_ne!(editor.buffer_id(), old_snapshot);
assert!(editor.mutation_blocked());
assert!(!editor.apply_diff_result(refresh, Ok(Outcome::CheckoutRefresh(None))));
assert_ne!(editor.diff().unwrap().comparison_id, Some(old_comparison));
}
#[test]
fn bookmark_refresh_errors_recover_and_hidden_replacements_preserve_dirty_buffers() {
let directory = Directory::new();
let path = directory.path("file.txt");
fs::write(&path, "original").unwrap();
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
working_diff(&mut editor, &directory.0, &["file.txt"]);
editor.diff.as_mut().unwrap().after.as_mut().unwrap().target = RevisionTarget::Bookmark {
name: "topic".into(),
remote: None,
};
editor.diff.as_mut().unwrap().focus = Focus::After;
editor.bind_diff_buffer();
let dirty_buffer = editor.buffer_id();
type_text(&mut editor, "ix");
editor.diff.as_mut().unwrap().reviewed_files.insert(0);
let comparison_id = editor.diff().unwrap().comparison_id;
editor.queue_checkout_refreshes(false);
let (_, request, _) = editor.take_next_diff_request().unwrap();
assert!(editor.apply_diff_result(request, Err("bookmark is conflicted".into())));
assert_eq!(editor.diff().unwrap().comparison_id, comparison_id);
assert_eq!(editor.lines(), ["original"]);
assert!(editor.diff().unwrap().reviewed_files.contains(&0));
assert_eq!(editor.buffer(dirty_buffer).unwrap().lines, ["xoriginal"]);
assert!(editor.buffer(dirty_buffer).unwrap().dirty());
editor.queue_checkout_refreshes(false);
let (_, recovery, _) = editor.take_next_diff_request().unwrap();
assert!(editor.apply_diff_result(recovery, Ok(Outcome::CheckoutRefresh(None))));
assert!(editor.diff().unwrap().error.is_none());
assert_eq!(editor.diff().unwrap().comparison_id, comparison_id);
assert!(editor.diff().unwrap().reviewed_files.contains(&0));
let hidden_tab = editor.tab_id();
let owner = editor.pane_id();
let old_snapshot = editor.buffer_id();
editor.execute("tab.new");
let visible_tab = editor.tab_id();
editor.queue_checkout_refreshes(false);
let (tab, request, _) = editor.take_next_diff_request().unwrap();
assert_eq!(tab, hidden_tab);
let mut replacement = editor.with_pane(owner, |editor| {
editor.diff.as_mut().unwrap().comparison.take().unwrap()
});
replacement.files[0].after = vec!["moved bookmark".into()];
assert!(editor.apply_diff_result(request, Ok(Outcome::BookmarkRefresh(replacement))));
assert_eq!(editor.tab_id(), visible_tab);
assert_eq!(editor.buffer_id(), old_snapshot);
assert_eq!(editor.lines(), ["original"]);
assert!(editor.mutation_blocked());
assert_eq!(editor.buffer(dirty_buffer).unwrap().lines, ["xoriginal"]);
assert!(editor.buffer(dirty_buffer).unwrap().dirty());
editor.with_pane(owner, |editor| {
assert_eq!(editor.lines(), ["moved bookmark"]);
assert!(editor.diff().unwrap().reviewed_files.is_empty());
assert!(editor.mutation_blocked());
});
assert_eq!(fs::read_to_string(&path).unwrap(), "original");
}
#[test]
fn completed_working_refresh_serializes_lazy_selection_and_invalidates_pending_navigation() {
for request_kind in [