Theme-able Vim Mode wrapper (#39813)

Willy Hetland , willyHetland , and Conrad Irwin created

Closes [#14093](https://github.com/zed-industries/zed/issues/14093)
Builds on [#32279](https://github.com/zed-industries/zed/pull/32279) by
making it theme dependent.
Discussion
[#37816](https://github.com/zed-industries/zed/discussions/37816)

Wraps the mode label indicator in a div and makes the wrapper and label
theme-able. Label weight to medium
Mode indicator will render like previously if not theme colors have been
set. (i.e., they match zed default- and fallbacks)
Really helps with visual confirmation of current mode.

_Did not investigate further if there is a way to keep the leading and
trailing -- if no theme var given._

Can be applied either by a theme itself or using `theme_overrides` in
settings.json

Theme colors applied via `theme_overrides`
<img width="233" height="34" alt="Screenshot 2025-10-08 at 23 01 08"
src="https://github.com/user-attachments/assets/a00d9ae4-b6db-46a0-84e2-98d2691a11ad"
/>
<img width="233" height="34" alt="Screenshot 2025-10-08 at 23 01 16"
src="https://github.com/user-attachments/assets/f27fddab-524d-43c4-9307-46b6a656cd35"
/>
<img width="233" height="34" alt="Screenshot 2025-10-08 at 23 01 23"
src="https://github.com/user-attachments/assets/7e477fff-7a40-4c01-95a7-fbd40fff6caa"
/>

No theme applied
<img width="233" height="34" alt="Screenshot 2025-10-08 at 23 01 31"
src="https://github.com/user-attachments/assets/8b7b2c75-007b-4074-a552-181c53f31213"
/>
<img width="233" height="34" alt="Screenshot 2025-10-08 at 23 01 36"
src="https://github.com/user-attachments/assets/7a708d81-2033-4d72-a844-57607a0434ea"
/>
<img width="233" height="34" alt="Screenshot 2025-10-08 at 23 01 40"
src="https://github.com/user-attachments/assets/526f9d10-4d0f-4bc5-af89-31fcca538ce4"
/>



https://github.com/user-attachments/assets/d0d71d4d-504f-4d18-bbd9-83d3a4b2adb7


Release Notes:

- Vim make mode indicator themeable

---------

Co-authored-by: willyHetland <willy.hetland@zeekit.no>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/settings/src/settings_content/theme.rs | 29 ++++++++
crates/theme/src/default_colors.rs            | 18 ++++
crates/theme/src/fallback_themes.rs           | 10 ++
crates/theme/src/schema.rs                    | 36 +++++++++
crates/theme/src/styles/colors.rs             | 19 +++++
crates/vim/src/mode_indicator.rs              | 76 +++++++++++++++++---
6 files changed, 176 insertions(+), 12 deletions(-)

Detailed changes

crates/settings/src/settings_content/theme.rs 🔗

@@ -889,6 +889,35 @@ pub struct ThemeColorsContent {
     /// Deprecated in favor of `version_control_conflict_marker_theirs`.
     #[deprecated]
     pub version_control_conflict_theirs_background: Option<String>,
+
+    /// Background color for Vim Normal mode indicator.
+    #[serde(rename = "vim.normal.background")]
+    pub vim_normal_background: Option<String>,
+    /// Background color for Vim Insert mode indicator.
+    #[serde(rename = "vim.insert.background")]
+    pub vim_insert_background: Option<String>,
+    /// Background color for Vim Replace mode indicator.
+    #[serde(rename = "vim.replace.background")]
+    pub vim_replace_background: Option<String>,
+    /// Background color for Vim Visual mode indicator.
+    #[serde(rename = "vim.visual.background")]
+    pub vim_visual_background: Option<String>,
+    /// Background color for Vim Visual Line mode indicator.
+    #[serde(rename = "vim.visual_line.background")]
+    pub vim_visual_line_background: Option<String>,
+    /// Background color for Vim Visual Block mode indicator.
+    #[serde(rename = "vim.visual_block.background")]
+    pub vim_visual_block_background: Option<String>,
+    /// Background color for Vim Helix Normal mode indicator.
+    #[serde(rename = "vim.helix_normal.background")]
+    pub vim_helix_normal_background: Option<String>,
+    /// Background color for Vim Helix Select mode indicator.
+    #[serde(rename = "vim.helix_select.background")]
+    pub vim_helix_select_background: Option<String>,
+
+    /// Text color for Vim mode indicator label.
+    #[serde(rename = "vim.mode.text")]
+    pub vim_mode_text: Option<String>,
 }
 
 #[skip_serializing_none]

crates/theme/src/default_colors.rs 🔗

@@ -154,6 +154,15 @@ impl ThemeColors {
             version_control_ignored: gray().light().step_12(),
             version_control_conflict_marker_ours: green().light().step_10().alpha(0.5),
             version_control_conflict_marker_theirs: blue().light().step_10().alpha(0.5),
+            vim_normal_background: system.transparent,
+            vim_insert_background: system.transparent,
+            vim_replace_background: system.transparent,
+            vim_visual_background: system.transparent,
+            vim_visual_line_background: system.transparent,
+            vim_visual_block_background: system.transparent,
+            vim_helix_normal_background: system.transparent,
+            vim_helix_select_background: system.transparent,
+            vim_mode_text: system.transparent,
         }
     }
 
@@ -280,6 +289,15 @@ impl ThemeColors {
             version_control_ignored: gray().dark().step_12(),
             version_control_conflict_marker_ours: green().dark().step_10().alpha(0.5),
             version_control_conflict_marker_theirs: blue().dark().step_10().alpha(0.5),
+            vim_normal_background: system.transparent,
+            vim_insert_background: system.transparent,
+            vim_replace_background: system.transparent,
+            vim_visual_background: system.transparent,
+            vim_visual_line_background: system.transparent,
+            vim_visual_block_background: system.transparent,
+            vim_helix_normal_background: system.transparent,
+            vim_helix_select_background: system.transparent,
+            vim_mode_text: system.transparent,
         }
     }
 }

crates/theme/src/fallback_themes.rs 🔗

@@ -233,6 +233,16 @@ pub(crate) fn zed_default_dark() -> Theme {
                 version_control_ignored: crate::gray().light().step_12(),
                 version_control_conflict_marker_ours: crate::green().light().step_12().alpha(0.5),
                 version_control_conflict_marker_theirs: crate::blue().light().step_12().alpha(0.5),
+
+                vim_normal_background: SystemColors::default().transparent,
+                vim_insert_background: SystemColors::default().transparent,
+                vim_replace_background: SystemColors::default().transparent,
+                vim_visual_background: SystemColors::default().transparent,
+                vim_visual_line_background: SystemColors::default().transparent,
+                vim_visual_block_background: SystemColors::default().transparent,
+                vim_helix_normal_background: SystemColors::default().transparent,
+                vim_helix_select_background: SystemColors::default().transparent,
+                vim_mode_text: SystemColors::default().transparent,
             },
             status: StatusColors {
                 conflict: yellow,

crates/theme/src/schema.rs 🔗

@@ -756,6 +756,42 @@ pub fn theme_colors_refinement(
             .as_ref()
             .or(this.version_control_conflict_theirs_background.as_ref())
             .and_then(|color| try_parse_color(color).ok()),
+        vim_normal_background: this
+            .vim_normal_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        vim_insert_background: this
+            .vim_insert_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        vim_replace_background: this
+            .vim_replace_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        vim_visual_background: this
+            .vim_visual_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        vim_visual_line_background: this
+            .vim_visual_line_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        vim_visual_block_background: this
+            .vim_visual_block_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        vim_helix_normal_background: this
+            .vim_helix_normal_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        vim_helix_select_background: this
+            .vim_helix_select_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        vim_mode_text: this
+            .vim_mode_text
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
     }
 }
 

crates/theme/src/styles/colors.rs 🔗

@@ -162,6 +162,25 @@ pub struct ThemeColors {
     /// The border color of the minimap thumb.
     pub minimap_thumb_border: Hsla,
 
+    /// Background color for Vim Normal mode indicator.
+    pub vim_normal_background: Hsla,
+    /// Background color for Vim Insert mode indicator.
+    pub vim_insert_background: Hsla,
+    /// Background color for Vim Replace mode indicator.
+    pub vim_replace_background: Hsla,
+    /// Background color for Vim Visual mode indicator.
+    pub vim_visual_background: Hsla,
+    /// Background color for Vim Visual Line mode indicator.
+    pub vim_visual_line_background: Hsla,
+    /// Background color for Vim Visual Block mode indicator.
+    pub vim_visual_block_background: Hsla,
+    /// Background color for Vim Helix Normal mode indicator.
+    pub vim_helix_normal_background: Hsla,
+    /// Background color for Vim Helix Select mode indicator.
+    pub vim_helix_select_background: Hsla,
+    /// Text color for Vim mode indicator label.
+    pub vim_mode_text: Hsla,
+
     // ===
     // Editor
     // ===

crates/vim/src/mode_indicator.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{Context, Entity, Render, Subscription, WeakEntity, Window, div};
+use gpui::{Context, Element, Entity, FontWeight, Render, Subscription, WeakEntity, Window, div};
 use ui::text_for_keystrokes;
 use workspace::{StatusItemView, item::ItemHandle, ui::prelude::*};
 
@@ -93,13 +93,33 @@ impl Render for ModeIndicator {
         };
 
         let vim_readable = vim.read(cx);
-        let label = if let Some(label) = vim_readable.status_label.clone() {
-            label
+        let status_label = vim_readable.status_label.clone();
+        let temp_mode = vim_readable.temp_mode;
+        let mode = vim_readable.mode;
+
+        let theme = cx.theme();
+        let colors = theme.colors();
+        let system_transparent = gpui::hsla(0.0, 0.0, 0.0, 0.0);
+        let vim_mode_text = colors.vim_mode_text;
+        let bg_color = match mode {
+            crate::state::Mode::Normal => colors.vim_normal_background,
+            crate::state::Mode::Insert => colors.vim_insert_background,
+            crate::state::Mode::Replace => colors.vim_replace_background,
+            crate::state::Mode::Visual => colors.vim_visual_background,
+            crate::state::Mode::VisualLine => colors.vim_visual_line_background,
+            crate::state::Mode::VisualBlock => colors.vim_visual_block_background,
+            crate::state::Mode::HelixNormal => colors.vim_helix_normal_background,
+            crate::state::Mode::HelixSelect => colors.vim_helix_select_background,
+        };
+
+        let (label, mode): (SharedString, Option<SharedString>) = if let Some(label) = status_label
+        {
+            (label, None)
         } else {
-            let mode = if vim_readable.temp_mode {
-                format!("(insert) {}", vim_readable.mode)
+            let mode_str = if temp_mode {
+                format!("(insert) {}", mode)
             } else {
-                vim_readable.mode.to_string()
+                mode.to_string()
             };
 
             let current_operators_description = self.current_operators_description(vim.clone(), cx);
@@ -107,13 +127,45 @@ impl Render for ModeIndicator {
                 .pending_keys
                 .as_ref()
                 .unwrap_or(&current_operators_description);
-            format!("{} -- {} --", pending, mode).into()
+            let mode = if bg_color != system_transparent {
+                mode_str.into()
+            } else {
+                format!("-- {} --", mode_str).into()
+            };
+            (pending.into(), Some(mode))
         };
-
-        Label::new(label)
-            .size(LabelSize::Small)
-            .line_height_style(LineHeightStyle::UiLabel)
-            .into_any_element()
+        h_flex()
+            .gap_1()
+            .when(!label.is_empty(), |el| {
+                el.child(
+                    Label::new(label)
+                        .line_height_style(LineHeightStyle::UiLabel)
+                        .weight(FontWeight::MEDIUM),
+                )
+            })
+            .when_some(mode, |el, mode| {
+                el.child(
+                    v_flex()
+                        .when(bg_color != system_transparent, |el| el.px_2())
+                        // match with other icons at the bottom that use default buttons
+                        .h(ButtonSize::Default.rems())
+                        .justify_center()
+                        .rounded_sm()
+                        .bg(bg_color)
+                        .child(
+                            Label::new(mode)
+                                .size(LabelSize::Small)
+                                .line_height_style(LineHeightStyle::UiLabel)
+                                .weight(FontWeight::MEDIUM)
+                                .when(
+                                    bg_color != system_transparent
+                                        && vim_mode_text != system_transparent,
+                                    |el| el.color(Color::Custom(vim_mode_text)),
+                                ),
+                        ),
+                )
+            })
+            .into_any()
     }
 }