From 9a6e8a19b573a62ca74155aa77b2a343ed501ab6 Mon Sep 17 00:00:00 2001 From: Dino Date: Thu, 12 Jun 2025 23:17:15 -0700 Subject: [PATCH] vim: Add horizontal scrolling support in vim mode (#32558) Release Notes: - Added initial support for both `z l` and `z h` in vim mode These changes relate to #17219 but don't yet close the issue, as this Pull Request is simply adding support for horizontal scrolling in vim mode and actually moving the cursor to the correct column in the current row will be handled in a different Pull Request. Some notes on these changes: - 2 new default keybindings added to vim's keymap - `z l` which triggers the new `vim::ColumnRight` action - `z h` which triggers the new `vim::ColumnLeft` action - Introduced a new `ScrollAmount` variant, `ScrollAmount::Column(f32)` to represent horizontal scrolling - Replaced usage of `em_width` with `em_advance` to actually scroll by the width of the cursor, instead of the width of the character --------- Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 2 ++ crates/editor/src/element.rs | 12 +++++------- crates/editor/src/scroll.rs | 17 ++++++++++++++--- crates/editor/src/scroll/scroll_amount.rs | 21 +++++++++++++++++++++ crates/vim/src/normal/scroll.rs | 19 ++++++++++++++++++- 5 files changed, 60 insertions(+), 11 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index f2d50bd13f3052c2de03efb164d4c751b426bf8d..59c5dd9aa83a5e3966059ba9d1679ceaf6a93a0a 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -184,6 +184,8 @@ "z f": "editor::FoldSelectedRanges", "z shift-m": "editor::FoldAll", "z shift-r": "editor::UnfoldAll", + "z l": "vim::ColumnRight", + "z h": "vim::ColumnLeft", "shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }], "shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }], // Count support diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index fae6654af5614492fb9ddc6d3de0495234e5d731..48ec6c885eb65649ba27eef3907448c200ebcf73 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7739,8 +7739,7 @@ impl Element for EditorElement { let line_height = style.text.line_height_in_pixels(window.rem_size()); let em_width = window.text_system().em_width(font_id, font_size).unwrap(); let em_advance = window.text_system().em_advance(font_id, font_size).unwrap(); - - let glyph_grid_cell = size(em_width, line_height); + let glyph_grid_cell = size(em_advance, line_height); let gutter_dimensions = snapshot .gutter_dimensions( @@ -8299,7 +8298,7 @@ impl Element for EditorElement { MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row); let scroll_max = point( - ((scroll_width - editor_content_width) / em_width).max(0.0), + ((scroll_width - editor_content_width) / em_advance).max(0.0), max_scroll_top, ); @@ -8311,7 +8310,7 @@ impl Element for EditorElement { start_row, editor_content_width, scroll_width, - em_width, + em_advance, &line_layouts, cx, ) @@ -8326,10 +8325,9 @@ impl Element for EditorElement { }); let scroll_pixel_position = point( - scroll_position.x * em_width, + scroll_position.x * em_advance, scroll_position.y * line_height, ); - let indent_guides = self.layout_indent_guides( content_origin, text_hitbox.origin, @@ -9454,7 +9452,7 @@ impl PositionMap { let scroll_position = self.snapshot.scroll_position(); let position = position - text_bounds.origin; let y = position.y.max(px(0.)).min(self.size.height); - let x = position.x + (scroll_position.x * self.em_width); + let x = position.x + (scroll_position.x * self.em_advance); let row = ((y / self.line_height) + scroll_position.y) as u32; let (column, x_overshoot_after_line_end) = if let Some(line) = self diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index a8081b95bde9f52e07dd98d109d46d72971f6165..6cc483cb650d102ba3a8f569f9ac3e99cb95727c 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -669,12 +669,23 @@ impl Editor { return; } - let cur_position = self.scroll_position(cx); + let mut current_position = self.scroll_position(cx); let Some(visible_line_count) = self.visible_line_count() else { return; }; - let new_pos = cur_position + point(0., amount.lines(visible_line_count)); - self.set_scroll_position(new_pos, window, cx); + + // If the scroll position is currently at the left edge of the document + // (x == 0.0) and the intent is to scroll right, the gutter's margin + // should first be added to the current position, otherwise the cursor + // will end at the column position minus the margin, which looks off. + if current_position.x == 0.0 && amount.columns() > 0. { + if let Some(last_position_map) = &self.last_position_map { + current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance; + } + } + let new_position = + current_position + point(amount.columns(), amount.lines(visible_line_count)); + self.set_scroll_position(new_position, window, cx); } /// Returns an ordering. The newest selection is: diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index 0c0319b8212d43d3f5ab2f0bc9681cef3eba2137..bc9d4757f1d6b30192c5888e5a3d576ea34fec25 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -5,6 +5,8 @@ use ui::{Pixels, px}; pub enum ScrollDirection { Upwards, Downwards, + Rightwards, + Leftwards, } impl ScrollDirection { @@ -19,6 +21,8 @@ pub enum ScrollAmount { Line(f32), // Scroll N pages (positive is towards the end of the document) Page(f32), + // Scroll N columns (positive is towards the right of the document) + Column(f32), } impl ScrollAmount { @@ -32,6 +36,15 @@ impl ScrollAmount { } (visible_line_count * count).trunc() } + Self::Column(_count) => 0.0, + } + } + + pub fn columns(&self) -> f32 { + match self { + Self::Line(_count) => 0.0, + Self::Page(_count) => 0.0, + Self::Column(count) => *count, } } @@ -39,6 +52,12 @@ impl ScrollAmount { match self { ScrollAmount::Line(x) => px(line_height.0 * x), ScrollAmount::Page(x) => px(height.0 * x), + // This function seems to only be leveraged by the popover that is + // displayed by the editor when, for example, viewing a function's + // documentation. Right now that only supports vertical scrolling, + // so I'm leaving this at 0.0 for now to try and make it clear that + // this should not have an impact on that? + ScrollAmount::Column(_) => px(0.0), } } @@ -53,6 +72,8 @@ impl ScrollAmount { match self { Self::Line(amount) if amount.is_sign_positive() => ScrollDirection::Downwards, Self::Page(amount) if amount.is_sign_positive() => ScrollDirection::Downwards, + Self::Column(amount) if amount.is_sign_positive() => ScrollDirection::Rightwards, + Self::Column(amount) if amount.is_sign_negative() => ScrollDirection::Leftwards, _ => ScrollDirection::Upwards, } } diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 5f3c6073be55b537575507b7d0b0f537c28f7c3e..f227f982cbe522c61122e27e3ba3ae3413dbf3ca 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -10,7 +10,16 @@ use settings::Settings; actions!( vim, - [LineUp, LineDown, ScrollUp, ScrollDown, PageUp, PageDown] + [ + LineUp, + LineDown, + ColumnRight, + ColumnLeft, + ScrollUp, + ScrollDown, + PageUp, + PageDown + ] ); pub fn register(editor: &mut Editor, cx: &mut Context) { @@ -20,6 +29,14 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &LineUp, window, cx| { vim.scroll(false, window, cx, |c| ScrollAmount::Line(-c.unwrap_or(1.))) }); + Vim::action(editor, cx, |vim, _: &ColumnRight, window, cx| { + vim.scroll(false, window, cx, |c| ScrollAmount::Column(c.unwrap_or(1.))) + }); + Vim::action(editor, cx, |vim, _: &ColumnLeft, window, cx| { + vim.scroll(false, window, cx, |c| { + ScrollAmount::Column(-c.unwrap_or(1.)) + }) + }); Vim::action(editor, cx, |vim, _: &PageDown, window, cx| { vim.scroll(false, window, cx, |c| ScrollAmount::Page(c.unwrap_or(1.))) });