Merge pull request #1297 from zed-industries/back-and-forward-buttons

Max Brunsfeld created

Back and forward buttons

Change summary

assets/icons/arrow-left.svg       |   3 
assets/icons/arrow-right.svg      |   3 
crates/editor/src/editor.rs       |  57 ++++++++-----
crates/theme/src/theme.rs         |  30 +++----
crates/workspace/src/pane.rs      | 131 +++++++++++++++++++++-----------
crates/workspace/src/toolbar.rs   |  90 ++++++++++++++++++++--
crates/workspace/src/workspace.rs | 123 ++++++++++++++++++++++++++++--
styles/src/styleTree/workspace.ts |  13 +++
8 files changed, 347 insertions(+), 103 deletions(-)

Detailed changes

assets/icons/arrow-left.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 3.99999C8 4.31671 7.76023 4.57258 7.44352 4.57258H1.95565L3.8416 6.45853C4.06527 6.6822 4.06527 7.04454 3.8416 7.2682C3.72887 7.38004 3.58215 7.43551 3.43542 7.43551C3.2887 7.43551 3.14233 7.37959 3.03068 7.26776L0.16775 4.40483C-0.0559165 4.18116 -0.0559165 3.81883 0.16775 3.59516L3.03068 0.732233C3.25434 0.508567 3.61668 0.508567 3.84035 0.732233C4.06401 0.955899 4.06401 1.31824 3.84035 1.5419L1.95565 3.42741H7.44352C7.76023 3.42741 8 3.68328 8 3.99999Z" fill="#839496"/>
+</svg>

assets/icons/arrow-right.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.83265 4.40382L4.97532 7.26115C4.8646 7.37365 4.71816 7.42901 4.57172 7.42901C4.42528 7.42901 4.2792 7.37321 4.16777 7.26159C3.94454 7.03836 3.94454 6.67673 4.16777 6.4535L6.05039 4.57169H0.571465C0.255909 4.57169 0 4.31631 0 4.00022C0 3.68413 0.255731 3.42876 0.571287 3.42876H6.05021L4.16795 1.54649C3.94472 1.32326 3.94472 0.961634 4.16795 0.738405C4.39117 0.515177 4.75281 0.515177 4.97603 0.738405L7.83336 3.59573C8.0557 3.81985 8.0557 4.18059 7.83247 4.40382H7.83265Z" fill="#FDF6E3"/>
+</svg>

crates/editor/src/editor.rs 🔗

@@ -4065,13 +4065,16 @@ impl Editor {
                 }
             }
 
-            nav_history.push(Some(NavigationData {
-                cursor_anchor: position,
-                cursor_position: point,
-                scroll_position: self.scroll_position,
-                scroll_top_anchor: self.scroll_top_anchor.clone(),
-                scroll_top_row,
-            }));
+            nav_history.push(
+                Some(NavigationData {
+                    cursor_anchor: position,
+                    cursor_position: point,
+                    scroll_position: self.scroll_position,
+                    scroll_top_anchor: self.scroll_top_anchor.clone(),
+                    scroll_top_row,
+                }),
+                cx,
+            );
         }
     }
 
@@ -4669,7 +4672,7 @@ impl Editor {
         definitions: Vec<LocationLink>,
         cx: &mut ViewContext<Workspace>,
     ) {
-        let nav_history = workspace.active_pane().read(cx).nav_history().clone();
+        let pane = workspace.active_pane().clone();
         for definition in definitions {
             let range = definition
                 .target
@@ -4681,13 +4684,13 @@ 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.
                 if editor_handle != target_editor_handle {
-                    nav_history.borrow_mut().disable();
+                    pane.update(cx, |pane, _| pane.disable_history());
                 }
                 target_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
                     s.select_ranges([range]);
                 });
 
-                nav_history.borrow_mut().enable();
+                pane.update(cx, |pane, _| pane.enable_history());
             });
         }
     }
