vim: Refactor and fix multiline operations (#25055)

Changes:
- [x] Cursor at the start during yank operations on objects (`yip`,
`yab` etc).
- [x] Refactors this: Trim all leading and trailing whitespace from
inner multiline bracket selection.
  - This leaves a nicely indented line when doing `ci{` `vi{d` etc
  - [x] Checks for empty selection
  - [x] Removed moving cursor to the start in visual bracket operations

This cleans up the previous implementation by providing a simpler check
in `surrounding_markers`, instead of calling a new function in
`expand_object`. No functionality was changed there except for handling
the empty selection and removing some cursor adjustments that should not
have been there after further testing.

Release Notes:

- N/A
This commit is contained in:
5brian 2025-02-24 20:30:21 -05:00 committed by GitHub
parent 980e1b533f
commit 52f73e0c2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 56 additions and 109 deletions

View file

@ -58,18 +58,18 @@ impl Vim {
self.update_editor(window, cx, |vim, editor, window, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
let mut start_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, window, cx, |s| {
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
object.expand_selection(map, selection, around);
original_positions.insert(selection.id, original_position);
let start_position = (selection.start, selection.goal);
start_positions.insert(selection.id, start_position);
});
});
vim.yank_selections_content(editor, false, cx);
editor.change_selections(None, window, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = original_positions.remove(&selection.id).unwrap();
let (head, goal) = start_positions.remove(&selection.id).unwrap();
selection.collapse_to(head, goal);
});
});

View file

