fix: restore tab and diff context through jump history
This commit is contained in:
@@ -37,7 +37,7 @@ Frontends inspect `count()` for the optional pending numeric prefix; parsing, co
|
|||||||
Frontends inspect `sequence_pending()` for the cached display of a pending key sequence; prefix recognition, cancellation, and dispatch remain editor-owned. Symbolic leader routes and explicit key prefixes share the same binding storage and dispatcher; explicit prefixes do not change when the leader is reconfigured.
|
Frontends inspect `sequence_pending()` for the cached display of a pending key sequence; prefix recognition, cancellation, and dispatch remain editor-owned. Symbolic leader routes and explicit key prefixes share the same binding storage and dispatcher; explicit prefixes do not change when the leader is reconfigured.
|
||||||
Frontends inspect `g_pending()` for the built-in Normal/Visual `gt`/`gb` prefix and Normal-only `gf`. Document-edge motions share multi-target motion dispatch and viewport reveal, retaining Visual anchors and selection shape. Markdown following preflights links and headings before buffer/caret mutation; an unloaded target is validated/prepared once, while loaded targets use their current unsaved text. File identity and activation stay editor-owned.
|
Frontends inspect `g_pending()` for the built-in Normal/Visual `gt`/`gb` prefix and Normal-only `gf`. Document-edge motions share multi-target motion dispatch and viewport reveal, retaining Visual anchors and selection shape. Markdown following preflights links and headings before buffer/caret mutation; an unloaded target is validated/prepared once, while loaded targets use their current unsaved text. File identity and activation stay editor-owned.
|
||||||
Frontends inspect `line_operator_pending()` for the optional Normal `d`/`c` prefix. Dedicated `dd`/`cc` actions project counted caret rows through existing linewise selection normalization, clipboard publication, and atomic range editing, without exposing a temporary Visual mode. Clipboard failures restore the original targets before returning. Repeat recipes retain the requested row count rather than the originally clamped range; change recipes share the existing Insert undo group. There is no general operator–motion parser.
|
Frontends inspect `line_operator_pending()` for the optional Normal `d`/`c` prefix. Dedicated `dd`/`cc` actions project counted caret rows through existing linewise selection normalization, clipboard publication, and atomic range editing, without exposing a temporary Visual mode. Clipboard failures restore the original targets before returning. Repeat recipes retain the requested row count rather than the originally clamped range; change recipes share the existing Insert undo group. There is no general operator–motion parser.
|
||||||
Editor-owned jump history is a bounded 100-location `VecDeque` of buffer IDs and grapheme cursors plus a current index, with no copied paths/text. Successful high-level navigation records departure/destination once; traversal uses unrecorded buffer activation and position placement, skips closed/clamped-duplicate locations, and truncates forward entries only on a distinct new jump. Session synchronization permanently invalidates pending LSP position requests after their source buffer/revision/caret/mode is left, so returning through history cannot revive an old definition reply.
|
Editor-owned jump history is a bounded 100-location `VecDeque` of stable tab/pane identities, buffer IDs, grapheme cursors, and optional comparison identity/file/focus/binding metadata, with no copied paths/text. Successful high-level navigation records departure/destination once; traversal resolves a valid destination view before switching buffers, restores comparison binding and exact cursor visibility, and truncates forward entries only on a distinct new jump. Closed or replaced views fall back only to safe existing ordinary panes; history cannot resurrect a closed comparison or replace an unrelated one. Unavailable destinations are skipped without corrupting branch position. Session synchronization permanently invalidates pending LSP position requests after their source buffer/revision/caret/mode is left, so returning through history cannot revive an old definition reply.
|
||||||
Frontends inspect `buffer_id()`, `buffer_count()`, `has_buffer(id)`, and the ascending-ID `buffers()` iterator of borrowed path/dirty/active metadata. Existing text, cursor, selection, search, and mode methods describe only the active buffer.
|
Frontends inspect `buffer_id()`, `buffer_count()`, `has_buffer(id)`, and the ascending-ID `buffers()` iterator of borrowed path/dirty/active metadata. Existing text, cursor, selection, search, and mode methods describe only the active buffer.
|
||||||
Frontends inspect `tab_id()` for stable identity, `tab_index()` for display order, `tab_count()`, and `tab_buffer(index)` for borrowed title metadata. `pane_layout()` describes only the active tab; globally unique pane IDs keep inactive tabs' viewports alive. Documents remain unique across all tabs, while independent pane/buffer views are parked and restored without copying text.
|
Frontends inspect `tab_id()` for stable identity, `tab_index()` for display order, `tab_count()`, and `tab_buffer(index)` for borrowed title metadata. `pane_layout()` describes only the active tab; globally unique pane IDs keep inactive tabs' viewports alive. Documents remain unique across all tabs, while independent pane/buffer views are parked and restored without copying text.
|
||||||
Frontends borrow `top_bar_config()` and `bottom_bar_config()` for validated ordered left/center/right segment groups and effective segment styles, and `label(name)` for session-wide custom values. The TUI owns terminal-cell widths, full-row center anchoring, clipping, and accent styling. Label effects carry only a name and plain text; they are global presentation updates, not buffer/revision-bound results or render callbacks.
|
Frontends borrow `top_bar_config()` and `bottom_bar_config()` for validated ordered left/center/right segment groups and effective segment styles, and `label(name)` for session-wide custom values. The TUI owns terminal-cell widths, full-row center anchoring, clipping, and accent styling. Label effects carry only a name and plain text; they are global presentation updates, not buffer/revision-bound results or render callbacks.
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Vamp.configure do |config|
|
|||||||
config.bar.bottom = {
|
config.bar.bottom = {
|
||||||
left: [:tabs], center: [], right: [:battery],
|
left: [:tabs], center: [], right: [:battery],
|
||||||
tabs: {
|
tabs: {
|
||||||
title: "{index} {name}{modified}", padding: 1,
|
title: "{index} {view}{name}{modified}", padding: 1,
|
||||||
leading_edge: "░▒▓", trailing_edge: "▓▒░", first_leading_edge: ""
|
leading_edge: "░▒▓", trailing_edge: "▓▒░", first_leading_edge: ""
|
||||||
},
|
},
|
||||||
battery: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
battery: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
||||||
@@ -75,7 +75,7 @@ Builtins are `:tabs`, `:battery`, `:mode`, `:path`, `:position`, `:language`, `:
|
|||||||
|
|
||||||
A bar with no visible content consumes no row. For example, `config.bar.top = { left: [], center: [], right: [] }` removes the top bar. A bar whose only visible content is `:tabs` is hidden with a single tab, whether above or below the workspace. Narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped. Commands and buffer searches use floating input; operation feedback uses a bottom-right popup. Neither reserves a footer row.
|
A bar with no visible content consumes no row. For example, `config.bar.top = { left: [], center: [], right: [] }` removes the top bar. A bar whose only visible content is `:tabs` is hidden with a single tab, whether above or below the workspace. Narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped. Commands and buffer searches use floating input; operation feedback uses a bottom-right popup. Neither reserves a footer row.
|
||||||
|
|
||||||
Each segment's `title` is a literal template supporting only `{index}`, `{name}`, `{path}`, and `{modified}`: the one-based tab index, filename, file path, and dirty marker (`" [+]"` when dirty, `""` when clean). Tabs use each tab's buffer metadata; other segments use the active buffer. Tabs default to `"{index} {name}{modified}"`; an empty title on another segment uses its builtin content or named label value. Templates are not Ruby code and never invoke a render callback.
|
Each segment's `title` is a literal template supporting only `{index}`, `{view}`, `{name}`, `{path}`, and `{modified}`: the one-based tab index, comparison-view marker, filename, file path, and dirty marker (`" [+]"` when dirty, `""` when clean). `{view}` expands to `"Diff · "` when the tab owns a comparison or revision selector, otherwise `""`. Tabs use each tab's view and buffer metadata; other segments use the active context. Tabs default to `"{index} {view}{name}{modified}"`; custom templates without `{view}` retain their output. An empty title on another segment uses its builtin content or named label value. Templates are not Ruby code and never invoke a render callback.
|
||||||
|
|
||||||
`:path` and `{path}` abbreviate the actual home-directory prefix to `~/` (`~` for the home directory itself). This is display-only: save paths, buffer identity, and LSP file URIs retain the original path. Sibling directories with similar names are not abbreviated.
|
`:path` and `{path}` abbreviate the actual home-directory prefix to `~/` (`~` for the home directory itself). This is display-only: save paths, buffer identity, and LSP file URIs retain the original path. Sibling directories with similar names are not abbreviated.
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -359,11 +359,11 @@ Space begins a leader sequence in Normal/Visual by default. Explicit sequences s
|
|||||||
|
|
||||||
## Jump history
|
## Jump history
|
||||||
|
|
||||||
Normal Ctrl+o moves back through previous locations; Ctrl+i moves forward. Traditional terminals encode Ctrl+i as Tab, so Normal Tab performs the same action while Insert Tab inserts text outside snippets. Counts work in either direction, such as `3` followed by Ctrl+o, and stop at the available history.
|
Normal Ctrl+o moves back through previous locations; Ctrl+i moves forward. Traditional terminals encode Ctrl+i as Tab, so Normal Tab performs the same action outside comparisons while Insert Tab inserts text outside snippets. In a comparison, Ctrl+i/Tab retains its diff-side focus behavior; use the **Jump forward** palette action or another configured key such as `config.bind "ctrl+y", config.core.jump_forward`. Counts work in either direction, such as `3` followed by Ctrl+o, and stop at the available history.
|
||||||
|
|
||||||
The session keeps up to 100 primary-caret locations across buffers. Successful Markdown links, accepted LSP definitions, file-picker opens, `:edit`, `:buffer`, `:bnext`/`:bprev`, and search `/`/`n`/`N` jumps record locations. Ordinary cursor/word motion, edits, failed navigation, and jumps that do not change location do not add entries. Traversal itself does not record another jump.
|
The session keeps up to 100 primary-caret locations with their tab, pane, and comparison context, not just buffer coordinates. Successful Markdown links, accepted LSP definitions, file-picker opens, `:edit`, `:buffer`, `:bnext`/`:bprev`, and search `/`/`n`/`N` jumps record locations. Back/forward restores the original view, including its comparison file and side; an external LSP definition round-trip does not replace the comparison with an ordinary buffer. Identical buffer/cursor positions in different views remain distinct. Ordinary cursor/word motion, edits, failed navigation, manual tab/pane selection, and jumps that do not change location do not add entries. Traversal itself does not record another jump.
|
||||||
|
|
||||||
Moving away from a visited point before traversing saves the actual departure position for the return trip. A successful new jump after going back discards the forward branch; failed/no-op jumps preserve it. Closed buffers are skipped rather than reopened. Stored row/grapheme coordinates clamp to the current text after edits; text, dirty state, undo/redo, and file contents are unchanged by traversal. History navigation returns to Normal with one primary caret.
|
Moving away from a visited point before traversing saves the actual departure position for the return trip. A successful new jump after going back discards the forward branch; failed/no-op jumps preserve it. Closed buffers are skipped rather than reopened. An unavailable or replaced comparison is never resurrected or overwritten: history uses a safe existing ordinary pane for an available buffer, retaining snapshot read-only identity, or skips the destination if none exists. It never creates a replacement tab. Stored row/grapheme coordinates clamp to current text after edits; text, dirty state, undo/redo, and file contents remain unchanged. History navigation returns to Normal with one primary caret.
|
||||||
|
|
||||||
The typed actions are `config.core.jump_back` and `config.core.jump_forward`; custom bindings can use any supported single key or leader sequence. History is in memory for this session only.
|
The typed actions are `config.core.jump_back` and `config.core.jump_forward`; custom bindings can use any supported single key or leader sequence. History is in memory for this session only.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ Vamp.configure do |config|
|
|||||||
config.bar.bottom = {
|
config.bar.bottom = {
|
||||||
left: [:tabs], center: [], right: [:battery],
|
left: [:tabs], center: [], right: [:battery],
|
||||||
tabs: {
|
tabs: {
|
||||||
title: "{index} {name}{modified}", padding: 1,
|
title: "{index} {view}{name}{modified}", padding: 1,
|
||||||
leading_edge: "░▒▓", trailing_edge: "▓▒░", first_leading_edge: ""
|
leading_edge: "░▒▓", trailing_edge: "▓▒░", first_leading_edge: ""
|
||||||
},
|
},
|
||||||
battery: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
battery: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
||||||
|
|||||||
+38
-3
@@ -205,7 +205,7 @@ fn default_segment(name: &str) -> SegmentConfig {
|
|||||||
segment.trailing_edge = "▓▒░".into();
|
segment.trailing_edge = "▓▒░".into();
|
||||||
}
|
}
|
||||||
if name == "tabs" {
|
if name == "tabs" {
|
||||||
segment.title = "{index} {name}{modified}".into();
|
segment.title = "{index} {view}{name}{modified}".into();
|
||||||
segment.first_leading_edge = Some(String::new());
|
segment.first_leading_edge = Some(String::new());
|
||||||
}
|
}
|
||||||
segment
|
segment
|
||||||
@@ -234,9 +234,9 @@ fn validate_title(title: &str) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
let end = rest.find('}').ok_or("title contains an unmatched '{'")?;
|
let end = rest.find('}').ok_or("title contains an unmatched '{'")?;
|
||||||
let placeholder = &rest[1..end];
|
let placeholder = &rest[1..end];
|
||||||
if !matches!(placeholder, "index" | "name" | "path" | "modified") {
|
if !matches!(placeholder, "index" | "view" | "name" | "path" | "modified") {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"unknown title placeholder {{{placeholder}}}; expected index, name, path, or modified"
|
"unknown title placeholder {{{placeholder}}}; expected index, view, name, path, or modified"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
rest = &rest[end + 1..];
|
rest = &rest[end + 1..];
|
||||||
@@ -480,6 +480,41 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn view_placeholder_is_valid_in_every_segment_and_bar() {
|
||||||
|
for name in [
|
||||||
|
"tabs",
|
||||||
|
"mode",
|
||||||
|
"pending",
|
||||||
|
"buffer",
|
||||||
|
"diagnostics",
|
||||||
|
"path",
|
||||||
|
"selections",
|
||||||
|
"count",
|
||||||
|
"language",
|
||||||
|
"lsp",
|
||||||
|
"position",
|
||||||
|
"battery",
|
||||||
|
"workspace",
|
||||||
|
] {
|
||||||
|
let options = serde_json::json!({
|
||||||
|
"left": [name],
|
||||||
|
(name): {"title": "{index}:{view}{name}:{path}{modified}"}
|
||||||
|
});
|
||||||
|
let lines: Lines = serde_json::from_value(serde_json::json!({
|
||||||
|
"top": options,
|
||||||
|
"bottom": options,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
for line in [&lines.top, &lines.bottom] {
|
||||||
|
assert_eq!(
|
||||||
|
line.segment(name).title,
|
||||||
|
"{index}:{view}{name}:{path}{modified}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn malformed_chrome_is_rejected_at_the_boundary() {
|
fn malformed_chrome_is_rejected_at_the_boundary() {
|
||||||
for options in [
|
for options 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)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
struct JumpLocation {
|
struct JumpLocation {
|
||||||
|
tab_id: usize,
|
||||||
|
pane_id: usize,
|
||||||
buffer_id: usize,
|
buffer_id: usize,
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
|
diff: Option<DiffJumpLocation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PendingLspNavigation {
|
struct PendingLspNavigation {
|
||||||
@@ -1367,6 +1379,7 @@ pub struct Editor {
|
|||||||
diff_owner: Option<usize>,
|
diff_owner: Option<usize>,
|
||||||
parked_diff: Option<DiffState>,
|
parked_diff: Option<DiffState>,
|
||||||
diff_pane_bounds: [(Focus, PaneRect); 3],
|
diff_pane_bounds: [(Focus, PaneRect); 3],
|
||||||
|
// Closed destinations stay until bounded eviction so they cannot shift jump_index.
|
||||||
jumps: VecDeque<JumpLocation>,
|
jumps: VecDeque<JumpLocation>,
|
||||||
jump_index: usize,
|
jump_index: usize,
|
||||||
clipboard: Vec<String>,
|
clipboard: Vec<String>,
|
||||||
@@ -1601,6 +1614,12 @@ impl Editor {
|
|||||||
.map(|buffer| buffer.info(id == self.tab_id))
|
.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> {
|
pub(crate) fn tab_diff(&self, id: usize) -> Option<&DiffState> {
|
||||||
if id == self.tab_id {
|
if id == self.tab_id {
|
||||||
self.workspace_diff()
|
self.workspace_diff()
|
||||||
@@ -1979,8 +1998,6 @@ impl Editor {
|
|||||||
for pane in self.panes.values_mut() {
|
for pane in self.panes.values_mut() {
|
||||||
pane.views.remove(&id);
|
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) {
|
fn close_tab(&mut self, force: bool) {
|
||||||
@@ -2441,10 +2458,6 @@ impl Editor {
|
|||||||
pane.views
|
pane.views
|
||||||
.retain(|id, _| *id == self.active.id || self.parked.contains_key(id));
|
.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_bound = None;
|
||||||
self.diff_return_buffer = None;
|
self.diff_return_buffer = None;
|
||||||
}
|
}
|
||||||
@@ -5767,9 +5780,22 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn jump_location(&self) -> JumpLocation {
|
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 {
|
JumpLocation {
|
||||||
|
tab_id: self.tab_id,
|
||||||
|
pane_id: self.pane_id,
|
||||||
buffer_id: self.active.id,
|
buffer_id: self.active.id,
|
||||||
cursor: self.cursor(),
|
cursor: self.cursor(),
|
||||||
|
diff,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5791,6 +5817,113 @@ impl Editor {
|
|||||||
self.jump_index = self.jumps.len() - 1;
|
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) {
|
fn traverse_jumps(&mut self, forward: bool, count: usize) {
|
||||||
self.reveal_caret();
|
self.reveal_caret();
|
||||||
let departure = self.jump_location();
|
let departure = self.jump_location();
|
||||||
@@ -5807,15 +5940,9 @@ impl Editor {
|
|||||||
};
|
};
|
||||||
let Some(next) = next else { break };
|
let Some(next) = next else { break };
|
||||||
index = next;
|
index = next;
|
||||||
let mut location = self.jumps[index];
|
let Some(location) = self.resolve_jump(self.jumps[index]) else {
|
||||||
let Some(buffer) = self.buffer(location.buffer_id) else {
|
|
||||||
continue;
|
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 {
|
if location == destination {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -5832,11 +5959,7 @@ impl Editor {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.jumps[self.jump_index] = departure;
|
self.jumps[self.jump_index] = departure;
|
||||||
self.switch_buffer(destination.buffer_id);
|
self.restore_jump(destination);
|
||||||
self.place_navigation(TextPosition {
|
|
||||||
row: destination.cursor.row,
|
|
||||||
column: destination.cursor.column,
|
|
||||||
});
|
|
||||||
self.jump_index = index;
|
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!(
|
self.set_message(format!(
|
||||||
"Deleted buffer {id}; active buffer {}",
|
"Deleted buffer {id}; active buffer {}",
|
||||||
self.active.id
|
self.active.id
|
||||||
@@ -11805,6 +11926,37 @@ mod tests {
|
|||||||
assert_eq!(editor.active.view.targets, targets);
|
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]
|
#[test]
|
||||||
fn jump_history_clamps_edited_locations_and_preserves_dirty_text_and_undo() {
|
fn jump_history_clamps_edited_locations_and_preserves_dirty_text_and_undo() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
@@ -11843,6 +11995,187 @@ mod tests {
|
|||||||
assert_eq!(editor.context().text, text);
|
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]
|
#[test]
|
||||||
fn jump_history_skips_entries_clamped_to_the_current_location() {
|
fn jump_history_skips_entries_clamped_to_the_current_location() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
|
|||||||
+105
-7
@@ -1057,6 +1057,7 @@ fn chrome_title(
|
|||||||
index: usize,
|
index: usize,
|
||||||
path: Option<&std::path::Path>,
|
path: Option<&std::path::Path>,
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
|
is_diff: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
let name = path
|
let name = path
|
||||||
.map(|path| {
|
.map(|path| {
|
||||||
@@ -1075,6 +1076,7 @@ fn chrome_title(
|
|||||||
};
|
};
|
||||||
match field {
|
match field {
|
||||||
"index" => write!(title, "{index}").unwrap(),
|
"index" => write!(title, "{index}").unwrap(),
|
||||||
|
"view" if is_diff => title.push_str("Diff · "),
|
||||||
"name" => title.push_str(&name),
|
"name" => title.push_str(&name),
|
||||||
"path" => title.push_str(&path),
|
"path" => title.push_str(&path),
|
||||||
"modified" if dirty => title.push_str(" [+]"),
|
"modified" if dirty => title.push_str(" [+]"),
|
||||||
@@ -1109,6 +1111,7 @@ fn chrome_items<'a>(
|
|||||||
index + 1,
|
index + 1,
|
||||||
buffer.as_ref().and_then(|buffer| buffer.path),
|
buffer.as_ref().and_then(|buffer| buffer.path),
|
||||||
buffer.is_some_and(|buffer| buffer.dirty),
|
buffer.is_some_and(|buffer| buffer.dirty),
|
||||||
|
editor.tab_is_diff(index),
|
||||||
)),
|
)),
|
||||||
config,
|
config,
|
||||||
tab: Some(index),
|
tab: Some(index),
|
||||||
@@ -1133,6 +1136,7 @@ fn chrome_items<'a>(
|
|||||||
editor.tab_index() + 1,
|
editor.tab_index() + 1,
|
||||||
editor.path(),
|
editor.path(),
|
||||||
editor.dirty(),
|
editor.dirty(),
|
||||||
|
editor.tab_is_diff(editor.tab_index()),
|
||||||
)),
|
)),
|
||||||
"position" => Cow::Owned(format!(
|
"position" => Cow::Owned(format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
@@ -1207,6 +1211,7 @@ fn chrome_items<'a>(
|
|||||||
editor.tab_index() + 1,
|
editor.tab_index() + 1,
|
||||||
editor.path(),
|
editor.path(),
|
||||||
editor.dirty(),
|
editor.dirty(),
|
||||||
|
editor.tab_is_diff(editor.tab_index()),
|
||||||
));
|
));
|
||||||
} else if text.chars().any(char::is_control) {
|
} else if text.chars().any(char::is_control) {
|
||||||
text = Cow::Owned(display_text(&text));
|
text = Cow::Owned(display_text(&text));
|
||||||
@@ -5574,7 +5579,7 @@ mod tests {
|
|||||||
let path = directory.file("λ\t{index}.rs", "source");
|
let path = directory.file("λ\t{index}.rs", "source");
|
||||||
let editor = Editor::open(Some(path.clone()), vec![]).unwrap();
|
let editor = Editor::open(Some(path.clone()), vec![]).unwrap();
|
||||||
let groups = chrome_groups(&editor, editor.bottom_bar_config(), &SyntaxCache::new());
|
let groups = chrome_groups(&editor, editor.bottom_bar_config(), &SyntaxCache::new());
|
||||||
let title = chrome_title("{path}", 2, Some(&path), false);
|
let title = chrome_title("{path}", 2, Some(&path), false, false);
|
||||||
assert_eq!(title, display_text(&display_path(&path)));
|
assert_eq!(title, display_text(&display_path(&path)));
|
||||||
assert!(groups.iter().flatten().any(|item| item.text == title));
|
assert!(groups.iter().flatten().any(|item| item.text == title));
|
||||||
assert_eq!(editor.path(), Some(path.as_path()));
|
assert_eq!(editor.path(), Some(path.as_path()));
|
||||||
@@ -6129,6 +6134,105 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comparison_titles_distinguish_same_named_tabs_and_follow_current_context() {
|
||||||
|
let directory = Directory::new();
|
||||||
|
let path = directory.file("source.rs", "fn main() {}");
|
||||||
|
for phase in ["loading", "before", "after", "view"] {
|
||||||
|
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
|
||||||
|
editor.execute("tab.new");
|
||||||
|
editor.execute("diff");
|
||||||
|
if phase != "loading" {
|
||||||
|
let (id, _) = editor.take_diff_request().unwrap();
|
||||||
|
editor.apply_diff_result(id, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
|
||||||
|
}
|
||||||
|
if matches!(phase, "after" | "view") {
|
||||||
|
editor.handle(Input::Down);
|
||||||
|
editor.handle(Input::Enter);
|
||||||
|
}
|
||||||
|
if phase == "view" {
|
||||||
|
editor.handle(Input::Down);
|
||||||
|
editor.handle(Input::Enter);
|
||||||
|
let (id, _) = editor.take_diff_request().unwrap();
|
||||||
|
editor.apply_diff_result(
|
||||||
|
id,
|
||||||
|
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||||
|
root: directory.0.clone(),
|
||||||
|
editable_paths: Default::default(),
|
||||||
|
before: "baseline".into(),
|
||||||
|
after: "historic".into(),
|
||||||
|
files: vec![syntax_diff_file("source.rs", "fn old() {}", "fn main() {}")],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let default_tabs = editor.top_bar_config().segment("tabs").clone();
|
||||||
|
for (template, ordinary, comparison, marker_count) in [
|
||||||
|
(None, "1 source.rs", "2 Diff · source.rs", 1),
|
||||||
|
(
|
||||||
|
Some("{index} {name}{modified}"),
|
||||||
|
"1 source.rs",
|
||||||
|
"2 source.rs",
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Some("T{index}:{view}{name}{modified}"),
|
||||||
|
"T1:source.rs",
|
||||||
|
"T2:Diff · source.rs",
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
configure_chrome(&mut editor, |top, bottom| {
|
||||||
|
top.segments.insert("tabs".into(), default_tabs.clone());
|
||||||
|
if let Some(template) = template {
|
||||||
|
top.segments.get_mut("tabs").unwrap().title = template.into();
|
||||||
|
}
|
||||||
|
bottom.left = vec!["mode".into()];
|
||||||
|
bottom.center.clear();
|
||||||
|
bottom.right.clear();
|
||||||
|
bottom.segments.get_mut("mode").unwrap().title = "Current:{view}{name}".into();
|
||||||
|
});
|
||||||
|
for comparison_active in [true, false] {
|
||||||
|
let mut terminal =
|
||||||
|
Terminal::new(ratatui::backend::TestBackend::new(160, 2)).unwrap();
|
||||||
|
terminal
|
||||||
|
.draw(|frame| {
|
||||||
|
for (line, y) in [
|
||||||
|
(editor.top_bar_config(), 0),
|
||||||
|
(editor.bottom_bar_config(), 1),
|
||||||
|
] {
|
||||||
|
draw_chrome(
|
||||||
|
frame,
|
||||||
|
chrome_groups(&editor, line, &SyntaxCache::new()),
|
||||||
|
Rect::new(0, y, 160, 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
|
||||||
|
assert!(titles.contains(ordinary), "{phase}: {titles}");
|
||||||
|
assert!(titles.contains(comparison), "{phase}: {titles}");
|
||||||
|
assert_eq!(
|
||||||
|
titles.matches("Diff · ").count(),
|
||||||
|
marker_count,
|
||||||
|
"{phase}: {titles}"
|
||||||
|
);
|
||||||
|
let current = pane_screen(&terminal, Rect::new(0, 1, 160, 1));
|
||||||
|
let expected = if comparison_active {
|
||||||
|
"Current:Diff · source.rs"
|
||||||
|
} else {
|
||||||
|
"Current:source.rs"
|
||||||
|
};
|
||||||
|
assert!(current.contains(expected), "{phase}: {current}");
|
||||||
|
editor.execute(if comparison_active {
|
||||||
|
"tab.previous"
|
||||||
|
} else {
|
||||||
|
"tab.next"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tab_titles_update_templates_without_reinterpreting_file_names() {
|
fn tab_titles_update_templates_without_reinterpreting_file_names() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
@@ -6158,12 +6262,6 @@ mod tests {
|
|||||||
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
|
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
|
||||||
assert!(titles.contains("T1:λ{index}�.rs [+]"), "{titles}");
|
assert!(titles.contains("T1:λ{index}�.rs [+]"), "{titles}");
|
||||||
assert!(titles.contains("T2:λ{index}�.rs [+]"), "{titles}");
|
assert!(titles.contains("T2:λ{index}�.rs [+]"), "{titles}");
|
||||||
let buffer = terminal.backend().buffer();
|
|
||||||
assert_eq!(buffer[(0, 0)].bg, Color::Reset);
|
|
||||||
let active = (0..159)
|
|
||||||
.find(|&x| buffer[(x, 0)].symbol() == "T" && buffer[(x + 1, 0)].symbol() == "2")
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(buffer[(active, 0)].bg, Color::Cyan);
|
|
||||||
|
|
||||||
configure_chrome(&mut editor, |top, _| {
|
configure_chrome(&mut editor, |top, _| {
|
||||||
top.segments.get_mut("tabs").unwrap().title = "{index}:{path}{modified}".into();
|
top.segments.get_mut("tabs").unwrap().title = "{index}:{path}{modified}".into();
|
||||||
|
|||||||
+176
-2
@@ -682,6 +682,15 @@ fn shown_diff_lines(session: &EditorSession, focus: Focus) -> Vec<usize> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn palette_action(session: &mut EditorSession, action: &str) {
|
||||||
|
session.handle(Input::Ctrl('p'));
|
||||||
|
for ch in action.chars() {
|
||||||
|
session.handle(Input::Char(ch));
|
||||||
|
}
|
||||||
|
session.handle(Input::Enter);
|
||||||
|
assert!(session.editor().palette().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
impl Drop for Fixture {
|
impl Drop for Fixture {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = fs::remove_dir_all(&self.root);
|
let _ = fs::remove_dir_all(&self.root);
|
||||||
@@ -3630,6 +3639,10 @@ fn lsp_diff_definition_routes_exact_changed_lines_in_the_same_comparison() {
|
|||||||
open_comparison(&mut session, &fixture, &before, after.as_deref(), focus);
|
open_comparison(&mut session, &fixture, &before, after.as_deref(), focus);
|
||||||
let tab = session.editor().tab_id();
|
let tab = session.editor().tab_id();
|
||||||
let comparison = session.editor().diff().unwrap().comparison_id;
|
let comparison = session.editor().diff().unwrap().comparison_id;
|
||||||
|
let source_buffer = session.editor().buffer_id();
|
||||||
|
let source_cursor = session.editor().cursor();
|
||||||
|
let source_file = session.editor().diff().unwrap().file;
|
||||||
|
let source_excerpts = shown_diff_lines(&session, focus);
|
||||||
let files = &session
|
let files = &session
|
||||||
.editor()
|
.editor()
|
||||||
.diff()
|
.diff()
|
||||||
@@ -3691,6 +3704,125 @@ fn lsp_diff_definition_routes_exact_changed_lines_in_the_same_comparison() {
|
|||||||
);
|
);
|
||||||
assert_eq!(session.editor().diff().unwrap().comparison_id, comparison);
|
assert_eq!(session.editor().diff().unwrap().comparison_id, comparison);
|
||||||
assert_eq!(session.editor().tab_count(), 1);
|
assert_eq!(session.editor().tab_count(), 1);
|
||||||
|
session.handle(Input::Ctrl('o'));
|
||||||
|
assert_eq!(session.editor().cursor(), Cursor { row, column: 3 });
|
||||||
|
assert_eq!(session.editor().diff().unwrap().file, target_index);
|
||||||
|
session.handle(Input::Ctrl('o'));
|
||||||
|
assert_eq!(session.editor().buffer_id(), source_buffer);
|
||||||
|
assert_eq!(session.editor().cursor(), source_cursor);
|
||||||
|
assert_eq!(session.editor().diff().unwrap().file, source_file);
|
||||||
|
assert_eq!(session.editor().diff().unwrap().focus, focus);
|
||||||
|
assert_eq!(shown_diff_lines(&session, focus), source_excerpts);
|
||||||
|
palette_action(&mut session, "core.jump_forward");
|
||||||
|
assert_eq!(session.editor().cursor(), Cursor { row, column: 3 });
|
||||||
|
assert_eq!(session.editor().diff().unwrap().file, target_index);
|
||||||
|
palette_action(&mut session, "core.jump_forward");
|
||||||
|
assert_eq!(
|
||||||
|
session.editor().cursor(),
|
||||||
|
Cursor {
|
||||||
|
row: same_row,
|
||||||
|
column: 2
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(session.editor().diff().unwrap().comparison_id, comparison);
|
||||||
|
assert_eq!(session.editor().tab_id(), tab);
|
||||||
|
assert_eq!(session.editor().tab_count(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lsp_diff_external_definition_jump_history_restores_the_exact_comparison_view() {
|
||||||
|
for (historical, focus) in [
|
||||||
|
(false, Focus::Before),
|
||||||
|
(false, Focus::After),
|
||||||
|
(true, Focus::Before),
|
||||||
|
(true, Focus::After),
|
||||||
|
] {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let external = Fixture::new();
|
||||||
|
let (before, after) = diff_repository(&fixture, historical);
|
||||||
|
let mut session = fixture.open("controlled-diff");
|
||||||
|
session.execute(&format!("edit {}", external.source.display()));
|
||||||
|
session.handle(Input::Char('i'));
|
||||||
|
session.handle(Input::Paste("dirty ".into()));
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
let external_buffer = session.editor().buffer_id();
|
||||||
|
let external_text = session.editor().context().text;
|
||||||
|
session.execute("buffer 1");
|
||||||
|
let sibling = session.editor().pane_id();
|
||||||
|
let sibling_buffer = session.editor().buffer_id();
|
||||||
|
session.execute("pane.split_vertical");
|
||||||
|
open_comparison(&mut session, &fixture, &before, after.as_deref(), focus);
|
||||||
|
if !historical && focus == Focus::After {
|
||||||
|
session.handle(Input::Char('A'));
|
||||||
|
session.handle(Input::Char('!'));
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
}
|
||||||
|
let source_tab = session.editor().tab_id();
|
||||||
|
let source_pane = session.editor().pane_id();
|
||||||
|
let source_buffer = session.editor().buffer_id();
|
||||||
|
let source_cursor = session.editor().cursor();
|
||||||
|
let source_text = session.editor().context().text;
|
||||||
|
let source_dirty = session.editor().dirty();
|
||||||
|
let layout = format!("{:?}", session.editor().pane_layout());
|
||||||
|
let diff = session.editor().diff().unwrap();
|
||||||
|
let comparison = diff.comparison_id;
|
||||||
|
let file = diff.file;
|
||||||
|
let excerpts = shown_diff_lines(&session, focus);
|
||||||
|
request_position(&mut session, &fixture, "textDocument/definition");
|
||||||
|
position_reply(
|
||||||
|
&mut session,
|
||||||
|
&fixture,
|
||||||
|
1,
|
||||||
|
json!({
|
||||||
|
"action": "definition-result", "path": external.source, "row": 0, "column": 11
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let external_tab = session.editor().tab_id();
|
||||||
|
let external_pane = session.editor().pane_id();
|
||||||
|
assert_ne!(external_tab, source_tab);
|
||||||
|
assert_eq!(session.editor().buffer_id(), external_buffer);
|
||||||
|
for _ in 0..2 {
|
||||||
|
session.handle(Input::Ctrl('o'));
|
||||||
|
assert_eq!(session.editor().tab_id(), source_tab);
|
||||||
|
assert_eq!(session.editor().pane_id(), source_pane);
|
||||||
|
assert_eq!(session.editor().buffer_id(), source_buffer);
|
||||||
|
assert_eq!(session.editor().cursor(), source_cursor);
|
||||||
|
assert_eq!(session.editor().context().text, source_text);
|
||||||
|
assert_eq!(session.editor().dirty(), source_dirty);
|
||||||
|
assert_eq!(format!("{:?}", session.editor().pane_layout()), layout);
|
||||||
|
assert_eq!(session.editor().diff().unwrap().comparison_id, comparison);
|
||||||
|
assert_eq!(session.editor().diff().unwrap().file, file);
|
||||||
|
assert_eq!(session.editor().diff().unwrap().focus, focus);
|
||||||
|
assert_eq!(shown_diff_lines(&session, focus), excerpts);
|
||||||
|
assert_eq!(
|
||||||
|
session.editor().pane_buffer_id(sibling),
|
||||||
|
Some(sibling_buffer)
|
||||||
|
);
|
||||||
|
palette_action(&mut session, "core.jump_forward");
|
||||||
|
assert_eq!(session.editor().tab_id(), external_tab);
|
||||||
|
assert_eq!(session.editor().pane_id(), external_pane);
|
||||||
|
assert_eq!(session.editor().buffer_id(), external_buffer);
|
||||||
|
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 9 });
|
||||||
|
assert_eq!(session.editor().context().text, external_text);
|
||||||
|
assert!(session.editor().dirty());
|
||||||
|
assert!(session.editor().diff().is_none());
|
||||||
|
assert_eq!(session.editor().tab_count(), 2);
|
||||||
|
assert!(session.editor().tab_is_diff(0));
|
||||||
|
assert!(!session.editor().tab_is_diff(1));
|
||||||
|
}
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().context().text, "A😀e\u{301}Z\n");
|
||||||
|
session.execute("tab.previous");
|
||||||
|
assert_eq!(session.editor().diff().unwrap().comparison_id, comparison);
|
||||||
|
assert_eq!(session.editor().cursor(), source_cursor);
|
||||||
|
if source_dirty {
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(
|
||||||
|
session.editor().context().text,
|
||||||
|
comparison_text("source", "after")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3711,6 +3843,18 @@ fn lsp_diff_unchanged_context_and_omitted_symbols_open_shared_ordinary_tabs() {
|
|||||||
] {
|
] {
|
||||||
session.handle(input);
|
session.handle(input);
|
||||||
}
|
}
|
||||||
|
if row == 18 {
|
||||||
|
for input in [
|
||||||
|
Input::Char('g'),
|
||||||
|
Input::Char('t'),
|
||||||
|
Input::Right,
|
||||||
|
Input::Right,
|
||||||
|
Input::Right,
|
||||||
|
] {
|
||||||
|
session.handle(input);
|
||||||
|
}
|
||||||
|
assert_eq!(session.editor().cursor(), Cursor { row, column: 3 });
|
||||||
|
}
|
||||||
let text = session.editor().context().text;
|
let text = session.editor().context().text;
|
||||||
let source = session.editor().buffer_id();
|
let source = session.editor().buffer_id();
|
||||||
let cursor = session.editor().cursor();
|
let cursor = session.editor().cursor();
|
||||||
@@ -3729,6 +3873,7 @@ fn lsp_diff_unchanged_context_and_omitted_symbols_open_shared_ordinary_tabs() {
|
|||||||
"action": "definition-result", "path": fixture.source, "row": row, "column": 5
|
"action": "definition-result", "path": fixture.source, "row": row, "column": 5
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
let ordinary_tab = session.editor().tab_id();
|
||||||
assert_eq!(session.editor().tab_count(), 2);
|
assert_eq!(session.editor().tab_count(), 2);
|
||||||
assert_ne!(session.editor().tab_id(), tab);
|
assert_ne!(session.editor().tab_id(), tab);
|
||||||
assert!(session.editor().diff().is_none());
|
assert!(session.editor().diff().is_none());
|
||||||
@@ -3736,13 +3881,22 @@ fn lsp_diff_unchanged_context_and_omitted_symbols_open_shared_ordinary_tabs() {
|
|||||||
assert_eq!(session.editor().cursor(), Cursor { row, column: 3 });
|
assert_eq!(session.editor().cursor(), Cursor { row, column: 3 });
|
||||||
assert_eq!(session.editor().context().text, text);
|
assert_eq!(session.editor().context().text, text);
|
||||||
assert!(session.editor().dirty());
|
assert!(session.editor().dirty());
|
||||||
session.execute("tab.previous");
|
session.handle(Input::Ctrl('o'));
|
||||||
assert_eq!(session.editor().tab_id(), tab);
|
assert_eq!(session.editor().tab_id(), tab);
|
||||||
assert_eq!(session.editor().pane_id(), pane);
|
assert_eq!(session.editor().pane_id(), pane);
|
||||||
assert_eq!(format!("{:?}", session.editor().pane_layout()), layout);
|
assert_eq!(format!("{:?}", session.editor().pane_layout()), layout);
|
||||||
assert_eq!(session.editor().cursor(), cursor);
|
assert_eq!(session.editor().cursor(), cursor);
|
||||||
assert_eq!(session.editor().diff().unwrap().comparison_id, comparison);
|
assert_eq!(session.editor().diff().unwrap().comparison_id, comparison);
|
||||||
assert_eq!(shown_diff_lines(&session, Focus::After), excerpts);
|
assert_eq!(shown_diff_lines(&session, Focus::After), excerpts);
|
||||||
|
palette_action(&mut session, "core.jump_forward");
|
||||||
|
assert_eq!(session.editor().tab_id(), ordinary_tab);
|
||||||
|
assert_eq!(session.editor().buffer_id(), source);
|
||||||
|
assert_eq!(session.editor().cursor(), Cursor { row, column: 3 });
|
||||||
|
assert!(session.editor().diff().is_none());
|
||||||
|
assert_eq!(session.editor().tab_count(), 2);
|
||||||
|
session.handle(Input::Ctrl('o'));
|
||||||
|
assert_eq!(session.editor().tab_id(), tab);
|
||||||
|
assert_eq!(session.editor().cursor(), cursor);
|
||||||
session.handle(Input::Char('u'));
|
session.handle(Input::Char('u'));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
session.editor().context().text,
|
session.editor().context().text,
|
||||||
@@ -3817,6 +3971,7 @@ fn lsp_diff_unchanged_snapshot_uri_opens_an_immutable_tab_that_outlives_the_comp
|
|||||||
let snapshot = session.editor().buffer_id();
|
let snapshot = session.editor().buffer_id();
|
||||||
let comparison = session.editor().diff().unwrap().comparison_id;
|
let comparison = session.editor().diff().unwrap().comparison_id;
|
||||||
let cursor = session.editor().cursor();
|
let cursor = session.editor().cursor();
|
||||||
|
let source_tab = session.editor().tab_id();
|
||||||
request_position(&mut session, &fixture, "textDocument/definition");
|
request_position(&mut session, &fixture, "textDocument/definition");
|
||||||
position_reply(
|
position_reply(
|
||||||
&mut session,
|
&mut session,
|
||||||
@@ -3826,6 +3981,7 @@ fn lsp_diff_unchanged_snapshot_uri_opens_an_immutable_tab_that_outlives_the_comp
|
|||||||
"action": "definition-result", "uri": document["uri"], "row": 39, "column": 5
|
"action": "definition-result", "uri": document["uri"], "row": 39, "column": 5
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
let ordinary_tab = session.editor().tab_id();
|
||||||
assert_eq!(session.editor().tab_count(), 2);
|
assert_eq!(session.editor().tab_count(), 2);
|
||||||
assert!(session.editor().diff().is_none());
|
assert!(session.editor().diff().is_none());
|
||||||
assert_eq!(session.editor().buffer_id(), snapshot);
|
assert_eq!(session.editor().buffer_id(), snapshot);
|
||||||
@@ -3834,11 +3990,29 @@ fn lsp_diff_unchanged_snapshot_uri_opens_an_immutable_tab_that_outlives_the_comp
|
|||||||
session.editor().context().text,
|
session.editor().context().text,
|
||||||
comparison_text("source", version)
|
comparison_text("source", version)
|
||||||
);
|
);
|
||||||
session.execute("tab.previous");
|
session.handle(Input::Ctrl('o'));
|
||||||
|
assert_eq!(session.editor().tab_id(), source_tab);
|
||||||
assert_eq!(session.editor().cursor(), cursor);
|
assert_eq!(session.editor().cursor(), cursor);
|
||||||
assert_eq!(session.editor().diff().unwrap().comparison_id, comparison);
|
assert_eq!(session.editor().diff().unwrap().comparison_id, comparison);
|
||||||
|
palette_action(&mut session, "core.jump_forward");
|
||||||
|
assert_eq!(session.editor().tab_id(), ordinary_tab);
|
||||||
|
assert_eq!(session.editor().cursor(), Cursor { row: 39, column: 3 });
|
||||||
|
assert!(session.editor().diff().is_none());
|
||||||
|
// Close the source without traversing away from the timeline's destination.
|
||||||
|
session.execute("tab.previous");
|
||||||
session.execute("diff.close");
|
session.execute("diff.close");
|
||||||
session.execute("tab.next");
|
session.execute("tab.next");
|
||||||
|
session.handle(Input::Ctrl('o'));
|
||||||
|
assert_eq!(session.editor().tab_id(), source_tab);
|
||||||
|
assert_eq!(session.editor().buffer_id(), snapshot);
|
||||||
|
assert_eq!(session.editor().cursor(), cursor);
|
||||||
|
assert!(session.editor().diff().is_none());
|
||||||
|
palette_action(&mut session, "core.jump_forward");
|
||||||
|
assert_eq!(session.editor().tab_id(), ordinary_tab);
|
||||||
|
assert_eq!(session.editor().cursor(), Cursor { row: 39, column: 3 });
|
||||||
|
assert_eq!(session.editor().tab_count(), 2);
|
||||||
|
assert!(!session.editor().tab_is_diff(0));
|
||||||
|
assert!(!session.editor().tab_is_diff(1));
|
||||||
for input in [
|
for input in [
|
||||||
Input::Char('i'),
|
Input::Char('i'),
|
||||||
Input::Paste("bad".into()),
|
Input::Paste("bad".into()),
|
||||||
|
|||||||
Reference in New Issue
Block a user