Fix vim selection to include entire range (#2787)
Update vim mode to have vim selection and editor selections match. Before this we had to adjust between vim selections and real selections when making changes; now we have to adjust when making selections. Release Notes: - vim: Ensure editor selection matches the vim selection ([#1796](https://github.com/zed-industries/community/issues/1796)). - vim: Fix `s` in visual line mode - vim: Add `o` and `shift-o` to toggle direction of visual selection - vim: Fix `v` and `shift-v` to toggle back to normal mode - vim: Fix block selections like `vi}` to contain correct whitespace
This commit is contained in:
commit
404b1aa65a
25 changed files with 1007 additions and 454 deletions
|
@ -353,19 +353,26 @@ impl DisplaySnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
// used by line_mode selections and tries to match vim behaviour
|
||||
pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
|
||||
let mut new_start = self.prev_line_boundary(range.start).0;
|
||||
let mut new_end = self.next_line_boundary(range.end).0;
|
||||
let new_start = if range.start.row == 0 {
|
||||
Point::new(0, 0)
|
||||
} else if range.start.row == self.max_buffer_row()
|
||||
|| (range.end.column > 0 && range.end.row == self.max_buffer_row())
|
||||
{
|
||||
Point::new(range.start.row - 1, self.line_len(range.start.row - 1))
|
||||
} else {
|
||||
self.prev_line_boundary(range.start).0
|
||||
};
|
||||
|
||||
if new_start.row == range.start.row && new_end.row == range.end.row {
|
||||
if new_end.row < self.buffer_snapshot.max_point().row {
|
||||
new_end.row += 1;
|
||||
new_end.column = 0;
|
||||
} else if new_start.row > 0 {
|
||||
new_start.row -= 1;
|
||||
new_start.column = self.buffer_snapshot.line_len(new_start.row);
|
||||
}
|
||||
}
|
||||
let new_end = if range.end.column == 0 {
|
||||
range.end
|
||||
} else if range.end.row < self.max_buffer_row() {
|
||||
self.buffer_snapshot
|
||||
.clip_point(Point::new(range.end.row + 1, 0), Bias::Left)
|
||||
} else {
|
||||
self.buffer_snapshot.max_point()
|
||||
};
|
||||
|
||||
new_start..new_end
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ struct SelectionLayout {
|
|||
cursor_shape: CursorShape,
|
||||
is_newest: bool,
|
||||
range: Range<DisplayPoint>,
|
||||
active_rows: Range<u32>,
|
||||
}
|
||||
|
||||
impl SelectionLayout {
|
||||
|
@ -73,25 +74,44 @@ impl SelectionLayout {
|
|||
map: &DisplaySnapshot,
|
||||
is_newest: bool,
|
||||
) -> Self {
|
||||
let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
|
||||
let display_selection = point_selection.map(|p| p.to_display_point(map));
|
||||
let mut range = display_selection.range();
|
||||
let mut head = display_selection.head();
|
||||
let mut active_rows = map.prev_line_boundary(point_selection.start).1.row()
|
||||
..map.next_line_boundary(point_selection.end).1.row();
|
||||
|
||||
// vim visual line mode
|
||||
if line_mode {
|
||||
let selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
|
||||
let point_range = map.expand_to_line(selection.range());
|
||||
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),
|
||||
}
|
||||
} else {
|
||||
let selection = selection.map(|p| p.to_display_point(map));
|
||||
Self {
|
||||
head: selection.head(),
|
||||
cursor_shape,
|
||||
is_newest,
|
||||
range: selection.range(),
|
||||
let point_range = map.expand_to_line(point_selection.range());
|
||||
range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map);
|
||||
}
|
||||
|
||||
// any vim visual mode (including line mode)
|
||||
if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed {
|
||||
if head.column() > 0 {
|
||||
head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left)
|
||||
} else if head.row() > 0 && head != map.max_point() {
|
||||
head = map.clip_point(
|
||||
DisplayPoint::new(head.row() - 1, map.line_len(head.row() - 1)),
|
||||
Bias::Left,
|
||||
);
|
||||
// updating range.end is a no-op unless you're cursor is
|
||||
// on the newline containing a multi-buffer divider
|
||||
// in which case the clip_point may have moved the head up
|
||||
// an additional row.
|
||||
range.end = DisplayPoint::new(head.row() + 1, 0);
|
||||
active_rows.end = head.row();
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
head,
|
||||
cursor_shape,
|
||||
is_newest,
|
||||
range,
|
||||
active_rows,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2152,22 +2172,37 @@ impl Element<Editor> for EditorElement {
|
|||
}
|
||||
selections.extend(remote_selections);
|
||||
|
||||
let mut newest_selection_head = None;
|
||||
|
||||
if editor.show_local_selections {
|
||||
let mut local_selections = editor
|
||||
let mut local_selections: Vec<Selection<Point>> = editor
|
||||
.selections
|
||||
.disjoint_in_range(start_anchor..end_anchor, cx);
|
||||
local_selections.extend(editor.selections.pending(cx));
|
||||
let mut layouts = Vec::new();
|
||||
let newest = editor.selections.newest(cx);
|
||||
for selection in &local_selections {
|
||||
for selection in local_selections.drain(..) {
|
||||
let is_empty = selection.start == selection.end;
|
||||
let selection_start = snapshot.prev_line_boundary(selection.start).1;
|
||||
let selection_end = snapshot.next_line_boundary(selection.end).1;
|
||||
for row in cmp::max(selection_start.row(), start_row)
|
||||
..=cmp::min(selection_end.row(), end_row)
|
||||
let is_newest = selection == newest;
|
||||
|
||||
let layout = SelectionLayout::new(
|
||||
selection,
|
||||
editor.selections.line_mode,
|
||||
editor.cursor_shape,
|
||||
&snapshot.display_snapshot,
|
||||
is_newest,
|
||||
);
|
||||
if is_newest {
|
||||
newest_selection_head = Some(layout.head);
|
||||
}
|
||||
|
||||
for row in cmp::max(layout.active_rows.start, start_row)
|
||||
..=cmp::min(layout.active_rows.end, end_row)
|
||||
{
|
||||
let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
|
||||
*contains_non_empty_selection |= !is_empty;
|
||||
}
|
||||
layouts.push(layout);
|
||||
}
|
||||
|
||||
// Render the local selections in the leader's color when following.
|
||||
|
@ -2175,22 +2210,7 @@ impl Element<Editor> for EditorElement {
|
|||
.leader_replica_id
|
||||
.unwrap_or_else(|| editor.replica_id(cx));
|
||||
|
||||
selections.push((
|
||||
local_replica_id,
|
||||
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(),
|
||||
));
|
||||
selections.push((local_replica_id, layouts));
|
||||
}
|
||||
|
||||
let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
|
||||
|
@ -2295,28 +2315,26 @@ impl Element<Editor> for EditorElement {
|
|||
snapshot = editor.snapshot(cx);
|
||||
}
|
||||
|
||||
let newest_selection_head = editor
|
||||
.selections
|
||||
.newest::<usize>(cx)
|
||||
.head()
|
||||
.to_display_point(&snapshot);
|
||||
let style = editor.style(cx);
|
||||
|
||||
let mut context_menu = None;
|
||||
let mut code_actions_indicator = None;
|
||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||
if editor.context_menu_visible() {
|
||||
context_menu = editor.render_context_menu(newest_selection_head, style.clone(), cx);
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||
if editor.context_menu_visible() {
|
||||
context_menu =
|
||||
editor.render_context_menu(newest_selection_head, style.clone(), cx);
|
||||
}
|
||||
|
||||
let active = matches!(
|
||||
editor.context_menu,
|
||||
Some(crate::ContextMenu::CodeActions(_))
|
||||
);
|
||||
|
||||
code_actions_indicator = editor
|
||||
.render_code_actions_indicator(&style, active, cx)
|
||||
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||
}
|
||||
|
||||
let active = matches!(
|
||||
editor.context_menu,
|
||||
Some(crate::ContextMenu::CodeActions(_))
|
||||
);
|
||||
|
||||
code_actions_indicator = editor
|
||||
.render_code_actions_indicator(&style, active, cx)
|
||||
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||
}
|
||||
|
||||
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
||||
|
@ -2995,6 +3013,154 @@ mod tests {
|
|||
assert_eq!(layouts.len(), 6);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_vim_visual_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let editor = cx
|
||||
.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
||||
})
|
||||
.root(cx);
|
||||
let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
|
||||
let (_, state) = editor.update(cx, |editor, cx| {
|
||||
editor.cursor_shape = CursorShape::Block;
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([
|
||||
Point::new(0, 0)..Point::new(1, 0),
|
||||
Point::new(3, 2)..Point::new(3, 3),
|
||||
Point::new(5, 6)..Point::new(6, 0),
|
||||
]);
|
||||
});
|
||||
let mut new_parents = Default::default();
|
||||
let mut notify_views_if_parents_change = Default::default();
|
||||
let mut layout_cx = LayoutContext::new(
|
||||
cx,
|
||||
&mut new_parents,
|
||||
&mut notify_views_if_parents_change,
|
||||
false,
|
||||
);
|
||||
element.layout(
|
||||
SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
|
||||
editor,
|
||||
&mut layout_cx,
|
||||
)
|
||||
});
|
||||
assert_eq!(state.selections.len(), 1);
|
||||
let local_selections = &state.selections[0].1;
|
||||
assert_eq!(local_selections.len(), 3);
|
||||
// moves cursor back one line
|
||||
assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6));
|
||||
assert_eq!(
|
||||
local_selections[0].range,
|
||||
DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0)
|
||||
);
|
||||
|
||||
// moves cursor back one column
|
||||
assert_eq!(
|
||||
local_selections[1].range,
|
||||
DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3)
|
||||
);
|
||||
assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2));
|
||||
|
||||
// leaves cursor on the max point
|
||||
assert_eq!(
|
||||
local_selections[2].range,
|
||||
DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0)
|
||||
);
|
||||
assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0));
|
||||
|
||||
// active lines does not include 1 (even though the range of the selection does)
|
||||
assert_eq!(
|
||||
state.active_rows.keys().cloned().collect::<Vec<u32>>(),
|
||||
vec![0, 3, 5, 6]
|
||||
);
|
||||
|
||||
// multi-buffer support
|
||||
// in DisplayPoint co-ordinates, this is what we're dealing with:
|
||||
// 0: [[file
|
||||
// 1: header]]
|
||||
// 2: aaaaaa
|
||||
// 3: bbbbbb
|
||||
// 4: cccccc
|
||||
// 5:
|
||||
// 6: ...
|
||||
// 7: ffffff
|
||||
// 8: gggggg
|
||||
// 9: hhhhhh
|
||||
// 10:
|
||||
// 11: [[file
|
||||
// 12: header]]
|
||||
// 13: bbbbbb
|
||||
// 14: cccccc
|
||||
// 15: dddddd
|
||||
let editor = cx
|
||||
.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_multi(
|
||||
[
|
||||
(
|
||||
&(sample_text(8, 6, 'a') + "\n"),
|
||||
vec![
|
||||
Point::new(0, 0)..Point::new(3, 0),
|
||||
Point::new(4, 0)..Point::new(7, 0),
|
||||
],
|
||||
),
|
||||
(
|
||||
&(sample_text(8, 6, 'a') + "\n"),
|
||||
vec![Point::new(1, 0)..Point::new(3, 0)],
|
||||
),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
||||
})
|
||||
.root(cx);
|
||||
let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
|
||||
let (_, state) = editor.update(cx, |editor, cx| {
|
||||
editor.cursor_shape = CursorShape::Block;
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0),
|
||||
DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0),
|
||||
]);
|
||||
});
|
||||
let mut new_parents = Default::default();
|
||||
let mut notify_views_if_parents_change = Default::default();
|
||||
let mut layout_cx = LayoutContext::new(
|
||||
cx,
|
||||
&mut new_parents,
|
||||
&mut notify_views_if_parents_change,
|
||||
false,
|
||||
);
|
||||
element.layout(
|
||||
SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
|
||||
editor,
|
||||
&mut layout_cx,
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(state.selections.len(), 1);
|
||||
let local_selections = &state.selections[0].1;
|
||||
assert_eq!(local_selections.len(), 2);
|
||||
|
||||
// moves cursor on excerpt boundary back a line
|
||||
// and doesn't allow selection to bleed through
|
||||
assert_eq!(
|
||||
local_selections[0].range,
|
||||
DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0)
|
||||
);
|
||||
assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0));
|
||||
|
||||
// moves cursor on buffer boundary back two lines
|
||||
// and doesn't allow selection to bleed through
|
||||
assert_eq!(
|
||||
local_selections[1].range,
|
||||
DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0)
|
||||
);
|
||||
assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
|
|
@ -13,6 +13,13 @@ pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
|||
map.clip_point(point, Bias::Left)
|
||||
}
|
||||
|
||||
pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
if point.column() > 0 {
|
||||
*point.column_mut() -= 1;
|
||||
}
|
||||
map.clip_point(point, Bias::Left)
|
||||
}
|
||||
|
||||
pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
let max_column = map.line_len(point.row());
|
||||
if point.column() < max_column {
|
||||
|
@ -24,6 +31,11 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
|||
map.clip_point(point, Bias::Right)
|
||||
}
|
||||
|
||||
pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
*point.column_mut() += 1;
|
||||
map.clip_point(point, Bias::Right)
|
||||
}
|
||||
|
||||
pub fn up(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
|
|
|
@ -1565,6 +1565,25 @@ impl MultiBuffer {
|
|||
cx.add_model(|cx| Self::singleton(buffer, cx))
|
||||
}
|
||||
|
||||
pub fn build_multi<const COUNT: usize>(
|
||||
excerpts: [(&str, Vec<Range<Point>>); COUNT],
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> ModelHandle<Self> {
|
||||
let multi = cx.add_model(|_| Self::new(0));
|
||||
for (text, ranges) in excerpts {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
|
||||
let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange {
|
||||
context: range,
|
||||
primary: None,
|
||||
});
|
||||
multi.update(cx, |multi, cx| {
|
||||
multi.push_excerpts(buffer, excerpt_ranges, cx)
|
||||
});
|
||||
}
|
||||
|
||||
multi
|
||||
}
|
||||
|
||||
pub fn build_from_buffer(
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: &mut gpui::AppContext,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue