Tag stack 2

Conrad Irwin created

Change summary

assets/keymaps/vim.json                      |   1 
crates/agent_ui/src/agent_diff.rs            |   8 -
crates/agent_ui/src/text_thread_editor.rs    |   2 
crates/collab_ui/src/channel_view.rs         |   8 -
crates/debugger_ui/src/stack_trace_view.rs   |  12 -
crates/diagnostics/src/buffer_diagnostics.rs |   8 -
crates/diagnostics/src/diagnostics.rs        |   8 -
crates/editor/src/editor.rs                  |  64 ++++++++++
crates/editor/src/editor_tests.rs            |   2 
crates/editor/src/hover_links.rs             |  13 ++
crates/editor/src/items.rs                   |   3 
crates/git_ui/src/commit_view.rs             |   8 -
crates/git_ui/src/file_diff_view.rs          |   8 -
crates/git_ui/src/project_diff.rs            |  12 -
crates/git_ui/src/text_diff_view.rs          |   8 -
crates/search/src/project_search.rs          |   8 -
crates/workspace/src/item.rs                 |  15 +-
crates/workspace/src/pane.rs                 | 125 ++++++++++++++++++++-
crates/workspace/src/workspace.rs            | 121 +++++++++++++++++++++
19 files changed, 353 insertions(+), 81 deletions(-)

Detailed changes

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",

crates/agent_ui/src/agent_diff.rs 🔗

