diff --git a/assets/settings/default.json b/assets/settings/default.json index 96c886ce63ff7575d7405cb5d820f14ad1aadc9b..bb0186eccb98c0e36a3f84f89f87c7c0ae51c8bc 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -2275,6 +2275,7 @@ "toggle_relative_line_numbers": false, "use_system_clipboard": "always", "use_smartcase_find": false, + "gdefault": false, "highlight_on_yank_duration": 200, "custom_digraphs": {}, // Cursor shape for each mode. diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 6923ba088b29afeaf3bc6ddbf6ca137c10f97166..86c58dc5400e441bdca9fe8c33c4a66adef6464e 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -701,6 +701,9 @@ pub struct VimSettingsContent { pub toggle_relative_line_numbers: Option, pub use_system_clipboard: Option, pub use_smartcase_find: Option, + /// When enabled, the `:substitute` command replaces all matches in a line + /// by default. The 'g' flag then toggles this behavior., + pub gdefault: Option, pub custom_digraphs: Option>>, pub highlight_on_yank_duration: Option, pub cursor_shape: Option, diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index d82fdf338a9e0f8b139a5ccfcb0eb40114b7d41f..7b31b94265da1db4ee56b295b2178f57ca6f3c78 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -2410,7 +2410,7 @@ fn editor_page() -> SettingsPage { ] } - fn vim_settings_section() -> [SettingsPageItem; 11] { + fn vim_settings_section() -> [SettingsPageItem; 12] { [ SettingsPageItem::SectionHeader("Vim"), SettingsPageItem::SettingItem(SettingItem { @@ -2484,6 +2484,19 @@ fn editor_page() -> SettingsPage { metadata: None, files: USER, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Global Substitution Default", + description: "When enabled, the :substitute command replaces all matches in a line by default. The 'g' flag then toggles this behavior.", + field: Box::new(SettingField { + json_path: Some("vim.gdefault"), + pick: |settings_content| settings_content.vim.as_ref()?.gdefault.as_ref(), + write: |settings_content, value| { + settings_content.vim.get_or_insert_default().gdefault = value; + }, + }), + metadata: None, + files: USER, + }), SettingsPageItem::SettingItem(SettingItem { title: "Highlight on Yank Duration", description: "Duration in milliseconds to highlight yanked text in Vim mode.", diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index e320790a9b9cef8ea8ffd2ba9d8676dbfd6b87b5..2498ed812938c5b53120e55663ce9ba9a8787d88 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -40,7 +40,7 @@ use workspace::{SplitDirection, notifications::DetachAndPromptErr}; use zed_actions::{OpenDocs, RevealTarget}; use crate::{ - ToggleMarksView, ToggleRegistersView, Vim, + ToggleMarksView, ToggleRegistersView, Vim, VimSettings, motion::{EndOfDocument, Motion, MotionKind, StartOfDocument}, normal::{ JoinLines, @@ -88,6 +88,7 @@ pub enum VimOption { Number(bool), RelativeNumber(bool), IgnoreCase(bool), + GDefault(bool), } impl VimOption { @@ -134,6 +135,10 @@ impl VimOption { (None, VimOption::IgnoreCase(false)), (Some("ic"), VimOption::IgnoreCase(true)), (Some("noic"), VimOption::IgnoreCase(false)), + (None, VimOption::GDefault(true)), + (Some("gd"), VimOption::GDefault(true)), + (None, VimOption::GDefault(false)), + (Some("nogd"), VimOption::GDefault(false)), ] .into_iter() .filter(move |(prefix, option)| prefix.unwrap_or(option.to_string()).starts_with(query)) @@ -160,6 +165,11 @@ impl VimOption { "noignorecase" => Some(Self::IgnoreCase(false)), "noic" => Some(Self::IgnoreCase(false)), + "gdefault" => Some(Self::GDefault(true)), + "gd" => Some(Self::GDefault(true)), + "nogdefault" => Some(Self::GDefault(false)), + "nogd" => Some(Self::GDefault(false)), + _ => None, } } @@ -174,6 +184,8 @@ impl VimOption { VimOption::RelativeNumber(false) => "norelativenumber", VimOption::IgnoreCase(true) => "ignorecase", VimOption::IgnoreCase(false) => "noignorecase", + VimOption::GDefault(true) => "gdefault", + VimOption::GDefault(false) => "nogdefault", } } } @@ -271,7 +283,6 @@ impl Deref for WrappedAction { } pub fn register(editor: &mut Editor, cx: &mut Context) { - // Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| { Vim::action(editor, cx, |vim, action: &VimSet, _, cx| { for option in action.options.iter() { vim.update_editor(cx, |_, editor, cx| match option { @@ -295,6 +306,14 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { store.override_global(settings); }); } + VimOption::GDefault(enabled) => { + let mut settings = VimSettings::get_global(cx).clone(); + settings.gdefault = *enabled; + + SettingsStore::update(cx, |store, _| { + store.override_global(settings); + }) + } }); } }); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index d945aea263305ba2f92ccd2ee88afe9393990b34..4828ae9560065c4824ea5cd38813c459d91f8882 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -10,7 +10,7 @@ use util::serde::default_true; use workspace::{notifications::NotifyResultExt, searchable::Direction}; use crate::{ - Vim, + Vim, VimSettings, command::CommandRange, motion::Motion, state::{Mode, SearchState}, @@ -581,7 +581,9 @@ impl Vim { ) } - if !replacement.flag_g { + // gdefault inverts the behavior of the 'g' flag. + let replace_all = VimSettings::get_global(cx).gdefault != replacement.flag_g; + if !replace_all { options.set(SearchOptions::ONE_MATCH_PER_LINE, true); } @@ -704,7 +706,7 @@ impl Replacement { for c in flags.chars() { match c { - 'g' => replacement.flag_g = true, + 'g' => replacement.flag_g = !replacement.flag_g, 'n' => replacement.flag_n = true, 'c' => replacement.flag_c = true, 'i' => replacement.case_sensitive = Some(false), @@ -1143,6 +1145,59 @@ mod test { }); } + #[gpui::test] + async fn test_replace_gdefault(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + // Set the `gdefault` option in both Zed and Neovim. + cx.simulate_shared_keystrokes(": s e t space g d e f a u l t") + .await; + cx.simulate_shared_keystrokes("enter").await; + + cx.set_shared_state(indoc! { + "ˇaa aa aa aa + aa + aa" + }) + .await; + + // With gdefault on, :s/// replaces all matches (like :s///g normally). + cx.simulate_shared_keystrokes(": s / a a / b b").await; + cx.simulate_shared_keystrokes("enter").await; + cx.shared_state().await.assert_eq(indoc! { + "ˇbb bb bb bb + aa + aa" + }); + + // With gdefault on, :s///g replaces only the first match. + cx.simulate_shared_keystrokes(": s / b b / c c / g").await; + cx.simulate_shared_keystrokes("enter").await; + cx.shared_state().await.assert_eq(indoc! { + "ˇcc bb bb bb + aa + aa" + }); + + // Each successive `/g` flag should invert the one before it. + cx.simulate_shared_keystrokes(": s / b b / d d / g g").await; + cx.simulate_shared_keystrokes("enter").await; + cx.shared_state().await.assert_eq(indoc! { + "ˇcc dd dd dd + aa + aa" + }); + + cx.simulate_shared_keystrokes(": s / c c / e e / g g g") + .await; + cx.simulate_shared_keystrokes("enter").await; + cx.shared_state().await.assert_eq(indoc! { + "ˇee dd dd dd + aa + aa" + }); + } + #[gpui::test] async fn test_replace_c(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 860af285c42753ce0e6c559831f8f22745b6ed6d..2ede08172f322f41c16369714e28ca4c894b53ef 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -2075,12 +2075,13 @@ struct VimEditorSettingsState { hide_edit_predictions: bool, } -#[derive(RegisterSetting)] +#[derive(Clone, RegisterSetting)] struct VimSettings { pub default_mode: Mode, pub toggle_relative_line_numbers: bool, pub use_system_clipboard: settings::UseSystemClipboard, pub use_smartcase_find: bool, + pub gdefault: bool, pub custom_digraphs: HashMap>, pub highlight_on_yank_duration: u64, pub cursor_shape: CursorShapeSettings, @@ -2166,6 +2167,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(), + gdefault: vim.gdefault.unwrap(), custom_digraphs: vim.custom_digraphs.unwrap(), highlight_on_yank_duration: vim.highlight_on_yank_duration.unwrap(), cursor_shape: vim.cursor_shape.unwrap().into(), diff --git a/crates/vim/test_data/test_replace_gdefault.json b/crates/vim/test_data/test_replace_gdefault.json new file mode 100644 index 0000000000000000000000000000000000000000..c13f583acb2de4e28cbc00716ad660d630e1bbfd --- /dev/null +++ b/crates/vim/test_data/test_replace_gdefault.json @@ -0,0 +1,64 @@ +{"Key":":"} +{"Key":"s"} +{"Key":"e"} +{"Key":"t"} +{"Key":"space"} +{"Key":"g"} +{"Key":"d"} +{"Key":"e"} +{"Key":"f"} +{"Key":"a"} +{"Key":"u"} +{"Key":"l"} +{"Key":"t"} +{"Key":"enter"} +{"Put":{"state":"ˇaa aa aa aa\naa\naa"}} +{"Key":":"} +{"Key":"s"} +{"Key":"/"} +{"Key":"a"} +{"Key":"a"} +{"Key":"/"} +{"Key":"b"} +{"Key":"b"} +{"Key":"enter"} +{"Get":{"state":"ˇbb bb bb bb\naa\naa","mode":"Normal"}} +{"Key":":"} +{"Key":"s"} +{"Key":"/"} +{"Key":"b"} +{"Key":"b"} +{"Key":"/"} +{"Key":"c"} +{"Key":"c"} +{"Key":"/"} +{"Key":"g"} +{"Key":"enter"} +{"Get":{"state":"ˇcc bb bb bb\naa\naa","mode":"Normal"}} +{"Key":":"} +{"Key":"s"} +{"Key":"/"} +{"Key":"b"} +{"Key":"b"} +{"Key":"/"} +{"Key":"d"} +{"Key":"d"} +{"Key":"/"} +{"Key":"g"} +{"Key":"g"} +{"Key":"enter"} +{"Get":{"state":"ˇcc dd dd dd\naa\naa","mode":"Normal"}} +{"Key":":"} +{"Key":"s"} +{"Key":"/"} +{"Key":"c"} +{"Key":"c"} +{"Key":"/"} +{"Key":"e"} +{"Key":"e"} +{"Key":"/"} +{"Key":"g"} +{"Key":"g"} +{"Key":"g"} +{"Key":"enter"} +{"Get":{"state":"ˇee dd dd dd\naa\naa","mode":"Normal"}} diff --git a/docs/src/vim.md b/docs/src/vim.md index 687c049a409c0f141667a0f1b6ea814f487f6c2f..2c3311e712030c083e0b3112e38bfa00b30bf587 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -569,6 +569,7 @@ You can change the following settings to modify vim mode's behavior: | use_system_clipboard | Determines how system clipboard is used:
  • "always": use for all operations
  • "never": only use when explicitly specified
  • "on_yank": use for yank operations
| "always" | | use_multiline_find | deprecated | | use_smartcase_find | If `true`, `f` and `t` motions are case-insensitive when the target letter is lowercase. | false | +| 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. | {} | | highlight_on_yank_duration | The duration of the highlight animation(in ms). Set to `0` to disable | 200 | @@ -593,6 +594,7 @@ Here's an example of these settings changed: "default_mode": "insert", "use_system_clipboard": "never", "use_smartcase_find": true, + "gdefault": true, "relative_line_numbers": "enabled", "highlight_on_yank_duration": 50, "custom_digraphs": {