From 71ea133d72eba0d8ab0216d78ae0ac05fd10c406 Mon Sep 17 00:00:00 2001
From: Willy Hetland <58665964+willeyh-git@users.noreply.github.com>
Date: Tue, 21 Oct 2025 06:30:30 +0200
Subject: [PATCH] Theme-able Vim Mode wrapper (#39813)
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`
No theme applied
https://github.com/user-attachments/assets/d0d71d4d-504f-4d18-bbd9-83d3a4b2adb7
Release Notes:
- Vim make mode indicator themeable
---------
Co-authored-by: willyHetland
Co-authored-by: Conrad Irwin
---
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(-)
diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs
index 0228fcbfe832f56c8ad504fd289b273b0a1c0ec7..487ffad34e313f403d97f91af5df205294df61df 100644
--- a/crates/settings/src/settings_content/theme.rs
+++ b/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,
+
+ /// Background color for Vim Normal mode indicator.
+ #[serde(rename = "vim.normal.background")]
+ pub vim_normal_background: Option,
+ /// Background color for Vim Insert mode indicator.
+ #[serde(rename = "vim.insert.background")]
+ pub vim_insert_background: Option,
+ /// Background color for Vim Replace mode indicator.
+ #[serde(rename = "vim.replace.background")]
+ pub vim_replace_background: Option,
+ /// Background color for Vim Visual mode indicator.
+ #[serde(rename = "vim.visual.background")]
+ pub vim_visual_background: Option,
+ /// Background color for Vim Visual Line mode indicator.
+ #[serde(rename = "vim.visual_line.background")]
+ pub vim_visual_line_background: Option,
+ /// Background color for Vim Visual Block mode indicator.
+ #[serde(rename = "vim.visual_block.background")]
+ pub vim_visual_block_background: Option,
+ /// Background color for Vim Helix Normal mode indicator.
+ #[serde(rename = "vim.helix_normal.background")]
+ pub vim_helix_normal_background: Option,
+ /// Background color for Vim Helix Select mode indicator.
+ #[serde(rename = "vim.helix_select.background")]
+ pub vim_helix_select_background: Option,
+
+ /// Text color for Vim mode indicator label.
+ #[serde(rename = "vim.mode.text")]
+ pub vim_mode_text: Option,
}
#[skip_serializing_none]
diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs
index 051b7acf102597b6f11581afdd45611b9a4b76e3..80ad845e989b244a5bcd5eb529720d10416ea7bc 100644
--- a/crates/theme/src/default_colors.rs
+++ b/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,
}
}
}
diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs
index 4fb8069bc16d1967dfe10b2e6a577b990d942db7..ae120165f23095266cf92fd33a1cd1ccb88fe309 100644
--- a/crates/theme/src/fallback_themes.rs
+++ b/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,
diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs
index 2d7e1ff9d823eae0d48b375592c6d1f91318f472..c4ed624bf642e0820fd9187224f96e2acfa92018 100644
--- a/crates/theme/src/schema.rs
+++ b/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()),
}
}
diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs
index 198ad97adb5d964a1d8f62c5bde99d1d5be5adf7..179d02b91684410bb641893e87759bd30cc73b36 100644
--- a/crates/theme/src/styles/colors.rs
+++ b/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
// ===
diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs
index ed182eb74d8e01cc9b8f543cbbc928f6864391e3..42d4915fc509e0f373c8d2c5a2a422b74cc84a8f 100644
--- a/crates/vim/src/mode_indicator.rs
+++ b/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) = 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(¤t_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()
}
}