From e1379f0ef051cf43b524d292741c0065dbbd1e40 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 19 Jul 2023 17:58:21 -0600 Subject: [PATCH 1/6] Add support for activating a pane by direction Contributes: zed-industries/community#476 Contributes: zed-industries/community#478 --- crates/editor/src/editor.rs | 2 + crates/editor/src/element.rs | 14 ++++++ crates/editor/src/items.rs | 10 +++- crates/workspace/src/item.rs | 9 ++++ crates/workspace/src/pane.rs | 6 +++ crates/workspace/src/pane_group.rs | 74 +++++++++++++++++++++++++++++- crates/workspace/src/workspace.rs | 47 ++++++++++++++++++- 7 files changed, 157 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cbf3d1a173..b790543ee5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -563,6 +563,7 @@ pub struct Editor { inlay_hint_cache: InlayHintCache, next_inlay_id: usize, _subscriptions: Vec, + pixel_position_of_newest_cursor: Option, } 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), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4962b08db2..7a53289232 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -61,6 +61,7 @@ enum FoldMarkers {} struct SelectionLayout { head: DisplayPoint, cursor_shape: CursorShape, + is_newest: bool, range: Range, } @@ -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, @@ -2108,6 +2118,7 @@ impl Element for EditorElement { line_mode, cursor_shape, &snapshot.display_snapshot, + false, )); } selections.extend(remote_selections); @@ -2117,6 +2128,7 @@ impl Element 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; @@ -2139,11 +2151,13 @@ impl Element 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(), diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 0ce41a97c9..7c8fe12aa0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -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 { + self.pixel_position_of_newest_cursor + } + fn breadcrumb_location(&self) -> ToolbarItemLocation { ToolbarItemLocation::PrimaryLeft { flex: None } } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 460698efb8..f0af080d4a 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -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 { + 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>; fn serialized_item_kind(&self) -> Option<&'static str>; fn show_toolbar(&self, cx: &AppContext) -> bool; + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option; } pub trait WeakItemHandle { @@ -615,6 +620,10 @@ impl ItemHandle for ViewHandle { fn show_toolbar(&self, cx: &AppContext) -> bool { self.read(cx).show_toolbar() } + + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.read(cx).pixel_position_of_cursor() + } } impl From> for AnyViewHandle { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index f5b96fd421..2972c307f2 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -542,6 +542,12 @@ impl Pane { self.items.get(self.active_item_index).cloned() } + pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.items + .get(self.active_item_index)? + .pixel_position_of_cursor(cx) + } + pub fn item_for_entry( &self, entry_id: ProjectEntryId, diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 52761b06c8..baa654d967 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -54,6 +54,20 @@ impl PaneGroup { } } + pub fn bounding_box_for_pane(&self, pane: &ViewHandle) -> Option { + 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> { + 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, pub flexes: Rc>>, + pub bounding_boxes: Rc>>>, } impl PaneAxis { pub fn new(axis: Axis, members: Vec) -> 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,40 @@ impl PaneAxis { } } + fn bounding_box_for_pane(&self, pane: &ViewHandle) -> Option { + 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> { + 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, @@ -423,7 +476,12 @@ impl PaneAxis { ) -> AnyElement { 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 +604,21 @@ mod element { active_pane_ix: Option, flexes: Rc>>, children: Vec>, + bounding_boxes: Rc>>>, } impl PaneAxisElement { - pub fn new(axis: Axis, basis: usize, flexes: Rc>>) -> Self { + pub fn new( + axis: Axis, + basis: usize, + flexes: Rc>>, + bounding_boxes: Rc>>>, + ) -> Self { Self { axis, basis, flexes, + bounding_boxes, active_pane_ix: None, children: Default::default(), } @@ -708,11 +773,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()), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3e62af8ea6..e87e8f1855 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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, 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,40 @@ impl Workspace { } } + pub fn activate_pane_in_direction( + &mut self, + direction: SplitDirection, + cx: &mut ViewContext, + ) { + 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) => cursor, + None => bounding_box.center(), + }; + + // currently there's a small gap between panes, so we can't just look "1px to the left" + // instead of trying to calcuate this exactly, we assume it'll always be smaller than + // "pane_gap" pixels (and that no-one uses panes smaller in any dimension than pane_gap). + let pane_gap = 20.; + + let target = match direction { + SplitDirection::Left => vec2f(bounding_box.origin_x() - pane_gap, center.y()), + SplitDirection::Right => vec2f(bounding_box.max_x() + pane_gap, center.y()), + SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - pane_gap), + SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + pane_gap), + }; + + if let Some(pane) = self.center.pane_at_pixel_position(target) { + cx.focus(pane); + } + } + fn handle_pane_focused(&mut self, pane: ViewHandle, cx: &mut ViewContext) { if self.active_pane != pane { self.active_pane = pane.clone(); @@ -3030,6 +3074,7 @@ impl Workspace { axis, members, flexes, + bounding_boxes: _, }) => SerializedPaneGroup::Group { axis: *axis, children: members From 2762f9b1c681a0af0cf3c6e08e8f1a3855352faf Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 19 Jul 2023 18:29:13 -0600 Subject: [PATCH 2/6] vim: Add support for ctrl-w commands Primarily {h,j,k,l,left,right,up,down} for moving to a pane by direction; but also {w,W,p} for going forward/back, and {v,s} for splitting a pane vertically/horizontally, and {c,q} to close a pane. There are a large number of ctrl-w commands that are not supported, and which fall into three buckets: * switch this pane with that one (VScode also has this, and it's a requested feature) * move to top/bottom/leftmost/rightmost * counts on any of these * jump to "definition/file-under-cursor/etc.etc." in a new pane. --- assets/keymaps/vim.json | 70 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index aa1e235414..8fb174c71e 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -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" } }, { From 15dc8b43c476a6da0a44457bf7d8581228851b2a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 19 Jul 2023 18:33:08 -0600 Subject: [PATCH 3/6] Default keybindings for activating pane by direction Breaking change: previously cmd-k cmd-{left,right} moved to the {previous,next} pane; now they will move in the specified direction. --- assets/keymaps/default.json | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1a13d8cdb3..883b0c1872 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -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 From d6a463afb868e5616ee365030d2b516a207f5413 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 20 Jul 2023 11:06:16 -0600 Subject: [PATCH 4/6] Better calculation of pane distance --- crates/workspace/src/workspace.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e87e8f1855..2f3f2f9010 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2081,16 +2081,13 @@ impl Workspace { None => bounding_box.center(), }; - // currently there's a small gap between panes, so we can't just look "1px to the left" - // instead of trying to calcuate this exactly, we assume it'll always be smaller than - // "pane_gap" pixels (and that no-one uses panes smaller in any dimension than pane_gap). - let pane_gap = 20.; + let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.; let target = match direction { - SplitDirection::Left => vec2f(bounding_box.origin_x() - pane_gap, center.y()), - SplitDirection::Right => vec2f(bounding_box.max_x() + pane_gap, center.y()), - SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - pane_gap), - SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + pane_gap), + 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) { From 464cc2e71affec45785a04a123707a4931464a82 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 20 Jul 2023 11:11:37 -0600 Subject: [PATCH 5/6] Assertions for assumptions --- crates/workspace/src/pane_group.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index baa654d967..7b7c845616 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -429,6 +429,8 @@ impl PaneAxis { } fn bounding_box_for_pane(&self, pane: &ViewHandle) -> Option { + debug_assert!(self.members.len() == self.bounding_boxes.borrow().len()); + for (idx, member) in self.members.iter().enumerate() { match member { Member::Pane(found) => { @@ -447,6 +449,8 @@ impl PaneAxis { } fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle> { + 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() { From 0e984e1e69e7125ed38397556f67fcde1e80a415 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 20 Jul 2023 11:11:47 -0600 Subject: [PATCH 6/6] Ignore off-screen cursors --- crates/workspace/src/workspace.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2f3f2f9010..0ebd01e1f7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2077,8 +2077,8 @@ impl Workspace { }; let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx); let center = match cursor { - Some(cursor) => cursor, - None => bounding_box.center(), + Some(cursor) if bounding_box.contains_point(cursor) => cursor, + _ => bounding_box.center(), }; let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.;