Merge pull request #1257 from zed-industries/vscode-pane-bindings

Max Brunsfeld created

Adjust pane, tab, panel management bindings to match VS Code

Change summary

assets/keymaps/default.json                 | 107 ++++++++++++++++------
crates/contacts_panel/src/contacts_panel.rs |   3 
crates/project_panel/src/project_panel.rs   |   3 
crates/workspace/src/pane.rs                |  14 +-
crates/workspace/src/sidebar.rs             |  54 +++++++---
crates/workspace/src/waiting_room.rs        |  13 --
crates/workspace/src/workspace.rs           |  70 +++++++++++---
crates/zed/src/menus.rs                     |  41 +++++++-
crates/zed/src/zed.rs                       |  14 ++
9 files changed, 234 insertions(+), 85 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -13,6 +13,8 @@
             "ctrl-c": "menu::Cancel",
             "shift-cmd-{": "pane::ActivatePrevItem",
             "shift-cmd-}": "pane::ActivateNextItem",
+            "alt-cmd-left": "pane::ActivatePrevItem",
+            "alt-cmd-right": "pane::ActivateNextItem",
             "cmd-w": "pane::CloseActiveItem",
             "cmd-shift-W": "workspace::CloseWindow",
             "alt-cmd-t": "pane::CloseInactiveItems",
