Add a mode indicator for vim

Conrad Irwin created

This is the second most common remaining complaint (after :w not
working).

Fixes: zed-industries/community#409

Change summary

Cargo.lock                          |  1 
crates/theme/src/theme.rs           |  1 
crates/vim/Cargo.toml               |  2 
crates/vim/src/mode_indicator.rs    | 68 +++++++++++++++++++++++++++++++
crates/vim/src/vim.rs               |  2 
crates/zed/src/zed.rs               |  2 
styles/src/style_tree/status_bar.ts |  1 
7 files changed, 77 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -8523,6 +8523,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "settings",
+ "theme",
  "tokio",
  "util",
  "workspace",

crates/theme/src/theme.rs 🔗

@@ -402,6 +402,7 @@ pub struct StatusBar {
     pub height: f32,
     pub item_spacing: f32,
     pub cursor_position: TextStyle,
+    pub vim_mode: TextStyle,
     pub active_language: Interactive<ContainedText>,
     pub auto_update_progress_message: TextStyle,
     pub auto_update_done_message: TextStyle,

crates/vim/Cargo.toml 🔗

@@ -32,6 +32,7 @@ language = { path = "../language" }
 search = { path = "../search" }
 settings = { path = "../settings" }
 workspace = { path = "../workspace" }
+theme = { path = "../theme" }
 
 [dev-dependencies]
 indoc.workspace = true
@@ -44,3 +45,4 @@ project = { path = "../project", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 settings = { path = "../settings" }
 workspace = { path = "../workspace", features = ["test-support"] }
+theme = { path = "../theme", features = ["test-support"] }

crates/vim/src/mode_indicator.rs 🔗

@@ -0,0 +1,68 @@
+use gpui::{
+    elements::{Empty, Label},
+    AnyElement, Element, Entity, View, ViewContext,
+};
+use workspace::{item::ItemHandle, StatusItemView};
+
+use crate::{state::Mode, Vim};
+
+pub struct ModeIndicator {
+    mode: Option<Mode>,
+}
+
+impl ModeIndicator {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+        cx.observe_global::<Vim, _>(|this, cx| {
+            let vim = Vim::read(cx);
+            if vim.enabled {
+                this.set_mode(Some(Vim::read(cx).state.mode), cx)
+            } else {
+                this.set_mode(None, cx)
+            }
+        })
+        .detach();
+        Self { mode: None }
+    }
+
+    pub fn set_mode(&mut self, mode: Option<Mode>, cx: &mut ViewContext<Self>) {
+        if mode != self.mode {
+            self.mode = mode;
+            cx.notify();
+        }
+    }
+}
+
+impl Entity for ModeIndicator {
+    type Event = ();
+}
+
+impl View for ModeIndicator {
+    fn ui_name() -> &'static str {
+        "ModeIndicator"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        if let Some(mode) = self.mode {
+            let theme = &theme::current(cx).workspace.status_bar;
+            let text = match mode {
+                Mode::Normal => "",
+                Mode::Insert => "--- INSERT ---",
+                Mode::Visual { line: false } => "--- VISUAL ---",
+                Mode::Visual { line: true } => "--- VISUAL LINE ---",
+            };
+            Label::new(text, theme.vim_mode.clone()).into_any()
+        } else {
+            Empty::new().into_any()
+        }
+    }
+}
+
+impl StatusItemView for ModeIndicator {
+    fn set_active_pane_item(
+        &mut self,
+        _active_pane_item: Option<&dyn ItemHandle>,
+        _cx: &mut ViewContext<Self>,
+    ) {
+        // nothing to do.
+    }
+}

crates/vim/src/vim.rs 🔗

@@ -3,6 +3,7 @@ mod test;
 
 mod editor_events;
 mod insert;
+mod mode_indicator;
 mod motion;
 mod normal;
 mod object;
@@ -18,6 +19,7 @@ use gpui::{
     ViewHandle, WeakViewHandle, WindowContext,
 };
 use language::CursorShape;
+pub use mode_indicator::ModeIndicator;
 use motion::Motion;
 use normal::normal_replace;
 use serde::Deserialize;

crates/zed/src/zed.rs 🔗

@@ -312,8 +312,10 @@ pub fn initialize_workspace(
                 feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
             });
             let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
+            let vim_mode = cx.add_view(|cx| vim::ModeIndicator::new(cx));
             workspace.status_bar().update(cx, |status_bar, cx| {
                 status_bar.add_left_item(diagnostic_summary, cx);
+                status_bar.add_left_item(vim_mode, cx);
                 status_bar.add_left_item(activity_indicator, cx);
                 status_bar.add_right_item(feedback_button, cx);
                 status_bar.add_right_item(copilot, cx);

styles/src/style_tree/status_bar.ts 🔗

@@ -27,6 +27,7 @@ export default function status_bar(): any {
         },
         border: border(layer, { top: true, overlay: true }),
         cursor_position: text(layer, "sans", "variant"),
+        vim_mode: text(layer, "sans", "variant"),
         active_language: interactive({
             base: {
                 padding: { left: 6, right: 6 },