Remove methods taking `&mut Workspace` from `Pane` (#2540)

Antonio Scandurra created

This pull request simplifies the `Pane` struct by replacing methods like
`Pane::add_item` that would previously take a `&mut Workspace` with
methods that take a `&mut self`. When access to the workspace is needed,
we now either emit an event from the `Pane` or directly move the method
to the `Workspace` struct.

Change summary

crates/collab/src/tests/integration_tests.rs       |   6 
crates/editor/src/editor.rs                        |   4 
crates/project/src/project.rs                      |  14 
crates/terminal_view/src/terminal_panel.rs         |  18 
crates/workspace/src/pane.rs                       | 665 +++++----------
crates/workspace/src/pane/dragged_item_receiver.rs |   2 
crates/workspace/src/persistence/model.rs          |  18 
crates/workspace/src/shared_screen.rs              |   2 
crates/workspace/src/toolbar.rs                    |  10 
crates/workspace/src/workspace.rs                  | 208 ++++
crates/zed/src/menus.rs                            |   4 
crates/zed/src/zed.rs                              |  72 +
12 files changed, 491 insertions(+), 532 deletions(-)

Detailed changes

crates/collab/src/tests/integration_tests.rs 🔗

@@ -6608,7 +6608,7 @@ async fn test_basic_following(
     // When client A navigates back and forth, client B does so as well.
     workspace_a
         .update(cx_a, |workspace, cx| {
-            workspace::Pane::go_back(workspace, None, cx)
+            workspace.go_back(workspace.active_pane().downgrade(), cx)
         })
         .await
         .unwrap();
@@ -6619,7 +6619,7 @@ async fn test_basic_following(
 
     workspace_a
         .update(cx_a, |workspace, cx| {
-            workspace::Pane::go_back(workspace, None, cx)
+            workspace.go_back(workspace.active_pane().downgrade(), cx)
         })
         .await
         .unwrap();
@@ -6630,7 +6630,7 @@ async fn test_basic_following(
 
     workspace_a
         .update(cx_a, |workspace, cx| {
-            workspace::Pane::go_forward(workspace, None, cx)
+            workspace.go_forward(workspace.active_pane().downgrade(), cx)
         })
         .await
         .unwrap();

crates/editor/src/editor.rs 🔗

@@ -4931,12 +4931,12 @@ impl Editor {
     }
 
     fn push_to_nav_history(
-        &self,
+        &mut self,
         cursor_anchor: Anchor,
         new_position: Option<Point>,
         cx: &mut ViewContext<Self>,
     ) {
-        if let Some(nav_history) = &self.nav_history {
+        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();

crates/project/src/project.rs 🔗

@@ -5264,6 +5264,20 @@ impl Project {
         Some(ProjectPath { worktree_id, path })
     }
 
+    pub fn absolute_path(&self, project_path: &ProjectPath, cx: &AppContext) -> Option<PathBuf> {
+        let workspace_root = self
+            .worktree_for_id(project_path.worktree_id, cx)?
+            .read(cx)
+            .abs_path();
+        let project_path = project_path.path.as_ref();
+
+        Some(if project_path == Path::new("") {
+            workspace_root.to_path_buf()
+        } else {
+            workspace_root.join(project_path)
+        })
+    }
+
     // RPC message handlers
 
     async fn handle_unshare_project(

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -50,6 +50,7 @@ impl TerminalPanel {
             let window_id = cx.window_id();
             let mut pane = Pane::new(
                 workspace.weak_handle(),
+                workspace.project().clone(),
                 workspace.app_state().background_actions,
                 Default::default(),
                 cx,
@@ -176,8 +177,9 @@ impl TerminalPanel {
                 (panel, pane, items)
             })?;
 
+            let pane = pane.downgrade();
             let items = futures::future::join_all(items).await;
-            workspace.update(&mut cx, |workspace, cx| {
+            pane.update(&mut cx, |pane, cx| {
                 let active_item_id = serialized_panel
                     .as_ref()
                     .and_then(|panel| panel.active_item_id);
@@ -185,17 +187,15 @@ impl TerminalPanel {
                 for item in items {
                     if let Some(item) = item.log_err() {
                         let item_id = item.id();
-                        Pane::add_item(workspace, &pane, Box::new(item), false, false, None, cx);
+                        pane.add_item(Box::new(item), false, false, None, cx);
                         if Some(item_id) == active_item_id {
-                            active_ix = Some(pane.read(cx).items_len() - 1);
+                            active_ix = Some(pane.items_len() - 1);
                         }
                     }
                 }
 
                 if let Some(active_ix) = active_ix {
-                    pane.update(cx, |pane, cx| {
-                        pane.activate_item(active_ix, false, false, cx)
-                    });
+                    pane.activate_item(active_ix, false, false, cx)
                 }
             })?;
 
@@ -240,8 +240,10 @@ impl TerminalPanel {
                         Box::new(cx.add_view(|cx| {
                             TerminalView::new(terminal, workspace.database_id(), cx)
                         }));
-                    let focus = pane.read(cx).has_focus();
-                    Pane::add_item(workspace, &pane, terminal, true, focus, None, cx);
+                    pane.update(cx, |pane, cx| {
+                        let focus = pane.has_focus();
+                        pane.add_item(terminal, true, focus, None, cx);
+                    });
                 }
             })?;
             this.update(&mut cx, |this, cx| this.serialize(cx))?;

crates/workspace/src/pane.rs 🔗

@@ -5,7 +5,7 @@ use crate::{
     item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewFile, NewSearch, NewTerminal,
     ToggleZoom, Workspace, WorkspaceSettings,
 };
-use anyhow::{anyhow, Result};
+use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use context_menu::{ContextMenu, ContextMenuItem};
 use drag_and_drop::{DragAndDrop, Draggable};
@@ -39,7 +39,6 @@ use std::{
     },
 };
 use theme::{Theme, ThemeSettings};
-use util::ResultExt;
 
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivateItem(pub usize);
@@ -74,6 +73,8 @@ actions!(
         CloseItemsToTheLeft,
         CloseItemsToTheRight,
         CloseAllItems,
+        GoBack,
+        GoForward,
         ReopenClosedItem,
         SplitLeft,
         SplitUp,
@@ -82,19 +83,7 @@ actions!(
     ]
 );
 
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct GoBack {
-    #[serde(skip_deserializing)]
-    pub pane: Option<WeakViewHandle<Pane>>,
-}
-
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct GoForward {
-    #[serde(skip_deserializing)]
-    pub pane: Option<WeakViewHandle<Pane>>,
-}
-
-impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
+impl_actions!(pane, [ActivateItem]);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -124,19 +113,11 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
     cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
     cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
-    cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
-        Pane::reopen_closed_item(workspace, cx).detach();
-    });
-    cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
-        Pane::go_back(workspace, action.pane.clone(), cx).detach();
-    });
-    cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
-        Pane::go_forward(workspace, action.pane.clone(), cx).detach();
-    });
 }
 
 #[derive(Debug)]
 pub enum Event {
+    AddItem { item: Box<dyn ItemHandle> },
     ActivateItem { local: bool },
     Remove,
     RemoveItem { item_id: usize },
@@ -155,27 +136,28 @@ pub struct Pane {
     active_item_index: usize,
     last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
     autoscroll: bool,
-    nav_history: Rc<RefCell<NavHistory>>,
+    nav_history: NavHistory,
     toolbar: ViewHandle<Toolbar>,
     tab_bar_context_menu: TabBarContextMenu,
     tab_context_menu: ViewHandle<ContextMenu>,
     _background_actions: BackgroundActions,
     workspace: WeakViewHandle<Workspace>,
+    project: ModelHandle<Project>,
     has_focus: bool,
     can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
     can_split: bool,
-    can_navigate: bool,
     render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
 }
 
 pub struct ItemNavHistory {
-    history: Rc<RefCell<NavHistory>>,
+    history: NavHistory,
     item: Rc<dyn WeakItemHandle>,
 }
 
-pub struct PaneNavHistory(Rc<RefCell<NavHistory>>);
+#[derive(Clone)]
+pub struct NavHistory(Rc<RefCell<NavHistoryState>>);
 
-struct NavHistory {
+struct NavHistoryState {
     mode: NavigationMode,
     backward_stack: VecDeque<NavigationEntry>,
     forward_stack: VecDeque<NavigationEntry>,
@@ -186,7 +168,7 @@ struct NavHistory {
 }
 
 #[derive(Copy, Clone)]
-enum NavigationMode {
+pub enum NavigationMode {
     Normal,
     GoingBack,
     GoingForward,
@@ -241,6 +223,7 @@ impl TabBarContextMenu {
 impl Pane {
     pub fn new(
         workspace: WeakViewHandle<Workspace>,
+        project: ModelHandle<Project>,
         background_actions: BackgroundActions,
         next_timestamp: Arc<AtomicUsize>,
         cx: &mut ViewContext<Self>,
@@ -260,7 +243,7 @@ impl Pane {
             active_item_index: 0,
             last_focused_view_by_item: Default::default(),
             autoscroll: false,
-            nav_history: Rc::new(RefCell::new(NavHistory {
+            nav_history: NavHistory(Rc::new(RefCell::new(NavHistoryState {
                 mode: NavigationMode::Normal,
                 backward_stack: Default::default(),
                 forward_stack: Default::default(),
@@ -268,7 +251,7 @@ impl Pane {
                 paths_by_item: Default::default(),
                 pane: handle.clone(),
                 next_timestamp,
-            })),
+            }))),
             toolbar: cx.add_view(|_| Toolbar::new(handle)),
             tab_bar_context_menu: TabBarContextMenu {
                 kind: TabBarContextMenuKind::New,
@@ -277,10 +260,10 @@ impl Pane {
             tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)),
             _background_actions: background_actions,
             workspace,
+            project,
             has_focus: false,
             can_drop: Rc::new(|_, _| true),
             can_split: true,
-            can_navigate: true,
             render_tab_bar_buttons: Rc::new(|pane, cx| {
                 Flex::row()
                     // New menu
@@ -336,6 +319,10 @@ impl Pane {
         self.has_focus
     }
 
+    pub fn active_item_index(&self) -> usize {
+        self.active_item_index
+    }
+
     pub fn on_can_drop<F>(&mut self, can_drop: F)
     where
         F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
@@ -349,7 +336,6 @@ impl Pane {
     }
 
     pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
-        self.can_navigate = can_navigate;
         self.toolbar.update(cx, |toolbar, cx| {
             toolbar.set_can_navigate(can_navigate, cx);
         });
@@ -371,234 +357,76 @@ impl Pane {
         }
     }
 
-    pub fn nav_history(&self) -> PaneNavHistory {
-        PaneNavHistory(self.nav_history.clone())
-    }
-
-    pub fn go_back(
-        workspace: &mut Workspace,
-        pane: Option<WeakViewHandle<Pane>>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Task<Result<()>> {
-        Self::navigate_history(
-            workspace,
-            pane.unwrap_or_else(|| workspace.active_pane().downgrade()),
-            NavigationMode::GoingBack,
-            cx,
-        )
-    }
-
-    pub fn go_forward(
-        workspace: &mut Workspace,
-        pane: Option<WeakViewHandle<Pane>>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Task<Result<()>> {
-        Self::navigate_history(
-            workspace,
-            pane.unwrap_or_else(|| workspace.active_pane().downgrade()),
-            NavigationMode::GoingForward,
-            cx,
-        )
+    pub fn nav_history(&self) -> &NavHistory {
+        &self.nav_history
     }
 
-    pub fn reopen_closed_item(
-        workspace: &mut Workspace,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Task<Result<()>> {
-        Self::navigate_history(
-            workspace,
-            workspace.active_pane().downgrade(),
-            NavigationMode::ReopeningClosedItem,
-            cx,
-        )
+    pub fn nav_history_mut(&mut self) -> &mut NavHistory {
+        &mut self.nav_history
     }
 
     pub fn disable_history(&mut self) {
-        self.nav_history.borrow_mut().disable();
+        self.nav_history.disable();
     }
 
     pub fn enable_history(&mut self) {
-        self.nav_history.borrow_mut().enable();
+        self.nav_history.enable();
     }
 
     pub fn can_navigate_backward(&self) -> bool {
-        !self.nav_history.borrow().backward_stack.is_empty()
+        !self.nav_history.0.borrow().backward_stack.is_empty()
     }
 
     pub fn can_navigate_forward(&self) -> bool {
-        !self.nav_history.borrow().forward_stack.is_empty()
+        !self.nav_history.0.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: WeakViewHandle<Pane>,
-        mode: NavigationMode,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Task<Result<()>> {
-        let to_load = if let Some(pane) = pane.upgrade(cx) {
-            if !pane.read(cx).can_navigate {
-                return Task::ready(Ok(()));
-            }
-
-            cx.focus(&pane);
-
-            pane.update(cx, |pane, cx| {
-                loop {
-                    // Retrieve the weak item handle from the history.
-                    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
-                        .item
-                        .upgrade(cx)
-                        .and_then(|v| pane.index_for_item(v.as_ref()))
-                    {
-                        let prev_active_item_index = pane.active_item_index;
-                        pane.nav_history.borrow_mut().set_mode(mode);
-                        pane.activate_item(index, true, true, cx);
-                        pane.nav_history
-                            .borrow_mut()
-                            .set_mode(NavigationMode::Normal);
-
-                        let mut navigated = prev_active_item_index != pane.active_item_index;
-                        if let Some(data) = entry.data {
-                            navigated |= pane.active_item()?.navigate(data, cx);
-                        }
-
-                        if navigated {
-                            break None;
-                        }
-                    }
-                    // If the item is no longer present in this pane, then retrieve its
-                    // project path in order to reopen it.
-                    else {
-                        break pane
-                            .nav_history
-                            .borrow()
-                            .paths_by_item
-                            .get(&entry.item.id())
-                            .cloned()
-                            .map(|(project_path, _)| (project_path, entry));
-                    }
-                }
-            })
-        } else {
-            None
-        };
-
-        if let Some((project_path, entry)) = to_load {
-            // 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 task = task.await;
-                let mut navigated = false;
-                if let Some((project_entry_id, build_item)) = task.log_err() {
-                    let prev_active_item_id = pane.update(&mut cx, |pane, _| {
-                        pane.nav_history.borrow_mut().set_mode(mode);
-                        pane.active_item().map(|p| p.id())
-                    })?;
-
-                    let item = workspace.update(&mut cx, |workspace, cx| {
-                        let pane = pane
-                            .upgrade(cx)
-                            .ok_or_else(|| anyhow!("pane was dropped"))?;
-                        anyhow::Ok(Self::open_item(
-                            workspace,
-                            pane.clone(),
-                            project_entry_id,
-                            true,
-                            cx,
-                            build_item,
-                        ))
-                    })??;
-
-                    pane.update(&mut cx, |pane, cx| {
-                        navigated |= Some(item.id()) != prev_active_item_id;
-                        pane.nav_history
-                            .borrow_mut()
-                            .set_mode(NavigationMode::Normal);
-                        if let Some(data) = entry.data {
-                            navigated |= item.navigate(data, cx);
-                        }
-                    })?;
-                }
-
-                if !navigated {
-                    workspace
-                        .update(&mut cx, |workspace, cx| {
-                            Self::navigate_history(workspace, pane, mode, cx)
-                        })?
-                        .await?;
-                }
-
-                Ok(())
-            })
-        } else {
-            Task::ready(Ok(()))
-        }
-    }
-
     pub(crate) fn open_item(
-        workspace: &mut Workspace,
-        pane: ViewHandle<Pane>,
+        &mut self,
         project_entry_id: ProjectEntryId,
         focus_item: bool,
-        cx: &mut ViewContext<Workspace>,
+        cx: &mut ViewContext<Self>,
         build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
     ) -> Box<dyn ItemHandle> {
-        let existing_item = pane.update(cx, |pane, cx| {
-            for (index, item) in pane.items.iter().enumerate() {
-                if item.is_singleton(cx)
-                    && item.project_entry_ids(cx).as_slice() == [project_entry_id]
-                {
-                    let item = item.boxed_clone();
-                    return Some((index, item));
-                }
+        let mut existing_item = None;
+        for (index, item) in self.items.iter().enumerate() {
+            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
+            {
+                let item = item.boxed_clone();
+                existing_item = Some((index, item));
+                break;
             }
-            None
-        });
+        }
 
         if let Some((index, existing_item)) = existing_item {
-            pane.update(cx, |pane, cx| {
-                pane.activate_item(index, focus_item, focus_item, cx);
-            });
+            self.activate_item(index, focus_item, focus_item, cx);
             existing_item
         } else {
-            let new_item = pane.update(cx, |_, cx| build_item(cx));
-            Pane::add_item(
-                workspace,
-                &pane,
-                new_item.clone(),
-                true,
-                focus_item,
-                None,
-                cx,
-            );
+            let new_item = build_item(cx);
+            self.add_item(new_item.clone(), true, focus_item, None, cx);
             new_item
         }
     }
 
     pub fn add_item(
-        workspace: &mut Workspace,
-        pane: &ViewHandle<Pane>,
+        &mut self,
         item: Box<dyn ItemHandle>,
         activate_pane: bool,
         focus_item: bool,
         destination_index: Option<usize>,
-        cx: &mut ViewContext<Workspace>,
+        cx: &mut ViewContext<Self>,
     ) {
         if item.is_singleton(cx) {
             if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
-                if let Some(project_path) =
-                    workspace.project().read(cx).path_for_entry(entry_id, cx)
-                {
-                    let abs_path = workspace.absolute_path(&project_path, cx);
-                    pane.read(cx)
-                        .nav_history
+                let project = self.project.read(cx);
+                if let Some(project_path) = project.path_for_entry(entry_id, cx) {
+                    let abs_path = project.absolute_path(&project_path, cx);
+                    self.nav_history
+                        .0
                         .borrow_mut()
                         .paths_by_item
                         .insert(item.id(), (project_path, abs_path));
@@ -607,19 +435,16 @@ impl Pane {
         }
         // If no destination index is specified, add or move the item after the active item.
         let mut insertion_index = {
-            let pane = pane.read(cx);
             cmp::min(
                 if let Some(destination_index) = destination_index {
                     destination_index
                 } else {
-                    pane.active_item_index + 1
+                    self.active_item_index + 1
                 },
-                pane.items.len(),
+                self.items.len(),
             )
         };
 
-        item.added_to_pane(workspace, pane.clone(), cx);
-
         // Does the item already exist?
         let project_entry_id = if item.is_singleton(cx) {
             item.project_entry_ids(cx).get(0).copied()
@@ -627,7 +452,7 @@ impl Pane {
             None
         };
 
-        let existing_item_index = pane.read(cx).items.iter().position(|existing_item| {
+        let existing_item_index = self.items.iter().position(|existing_item| {
             if existing_item.id() == item.id() {
                 true
             } else if existing_item.is_singleton(cx) {
@@ -644,46 +469,45 @@ impl Pane {
 
         if let Some(existing_item_index) = existing_item_index {
             // If the item already exists, move it to the desired destination and activate it
-            pane.update(cx, |pane, cx| {
-                if existing_item_index != insertion_index {
-                    let existing_item_is_active = existing_item_index == pane.active_item_index;
-
-                    // If the caller didn't specify a destination and the added item is already
-                    // the active one, don't move it
-                    if existing_item_is_active && destination_index.is_none() {
-                        insertion_index = existing_item_index;
-                    } else {
-                        pane.items.remove(existing_item_index);
-                        if existing_item_index < pane.active_item_index {
-                            pane.active_item_index -= 1;
-                        }
-                        insertion_index = insertion_index.min(pane.items.len());
 
-                        pane.items.insert(insertion_index, item.clone());
+            if existing_item_index != insertion_index {
+                let existing_item_is_active = existing_item_index == self.active_item_index;
 
-                        if existing_item_is_active {
-                            pane.active_item_index = insertion_index;
-                        } else if insertion_index <= pane.active_item_index {
-                            pane.active_item_index += 1;
-                        }
+                // If the caller didn't specify a destination and the added item is already
+                // the active one, don't move it
+                if existing_item_is_active && destination_index.is_none() {
+                    insertion_index = existing_item_index;
+                } else {
+                    self.items.remove(existing_item_index);
+                    if existing_item_index < self.active_item_index {
+                        self.active_item_index -= 1;
                     }
+                    insertion_index = insertion_index.min(self.items.len());
 
-                    cx.notify();
-                }
+                    self.items.insert(insertion_index, item.clone());
 
-                pane.activate_item(insertion_index, activate_pane, focus_item, cx);
-            });
-        } else {
-            pane.update(cx, |pane, cx| {
-                pane.items.insert(insertion_index, item);
-                if insertion_index <= pane.active_item_index {
-                    pane.active_item_index += 1;
+                    if existing_item_is_active {
+                        self.active_item_index = insertion_index;
+                    } else if insertion_index <= self.active_item_index {
+                        self.active_item_index += 1;
+                    }
                 }
 
-                pane.activate_item(insertion_index, activate_pane, focus_item, cx);
                 cx.notify();
-            });
+            }
+
+            self.activate_item(insertion_index, activate_pane, focus_item, cx);
+        } else {
+            self.items.insert(insertion_index, item.clone());
+            if insertion_index <= self.active_item_index {
+                self.active_item_index += 1;
+            }
+
+            self.activate_item(insertion_index, activate_pane, focus_item, cx);
+            cx.notify();
         }
+
+        cx.emit(Event::AddItem { item });
     }
 
     pub fn items_len(&self) -> usize {
@@ -745,7 +569,7 @@ impl Pane {
         if index < self.items.len() {
             let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
             if prev_active_item_ix != self.active_item_index
-                || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
+                || matches!(self.nav_history.mode(), GoingBack | GoingForward)
             {
                 if let Some(prev_item) = self.items.get(prev_active_item_ix) {
                     prev_item.deactivated(cx);
@@ -974,7 +798,12 @@ impl Pane {
         })
     }
 
-    fn remove_item(&mut self, item_index: usize, activate_pane: bool, cx: &mut ViewContext<Self>) {
+    pub fn remove_item(
+        &mut self,
+        item_index: usize,
+        activate_pane: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
         self.activation_history
             .retain(|&history_entry| history_entry != self.items[item_index].id());
 
@@ -1008,27 +837,26 @@ impl Pane {
             self.active_item_index -= 1;
         }
 
-        self.nav_history
-            .borrow_mut()
-            .set_mode(NavigationMode::ClosingItem);
+        self.nav_history.set_mode(NavigationMode::ClosingItem);
         item.deactivated(cx);
-        self.nav_history
-            .borrow_mut()
-            .set_mode(NavigationMode::Normal);
+        self.nav_history.set_mode(NavigationMode::Normal);
 
         if let Some(path) = item.project_path(cx) {
             let abs_path = self
                 .nav_history
+                .0
                 .borrow()
                 .paths_by_item
                 .get(&item.id())
                 .and_then(|(_, abs_path)| abs_path.clone());
             self.nav_history
+                .0
                 .borrow_mut()
                 .paths_by_item
                 .insert(item.id(), (path, abs_path));
         } else {
             self.nav_history
+                .0
                 .borrow_mut()
                 .paths_by_item
                 .remove(&item.id());
@@ -1148,48 +976,6 @@ impl Pane {
         }
     }
 
-    pub fn move_item(
-        workspace: &mut Workspace,
-        from: ViewHandle<Pane>,
-        to: ViewHandle<Pane>,
-        item_id_to_move: usize,
-        destination_index: usize,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        let item_to_move = from
-            .read(cx)
-            .items()
-            .enumerate()
-            .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
-
-        if item_to_move.is_none() {
-            log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
-            return;
-        }
-        let (item_ix, item_handle) = item_to_move.unwrap();
-        let item_handle = item_handle.clone();
-
-        if from != to {
-            // Close item from previous pane
-            from.update(cx, |from, cx| {
-                from.remove_item(item_ix, false, cx);
-            });
-        }
-
-        // This automatically removes duplicate items in the pane
-        Pane::add_item(
-            workspace,
-            &to,
-            item_handle,
-            true,
-            true,
-            Some(destination_index),
-            cx,
-        );
-
-        cx.focus(&to);
-    }
-
     pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
         cx.emit(Event::Split(direction));
     }
@@ -1318,7 +1104,7 @@ impl Pane {
         })?;
 
         self.remove_item(item_index_to_delete, false, cx);
-        self.nav_history.borrow_mut().remove_item(item_id);
+        self.nav_history.remove_item(item_id);
 
         Some(())
     }
@@ -1788,7 +1574,7 @@ impl View for Pane {
                     let pane = cx.weak_handle();
                     cx.window_context().defer(move |cx| {
                         workspace.update(cx, |workspace, cx| {
-                            Pane::go_back(workspace, Some(pane), cx).detach_and_log_err(cx)
+                            workspace.go_back(pane, cx).detach_and_log_err(cx)
                         })
                     })
                 }
@@ -1800,7 +1586,7 @@ impl View for Pane {
                     let pane = cx.weak_handle();
                     cx.window_context().defer(move |cx| {
                         workspace.update(cx, |workspace, cx| {
-                            Pane::go_forward(workspace, Some(pane), cx).detach_and_log_err(cx)
+                            workspace.go_forward(pane, cx).detach_and_log_err(cx)
                         })
                     })
                 }
@@ -1855,144 +1641,157 @@ impl View for Pane {
 }
 
 impl ItemNavHistory {
-    pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut WindowContext) {
-        self.history.borrow_mut().push(data, self.item.clone(), cx);
+    pub fn push<D: 'static + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
+        self.history.push(data, self.item.clone(), cx);
     }
 
-    pub fn pop_backward(&self, cx: &mut WindowContext) -> Option<NavigationEntry> {
-        self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
+    pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
+        self.history.pop(NavigationMode::GoingBack, cx)
     }
 
-    pub fn pop_forward(&self, cx: &mut WindowContext) -> Option<NavigationEntry> {
-        self.history
-            .borrow_mut()
-            .pop(NavigationMode::GoingForward, cx)
+    pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
+        self.history.pop(NavigationMode::GoingForward, cx)
     }
 }
 
 impl NavHistory {
-    fn set_mode(&mut self, mode: NavigationMode) {
-        self.mode = mode;
+    pub fn for_each_entry(
+        &self,
+        cx: &AppContext,
+        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
+    ) {
+        let borrowed_history = self.0.borrow();
+        borrowed_history
+            .forward_stack
+            .iter()
+            .chain(borrowed_history.backward_stack.iter())
+            .chain(borrowed_history.closed_stack.iter())
+            .for_each(|entry| {
+                if let Some(project_and_abs_path) =
+                    borrowed_history.paths_by_item.get(&entry.item.id())
+                {
+                    f(entry, project_and_abs_path.clone());
+                } else if let Some(item) = entry.item.upgrade(cx) {
+                    if let Some(path) = item.project_path(cx) {
+                        f(entry, (path, None));
+                    }
+                }
+            })
+    }
+
+    pub fn set_mode(&mut self, mode: NavigationMode) {
+        self.0.borrow_mut().mode = mode;
     }
 
-    fn disable(&mut self) {
-        self.mode = NavigationMode::Disabled;
+    pub fn mode(&self) -> NavigationMode {
+        self.0.borrow().mode
     }
 
-    fn enable(&mut self) {
-        self.mode = NavigationMode::Normal;
+    pub fn disable(&mut self) {
+        self.0.borrow_mut().mode = NavigationMode::Disabled;
     }
 
-    fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
+    pub fn enable(&mut self) {
+        self.0.borrow_mut().mode = NavigationMode::Normal;
+    }
+
+    pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
+        let mut state = self.0.borrow_mut();
         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,
+            NavigationMode::GoingBack => &mut state.backward_stack,
+            NavigationMode::GoingForward => &mut state.forward_stack,
+            NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
         }
         .pop_back();
         if entry.is_some() {
-            self.did_update(cx);
+            state.did_update(cx);
         }
         entry
     }
 
-    fn push<D: 'static + Any>(
+    pub fn push<D: 'static + Any>(
         &mut self,
         data: Option<D>,
         item: Rc<dyn WeakItemHandle>,
         cx: &mut WindowContext,
     ) {
-        match self.mode {
+        let state = &mut *self.0.borrow_mut();
+        match state.mode {
             NavigationMode::Disabled => {}
             NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
-                if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
-                    self.backward_stack.pop_front();
+                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
+                    state.backward_stack.pop_front();
                 }
-                self.backward_stack.push_back(NavigationEntry {
+                state.backward_stack.push_back(NavigationEntry {
                     item,
                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
-                    timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
+                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
                 });
-                self.forward_stack.clear();
+                state.forward_stack.clear();
             }
             NavigationMode::GoingBack => {
-                if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
-                    self.forward_stack.pop_front();
+                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
+                    state.forward_stack.pop_front();
                 }
-                self.forward_stack.push_back(NavigationEntry {
+                state.forward_stack.push_back(NavigationEntry {
                     item,
                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
-                    timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
+                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
                 });
             }
             NavigationMode::GoingForward => {
-                if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
-                    self.backward_stack.pop_front();
+                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
+                    state.backward_stack.pop_front();
                 }
-                self.backward_stack.push_back(NavigationEntry {
+                state.backward_stack.push_back(NavigationEntry {
                     item,
                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
-                    timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
+                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
                 });
             }
             NavigationMode::ClosingItem => {
-                if self.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
-                    self.closed_stack.pop_front();
+                if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
+                    state.closed_stack.pop_front();
                 }
-                self.closed_stack.push_back(NavigationEntry {
+                state.closed_stack.push_back(NavigationEntry {
                     item,
                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
-                    timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst),
+                    timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
                 });
             }
         }
-        self.did_update(cx);
-    }
-
-    fn did_update(&self, cx: &mut WindowContext) {
-        if let Some(pane) = self.pane.upgrade(cx) {
-            cx.defer(move |cx| {
-                pane.update(cx, |pane, cx| pane.history_updated(cx));
-            });
-        }
+        state.did_update(cx);
     }
 
-    fn remove_item(&mut self, item_id: usize) {
-        self.paths_by_item.remove(&item_id);
-        self.backward_stack
+    pub fn remove_item(&mut self, item_id: usize) {
+        let mut state = self.0.borrow_mut();
+        state.paths_by_item.remove(&item_id);
+        state
+            .backward_stack
+            .retain(|entry| entry.item.id() != item_id);
+        state
+            .forward_stack
             .retain(|entry| entry.item.id() != item_id);
-        self.forward_stack
+        state
+            .closed_stack
             .retain(|entry| entry.item.id() != item_id);
-        self.closed_stack.retain(|entry| entry.item.id() != item_id);
+    }
+
+    pub fn path_for_item(&self, item_id: usize) -> Option<(ProjectPath, Option<PathBuf>)> {
+        self.0.borrow().paths_by_item.get(&item_id).cloned()
     }
 }
 
-impl PaneNavHistory {
-    pub fn for_each_entry(
-        &self,
-        cx: &AppContext,
-        mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
-    ) {
-        let borrowed_history = self.0.borrow();
-        borrowed_history
-            .forward_stack
-            .iter()
-            .chain(borrowed_history.backward_stack.iter())
-            .chain(borrowed_history.closed_stack.iter())
-            .for_each(|entry| {
-                if let Some(project_and_abs_path) =
-                    borrowed_history.paths_by_item.get(&entry.item.id())
-                {
-                    f(entry, project_and_abs_path.clone());
-                } else if let Some(item) = entry.item.upgrade(cx) {
-                    if let Some(path) = item.project_path(cx) {
-                        f(entry, (path, None));
-                    }
-                }
-            })
+impl NavHistoryState {
+    pub fn did_update(&self, cx: &mut WindowContext) {
+        if let Some(pane) = self.pane.upgrade(cx) {
+            cx.defer(move |cx| {
+                pane.update(cx, |pane, cx| pane.history_updated(cx));
+            });
+        }
     }
 }
 
@@ -2124,11 +1923,9 @@ mod tests {
 
         // 1. Add with a destination index
         //   a. Add before the active item
-        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(
-                workspace,
-                &pane,
+        set_labeled_items(&pane, ["A", "B*", "C"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(
                 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
                 false,
                 false,
@@ -2139,11 +1936,9 @@ mod tests {
         assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
 
         //   b. Add after the active item
-        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(
-                workspace,
-                &pane,
+        set_labeled_items(&pane, ["A", "B*", "C"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(
                 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
                 false,
                 false,
@@ -2154,11 +1949,9 @@ mod tests {
         assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
 
         //   c. Add at the end of the item list (including off the length)
-        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(
-                workspace,
-                &pane,
+        set_labeled_items(&pane, ["A", "B*", "C"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(
                 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
                 false,
                 false,
@@ -2170,11 +1963,9 @@ mod tests {
 
         // 2. Add without a destination index
         //   a. Add with active item at the start of the item list
-        set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(
-                workspace,
-                &pane,
+        set_labeled_items(&pane, ["A*", "B", "C"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(
                 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
                 false,
                 false,
@@ -2182,14 +1973,12 @@ mod tests {
                 cx,
             );
         });
-        set_labeled_items(&workspace, &pane, ["A", "D*", "B", "C"], cx);
+        set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
 
         //   b. Add with active item at the end of the item list
-        set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(
-                workspace,
-                &pane,
+        set_labeled_items(&pane, ["A", "B", "C*"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(
                 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
                 false,
                 false,
@@ -2212,66 +2001,66 @@ mod tests {
 
         // 1. Add with a destination index
         //   1a. Add before the active item
-        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(workspace, &pane, d, false, false, Some(0), cx);
+        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(d, false, false, Some(0), cx);
         });
         assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
 
         //   1b. Add after the active item
-        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(workspace, &pane, d, false, false, Some(2), cx);
+        let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(d, false, false, Some(2), cx);
         });
         assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
 
         //   1c. Add at the end of the item list (including off the length)
-        let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(workspace, &pane, a, false, false, Some(5), cx);
+        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(a, false, false, Some(5), cx);
         });
         assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
 
         //   1d. Add same item to active index
-        let [_, b, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(workspace, &pane, b, false, false, Some(1), cx);
+        let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(b, false, false, Some(1), cx);
         });
         assert_item_labels(&pane, ["A", "B*", "C"], cx);
 
         //   1e. Add item to index after same item in last position
-        let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(workspace, &pane, c, false, false, Some(2), cx);
+        let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(c, false, false, Some(2), cx);
         });
         assert_item_labels(&pane, ["A", "B", "C*"], cx);
 
         // 2. Add without a destination index
         //   2a. Add with active item at the start of the item list
-        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A*", "B", "C", "D"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(workspace, &pane, d, false, false, None, cx);
+        let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(d, false, false, None, cx);
         });
         assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
 
         //   2b. Add with active item at the end of the item list
-        let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B", "C", "D*"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(workspace, &pane, a, false, false, None, cx);
+        let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(a, false, false, None, cx);
         });
         assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
 
         //   2c. Add active item to active item at end of list
-        let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(workspace, &pane, c, false, false, None, cx);
+        let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(c, false, false, None, cx);
         });
         assert_item_labels(&pane, ["A", "B", "C*"], cx);
 
         //   2d. Add active item to active item at start of list
-        let [a, _, _] = set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
-        workspace.update(cx, |workspace, cx| {
-            Pane::add_item(workspace, &pane, a, false, false, None, cx);
+        let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
+        pane.update(cx, |pane, cx| {
+            pane.add_item(a, false, false, None, cx);
         });
         assert_item_labels(&pane, ["A*", "B", "C"], cx);
     }

crates/workspace/src/pane/dragged_item_receiver.rs 🔗

@@ -183,7 +183,7 @@ pub fn handle_dropped_item<V: View>(
                             .zip(pane.upgrade(cx))
                         {
                             workspace.update(cx, |workspace, cx| {
-                                Pane::move_item(workspace, from, to, item_id, index, cx);
+                                workspace.move_item(from, to, item_id, index, cx);
                             })
                         }
                     });

crates/workspace/src/persistence/model.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{Context, Result};
 use async_recursion::async_recursion;
 use db::sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
@@ -230,7 +230,7 @@ impl SerializedPane {
     pub async fn deserialize_to(
         &self,
         project: &ModelHandle<Project>,
-        pane_handle: &WeakViewHandle<Pane>,
+        pane: &WeakViewHandle<Pane>,
         workspace_id: WorkspaceId,
         workspace: &WeakViewHandle<Workspace>,
         cx: &mut AsyncAppContext,
@@ -239,7 +239,7 @@ impl SerializedPane {
         let mut active_item_index = None;
         for (index, item) in self.children.iter().enumerate() {
             let project = project.clone();
-            let item_handle = pane_handle
+            let item_handle = pane
                 .update(cx, |_, cx| {
                     if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
                         deserializer(project, workspace.clone(), workspace_id, item.item_id, cx)
@@ -256,13 +256,9 @@ impl SerializedPane {
             items.push(item_handle.clone());
 
             if let Some(item_handle) = item_handle {
-                workspace.update(cx, |workspace, cx| {
-                    let pane_handle = pane_handle
-                        .upgrade(cx)
-                        .ok_or_else(|| anyhow!("pane was dropped"))?;
-                    Pane::add_item(workspace, &pane_handle, item_handle, true, true, None, cx);
-                    anyhow::Ok(())
-                })??;
+                pane.update(cx, |pane, cx| {
+                    pane.add_item(item_handle.clone(), true, true, None, cx);
+                })?;
             }
 
             if item.active {
@@ -271,7 +267,7 @@ impl SerializedPane {
         }
 
         if let Some(active_item_index) = active_item_index {
-            pane_handle.update(cx, |pane, cx| {
+            pane.update(cx, |pane, cx| {
                 pane.activate_item(active_item_index, false, false, cx);
             })?;
         }

crates/workspace/src/shared_screen.rs 🔗

@@ -99,7 +99,7 @@ impl Item for SharedScreen {
         Some(format!("{}'s screen", self.user.github_login).into())
     }
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(nav_history) = self.nav_history.as_ref() {
+        if let Some(nav_history) = self.nav_history.as_mut() {
             nav_history.push::<()>(None, cx);
         }
     }

crates/workspace/src/toolbar.rs 🔗

@@ -153,14 +153,13 @@ impl View for Toolbar {
                             let pane = pane.clone();
                             cx.window_context().defer(move |cx| {
                                 workspace.update(cx, |workspace, cx| {
-                                    Pane::go_back(workspace, Some(pane.clone()), cx)
-                                        .detach_and_log_err(cx);
+                                    workspace.go_back(pane.clone(), cx).detach_and_log_err(cx);
                                 });
                             })
                         }
                     }
                 },
-                super::GoBack { pane: None },
+                super::GoBack,
                 "Go Back",
                 cx,
             ));
@@ -182,14 +181,15 @@ impl View for Toolbar {
                             let pane = pane.clone();
                             cx.window_context().defer(move |cx| {
                                 workspace.update(cx, |workspace, cx| {
-                                    Pane::go_forward(workspace, Some(pane.clone()), cx)
+                                    workspace
+                                        .go_forward(pane.clone(), cx)
                                         .detach_and_log_err(cx);
                                 });
                             });
                         }
                     }
                 },
-                super::GoForward { pane: None },
+                super::GoForward,
                 "Go Forward",
                 cx,
             ));

crates/workspace/src/workspace.rs 🔗

@@ -278,6 +278,19 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         workspace.toggle_dock(DockPosition::Bottom, action.focus, cx);
     });
     cx.add_action(Workspace::activate_pane_at_index);
+    cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
+        workspace.reopen_closed_item(cx).detach();
+    });
+    cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
+        workspace
+            .go_back(workspace.active_pane().downgrade(), cx)
+            .detach();
+    });
+    cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
+        workspace
+            .go_forward(workspace.active_pane().downgrade(), cx)
+            .detach();
+    });
 
     cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
         cx.spawn(|workspace, mut cx| async move {
@@ -583,6 +596,7 @@ impl Workspace {
         let center_pane = cx.add_view(|cx| {
             Pane::new(
                 weak_handle.clone(),
+                project.clone(),
                 app_state.background_actions,
                 pane_history_timestamp.clone(),
                 cx,
@@ -999,6 +1013,115 @@ impl Workspace {
             .collect()
     }
 
+    fn navigate_history(
+        &mut self,
+        pane: WeakViewHandle<Pane>,
+        mode: NavigationMode,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<()>> {
+        let to_load = if let Some(pane) = pane.upgrade(cx) {
+            cx.focus(&pane);
+
+            pane.update(cx, |pane, cx| {
+                loop {
+                    // Retrieve the weak item handle from the history.
+                    let entry = pane.nav_history_mut().pop(mode, cx)?;
+
+                    // If the item is still present in this pane, then activate it.
+                    if let Some(index) = entry
+                        .item
+                        .upgrade(cx)
+                        .and_then(|v| pane.index_for_item(v.as_ref()))
+                    {
+                        let prev_active_item_index = pane.active_item_index();
+                        pane.nav_history_mut().set_mode(mode);
+                        pane.activate_item(index, true, true, cx);
+                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
+
+                        let mut navigated = prev_active_item_index != pane.active_item_index();
+                        if let Some(data) = entry.data {
+                            navigated |= pane.active_item()?.navigate(data, cx);
+                        }
+
+                        if navigated {
+                            break None;
+                        }
+                    }
+                    // If the item is no longer present in this pane, then retrieve its
+                    // project path in order to reopen it.
+                    else {
+                        break pane
+                            .nav_history()
+                            .path_for_item(entry.item.id())
+                            .map(|(project_path, _)| (project_path, entry));
+                    }
+                }
+            })
+        } else {
+            None
+        };
+
+        if let Some((project_path, entry)) = to_load {
+            // If the item was no longer present, then load it again from its previous path.
+            let task = self.load_path(project_path, cx);
+            cx.spawn(|workspace, mut cx| async move {
+                let task = task.await;
+                let mut navigated = false;
+                if let Some((project_entry_id, build_item)) = task.log_err() {
+                    let prev_active_item_id = pane.update(&mut cx, |pane, _| {
+                        pane.nav_history_mut().set_mode(mode);
+                        pane.active_item().map(|p| p.id())
+                    })?;
+
+                    pane.update(&mut cx, |pane, cx| {
+                        let item = pane.open_item(project_entry_id, true, cx, build_item);
+                        navigated |= Some(item.id()) != prev_active_item_id;
+                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
+                        if let Some(data) = entry.data {
+                            navigated |= item.navigate(data, cx);
+                        }
+                    })?;
+                }
+
+                if !navigated {
+                    workspace
+                        .update(&mut cx, |workspace, cx| {
+                            Self::navigate_history(workspace, pane, mode, cx)
+                        })?
+                        .await?;
+                }
+
+                Ok(())
+            })
+        } else {
+            Task::ready(Ok(()))
+        }
+    }
+
+    pub fn go_back(
+        &mut self,
+        pane: WeakViewHandle<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<()>> {
+        self.navigate_history(pane, NavigationMode::GoingBack, cx)
+    }
+
+    pub fn go_forward(
+        &mut self,
+        pane: WeakViewHandle<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<()>> {
+        self.navigate_history(pane, NavigationMode::GoingForward, cx)
+    }
+
+    pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
+        self.navigate_history(
+            self.active_pane().downgrade(),
+            NavigationMode::ReopeningClosedItem,
+            cx,
+        )
+    }
+
     pub fn client(&self) -> &Client {
         &self.app_state.client
     }
@@ -1307,22 +1430,6 @@ impl Workspace {
         })
     }
 
-    pub fn absolute_path(&self, project_path: &ProjectPath, cx: &AppContext) -> Option<PathBuf> {
-        let workspace_root = self
-            .project()
-            .read(cx)
-            .worktree_for_id(project_path.worktree_id, cx)?
-            .read(cx)
-            .abs_path();
-        let project_path = project_path.path.as_ref();
-
-        Some(if project_path == Path::new("") {
-            workspace_root.to_path_buf()
-        } else {
-            workspace_root.join(project_path)
-        })
-    }
-
     fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
         let mut paths = cx.prompt_for_paths(PathPromptOptions {
             files: false,
@@ -1588,6 +1695,7 @@ impl Workspace {
         let pane = cx.add_view(|cx| {
             Pane::new(
                 self.weak_handle(),
+                self.project.clone(),
                 self.app_state.background_actions,
                 self.pane_history_timestamp.clone(),
                 cx,
@@ -1607,7 +1715,7 @@ impl Workspace {
     ) -> bool {
         if let Some(center_pane) = self.last_active_center_pane.clone() {
             if let Some(center_pane) = center_pane.upgrade(cx) {
-                Pane::add_item(self, &center_pane, item, true, true, None, cx);
+                center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
                 true
             } else {
                 false
@@ -1618,8 +1726,8 @@ impl Workspace {
     }
 
     pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
-        let active_pane = self.active_pane().clone();
-        Pane::add_item(self, &active_pane, item, true, true, None, cx);
+        self.active_pane
+            .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
     }
 
     pub fn open_abs_path(
@@ -1669,13 +1777,10 @@ impl Workspace {
         });
 
         let task = self.load_path(path.into(), cx);
-        cx.spawn(|this, mut cx| async move {
+        cx.spawn(|_, mut cx| async move {
             let (project_entry_id, build_item) = task.await?;
-            let pane = pane
-                .upgrade(&cx)
-                .ok_or_else(|| anyhow!("pane was closed"))?;
-            this.update(&mut cx, |this, cx| {
-                Pane::open_item(this, pane, project_entry_id, focus_item, cx, build_item)
+            pane.update(&mut cx, |pane, cx| {
+                pane.open_item(project_entry_id, focus_item, cx, build_item)
             })
         })
     }
@@ -1732,8 +1837,9 @@ impl Workspace {
 
     pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
         if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
-            let pane = self.active_pane.clone();
-            Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
+            self.active_pane.update(cx, |pane, cx| {
+                pane.add_item(Box::new(shared_screen), false, true, None, cx)
+            });
         }
     }
 
@@ -1820,6 +1926,7 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
+            pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
             pane::Event::Split(direction) => {
                 self.split_pane(pane, *direction, cx);
             }
@@ -1874,7 +1981,7 @@ impl Workspace {
         let item = pane.read(cx).active_item()?;
         let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
             let new_pane = self.add_pane(cx);
-            Pane::add_item(self, &new_pane, clone, true, true, None, cx);
+            new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
             self.center.split(&pane, &new_pane, direction).unwrap();
             Some(new_pane)
         } else {
@@ -1896,7 +2003,7 @@ impl Workspace {
         let Some(from) = from.upgrade(cx) else { return; };
 
         let new_pane = self.add_pane(cx);
-        Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
+        self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
         self.center
             .split(&pane_to_split, &new_pane, split_direction)
             .unwrap();
@@ -1924,6 +2031,41 @@ impl Workspace {
         }))
     }
 
+    pub fn move_item(
+        &mut self,
+        source: ViewHandle<Pane>,
+        destination: ViewHandle<Pane>,
+        item_id_to_move: usize,
+        destination_index: usize,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let item_to_move = source
+            .read(cx)
+            .items()
+            .enumerate()
+            .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
+
+        if item_to_move.is_none() {
+            log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
+            return;
+        }
+        let (item_ix, item_handle) = item_to_move.unwrap();
+        let item_handle = item_handle.clone();
+
+        if source != destination {
+            // Close item from previous pane
+            source.update(cx, |source, cx| {
+                source.remove_item(item_ix, false, cx);
+            });
+        }
+
+        // This automatically removes duplicate items in the pane
+        destination.update(cx, |destination, cx| {
+            destination.add_item(item_handle, true, true, Some(destination_index), cx);
+            cx.focus_self();
+        });
+    }
+
     fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
         if self.center.remove(&pane).unwrap() {
             self.force_remove_pane(&pane, cx);
@@ -2527,7 +2669,9 @@ impl Workspace {
             if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
                 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
             } else {
-                Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
+                pane.update(cx, |pane, cx| {
+                    pane.add_item(item.boxed_clone(), false, false, None, cx)
+                });
             }
 
             if pane_was_focused {
@@ -4016,9 +4160,7 @@ mod tests {
         });
 
         workspace
-            .update(cx, |workspace, cx| {
-                Pane::go_back(workspace, Some(pane.downgrade()), cx)
-            })
+            .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
             .await
             .unwrap();
 

crates/zed/src/menus.rs 🔗

@@ -120,8 +120,8 @@ pub fn menus() -> Vec<Menu<'static>> {
         Menu {
             name: "Go",
             items: vec![
-                MenuItem::action("Back", workspace::GoBack { pane: None }),
-                MenuItem::action("Forward", workspace::GoForward { pane: None }),
+                MenuItem::action("Back", workspace::GoBack),
+                MenuItem::action("Forward", workspace::GoForward),
                 MenuItem::separator(),
                 MenuItem::action("Go to File", file_finder::Toggle),
                 MenuItem::action("Go to Symbol in Project", project_symbols::Toggle),

crates/zed/src/zed.rs 🔗

@@ -663,7 +663,7 @@ mod tests {
     use util::http::FakeHttpClient;
     use workspace::{
         item::{Item, ItemHandle},
-        open_new, open_paths, pane, NewFile, Pane, SplitDirection, WorkspaceHandle,
+        open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle,
     };
 
     #[gpui::test]
@@ -1488,7 +1488,7 @@ mod tests {
         );
 
         workspace
-            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1497,7 +1497,7 @@ mod tests {
         );
 
         workspace
-            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1506,7 +1506,7 @@ mod tests {
         );
 
         workspace
-            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1515,7 +1515,7 @@ mod tests {
         );
 
         workspace
-            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1525,7 +1525,7 @@ mod tests {
 
         // Go back one more time and ensure we don't navigate past the first item in the history.
         workspace
-            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1534,7 +1534,7 @@ mod tests {
         );
 
         workspace
-            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1543,7 +1543,7 @@ mod tests {
         );
 
         workspace
-            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1561,7 +1561,7 @@ mod tests {
         .await
         .unwrap();
         workspace
-            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1570,7 +1570,7 @@ mod tests {
         );
 
         workspace
-            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1579,7 +1579,7 @@ mod tests {
         );
 
         workspace
-            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1601,7 +1601,7 @@ mod tests {
             .await
             .unwrap();
         workspace
-            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1609,7 +1609,7 @@ mod tests {
             (file1.clone(), DisplayPoint::new(10, 0), 0.)
         );
         workspace
-            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1653,7 +1653,7 @@ mod tests {
             })
         });
         workspace
-            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1661,7 +1661,7 @@ mod tests {
             (file1.clone(), DisplayPoint::new(2, 0), 0.)
         );
         workspace
-            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
             .await
             .unwrap();
         assert_eq!(
@@ -1766,81 +1766,97 @@ mod tests {
         // Reopen all the closed items, ensuring they are reopened in the same order
         // in which they were closed.
         workspace
-            .update(cx, Pane::reopen_closed_item)
+            .update(cx, Workspace::reopen_closed_item)
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
 
         workspace
-            .update(cx, Pane::reopen_closed_item)
+            .update(cx, Workspace::reopen_closed_item)
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
 
         workspace
-            .update(cx, Pane::reopen_closed_item)
+            .update(cx, Workspace::reopen_closed_item)
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
 
         workspace
-            .update(cx, Pane::reopen_closed_item)
+            .update(cx, Workspace::reopen_closed_item)
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
 
         // Reopening past the last closed item is a no-op.
         workspace
-            .update(cx, Pane::reopen_closed_item)
+            .update(cx, Workspace::reopen_closed_item)
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
 
         // Reopening closed items doesn't interfere with navigation history.
         workspace
-            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
 
         workspace
-            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
 
         workspace
-            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
 
         workspace
-            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
 
         workspace
-            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
 
         workspace
-            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
 
         workspace
-            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
 
         workspace
-            .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
+            .update(cx, |workspace, cx| {
+                workspace.go_back(workspace.active_pane().downgrade(), cx)
+            })
             .await
             .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));