From 1ded60a660046b150d4a240d5a8528ef38ae6e2d Mon Sep 17 00:00:00 2001 From: Carl Jackson Date: Thu, 15 Jan 2026 09:48:15 -0800 Subject: [PATCH] Implement Vim's tag stack (#46002) Happy New Years! This PR is a second take at https://github.com/zed-industries/zed/pull/38127 (cc @ConradIrwin) This PR is significantly less complicated than the last attempt: while we still keep our data on the `NavigationHistory` object, we no longer tightly integrate it with the existing back/forward "browser history." Instead, we keep our own stack of `(origin, target)` pairs (in a struct to make it easy to extend with e.g., tag names in the future). The PR is split into two separable commits. Most of the implementation is in the second commit, which: - Defines the stack data structure - Implements `pane::GoToOlderTag` and `pane::GoToNewerTag` in terms of the stack - Hooks into `navigate_to_hover_links` to push tag stack entries This last bit is the most fiddly. The core challenge is that we need to keep track of the `origin` location and calculate the `target` location across three codepaths that might involve creating a new editor and/or splitting the pane. One thing in particular I found difficult was that an editor's `nav_history` (an `ItemNavHistory`) seems to be populated asynchronously. Instead of relying on it, I decided in this code to make my own `ItemNavHistory`. I briefly tried to refactor the code in question, but it seemed like it would significantly increase the scope of the change. I prefer this all-in-one tracking centered around `navigate_to_hover_links ` to the `start/finish` approach taken in https://github.com/zed-industries/zed/commit/b69a2ea200561de9de0edf4d0c4428ee887fb918 because I find it easier to convince myself that the right data is being populated at the right times. Of course, let me know if you think there's a better solution. Closes #14206 Release Notes: - ??? I don't know what to write here! Suggestions welcome --- assets/keymaps/vim.json | 1 + crates/agent_ui/src/agent_diff.rs | 2 +- crates/agent_ui/src/text_thread_editor.rs | 4 +- crates/collab/src/tests/integration_tests.rs | 7 + crates/collab_ui/src/channel_view.rs | 2 +- crates/debugger_ui/src/stack_trace_view.rs | 7 +- crates/diagnostics/src/buffer_diagnostics.rs | 2 +- crates/diagnostics/src/diagnostics.rs | 2 +- crates/editor/src/editor.rs | 134 ++++++++++++----- crates/editor/src/editor_tests.rs | 2 +- crates/editor/src/hover_links.rs | 11 +- crates/editor/src/items.rs | 6 +- crates/editor/src/rust_analyzer_ext.rs | 3 + crates/git_ui/src/commit_view.rs | 2 +- crates/git_ui/src/file_diff_view.rs | 2 +- crates/git_ui/src/file_history_view.rs | 8 +- crates/git_ui/src/project_diff.rs | 2 +- crates/git_ui/src/text_diff_view.rs | 2 +- crates/search/src/project_search.rs | 2 +- crates/workspace/src/item.rs | 27 ++-- crates/workspace/src/pane.rs | 146 +++++++++++++++++-- crates/workspace/src/workspace.rs | 31 +++- 22 files changed, 328 insertions(+), 77 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 8fb9528a5f71cb62bda6811a9ddb862bfd6ccc81..9832ce8fe08fe23d610a1c2ee1a95ad4c2c2574c 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -67,6 +67,7 @@ "ctrl-o": "pane::GoBack", "ctrl-i": "pane::GoForward", "ctrl-]": "editor::GoToDefinition", + "ctrl-t": "pane::GoToOlderTag", "escape": "vim::SwitchToNormalMode", "ctrl-[": "vim::SwitchToNormalMode", "v": "vim::ToggleVisual", diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 282d6257e1fdc1a05d5eda320fc24e4bb1e05750..058933d14ade268c86a6158f851a1aaf3bcb69ef 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -472,7 +472,7 @@ impl Item for AgentDiffPane { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 2fc1b4e35d4535b60d1404e74916ce41dc58b589..85e9f4c444711695f787e36fbcf5341bca2b33c0 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -48,7 +48,7 @@ use settings::{ update_settings_file, }; use std::{ - any::TypeId, + any::{Any, TypeId}, cmp, ops::Range, path::{Path, PathBuf}, @@ -2894,7 +2894,7 @@ impl Item for TextThreadEditor { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e1708e04ead5a115a329673e173c33b85f98e3e2..f08f207c3eb013c042bc4c3dc8630334c79145b0 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6759,6 +6759,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) { cx.run_until_parked(); let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + right_pane.update(cx, |pane, cx| { + // Nav history is now cloned in an pane split, but that's inconvenient + // for this test, which uses the presence of a backwards history item as + // an indication that a preview item was successfully opened + pane.nav_history_mut().clear(cx); + }); + pane.update(cx, |pane, cx| { assert_eq!(pane.items_len(), 1); assert_eq!(get_path(pane, 0, cx), path_1.clone()); diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 0e6ecaae6b89cd4ba34abc85a2bc6941b1b085a3..859def4415a401f9f21a1779d5fde6c9101b07b1 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -517,7 +517,7 @@ impl Item for ChannelView { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { diff --git a/crates/debugger_ui/src/stack_trace_view.rs b/crates/debugger_ui/src/stack_trace_view.rs index 1d274ba63da839a4a7e9da9cae4cacdc1d872aa5..9072547c6b01f5c748f34521dba283bb0cf8294d 100644 --- a/crates/debugger_ui/src/stack_trace_view.rs +++ b/crates/debugger_ui/src/stack_trace_view.rs @@ -1,4 +1,7 @@ -use std::any::{Any, TypeId}; +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; use collections::HashMap; use dap::StackFrameId; @@ -333,7 +336,7 @@ impl Item for StackTraceView { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { diff --git a/crates/diagnostics/src/buffer_diagnostics.rs b/crates/diagnostics/src/buffer_diagnostics.rs index ef701241c5c50e2b5b42d9488a7d47985edbe4fd..6360e868d88ddeec677935beeba536d04cbc9131 100644 --- a/crates/diagnostics/src/buffer_diagnostics.rs +++ b/crates/diagnostics/src/buffer_diagnostics.rs @@ -757,7 +757,7 @@ impl Item for BufferDiagnosticsEditor { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 806e164a68aa9d80adc8ad23e6ce9363970c768a..2683582ae92711ef130db44619566cf5328c48bf 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -728,7 +728,7 @@ impl Item for ProjectDiagnosticsEditor { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e9012c17a22cd2082ee49ed1c2b327804275e5b3..554b98285ef8c14e55b88a780de2e25fdb8a6f38 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -203,9 +203,9 @@ use ui::{ }; use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc}; use workspace::{ - CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal, - RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast, - ViewId, Workspace, WorkspaceId, WorkspaceSettings, + CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, NavigationEntry, OpenInTerminal, + OpenTerminal, Pane, RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, + TabBarSettings, Toast, ViewId, Workspace, WorkspaceId, WorkspaceSettings, item::{BreadcrumbText, ItemBufferKind, ItemHandle, PreviewTabsSettings, SaveOptions}, notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, searchable::{CollapseDirection, SearchEvent}, @@ -1778,7 +1778,7 @@ enum SelectSyntaxNodeScrollBehavior { CursorBottom, } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub(crate) struct NavigationData { cursor_anchor: Anchor, cursor_position: Point, @@ -14913,6 +14913,29 @@ impl Editor { ); } + fn navigation_data(&self, cursor_anchor: Anchor, cx: &App) -> NavigationData { + let buffer = self.buffer.read(cx).read(cx); + let cursor_position = cursor_anchor.to_point(&buffer); + let scroll_anchor = self.scroll_manager.anchor(); + let scroll_top_row = scroll_anchor.top_row(&buffer); + drop(buffer); + + NavigationData { + cursor_anchor, + cursor_position, + scroll_anchor, + scroll_top_row, + } + } + + fn navigation_entry(&self, cursor_anchor: Anchor, cx: &App) -> Option { + let Some(history) = self.nav_history.clone() else { + return None; + }; + let data = self.navigation_data(cursor_anchor, cx); + Some(history.navigation_entry(Some(Arc::new(data) as Arc))) + } + fn push_to_nav_history( &mut self, cursor_anchor: Anchor, @@ -14921,29 +14944,16 @@ impl Editor { always: bool, cx: &mut Context, ) { + let data = self.navigation_data(cursor_anchor, cx); if let Some(nav_history) = self.nav_history.as_mut() { - let buffer = self.buffer.read(cx).read(cx); - let cursor_position = cursor_anchor.to_point(&buffer); - let scroll_state = self.scroll_manager.anchor(); - let scroll_top_row = scroll_state.top_row(&buffer); - drop(buffer); - if let Some(new_position) = new_position { - let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs(); + let row_delta = (new_position.row as i64 - data.cursor_position.row as i64).abs(); if row_delta == 0 || (row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA && !always) { return; } } - nav_history.push( - Some(NavigationData { - cursor_anchor, - cursor_position, - scroll_anchor: scroll_state, - scroll_top_row, - }), - cx, - ); + nav_history.push(Some(data), cx); cx.emit(EditorEvent::PushedToNavHistory { anchor: cursor_anchor, is_deactivate, @@ -17556,6 +17566,8 @@ impl Editor { return Task::ready(Ok(Navigated::No)); }; + let nav_entry = self.navigation_entry(self.selections.newest_anchor().head(), cx); + cx.spawn_in(window, async move |editor, cx| { let Some(definitions) = definitions.await? else { return Ok(Navigated::No); @@ -17571,6 +17583,7 @@ impl Editor { }) .map(HoverLink::Text) .collect::>(), + nav_entry, split, window, cx, @@ -17663,6 +17676,7 @@ impl Editor { &mut self, kind: Option, definitions: Vec, + origin: Option, split: bool, window: &mut Window, cx: &mut Context, @@ -17752,16 +17766,34 @@ impl Editor { .update_in(cx, |workspace, window, cx| { let allow_preview = PreviewTabsSettings::get_global(cx) .enable_preview_multibuffer_from_code_navigation; - Self::open_locations_in_multibuffer( - workspace, - locations, - title, - split, - allow_preview, - MultibufferSelectionMode::First, - window, - cx, - ) + if let Some((target_editor, target_pane)) = + Self::open_locations_in_multibuffer( + workspace, + locations, + title, + split, + allow_preview, + MultibufferSelectionMode::First, + window, + cx, + ) + { + // We create our own nav history instead of using + // `target_editor.nav_history` because `nav_history` + // seems to be populated asynchronously when an item + // is added to a pane + let mut nav_history = target_pane + .update(cx, |pane, _| pane.nav_history_for_item(&target_editor)); + target_editor.update(cx, |editor, cx| { + let nav_data = editor + .navigation_data(editor.selections.newest_anchor().head(), cx); + let target = + Some(nav_history.navigation_entry(Some( + Arc::new(nav_data) as Arc + ))); + nav_history.push_tag(origin, target); + }) + } }) .is_ok(); @@ -17801,21 +17833,26 @@ impl Editor { let target_range = target_ranges.first().unwrap().clone(); editor.update_in(cx, |editor, window, cx| { - let range = target_range.to_point(target_buffer.read(cx)); - let range = editor.range_for_match(&range); + let range = editor.range_for_match(&target_range); let range = collapse_multiline_range(range); if !split && Some(&target_buffer) == editor.buffer.read(cx).as_singleton().as_ref() { editor.go_to_singleton_buffer_range(range, window, cx); + + let target = + editor.navigation_entry(editor.selections.newest_anchor().head(), cx); + if let Some(mut nav_history) = editor.nav_history.clone() { + nav_history.push_tag(origin, target); + } } else { let Some(workspace) = workspace else { return Navigated::No; }; let pane = workspace.read(cx).active_pane().clone(); window.defer(cx, move |window, cx| { - let target_editor: Entity = + let (target_editor, target_pane): (Entity, Entity) = workspace.update(cx, |workspace, cx| { let pane = if split { workspace.adjacent_pane(window, cx) @@ -17829,8 +17866,8 @@ impl Editor { let allow_new_preview = preview_tabs_settings .enable_preview_file_from_code_navigation; - workspace.open_project_item( - pane, + let editor = workspace.open_project_item( + pane.clone(), target_buffer.clone(), true, true, @@ -17838,13 +17875,30 @@ impl Editor { allow_new_preview, window, cx, - ) + ); + (editor, pane) }); + // We create our own nav history instead of using + // `target_editor.nav_history` because `nav_history` + // seems to be populated asynchronously when an item + // is added to a pane + let mut nav_history = target_pane + .update(cx, |pane, _| pane.nav_history_for_item(&target_editor)); target_editor.update(cx, |target_editor, cx| { // When selecting a definition in a different buffer, disable the nav history // to avoid creating a history entry at the previous cursor location. pane.update(cx, |pane, _| pane.disable_history()); target_editor.go_to_singleton_buffer_range(range, window, cx); + + let nav_data = target_editor.navigation_data( + target_editor.selections.newest_anchor().head(), + cx, + ); + let target = + Some(nav_history.navigation_entry(Some( + Arc::new(nav_data) as Arc + ))); + nav_history.push_tag(origin, target); pane.update(cx, |pane, _| pane.enable_history()); }); }); @@ -18203,10 +18257,10 @@ impl Editor { multibuffer_selection_mode: MultibufferSelectionMode, window: &mut Window, cx: &mut Context, - ) { + ) -> Option<(Entity, Entity)> { if locations.is_empty() { log::error!("bug: open_locations_in_multibuffer called with empty list of locations"); - return; + return None; } let capability = workspace.project().read(cx).capability(); @@ -18287,7 +18341,7 @@ impl Editor { } }); - let item = Box::new(editor); + let item = Box::new(editor.clone()); let pane = if split { workspace.adjacent_pane(window, cx) @@ -18306,6 +18360,8 @@ impl Editor { } pane.add_item(item, activate_pane, true, destination_index, window, cx); }); + + Some((editor, pane)) } pub fn rename( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ad0c1a2db38bdcb91a24955bce83c5eae1432e49..4a563c32cecff17b77ca26d25d57a06779f1a589 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -954,7 +954,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok(); let invalid_point = Point::new(9999, 0); editor.navigate( - Box::new(NavigationData { + Arc::new(NavigationData { cursor_anchor: invalid_anchor, cursor_position: invalid_point, scroll_anchor: ScrollAnchor { diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 5ee81909a1c665b847f3aef05cdc495e918f4c6e..e812784e5bb6a8a681daf6ab967db3985383cf10 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -232,6 +232,13 @@ impl Editor { else { return Task::ready(Ok(Navigated::No)); }; + let Some(mb_anchor) = self + .buffer() + .read(cx) + .buffer_anchor_to_anchor(&buffer, anchor, cx) + else { + return Task::ready(Ok(Navigated::No)); + }; let links = hovered_link_state .links .into_iter() @@ -243,8 +250,10 @@ impl Editor { } }) .collect(); + let nav_entry = self.navigation_entry(mb_anchor, cx); let split = Self::is_alt_pressed(&modifiers, cx); - let navigate_task = self.navigate_to_hover_links(None, links, split, window, cx); + let navigate_task = + self.navigate_to_hover_links(None, links, nav_entry, split, window, cx); self.select(SelectPhase::End, window, cx); return navigate_task; } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index fc2d5e648a17568a0c242094219fa5ce71ef1859..dc1cc5f0c7a33eb2913396d44c0b79c5d6442696 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -29,7 +29,7 @@ use project::{ use rpc::proto::{self, update_view}; use settings::Settings; use std::{ - any::TypeId, + any::{Any, TypeId}, borrow::Cow, cmp::{self, Ordering}, iter, @@ -593,11 +593,11 @@ impl Item for Editor { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { - if let Ok(data) = data.downcast::() { + if let Some(data) = data.downcast_ref::() { let newest_selection = self.selections.newest::(&self.display_snapshot(cx)); let buffer = self.buffer.read(cx).read(cx); let offset = if buffer.can_resolve(&data.cursor_anchor) { diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index 79bdfd660e0a08fa6bae12920be450d7651f7ee1..41c062b5dfed675fbf1fb2fefc378b00f4ab4bbc 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -76,6 +76,8 @@ pub fn go_to_parent_module( return; }; + let nav_entry = editor.navigation_entry(editor.selections.newest_anchor().head(), cx); + let project = project.clone(); let lsp_store = project.read(cx).lsp_store(); let upstream_client = lsp_store.read(cx).upstream_client(); @@ -123,6 +125,7 @@ pub fn go_to_parent_module( editor.navigate_to_hover_links( Some(GotoDefinitionKind::Declaration), location_links.into_iter().map(HoverLink::Text).collect(), + nav_entry, false, window, cx, diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index f33f877851455c292dd83dcf140ee55407b3b481..46e79f6dd4e0d14b74fa400baa67d10dd5e3d0fa 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -1005,7 +1005,7 @@ impl Item for CommitView { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index 048aa82cb58b04dee88df81f425583b129d52b75..0f295270d241dc109926a06e2ae6abad62b65a45 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -311,7 +311,7 @@ impl Item for FileDiffView { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { diff --git a/crates/git_ui/src/file_history_view.rs b/crates/git_ui/src/file_history_view.rs index f48160719ba5d9b00b8961b75e9ea402c80dd06a..121e44e29eb8ff3bc829522cb0a6b1d00f799c8f 100644 --- a/crates/git_ui/src/file_history_view.rs +++ b/crates/git_ui/src/file_history_view.rs @@ -12,6 +12,7 @@ use project::{ git_store::{GitStore, Repository}, }; use std::any::{Any, TypeId}; +use std::sync::Arc; use time::OffsetDateTime; use ui::{Avatar, Chip, Divider, ListItem, WithScrollbar, prelude::*}; @@ -574,7 +575,12 @@ impl Item for FileHistoryView { Task::ready(None) } - fn navigate(&mut self, _: Box, _window: &mut Window, _: &mut Context) -> bool { + fn navigate( + &mut self, + _: Arc, + _window: &mut Window, + _: &mut Context, + ) -> bool { false } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index e56ee5a814d04da551c0af5cb387acf291c53ba5..05c8e482107cd30e37c6009f49ea6428fa0804fc 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -840,7 +840,7 @@ impl Item for ProjectDiff { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index 59b974248400f0cbe1b8abfecb699b0849d797f9..c05384970a4f234fe011156271fb858c75c149cc 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -369,7 +369,7 @@ impl Item for TextDiffView { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 60c5eaccba25fe8be4cfd7186a06f83c5f530661..0bb58e43e5bd21460f0b2338e1408bac5e3447fd 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -642,7 +642,7 @@ impl Item for ProjectSearchView { fn navigate( &mut self, - data: Box, + data: Arc, window: &mut Window, cx: &mut Context, ) -> bool { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index eaf5cfd99704c415039bf30f94435bb75c0f79cc..f0ed65c8dac06ad88ad38d190ca22f50b319b2b1 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -219,7 +219,12 @@ pub trait Item: Focusable + EventEmitter + Render + Sized { fn discarded(&self, _project: Entity, _window: &mut Window, _cx: &mut Context) {} fn on_removed(&self, _cx: &App) {} fn workspace_deactivated(&mut self, _window: &mut Window, _: &mut Context) {} - fn navigate(&mut self, _: Box, _window: &mut Window, _: &mut Context) -> bool { + fn navigate( + &mut self, + _: Arc, + _window: &mut Window, + _: &mut Context, + ) -> bool { false } @@ -480,7 +485,7 @@ pub trait ItemHandle: 'static + Send { fn deactivated(&self, window: &mut Window, cx: &mut App); fn on_removed(&self, cx: &App); fn workspace_deactivated(&self, window: &mut Window, cx: &mut App); - fn navigate(&self, data: Box, window: &mut Window, cx: &mut App) -> bool; + fn navigate(&self, data: Arc, window: &mut Window, cx: &mut App) -> bool; fn item_id(&self) -> EntityId; fn to_any_view(&self) -> AnyView; fn is_dirty(&self, cx: &App) -> bool; @@ -944,7 +949,7 @@ impl ItemHandle for Entity { self.update(cx, |this, cx| this.workspace_deactivated(window, cx)); } - fn navigate(&self, data: Box, window: &mut Window, cx: &mut App) -> bool { + fn navigate(&self, data: Arc, window: &mut Window, cx: &mut App) -> bool { self.update(cx, |this, cx| this.navigate(data, window, cx)) } @@ -1331,7 +1336,7 @@ pub mod test { InteractiveElement, IntoElement, Render, SharedString, Task, WeakEntity, Window, }; use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; - use std::{any::Any, cell::Cell}; + use std::{any::Any, cell::Cell, sync::Arc}; use util::rel_path::rel_path; pub struct TestProjectItem { @@ -1564,14 +1569,18 @@ pub mod test { fn navigate( &mut self, - state: Box, + state: Arc, _window: &mut Window, _: &mut Context, ) -> bool { - let state = *state.downcast::().unwrap_or_default(); - if state != self.state { - self.state = state; - true + if let Some(state) = state.downcast_ref::>() { + let state = *state.clone(); + if state != self.state { + false + } else { + self.state = state; + true + } } else { false } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 9e24a132b08a666b14f292137b966209af8489af..2096c1fa23bc85690260007f3c89c5fe2b839a00 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -247,6 +247,10 @@ actions!( GoBack, /// Navigates forward in history. GoForward, + /// Navigates back in the tag stack. + GoToOlderTag, + /// Navigates forward in the tag stack. + GoToNewerTag, /// Joins this pane into the next pane. JoinIntoNext, /// Joins all panes into one. @@ -429,6 +433,7 @@ pub struct ActivationHistoryEntry { pub timestamp: usize, } +#[derive(Clone)] pub struct ItemNavHistory { history: NavHistory, item: Arc, @@ -438,11 +443,14 @@ pub struct ItemNavHistory { #[derive(Clone)] pub struct NavHistory(Arc>); +#[derive(Clone)] struct NavHistoryState { mode: NavigationMode, backward_stack: VecDeque, forward_stack: VecDeque, closed_stack: VecDeque, + tag_stack: VecDeque, + tag_stack_pos: usize, paths_by_item: HashMap)>, pane: WeakEntity, next_timestamp: Arc, @@ -459,13 +467,27 @@ pub enum NavigationMode { Disabled, } +#[derive(Debug, Default, Copy, Clone)] +pub enum TagNavigationMode { + #[default] + Older, + Newer, +} + +#[derive(Clone)] pub struct NavigationEntry { - pub item: Arc, - pub data: Option>, + pub item: Arc, + pub data: Option>, pub timestamp: usize, pub is_preview: bool, } +#[derive(Clone)] +pub struct TagStackEntry { + pub origin: NavigationEntry, + pub target: NavigationEntry, +} + #[derive(Clone)] pub struct DraggedTab { pub pane: Entity, @@ -534,6 +556,8 @@ impl Pane { backward_stack: Default::default(), forward_stack: Default::default(), closed_stack: Default::default(), + tag_stack: Default::default(), + tag_stack_pos: Default::default(), paths_by_item: Default::default(), pane: handle, next_timestamp, @@ -839,6 +863,16 @@ impl Pane { &mut self.nav_history } + pub fn fork_nav_history(&self) -> NavHistory { + let history = self.nav_history.0.lock().clone(); + NavHistory(Arc::new(Mutex::new(history))) + } + + pub fn set_nav_history(&mut self, history: NavHistory, cx: &Context) { + self.nav_history = history; + self.nav_history().0.lock().pane = cx.entity().downgrade(); + } + pub fn disable_history(&mut self) { self.nav_history.disable(); } @@ -879,6 +913,42 @@ impl Pane { } } + pub fn go_to_older_tag( + &mut self, + _: &GoToOlderTag, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(workspace) = self.workspace.upgrade() { + let pane = cx.entity().downgrade(); + window.defer(cx, move |window, cx| { + workspace.update(cx, |workspace, cx| { + workspace + .navigate_tag_history(pane, TagNavigationMode::Older, window, cx) + .detach_and_log_err(cx) + }) + }) + } + } + + pub fn go_to_newer_tag( + &mut self, + _: &GoToNewerTag, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(workspace) = self.workspace.upgrade() { + let pane = cx.entity().downgrade(); + window.defer(cx, move |window, cx| { + workspace.update(cx, |workspace, cx| { + workspace + .navigate_tag_history(pane, TagNavigationMode::Newer, window, cx) + .detach_and_log_err(cx) + }) + }) + } + } + fn history_updated(&mut self, cx: &mut Context) { self.toolbar.update(cx, |_, cx| cx.notify()); } @@ -4159,6 +4229,8 @@ impl Render for Pane { .on_action(cx.listener(Pane::zoom_out)) .on_action(cx.listener(Self::navigate_backward)) .on_action(cx.listener(Self::navigate_forward)) + .on_action(cx.listener(Self::go_to_older_tag)) + .on_action(cx.listener(Self::go_to_newer_tag)) .on_action( cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| { pane.activate_item( @@ -4391,7 +4463,7 @@ impl Render for Pane { } impl ItemNavHistory { - pub fn push(&mut self, data: Option, cx: &mut App) { + pub fn push(&mut self, data: Option, cx: &mut App) { if self .item .upgrade() @@ -4402,6 +4474,21 @@ impl ItemNavHistory { } } + pub fn navigation_entry(&self, data: Option>) -> NavigationEntry { + NavigationEntry { + item: self.item.clone(), + data: data, + timestamp: 0, // not used + is_preview: self.is_preview, + } + } + + pub fn push_tag(&mut self, origin: Option, target: Option) { + if let (Some(origin_entry), Some(target_entry)) = (origin, target) { + self.history.push_tag(origin_entry, target_entry); + } + } + pub fn pop_backward(&mut self, cx: &mut App) -> Option { self.history.pop(NavigationMode::GoingBack, cx) } @@ -4459,6 +4546,7 @@ impl NavHistory { && state.forward_stack.is_empty() && state.closed_stack.is_empty() && state.paths_by_item.is_empty() + && state.tag_stack.is_empty() { return; } @@ -4468,6 +4556,8 @@ impl NavHistory { state.forward_stack.clear(); state.closed_stack.clear(); state.paths_by_item.clear(); + state.tag_stack.clear(); + state.tag_stack_pos = 0; state.did_update(cx); } @@ -4488,10 +4578,10 @@ impl NavHistory { entry } - pub fn push( + pub fn push( &mut self, data: Option, - item: Arc, + item: Arc, is_preview: bool, cx: &mut App, ) { @@ -4504,7 +4594,7 @@ impl NavHistory { } state.backward_stack.push_back(NavigationEntry { item, - data: data.map(|data| Box::new(data) as Box), + data: data.map(|data| Arc::new(data) as Arc), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), is_preview, }); @@ -4516,7 +4606,7 @@ impl NavHistory { } state.forward_stack.push_back(NavigationEntry { item, - data: data.map(|data| Box::new(data) as Box), + data: data.map(|data| Arc::new(data) as Arc), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), is_preview, }); @@ -4527,7 +4617,7 @@ impl NavHistory { } state.backward_stack.push_back(NavigationEntry { item, - data: data.map(|data| Box::new(data) as Box), + data: data.map(|data| Arc::new(data) as Arc), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), is_preview, }); @@ -4539,7 +4629,7 @@ impl NavHistory { } state.closed_stack.push_back(NavigationEntry { item, - data: data.map(|data| Box::new(data) as Box), + data: data.map(|data| Arc::new(data) as Arc), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), is_preview, }); @@ -4560,6 +4650,9 @@ impl NavHistory { state .closed_stack .retain(|entry| entry.item.id() != item_id); + state + .tag_stack + .retain(|entry| entry.origin.item.id() != item_id && entry.target.item.id() != item_id); } pub fn rename_item( @@ -4579,6 +4672,41 @@ impl NavHistory { pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option)> { self.0.lock().paths_by_item.get(&item_id).cloned() } + + pub fn push_tag(&mut self, origin: NavigationEntry, target: NavigationEntry) { + let mut state = self.0.lock(); + let truncate_to = state.tag_stack_pos; + state.tag_stack.truncate(truncate_to); + state.tag_stack.push_back(TagStackEntry { origin, target }); + state.tag_stack_pos = state.tag_stack.len(); + } + + pub fn pop_tag(&mut self, mode: TagNavigationMode) -> Option { + let mut state = self.0.lock(); + match mode { + TagNavigationMode::Older => { + if state.tag_stack_pos > 0 { + state.tag_stack_pos -= 1; + state + .tag_stack + .get(state.tag_stack_pos) + .map(|e| e.origin.clone()) + } else { + None + } + } + TagNavigationMode::Newer => { + let entry = state + .tag_stack + .get(state.tag_stack_pos) + .map(|e| e.target.clone()); + if state.tag_stack_pos < state.tag_stack.len() { + state.tag_stack_pos += 1; + } + entry + } + } + } } impl NavHistoryState { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 08e8088abf5003104a5667e74a189ac59694a809..eb8d6414ceea309fa83e96a3996e2aa8c3c71be5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2080,13 +2080,40 @@ impl Workspace { mode: NavigationMode, window: &mut Window, cx: &mut Context, + ) -> Task> { + self.navigate_history_impl(pane, mode, window, |history, cx| history.pop(mode, cx), cx) + } + + fn navigate_tag_history( + &mut self, + pane: WeakEntity, + mode: TagNavigationMode, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.navigate_history_impl( + pane, + NavigationMode::Normal, + window, + |history, _cx| history.pop_tag(mode), + cx, + ) + } + + fn navigate_history_impl( + &mut self, + pane: WeakEntity, + mode: NavigationMode, + window: &mut Window, + mut cb: impl FnMut(&mut NavHistory, &mut App) -> Option, + cx: &mut Context, ) -> Task> { let to_load = if let Some(pane) = pane.upgrade() { pane.update(cx, |pane, cx| { window.focus(&pane.focus_handle(cx), cx); loop { // Retrieve the weak item handle from the history. - let entry = pane.nav_history_mut().pop(mode, cx)?; + let entry = cb(pane.nav_history_mut(), cx)?; // If the item is still present in this pane, then activate it. if let Some(index) = entry @@ -4553,7 +4580,9 @@ impl Workspace { if let Some(clone) = task.await { this.update_in(cx, |this, window, cx| { let new_pane = this.add_pane(window, cx); + let nav_history = pane.read(cx).fork_nav_history(); new_pane.update(cx, |pane, cx| { + pane.set_nav_history(nav_history, cx); pane.add_item(clone, true, true, None, window, cx) }); this.center.split(&pane, &new_pane, direction, cx).unwrap();