vim: Add gdefault setting to set /g as a default substitution flag (#47664)

Add support for Vim's `gdefault` option which makes the `:substitute`
command replace all matches in a line by default, instead of just the
first match. When enabled, the `/g` flag inverts this behavior.

- Add `vim.gdefault` setting
- Add `:set gdefault`, `:set nogdefault` (and short forms `:set gd`, `:set nogd`)
- Fix handling of multiple `/g` flags so that each one inverts the one before

Closes #36209

Release Notes:

- vim: Add `vim.gdefault` setting to make `/g` (replace all matches in a line) the default for substitutions, along with `:set gdefault` and `:set nogdefault` commands (short forms: `gd`, `nogd`)

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
This commit is contained in:
Ran Benitaanddino authored and GitHub committed 2026-01-27 17:04:10 +00:00
1 parent 559a8500a0
commit 1169dff02b
8 files changed
+166 -7

No files matched your search

+1
View File
@@ -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>,
+14 -1
View File
@@ -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.",
+21 -2
View File
@@ -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);
})
}
});
}
});
+58 -3
View File
@@ -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;
+3 -1
View File
@@ -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"}}
+2
View File
@@ -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": {