Detailed changes
@@ -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.
@@ -701,6 +701,9 @@ pub struct VimSettingsContent {
pub toggle_relative_line_numbers: Option<bool>,
pub use_system_clipboard: Option<UseSystemClipboard>,
pub use_smartcase_find: 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>,
pub custom_digraphs: Option<HashMap<String, Arc<str>>>,
pub highlight_on_yank_duration: Option<u64>,
pub cursor_shape: Option<CursorShapeSettings>,
@@ -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.",
@@ -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>) {
- // 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<Vim>) {
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);
+ })
+ }
});
}
});
@@ -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;
@@ -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<String, Arc<str>>,
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(),
@@ -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"}}
@@ -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:<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 |
+| 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": {