Start laying the foundation for a per-pane navigation system

Antonio Scandurra created

Change summary

crates/editor/src/editor.rs       |  6 +
crates/editor/src/items.rs        | 18 +++++-
crates/workspace/Cargo.toml       |  2 
crates/workspace/src/pane.rs      | 92 ++++++++++++++++++++++++++++++--
crates/workspace/src/workspace.rs | 33 ++++++++++-
5 files changed, 135 insertions(+), 16 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -41,6 +41,7 @@ use std::{
     iter::{self, FromIterator},
     mem,
     ops::{Deref, Range, RangeInclusive, Sub},
+    rc::Rc,
     sync::Arc,
     time::{Duration, Instant},
 };
@@ -48,7 +49,7 @@ use sum_tree::Bias;
 use text::rope::TextDimension;
 use theme::{DiagnosticStyle, EditorStyle};
 use util::post_inc;
-use workspace::{PathOpener, Workspace};
+use workspace::{Navigation, PathOpener, Workspace};
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
@@ -377,6 +378,7 @@ pub struct Editor {
     mode: EditorMode,
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
+    navigation: Option<Rc<Navigation>>,
 }
 
 pub struct EditorSnapshot {
@@ -457,6 +459,7 @@ impl Editor {
         let mut clone = Self::new(self.buffer.clone(), self.build_settings.clone(), cx);
         clone.scroll_position = self.scroll_position;
         clone.scroll_top_anchor = self.scroll_top_anchor.clone();
+        clone.navigation = self.navigation.clone();
         clone
     }
 
@@ -506,6 +509,7 @@ impl Editor {
             mode: EditorMode::Full,
             placeholder_text: None,
             highlighted_rows: None,
+            navigation: None,
         };
         let selection = Selection {
             id: post_inc(&mut this.next_selection_id),

crates/editor/src/items.rs 🔗

@@ -10,11 +10,12 @@ use postage::watch;
 use project::{File, ProjectPath, Worktree};
 use std::fmt::Write;
 use std::path::Path;
+use std::rc::Rc;
 use text::{Point, Selection};
 use util::TryFutureExt;
 use workspace::{
-    ItemHandle, ItemView, ItemViewHandle, PathOpener, Settings, StatusItemView, WeakItemHandle,
-    Workspace,
+    ItemHandle, ItemView, ItemViewHandle, Navigation, PathOpener, Settings, StatusItemView,
+    WeakItemHandle, Workspace,
 };
 
 pub struct BufferOpener;
@@ -46,16 +47,19 @@ impl ItemHandle for BufferItemHandle {
         &self,
         window_id: usize,
         workspace: &Workspace,
+        navigation: Rc<Navigation>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx));
         let weak_buffer = buffer.downgrade();
         Box::new(cx.add_view(window_id, |cx| {
-            Editor::for_buffer(
+            let mut editor = Editor::for_buffer(
                 buffer,
                 crate::settings_builder(weak_buffer, workspace.settings()),
                 cx,
-            )
+            );
+            editor.navigation = Some(navigation);
+            editor
         }))
     }
 
@@ -129,6 +133,12 @@ impl ItemView for Editor {
         Some(self.clone(cx))
     }
 
+    fn activated(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(navigation) = self.navigation.as_ref() {
+            navigation.push::<(), _>(None, cx);
+        }
+    }
+
     fn is_dirty(&self, cx: &AppContext) -> bool {
         self.buffer().read(cx).read(cx).is_dirty()
     }

crates/workspace/Cargo.toml 🔗

@@ -1,7 +1,7 @@
 [package]
 name = "workspace"
 version = "0.1.0"
-edition = "2018"
+edition = "2021"
 
 [lib]
 path = "src/workspace.rs"

crates/workspace/src/pane.rs 🔗

@@ -1,5 +1,6 @@
 use super::{ItemViewHandle, SplitDirection};
-use crate::{ItemHandle, Settings, Workspace};
+use crate::{ItemHandle, ItemView, Settings, WeakItemViewHandle, Workspace};
+use collections::HashMap;
 use gpui::{
     action,
     elements::*,
@@ -9,7 +10,8 @@ use gpui::{
     Entity, MutableAppContext, Quad, RenderContext, View, ViewContext,
 };
 use postage::watch;
-use std::cmp;
+use project::ProjectPath;
+use std::{any::Any, cell::RefCell, cmp, rc::Rc};
 
 action!(Split, SplitDirection);
 action!(ActivateItem, usize);
@@ -17,6 +19,8 @@ action!(ActivatePrevItem);
 action!(ActivateNextItem);
 action!(CloseActiveItem);
 action!(CloseItem, usize);
+action!(GoBack);
+action!(GoForward);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
@@ -37,6 +41,8 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, action: &Split, cx| {
         pane.split(action.0, cx);
     });
