Add workspace::ActivatePaneInDirection (#2757)

This change adds support for choosing a pane based on direction; and
adds default keybindings (`cmd+k cmd+{left,right,up,down}`) and vim
keybindings.

Release Notes:

- Add support for navigating to the next pane in a given direction using
`cmd+k cmd-{up,down,left,right}`
([#476](https://github.com/zed-industries/community/issues/476),
[#478](https://github.com/zed-industries/community/issues/478))
- Vim: adds support for many window related shortcuts: `ctrl-w
{h,j,k,l,up,down,left,right,w,W,p}` for navigating around panes, `ctrl-w
{q,c}` for closing panes and `ctrl-w {v,s}` for splitting panes.
This commit is contained in:
Conrad Irwin 2023-07-20 11:17:13 -06:00 committed by GitHub
commit 372f66c88a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 243 additions and 8 deletions

View File

@ -446,8 +446,22 @@
},
{
"bindings": {
"cmd-k cmd-left": "workspace::ActivatePreviousPane",
"cmd-k cmd-right": "workspace::ActivateNextPane"
"cmd-k cmd-left": [
"workspace::ActivatePaneInDirection",
"Left"
],
"cmd-k cmd-right": [
"workspace::ActivatePaneInDirection",
"Right"
],
"cmd-k cmd-up": [
"workspace::ActivatePaneInDirection",
"Up"
],
"cmd-k cmd-down": [
"workspace::ActivatePaneInDirection",
"Down"
]
}
},
// Bindings from Atom

View File

@ -145,7 +145,75 @@
"9": [
"vim::Number",
9
]
],
// window related commands (ctrl-w X)
"ctrl-w left": [
"workspace::ActivatePaneInDirection",
"Left"
],
"ctrl-w right": [
"workspace::ActivatePaneInDirection",
"Right"
],
"ctrl-w up": [
"workspace::ActivatePaneInDirection",
"Up"
],
"ctrl-w down": [
"workspace::ActivatePaneInDirection",
"Down"
],
"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"
],
"ctrl-w g t": "pane::ActivateNextItem",
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
"ctrl-w g shift-t": "pane::ActivatePrevItem",
"ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem",
"ctrl-w w": "workspace::ActivateNextPane",
"ctrl-w ctrl-w": "workspace::ActivateNextPane",
"ctrl-w p": "workspace::ActivatePreviousPane",
"ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
"ctrl-w shift-w": "workspace::ActivatePreviousPane",
"ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
"ctrl-w v": "pane::SplitLeft",
"ctrl-w ctrl-v": "pane::SplitLeft",
"ctrl-w s": "pane::SplitUp",
"ctrl-w shift-s": "pane::SplitUp",
"ctrl-w ctrl-s": "pane::SplitUp",
"ctrl-w c": "pane::CloseAllItems",
"ctrl-w ctrl-c": "pane::CloseAllItems",
"ctrl-w q": "pane::CloseAllItems",
"ctrl-w ctrl-q": "pane::CloseAllItems"
}
},
{

View File

@ -563,6 +563,7 @@ pub struct Editor {
inlay_hint_cache: InlayHintCache,
next_inlay_id: usize,
_subscriptions: Vec<Subscription>,
pixel_position_of_newest_cursor: Option<Vector2F>,
}
pub struct EditorSnapshot {
@ -1394,6 +1395,7 @@ impl Editor {
copilot_state: Default::default(),
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),

View File

@ -61,6 +61,7 @@ enum FoldMarkers {}
struct SelectionLayout {
head: DisplayPoint,
cursor_shape: CursorShape,
is_newest: bool,
range: Range<DisplayPoint>,
}
@ -70,6 +71,7 @@ impl SelectionLayout {
line_mode: bool,
cursor_shape: CursorShape,
map: &DisplaySnapshot,
is_newest: bool,
) -> Self {
if line_mode {
let selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
@ -77,6 +79,7 @@ impl SelectionLayout {
Self {
head: selection.head().to_display_point(map),
cursor_shape,
is_newest,
range: point_range.start.to_display_point(map)
..point_range.end.to_display_point(map),
}
@ -85,6 +88,7 @@ impl SelectionLayout {
Self {
head: selection.head(),
cursor_shape,
is_newest,
range: selection.range(),
}
}
@ -864,6 +868,12 @@ impl EditorElement {
let x = cursor_character_x - scroll_left;
let y = cursor_position.row() as f32 * layout.position_map.line_height
- scroll_top;
if selection.is_newest {
editor.pixel_position_of_newest_cursor = Some(vec2f(
bounds.origin_x() + x + block_width / 2.,
bounds.origin_y() + y + layout.position_map.line_height / 2.,
));
}
cursors.push(Cursor {
color: selection_style.cursor,
block_width,
@ -2109,6 +2119,7 @@ impl Element<Editor> for EditorElement {
line_mode,
cursor_shape,
&snapshot.display_snapshot,
false,
));
}
selections.extend(remote_selections);
@ -2118,6 +2129,7 @@ impl Element<Editor> for EditorElement {
.selections
.disjoint_in_range(start_anchor..end_anchor, cx);
local_selections.extend(editor.selections.pending(cx));
let newest = editor.selections.newest(cx);
for selection in &local_selections {
let is_empty = selection.start == selection.end;
let selection_start = snapshot.prev_line_boundary(selection.start).1;
@ -2140,11 +2152,13 @@ impl Element<Editor> for EditorElement {
local_selections
.into_iter()
.map(|selection| {
let is_newest = selection == newest;
SelectionLayout::new(
selection,
editor.selections.line_mode,
editor.cursor_shape,
&snapshot.display_snapshot,
is_newest,
)
})
.collect(),

View File

@ -7,8 +7,10 @@ use anyhow::{Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
elements::*,
geometry::vector::{vec2f, Vector2F},
AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
@ -750,6 +752,10 @@ impl Item for Editor {
Some(Box::new(handle.clone()))
}
fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
self.pixel_position_of_newest_cursor
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft { flex: None }
}

View File

@ -5,6 +5,7 @@ use crate::{
use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
use anyhow::Result;
use client::{proto, Client};
use gpui::geometry::vector::Vector2F;
use gpui::{
fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, ModelHandle, Task, View,
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
@ -203,6 +204,9 @@ pub trait Item: View {
fn show_toolbar(&self) -> bool {
true
}
fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
None
}
}
pub trait ItemHandle: 'static + fmt::Debug {
@ -271,6 +275,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
fn serialized_item_kind(&self) -> Option<&'static str>;
fn show_toolbar(&self, cx: &AppContext) -> bool;
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F>;
}
pub trait WeakItemHandle {
@ -615,6 +620,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
fn show_toolbar(&self, cx: &AppContext) -> bool {
self.read(cx).show_toolbar()
}
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
self.read(cx).pixel_position_of_cursor()
}
}
impl From<Box<dyn ItemHandle>> for AnyViewHandle {

View File

@ -542,6 +542,12 @@ impl Pane {
self.items.get(self.active_item_index).cloned()
}
pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
self.items
.get(self.active_item_index)?
.pixel_position_of_cursor(cx)
}
pub fn item_for_entry(
&self,
entry_id: ProjectEntryId,

View File

@ -54,6 +54,20 @@ impl PaneGroup {
}
}
pub fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
match &self.root {
Member::Pane(_) => None,
Member::Axis(axis) => axis.bounding_box_for_pane(pane),
}
}
pub fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle<Pane>> {
match &self.root {
Member::Pane(pane) => Some(pane),
Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
}
}
/// Returns:
/// - Ok(true) if it found and removed a pane
/// - Ok(false) if it found but did not remove the pane
@ -309,15 +323,18 @@ pub(crate) struct PaneAxis {
pub axis: Axis,
pub members: Vec<Member>,
pub flexes: Rc<RefCell<Vec<f32>>>,
pub bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
}
impl PaneAxis {
pub fn new(axis: Axis, members: Vec<Member>) -> Self {
let flexes = Rc::new(RefCell::new(vec![1.; members.len()]));
let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
Self {
axis,
members,
flexes,
bounding_boxes,
}
}
@ -326,10 +343,12 @@ impl PaneAxis {
debug_assert!(members.len() == flexes.len());
let flexes = Rc::new(RefCell::new(flexes));
let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
Self {
axis,
members,
flexes,
bounding_boxes,
}
}
@ -409,6 +428,44 @@ impl PaneAxis {
}
}
fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
for (idx, member) in self.members.iter().enumerate() {
match member {
Member::Pane(found) => {
if pane == found {
return self.bounding_boxes.borrow()[idx];
}
}
Member::Axis(axis) => {
if let Some(rect) = axis.bounding_box_for_pane(pane) {
return Some(rect);
}
}
}
}
None
}
fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle<Pane>> {
debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
let bounding_boxes = self.bounding_boxes.borrow();
for (idx, member) in self.members.iter().enumerate() {
if let Some(coordinates) = bounding_boxes[idx] {
if coordinates.contains_point(coordinate) {
return match member {
Member::Pane(found) => Some(found),
Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
};
}
}
}
None
}
fn render(
&self,
project: &ModelHandle<Project>,
@ -423,7 +480,12 @@ impl PaneAxis {
) -> AnyElement<Workspace> {
debug_assert!(self.members.len() == self.flexes.borrow().len());
let mut pane_axis = PaneAxisElement::new(self.axis, basis, self.flexes.clone());
let mut pane_axis = PaneAxisElement::new(
self.axis,
basis,
self.flexes.clone(),
self.bounding_boxes.clone(),
);
let mut active_pane_ix = None;
let mut members = self.members.iter().enumerate().peekable();
@ -546,14 +608,21 @@ mod element {
active_pane_ix: Option<usize>,
flexes: Rc<RefCell<Vec<f32>>>,
children: Vec<AnyElement<Workspace>>,
bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
}
impl PaneAxisElement {
pub fn new(axis: Axis, basis: usize, flexes: Rc<RefCell<Vec<f32>>>) -> Self {
pub fn new(
axis: Axis,
basis: usize,
flexes: Rc<RefCell<Vec<f32>>>,
bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
) -> Self {
Self {
axis,
basis,
flexes,
bounding_boxes,
active_pane_ix: None,
children: Default::default(),
}
@ -708,11 +777,16 @@ mod element {
let mut child_origin = bounds.origin();
let mut bounding_boxes = self.bounding_boxes.borrow_mut();
bounding_boxes.clear();
let mut children_iter = self.children.iter_mut().enumerate().peekable();
while let Some((ix, child)) = children_iter.next() {
let child_start = child_origin.clone();
child.paint(scene, child_origin, visible_bounds, view, cx);
bounding_boxes.push(Some(RectF::new(child_origin, child.size())));
match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),

View File

@ -152,6 +152,9 @@ pub struct OpenPaths {
#[derive(Clone, Deserialize, PartialEq)]
pub struct ActivatePane(pub usize);
#[derive(Clone, Deserialize, PartialEq)]
pub struct ActivatePaneInDirection(pub SplitDirection);
#[derive(Deserialize)]
pub struct Toast {
id: usize,
@ -197,7 +200,7 @@ impl Clone for Toast {
}
}
impl_actions!(workspace, [ActivatePane, Toast]);
impl_actions!(workspace, [ActivatePane, ActivatePaneInDirection, Toast]);
pub type WorkspaceId = i64;
@ -262,6 +265,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
workspace.activate_next_pane(cx)
});
cx.add_action(
|workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
workspace.activate_pane_in_direction(action.0, cx)
},
);
cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
workspace.toggle_dock(DockPosition::Left, cx);
});
@ -2054,6 +2064,37 @@ impl Workspace {
}
}
pub fn activate_pane_in_direction(
&mut self,
direction: SplitDirection,
cx: &mut ViewContext<Self>,
) {
let bounding_box = match self.center.bounding_box_for_pane(&self.active_pane) {
Some(coordinates) => coordinates,
None => {
return;
}
};
let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
let center = match cursor {
Some(cursor) if bounding_box.contains_point(cursor) => cursor,
_ => bounding_box.center(),
};
let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.;
let target = match direction {
SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()),
SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()),
SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
};
if let Some(pane) = self.center.pane_at_pixel_position(target) {
cx.focus(pane);
}
}
fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
if self.active_pane != pane {
self.active_pane = pane.clone();
@ -3030,6 +3071,7 @@ impl Workspace {
axis,
members,
flexes,
bounding_boxes: _,
}) => SerializedPaneGroup::Group {
axis: *axis,
children: members