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 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" } }, { 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 59a2b222d8..1189503c58 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, @@ -2109,6 +2119,7 @@ impl Element for EditorElement { line_mode, cursor_shape, &snapshot.display_snapshot, + false, )); } selections.extend(remote_selections); @@ -2118,6 +2129,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; @@ -2140,11 +2152,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..7b7c845616 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,44 @@ 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) => { + 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> { + 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, @@ -423,7 +480,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 +608,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 +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()), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3e62af8ea6..0ebd01e1f7 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,37 @@ 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) 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, cx: &mut ViewContext) { 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