+    cx.add_action(Pane::go_back);
+    cx.add_action(Pane::go_forward);
 
     cx.add_bindings(vec![
         Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
@@ -46,6 +52,8 @@ pub fn init(cx: &mut MutableAppContext) {
         Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
         Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
         Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
+        Binding::new("ctrl-", GoBack, Some("Pane")),
+        Binding::new("ctrl-shift-_", GoForward, Some("Pane")),
     ]);
 }
 
@@ -61,6 +69,22 @@ pub struct Pane {
     item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
     active_item: usize,
     settings: watch::Receiver<Settings>,
+    navigation: Rc<Navigation>,
+}
+
+#[derive(Default)]
+pub struct Navigation(RefCell<NavigationHistory>);
+
+#[derive(Default)]
+struct NavigationHistory {
+    backward_stack: Vec<NavigationEntry>,
+    forward_stack: Vec<NavigationEntry>,
+    paths_by_item: HashMap<usize, ProjectPath>,
+}
+
+struct NavigationEntry {
+    item_view: Box<dyn WeakItemViewHandle>,
+    data: Option<Box<dyn Any>>,
 }
 
 impl Pane {
@@ -69,6 +93,7 @@ impl Pane {
             item_views: Vec::new(),
             active_item: 0,
             settings,
+            navigation: Default::default(),
         }
     }
 
@@ -76,6 +101,16 @@ impl Pane {
         cx.emit(Event::Activate);
     }
 
+    pub fn go_back(&mut self, _: &GoBack, cx: &mut ViewContext<Self>) {
+        let mut navigation = self.navigation.0.borrow_mut();
+        if let Some(entry) = navigation.go_back() {}
+    }
+
+    pub fn go_forward(&mut self, _: &GoForward, cx: &mut ViewContext<Self>) {
+        let mut navigation = self.navigation.0.borrow_mut();
+        if let Some(entry) = navigation.go_forward() {}
+    }
+
     pub fn open_item<T>(
         &mut self,
         item_handle: T,
@@ -93,14 +128,15 @@ impl Pane {
             }
         }
 
-        let item_view = item_handle.add_view(cx.window_id(), workspace, cx);
+        let item_view =
+            item_handle.add_view(cx.window_id(), workspace, self.navigation.clone(), cx);
         self.add_item_view(item_view.boxed_clone(), cx);
         item_view
     }
 
     pub fn add_item_view(
         &mut self,
-        item_view: Box<dyn ItemViewHandle>,
+        mut item_view: Box<dyn ItemViewHandle>,
         cx: &mut ViewContext<Self>,
     ) {
         item_view.added_to_pane(cx);
@@ -142,6 +178,7 @@ impl Pane {
         if index < self.item_views.len() {
             self.active_item = index;
             self.focus_active_item(cx);
+            self.item_views[index].1.activated(cx);
             cx.notify();
         }
     }
@@ -172,8 +209,21 @@ impl Pane {
         }
     }
 
