vim: Add editor setting for changing regex mode default in vim searches (#53092)

Sean Hagstrom created

Closes #48007 

Release Notes:

- Added editor setting for changing regex mode default in vim searches

Summary:

- Based on the report in #48007 and the discussion here
https://github.com/zed-industries/zed/pull/48127#issuecomment-3838678903
- There was feedback mentioning that vim-mode needs to default
vim-searches to use regex-mode (even when the editor regex-search
setting is disabled). However, it was suggested that a vim search
setting could be configured to adjust this behaviour.
- In this PR a new vim setting was added to change whether vim-searches
will use regex-mode by default, so now users can can configure
vim-search to not use regex-mode when typing the `/` character (or using
the vim search command).

Screen Captures:


https://github.com/user-attachments/assets/172669fb-ab78-41a1-9485-c973825543c5

Change summary

assets/settings/default.json                    |  1 
crates/settings_content/src/settings_content.rs |  1 
crates/settings_ui/src/page_data.rs             | 20 +++++
crates/vim/src/normal/search.rs                 | 64 ++++++++++++++++++
crates/vim/src/vim.rs                           |  2 
docs/src/vim.md                                 |  2 
6 files changed, 88 insertions(+), 2 deletions(-)

Detailed changes

assets/settings/default.json πŸ”—

@@ -2417,6 +2417,7 @@
     "toggle_relative_line_numbers": false,
     "use_system_clipboard": "always",
     "use_smartcase_find": false,
+    "use_regex_search": true,
     "gdefault": false,
     "highlight_on_yank_duration": 200,
     "custom_digraphs": {},

crates/settings_content/src/settings_content.rs πŸ”—

@@ -763,6 +763,7 @@ pub struct VimSettingsContent {
     pub toggle_relative_line_numbers: Option<bool>,
     pub use_system_clipboard: Option<UseSystemClipboard>,
     pub use_smartcase_find: Option<bool>,
+    pub use_regex_search: Option<bool>,
     /// When enabled, the `:substitute` command replaces all matches in a line
     /// by default. The 'g' flag then toggles this behavior.,
     pub gdefault: Option<bool>,

crates/settings_ui/src/page_data.rs πŸ”—

@@ -2447,7 +2447,7 @@ fn editor_page() -> SettingsPage {
         ]
     }
 
-    fn vim_settings_section() -> [SettingsPageItem; 12] {
+    fn vim_settings_section() -> [SettingsPageItem; 13] {
         [
             SettingsPageItem::SectionHeader("Vim"),
             SettingsPageItem::SettingItem(SettingItem {
@@ -2556,6 +2556,24 @@ fn editor_page() -> SettingsPage {
                 metadata: None,
                 files: USER,
             }),
+            SettingsPageItem::SettingItem(SettingItem {
+                title: "Regex Search",
+                description: "Use regex search by default in Vim search.",
+                field: Box::new(SettingField {
+                    json_path: Some("vim.use_regex_search"),
+                    pick: |settings_content| {
+                        settings_content.vim.as_ref()?.use_regex_search.as_ref()
+                    },
+                    write: |settings_content, value| {
+                        settings_content
+                            .vim
+                            .get_or_insert_default()
+                            .use_regex_search = value;
+                    },
+                }),
+                metadata: None,
+                files: USER,
+            }),
             SettingsPageItem::SettingItem(SettingItem {
                 title: "Cursor Shape - Normal Mode",
                 description: "Cursor shape for normal mode.",

crates/vim/src/normal/search.rs πŸ”—

@@ -245,7 +245,7 @@ impl Vim {
 
             search_bar.set_replacement(None, cx);
             let mut options = SearchOptions::NONE;
-            if action.regex {
+            if action.regex && VimSettings::get_global(cx).use_regex_search {
                 options |= SearchOptions::REGEX;
             }
             if action.backwards {
@@ -1446,4 +1446,66 @@ mod test {
         // The cursor should be at the match location on line 3 (row 2).
         cx.assert_state("hello world\nfoo bar\nhello Λ‡again\n", Mode::Normal);
     }
+
+    #[gpui::test]
+    async fn test_vim_search_respects_search_settings(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings(cx, |settings| {
+                settings.vim.get_or_insert_default().use_regex_search = Some(false);
+            });
+        });
+
+        cx.set_state("Λ‡content", Mode::Normal);
+        cx.simulate_keystrokes("/");
+        cx.run_until_parked();
+
+        // Verify search options are set from settings
+        let search_bar = cx.workspace(|workspace, _, cx| {
+            workspace
+                .active_pane()
+                .read(cx)
+                .toolbar()
+                .read(cx)
+                .item_of_type::<BufferSearchBar>()
+                .expect("Buffer search bar should be active")
+        });
+
+        cx.update_entity(search_bar, |bar, _window, _cx| {
+            assert!(
+                !bar.has_search_option(search::SearchOptions::REGEX),
+                "Vim search open without regex mode"
+            );
+        });
+
+        cx.simulate_keystrokes("escape");
+        cx.run_until_parked();
+
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings(cx, |settings| {
+                settings.vim.get_or_insert_default().use_regex_search = Some(true);
+            });
+        });
+
+        cx.simulate_keystrokes("/");
+        cx.run_until_parked();
+
+        let search_bar = cx.workspace(|workspace, _, cx| {
+            workspace
+                .active_pane()
+                .read(cx)
+                .toolbar()
+                .read(cx)
+                .item_of_type::<BufferSearchBar>()
+                .expect("Buffer search bar should be active")
+        });
+
+        cx.update_entity(search_bar, |bar, _window, _cx| {
+            assert!(
+                bar.has_search_option(search::SearchOptions::REGEX),
+                "Vim search opens with regex mode"
+            );
+        });
+    }
 }

crates/vim/src/vim.rs πŸ”—

@@ -2141,6 +2141,7 @@ struct VimSettings {
     pub toggle_relative_line_numbers: bool,
     pub use_system_clipboard: settings::UseSystemClipboard,
     pub use_smartcase_find: bool,
+    pub use_regex_search: bool,
     pub gdefault: bool,
     pub custom_digraphs: HashMap<String, Arc<str>>,
     pub highlight_on_yank_duration: u64,
@@ -2227,6 +2228,7 @@ impl Settings for VimSettings {
             toggle_relative_line_numbers: vim.toggle_relative_line_numbers.unwrap(),
             use_system_clipboard: vim.use_system_clipboard.unwrap(),
             use_smartcase_find: vim.use_smartcase_find.unwrap(),
+            use_regex_search: vim.use_regex_search.unwrap(),
             gdefault: vim.gdefault.unwrap(),
             custom_digraphs: vim.custom_digraphs.unwrap(),
             highlight_on_yank_duration: vim.highlight_on_yank_duration.unwrap(),

docs/src/vim.md πŸ”—

@@ -562,6 +562,7 @@ You can change the following settings to modify vim mode's behavior:
 | use_system_clipboard         | Determines how system clipboard is used:<br><ul><li>"always": use for all operations</li><li>"never": only use when explicitly specified</li><li>"on_yank": use for yank operations</li></ul> | "always"      |
 | use_multiline_find           | deprecated                                                                                                                                                                                    |
 | use_smartcase_find           | If `true`, `f` and `t` motions are case-insensitive when the target letter is lowercase.                                                                                                      | false         |
+| use_regex_search             | If `true`, then vim search will use regex mode                                                                                                                                                | true          |
 | gdefault                     | If `true`, the `:substitute` command replaces all matches in a line by default (as if `g` flag was given). The `g` flag then toggles this, replacing only the first match.                    | false         |
 | toggle_relative_line_numbers | If `true`, line numbers are relative in normal mode and absolute in insert mode, giving you the best of both options.                                                                         | false         |
 | custom_digraphs              | An object that allows you to add custom digraphs. Read below for an example.                                                                                                                  | {}            |
@@ -587,6 +588,7 @@ Here's an example of these settings changed:
     "default_mode": "insert",
     "use_system_clipboard": "never",
     "use_smartcase_find": true,
+    "use_regex_search": true,
     "gdefault": true,
     "toggle_relative_line_numbers": true,
     "highlight_on_yank_duration": 50,