@ -560,9 +560,6 @@ impl Object {
if let Some(range) = self.range(map, selection.clone(), around) {
selection.start = range.start;
selection.end = range.end;
if !around && self.is_multiline() {
preserve_indented_newline(map, selection);
}
true
} else {
false
@ -570,50 +567,6 @@ impl Object {
}
}
/// Returns a range without the final newline char.
///
/// If the selection spans multiple lines and is preceded by an opening brace (`{`),
/// this function will trim the selection to exclude the final newline
/// in order to preserve a properly indented line.
pub fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
let (start_point, end_point) = (selection.start.to_point(map), selection.end.to_point(map));
if start_point.row == end_point.row {
return;
}
let start_offset = selection.start.to_offset(map, Bias::Left);
let mut pos = start_offset;
while pos > 0 {
pos -= 1;
let current_char = map.buffer_chars_at(pos).next().map(|(ch, _)| ch);
match current_char {
Some(ch) if !ch.is_whitespace() => break,
Some('\n') if pos > 0 => {
let prev_char = map.buffer_chars_at(pos - 1).next().map(|(ch, _)| ch);
if prev_char == Some('{') {
let end_pos = selection.end.to_offset(map, Bias::Left);
for (ch, offset) in map.reverse_buffer_chars_at(end_pos) {
match ch {
'\n' => {
selection.end = offset.to_display_point(map);
selection.reversed = true;
break;
}
ch if !ch.is_whitespace() => break,
_ => continue,
}
}
}
break;
}
_ => continue,
}
}
}
/// Returns a range that surrounds the word `relative_to` is in.
///
/// If `relative_to` is at the start of a word, return the word.
@ -1518,38 +1471,37 @@ fn surrounding_markers(
}
}
if !around && search_across_lines {
// Handle trailing newline after opening
if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
if ch == '\n' {
opening.end = range.end;
// Adjust selection to remove leading and trailing whitespace for multiline inner brackets
if !around && open_marker != close_marker {
let start_point = opening.end.to_display_point(map);
let end_point = closing.start.to_display_point(map);
let start_offset = start_point.to_offset(map, Bias::Left);
let end_offset = end_point.to_offset(map, Bias::Left);
// After newline, skip leading whitespace
let mut chars = movement::chars_after(map, opening.end).peekable();
while let Some((ch, range)) = chars.peek() {
if !ch.is_whitespace() {
break;
}
opening.end = range.end;
chars.next();
if start_point.row() != end_point.row()
&& map
.buffer_chars_at(start_offset)
.take_while(|(_, offset)| offset < &end_offset)
.any(|(ch, _)| !ch.is_whitespace())
{
let mut first_non_ws = None;
let mut last_non_ws = None;
for (ch, offset) in map.buffer_chars_at(start_offset) {
if !ch.is_whitespace() {
first_non_ws = Some(offset);
break;
}
}
}
// Handle leading whitespace before closing
let mut last_newline_end = None;
for (ch, range) in movement::chars_before(map, closing.start) {
if !ch.is_whitespace() {
break;
for (ch, offset) in map.reverse_buffer_chars_at(end_offset) {
if !ch.is_whitespace() {
last_non_ws = Some(offset + ch.len_utf8());
break;
}
}
if ch == '\n' {
last_newline_end = Some(range.end);
break;
if let Some(start) = first_non_ws {
opening.end = start;
}
}
// Adjust closing.start to exclude whitespace after a newline, if present
if let Some(end) = last_newline_end {
if end > opening.end {
if let Some(end) = last_non_ws {
closing.start = end;
}
}
@ -1904,10 +1856,10 @@ mod test {
cx.assert_state(
indoc! {
"func empty(a string) bool {
«ˇif a == \"\" {
«if a == \"\" {
return true
}
return false»
return falseˇ»
}"
},
Mode::Visual,
@ -1929,7 +1881,7 @@ mod test {
indoc! {
"func empty(a string) bool {
if a == \"\" {
«ˇreturn true»
«return trueˇ»
}
return false
}"
@ -1953,7 +1905,7 @@ mod test {
indoc! {
"func empty(a string) bool {
if a == \"\" {
«ˇreturn true»
«return trueˇ»
}
return false
}"
@ -1976,14 +1928,33 @@ mod test {
cx.assert_state(
indoc! {
"func empty(a string) bool {
«ˇif a == \"\" {
«if a == \"\" {
return true
}
return false»
return falseˇ»
}"
},
Mode::Visual,
);
cx.set_state(
indoc! {
"func empty(a string) bool {
if a == \"\" {
ˇ
}"
},
Mode::Normal,
);
cx.simulate_keystrokes("c i {");
cx.assert_state(
indoc! {
"func empty(a string) bool {
if a == \"\" {ˇ}"
},
Mode::Insert,
);
}
#[gpui::test]
@ -2603,7 +2574,6 @@ mod test {
#[gpui::test]
async fn test_anybrackets_trailing_space(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("(trailingˇ whitespace )")
.await;
cx.simulate_shared_keystrokes("v i b").await;

View file

@ -16,7 +16,7 @@ use workspace::searchable::Direction;
use crate::{
motion::{first_non_whitespace, next_line_end, start_of_line, Motion},
object::{self, Object},
object::Object,
state::{Mode, Operator},
Vim,
};
@ -375,9 +375,6 @@ impl Vim {
} else {
selection.end = range.end;
}
if !around && object.is_multiline() {
object::preserve_indented_newline(map, selection);
}
}
// In the visual selection result of a paragraph object, the cursor is

View file

@ -1,20 +0,0 @@
{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n return true\n }\n ˇreturn false\n}"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"{"}
{"Get":{"state":"func empty(a string) bool {\n «ˇif a == \"\" {\n return true\n }\n return false»\n}","mode":"Visual"}}
{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n ˇreturn true\n }\n return false\n}"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"{"}
{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n «ˇreturn true»\n }\n return false\n}","mode":"Visual"}}
{"Put":{"state":"func empty(a string) bool {\n if a == \"\" ˇ{\n return true\n }\n return false\n}"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"{"}
{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n «ˇreturn true»\n }\n return false\n}","mode":"Visual"}}
{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n return true\n }\n return false\nˇ}"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"{"}
{"Get":{"state":"func empty(a string) bool {\n «ˇif a == \"\" {\n return true\n }\n return false»\n}","mode":"Visual"}}