-    pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext<Self>) {
-        self.item_views.retain(|(_, item)| item.id() != item_id);
+    pub fn close_item(&mut self, item_view_id: usize, cx: &mut ViewContext<Self>) {
+        self.item_views.retain(|(item_id, item)| {
+            if item.id() == item_view_id {
+                let mut navigation = self.navigation.0.borrow_mut();
+                if let Some(path) = item.project_path(cx) {
+                    navigation.paths_by_item.insert(*item_id, path);
+                } else {
+                    navigation.paths_by_item.remove(item_id);
+                }
+
+                false
+            } else {
+                true
+            }
+        });
         self.active_item = cmp::min(self.active_item, self.item_views.len().saturating_sub(1));
         if self.item_views.is_empty() {
             cx.emit(Event::Remove);
@@ -369,3 +419,33 @@ impl View for Pane {
         self.focus_active_item(cx);
     }
 }
+
+impl Navigation {
+    pub fn push<D: 'static + Any, T: ItemView>(&self, data: Option<D>, cx: &mut ViewContext<T>) {
+        let mut state = self.0.borrow_mut();
+        state.backward_stack.push(NavigationEntry {
+            item_view: Box::new(cx.weak_handle()),
+            data: data.map(|data| Box::new(data) as Box<dyn Any>),
+        });
+    }
+}
+
+impl NavigationHistory {
+    fn go_back(&mut self) -> Option<&NavigationEntry> {
+        if let Some(backward) = self.backward_stack.pop() {
+            self.forward_stack.push(backward);
+            self.forward_stack.last()
+        } else {
+            None
+        }
+    }
+
+    fn go_forward(&mut self) -> Option<&NavigationEntry> {
+        if let Some(forward) = self.forward_stack.pop() {
+            self.backward_stack.push(forward);
+            self.backward_stack.last()
+        } else {
+            None
+        }
+    }
+}

crates/workspace/src/workspace.rs 🔗

@@ -36,6 +36,7 @@ use std::{
     future::Future,
     hash::{Hash, Hasher},
     path::{Path, PathBuf},
+    rc::Rc,
     sync::Arc,
 };
 use theme::{Theme, ThemeRegistry};
@@ -135,6 +136,7 @@ pub trait Item: Entity + Sized {
     fn build_view(
         handle: ModelHandle<Self>,
         workspace: &Workspace,
+        navigation: Rc<Navigation>,
         cx: &mut ViewContext<Self::View>,
     ) -> Self::View;
 
@@ -144,6 +146,8 @@ pub trait Item: Entity + Sized {
 pub trait ItemView: View {
     type ItemHandle: ItemHandle;
 
+    fn added_to_pane(&mut self, _: Rc<Navigation>, _: &mut ViewContext<Self>) {}
+    fn activated(&mut self, _: &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>;
@@ -185,6 +189,7 @@ pub trait ItemHandle: Send + Sync {
         &self,
         window_id: usize,
         workspace: &Workspace,
+        navigation: Rc<Navigation>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle>;
     fn boxed_clone(&self) -> Box<dyn ItemHandle>;
@@ -204,7 +209,8 @@ pub trait ItemViewHandle {
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
     fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
     fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
-    fn added_to_pane(&self, cx: &mut ViewContext<Pane>);
+    fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>);
+    fn activated(&mut self, cx: &mut MutableAppContext);
     fn id(&self) -> usize;
     fn to_any(&self) -> AnyViewHandle;
     fn is_dirty(&self, cx: &AppContext) -> bool;
@@ -220,6 +226,10 @@ pub trait ItemViewHandle {
     ) -> Task<anyhow::Result<()>>;
 }
 
+pub trait WeakItemViewHandle {
+    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>>;
+}
+
 impl<T: Item> ItemHandle for ModelHandle<T> {
     fn id(&self) -> usize {
         self.id()
@@ -229,9 +239,12 @@ impl<T: Item> ItemHandle for ModelHandle<T> {
         &self,
         window_id: usize,
         workspace: &Workspace,
+        navigation: Rc<Navigation>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
-        Box::new(cx.add_view(window_id, |cx| T::build_view(self.clone(), workspace, cx)))
+        Box::new(cx.add_view(window_id, |cx| {
+            T::build_view(self.clone(), workspace, navigation, cx)
+        }))
     }
 
     fn boxed_clone(&self) -> Box<dyn ItemHandle> {
@@ -260,9 +273,10 @@ impl ItemHandle for Box<dyn ItemHandle> {
         &self,
         window_id: usize,
         workspace: &Workspace,
+        navigation: Rc<Navigation>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
-        ItemHandle::add_view(self.as_ref(), window_id, workspace, cx)
+        ItemHandle::add_view(self.as_ref(), window_id, workspace, navigation, cx)
     }
 
     fn boxed_clone(&self) -> Box<dyn ItemHandle> {
@@ -330,7 +344,7 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
     }
 
-    fn added_to_pane(&self, cx: &mut ViewContext<Pane>) {
+    fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>) {
         cx.subscribe(self, |pane, item, event, cx| {
             if T::should_close_item_on_event(event) {
                 pane.close_item(item.id(), cx);
@@ -349,6 +363,10 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         .detach();
     }
 
+    fn activated(&mut self, cx: &mut MutableAppContext) {
+        self.update(cx, |this, cx| this.activated(cx));
+    }
+
     fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
         self.update(cx, |item, cx| item.save(cx))
     }
@@ -399,6 +417,13 @@ impl Clone for Box<dyn ItemHandle> {
     }
 }
 
+impl<T: ItemView> WeakItemViewHandle for WeakViewHandle<T> {
+    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
+        self.upgrade(cx)
+            .map(|v| Box::new(v) as Box<dyn ItemViewHandle>)
+    }
+}
+
 #[derive(Clone)]
 pub struct WorkspaceParams {
     pub project: ModelHandle<Project>,