@@ -25,6 +25,7 @@ use std::{
     any::{Any, TypeId},
     collections::hash_map::Entry,
     ops::Range,
+    rc::Rc,
     sync::Arc,
 };
 use ui::{CommonAnimationExt, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
@@ -516,12 +517,7 @@ impl Item for AgentDiffPane {
             .update(cx, |editor, cx| editor.deactivated(window, cx));
     }
 
-    fn navigate(
-        &mut self,
-        data: Box<dyn Any>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> bool {
+    fn navigate(&mut self, data: Rc<dyn Any>, window: &mut Window, cx: &mut Context<Self>) -> bool {
         self.editor
             .update(cx, |editor, cx| editor.navigate(data, window, cx))
     }

crates/agent_ui/src/text_thread_editor.rs 🔗

@@ -2530,7 +2530,7 @@ impl Item for TextThreadEditor {
 
     fn navigate(
         &mut self,
-        data: Box<dyn std::any::Any>,
+        data: Rc<dyn std::any::Any>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> bool {

crates/collab_ui/src/channel_view.rs 🔗

@@ -18,6 +18,7 @@ use project::Project;
 use rpc::proto::ChannelVisibility;
 use std::{
     any::{Any, TypeId},
+    rc::Rc,
     sync::Arc,
 };
 use ui::prelude::*;
@@ -515,12 +516,7 @@ impl Item for ChannelView {
         })))
     }
 
-    fn navigate(
-        &mut self,
-        data: Box<dyn Any>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> bool {
+    fn navigate(&mut self, data: Rc<dyn Any>, window: &mut Window, cx: &mut Context<Self>) -> bool {
         self.editor
             .update(cx, |editor, cx| editor.navigate(data, window, cx))
     }

crates/debugger_ui/src/stack_trace_view.rs 🔗

@@ -1,4 +1,7 @@
-use std::any::{Any, TypeId};
+use std::{
+    any::{Any, TypeId},
+    rc::Rc,
+};
 
 use collections::HashMap;
 use dap::StackFrameId;
@@ -331,12 +334,7 @@ impl Item for StackTraceView {
             .update(cx, |editor, cx| editor.deactivated(window, cx));
     }
 
-    fn navigate(
-        &mut self,
-        data: Box<dyn Any>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> bool {
+    fn navigate(&mut self, data: Rc<dyn Any>, window: &mut Window, cx: &mut Context<Self>) -> bool {
         self.editor
             .update(cx, |editor, cx| editor.navigate(data, window, cx))
     }

crates/diagnostics/src/buffer_diagnostics.rs 🔗

@@ -24,6 +24,7 @@ use settings::Settings;
 use std::{
     any::{Any, TypeId},
     cmp::Ordering,
+    rc::Rc,
     sync::Arc,
 };
 use text::{Anchor, BufferSnapshot, OffsetRangeExt};
@@ -734,12 +735,7 @@ impl Item for BufferDiagnosticsEditor {
         self.multibuffer.read(cx).is_dirty(cx)
     }
 
-    fn navigate(
-        &mut self,
-        data: Box<dyn Any>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> bool {
+    fn navigate(&mut self, data: Rc<dyn Any>, window: &mut Window, cx: &mut Context<Self>) -> bool {
         self.editor
             .update(cx, |editor, cx| editor.navigate(data, window, cx))
     }

crates/diagnostics/src/diagnostics.rs 🔗

@@ -35,6 +35,7 @@ use std::{
     any::{Any, TypeId},
     cmp,
     ops::{Range, RangeInclusive},
+    rc::Rc,
     sync::Arc,
     time::Duration,
 };
@@ -728,12 +729,7 @@ impl Item for ProjectDiagnosticsEditor {
             .update(cx, |editor, cx| editor.deactivated(window, cx));
     }
 
-    fn navigate(
-        &mut self,
-        data: Box<dyn Any>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> bool {
+    fn navigate(&mut self, data: Rc<dyn Any>, window: &mut Window, cx: &mut Context<Self>) -> bool {
         self.editor
             .update(cx, |editor, cx| editor.navigate(data, window, cx))
     }

crates/editor/src/editor.rs 🔗

@@ -14038,6 +14038,27 @@ impl Editor {
         );
     }
 
+    fn finish_tag_jump(&mut self, point: Point, cx: &mut Context<Self>) {
+        let Some(nav_history) = self.nav_history.as_mut() else {
+            return;
+        };
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let cursor_anchor = snapshot.anchor_after(point);
+        let cursor_position = cursor_anchor.to_point(&snapshot);
+        let scroll_state = self.scroll_manager.anchor();
+        let scroll_top_row = scroll_state.top_row(&snapshot);
+        dbg!("finish tag jump", cursor_position);
+        nav_history.finish_tag_jump(
+            Some(NavigationData {
+                cursor_anchor,
+                cursor_position,
+                scroll_anchor: scroll_state,
+                scroll_top_row,
+            }),
+            cx,
+        );
+    }
+
     fn push_to_nav_history(
         &mut self,
         cursor_anchor: Anchor,
@@ -16437,18 +16458,37 @@ impl Editor {
         let Some(provider) = self.semantics_provider.clone() else {
             return Task::ready(Ok(Navigated::No));
         };
-        let head = self
+        let cursor = self
             .selections
             .newest::<usize>(&self.display_snapshot(cx))
             .head();
-        let buffer = self.buffer.read(cx);
-        let Some((buffer, head)) = buffer.text_anchor_for_position(head, cx) else {
+        let multi_buffer = self.buffer.read(cx);
+        let Some((buffer, head)) = multi_buffer.text_anchor_for_position(cursor, cx) else {
             return Task::ready(Ok(Navigated::No));
         };
         let Some(definitions) = provider.definitions(&buffer, head, kind, cx) else {
             return Task::ready(Ok(Navigated::No));
         };
 
+        if let Some(nav_history) = self.nav_history.as_mut() {
+            let snapshot = self.buffer.read(cx).snapshot(cx);
+            let cursor_anchor = snapshot.anchor_after(cursor);
+            let cursor_position = snapshot.offset_to_point(cursor);
+            let scroll_anchor = self.scroll_manager.anchor();
+            let scroll_top_row = scroll_anchor.top_row(&snapshot);
+
+            dbg!("start tag jump", cursor_position);
+            nav_history.start_tag_jump(
+                Some(NavigationData {
+                    cursor_anchor,
+                    cursor_position,
+                    scroll_anchor,
+                    scroll_top_row,
+                }),
+                cx,
+            );
+        }
+
         cx.spawn_in(window, async move |editor, cx| {
             let Some(definitions) = definitions.await? else {
                 return Ok(Navigated::No);
@@ -16693,6 +16733,7 @@ impl Editor {
                     if !split
                         && Some(&target_buffer) == editor.buffer.read(cx).as_singleton().as_ref()
                     {
+                        editor.finish_tag_jump(range.start, cx);
                         editor.go_to_singleton_buffer_range(range, window, cx);
                     } else {
                         let pane = workspace.read(cx).active_pane().clone();
@@ -16718,6 +16759,7 @@ impl Editor {
                                 // 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.finish_tag_jump(range.start, cx);
                                 target_editor.go_to_singleton_buffer_range(range, window, cx);
                                 pane.update(cx, |pane, _| pane.enable_history());
                             });
@@ -17039,6 +17081,7 @@ impl Editor {
 
             multibuffer.with_title(title)
         });
+        let first_range = ranges.first().cloned();
         let existing = workspace.active_pane().update(cx, |pane, cx| {
             pane.items()
                 .filter_map(|item| item.downcast::<Editor>())
@@ -17091,6 +17134,21 @@ impl Editor {
                 });
             }
         });
+        cx.defer({
+            let editor = editor.clone();
+            move |cx| {
+                let Some(range) = first_range else { return };
+                editor.update(cx, |editor, cx| {
+                    let point = editor
+                        .buffer()
+                        .read(cx)
+                        .snapshot(cx)
+                        .summary_for_anchor(&range.start);
+
+                    editor.finish_tag_jump(point, cx)
+                })
+            }
+        });
 
         let item = Box::new(editor);
         let item_id = item.item_id();

crates/editor/src/editor_tests.rs 🔗

@@ -911,7 +911,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 {
+                Rc::new(NavigationData {
                     cursor_anchor: invalid_anchor,
                     cursor_position: invalid_point,
                     scroll_anchor: ScrollAnchor {

crates/editor/src/hover_links.rs 🔗

@@ -241,6 +241,19 @@ impl Editor {
                         }
                     })
                     .collect();
+
+                // todo!()
+                // if let Some(nav_history) = self.nav_history.as_mut() {
+                //     nav_history.start_tag_jump(
+                //         Some(NavigationData {
+                //             cursor_anchor,
+                //             cursor_position,
+                //             scroll_anchor: scroll_state,
+                //             scroll_top_row,
+                //         }),
+                //         cx,
+                //     );
+                // }
                 let navigate_task =
                     self.navigate_to_hover_links(None, links, modifiers.alt, window, cx);
                 self.select(SelectPhase::End, window, cx);

crates/editor/src/items.rs 🔗

@@ -34,6 +34,7 @@ use std::{
     iter,
     ops::Range,
     path::{Path, PathBuf},
+    rc::Rc,
     sync::Arc,
 };
 use text::{BufferId, BufferSnapshot, Selection};
@@ -589,7 +590,7 @@ impl Item for Editor {
 
     fn navigate(
         &mut self,
-        data: Box<dyn std::any::Any>,
+        data: Rc<dyn std::any::Any>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> bool {

crates/git_ui/src/commit_view.rs 🔗

@@ -17,6 +17,7 @@ use std::{
     any::{Any, TypeId},
     fmt::Write as _,
     path::PathBuf,
+    rc::Rc,
     sync::Arc,
 };
 use ui::{
@@ -527,12 +528,7 @@ impl Item for CommitView {
         });
     }
 
-    fn navigate(
-        &mut self,
-        data: Box<dyn Any>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> bool {
+    fn navigate(&mut self, data: Rc<dyn Any>, window: &mut Window, cx: &mut Context<Self>) -> bool {
         self.editor
             .update(cx, |editor, cx| editor.navigate(data, window, cx))
     }

crates/git_ui/src/file_diff_view.rs 🔗

@@ -14,6 +14,7 @@ use std::{
     any::{Any, TypeId},
     path::PathBuf,
     pin::pin,
+    rc::Rc,
     sync::Arc,
     time::Duration,
 };
@@ -301,12 +302,7 @@ impl Item for FileDiffView {
         });
     }
 
-    fn navigate(
-        &mut self,
-        data: Box<dyn Any>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> bool {
+    fn navigate(&mut self, data: Rc<dyn Any>, window: &mut Window, cx: &mut Context<Self>) -> bool {
         self.editor
             .update(cx, |editor, cx| editor.navigate(data, window, cx))
     }

crates/git_ui/src/project_diff.rs 🔗

@@ -32,9 +32,12 @@ use project::{
     },
 };
 use settings::{Settings, SettingsStore};
-use std::any::{Any, TypeId};
 use std::ops::Range;
 use std::sync::Arc;
+use std::{
+    any::{Any, TypeId},
+    rc::Rc,
+};
 use theme::ActiveTheme;
 use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};
 use util::{ResultExt as _, rel_path::RelPath};
@@ -649,12 +652,7 @@ impl Item for ProjectDiff {
             .update(cx, |editor, cx| editor.deactivated(window, cx));
     }
 
-    fn navigate(
-        &mut self,
-        data: Box<dyn Any>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> bool {
+    fn navigate(&mut self, data: Rc<dyn Any>, window: &mut Window, cx: &mut Context<Self>) -> bool {
         self.editor
             .update(cx, |editor, cx| editor.navigate(data, window, cx))
     }

crates/git_ui/src/text_diff_view.rs 🔗

@@ -15,6 +15,7 @@ use std::{
     cmp,
     ops::Range,
     pin::pin,
+    rc::Rc,
     sync::Arc,
     time::Duration,
 };
@@ -362,12 +363,7 @@ impl Item for TextDiffView {
         });
     }
 
-    fn navigate(
-        &mut self,
-        data: Box<dyn Any>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> bool {
+    fn navigate(&mut self, data: Rc<dyn Any>, window: &mut Window, cx: &mut Context<Self>) -> bool {
         self.diff_editor
             .update(cx, |editor, cx| editor.navigate(data, window, cx))
     }

crates/search/src/project_search.rs 🔗

@@ -36,6 +36,7 @@ use std::{
     mem,
     ops::{Not, Range},
     pin::pin,
+    rc::Rc,
     sync::Arc,
 };
 use ui::{IconButtonShape, KeyBinding, Toggleable, Tooltip, prelude::*, utils::SearchInputWidth};
@@ -633,12 +634,7 @@ impl Item for ProjectSearchView {
         });
     }
 
-    fn navigate(
-        &mut self,
-        data: Box<dyn Any>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> bool {
+    fn navigate(&mut self, data: Rc<dyn Any>, window: &mut Window, cx: &mut Context<Self>) -> bool {
         self.results_editor
             .update(cx, |editor, cx| editor.navigate(data, window, cx))
     }

crates/workspace/src/item.rs 🔗

@@ -194,7 +194,7 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
     fn discarded(&self, _project: Entity<Project>, _window: &mut Window, _cx: &mut Context<Self>) {}
     fn on_removed(&self, _cx: &App) {}
     fn workspace_deactivated(&mut self, _window: &mut Window, _: &mut Context<Self>) {}
-    fn navigate(&mut self, _: Box<dyn Any>, _window: &mut Window, _: &mut Context<Self>) -> bool {
+    fn navigate(&mut self, _: Rc<dyn Any>, _window: &mut Window, _: &mut Context<Self>) -> bool {
         false
     }
 
@@ -449,7 +449,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<dyn Any>, window: &mut Window, cx: &mut App) -> bool;
+    fn navigate(&self, data: Rc<dyn Any>, window: &mut Window, cx: &mut App) -> bool;
     fn item_id(&self) -> EntityId;
     fn to_any(&self) -> AnyView;
     fn is_dirty(&self, cx: &App) -> bool;
@@ -900,7 +900,7 @@ impl<T: Item> ItemHandle for Entity<T> {
         self.update(cx, |this, cx| this.workspace_deactivated(window, cx));
     }
 
-    fn navigate(&self, data: Box<dyn Any>, window: &mut Window, cx: &mut App) -> bool {
+    fn navigate(&self, data: Rc<dyn Any>, window: &mut Window, cx: &mut App) -> bool {
         self.update(cx, |this, cx| this.navigate(data, window, cx))
     }
 
@@ -1277,7 +1277,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, rc::Rc};
     use util::rel_path::rel_path;
 
     pub struct TestProjectItem {
@@ -1510,11 +1510,14 @@ pub mod test {
 
         fn navigate(
             &mut self,
-            state: Box<dyn Any>,
+            state: Rc<dyn Any>,
             _window: &mut Window,
             _: &mut Context<Self>,
         ) -> bool {
-            let state = *state.downcast::<String>().unwrap_or_default();
+            let state = state
+                .downcast_ref::<String>()
+                .map(|s| s.to_string())
+                .unwrap_or_default();
             if state != self.state {
                 self.state = state;
                 true

crates/workspace/src/pane.rs 🔗

@@ -211,6 +211,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.
@@ -417,6 +421,9 @@ struct NavHistoryState {
     backward_stack: VecDeque<NavigationEntry>,
     forward_stack: VecDeque<NavigationEntry>,
     closed_stack: VecDeque<NavigationEntry>,
+    tag_stack: VecDeque<(NavigationEntry, NavigationEntry)>,
+    tag_stack_pos: usize,
+    pending_tag_source: Option<NavigationEntry>,
     paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
     pane: WeakEntity<Pane>,
     next_timestamp: Arc<AtomicUsize>,
@@ -438,9 +445,10 @@ impl Default for NavigationMode {
     }
 }
 
+#[derive(Clone)]
 pub struct NavigationEntry {
     pub item: Arc<dyn WeakItemHandle>,
-    pub data: Option<Box<dyn Any + Send>>,
+    pub data: Option<Rc<dyn Any + Send>>,
     pub timestamp: usize,
     pub is_preview: bool,
 }
@@ -513,6 +521,9 @@ impl Pane {
                 backward_stack: Default::default(),
                 forward_stack: Default::default(),
                 closed_stack: Default::default(),
+                tag_stack: Default::default(),
+                tag_stack_pos: 0,
+                pending_tag_source: None,
                 paths_by_item: Default::default(),
                 pane: handle,
                 next_timestamp,
@@ -850,6 +861,24 @@ impl Pane {
         }
     }
 
+    pub fn go_to_older_tag(
+        &mut self,
+        _: &GoToOlderTag,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        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, window, cx)
+                        .detach_and_log_err(cx)
+                })
+            })
+        }
+    }
+
     fn history_updated(&mut self, cx: &mut Context<Self>) {
         self.toolbar.update(cx, |_, cx| cx.notify());
     }
@@ -3756,6 +3785,7 @@ impl Render for Pane {
             .on_action(cx.listener(Pane::toggle_zoom))
             .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(|pane: &mut Pane, action: &ActivateItem, window, cx| {
                     pane.activate_item(
@@ -3996,8 +4026,40 @@ impl ItemNavHistory {
         self.history.pop(NavigationMode::GoingBack, cx)
     }
 
-    pub fn pop_forward(&mut self, cx: &mut App) -> Option<NavigationEntry> {
-        self.history.pop(NavigationMode::GoingForward, cx)
+    pub fn start_tag_jump<D>(&mut self, data: Option<D>, cx: &mut App)
+    where
+        D: 'static + Any + Send,
+    {
+        if self
+            .item
+            .upgrade()
+            .is_some_and(|item| item.include_in_nav_history())
+        {
+            self.history.start_tag_jump(
+                data.map(|data| Rc::new(data) as Rc<dyn Any + Send>),
+                self.item.clone(),
+                self.is_preview,
+                cx,
+            );
+        }
+    }
+
+    pub fn finish_tag_jump<D>(&mut self, data: Option<D>, cx: &mut App)
+    where
+        D: 'static + Any + Send,
+    {
+        if self
+            .item
+            .upgrade()
+            .is_some_and(|item| item.include_in_nav_history())
+        {
+            self.history.finish_tag_jump(
+                data.map(|data| Rc::new(data) as Rc<dyn Any + Send>),
+                self.item.clone(),
+                self.is_preview,
+                cx,
+            );
+        }
     }
 }
 
@@ -4075,7 +4137,7 @@ impl NavHistory {
                 }
                 state.backward_stack.push_back(NavigationEntry {
                     item,
-                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
+                    data: data.map(|data| Rc::new(data) as Rc<dyn Any + Send>),
                     timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
                     is_preview,
                 });
@@ -4087,7 +4149,7 @@ impl NavHistory {
                 }
                 state.forward_stack.push_back(NavigationEntry {
                     item,
-                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
+                    data: data.map(|data| Rc::new(data) as Rc<dyn Any + Send>),
                     timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
                     is_preview,
                 });
@@ -4098,7 +4160,7 @@ impl NavHistory {
                 }
                 state.backward_stack.push_back(NavigationEntry {
                     item,
-                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
+                    data: data.map(|data| Rc::new(data) as Rc<dyn Any + Send>),
                     timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
                     is_preview,
                 });
@@ -4109,7 +4171,7 @@ impl NavHistory {
                 }
                 state.closed_stack.push_back(NavigationEntry {
                     item,
-                    data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
+                    data: data.map(|data| Rc::new(data) as Rc<dyn Any + Send>),
                     timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
                     is_preview,
                 });
@@ -4135,6 +4197,55 @@ impl NavHistory {
     pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
         self.0.lock().paths_by_item.get(&item_id).cloned()
     }
+
+    pub fn start_tag_jump(
+        &mut self,
+        data: Option<Rc<dyn Any + Send>>,
+        item: Arc<dyn WeakItemHandle>,
+        is_preview: bool,
+        _cx: &mut App,
+    ) {
+        self.0.lock().pending_tag_source.replace(NavigationEntry {
+            item,
+            data,
+            timestamp: 0,
+            is_preview,
+        });
+    }
+
+    pub fn finish_tag_jump(
+        &mut self,
+        data: Option<Rc<dyn Any + Send>>,
+        item: Arc<dyn WeakItemHandle>,
+        is_preview: bool,
+        _cx: &mut App,
+    ) {
+        let mut state = self.0.lock();
+        let Some(source) = state.pending_tag_source.take() else {
+            debug_panic!("Finished tag jump without starting one?");
+            return;
+        };
+        let dest = NavigationEntry {
+            item,
+            data,
+            timestamp: 0,
+            is_preview,
+        };
+        let truncate_to = state.tag_stack_pos;
+        state.tag_stack.truncate(truncate_to);
+        state.tag_stack.push_back((source, dest));
+        state.tag_stack_pos += 1;
+    }
+
+    pub fn tag_stack_back(&mut self) -> Option<NavigationEntry> {
+        let mut state = self.0.lock();
+        if state.tag_stack_pos > 0 {
+            state.tag_stack_pos -= 1;
+            Some(state.tag_stack[state.tag_stack_pos].0.clone())
+        } else {
+            None
+        }
+    }
 }
 
 impl NavHistoryState {

crates/workspace/src/workspace.rs 🔗

@@ -1927,6 +1927,127 @@ impl Workspace {
             .collect()
     }
 
+    fn navigate_tag_history(
+        &mut self,
+        pane: WeakEntity<Pane>,
+        window: &mut Window,
+        cx: &mut Context<Workspace>,
+    ) -> Task<Result<()>> {
+        let to_load = if let Some(pane) = pane.upgrade() {
+            pane.update(cx, |pane, cx| {
+                window.focus(&pane.focus_handle(cx));
+                loop {
+                    // Retrieve the weak item handle from the history.
+                    let entry = pane.nav_history_mut().tag_stack_back()?;
+
+                    // If the item is still present in this pane, then activate it.
+                    if let Some(index) = entry
+                        .item
+                        .upgrade()
+                        .and_then(|v| pane.index_for_item(v.as_ref()))
+                    {
+                        let prev_active_item_index = pane.active_item_index();
+                        pane.activate_item(index, true, true, window, cx);
+
+                        let mut navigated = prev_active_item_index != pane.active_item_index();
+                        if let Some(data) = entry.data {
+                            navigated |= pane.active_item()?.navigate(data, window, cx);
+                        }
+
+                        if navigated {
+                            break None;
+                        }
+                    } else {
+                        // If the item is no longer present in this pane, then retrieve its
+                        // path info in order to reopen it.
+                        break pane
+                            .nav_history()
+                            .path_for_item(entry.item.id())
+                            .map(|(project_path, abs_path)| (project_path, abs_path, entry));
+                    }
+                }
+            })
+        } else {
+            None
+        };
+
+        if let Some((project_path, abs_path, entry)) = to_load {
+            // If the item was no longer present, then load it again from its previous path, first try the local path
+            let open_by_project_path = self.load_path(project_path.clone(), window, cx);
+
+            cx.spawn_in(window, async move  |workspace, cx| {
+                let open_by_project_path = open_by_project_path.await;
+                let mut navigated = false;
+                match open_by_project_path
+                    .with_context(|| format!("Navigating to {project_path:?}"))
+                {
+                    Ok((project_entry_id, build_item)) => {
+                        let prev_active_item_id = pane.update(cx, |pane, _| {
+                            pane.active_item().map(|p| p.item_id())
+                        })?;
+
+                        pane.update_in(cx, |pane, window, cx| {
+                            let item = pane.open_item(
+                                project_entry_id,
+                                project_path,
+                                true,
+                                entry.is_preview,
+                                true,
+                                None,
+                                window, cx,
+                                build_item,
+                            );
+                            navigated |= Some(item.item_id()) != prev_active_item_id;
+                            if let Some(data) = entry.data {
+                                navigated |= item.navigate(data, window, cx);
+                            }
+                        })?;
+                    }
+                    Err(open_by_project_path_e) => {
+                        // Fall back to opening by abs path, in case an external file was opened and closed,
+                        // and its worktree is now dropped
+                        if let Some(abs_path) = abs_path {
+                            let prev_active_item_id = pane.update(cx, |pane, _| {
+                                pane.active_item().map(|p| p.item_id())
+                            })?;
+                            let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
+                                workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
+                            })?;
+                            match open_by_abs_path
+                                .await
+                                .with_context(|| format!("Navigating to {abs_path:?}"))
+                            {
+                                Ok(item) => {
+                                    pane.update_in(cx, |pane, window, cx| {
+                                        navigated |= Some(item.item_id()) != prev_active_item_id;
+                                        if let Some(data) = entry.data {
+                                            navigated |= item.navigate(data, window, cx);
+                                        }
+                                    })?;
+                                }
+                                Err(open_by_abs_path_e) => {
+                                    log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if !navigated {
+                    workspace
+                        .update_in(cx, |workspace, window, cx| {
+                            Self::navigate_tag_history(workspace, pane, window, cx)
+                        })?
+                        .await?;
+                }
+
+                Ok(())
+            })
+        } else {
+            Task::ready(Ok(()))
+        }
+    }
+
     fn navigate_history(
         &mut self,
         pane: WeakEntity<Pane>,