Add tab context menu

Joseph Lyons created

Change summary

crates/workspace/src/item.rs |   2 
crates/workspace/src/pane.rs | 333 +++++++++++++++++++++++++++++++++----
crates/zed/src/zed.rs        |  12 
3 files changed, 300 insertions(+), 47 deletions(-)

Detailed changes

crates/workspace/src/item.rs 🔗

@@ -402,7 +402,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                     for item_event in T::to_item_events(event).into_iter() {
                         match item_event {
                             ItemEvent::CloseItem => {
-                                Pane::close_item(workspace, pane, item.id(), cx)
+                                Pane::close_item_by_id(workspace, pane, item.id(), cx)
                                     .detach_and_log_err(cx);
                                 return;
                             }

crates/workspace/src/pane.rs 🔗

@@ -37,6 +37,24 @@ use util::ResultExt;
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivateItem(pub usize);
 
+#[derive(Clone, PartialEq)]
+pub struct CloseItemById {
+    pub item_id: usize,
+    pub pane: WeakViewHandle<Pane>,
+}
+
+#[derive(Clone, PartialEq)]
+pub struct CloseItemsToTheLeftById {
+    pub item_id: usize,
+    pub pane: WeakViewHandle<Pane>,
+}
+
+#[derive(Clone, PartialEq)]
+pub struct CloseItemsToTheRightById {
+    pub item_id: usize,
+    pub pane: WeakViewHandle<Pane>,
+}
+
 actions!(
     pane,
     [
@@ -57,12 +75,6 @@ actions!(
     ]
 );
 
-#[derive(Clone, PartialEq)]
-pub struct CloseItem {
-    pub item_id: usize,
-    pub pane: WeakViewHandle<Pane>,
-}
-
 #[derive(Clone, PartialEq)]
 pub struct MoveItem {
     pub item_id: usize,
@@ -92,11 +104,21 @@ pub struct DeployDockMenu;
 #[derive(Clone, PartialEq)]
 pub struct DeployNewMenu;
 
+#[derive(Clone, PartialEq)]
+pub struct DeployTabContextMenu {
+    pub position: Vector2F,
+    pub item_id: usize,
+    pub pane: WeakViewHandle<Pane>,
+}
+
 impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
 impl_internal_actions!(
     pane,
     [
-        CloseItem,
+        CloseItemById,
+        CloseItemsToTheLeftById,
+        CloseItemsToTheRightById,
+        DeployTabContextMenu,
         DeploySplitMenu,
         DeployNewMenu,
         DeployDockMenu,
@@ -127,14 +149,34 @@ pub fn init(cx: &mut AppContext) {
     cx.add_async_action(Pane::close_items_to_the_left);
     cx.add_async_action(Pane::close_items_to_the_right);
     cx.add_async_action(Pane::close_all_items);
-    cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
+    cx.add_async_action(|workspace: &mut Workspace, action: &CloseItemById, cx| {
         let pane = action.pane.upgrade(cx)?;
-        let task = Pane::close_item(workspace, pane, action.item_id, cx);
+        let task = Pane::close_item_by_id(workspace, pane, action.item_id, cx);
         Some(cx.foreground().spawn(async move {
             task.await?;
             Ok(())
         }))
     });
+    cx.add_async_action(
+        |workspace: &mut Workspace, action: &CloseItemsToTheLeftById, cx| {
+            let pane = action.pane.upgrade(cx)?;
+            let task = Pane::close_items_to_the_left_by_id(workspace, pane, action.item_id, cx);
+            Some(cx.foreground().spawn(async move {
+                task.await?;
+                Ok(())
+            }))
+        },
+    );
+    cx.add_async_action(
+        |workspace: &mut Workspace, action: &CloseItemsToTheRightById, cx| {
+            let pane = action.pane.upgrade(cx)?;
+            let task = Pane::close_items_to_the_right_by_id(workspace, pane, action.item_id, cx);
+            Some(cx.foreground().spawn(async move {
+                task.await?;
+                Ok(())
+            }))
+        },
+    );
     cx.add_action(
         |workspace,
          MoveItem {
@@ -168,6 +210,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Pane::deploy_split_menu);
     cx.add_action(Pane::deploy_dock_menu);
     cx.add_action(Pane::deploy_new_menu);
+    cx.add_action(Pane::deploy_tab_context_menu);
     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         Pane::reopen_closed_item(workspace, cx).detach();
     });
@@ -214,6 +257,7 @@ pub struct Pane {
     nav_history: Rc<RefCell<NavHistory>>,
     toolbar: ViewHandle<Toolbar>,
     tab_bar_context_menu: TabBarContextMenu,
+    tab_context_menu: ViewHandle<ContextMenu>,
     docked: Option<DockAnchor>,
     _background_actions: BackgroundActions,
     _workspace_id: usize,
@@ -319,6 +363,7 @@ impl Pane {
                 kind: TabBarContextMenuKind::New,
                 handle: context_menu,
             },
+            tab_context_menu: cx.add_view(ContextMenu::new),
             docked,
             _background_actions: background_actions,
             _workspace_id: workspace_id,
@@ -742,9 +787,7 @@ impl Pane {
         let pane = pane_handle.read(cx);
         let active_item_id = pane.items[pane.active_item_index].id();
 
-        let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
-            item_id == active_item_id
-        });
+        let task = Self::close_item_by_id(workspace, pane_handle, active_item_id, cx);
 
         Some(cx.foreground().spawn(async move {
             task.await?;
@@ -752,6 +795,17 @@ impl Pane {
         }))
     }
 
+    pub fn close_item_by_id(
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        item_id_to_close: usize,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<()>> {
+        Self::close_items(workspace, pane, cx, move |view_id| {
+            view_id == item_id_to_close
+        })
+    }
+
     pub fn close_inactive_items(
         workspace: &mut Workspace,
         _: &CloseInactiveItems,
@@ -804,20 +858,35 @@ impl Pane {
         let pane = pane_handle.read(cx);
         let active_item_id = pane.items[pane.active_item_index].id();
 
+        let task = Self::close_items_to_the_left_by_id(workspace, pane_handle, active_item_id, cx);
+
+        Some(cx.foreground().spawn(async move {
+            task.await?;
+            Ok(())
+        }))
+    }
+
+    pub fn close_items_to_the_left_by_id(
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        item_id: usize,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<()>> {
         let item_ids: Vec<_> = pane
+            .read(cx)
             .items()
-            .take_while(|item| item.id() != active_item_id)
+            .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
 
-        let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
+        let task = Self::close_items(workspace, pane, cx, move |item_id| {
             item_ids.contains(&item_id)
         });
 
-        Some(cx.foreground().spawn(async move {
+        cx.foreground().spawn(async move {
             task.await?;
             Ok(())
-        }))
+        })
     }
 
     pub fn close_items_to_the_right(
@@ -829,21 +898,36 @@ impl Pane {
         let pane = pane_handle.read(cx);
         let active_item_id = pane.items[pane.active_item_index].id();
 
+        let task = Self::close_items_to_the_right_by_id(workspace, pane_handle, active_item_id, cx);
+
+        Some(cx.foreground().spawn(async move {
+            task.await?;
+            Ok(())
+        }))
+    }
+
+    pub fn close_items_to_the_right_by_id(
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        item_id: usize,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<()>> {
         let item_ids: Vec<_> = pane
+            .read(cx)
             .items()
             .rev()
-            .take_while(|item| item.id() != active_item_id)
+            .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
 
-        let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
+        let task = Self::close_items(workspace, pane, cx, move |item_id| {
             item_ids.contains(&item_id)
         });
 
-        Some(cx.foreground().spawn(async move {
+        cx.foreground().spawn(async move {
             task.await?;
             Ok(())
-        }))
+        })
     }
 
     pub fn close_all_items(
@@ -861,17 +945,6 @@ impl Pane {
         }))
     }
 
-    pub fn close_item(
-        workspace: &mut Workspace,
-        pane: ViewHandle<Pane>,
-        item_id_to_close: usize,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Task<Result<()>> {
-        Self::close_items(workspace, pane, cx, move |view_id| {
-            view_id == item_id_to_close
-        })
-    }
-
     pub fn close_items(
         workspace: &mut Workspace,
         pane: ViewHandle<Pane>,
@@ -1207,6 +1280,65 @@ impl Pane {
         self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
     }
 
+    fn deploy_tab_context_menu(
+        &mut self,
+        action: &DeployTabContextMenu,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let target_item_id = action.item_id;
+        let target_pane = action.pane.clone();
+        let active_item_id = self.items[self.active_item_index].id();
+        let is_active_item = target_item_id == active_item_id;
+
+        let mut options = Vec::new();
+
+        // TODO: Explain why we are doing this - for the key bindings
+        options.push(if is_active_item {
+            ContextMenuItem::item("Close Active Item", CloseActiveItem)
+        } else {
+            ContextMenuItem::item(
+                "Close Inactive Item",
+                CloseItemById {
+                    item_id: target_item_id,
+                    pane: target_pane.clone(),
+                },
+            )
+        });
+        // This should really be called "close others" and the behaviour should be dynamically based on the tab the action is ran on.  Currenlty, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
+        options.push(ContextMenuItem::item(
+            "Close Inactive Items",
+            CloseInactiveItems,
+        ));
+        options.push(ContextMenuItem::item("Close Clean Items", CloseCleanItems));
+        options.push(if is_active_item {
+            ContextMenuItem::item("Close Items To The Left", CloseItemsToTheLeft)
+        } else {
+            ContextMenuItem::item(
+                "Close Items To The Left",
+                CloseItemsToTheLeftById {
+                    item_id: target_item_id,
+                    pane: target_pane.clone(),
+                },
+            )
+        });
+        options.push(if is_active_item {
+            ContextMenuItem::item("Close Items To The Right", CloseItemsToTheRight)
+        } else {
+            ContextMenuItem::item(
+                "Close Items To The Right",
+                CloseItemsToTheRightById {
+                    item_id: target_item_id,
+                    pane: target_pane.clone(),
+                },
+            )
+        });
+        options.push(ContextMenuItem::item("Close All Items", CloseAllItems));
+
+        self.tab_context_menu.update(cx, |menu, cx| {
+            menu.show(action.position, AnchorCorner::TopLeft, options, cx);
+        });
+    }
+
     pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
         &self.toolbar
     }
@@ -1277,13 +1409,22 @@ impl Pane {
                             })
                             .on_click(MouseButton::Middle, {
                                 let item = item.clone();
+                                let pane = pane.clone();
                                 move |_, cx: &mut EventContext| {
-                                    cx.dispatch_action(CloseItem {
+                                    cx.dispatch_action(CloseItemById {
                                         item_id: item.id(),
                                         pane: pane.clone(),
                                     })
                                 }
                             })
+                            .on_down(MouseButton::Right, move |e, cx| {
+                                let item = item.clone();
+                                cx.dispatch_action(DeployTabContextMenu {
+                                    position: e.position,
+                                    item_id: item.id(),
+                                    pane: pane.clone(),
+                                });
+                            })
                             .boxed()
                         }
                     });
@@ -1454,7 +1595,7 @@ impl Pane {
                         .on_click(MouseButton::Left, {
                             let pane = pane.clone();
                             move |_, cx| {
-                                cx.dispatch_action(CloseItem {
+                                cx.dispatch_action(CloseItemById {
                                     item_id,
                                     pane: pane.clone(),
                                 })
@@ -1624,6 +1765,7 @@ impl View for Pane {
                                 .flex(1., true)
                                 .boxed()
                             })
+                            .with_child(ChildView::new(&self.tab_context_menu, cx).boxed())
                             .boxed()
                     } else {
                         enum EmptyPane {}
@@ -2219,14 +2361,14 @@ mod tests {
         let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
-        add_labled_item(&workspace, &pane, "A", cx);
-        add_labled_item(&workspace, &pane, "B", cx);
-        add_labled_item(&workspace, &pane, "C", cx);
-        add_labled_item(&workspace, &pane, "D", cx);
+        add_labeled_item(&workspace, &pane, "A", false, cx);
+        add_labeled_item(&workspace, &pane, "B", false, cx);
+        add_labeled_item(&workspace, &pane, "C", false, cx);
+        add_labeled_item(&workspace, &pane, "D", false, cx);
         assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
 
         pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
-        add_labled_item(&workspace, &pane, "1", cx);
+        add_labeled_item(&workspace, &pane, "1", false, cx);
         assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
 
         workspace.update(cx, |workspace, cx| {
@@ -2257,14 +2399,125 @@ mod tests {
         assert_item_labels(&pane, ["A*"], cx);
     }
 
-    fn add_labled_item(
+    #[gpui::test]
+    async fn test_close_inactive_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
+
+        workspace.update(cx, |workspace, cx| {
+            Pane::close_inactive_items(workspace, &CloseInactiveItems, cx);
+        });
+
+        deterministic.run_until_parked();
+        assert_item_labels(&pane, ["C*"], cx);
+    }
+
+    #[gpui::test]
+    async fn test_close_clean_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        add_labeled_item(&workspace, &pane, "A", true, cx);
+        add_labeled_item(&workspace, &pane, "B", false, cx);
+        add_labeled_item(&workspace, &pane, "C", false, cx);
+        add_labeled_item(&workspace, &pane, "D", false, cx);
+        add_labeled_item(&workspace, &pane, "E", false, cx);
+        assert_item_labels(&pane, ["A", "B", "C", "D", "E*"], cx);
+
+        workspace.update(cx, |workspace, cx| {
+            Pane::close_clean_items(workspace, &CloseCleanItems, cx);
+        });
+
+        deterministic.run_until_parked();
+        assert_item_labels(&pane, ["A*"], cx);
+    }
+
+    #[gpui::test]
+    async fn test_close_items_to_the_left(
+        deterministic: Arc<Deterministic>,
+        cx: &mut TestAppContext,
+    ) {
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
+
+        workspace.update(cx, |workspace, cx| {
+            Pane::close_items_to_the_left(workspace, &CloseItemsToTheLeft, cx);
+        });
+
+        deterministic.run_until_parked();
+        assert_item_labels(&pane, ["C*", "D", "E"], cx);
+    }
+
+    #[gpui::test]
+    async fn test_close_items_to_the_right(
+        deterministic: Arc<Deterministic>,
+        cx: &mut TestAppContext,
+    ) {
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
+
+        workspace.update(cx, |workspace, cx| {
+            Pane::close_items_to_the_right(workspace, &CloseItemsToTheRight, cx);
+        });
+
+        deterministic.run_until_parked();
+        assert_item_labels(&pane, ["A", "B", "C*"], cx);
+    }
+
+    #[gpui::test]
+    async fn test_close_all_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        add_labeled_item(&workspace, &pane, "A", false, cx);
+        add_labeled_item(&workspace, &pane, "B", false, cx);
+        add_labeled_item(&workspace, &pane, "C", false, cx);
+        assert_item_labels(&pane, ["A", "B", "C*"], cx);
+
+        workspace.update(cx, |workspace, cx| {
+            Pane::close_all_items(workspace, &CloseAllItems, cx);
+        });
+
+        deterministic.run_until_parked();
+        assert_item_labels(&pane, [], cx);
+    }
+
+    fn add_labeled_item(
         workspace: &ViewHandle<Workspace>,
         pane: &ViewHandle<Pane>,
         label: &str,
+        is_dirty: bool,
         cx: &mut TestAppContext,
     ) -> Box<ViewHandle<TestItem>> {
         workspace.update(cx, |workspace, cx| {
-            let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
+            let labeled_item =
+                Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty)));
 
             Pane::add_item(
                 workspace,

crates/zed/src/zed.rs 🔗

@@ -1548,7 +1548,7 @@ mod tests {
             .update(cx, |workspace, cx| {
                 let editor3_id = editor3.id();
                 drop(editor3);
-                Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
+                Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor3_id, cx)
             })
             .await
             .unwrap();
@@ -1581,7 +1581,7 @@ mod tests {
             .update(cx, |workspace, cx| {
                 let editor2_id = editor2.id();
                 drop(editor2);
-                Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
+                Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor2_id, cx)
             })
             .await
             .unwrap();
@@ -1731,7 +1731,7 @@ mod tests {
         // Close all the pane items in some arbitrary order.
         workspace
             .update(cx, |workspace, cx| {
-                Pane::close_item(workspace, pane.clone(), file1_item_id, cx)
+                Pane::close_item_by_id(workspace, pane.clone(), file1_item_id, cx)
             })
             .await
             .unwrap();
@@ -1739,7 +1739,7 @@ mod tests {
 
         workspace
             .update(cx, |workspace, cx| {
-                Pane::close_item(workspace, pane.clone(), file4_item_id, cx)
+                Pane::close_item_by_id(workspace, pane.clone(), file4_item_id, cx)
             })
             .await
             .unwrap();
@@ -1747,7 +1747,7 @@ mod tests {
 
         workspace
             .update(cx, |workspace, cx| {
-                Pane::close_item(workspace, pane.clone(), file2_item_id, cx)
+                Pane::close_item_by_id(workspace, pane.clone(), file2_item_id, cx)
             })
             .await
             .unwrap();
@@ -1755,7 +1755,7 @@ mod tests {
 
         workspace
             .update(cx, |workspace, cx| {
-                Pane::close_item(workspace, pane.clone(), file3_item_id, cx)
+                Pane::close_item_by_id(workspace, pane.clone(), file3_item_id, cx)
             })
             .await
             .unwrap();