From eed80aaef69787a06354c7fc2141fadf446d71eb Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 13 Jan 2026 13:52:44 -0600 Subject: [PATCH] settings_ui: Add vim settings (#46634) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- assets/settings/default.json | 12 +- crates/settings/src/settings_content.rs | 62 ++++++- crates/settings_ui/src/page_data.rs | 222 ++++++++++++++++++++++++ crates/settings_ui/src/settings_ui.rs | 3 + crates/vim/src/vim.rs | 68 ++++++-- 5 files changed, 342 insertions(+), 25 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 923261166a672a07ec471adc0329079f30765571..a6e166ae9d3612d44dc2bfaaaf5638efec6c8444 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -2284,11 +2284,15 @@ "use_smartcase_find": false, "highlight_on_yank_duration": 200, "custom_digraphs": {}, - // Cursor shape for the each mode. - // Specify the mode as the key and the shape as the value. - // The mode can be one of the following: "normal", "replace", "insert", "visual". + // Cursor shape for each mode. // The shape can be one of the following: "block", "bar", "underline", "hollow". - "cursor_shape": {}, + "cursor_shape": { + "normal": "block", + "replace": "underline", + "visual": "block", + // Set to "inherit" to use the editor's cursor_shape. + "insert": "inherit", + }, }, // Which-key popup settings "which_key": { diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index d3486cda47986677f85c61ec77c75c7a0a2fff6c..8c88a75f2cffa71ea74bc5062207ed33b34fc677 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -687,7 +687,19 @@ pub struct VimSettingsContent { pub cursor_shape: Option, } -#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Debug)] +#[derive( + Copy, + Clone, + Default, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + PartialEq, + Debug, + strum::VariantArray, + strum::VariantNames, +)] #[serde(rename_all = "snake_case")] pub enum ModeContent { #[default] @@ -696,7 +708,19 @@ pub enum ModeContent { } /// Controls when to use system clipboard. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] +#[derive( + Copy, + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] #[serde(rename_all = "snake_case")] pub enum UseSystemClipboard { /// Don't use system clipboard. @@ -707,9 +731,39 @@ pub enum UseSystemClipboard { OnYank, } +/// Cursor shape configuration for insert mode in Vim. +#[derive( + Copy, + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] +#[serde(rename_all = "snake_case")] +pub enum VimInsertModeCursorShape { + /// Inherit cursor shape from the editor's base cursor_shape setting. + Inherit, + /// Vertical bar cursor. + Bar, + /// Block cursor that surrounds the character. + Block, + /// Underline cursor. + Underline, + /// Hollow box cursor. + Hollow, +} + /// The settings for cursor shape. #[with_fallible_options] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] +#[derive( + Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, +)] pub struct CursorShapeSettings { /// Cursor shape for the normal mode. /// @@ -726,7 +780,7 @@ pub struct CursorShapeSettings { /// Cursor shape for the insert mode. /// /// The default value follows the primary cursor_shape. - pub insert: Option, + pub insert: Option, } /// Settings specific to journaling diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 7a3e54d02eec09646a862ba9487f7c173a246384..d81b432cf869897280ab21429665a265630f71a8 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -2414,6 +2414,227 @@ fn editor_page() -> SettingsPage { ] } + fn vim_settings_section() -> [SettingsPageItem; 11] { + [ + SettingsPageItem::SectionHeader("Vim"), + SettingsPageItem::SettingItem(SettingItem { + title: "Default Mode", + description: "The default mode when Vim starts.", + field: Box::new(SettingField { + json_path: Some("vim.default_mode"), + pick: |settings_content| settings_content.vim.as_ref()?.default_mode.as_ref(), + write: |settings_content, value| { + settings_content.vim.get_or_insert_default().default_mode = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Toggle Relative Line Numbers", + description: "Toggle relative line numbers in Vim mode.", + field: Box::new(SettingField { + json_path: Some("vim.toggle_relative_line_numbers"), + pick: |settings_content| { + settings_content + .vim + .as_ref()? + .toggle_relative_line_numbers + .as_ref() + }, + write: |settings_content, value| { + settings_content + .vim + .get_or_insert_default() + .toggle_relative_line_numbers = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Use System Clipboard", + description: "Controls when to use system clipboard in Vim mode.", + field: Box::new(SettingField { + json_path: Some("vim.use_system_clipboard"), + pick: |settings_content| { + settings_content.vim.as_ref()?.use_system_clipboard.as_ref() + }, + write: |settings_content, value| { + settings_content + .vim + .get_or_insert_default() + .use_system_clipboard = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Use Smartcase Find", + description: "Enable smartcase searching in Vim mode.", + field: Box::new(SettingField { + json_path: Some("vim.use_smartcase_find"), + pick: |settings_content| { + settings_content.vim.as_ref()?.use_smartcase_find.as_ref() + }, + write: |settings_content, value| { + settings_content + .vim + .get_or_insert_default() + .use_smartcase_find = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Highlight on Yank Duration", + description: "Duration in milliseconds to highlight yanked text in Vim mode.", + field: Box::new(SettingField { + json_path: Some("vim.highlight_on_yank_duration"), + pick: |settings_content| { + settings_content + .vim + .as_ref()? + .highlight_on_yank_duration + .as_ref() + }, + write: |settings_content, value| { + settings_content + .vim + .get_or_insert_default() + .highlight_on_yank_duration = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Cursor Shape - Normal Mode", + description: "Cursor shape for normal mode.", + field: Box::new(SettingField { + json_path: Some("vim.cursor_shape.normal"), + pick: |settings_content| { + settings_content + .vim + .as_ref()? + .cursor_shape + .as_ref()? + .normal + .as_ref() + }, + write: |settings_content, value| { + settings_content + .vim + .get_or_insert_default() + .cursor_shape + .get_or_insert_default() + .normal = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Cursor Shape - Insert Mode", + description: "Cursor shape for insert mode. Inherit uses the editor's cursor shape.", + field: Box::new(SettingField { + json_path: Some("vim.cursor_shape.insert"), + pick: |settings_content| { + settings_content + .vim + .as_ref()? + .cursor_shape + .as_ref()? + .insert + .as_ref() + }, + write: |settings_content, value| { + settings_content + .vim + .get_or_insert_default() + .cursor_shape + .get_or_insert_default() + .insert = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Cursor Shape - Replace Mode", + description: "Cursor shape for replace mode.", + field: Box::new(SettingField { + json_path: Some("vim.cursor_shape.replace"), + pick: |settings_content| { + settings_content + .vim + .as_ref()? + .cursor_shape + .as_ref()? + .replace + .as_ref() + }, + write: |settings_content, value| { + settings_content + .vim + .get_or_insert_default() + .cursor_shape + .get_or_insert_default() + .replace = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Cursor Shape - Visual Mode", + description: "Cursor shape for visual mode.", + field: Box::new(SettingField { + json_path: Some("vim.cursor_shape.visual"), + pick: |settings_content| { + settings_content + .vim + .as_ref()? + .cursor_shape + .as_ref()? + .visual + .as_ref() + }, + write: |settings_content, value| { + settings_content + .vim + .get_or_insert_default() + .cursor_shape + .get_or_insert_default() + .visual = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Custom Digraphs", + description: "Custom digraph mappings for Vim mode.", + field: Box::new( + SettingField { + json_path: Some("vim.custom_digraphs"), + pick: |settings_content| { + settings_content.vim.as_ref()?.custom_digraphs.as_ref() + }, + write: |settings_content, value| { + settings_content.vim.get_or_insert_default().custom_digraphs = value; + }, + } + .unimplemented(), + ), + metadata: None, + files: USER, + }), + ] + } + let items = concat_sections!( auto_save_section(), which_key_section(), @@ -2426,6 +2647,7 @@ fn editor_page() -> SettingsPage { scrollbar_section(), minimap_section(), toolbar_section(), + vim_settings_section(), language_settings_data(), ); diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index a3c20b276ac207188bcb019259ea4fbdaed620d8..29c278c80ac565b8cfea229d97a6d3be36328ee5 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -493,6 +493,9 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 2364b0d4608d81d81af46c2df6f47fffac224c7c..17ae2764ca4a905381dcb6a6a490cb980ad35fc2 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1322,18 +1322,21 @@ impl Vim { _ => CursorShape::Underline, } } else { - cursor_shape.normal.unwrap_or(CursorShape::Block) + cursor_shape.normal } } - Mode::HelixNormal => cursor_shape.normal.unwrap_or(CursorShape::Block), - Mode::Replace => cursor_shape.replace.unwrap_or(CursorShape::Underline), + Mode::HelixNormal => cursor_shape.normal, + Mode::Replace => cursor_shape.replace, Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => { - cursor_shape.visual.unwrap_or(CursorShape::Block) + cursor_shape.visual } - Mode::Insert => cursor_shape.insert.unwrap_or({ - let editor_settings = EditorSettings::get_global(cx); - editor_settings.cursor_shape.unwrap_or_default() - }), + Mode::Insert => match cursor_shape.insert { + InsertModeCursorShape::Explicit(shape) => shape, + InsertModeCursorShape::Inherit => { + let editor_settings = EditorSettings::get_global(cx); + editor_settings.cursor_shape.unwrap_or_default() + } + }, } } @@ -2047,34 +2050,65 @@ struct VimSettings { pub cursor_shape: CursorShapeSettings, } +/// Cursor shape configuration for insert mode. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum InsertModeCursorShape { + /// Inherit cursor shape from the editor's base cursor_shape setting. + /// This allows users to set their preferred editor cursor and have + /// it automatically apply to vim insert mode. + Inherit, + /// Use an explicit cursor shape for insert mode. + Explicit(CursorShape), +} + /// The settings for cursor shape. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct CursorShapeSettings { /// Cursor shape for the normal mode. /// /// Default: block - pub normal: Option, + pub normal: CursorShape, /// Cursor shape for the replace mode. /// /// Default: underline - pub replace: Option, + pub replace: CursorShape, /// Cursor shape for the visual mode. /// /// Default: block - pub visual: Option, + pub visual: CursorShape, /// Cursor shape for the insert mode. /// - /// The default value follows the primary cursor_shape. - pub insert: Option, + /// Default: Inherit (follows editor.cursor_shape) + pub insert: InsertModeCursorShape, +} + +impl From for InsertModeCursorShape { + fn from(shape: settings::VimInsertModeCursorShape) -> Self { + match shape { + settings::VimInsertModeCursorShape::Inherit => InsertModeCursorShape::Inherit, + settings::VimInsertModeCursorShape::Bar => { + InsertModeCursorShape::Explicit(CursorShape::Bar) + } + settings::VimInsertModeCursorShape::Block => { + InsertModeCursorShape::Explicit(CursorShape::Block) + } + settings::VimInsertModeCursorShape::Underline => { + InsertModeCursorShape::Explicit(CursorShape::Underline) + } + settings::VimInsertModeCursorShape::Hollow => { + InsertModeCursorShape::Explicit(CursorShape::Hollow) + } + } + } } impl From for CursorShapeSettings { fn from(settings: settings::CursorShapeSettings) -> Self { Self { - normal: settings.normal.map(Into::into), - replace: settings.replace.map(Into::into), - visual: settings.visual.map(Into::into), - insert: settings.insert.map(Into::into), + normal: settings.normal.unwrap().into(), + replace: settings.replace.unwrap().into(), + visual: settings.visual.unwrap().into(), + insert: settings.insert.unwrap().into(), } } }