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| { self.update_editor(window, cx, |vim, editor, window, cx| {
editor.transact(window, cx, |editor, window, cx| { editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, 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| { editor.change_selections(None, window, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
object.expand_selection(map, selection, around); 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); vim.yank_selections_content(editor, false, cx);
editor.change_selections(None, window, cx, |s| { editor.change_selections(None, window, cx, |s| {
s.move_with(|_, selection| { 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); selection.collapse_to(head, goal);
}); });
}); });

View file

@ -560,9 +560,6 @@ impl Object {
if let Some(range) = self.range(map, selection.clone(), around) { if let Some(range) = self.range(map, selection.clone(), around) {
selection.start = range.start; selection.start = range.start;
selection.end = range.end; selection.end = range.end;
if !around && self.is_multiline() {
preserve_indented_newline(map, selection);
}
true true
} else { } else {
false 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. /// Returns a range that surrounds the word `relative_to` is in.
/// ///
/// If `relative_to` is at the start of a word, return the word. /// 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 { // Adjust selection to remove leading and trailing whitespace for multiline inner brackets
// Handle trailing newline after opening if !around && open_marker != close_marker {
if let Some((ch, range)) = movement::chars_after(map, opening.end).next() { let start_point = opening.end.to_display_point(map);
if ch == '\n' { let end_point = closing.start.to_display_point(map);
opening.end = range.end; 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 if start_point.row() != end_point.row()
let mut chars = movement::chars_after(map, opening.end).peekable(); && map
while let Some((ch, range)) = chars.peek() { .buffer_chars_at(start_offset)
if !ch.is_whitespace() { .take_while(|(_, offset)| offset < &end_offset)
break; .any(|(ch, _)| !ch.is_whitespace())
} {
opening.end = range.end; let mut first_non_ws = None;
chars.next(); 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;
} }
} }
} for (ch, offset) in map.reverse_buffer_chars_at(end_offset) {
if !ch.is_whitespace() {
// Handle leading whitespace before closing last_non_ws = Some(offset + ch.len_utf8());
let mut last_newline_end = None; break;
for (ch, range) in movement::chars_before(map, closing.start) { }
if !ch.is_whitespace() {
break;
} }
if ch == '\n' { if let Some(start) = first_non_ws {
last_newline_end = Some(range.end); opening.end = start;
break;
} }
} if let Some(end) = last_non_ws {
// Adjust closing.start to exclude whitespace after a newline, if present
if let Some(end) = last_newline_end {
if end > opening.end {
closing.start = end; closing.start = end;
} }
} }
@ -1904,10 +1856,10 @@ mod test {
cx.assert_state( cx.assert_state(
indoc! { indoc! {
"func empty(a string) bool { "func empty(a string) bool {
«ˇif a == \"\" { «if a == \"\" {
return true return true
} }
return false» return falseˇ»
}" }"
}, },
Mode::Visual, Mode::Visual,
@ -1929,7 +1881,7 @@ mod test {
indoc! { indoc! {
"func empty(a string) bool { "func empty(a string) bool {
if a == \"\" { if a == \"\" {
«ˇreturn true» «return trueˇ»
} }
return false return false
}" }"
@ -1953,7 +1905,7 @@ mod test {
indoc! { indoc! {
"func empty(a string) bool { "func empty(a string) bool {
if a == \"\" { if a == \"\" {
«ˇreturn true» «return trueˇ»
} }
return false return false
}" }"
@ -1976,14 +1928,33 @@ mod test {
cx.assert_state( cx.assert_state(
indoc! { indoc! {
"func empty(a string) bool { "func empty(a string) bool {
«ˇif a == \"\" { «if a == \"\" {
return true return true
} }
return false» return falseˇ»
}" }"
}, },
Mode::Visual, 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] #[gpui::test]
@ -2603,7 +2574,6 @@ mod test {
#[gpui::test] #[gpui::test]
async fn test_anybrackets_trailing_space(cx: &mut gpui::TestAppContext) { async fn test_anybrackets_trailing_space(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await; let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("(trailingˇ whitespace )") cx.set_shared_state("(trailingˇ whitespace )")
.await; .await;
cx.simulate_shared_keystrokes("v i b").await; cx.simulate_shared_keystrokes("v i b").await;

View file

@ -16,7 +16,7 @@ use workspace::searchable::Direction;
use crate::{ use crate::{
motion::{first_non_whitespace, next_line_end, start_of_line, Motion}, motion::{first_non_whitespace, next_line_end, start_of_line, Motion},
object::{self, Object}, object::Object,
state::{Mode, Operator}, state::{Mode, Operator},
Vim, Vim,
}; };
@ -375,9 +375,6 @@ impl Vim {
} else { } else {
selection.end = range.end; 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 // 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"}}