From e65a76f0ec7b5e12028d4f0fa0cf538da896e47c Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 1 Feb 2024 12:18:12 +0100 Subject: [PATCH] Add ability to navigate to/from docks via keybindings (#7141) This adds the ability to navigate to/from docks (Terminal, Project, Collaboration, Assistant) via keybindings. When using the `ActivatePaneInDirection` keybinding from the left/bottom/right dock, we check whether the movement is towards the center panel. If it is, we focus the last active pane. Fixes https://github.com/zed-industries/zed/issues/6833 and it came up in a few other tickes/discussions. Release Notes: - Added ability to navigate to docks and back to the editor using the `workspace::ActivatePaneInDirection` action (by default bound to `Ctrl-w [hjkl]` in Vim mode). ([#6833](https://github.com/zed-industries/zed/issues/6833)). ## Drawback There's this weird behavior: if you start Zed and no files are opened, you focus terminal, go left (project panel), then back to right to terminal, the terminal isn't focused. Even though we focus it in the code. Maybe this is a bug in the current focus handling code? ## Demo https://github.com/zed-industries/zed/assets/1185253/5d56db40-36aa-4758-a3bc-7a0de20ce5d7 --------- Co-authored-by: Piotr --- assets/keymaps/vim.json | 13 ++++ crates/workspace/src/dock.rs | 18 ++++- crates/workspace/src/workspace.rs | 116 +++++++++++++++++++++++++----- 3 files changed, 127 insertions(+), 20 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 102e92511d..eb8bdb33ed 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -502,5 +502,18 @@ "enter": "vim::SearchSubmit", "escape": "buffer_search::Dismiss" } + }, + { + "context": "Dock", + "bindings": { + "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"] + } } ] diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 021383f73c..ffab5249e2 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -3,8 +3,9 @@ use crate::DraggedDock; use crate::{status_bar::StatusItemView, Workspace}; use gpui::{ div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Entity, EntityId, - EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render, - SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, + EventEmitter, FocusHandle, FocusableView, IntoElement, KeyContext, MouseButton, ParentElement, + Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, + WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -534,10 +535,18 @@ impl Dock { DockPosition::Right => crate::ToggleRightDock.boxed_clone(), } } + + fn dispatch_context() -> KeyContext { + let mut dispatch_context = KeyContext::default(); + dispatch_context.add("Dock"); + + dispatch_context + } } impl Render for Dock { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let dispatch_context = Self::dispatch_context(); if let Some(entry) = self.visible_entry() { let size = entry.panel.size(cx); @@ -588,6 +597,7 @@ impl Render for Dock { } div() + .key_context(dispatch_context) .track_focus(&self.focus_handle) .flex() .bg(cx.theme().colors().panel_background) @@ -612,7 +622,9 @@ impl Render for Dock { ) .child(handle) } else { - div().track_focus(&self.focus_handle) + div() + .key_context(dispatch_context) + .track_focus(&self.focus_handle) } } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 177d7384b6..19d203716b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2075,30 +2075,93 @@ impl Workspace { direction: SplitDirection, cx: &mut WindowContext, ) { - if let Some(pane) = self.find_pane_in_direction(direction, cx) { - cx.focus_view(pane); + use ActivateInDirectionTarget as Target; + enum Origin { + LeftDock, + RightDock, + BottomDock, + Center, } - } - pub fn swap_pane_in_direction( - &mut self, - direction: SplitDirection, - cx: &mut ViewContext, - ) { - if let Some(to) = self - .find_pane_in_direction(direction, cx) - .map(|pane| pane.clone()) - { - self.center.swap(&self.active_pane.clone(), &to); - cx.notify(); + let origin: Origin = [ + (&self.left_dock, Origin::LeftDock), + (&self.right_dock, Origin::RightDock), + (&self.bottom_dock, Origin::BottomDock), + ] + .into_iter() + .find_map(|(dock, origin)| { + if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() { + Some(origin) + } else { + None + } + }) + .unwrap_or(Origin::Center); + + let get_last_active_pane = || { + self.last_active_center_pane.as_ref().and_then(|p| { + let p = p.upgrade()?; + (p.read(cx).items_len() != 0).then_some(p) + }) + }; + + let try_dock = + |dock: &View| dock.read(cx).is_open().then(|| Target::Dock(dock.clone())); + + let target = match (origin, direction) { + // We're in the center, so we first try to go to a different pane, + // otherwise try to go to a dock. + (Origin::Center, direction) => { + if let Some(pane) = self.find_pane_in_direction(direction, cx) { + Some(Target::Pane(pane)) + } else { + match direction { + SplitDirection::Up => None, + SplitDirection::Down => try_dock(&self.bottom_dock), + SplitDirection::Left => try_dock(&self.left_dock), + SplitDirection::Right => try_dock(&self.right_dock), + } + } + } + + (Origin::LeftDock, SplitDirection::Right) => { + 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.right_dock)) + } + } + + (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), + (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)) + } + } + + _ => None, + }; + + match target { + Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane), + Some(ActivateInDirectionTarget::Dock(dock)) => cx.focus_view(&dock), + None => {} } } fn find_pane_in_direction( &mut self, direction: SplitDirection, - cx: &AppContext, - ) -> Option<&View> { + cx: &WindowContext, + ) -> Option> { let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else { return None; }; @@ -2124,7 +2187,21 @@ impl Workspace { Point::new(center.x, bounding_box.bottom() + distance_to_next.into()) } }; - self.center.pane_at_pixel_position(target) + self.center.pane_at_pixel_position(target).cloned() + } + + pub fn swap_pane_in_direction( + &mut self, + direction: SplitDirection, + cx: &mut ViewContext, + ) { + if let Some(to) = self + .find_pane_in_direction(direction, cx) + .map(|pane| pane.clone()) + { + self.center.swap(&self.active_pane.clone(), &to); + cx.notify(); + } } fn handle_pane_focused(&mut self, pane: View, cx: &mut ViewContext) { @@ -3488,6 +3565,11 @@ fn open_items( }) } +enum ActivateInDirectionTarget { + Pane(View), + Dock(View), +} + fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncAppContext) { const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";