settings_ui: Add vim settings (#46634)

Ben Kunkle created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

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(-)

Detailed changes

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": {

crates/settings/src/settings_content.rs 🔗

@@ -687,7 +687,19 @@ pub struct VimSettingsContent {
     pub cursor_shape: Option<CursorShapeSettings>,
 }
 
-#[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<CursorShape>,
+    pub insert: Option<VimInsertModeCursorShape>,
 }
 
 /// Settings specific to journaling

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(),
     );
 

crates/settings_ui/src/settings_ui.rs 🔗

@@ -493,6 +493,9 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<settings::DisplayIn>(render_dropdown)
         .add_basic_renderer::<settings::MinimapThumb>(render_dropdown)
         .add_basic_renderer::<settings::MinimapThumbBorder>(render_dropdown)
+        .add_basic_renderer::<settings::ModeContent>(render_dropdown)
+        .add_basic_renderer::<settings::UseSystemClipboard>(render_dropdown)
+        .add_basic_renderer::<settings::VimInsertModeCursorShape>(render_dropdown)
         .add_basic_renderer::<settings::SteppingGranularity>(render_dropdown)
         .add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
         .add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)

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<CursorShape>,
+    pub normal: CursorShape,
     /// Cursor shape for the replace mode.
     ///
     /// Default: underline
-    pub replace: Option<CursorShape>,
+    pub replace: CursorShape,
     /// Cursor shape for the visual mode.
     ///
     /// Default: block
-    pub visual: Option<CursorShape>,
+    pub visual: CursorShape,
     /// Cursor shape for the insert mode.
     ///
-    /// The default value follows the primary cursor_shape.
-    pub insert: Option<CursorShape>,
+    /// Default: Inherit (follows editor.cursor_shape)
+    pub insert: InsertModeCursorShape,
+}
+
+impl From<settings::VimInsertModeCursorShape> 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<settings::CursorShapeSettings> 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(),
         }
     }
 }