vim: Handle exclusive-linewise edgecase correctly (#27786)

Before this change we didn't explicitly handle vim's exclusive-linewise
edgecase
(https://neovim.io/doc/user/motion.html#exclusive).

Instead we had hard-coded workarounds in a few places to make our tests
pass.
The most pernicious of these workarounds was that we represented a
visual line
selection as including the trailing newline (or leading newline for
files that
end with no newline), which other code had to undo to get back to what
the user
indended.

Closes #21440
Updates #6900

Release Notes:

- vim: Fixed `d]}` to not delete the closing brace
- vim: Fixed `d}` from the start of the line to not delete the paragraph
separator
- vim: Fixed `d}` from the middle of the line to not delete the final
newline
This commit is contained in:
Conrad Irwin
2025-03-31 10:36:20 -06:00
committed by GitHub
parent e1e8c1786e
commit fc269dfaf9
27 changed files with 471 additions and 482 deletions
+16 -31
View File
@@ -1,7 +1,7 @@
use std::{ops::Range, time::Duration};
use crate::{
motion::Motion,
motion::{Motion, MotionKind},
object::Object,
state::{Mode, Register},
Vim, VimSettings,
@@ -29,14 +29,16 @@ impl Vim {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
let mut kind = None;
editor.change_selections(None, window, cx, |s| {
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
original_positions.insert(selection.id, original_position);
motion.expand_selection(map, selection, times, true, &text_layout_details);
});
kind = motion.expand_selection(map, selection, times, &text_layout_details);
})
});
vim.yank_selections_content(editor, motion.linewise(), window, cx);
let Some(kind) = kind else { return };
vim.yank_selections_content(editor, kind, window, cx);
editor.change_selections(None, window, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = original_positions.remove(&selection.id).unwrap();
@@ -66,7 +68,7 @@ impl Vim {
start_positions.insert(selection.id, start_position);
});
});
vim.yank_selections_content(editor, false, window, cx);
vim.yank_selections_content(editor, MotionKind::Exclusive, window, cx);
editor.change_selections(None, window, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = start_positions.remove(&selection.id).unwrap();
@@ -81,13 +83,13 @@ impl Vim {
pub fn yank_selections_content(
&mut self,
editor: &mut Editor,
linewise: bool,
kind: MotionKind,
window: &mut Window,
cx: &mut Context<Editor>,
) {
self.copy_ranges(
editor,
linewise,
kind,
true,
editor
.selections
@@ -103,13 +105,13 @@ impl Vim {
pub fn copy_selections_content(
&mut self,
editor: &mut Editor,
linewise: bool,
kind: MotionKind,
window: &mut Window,
cx: &mut Context<Editor>,
) {
self.copy_ranges(
editor,
linewise,
kind,
false,
editor
.selections
@@ -125,7 +127,7 @@ impl Vim {
pub(crate) fn copy_ranges(
&mut self,
editor: &mut Editor,
linewise: bool,
kind: MotionKind,
is_yank: bool,
selections: Vec<Range<Point>>,
window: &mut Window,
@@ -160,7 +162,7 @@ impl Vim {
{
let mut is_first = true;
for selection in selections.iter() {
let mut start = selection.start;
let start = selection.start;
let end = selection.end;
if is_first {
is_first = false;
@@ -169,23 +171,6 @@ impl Vim {
}
let initial_len = text.len();
// if the file does not end with \n, and our line-mode selection ends on
// that line, we will have expanded the start of the selection to ensure it
// contains a newline (so that delete works as expected). We undo that change
// here.
let max_point = buffer.max_point();
let should_adjust_start = linewise
&& end.row == max_point.row
&& max_point.column > 0
&& start.row < max_point.row
&& start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row)));
let should_add_newline =
should_adjust_start || (end == max_point && max_point.column > 0 && linewise);
if should_adjust_start {
start = Point::new(start.row + 1, 0);
}
let start_anchor = buffer.anchor_after(start);
let end_anchor = buffer.anchor_before(end);
ranges_to_highlight.push(start_anchor..end_anchor);
@@ -193,12 +178,12 @@ impl Vim {
for chunk in buffer.text_for_range(start..end) {
text.push_str(chunk);
}
if should_add_newline {
if kind.linewise() {
text.push('\n');
}
clipboard_selections.push(ClipboardSelection {
len: text.len() - initial_len,
is_entire_line: linewise,
is_entire_line: kind.linewise(),
first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
});
}
@@ -213,7 +198,7 @@ impl Vim {
},
selected_register,
is_yank,
linewise,
kind,
cx,
)
});