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]