Navigate to previous positions in editors when using navigation history

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs       |  13 +++
crates/editor/src/items.rs        |  14 ++-
crates/workspace/src/pane.rs      | 139 ++++++++++++++++++--------------
crates/workspace/src/workspace.rs |   7 +
4 files changed, 107 insertions(+), 66 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -2399,6 +2399,8 @@ impl Editor {
             return;
         }
 
+        self.push_to_navigation_history(cx);
+
         let selection = Selection {
             id: post_inc(&mut self.next_selection_id),
             start: 0,
@@ -2421,6 +2423,8 @@ impl Editor {
             return;
         }
 
+        self.push_to_navigation_history(cx);
+
         let cursor = self.buffer.read(cx).read(cx).len();
         let selection = Selection {
             id: post_inc(&mut self.next_selection_id),
@@ -2432,6 +2436,15 @@ impl Editor {
         self.update_selections(vec![selection], Some(Autoscroll::Fit), cx);
     }
 
+    fn push_to_navigation_history(&self, cx: &mut ViewContext<Self>) {
+        if let Some(navigation) = &self.navigation {
+            if let Some(last_selection) = self.selections.iter().max_by_key(|s| s.id) {
+                let cursor = last_selection.head();
+                navigation.push(Some(cursor), cx);
+            }
+        }
+    }
+
     pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext<Self>) {
         let mut selection = self.local_selections::<usize>(cx).first().unwrap().clone();
         selection.set_head(self.buffer.read(cx).read(cx).len());

crates/editor/src/items.rs 🔗

@@ -1,5 +1,4 @@
-use crate::{Autoscroll, Editor, Event};
-use crate::{MultiBuffer, ToPoint as _};
+use crate::{Anchor, Autoscroll, Editor, Event, MultiBuffer, ToOffset, ToPoint as _};
 use anyhow::Result;
 use gpui::{
     elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext,
@@ -106,6 +105,13 @@ impl ItemView for Editor {
         BufferItemHandle(self.buffer.read(cx).as_singleton().unwrap())
     }
 
+    fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
+        if let Some(anchor) = data.downcast_ref::<Anchor>() {
+            let offset = anchor.to_offset(&self.buffer.read(cx).read(cx));
+            self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
+        }
+    }
+
     fn title(&self, cx: &AppContext) -> String {
         let filename = self
             .buffer()
@@ -134,9 +140,7 @@ impl ItemView for Editor {
     }
 
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(navigation) = self.navigation.as_ref() {
-            navigation.push::<(), _>(None, cx);
-        }
+        self.push_to_navigation_history(cx);
     }
 
     fn is_dirty(&self, cx: &AppContext) -> bool {

crates/workspace/src/pane.rs 🔗

@@ -84,6 +84,7 @@ struct NavigationHistory {
     paths_by_item: HashMap<usize, ProjectPath>,
 }
 
+#[derive(Copy, Clone)]
 enum NavigationHistoryMode {
     Normal,
     GoingBack,
@@ -116,77 +117,81 @@ impl Pane {
     }
 
     pub fn go_back(workspace: &mut Workspace, _: &GoBack, cx: &mut ViewContext<Workspace>) {
-        let project_path = workspace.active_pane().update(cx, |pane, cx| {
-            let mut navigation = pane.navigation.0.borrow_mut();
-            if let Some(entry) = navigation.backward_stack.pop() {
-                if let Some(index) = entry
-                    .item_view
-                    .upgrade(cx)
-                    .and_then(|v| pane.index_for_item_view(v.as_ref()))
-                {
-                    if let Some(item_view) = pane.active_item() {
-                        pane.navigation.0.borrow_mut().mode = NavigationHistoryMode::GoingBack;
-                        item_view.deactivated(cx);
-                        pane.navigation.0.borrow_mut().mode = NavigationHistoryMode::Normal;
-                    }
-
-                    pane.active_item_index = index;
-                    drop(navigation);
-                    pane.focus_active_item(cx);
-                    cx.notify();
-                } else {
-                    return navigation.paths_by_item.get(&entry.item_view.id()).cloned();
-                }
-            }
-
-            None
-        });
-
-        if let Some(project_path) = project_path {
-            let task = workspace.load_path(project_path, cx);
-            cx.spawn(|workspace, mut cx| {
-                async move {
-                    let item = task.await?;
-                    workspace.update(&mut cx, |workspace, cx| {
-                        let pane = workspace.active_pane().clone();
-                        pane.update(cx, |pane, cx| {
-                            pane.navigation.0.borrow_mut().mode = NavigationHistoryMode::GoingBack;
-                            pane.open_item(item, workspace, cx);
-                            pane.navigation.0.borrow_mut().mode = NavigationHistoryMode::Normal;
-                        });
-                    });
-                    Ok(())
-                }
-                .log_err()
-            })
-            .detach();
-        }
+        Self::navigate_history(workspace, NavigationHistoryMode::GoingBack, cx);
     }
 
-    pub fn go_forward(&mut self, _: &GoForward, cx: &mut ViewContext<Self>) {
-        if self.navigation.0.borrow().forward_stack.is_empty() {
-            return;
-        }
+    pub fn go_forward(workspace: &mut Workspace, _: &GoForward, cx: &mut ViewContext<Workspace>) {
+        Self::navigate_history(workspace, NavigationHistoryMode::GoingForward, cx);
+    }
 
-        if let Some(item_view) = self.active_item() {
-            self.navigation.0.borrow_mut().mode = NavigationHistoryMode::GoingForward;
-            item_view.deactivated(cx);
-            self.navigation.0.borrow_mut().mode = NavigationHistoryMode::Normal;
-        }
+    fn navigate_history(
+        workspace: &mut Workspace,
+        mode: NavigationHistoryMode,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<()> {
+        let (project_path, entry) = workspace.active_pane().update(cx, |pane, cx| {
+            // Retrieve the weak item handle from the history.
+            let entry = pane.navigation.pop(mode)?;
 
-        let mut navigation = self.navigation.0.borrow_mut();
-        if let Some(entry) = navigation.forward_stack.pop() {
+            // If the item is still present in this pane, then activate it.
             if let Some(index) = entry
                 .item_view
                 .upgrade(cx)
-                .and_then(|v| self.index_for_item_view(v.as_ref()))
+                .and_then(|v| pane.index_for_item_view(v.as_ref()))
             {
-                self.active_item_index = index;
-                drop(navigation);
-                self.focus_active_item(cx);
+                if let Some(item_view) = pane.active_item() {
+                    pane.navigation.set_mode(mode);
+                    item_view.deactivated(cx);
+                    pane.navigation.set_mode(NavigationHistoryMode::Normal);
+                }
+
+                pane.active_item_index = index;
+                pane.focus_active_item(cx);
+                if let Some(data) = entry.data {
+                    pane.active_item()?.navigate(data, cx);
+                }
                 cx.notify();
+                None
             }
-        }
+            // If the item is no longer present in this pane, then retrieve its
+            // project path in order to reopen it.
+            else {
+                pane.navigation
+                    .0
+                    .borrow_mut()
+                    .paths_by_item
+                    .get(&entry.item_view.id())
+                    .cloned()
+                    .map(|project_path| (project_path, entry))
+            }
+        })?;
+
+        // If the item was no longer present, then load it again from its previous path.
+        let task = workspace.load_path(project_path, cx);
+        cx.spawn(|workspace, mut cx| {
+            async move {
+                let item = task.await?;
+                workspace.update(&mut cx, |workspace, cx| {
+                    let pane = workspace.active_pane().clone();
+                    pane.update(cx, |pane, cx| {
+                        pane.navigation.set_mode(mode);
+                        let item_view = pane.open_item(item, workspace, cx);
+                        pane.navigation.set_mode(NavigationHistoryMode::Normal);
+
+                        if let Some(data) = entry.data {
+                            item_view.navigate(data, cx);
+                        }
+
+                        cx.notify();
+                    });
+                });
+                Ok(())
+            }
+            .log_err()
+        })
+        .detach();
+
+        None
     }
 
     pub fn open_item<T>(
@@ -511,6 +516,18 @@ impl View for Pane {
 }
 
 impl Navigation {
+    fn pop(&self, mode: NavigationHistoryMode) -> Option<NavigationEntry> {
+        match mode {
+            NavigationHistoryMode::Normal => None,
+            NavigationHistoryMode::GoingBack => self.0.borrow_mut().backward_stack.pop(),
+            NavigationHistoryMode::GoingForward => self.0.borrow_mut().forward_stack.pop(),
+        }
+    }
+
+    fn set_mode(&self, mode: NavigationHistoryMode) {
+        self.0.borrow_mut().mode = mode;
+    }
+
     pub fn push<D: 'static + Any, T: ItemView>(&self, data: Option<D>, cx: &mut ViewContext<T>) {
         let mut state = self.0.borrow_mut();
         match state.mode {

crates/workspace/src/workspace.rs 🔗

@@ -33,6 +33,7 @@ use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItem
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use std::{
+    any::Any,
     future::Future,
     hash::{Hash, Hasher},
     path::{Path, PathBuf},
@@ -148,6 +149,7 @@ pub trait ItemView: View {
 
     fn added_to_pane(&mut self, _: Rc<Navigation>, _: &mut ViewContext<Self>) {}
     fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
+    fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) {}
     fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle;
     fn title(&self, cx: &AppContext) -> String;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
@@ -211,6 +213,7 @@ pub trait ItemViewHandle {
     fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
     fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>);
     fn deactivated(&self, cx: &mut MutableAppContext);
+    fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext);
     fn id(&self) -> usize;
     fn to_any(&self) -> AnyViewHandle;
     fn is_dirty(&self, cx: &AppContext) -> bool;
@@ -368,6 +371,10 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         self.update(cx, |this, cx| this.deactivated(cx));
     }
 
+    fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) {
+        self.update(cx, |this, cx| this.navigate(data, cx));
+    }
+
     fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
         self.update(cx, |item, cx| item.save(cx))
     }