vim: smartcase find option (#9033)

Rom Grk created

Release Notes:

- Added option `use_smartcase_find` to the vim-mode

Change summary

assets/settings/default.json                 |  3 
crates/vim/src/motion.rs                     | 96 +++++++++++++++++----
crates/vim/src/normal.rs                     | 42 +++++++++
crates/vim/src/vim.rs                        |  4 
docs/src/configuring_zed__configuring_vim.md |  4 
5 files changed, 127 insertions(+), 22 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -595,7 +595,8 @@
   // Vim settings
   "vim": {
     "use_system_clipboard": "always",
-    "use_multiline_find": false
+    "use_multiline_find": false,
+    "use_smartcase_find": false
   },
   // The server to connect to. If the environment variable
   // ZED_SERVER_URL is set, it will override this setting.

crates/vim/src/motion.rs 🔗

@@ -73,11 +73,13 @@ pub enum Motion {
         before: bool,
         char: char,
         mode: FindRange,
+        smartcase: bool,
     },
     FindBackward {
         after: bool,
         char: char,
         mode: FindRange,
+        smartcase: bool,
     },
     RepeatFind {
         last_find: Box<Motion>,
@@ -604,30 +606,54 @@ impl Motion {
             ),
             Matching => (matching(map, point), SelectionGoal::None),
             // t f
-            FindForward { before, char, mode } => {
-                return find_forward(map, point, *before, *char, times, *mode)
+            FindForward {
+                before,
+                char,
+                mode,
+                smartcase,
+            } => {
+                return find_forward(map, point, *before, *char, times, *mode, *smartcase)
                     .map(|new_point| (new_point, SelectionGoal::None))
             }
             // T F
-            FindBackward { after, char, mode } => (
-                find_backward(map, point, *after, *char, times, *mode),
+            FindBackward {
+                after,
+                char,
+                mode,
+                smartcase,
+            } => (
+                find_backward(map, point, *after, *char, times, *mode, *smartcase),
                 SelectionGoal::None,
             ),
             // ; -- repeat the last find done with t, f, T, F
             RepeatFind { last_find } => match **last_find {
-                Motion::FindForward { before, char, mode } => {
-                    let mut new_point = find_forward(map, point, before, char, times, mode);
+                Motion::FindForward {
+                    before,
+                    char,
+                    mode,
+                    smartcase,
+                } => {
+                    let mut new_point =
+                        find_forward(map, point, before, char, times, mode, smartcase);
                     if new_point == Some(point) {
-                        new_point = find_forward(map, point, before, char, times + 1, mode);
+                        new_point =
+                            find_forward(map, point, before, char, times + 1, mode, smartcase);
                     }
 
                     return new_point.map(|new_point| (new_point, SelectionGoal::None));
                 }
 
-                Motion::FindBackward { after, char, mode } => {
-                    let mut new_point = find_backward(map, point, after, char, times, mode);
+                Motion::FindBackward {
+                    after,
+                    char,
+                    mode,
+                    smartcase,
+                } => {
+                    let mut new_point =
+                        find_backward(map, point, after, char, times, mode, smartcase);
                     if new_point == point {
-                        new_point = find_backward(map, point, after, char, times + 1, mode);
+                        new_point =
+                            find_backward(map, point, after, char, times + 1, mode, smartcase);
                     }
 
                     (new_point, SelectionGoal::None)
@@ -636,19 +662,33 @@ impl Motion {
             },
             // , -- repeat the last find done with t, f, T, F, in opposite direction
             RepeatFindReversed { last_find } => match **last_find {
-                Motion::FindForward { before, char, mode } => {
-                    let mut new_point = find_backward(map, point, before, char, times, mode);
+                Motion::FindForward {
+                    before,
+                    char,
+                    mode,
+                    smartcase,
+                } => {
+                    let mut new_point =
+                        find_backward(map, point, before, char, times, mode, smartcase);
                     if new_point == point {
-                        new_point = find_backward(map, point, before, char, times + 1, mode);
+                        new_point =
+                            find_backward(map, point, before, char, times + 1, mode, smartcase);
                     }
 
                     (new_point, SelectionGoal::None)
                 }
 
-                Motion::FindBackward { after, char, mode } => {
-                    let mut new_point = find_forward(map, point, after, char, times, mode);
+                Motion::FindBackward {
+                    after,
+                    char,
+                    mode,
+                    smartcase,
+                } => {
+                    let mut new_point =
+                        find_forward(map, point, after, char, times, mode, smartcase);
                     if new_point == Some(point) {
-                        new_point = find_forward(map, point, after, char, times + 1, mode);
+                        new_point =
+                            find_forward(map, point, after, char, times + 1, mode, smartcase);
                     }
 
                     return new_point.map(|new_point| (new_point, SelectionGoal::None));
@@ -1368,6 +1408,7 @@ fn find_forward(
     target: char,
     times: usize,
     mode: FindRange,
+    smartcase: bool,
 ) -> Option<DisplayPoint> {
     let mut to = from;
     let mut found = false;
@@ -1375,7 +1416,7 @@ fn find_forward(
     for _ in 0..times {
         found = false;
         let new_to = find_boundary(map, to, mode, |_, right| {
-            found = right == target;
+            found = is_character_match(target, right, smartcase);
             found
         });
         if to == new_to {
@@ -1403,19 +1444,22 @@ fn find_backward(
     target: char,
     times: usize,
     mode: FindRange,
+    smartcase: bool,
 ) -> DisplayPoint {
     let mut to = from;
 
     for _ in 0..times {
-        let new_to =
-            find_preceding_boundary_display_point(map, to, mode, |_, right| right == target);
+        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
+            is_character_match(target, right, smartcase)
+        });
         if to == new_to {
             break;
         }
         to = new_to;
     }
 
-    if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
+    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
+    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
         if after {
             *to.column_mut() += 1;
             map.clip_point(to, Bias::Right)
@@ -1427,6 +1471,18 @@ fn find_backward(
     }
 }
 
+fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
+    if smartcase {
+        if target.is_uppercase() {
+            target == other
+        } else {
+            target == other.to_ascii_lowercase()
+        }
+    } else {
+        target == other
+    }
+}
+
 fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
     let correct_line = start_of_relative_buffer_row(map, point, times as isize);
     first_non_whitespace(map, false, correct_line)

crates/vim/src/normal.rs 🔗

@@ -1020,6 +1020,48 @@ mod test {
         );
     }
 
+    #[gpui::test]
+    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<VimSettings>(cx, |s| {
+                s.use_smartcase_find = Some(true);
+            });
+        });
+
+        cx.assert_binding(
+            ["f", "p"],
+            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
+            Mode::Normal,
+            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
+            Mode::Normal,
+        );
+
+        cx.assert_binding(
+            ["shift-f", "p"],
+            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
+            Mode::Normal,
+            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
+            Mode::Normal,
+        );
+
+        cx.assert_binding(
+            ["t", "p"],
+            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
+            Mode::Normal,
+            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
+            Mode::Normal,
+        );
+
+        cx.assert_binding(
+            ["shift-t", "p"],
+            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
+            Mode::Normal,
+            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
+            Mode::Normal,
+        );
+    }
+
     #[gpui::test]
     async fn test_percent(cx: &mut TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);

crates/vim/src/vim.rs 🔗

@@ -487,6 +487,7 @@ impl Vim {
                     } else {
                         FindRange::SingleLine
                     },
+                    smartcase: VimSettings::get_global(cx).use_smartcase_find,
                 };
                 Vim::update(cx, |vim, _| {
                     vim.workspace_state.last_find = Some(find.clone())
@@ -502,6 +503,7 @@ impl Vim {
                     } else {
                         FindRange::SingleLine
                     },
+                    smartcase: VimSettings::get_global(cx).use_smartcase_find,
                 };
                 Vim::update(cx, |vim, _| {
                     vim.workspace_state.last_find = Some(find.clone())
@@ -642,12 +644,14 @@ struct VimSettings {
     // some magic where yy is system and dd is not.
     pub use_system_clipboard: UseSystemClipboard,
     pub use_multiline_find: bool,
+    pub use_smartcase_find: bool,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 struct VimSettingsContent {
     pub use_system_clipboard: Option<UseSystemClipboard>,
     pub use_multiline_find: Option<bool>,
+    pub use_smartcase_find: Option<bool>,
 }
 
 impl Settings for VimSettings {

docs/src/configuring_zed__configuring_vim.md 🔗

@@ -172,7 +172,9 @@ Some vim settings are available to modify the default vim behavior:
     // "on_yank": use system clipboard for yank operations
     "use_system_clipboard": "always",
     // Enable multi-line find for `f` and `t` motions
-    "use_multiline_find": false
+    "use_multiline_find": false,
+    // Enable smartcase find for `f` and `t` motions
+    "use_smartcase_find": false
   }
 }
 ```