@@ -210,6 +212,43 @@
     {
         "context": "Pane",
         "bindings": {
+            "ctrl-1": [
+                "pane::ActivateItem",
+                0
+            ],
+            "ctrl-2": [
+                "pane::ActivateItem",
+                1
+            ],
+            "ctrl-3": [
+                "pane::ActivateItem",
+                2
+            ],
+            "ctrl-4": [
+                "pane::ActivateItem",
+                3
+            ],
+            "ctrl-5": [
+                "pane::ActivateItem",
+                4
+            ],
+            "ctrl-6": [
+                "pane::ActivateItem",
+                5
+            ],
+            "ctrl-7": [
+                "pane::ActivateItem",
+                6
+            ],
+            "ctrl-8": [
+                "pane::ActivateItem",
+                7
+            ],
+            "ctrl-9": [
+                "pane::ActivateItem",
+                8
+            ],
+            "ctrl-0": "pane::ActivateLastItem",
             "ctrl--": "pane::GoBack",
             "shift-ctrl-_": "pane::GoForward",
             "cmd-shift-T": "pane::ReopenClosedItem",
@@ -219,6 +258,43 @@
     {
         "context": "Workspace",
         "bindings": {
+            "cmd-1": [
+                "workspace::ActivatePane",
+                0
+            ],
+            "cmd-2": [
+                "workspace::ActivatePane",
+                1
+            ],
+            "cmd-3": [
+                "workspace::ActivatePane",
+                2
+            ],
+            "cmd-4": [
+                "workspace::ActivatePane",
+                3
+            ],
+            "cmd-5": [
+                "workspace::ActivatePane",
+                4
+            ],
+            "cmd-6": [
+                "workspace::ActivatePane",
+                5
+            ],
+            "cmd-7": [
+                "workspace::ActivatePane",
+                6
+            ],
+            "cmd-8": [
+                "workspace::ActivatePane",
+                7
+            ],
+            "cmd-9": [
+                "workspace::ActivatePane",
+                8
+            ],
+            "cmd-b": "workspace::ToggleLeftSidebar",
             "cmd-shift-F": "project_search::Deploy",
             "cmd-k cmd-t": "theme_selector::Toggle",
             "cmd-k cmd-s": "zed::OpenKeymap",
@@ -226,6 +302,7 @@
             "cmd-p": "file_finder::Toggle",
             "cmd-shift-P": "command_palette::Toggle",
             "cmd-shift-M": "diagnostics::Deploy",
+            "cmd-shift-E": "project_panel::Toggle",
             "cmd-alt-s": "workspace::SaveAll"
         }
     },
@@ -310,34 +387,8 @@
     {
         "context": "Workspace",
         "bindings": {
-            "cmd-1": [
-                "workspace::ToggleSidebarItemFocus",
-                {
-                    "side": "Left",
-                    "item_index": 0
-                }
-            ],
-            "cmd-shift-!": [
-                "workspace::ToggleSidebarItem",
-                {
-                    "side": "Left",
-                    "item_index": 0
-                }
-            ],
-            "cmd-9": [
-                "workspace::ToggleSidebarItemFocus",
-                {
-                    "side": "Right",
-                    "item_index": 0
-                }
-            ],
-            "cmd-shift-(": [
-                "workspace::ToggleSidebarItem",
-                {
-                    "side": "Right",
-                    "item_index": 0
-                }
-            ]
+            "cmd-shift-C": "contacts_panel::Toggle",
+            "cmd-shift-B": "workspace::ToggleRightSidebar"
         }
     },
     {

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -8,6 +8,7 @@ use contact_notification::ContactNotification;
 use editor::{Cancel, Editor};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
+    actions,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     impl_actions, impl_internal_actions,
@@ -24,6 +25,8 @@ use std::{ops::DerefMut, sync::Arc};
 use theme::IconButton;
 use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace};
 
+actions!(contacts_panel, [Toggle]);
+
 impl_actions!(
     contacts_panel,
     [RequestContact, RemoveContact, RespondToContactRequest]

crates/workspace/src/pane.rs 🔗

@@ -18,11 +18,15 @@ use settings::Settings;
 use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
 use util::ResultExt;
 
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct ActivateItem(pub usize);
+
 actions!(
     pane,
     [
         ActivatePrevItem,
         ActivateNextItem,
+        ActivateLastItem,
         CloseActiveItem,
         CloseInactiveItems,
         ReopenClosedItem,
@@ -39,9 +43,6 @@ pub struct CloseItem {
     pub pane: WeakViewHandle<Pane>,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct ActivateItem(pub usize);
-
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct GoBack {
     #[serde(skip_deserializing)]
@@ -54,8 +55,8 @@ pub struct GoForward {
     pub pane: Option<WeakViewHandle<Pane>>,
 }
 
-impl_actions!(pane, [GoBack, GoForward]);
-impl_internal_actions!(pane, [CloseItem, ActivateItem]);
+impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
+impl_internal_actions!(pane, [CloseItem]);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -63,6 +64,9 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
         pane.activate_item(action.0, true, true, cx);
     });
+    cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
+        pane.activate_item(pane.items.len() - 1, true, true, cx);
+    });
     cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
         pane.activate_prev_item(cx);
     });

crates/workspace/src/sidebar.rs 🔗

@@ -55,7 +55,8 @@ impl Into<AnyViewHandle> for &dyn SidebarItemHandle {
 pub struct Sidebar {
     side: Side,
     items: Vec<Item>,
-    active_item_ix: Option<usize>,
+    is_open: bool,
+    active_item_ix: usize,
     actual_width: Rc<RefCell<f32>>,
     custom_width: Rc<RefCell<f32>>,
 }
@@ -83,25 +84,41 @@ pub struct ToggleSidebarItem {
     pub item_index: usize,
 }
 
-#[derive(Clone, Debug, Deserialize, PartialEq)]
-pub struct ToggleSidebarItemFocus {
-    pub side: Side,
-    pub item_index: usize,
-}
-
-impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
+impl_actions!(workspace, [ToggleSidebarItem]);
 
 impl Sidebar {
     pub fn new(side: Side) -> Self {
         Self {
             side,
             items: Default::default(),
-            active_item_ix: None,
+            active_item_ix: 0,
+            is_open: false,
             actual_width: Rc::new(RefCell::new(260.)),
             custom_width: Rc::new(RefCell::new(260.)),
         }
     }
 
+    pub fn is_open(&self) -> bool {
+        self.is_open
+    }
+
+    pub fn active_item_ix(&self) -> usize {
+        self.active_item_ix
+    }
+
+    pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
+        if open != self.is_open {
+            self.is_open = open;
+            cx.notify();
+        }
+    }
+
+    pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
+        if self.is_open {}
+        self.is_open = !self.is_open;
+        cx.notify();
+    }
+
     pub fn add_item<T: SidebarItem>(
         &mut self,
         icon_path: &'static str,
@@ -133,23 +150,25 @@ impl Sidebar {
     }
 
     pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
-        self.active_item_ix = Some(item_ix);
+        self.active_item_ix = item_ix;
         cx.notify();
     }
 
     pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
-        if self.active_item_ix == Some(item_ix) {
-            self.active_item_ix = None;
+        if self.active_item_ix == item_ix {
+            self.is_open = false;
         } else {
-            self.active_item_ix = Some(item_ix);
+            self.active_item_ix = item_ix;
         }
         cx.notify();
     }
 
     pub fn active_item(&self) -> Option<&Rc<dyn SidebarItemHandle>> {
-        self.active_item_ix
-            .and_then(|ix| self.items.get(ix))
-            .map(|item| &item.view)
+        if self.is_open {
+            self.items.get(self.active_item_ix).map(|item| &item.view)
+        } else {
+            None
+        }
     }
 
     fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
@@ -249,6 +268,7 @@ impl View for SidebarButtons {
         let item_style = theme.item;
         let badge_style = theme.badge;
         let active_ix = sidebar.active_item_ix;
+        let is_open = sidebar.is_open;
         let side = sidebar.side;
         let group_style = match side {
             Side::Left => theme.group_left,
@@ -267,7 +287,7 @@ impl View for SidebarButtons {
                         item_index: ix,
                     };
                     MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, cx| {
-                        let is_active = Some(ix) == active_ix;
+                        let is_active = is_open && ix == active_ix;
                         let style = item_style.style_for(state, is_active);
                         Stack::new()
                             .with_child(Svg::new(icon_path).with_color(style.icon_color).boxed())

crates/workspace/src/waiting_room.rs 🔗

@@ -1,7 +1,4 @@
-use crate::{
-    sidebar::{Side, ToggleSidebarItem},
-    AppState, ToggleFollow, Workspace,
-};
+use crate::{sidebar::Side, AppState, ToggleFollow, Workspace};
 use anyhow::Result;
 use client::{proto, Client, Contact};
 use gpui::{
@@ -104,13 +101,7 @@ impl WaitingRoom {
                                             &app_state,
                                             cx,
                                         );
-                                        workspace.toggle_sidebar_item(
-                                            &ToggleSidebarItem {
-                                                side: Side::Left,
-                                                item_index: 0,
-                                            },
-                                            cx,
-                                        );
+                                        workspace.toggle_sidebar(Side::Left, cx);
                                         if let Some((host_peer_id, _)) =
                                             workspace.project.read(cx).collaborators().iter().find(
                                                 |(_, collaborator)| collaborator.replica_id == 0,

crates/workspace/src/workspace.rs 🔗

@@ -31,7 +31,7 @@ use postage::prelude::Stream;
 use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
 use serde::Deserialize;
 use settings::Settings;
-use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus};
+use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
 use smallvec::SmallVec;
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
@@ -90,6 +90,8 @@ actions!(
         ActivatePreviousPane,
         ActivateNextPane,
         FollowNextCollaborator,
+        ToggleLeftSidebar,
+        ToggleRightSidebar,
     ]
 );
 
@@ -104,6 +106,9 @@ pub struct ToggleProjectOnline {
     pub project: Option<ModelHandle<Project>>,
 }
 
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct ActivatePane(pub usize);
+
 #[derive(Clone, PartialEq)]
 pub struct ToggleFollow(pub PeerId);
 
@@ -122,7 +127,7 @@ impl_internal_actions!(
         RemoveWorktreeFromProject
     ]
 );
-impl_actions!(workspace, [ToggleProjectOnline]);
+impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
 
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     pane::init(cx);
@@ -185,7 +190,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
         },
     );
     cx.add_action(Workspace::toggle_sidebar_item);
-    cx.add_action(Workspace::toggle_sidebar_item_focus);
     cx.add_action(Workspace::focus_center);
     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
         workspace.activate_previous_pane(cx)
@@ -193,6 +197,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
         workspace.activate_next_pane(cx)
     });