@@ -5641,8 +5644,8 @@ impl Editor {
         editor_handle.update(cx, |editor, cx| {
             editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx);
         });
-        let nav_history = workspace.active_pane().read(cx).nav_history().clone();
-        nav_history.borrow_mut().disable();
+        let pane = workspace.active_pane().clone();
+        pane.update(cx, |pane, _| pane.disable_history());
 
         // We defer the pane interaction because we ourselves are a workspace item
         // and activating a new item causes the pane to call a method on us reentrantly,
@@ -5657,7 +5660,7 @@ impl Editor {
                 });
             }
 
-            nav_history.borrow_mut().enable();
+            pane.update(cx, |pane, _| pane.enable_history());
         });
     }
 
@@ -6241,7 +6244,7 @@ mod tests {
         assert_set_eq,
         test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
     };
-    use workspace::{FollowableItem, ItemHandle};
+    use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
 
     #[gpui::test]
     fn test_edit_events(cx: &mut MutableAppContext) {
@@ -6589,12 +6592,20 @@ mod tests {
     fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));
         use workspace::Item;
-        let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default()));
+        let pane = cx.add_view(Default::default(), |cx| Pane::new(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
 
         cx.add_window(Default::default(), |cx| {
             let mut editor = build_editor(buffer.clone(), cx);
-            editor.nav_history = Some(ItemNavHistory::new(nav_history.clone(), &cx.handle()));
+            let handle = cx.handle();
+            editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
+
+            fn pop_history(
+                editor: &mut Editor,
+                cx: &mut MutableAppContext,
+            ) -> Option<NavigationEntry> {
+                editor.nav_history.as_mut().unwrap().pop_backward(cx)
+            }
 
             // Move the cursor a small distance.
             // Nothing is added to the navigation history.
@@ -6604,21 +6615,21 @@ mod tests {
             editor.change_selections(None, cx, |s| {
                 s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
             });
-            assert!(nav_history.borrow_mut().pop_backward().is_none());
+            assert!(pop_history(&mut editor, cx).is_none());
 
             // Move the cursor a large distance.
             // The history can jump back to the previous position.
             editor.change_selections(None, cx, |s| {
                 s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
             });
-            let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
+            let nav_entry = pop_history(&mut editor, cx).unwrap();
             editor.navigate(nav_entry.data.unwrap(), cx);
             assert_eq!(nav_entry.item.id(), cx.view_id());
             assert_eq!(
                 editor.selections.display_ranges(cx),
                 &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
             );
-            assert!(nav_history.borrow_mut().pop_backward().is_none());
+            assert!(pop_history(&mut editor, cx).is_none());
 
             // Move the cursor a small distance via the mouse.
             // Nothing is added to the navigation history.
@@ -6628,7 +6639,7 @@ mod tests {
                 editor.selections.display_ranges(cx),
                 &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
             );
-            assert!(nav_history.borrow_mut().pop_backward().is_none());
+            assert!(pop_history(&mut editor, cx).is_none());
 
             // Move the cursor a large distance via the mouse.
             // The history can jump back to the previous position.
@@ -6638,14 +6649,14 @@ mod tests {
                 editor.selections.display_ranges(cx),
                 &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
             );
-            let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
+            let nav_entry = pop_history(&mut editor, cx).unwrap();
             editor.navigate(nav_entry.data.unwrap(), cx);
             assert_eq!(nav_entry.item.id(), cx.view_id());
             assert_eq!(
                 editor.selections.display_ranges(cx),
                 &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
             );
-            assert!(nav_history.borrow_mut().pop_backward().is_none());
+            assert!(pop_history(&mut editor, cx).is_none());
 
             // Set scroll position to check later
             editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
@@ -6658,7 +6669,7 @@ mod tests {
             assert_ne!(editor.scroll_position, original_scroll_position);
             assert_ne!(editor.scroll_top_anchor, original_scroll_top_anchor);
 
-            let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
+            let nav_entry = pop_history(&mut editor, cx).unwrap();
             editor.navigate(nav_entry.data.unwrap(), cx);
             assert_eq!(editor.scroll_position, original_scroll_position);
             assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);

crates/theme/src/theme.rs 🔗

@@ -108,6 +108,7 @@ pub struct Toolbar {
     pub container: ContainerStyle,
     pub height: f32,
     pub item_spacing: f32,
+    pub nav_button: Interactive<IconButton>,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -509,28 +510,23 @@ pub struct Interactive<T> {
     pub default: T,
     pub hover: Option<T>,
     pub active: Option<T>,
-    pub active_hover: Option<T>,
+    pub disabled: Option<T>,
 }
 
 impl<T> Interactive<T> {
     pub fn style_for(&self, state: MouseState, active: bool) -> &T {
         if active {
-            if state.hovered {
-                self.active_hover
-                    .as_ref()
-                    .or(self.active.as_ref())
-                    .unwrap_or(&self.default)
-            } else {
-                self.active.as_ref().unwrap_or(&self.default)
-            }
+            self.active.as_ref().unwrap_or(&self.default)
+        } else if state.hovered {
+            self.hover.as_ref().unwrap_or(&self.default)
         } else {
-            if state.hovered {
-                self.hover.as_ref().unwrap_or(&self.default)
-            } else {
-                &self.default
-            }
+            &self.default
         }
     }
+
+    pub fn disabled_style(&self) -> &T {
+        self.disabled.as_ref().unwrap_or(&self.default)
+    }
 }
 
 impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
@@ -544,7 +540,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
             default: Value,
             hover: Option<Value>,
             active: Option<Value>,
-            active_hover: Option<Value>,
+            disabled: Option<Value>,
         }
 
         let json = Helper::deserialize(deserializer)?;
@@ -570,14 +566,14 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
 
         let hover = deserialize_state(json.hover)?;
         let active = deserialize_state(json.active)?;
-        let active_hover = deserialize_state(json.active_hover)?;
+        let disabled = deserialize_state(json.disabled)?;
         let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
 
         Ok(Interactive {
             default,
             hover,
             active,
-            active_hover,
+            disabled,
         })
     }
 }

crates/workspace/src/pane.rs 🔗

@@ -136,13 +136,13 @@ pub struct ItemNavHistory {
     item: Rc<dyn WeakItemHandle>,
 }
 
-#[derive(Default)]
-pub struct NavHistory {
+struct NavHistory {
     mode: NavigationMode,
     backward_stack: VecDeque<NavigationEntry>,
     forward_stack: VecDeque<NavigationEntry>,
     closed_stack: VecDeque<NavigationEntry>,
     paths_by_item: HashMap<usize, ProjectPath>,
+    pane: WeakViewHandle<Pane>,
 }
 
 #[derive(Copy, Clone)]
@@ -168,17 +168,28 @@ pub struct NavigationEntry {
 
 impl Pane {
     pub fn new(cx: &mut ViewContext<Self>) -> Self {
+        let handle = cx.weak_handle();
         Self {
             items: Vec::new(),
             active_item_index: 0,
             autoscroll: false,
-            nav_history: Default::default(),
-            toolbar: cx.add_view(|_| Toolbar::new()),
+            nav_history: Rc::new(RefCell::new(NavHistory {
+                mode: NavigationMode::Normal,
+                backward_stack: Default::default(),
+                forward_stack: Default::default(),
+                closed_stack: Default::default(),
+                paths_by_item: Default::default(),
+                pane: handle.clone(),
+            })),
+            toolbar: cx.add_view(|_| Toolbar::new(handle)),
         }
     }
 
-    pub fn nav_history(&self) -> &Rc<RefCell<NavHistory>> {
-        &self.nav_history
+    pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
+        ItemNavHistory {
+            history: self.nav_history.clone(),
+            item: Rc::new(item.downgrade()),
+        }
     }
 
     pub fn activate(&self, cx: &mut ViewContext<Self>) {
@@ -223,6 +234,26 @@ impl Pane {
         )
     }
 
+    pub fn disable_history(&mut self) {
+        self.nav_history.borrow_mut().disable();
+    }
+
+    pub fn enable_history(&mut self) {
+        self.nav_history.borrow_mut().enable();
+    }
+
+    pub fn can_navigate_backward(&self) -> bool {
+        !self.nav_history.borrow().backward_stack.is_empty()
+    }
+
+    pub fn can_navigate_forward(&self) -> bool {
+        !self.nav_history.borrow().forward_stack.is_empty()
+    }
+
+    fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
+        self.toolbar.update(cx, |_, cx| cx.notify());
+    }
+
     fn navigate_history(
         workspace: &mut Workspace,
         pane: ViewHandle<Pane>,
@@ -234,7 +265,7 @@ impl Pane {
         let to_load = pane.update(cx, |pane, cx| {
             loop {
                 // Retrieve the weak item handle from the history.
-                let entry = pane.nav_history.borrow_mut().pop(mode)?;
+                let entry = pane.nav_history.borrow_mut().pop(mode, cx)?;
 
                 // If the item is still present in this pane, then activate it.
                 if let Some(index) = entry
@@ -367,7 +398,6 @@ impl Pane {
             return;
         }
 
-        item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
         item.added_to_pane(workspace, pane.clone(), cx);
         pane.update(cx, |pane, cx| {
             // If there is already an active item, then insert the new item
@@ -625,11 +655,16 @@ impl Pane {
                             .borrow_mut()
                             .set_mode(NavigationMode::Normal);
 
-                        let mut nav_history = pane.nav_history().borrow_mut();
                         if let Some(path) = item.project_path(cx) {
-                            nav_history.paths_by_item.insert(item.id(), path);
+                            pane.nav_history
+                                .borrow_mut()
+                                .paths_by_item
+                                .insert(item.id(), path);
                         } else {
-                            nav_history.paths_by_item.remove(&item.id());
+                            pane.nav_history
+                                .borrow_mut()
+                                .paths_by_item
+                                .remove(&item.id());
                         }
                     }
                 });
@@ -953,57 +988,56 @@ impl View for Pane {
 }
 
 impl ItemNavHistory {
-    pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
-        Self {
-            history,
-            item: Rc::new(item.downgrade()),
-        }
+    pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
+        self.history.borrow_mut().push(data, self.item.clone(), cx);
     }
 
-    pub fn history(&self) -> Rc<RefCell<NavHistory>> {
-        self.history.clone()
+    pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
+        self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
     }
 
-    pub fn push<D: 'static + Any>(&self, data: Option<D>) {
-        self.history.borrow_mut().push(data, self.item.clone());
+    pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
+        self.history
+            .borrow_mut()
+            .pop(NavigationMode::GoingForward, cx)
     }
 }
 
 impl NavHistory {
-    pub fn disable(&mut self) {
-        self.mode = NavigationMode::Disabled;
-    }
-
-    pub fn enable(&mut self) {
-        self.mode = NavigationMode::Normal;
-    }
-
-    pub fn pop_backward(&mut self) -> Option<NavigationEntry> {
-        self.backward_stack.pop_back()
+    fn set_mode(&mut self, mode: NavigationMode) {
+        self.mode = mode;
     }
 
-    pub fn pop_forward(&mut self) -> Option<NavigationEntry> {
-        self.forward_stack.pop_back()
+    fn disable(&mut self) {
+        self.mode = NavigationMode::Disabled;
     }
 
-    pub fn pop_closed(&mut self) -> Option<NavigationEntry> {
-        self.closed_stack.pop_back()
+    fn enable(&mut self) {
+        self.mode = NavigationMode::Normal;
     }
 
-    fn pop(&mut self, mode: NavigationMode) -> Option<NavigationEntry> {
-        match mode {
-            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => None,
-            NavigationMode::GoingBack => self.pop_backward(),
-            NavigationMode::GoingForward => self.pop_forward(),
-            NavigationMode::ReopeningClosedItem => self.pop_closed(),
+    fn pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
+        let entry = match mode {
+            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
+                return None
+            }
+            NavigationMode::GoingBack => &mut self.backward_stack,
+            NavigationMode::GoingForward => &mut self.forward_stack,
+            NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
         }
+        .pop_back();
+        if entry.is_some() {
+            self.did_update(cx);
+        }
+        entry
     }
 
-    fn set_mode(&mut self, mode: NavigationMode) {
-        self.mode = mode;
-    }
-
-    pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
+    fn push<D: 'static + Any>(
+        &mut self,
+        data: Option<D>,
+        item: Rc<dyn WeakItemHandle>,
+        cx: &mut MutableAppContext,
+    ) {
         match self.mode {
             NavigationMode::Disabled => {}
             NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
@@ -1044,5 +1078,12 @@ impl NavHistory {
                 });
             }
         }
+        self.did_update(cx);
+    }
+
+    fn did_update(&self, cx: &mut MutableAppContext) {
+        if let Some(pane) = self.pane.upgrade(cx) {
+            cx.defer(move |cx| pane.update(cx, |pane, cx| pane.history_updated(cx)));
+        }
     }
 }

crates/workspace/src/toolbar.rs 🔗

@@ -1,7 +1,7 @@
-use crate::ItemHandle;
+use crate::{ItemHandle, Pane};
 use gpui::{
-    elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
-    View, ViewContext, ViewHandle,
+    elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox, Entity,
+    MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use settings::Settings;
 
@@ -42,6 +42,7 @@ pub enum ToolbarItemLocation {
 
 pub struct Toolbar {
     active_pane_item: Option<Box<dyn ItemHandle>>,
+    pane: WeakViewHandle<Pane>,
     items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
 }
 
@@ -60,6 +61,7 @@ impl View for Toolbar {
         let mut primary_left_items = Vec::new();
         let mut primary_right_items = Vec::new();
         let mut secondary_item = None;
+        let spacing = theme.item_spacing;
 
         for (item, position) in &self.items {
             match *position {
@@ -68,7 +70,7 @@ impl View for Toolbar {
                     let left_item = ChildView::new(item.as_ref())
                         .aligned()
                         .contained()
-                        .with_margin_right(theme.item_spacing);
+                        .with_margin_right(spacing);
                     if let Some((flex, expanded)) = flex {
                         primary_left_items.push(left_item.flex(flex, expanded).boxed());
                     } else {
@@ -79,7 +81,7 @@ impl View for Toolbar {
                     let right_item = ChildView::new(item.as_ref())
                         .aligned()
                         .contained()
-                        .with_margin_left(theme.item_spacing)
+                        .with_margin_left(spacing)
                         .flex_float();
                     if let Some((flex, expanded)) = flex {
                         primary_right_items.push(right_item.flex(flex, expanded).boxed());
@@ -98,26 +100,98 @@ impl View for Toolbar {
             }
         }
 
+        let pane = self.pane.clone();
+        let mut enable_go_backward = false;
+        let mut enable_go_forward = false;
+        if let Some(pane) = pane.upgrade(cx) {
+            let pane = pane.read(cx);
+            enable_go_backward = pane.can_navigate_backward();
+            enable_go_forward = pane.can_navigate_forward();
+        }
+
+        let container_style = theme.container;
+        let height = theme.height;
+        let button_style = theme.nav_button;
+
         Flex::column()
             .with_child(
                 Flex::row()
+                    .with_child(nav_button(
+                        "icons/arrow-left.svg",
+                        button_style,
+                        enable_go_backward,
+                        spacing,
+                        super::GoBack {
+                            pane: Some(pane.clone()),
+                        },
+                        cx,
+                    ))
+                    .with_child(nav_button(
+                        "icons/arrow-right.svg",
+                        button_style,
+                        enable_go_forward,
+                        spacing,
+                        super::GoForward {
+                            pane: Some(pane.clone()),
+                        },
+                        cx,
+                    ))
                     .with_children(primary_left_items)
                     .with_children(primary_right_items)
                     .constrained()
-                    .with_height(theme.height)
+                    .with_height(height)
                     .boxed(),
             )
             .with_children(secondary_item)
             .contained()
-            .with_style(theme.container)
+            .with_style(container_style)
             .boxed()
     }
 }
 
+fn nav_button<A: Action + Clone>(
+    svg_path: &'static str,
+    style: theme::Interactive<theme::IconButton>,
+    enabled: bool,
+    spacing: f32,
+    action: A,
+    cx: &mut RenderContext<Toolbar>,
+) -> ElementBox {
+    MouseEventHandler::new::<A, _, _>(0, cx, |state, _| {
+        let style = if enabled {
+            style.style_for(state, false)
+        } else {
+            style.disabled_style()
+        };
+        Svg::new(svg_path)
+            .with_color(style.color)
+            .constrained()
+            .with_width(style.icon_width)
+            .aligned()
+            .contained()
+            .with_style(style.container)
+            .constrained()
+            .with_width(style.button_width)
+            .with_height(style.button_width)
+            .aligned()
+            .boxed()
+    })
+    .with_cursor_style(if enabled {
+        CursorStyle::PointingHand
+    } else {
+        CursorStyle::default()
+    })
+    .on_mouse_down(move |_, cx| cx.dispatch_action(action.clone()))
+    .contained()
+    .with_margin_right(spacing)
+    .boxed()
+}
+
 impl Toolbar {
-    pub fn new() -> Self {
+    pub fn new(pane: WeakViewHandle<Pane>) -> Self {
         Self {
             active_pane_item: None,
+            pane,
             items: Default::default(),
         }
     }

crates/workspace/src/workspace.rs 🔗

@@ -414,7 +414,6 @@ pub trait ItemHandle: 'static + fmt::Debug {
     fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
     fn is_singleton(&self, cx: &AppContext) -> bool;
     fn boxed_clone(&self) -> Box<dyn ItemHandle>;
-    fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
     fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
     fn added_to_pane(
         &self,
@@ -484,12 +483,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         Box::new(self.clone())
     }
 
-    fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
-        self.update(cx, |item, cx| {
-            item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
-        })
-    }
-
     fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
         self.update(cx, |item, cx| {
             cx.add_option_view(|cx| item.clone_on_split(cx))
@@ -503,6 +496,9 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         pane: ViewHandle<Pane>,
         cx: &mut ViewContext<Workspace>,
     ) {
+        let history = pane.read(cx).nav_history_for_item(self);
+        self.update(cx, |this, cx| this.set_nav_history(history, cx));
+
         if let Some(followed_item) = self.to_followable_item_handle(cx) {
             if let Some(message) = followed_item.to_state_proto(cx) {
                 workspace.update_followers(
@@ -3146,25 +3142,104 @@ mod tests {
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
     }
 
-    #[derive(Clone)]
+    #[gpui::test]
+    async fn test_pane_navigation(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        deterministic.forbid_parking();
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, [], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+
+        let item = cx.add_view(window_id, |_| {
+            let mut item = TestItem::new();
+            item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
+            item
+        });
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+        let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
+        let toolbar_notify_count = Rc::new(RefCell::new(0));
+
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item.clone()), cx);
+            let toolbar_notification_count = toolbar_notify_count.clone();
+            cx.observe(&toolbar, move |_, _, _| {
+                *toolbar_notification_count.borrow_mut() += 1
+            })
+            .detach();
+        });
+
+        pane.read_with(cx, |pane, _| {
+            assert!(!pane.can_navigate_backward());
+            assert!(!pane.can_navigate_forward());
+        });
+
+        item.update(cx, |item, cx| {
+            item.set_state("one".to_string(), cx);
+        });
+
+        // Toolbar must be notified to re-render the navigation buttons
+        assert_eq!(*toolbar_notify_count.borrow(), 1);
+
+        pane.read_with(cx, |pane, _| {
+            assert!(pane.can_navigate_backward());
+            assert!(!pane.can_navigate_forward());
+        });
+
+        workspace
+            .update(cx, |workspace, cx| {
+                Pane::go_back(workspace, Some(pane.clone()), cx)
+            })
+            .await;
+
+        assert_eq!(*toolbar_notify_count.borrow(), 3);
+        pane.read_with(cx, |pane, _| {
+            assert!(!pane.can_navigate_backward());
+            assert!(pane.can_navigate_forward());
+        });
+    }
+
     struct TestItem {
+        state: String,
         save_count: usize,
         save_as_count: usize,
         reload_count: usize,
         is_dirty: bool,
+        is_singleton: bool,
         has_conflict: bool,
         project_entry_ids: Vec<ProjectEntryId>,
         project_path: Option<ProjectPath>,
-        is_singleton: bool,
+        nav_history: Option<ItemNavHistory>,
     }
 
     enum TestItemEvent {
         Edit,
     }
 
+    impl Clone for TestItem {
+        fn clone(&self) -> Self {
+            Self {
+                state: self.state.clone(),
+                save_count: self.save_count,
+                save_as_count: self.save_as_count,
+                reload_count: self.reload_count,
+                is_dirty: self.is_dirty,
+                is_singleton: self.is_singleton,
+                has_conflict: self.has_conflict,
+                project_entry_ids: self.project_entry_ids.clone(),
+                project_path: self.project_path.clone(),
+                nav_history: None,
+            }
+        }
+    }
+
     impl TestItem {
         fn new() -> Self {
             Self {
+                state: String::new(),
                 save_count: 0,
                 save_as_count: 0,
                 reload_count: 0,
@@ -3173,6 +3248,18 @@ mod tests {
                 project_entry_ids: Vec::new(),
                 project_path: None,
                 is_singleton: true,
+                nav_history: None,
+            }
+        }
+
+        fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
+            self.push_to_nav_history(cx);
+            self.state = state;
+        }
+
+        fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
+            if let Some(history) = &mut self.nav_history {
+                history.push(Some(Box::new(self.state.clone())), cx);
             }
         }
     }
@@ -3208,7 +3295,23 @@ mod tests {
             self.is_singleton
         }
 
-        fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
+        fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
+            self.nav_history = Some(history);
+        }
+
+        fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
+            let state = *state.downcast::<String>().unwrap_or_default();
+            if state != self.state {
+                self.state = state;
+                true
+            } else {
+                false
+            }
+        }
+
+        fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+            self.push_to_nav_history(cx);
+        }
 
         fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
         where

styles/src/styleTree/workspace.ts 🔗

@@ -139,6 +139,19 @@ export default function workspace(theme: Theme) {
       background: backgroundColor(theme, 500),
       border: border(theme, "secondary", { bottom: true }),
       itemSpacing: 8,
+      navButton: {
+        color: iconColor(theme, "primary"),
+        iconWidth: 8,
+        buttonWidth: 18,
+        cornerRadius: 6,
+        hover: {
+          color: iconColor(theme, "active"),
+          background: backgroundColor(theme, 300),
+        },
+        disabled: {
+          color: iconColor(theme, "muted")
+        },
+      },
       padding: { left: 16, right: 8, top: 4, bottom: 4 },
     },
     breadcrumbs: {