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 <piotr@zed.dev>
This commit is contained in:
Thorsten Ball 2024-02-01 12:18:12 +01:00 committed by GitHub
parent 6c93c4bd35
commit e65a76f0ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 127 additions and 20 deletions

View file

@ -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"]
}
}
]

View file

@ -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<Self>) -> 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)
}
}
}

View file

@ -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<Self>,
) {
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>| 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<Pane>> {
cx: &WindowContext,
) -> Option<View<Pane>> {
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<Self>,
) {
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<Pane>, cx: &mut ViewContext<Self>) {
@ -3488,6 +3565,11 @@ fn open_items(
})
}
enum ActivateInDirectionTarget {
Pane(View<Pane>),
Dock(View<Dock>),
}
fn notify_if_database_failed(workspace: WindowHandle<Workspace>, 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";