+    cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
+        workspace.toggle_sidebar(Side::Left, cx);
+    });
+    cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| {
+        workspace.toggle_sidebar(Side::Right, cx);
+    });
+    cx.add_action(Workspace::activate_pane_at_index);
 
     let client = &app_state.client;
     client.add_view_request_handler(Workspace::handle_follow);
@@ -1248,17 +1259,39 @@ impl Workspace {
         }
     }
 
+    pub fn toggle_sidebar(&mut self, side: Side, cx: &mut ViewContext<Self>) {
+        let sidebar = match side {
+            Side::Left => &mut self.left_sidebar,
+            Side::Right => &mut self.right_sidebar,
+        };
+        sidebar.update(cx, |sidebar, cx| {
+            sidebar.set_open(!sidebar.is_open(), cx);
+        });
+        cx.focus_self();
+        cx.notify();
+    }
+
     pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
         let sidebar = match action.side {
             Side::Left => &mut self.left_sidebar,
             Side::Right => &mut self.right_sidebar,
         };
         let active_item = sidebar.update(cx, |sidebar, cx| {
-            sidebar.toggle_item(action.item_index, cx);
-            sidebar.active_item().map(|item| item.to_any())
+            if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
+                sidebar.set_open(false, cx);
+                None
+            } else {
+                sidebar.set_open(true, cx);
+                sidebar.activate_item(action.item_index, cx);
+                sidebar.active_item().cloned()
+            }
         });
         if let Some(active_item) = active_item {
-            cx.focus(active_item);
+            if active_item.is_focused(cx) {
+                cx.focus_self();
+            } else {
+                cx.focus(active_item.to_any());
+            }
         } else {
             cx.focus_self();
         }
