vim: Update `vi{` (#24601)

5brian created

Small fix: Following up on
https://github.com/zed-industries/zed/pull/24518 where i missed `vi{`.

Matching neovim(tree-sitter), `vi{` should not have the newline selected
(Now `vi{d`/`vi{c` can match `di{`/`ci{`).

Also moved the cursor to the start.

|prev|new|neovim|
|---|---|---|

|![image](https://github.com/user-attachments/assets/0311fbe5-df2e-4feb-977d-de33a3af7fdc)|![image](https://github.com/user-attachments/assets/a940c6ba-268b-4401-8c43-38ca17848542)|![image](https://github.com/user-attachments/assets/dab2c47d-660c-4ae3-bf79-635265222cc1)|

Release Notes:

- N/A

Change summary

crates/vim/src/object.rs                                               | 60 
crates/vim/src/visual.rs                                               |  5 
crates/vim/test_data/test_multiline_surrounding_character_objects.json | 13 
3 files changed, 72 insertions(+), 6 deletions(-)

Detailed changes

crates/vim/src/object.rs 🔗

@@ -422,7 +422,7 @@ impl Object {
 /// 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>) {
+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 {
@@ -446,6 +446,7 @@ fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection<Di
                         match ch {
                             '\n' => {
                                 selection.end = offset.to_display_point(map);
+                                selection.reversed = true;
                                 break;
                             }
                             ch if !ch.is_whitespace() => break,
@@ -1759,6 +1760,17 @@ mod test {
             Mode::Normal,
         );
         cx.simulate_keystrokes("v i {");
+        cx.assert_state(
+            indoc! {
+                "func empty(a string) bool {
+                   «ˇif a == \"\" {
+                      return true
+                   }
+                   return false»
+                }"
+            },
+            Mode::Visual,
+        );
 
         cx.set_state(
             indoc! {
@@ -1772,6 +1784,17 @@ mod test {
             Mode::Normal,
         );
         cx.simulate_keystrokes("v i {");
+        cx.assert_state(
+            indoc! {
+                "func empty(a string) bool {
+                     if a == \"\" {
+                         «ˇreturn true»
+                     }
+                     return false
+                }"
+            },
+            Mode::Visual,
+        );
 
         cx.set_state(
             indoc! {
@@ -1785,6 +1808,41 @@ mod test {
             Mode::Normal,
         );
         cx.simulate_keystrokes("v i {");
+        cx.assert_state(
+            indoc! {
+                "func empty(a string) bool {
+                     if a == \"\" {
+                         «ˇreturn true»
+                     }
+                     return false
+                }"
+            },
+            Mode::Visual,
+        );
+
+        cx.set_state(
+            indoc! {
+                "func empty(a string) bool {
+                     if a == \"\" {
+                         return true
+                     }
+                     return false
+                ˇ}"
+            },
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("v i {");
+        cx.assert_state(
+            indoc! {
+                "func empty(a string) bool {
+                     «ˇif a == \"\" {
+                         return true
+                     }
+                     return false»
+                }"
+            },
+            Mode::Visual,
+        );
     }
 
     #[gpui::test]

crates/vim/src/visual.rs 🔗

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

crates/vim/test_data/test_multiline_surrounding_character_objects.json 🔗

@@ -1,15 +1,20 @@
-{"Put":{"state":"func empty(a string) bool {\n   if a == \"\" {\n      return true\n   }\n   ˇreturn false\n}"}}
+{"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"}}
+{"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"}}
+{"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"}}
+{"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"}}