vim fidelity in sidebar

cameron created

Change summary

assets/keymaps/vim.json           |  2 +
crates/sidebar/src/sidebar.rs     | 43 +++++++++++++++++++++++++
crates/workspace/src/workspace.rs | 56 +++++++++++++++++++++++++-------
3 files changed, 88 insertions(+), 13 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -1123,6 +1123,8 @@
       "g g": "menu::SelectFirst",
       "shift-g": "menu::SelectLast",
       "/": "agents_sidebar::FocusSidebarFilter",
+      "d d": "agents_sidebar::RemoveSelected",
+      "o": "agents_sidebar::NewThreadInGroup",
       "z a": "editor::ToggleFold",
       "z c": "menu::SelectParent",
       "z o": "menu::SelectChild",

crates/sidebar/src/sidebar.rs 🔗

@@ -54,6 +54,10 @@ gpui::actions!(
         NewThreadInGroup,
         /// Toggles between the thread list and the archive view.
         ToggleArchive,
+        /// Closes the currently selected workspace.
+        RemoveSelectedWorkspace,
+        /// Removes the selected entry: archives a thread or closes a workspace.
+        RemoveSelected,
     ]
 );
 
@@ -2453,6 +2457,43 @@ impl Sidebar {
         self.archive_thread(&session_id, window, cx);
     }
 
+    fn remove_selected_workspace(
+        &mut self,
+        _: &RemoveSelectedWorkspace,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(ix) = self.selection else {
+            return;
+        };
+        let Some(ListEntry::ProjectHeader { workspace, .. }) = self.contents.entries.get(ix)
+        else {
+            return;
+        };
+        let workspace = workspace.clone();
+        self.remove_workspace(&workspace, window, cx);
+    }
+
+    fn remove_selected(
+        &mut self,
+        _: &RemoveSelected,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(ix) = self.selection else {
+            return;
+        };
+        match self.contents.entries.get(ix) {
+            Some(ListEntry::Thread(_)) => {
+                self.remove_selected_thread(&RemoveSelectedThread, window, cx);
+            }
+            Some(ListEntry::ProjectHeader { .. }) => {
+                self.remove_selected_workspace(&RemoveSelectedWorkspace, window, cx);
+            }
+            _ => {}
+        }
+    }
+
     fn render_thread(
         &self,
         ix: usize,
@@ -3160,6 +3201,8 @@ impl Render for Sidebar {
             .on_action(cx.listener(Self::unfold_all))
             .on_action(cx.listener(Self::cancel))
             .on_action(cx.listener(Self::remove_selected_thread))
+            .on_action(cx.listener(Self::remove_selected_workspace))
+            .on_action(cx.listener(Self::remove_selected))
             .on_action(cx.listener(Self::new_thread_in_group))
             .on_action(cx.listener(Self::toggle_archive))
             .on_action(cx.listener(Self::focus_sidebar_filter))

crates/workspace/src/workspace.rs 🔗

@@ -4730,12 +4730,36 @@ impl Workspace {
             .as_ref()
             .map(|h| Target::Sidebar(h.clone()));
 
+        let sidebar_on_left = self
+            .multi_workspace
+            .as_ref()
+            .and_then(|mw| mw.upgrade())
+            .map_or(true, |mw| mw.read(cx).sidebar_side(cx) == SidebarSide::Left);
+
+        let sidebar_on_side = |side: SplitDirection| -> Option<ActivateInDirectionTarget> {
+            if (side == SplitDirection::Left) == sidebar_on_left {
+                sidebar_target.clone()
+            } else {
+                None
+            }
+        };
+
         let target = match (origin, direction) {
-            // From the sidebar, only Right navigates into the workspace.
-            (Origin::Sidebar, SplitDirection::Right) => try_dock(&self.left_dock)
-                .or_else(|| get_last_active_pane().map(Target::Pane))
-                .or_else(|| try_dock(&self.bottom_dock))
-                .or_else(|| try_dock(&self.right_dock)),
+            // From the sidebar, only the inward direction navigates into
+            // the workspace. Which direction is "inward" depends on which
+            // side the sidebar is on.
+            (Origin::Sidebar, SplitDirection::Right) if sidebar_on_left => {
+                try_dock(&self.left_dock)
+                    .or_else(|| get_last_active_pane().map(Target::Pane))
+                    .or_else(|| try_dock(&self.bottom_dock))
+                    .or_else(|| try_dock(&self.right_dock))
+            }
+            (Origin::Sidebar, SplitDirection::Left) if !sidebar_on_left => {
+                try_dock(&self.right_dock)
+                    .or_else(|| get_last_active_pane().map(Target::Pane))
+                    .or_else(|| try_dock(&self.bottom_dock))
+                    .or_else(|| try_dock(&self.left_dock))
+            }
 
             (Origin::Sidebar, _) => None,
 
@@ -4748,8 +4772,12 @@ impl Workspace {
                     match direction {
                         SplitDirection::Up => None,
                         SplitDirection::Down => try_dock(&self.bottom_dock),
-                        SplitDirection::Left => try_dock(&self.left_dock).or(sidebar_target),
-                        SplitDirection::Right => try_dock(&self.right_dock),
+                        SplitDirection::Left => {
+                            try_dock(&self.left_dock).or_else(|| sidebar_on_side(direction))
+                        }
+                        SplitDirection::Right => {
+                            try_dock(&self.right_dock).or_else(|| sidebar_on_side(direction))
+                        }
                     }
                 }
             }
@@ -4762,27 +4790,29 @@ impl Workspace {
                 }
             }
 
-            (Origin::LeftDock, SplitDirection::Left) => sidebar_target,
+            (Origin::LeftDock, SplitDirection::Left) => sidebar_on_side(SplitDirection::Left),
 
             (Origin::LeftDock, SplitDirection::Down)
             | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
 
             (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
             (Origin::BottomDock, SplitDirection::Left) => {
-                try_dock(&self.left_dock).or(sidebar_target)
+                try_dock(&self.left_dock).or_else(|| sidebar_on_side(SplitDirection::Left))
+            }
+            (Origin::BottomDock, SplitDirection::Right) => {
+                try_dock(&self.right_dock).or_else(|| sidebar_on_side(SplitDirection::Right))
             }
-            (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
 
             (Origin::RightDock, SplitDirection::Left) => {
                 if let Some(last_active_pane) = get_last_active_pane() {
                     Some(Target::Pane(last_active_pane))
                 } else {
-                    try_dock(&self.bottom_dock)
-                        .or_else(|| try_dock(&self.left_dock))
-                        .or(sidebar_target)
+                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
                 }
             }
 
+            (Origin::RightDock, SplitDirection::Right) => sidebar_on_side(SplitDirection::Right),
+
             _ => None,
         };