fix: restore tab and diff context through jump history
This commit is contained in:
+353
-20
@@ -1331,10 +1331,22 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct DiffJumpLocation {
|
||||
comparison_id: u64,
|
||||
file: usize,
|
||||
focus: Focus,
|
||||
text_focus: Focus,
|
||||
live_buffer: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct JumpLocation {
|
||||
tab_id: usize,
|
||||
pane_id: usize,
|
||||
buffer_id: usize,
|
||||
cursor: Cursor,
|
||||
diff: Option<DiffJumpLocation>,
|
||||
}
|
||||
|
||||
struct PendingLspNavigation {
|
||||
@@ -1367,6 +1379,7 @@ pub struct Editor {
|
||||
diff_owner: Option<usize>,
|
||||
parked_diff: Option<DiffState>,
|
||||
diff_pane_bounds: [(Focus, PaneRect); 3],
|
||||
// Closed destinations stay until bounded eviction so they cannot shift jump_index.
|
||||
jumps: VecDeque<JumpLocation>,
|
||||
jump_index: usize,
|
||||
clipboard: Vec<String>,
|
||||
@@ -1601,6 +1614,12 @@ impl Editor {
|
||||
.map(|buffer| buffer.info(id == self.tab_id))
|
||||
}
|
||||
|
||||
pub fn tab_is_diff(&self, index: usize) -> bool {
|
||||
self.tab_order
|
||||
.get(index)
|
||||
.is_some_and(|id| self.tab_diff(*id).is_some())
|
||||
}
|
||||
|
||||
pub(crate) fn tab_diff(&self, id: usize) -> Option<&DiffState> {
|
||||
if id == self.tab_id {
|
||||
self.workspace_diff()
|
||||
@@ -1979,8 +1998,6 @@ impl Editor {
|
||||
for pane in self.panes.values_mut() {
|
||||
pane.views.remove(&id);
|
||||
}
|
||||
self.jumps.retain(|jump| jump.buffer_id != id);
|
||||
self.jump_index = self.jump_index.min(self.jumps.len().saturating_sub(1));
|
||||
}
|
||||
|
||||
fn close_tab(&mut self, force: bool) {
|
||||
@@ -2441,10 +2458,6 @@ impl Editor {
|
||||
pane.views
|
||||
.retain(|id, _| *id == self.active.id || self.parked.contains_key(id));
|
||||
}
|
||||
self.jumps.retain(|jump| {
|
||||
self.active.id == jump.buffer_id || self.parked.contains_key(&jump.buffer_id)
|
||||
});
|
||||
self.jump_index = self.jump_index.min(self.jumps.len().saturating_sub(1));
|
||||
self.diff_bound = None;
|
||||
self.diff_return_buffer = None;
|
||||
}
|
||||
@@ -5767,9 +5780,22 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn jump_location(&self) -> JumpLocation {
|
||||
let diff = self.diff_buffer_focus().and_then(|text_focus| {
|
||||
let diff = self.diff.as_ref()?;
|
||||
Some(DiffJumpLocation {
|
||||
comparison_id: diff.comparison_id?,
|
||||
file: diff.file,
|
||||
focus: diff.focus,
|
||||
text_focus,
|
||||
live_buffer: diff.live_buffer,
|
||||
})
|
||||
});
|
||||
JumpLocation {
|
||||
tab_id: self.tab_id,
|
||||
pane_id: self.pane_id,
|
||||
buffer_id: self.active.id,
|
||||
cursor: self.cursor(),
|
||||
diff,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5791,6 +5817,113 @@ impl Editor {
|
||||
self.jump_index = self.jumps.len() - 1;
|
||||
}
|
||||
|
||||
fn resolve_jump(&self, mut location: JumpLocation) -> Option<JumpLocation> {
|
||||
let buffer = self.buffer(location.buffer_id)?;
|
||||
location.cursor.row = location.cursor.row.min(buffer.lines.len() - 1);
|
||||
location.cursor.column = location
|
||||
.cursor
|
||||
.column
|
||||
.min(grapheme_count(&buffer.lines[location.cursor.row]).saturating_sub(1));
|
||||
let pane_tab = |pane| {
|
||||
if self.pane_layout.contains(pane) {
|
||||
Some(self.tab_id)
|
||||
} else {
|
||||
self.tabs
|
||||
.iter()
|
||||
.find(|(_, tab)| tab.pane_layout.contains(pane))
|
||||
.map(|(id, _)| *id)
|
||||
}
|
||||
};
|
||||
let owner = |tab| {
|
||||
if tab == self.tab_id {
|
||||
self.diff_owner
|
||||
} else {
|
||||
self.tabs.get(&tab).and_then(|tab| tab.diff_owner)
|
||||
}
|
||||
};
|
||||
let original_pane = pane_tab(location.pane_id) == Some(location.tab_id);
|
||||
if original_pane
|
||||
&& let Some(context) = location.diff
|
||||
&& owner(location.tab_id) == Some(location.pane_id)
|
||||
&& self.tab_diff(location.tab_id).is_some_and(|diff| {
|
||||
diff.phase == Phase::View
|
||||
&& diff.comparison_id == Some(context.comparison_id)
|
||||
&& diff
|
||||
.comparison
|
||||
.as_ref()
|
||||
.and_then(|comparison| comparison.files.get(context.file))
|
||||
.is_some_and(|file| file.loaded)
|
||||
})
|
||||
{
|
||||
return Some(location);
|
||||
}
|
||||
let ordinary_pane = |pane| {
|
||||
let tab = pane_tab(pane)?;
|
||||
(owner(tab) != Some(pane)).then_some((tab, pane))
|
||||
};
|
||||
// Closed/replaced comparisons are never revived or displaced by history.
|
||||
let (tab, pane) = original_pane
|
||||
.then(|| ordinary_pane(location.pane_id))
|
||||
.flatten()
|
||||
.or_else(|| {
|
||||
self.panes.iter().find_map(|(id, pane)| {
|
||||
(pane.buffer_id == location.buffer_id)
|
||||
.then(|| ordinary_pane(*id))
|
||||
.flatten()
|
||||
})
|
||||
})
|
||||
.or_else(|| ordinary_pane(self.pane_id))
|
||||
.or_else(|| self.panes.keys().find_map(|id| ordinary_pane(*id)))?;
|
||||
location.tab_id = tab;
|
||||
location.pane_id = pane;
|
||||
location.diff = None;
|
||||
Some(location)
|
||||
}
|
||||
|
||||
fn restore_jump(&mut self, location: JumpLocation) {
|
||||
self.finish_pane_interaction();
|
||||
self.select_tab(location.tab_id);
|
||||
self.select_pane(location.pane_id);
|
||||
self.switch_buffer(location.buffer_id);
|
||||
if let Some(context) = location.diff {
|
||||
let live_buffer = context.live_buffer.filter(|id| self.has_buffer(*id));
|
||||
let diff = self.diff.as_mut().expect("resolved comparison exists");
|
||||
if diff.file != context.file {
|
||||
diff.select_file_index(context.file);
|
||||
}
|
||||
if diff.live_buffer != live_buffer {
|
||||
diff.live_buffer = live_buffer;
|
||||
diff.live_revision = None;
|
||||
}
|
||||
diff.focus = context.focus;
|
||||
if context.focus == Focus::Files {
|
||||
diff.files_visible = true;
|
||||
}
|
||||
diff.normalize_focus();
|
||||
if let Some(files) = &mut diff.files {
|
||||
files.focused = diff.files_visible && diff.focus == Focus::Files;
|
||||
}
|
||||
if context.text_focus == Focus::After && self.active.diff_snapshot.is_none() {
|
||||
diff.live_buffer = Some(self.active.id);
|
||||
}
|
||||
self.diff_bound = Some((
|
||||
context.comparison_id,
|
||||
context.file,
|
||||
context.text_focus,
|
||||
self.active.id,
|
||||
));
|
||||
self.refresh_live_diff();
|
||||
self.diff
|
||||
.as_mut()
|
||||
.expect("resolved comparison exists")
|
||||
.reveal_source_row(context.text_focus, location.cursor.row);
|
||||
}
|
||||
self.place_navigation(TextPosition {
|
||||
row: location.cursor.row,
|
||||
column: location.cursor.column,
|
||||
});
|
||||
}
|
||||
|
||||
fn traverse_jumps(&mut self, forward: bool, count: usize) {
|
||||
self.reveal_caret();
|
||||
let departure = self.jump_location();
|
||||
@@ -5807,15 +5940,9 @@ impl Editor {
|
||||
};
|
||||
let Some(next) = next else { break };
|
||||
index = next;
|
||||
let mut location = self.jumps[index];
|
||||
let Some(buffer) = self.buffer(location.buffer_id) else {
|
||||
let Some(location) = self.resolve_jump(self.jumps[index]) else {
|
||||
continue;
|
||||
};
|
||||
location.cursor.row = location.cursor.row.min(buffer.lines.len() - 1);
|
||||
location.cursor.column = location
|
||||
.cursor
|
||||
.column
|
||||
.min(grapheme_count(&buffer.lines[location.cursor.row]).saturating_sub(1));
|
||||
if location == destination {
|
||||
continue;
|
||||
}
|
||||
@@ -5832,11 +5959,7 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
self.jumps[self.jump_index] = departure;
|
||||
self.switch_buffer(destination.buffer_id);
|
||||
self.place_navigation(TextPosition {
|
||||
row: destination.cursor.row,
|
||||
column: destination.cursor.column,
|
||||
});
|
||||
self.restore_jump(destination);
|
||||
self.jump_index = index;
|
||||
}
|
||||
|
||||
@@ -5991,8 +6114,6 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
}
|
||||
self.jumps.retain(|jump| jump.buffer_id != id);
|
||||
self.jump_index = self.jump_index.min(self.jumps.len().saturating_sub(1));
|
||||
self.set_message(format!(
|
||||
"Deleted buffer {id}; active buffer {}",
|
||||
self.active.id
|
||||
@@ -11805,6 +11926,37 @@ mod tests {
|
||||
assert_eq!(editor.active.view.targets, targets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_older_jump_buffers_preserves_the_current_position_and_forward_branch() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = editor();
|
||||
for name in ["b", "c"] {
|
||||
let path = directory.path(name);
|
||||
fs::write(&path, name).unwrap();
|
||||
editor
|
||||
.navigate_to(path, TextPosition { row: 0, column: 0 })
|
||||
.unwrap();
|
||||
}
|
||||
editor.handle(Input::Ctrl('o'));
|
||||
assert_eq!(editor.context().text, "b");
|
||||
editor.execute("bdelete 1");
|
||||
editor.handle(Input::Ctrl('i'));
|
||||
assert_eq!(editor.context().text, "c");
|
||||
editor.handle(Input::Ctrl('o'));
|
||||
let path = directory.path("d");
|
||||
fs::write(&path, "d").unwrap();
|
||||
editor
|
||||
.navigate_to(path, TextPosition { row: 0, column: 0 })
|
||||
.unwrap();
|
||||
editor.handle(Input::Ctrl('i'));
|
||||
assert_eq!(editor.context().text, "d");
|
||||
type_text(&mut editor, &usize::MAX.to_string());
|
||||
editor.handle(Input::Ctrl('o'));
|
||||
assert_eq!(editor.context().text, "b");
|
||||
editor.handle(Input::Ctrl('i'));
|
||||
assert_eq!(editor.context().text, "d");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_history_clamps_edited_locations_and_preserves_dirty_text_and_undo() {
|
||||
let directory = Directory::new();
|
||||
@@ -11843,6 +11995,187 @@ mod tests {
|
||||
assert_eq!(editor.context().text, text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_history_distinguishes_tabs_and_panes_at_identical_buffer_coordinates() {
|
||||
let mut editor = editor();
|
||||
editor
|
||||
.apply(vec![Effect::Replace("shared".into())])
|
||||
.unwrap();
|
||||
let source = editor.jump_location();
|
||||
editor.execute("tab.new");
|
||||
let tab = editor.tab_id();
|
||||
let tab_pane = editor.pane_id();
|
||||
editor.record_jump(source);
|
||||
editor.execute("pane.split_vertical");
|
||||
let pane = editor.pane_id();
|
||||
editor.record_jump(JumpLocation {
|
||||
tab_id: tab,
|
||||
pane_id: tab_pane,
|
||||
..editor.jump_location()
|
||||
});
|
||||
editor.handle(Input::Ctrl('o'));
|
||||
assert_eq!((editor.tab_id(), editor.pane_id()), (tab, tab_pane));
|
||||
editor.handle(Input::Ctrl('o'));
|
||||
assert_eq!(
|
||||
(editor.tab_id(), editor.pane_id()),
|
||||
(source.tab_id, source.pane_id)
|
||||
);
|
||||
type_text(&mut editor, "2");
|
||||
editor.handle(Input::Ctrl('i'));
|
||||
assert_eq!((editor.tab_id(), editor.pane_id()), (tab, pane));
|
||||
assert_eq!(editor.context().text, "shared");
|
||||
assert!(editor.dirty());
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_history_closed_tabs_and_panes_use_surviving_ordinary_buffer_views() {
|
||||
for close in ["tab.close", "pane.close"] {
|
||||
let directory = Directory::new();
|
||||
let path = directory.path("target.txt");
|
||||
fs::write(&path, "target").unwrap();
|
||||
let mut editor = editor();
|
||||
editor.execute(if close == "tab.close" {
|
||||
"tab.new"
|
||||
} else {
|
||||
"pane.split_vertical"
|
||||
});
|
||||
let closed = (editor.tab_id(), editor.pane_id());
|
||||
editor
|
||||
.navigate_to(path.clone(), TextPosition { row: 0, column: 2 })
|
||||
.unwrap();
|
||||
editor.handle(Input::Ctrl('o'));
|
||||
editor.execute(close);
|
||||
fs::remove_file(&path).unwrap();
|
||||
editor.handle(Input::Ctrl('i'));
|
||||
assert_ne!((editor.tab_id(), editor.pane_id()), closed);
|
||||
assert_eq!(editor.cursor(), Cursor { row: 0, column: 2 });
|
||||
assert_eq!(editor.context().text, "target");
|
||||
assert_eq!(editor.tab_count(), 1);
|
||||
assert_eq!(editor.pane_layout().len(), 1);
|
||||
assert_eq!(editor.buffer_count(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_history_closed_comparison_falls_back_without_reopening_or_losing_readonly_identity() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = editor();
|
||||
historical_diff(
|
||||
&mut editor,
|
||||
&directory.0,
|
||||
vec!["before".into()],
|
||||
vec!["after".into()],
|
||||
);
|
||||
let snapshot = editor.buffer_id();
|
||||
let document = editor.active.snapshot_document_path.clone();
|
||||
let source = editor.jump_location();
|
||||
editor.execute("tab.new");
|
||||
editor.record_jump(source);
|
||||
editor.execute("tab.previous");
|
||||
editor.execute("diff.close");
|
||||
editor.execute("tab.next");
|
||||
editor.handle(Input::Ctrl('o'));
|
||||
assert_eq!(editor.tab_id(), source.tab_id);
|
||||
assert_eq!(editor.buffer_id(), snapshot);
|
||||
assert_eq!(editor.active.snapshot_document_path, document);
|
||||
assert!(editor.diff().is_none());
|
||||
type_text(&mut editor, "ix");
|
||||
assert_eq!(editor.context().text, "before");
|
||||
assert!(!editor.dirty());
|
||||
editor.handle(Input::Ctrl('i'));
|
||||
assert_eq!(editor.tab_count(), 2);
|
||||
assert_ne!(editor.tab_id(), source.tab_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_history_never_replaces_a_new_comparison_in_an_old_destination_view() {
|
||||
let directory = Directory::new();
|
||||
let target = directory.path("external.txt");
|
||||
fs::write(&target, "external").unwrap();
|
||||
let mut editor = editor();
|
||||
fs::write(directory.path("file.txt"), "working source").unwrap();
|
||||
working_diff(&mut editor, &directory.0, &["file.txt"]);
|
||||
let source_buffer = editor.buffer_id();
|
||||
editor
|
||||
.navigate_lsp_to(target, lsp_point(0, 2), None)
|
||||
.unwrap();
|
||||
let external_tab = editor.tab_id();
|
||||
let external_buffer = editor.buffer_id();
|
||||
editor.execute("tab.previous");
|
||||
editor.execute("diff.close");
|
||||
historical_diff(
|
||||
&mut editor,
|
||||
&directory.0,
|
||||
vec!["new before".into()],
|
||||
vec!["new after".into()],
|
||||
);
|
||||
let comparison = editor.diff().unwrap().comparison_id;
|
||||
let snapshot = editor.buffer_id();
|
||||
let cursor = editor.cursor();
|
||||
editor.execute("tab.next");
|
||||
editor.handle(Input::Ctrl('o'));
|
||||
assert_eq!(editor.tab_id(), external_tab);
|
||||
assert_eq!(editor.buffer_id(), source_buffer);
|
||||
assert_eq!(editor.context().text, "working source");
|
||||
assert!(editor.diff().is_none());
|
||||
editor.handle(Input::Ctrl('i'));
|
||||
assert_eq!(editor.buffer_id(), external_buffer);
|
||||
assert_eq!(editor.cursor(), Cursor { row: 0, column: 2 });
|
||||
editor.execute("tab.previous");
|
||||
assert_eq!(editor.diff().unwrap().comparison_id, comparison);
|
||||
assert_eq!(editor.buffer_id(), snapshot);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
assert_eq!(editor.context().text, "new before");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_history_skips_missing_contexts_without_mutating_the_only_comparison() {
|
||||
let directory = Directory::new();
|
||||
let path = directory.path("old.txt");
|
||||
fs::write(&path, "old").unwrap();
|
||||
let mut editor = editor();
|
||||
editor
|
||||
.navigate_to(path, TextPosition { row: 0, column: 1 })
|
||||
.unwrap();
|
||||
let source_buffer = editor.buffer_id();
|
||||
historical_diff(
|
||||
&mut editor,
|
||||
&directory.0,
|
||||
vec!["before".into()],
|
||||
vec!["after".into()],
|
||||
);
|
||||
let comparison = editor.diff().unwrap().comparison_id;
|
||||
let snapshot = editor.buffer_id();
|
||||
let cursor = editor.cursor();
|
||||
type_text(&mut editor, &usize::MAX.to_string());
|
||||
editor.handle(Input::Ctrl('o'));
|
||||
assert_eq!(editor.buffer_id(), snapshot);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
assert_eq!(editor.diff().unwrap().comparison_id, comparison);
|
||||
assert!(editor.has_buffer(source_buffer));
|
||||
assert_eq!(editor.tab_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comparison_tab_identity_includes_selector_and_parked_pane_states() {
|
||||
let mut editor = editor();
|
||||
assert!(!editor.tab_is_diff(0));
|
||||
assert!(!editor.tab_is_diff(1));
|
||||
editor.execute("diff");
|
||||
assert!(editor.tab_is_diff(0));
|
||||
editor.execute("pane.split_vertical");
|
||||
assert!(editor.diff().is_none());
|
||||
assert!(editor.tab_is_diff(0));
|
||||
editor.execute("tab.new");
|
||||
assert!(editor.tab_is_diff(0));
|
||||
assert!(!editor.tab_is_diff(1));
|
||||
editor.execute("tab.previous");
|
||||
editor.execute("diff.close");
|
||||
assert!(!editor.tab_is_diff(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_history_skips_entries_clamped_to_the_current_location() {
|
||||
let directory = Directory::new();
|
||||
|
||||
Reference in New Issue
Block a user