From 69d415c8d01769dd89d3136d4427cbf068eda645 Mon Sep 17 00:00:00 2001 From: 5brian Date: Mon, 10 Feb 2025 10:45:06 -0500 Subject: [PATCH] vim: Multiline operation improvements (#24518) Closes #15711 Discussed changes to match neovim in https://github.com/zed-industries/zed/pull/24481#issuecomment-2644504695 -- `vi{` matches neovim with treesitter instead of vanilla neovim. Change and delete matches standard neovim. Not sure if this is the best way to do it, implemented post processing to change and delete objects. I think another way would be adjust the range to trim the trailing newline char on change and delete operations, instead of having to add it back. ||Before|After| |---|---|---| |initial|![image](https://github.com/user-attachments/assets/0bab37b7-c0ac-4992-a365-b7ec304a6800)|| | `vi{` | ![image](https://github.com/user-attachments/assets/4c802fcd-fa7e-45ba-b7d4-3283ed538e10) | ![image](https://github.com/user-attachments/assets/4394bb6e-418b-4463-9737-f9bdfc6d31c2) | | `ci{` | ![image](https://github.com/user-attachments/assets/b5eabb58-4a93-4c98-80b6-f34a6525b1fb) | ![image](https://github.com/user-attachments/assets/79af57e4-260c-4432-af66-eba5285d97a0) | | `di{` | ![image](https://github.com/user-attachments/assets/190a70e7-71fd-47fe-9d6c-2082f2034d0f) | ![image](https://github.com/user-attachments/assets/775b86a9-68c1-4397-a44b-c645a772de63) | Release Notes: - vim: Improved multi-line operations --- crates/vim/src/object.rs | 150 +++++++++++++++++++++++++-------------- 1 file changed, 97 insertions(+), 53 deletions(-) diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index eb2999cb7266065c7b6be5dbca30ddc5a4ffd948..ed5e3c21bf2fa59bcced32222c6795ec89cad6a5 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -407,6 +407,9 @@ 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 @@ -414,6 +417,49 @@ 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. +fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection) { + 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); + 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. @@ -1333,12 +1379,24 @@ 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 + opening.end = range.end; + + // 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(); + } } } + // 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() { @@ -1687,60 +1745,46 @@ mod test { #[gpui::test] async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; + let mut cx = VimTestContext::new(cx, true).await; - cx.set_shared_state(indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - } - ˇreturn false - }" - }) - .await; - cx.simulate_shared_keystrokes("v i {").await; - cx.shared_state().await.assert_eq(indoc! {" - func empty(a string) bool { - « if a == \"\" { - return true - } - return false - ˇ»}"}); - cx.set_shared_state(indoc! { - "func empty(a string) bool { - if a == \"\" { - ˇreturn true - } - return false - }" - }) - .await; - cx.simulate_shared_keystrokes("v i {").await; - cx.shared_state().await.assert_eq(indoc! {" - func empty(a string) bool { - if a == \"\" { - « return true - ˇ» } - return false - }"}); + cx.set_state( + indoc! { + "func empty(a string) bool { + if a == \"\" { + return true + } + ˇreturn false + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("v i {"); - cx.set_shared_state(indoc! { - "func empty(a string) bool { - if a == \"\" ˇ{ - return true - } - return false - }" - }) - .await; - cx.simulate_shared_keystrokes("v i {").await; - cx.shared_state().await.assert_eq(indoc! {" - func empty(a string) bool { - if a == \"\" { - « return true - ˇ» } - return false - }"}); + cx.set_state( + indoc! { + "func empty(a string) bool { + if a == \"\" { + ˇreturn true + } + return false + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("v i {"); + + cx.set_state( + indoc! { + "func empty(a string) bool { + if a == \"\" ˇ{ + return true + } + return false + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("v i {"); } #[gpui::test]