vim: Add Smart Relative Line Number (#16567)

0x2CA and Conrad Irwin created

Closes #16514

Release Notes:

- Added Vim: absolute numbering in any mode except `insert` mode

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

assets/settings/default.json |  1 
crates/editor/src/actions.rs |  1 
crates/editor/src/editor.rs  | 25 +++++++++++++++++++
crates/editor/src/element.rs |  3 +
crates/vim/src/state.rs      | 10 ++++++
crates/vim/src/vim.rs        | 49 ++++++++++++++++++++++++++++++++++++-
docs/src/vim.md              | 13 ++++++++-
7 files changed, 96 insertions(+), 6 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -946,6 +946,7 @@
   },
   // Vim settings
   "vim": {
+    "toggle_relative_line_numbers": false,
     "use_system_clipboard": "always",
     "use_multiline_find": false,
     "use_smartcase_find": false,

crates/editor/src/actions.rs 🔗

@@ -318,6 +318,7 @@ gpui::actions!(
         ToggleHunkDiff,
         ToggleInlayHints,
         ToggleLineNumbers,
+        ToggleRelativeLineNumbers,
         ToggleIndentGuides,
         ToggleSoftWrap,
         ToggleTabBar,

crates/editor/src/editor.rs 🔗

@@ -512,6 +512,7 @@ pub struct Editor {
     show_breadcrumbs: bool,
     show_gutter: bool,
     show_line_numbers: Option<bool>,
+    use_relative_line_numbers: Option<bool>,
     show_git_diff_gutter: Option<bool>,
     show_code_actions: Option<bool>,
     show_runnables: Option<bool>,
@@ -1853,6 +1854,7 @@ impl Editor {
             show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
             show_gutter: mode == EditorMode::Full,
             show_line_numbers: None,
+            use_relative_line_numbers: None,
             show_git_diff_gutter: None,
             show_code_actions: None,
             show_runnables: None,
@@ -10610,6 +10612,29 @@ impl Editor {
         EditorSettings::override_global(editor_settings, cx);
     }
 
+    pub fn should_use_relative_line_numbers(&self, cx: &WindowContext) -> bool {
+        self.use_relative_line_numbers
+            .unwrap_or(EditorSettings::get_global(cx).relative_line_numbers)
+    }
+
+    pub fn toggle_relative_line_numbers(
+        &mut self,
+        _: &ToggleRelativeLineNumbers,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let is_relative = self.should_use_relative_line_numbers(cx);
+        self.set_relative_line_number(Some(!is_relative), cx)
+    }
+
+    pub fn set_relative_line_number(
+        &mut self,
+        is_relative: Option<bool>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.use_relative_line_numbers = is_relative;
+        cx.notify();
+    }
+
     pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
         self.show_gutter = show_gutter;
         cx.notify();

crates/editor/src/element.rs 🔗

@@ -344,6 +344,7 @@ impl EditorElement {
         register_action(view, cx, Editor::toggle_soft_wrap);
         register_action(view, cx, Editor::toggle_tab_bar);
         register_action(view, cx, Editor::toggle_line_numbers);
+        register_action(view, cx, Editor::toggle_relative_line_numbers);
         register_action(view, cx, Editor::toggle_indent_guides);
         register_action(view, cx, Editor::toggle_inlay_hints);
         register_action(view, cx, hover_popover::hover);
@@ -1770,7 +1771,7 @@ impl EditorElement {
         });
         let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
 
-        let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
+        let is_relative = editor.should_use_relative_line_numbers(cx);
         let relative_to = if is_relative {
             Some(newest_selection_head.row())
         } else {

crates/vim/src/state.rs 🔗

@@ -9,7 +9,9 @@ use crate::{UseSystemClipboard, Vim, VimSettings};
 use collections::HashMap;
 use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
 use editor::{Anchor, ClipboardSelection, Editor};
-use gpui::{Action, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, Global};
+use gpui::{
+    Action, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, Global, View, WeakView,
+};
 use language::Point;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
@@ -160,6 +162,8 @@ pub struct VimGlobals {
     pub last_yank: Option<SharedString>,
     pub registers: HashMap<char, Register>,
     pub recordings: HashMap<char, Vec<ReplayableAction>>,
+
+    pub focused_vim: Option<WeakView<Vim>>,
 }
 impl Global for VimGlobals {}
 
@@ -373,6 +377,10 @@ impl VimGlobals {
             );
         }
     }
+
+    pub fn focused_vim(&self) -> Option<View<Vim>> {
+        self.focused_vim.as_ref().and_then(|vim| vim.upgrade())
+    }
 }
 
 impl Vim {

crates/vim/src/vim.rs 🔗

@@ -23,8 +23,8 @@ use editor::{
     Anchor, Bias, Editor, EditorEvent, EditorMode, ToPoint,
 };
 use gpui::{
-    actions, impl_actions, Action, AppContext, EventEmitter, KeyContext, KeystrokeEvent, Render,
-    View, ViewContext, WeakView,
+    actions, impl_actions, Action, AppContext, Entity, EventEmitter, KeyContext, KeystrokeEvent,
+    Render, View, ViewContext, WeakView,
 };
 use insert::NormalBefore;
 use language::{CursorShape, Point, Selection, SelectionGoal, TransactionId};
@@ -228,8 +228,21 @@ impl Vim {
         }
 
         let mut was_enabled = Vim::enabled(cx);
+        let mut was_toggle = VimSettings::get_global(cx).toggle_relative_line_numbers;
         cx.observe_global::<SettingsStore>(move |editor, cx| {
             let enabled = Vim::enabled(cx);
+            let toggle = VimSettings::get_global(cx).toggle_relative_line_numbers;
+            if enabled && was_enabled && (toggle != was_toggle) {
+                if toggle {
+                    let is_relative = editor
+                        .addon::<VimAddon>()
+                        .map(|vim| vim.view.read(cx).mode != Mode::Insert);
+                    editor.set_relative_line_number(is_relative, cx)
+                } else {
+                    editor.set_relative_line_number(None, cx)
+                }
+            }
+            was_toggle = VimSettings::get_global(cx).toggle_relative_line_numbers;
             if was_enabled == enabled {
                 return;
             }
@@ -296,6 +309,7 @@ impl Vim {
         editor.set_autoindent(true);
         editor.selections.line_mode = false;
         editor.unregister_addon::<VimAddon>();
+        editor.set_relative_line_number(None, cx)
     }
 
     /// Register an action on the editor.
@@ -424,6 +438,17 @@ impl Vim {
         // Sync editor settings like clip mode
         self.sync_vim_settings(cx);
 
+        if VimSettings::get_global(cx).toggle_relative_line_numbers {
+            if self.mode != self.last_mode {
+                if self.mode == Mode::Insert || self.last_mode == Mode::Insert {
+                    self.update_editor(cx, |vim, editor, cx| {
+                        let is_relative = vim.mode != Mode::Insert;
+                        editor.set_relative_line_number(Some(is_relative), cx)
+                    });
+                }
+            }
+        }
+
         if leave_selections {
             return;
         }
@@ -616,6 +641,24 @@ impl Vim {
 
         cx.emit(VimEvent::Focused);
         self.sync_vim_settings(cx);
+
+        if VimSettings::get_global(cx).toggle_relative_line_numbers {
+            if let Some(old_vim) = Vim::globals(cx).focused_vim() {
+                if old_vim.entity_id() != cx.view().entity_id() {
+                    old_vim.update(cx, |vim, cx| {
+                        vim.update_editor(cx, |_, editor, cx| {
+                            editor.set_relative_line_number(None, cx)
+                        });
+                    });
+
+                    self.update_editor(cx, |vim, editor, cx| {
+                        let is_relative = vim.mode != Mode::Insert;
+                        editor.set_relative_line_number(Some(is_relative), cx)
+                    });
+                }
+            }
+        }
+        Vim::globals(cx).focused_vim = Some(cx.view().downgrade());
     }
 
     fn blurred(&mut self, cx: &mut ViewContext<Self>) {
@@ -1039,6 +1082,7 @@ pub enum UseSystemClipboard {
 
 #[derive(Deserialize)]
 struct VimSettings {
+    pub toggle_relative_line_numbers: bool,
     pub use_system_clipboard: UseSystemClipboard,
     pub use_multiline_find: bool,
     pub use_smartcase_find: bool,
@@ -1047,6 +1091,7 @@ struct VimSettings {
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 struct VimSettingsContent {
+    pub toggle_relative_line_numbers: Option<bool>,
     pub use_system_clipboard: Option<UseSystemClipboard>,
     pub use_multiline_find: Option<bool>,
     pub use_smartcase_find: Option<bool>,

docs/src/vim.md 🔗

@@ -241,8 +241,17 @@ Some vim settings are available to modify the default vim behavior:
     // "never": don't use system clipboard unless "+ or "* is specified
     // "on_yank": use system clipboard for yank operations when no register is specified
     "use_system_clipboard": "always",
-    // Lets `f` and `t` motions extend across multiple lines
-    "use_multiline_find": true
+    // Let `f` and `t` motions extend across multiple lines
+    "use_multiline_find": true,
+    // Let `f` and `t` motions match case insensitively if the target is lowercase
+    "use_smartcase_find": true,
+    // Use relative line numbers in normal mode, absolute in insert mode
+    // c.f. https://github.com/jeffkreeftmeijer/vim-numbertoggle
+    "toggle_relative_line_numbers": true,
+    // Add custom digraphs (e.g. ctrl-k f z will insert a zombie emoji)
+    "custom_digraphs": {
+      "fz": "🧟‍♀️"
+    }
   }
 }
 ```