Detailed changes
@@ -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),
@@ -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,
@@ -2108,6 +2118,7 @@ impl Element<Editor> for EditorElement {
line_mode,
cursor_shape,
&snapshot.display_snapshot,
+ false,
));
}
selections.extend(remote_selections);
@@ -2117,6 +2128,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;
@@ -2139,11 +2151,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(),
@@ -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 }
}
@@ -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 {
@@ -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,
@@ -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,40 @@ impl PaneAxis {
}
}
+ fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
+ 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>> {
+ 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 +476,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 +604,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 +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()),
@@ -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,40 @@ 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) => 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<Pane>, cx: &mut ViewContext<Self>) {
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