@@ -1267,15 +1300,17 @@ impl Workspace {
 
     pub fn toggle_sidebar_item_focus(
         &mut self,
-        action: &ToggleSidebarItemFocus,
+        side: Side,
+        item_index: usize,
         cx: &mut ViewContext<Self>,
     ) {
-        let sidebar = match action.side {
+        let sidebar = match side {
             Side::Left => &mut self.left_sidebar,
             Side::Right => &mut self.right_sidebar,
         };
         let active_item = sidebar.update(cx, |sidebar, cx| {
-            sidebar.activate_item(action.item_index, cx);
+            sidebar.set_open(true, cx);
+            sidebar.activate_item(item_index, cx);
             sidebar.active_item().cloned()
         });
         if let Some(active_item) = active_item {
@@ -1405,6 +1440,15 @@ impl Workspace {
         }
     }
 
+    fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
+        let panes = self.center.panes();
+        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
+            self.activate_pane(pane, cx);
+        } else {
+            self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
+        }
+    }
+
     pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
         let next_pane = {
             let panes = self.center.panes();
@@ -2481,13 +2525,7 @@ pub fn open_paths(
                 let mut workspace = Workspace::new(project, cx);
                 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
                 if contains_directory {
-                    workspace.toggle_sidebar_item(
-                        &ToggleSidebarItem {
-                            side: Side::Left,
-                            item_index: 0,
-                        },
-                        cx,
-                    );
+                    workspace.toggle_sidebar(Side::Left, cx);
                 }
                 workspace
             })

crates/zed/src/menus.rs 🔗

@@ -187,11 +187,42 @@ pub fn menus() -> Vec<Menu<'static>> {
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
-                    name: "Project Browser",
-                    action: Box::new(workspace::sidebar::ToggleSidebarItemFocus {
-                        side: workspace::sidebar::Side::Left,
-                        item_index: 0,
-                    }),
+                    name: "Toggle Left Sidebar",
+                    action: Box::new(workspace::ToggleLeftSidebar),
+                },
+                MenuItem::Action {
+                    name: "Toggle Right Sidebar",
+                    action: Box::new(workspace::ToggleRightSidebar),
+                },
+                MenuItem::Submenu(Menu {
+                    name: "Editor Layout",
+                    items: vec![
+                        MenuItem::Action {
+                            name: "Split Up",
+                            action: Box::new(workspace::SplitUp),
+                        },
+                        MenuItem::Action {
+                            name: "Split Down",
+                            action: Box::new(workspace::SplitDown),
+                        },
+                        MenuItem::Action {
+                            name: "Split Left",
+                            action: Box::new(workspace::SplitLeft),
+                        },
+                        MenuItem::Action {
+                            name: "Split Right",
+                            action: Box::new(workspace::SplitRight),
+                        },
+                    ],
+                }),
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Project Panel",
+                    action: Box::new(project_panel::Toggle),
+                },
+                MenuItem::Action {
+                    name: "Contacts Panel",
+                    action: Box::new(contacts_panel::Toggle),
                 },
                 MenuItem::Action {
                     name: "Command Palette",

crates/zed/src/zed.rs 🔗

@@ -34,7 +34,7 @@ use std::{
 };
 use util::ResultExt;
 pub use workspace;
-use workspace::{AppState, Workspace};
+use workspace::{sidebar::Side, AppState, Workspace};
 
 #[derive(Deserialize, Clone, PartialEq)]
 struct OpenBrowser {
@@ -128,6 +128,16 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
             }
         },
     );
+    cx.add_action(
+        |workspace: &mut Workspace, _: &project_panel::Toggle, cx: &mut ViewContext<Workspace>| {
+            workspace.toggle_sidebar_item_focus(Side::Left, 0, cx);
+        },
+    );
+    cx.add_action(
+        |workspace: &mut Workspace, _: &contacts_panel::Toggle, cx: &mut ViewContext<Workspace>| {
+            workspace.toggle_sidebar_item_focus(Side::Right, 0, cx);
+        },
+    );
 
     lsp_status::init(cx);
     settings::KeymapFileContent::load_defaults(cx);
@@ -429,7 +439,7 @@ mod tests {
         let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
         workspace_1.update(cx, |workspace, cx| {
             assert_eq!(workspace.worktrees(cx).count(), 2);
-            assert!(workspace.left_sidebar().read(cx).active_item().is_some());
+            assert!(workspace.left_sidebar().read(cx).is_open());
             assert!(workspace.active_pane().is_focused(cx));
         });