@@ -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<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);
+ 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]