vim: Fix clipping when navigating over inlay hints (#22813)

Thorsten Ball created

This fixes the issue described in this comment:
https://github.com/zed-industries/zed/pull/22439#issuecomment-2563896422

Essentially, we'd clip in the wrong direction when there were multi-line
inlay hints.

It also fixes inline completions for non-Zeta-providers showing up in
normal mode.

Release Notes:

- N/A

Change summary

crates/copilot/src/copilot_completion_provider.rs       |  4 
crates/editor/src/display_map.rs                        |  2 
crates/editor/src/display_map/inlay_map.rs              |  2 
crates/editor/src/editor.rs                             |  6 
crates/editor/src/inline_completion_tests.rs            |  4 
crates/inline_completion/src/inline_completion.rs       |  6 +
crates/supermaven/src/supermaven_completion_provider.rs |  4 
crates/vim/src/motion.rs                                | 47 +++++++++-
crates/vim/src/vim.rs                                   | 10 ++
crates/zeta/src/zeta.rs                                 |  4 
10 files changed, 76 insertions(+), 13 deletions(-)

Detailed changes

crates/editor/src/display_map.rs ๐Ÿ”—

@@ -42,7 +42,7 @@ use fold_map::{FoldMap, FoldSnapshot};
 use gpui::{
     AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
 };
-pub(crate) use inlay_map::Inlay;
+pub use inlay_map::Inlay;
 use inlay_map::{InlayMap, InlaySnapshot};
 pub use inlay_map::{InlayOffset, InlayPoint};
 use invisibles::{is_invisible, replacement};

crates/editor/src/editor.rs ๐Ÿ”—

@@ -258,7 +258,7 @@ pub fn render_parsed_markdown(
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub(crate) enum InlayId {
+pub enum InlayId {
     InlineCompletion(usize),
     Hint(usize),
 }
@@ -3592,7 +3592,7 @@ impl Editor {
         }
     }
 
-    fn splice_inlays(
+    pub fn splice_inlays(
         &self,
         to_remove: Vec<InlayId>,
         to_insert: Vec<Inlay>,
@@ -4883,7 +4883,7 @@ impl Editor {
         }
     }
 
-    fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
+    pub fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
         Some(self.inline_completion_provider.as_ref()?.provider.clone())
     }
 

crates/editor/src/inline_completion_tests.rs ๐Ÿ”—

@@ -325,6 +325,10 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
         false
     }
 
+    fn show_completions_in_normal_mode() -> bool {
+        false
+    }
+
     fn is_enabled(
         &self,
         _buffer: &gpui::Model<language::Buffer>,

crates/inline_completion/src/inline_completion.rs ๐Ÿ”—

@@ -21,6 +21,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
     fn name() -> &'static str;
     fn display_name() -> &'static str;
     fn show_completions_in_menu() -> bool;
+    fn show_completions_in_normal_mode() -> bool;
     fn is_enabled(
         &self,
         buffer: &Model<Buffer>,
@@ -61,6 +62,7 @@ pub trait InlineCompletionProviderHandle {
         cx: &AppContext,
     ) -> bool;
     fn show_completions_in_menu(&self) -> bool;
+    fn show_completions_in_normal_mode(&self) -> bool;
     fn refresh(
         &self,
         buffer: Model<Buffer>,
@@ -101,6 +103,10 @@ where
         T::show_completions_in_menu()
     }
 
+    fn show_completions_in_normal_mode(&self) -> bool {
+        T::show_completions_in_normal_mode()
+    }
+
     fn is_enabled(
         &self,
         buffer: &Model<Buffer>,

crates/supermaven/src/supermaven_completion_provider.rs ๐Ÿ”—

@@ -106,6 +106,10 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
         false
     }
 
+    fn show_completions_in_normal_mode() -> bool {
+        false
+    }
+
     fn is_enabled(&self, buffer: &Model<Buffer>, cursor_position: Anchor, cx: &AppContext) -> bool {
         if !self.supermaven.read(cx).is_enabled() {
             return false;

crates/vim/src/motion.rs ๐Ÿ”—

@@ -1206,6 +1206,7 @@ fn up_down_buffer_rows(
     times: isize,
     text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
+    let bias = if times < 0 { Bias::Left } else { Bias::Right };
     let start = map.display_point_to_fold_point(point, Bias::Left);
     let begin_folded_line = map.fold_point_to_display_point(
         map.fold_snapshot
@@ -1229,14 +1230,14 @@ fn up_down_buffer_rows(
 
     let mut begin_folded_line = map.fold_point_to_display_point(
         map.fold_snapshot
-            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
+            .clip_point(FoldPoint::new(new_row, 0), bias),
     );
 
     let mut i = 0;
     while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
         let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
         if map
-            .display_point_to_fold_point(next_folded_line, Bias::Right)
+            .display_point_to_fold_point(next_folded_line, bias)
             .row()
             == new_row
         {
@@ -1254,10 +1255,7 @@ fn up_down_buffer_rows(
     };
 
     (
-        map.clip_point(
-            DisplayPoint::new(begin_folded_line.row(), new_col),
-            Bias::Left,
-        ),
+        map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias),
         goal,
     )
 }
@@ -2484,7 +2482,11 @@ fn section_motion(
 #[cfg(test)]
 mod test {
 
-    use crate::test::NeovimBackedTestContext;
+    use crate::{
+        state::Mode,
+        test::{NeovimBackedTestContext, VimTestContext},
+    };
+    use editor::display_map::Inlay;
     use indoc::indoc;
 
     #[gpui::test]
@@ -3146,4 +3148,35 @@ mod test {
             }ห‡ยป
         "});
     }
+
+    #[gpui::test]
+    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.set_state(
+            indoc! {"
+            struct Foo {
+            ห‡
+            }
+        "},
+            Mode::Normal,
+        );
+
+        cx.update_editor(|editor, cx| {
+            let range = editor.selections.newest_anchor().range();
+            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
+            let inlay = Inlay::inline_completion(1, range.start, inlay_text);
+            editor.splice_inlays(vec![], vec![inlay], cx);
+        });
+
+        cx.simulate_keystrokes("j");
+        cx.assert_state(
+            indoc! {"
+            struct Foo {
+
+            ห‡}
+        "},
+            Mode::Normal,
+        );
+    }
 }

crates/vim/src/vim.rs ๐Ÿ”—

@@ -1196,7 +1196,15 @@ impl Vim {
             editor.set_input_enabled(vim.editor_input_enabled());
             editor.set_autoindent(vim.should_autoindent());
             editor.selections.line_mode = matches!(vim.mode, Mode::VisualLine);
-            editor.set_inline_completions_enabled(matches!(vim.mode, Mode::Insert | Mode::Replace));
+
+            let enable_inline_completions = match vim.mode {
+                Mode::Insert | Mode::Replace => true,
+                Mode::Normal => editor
+                    .inline_completion_provider()
+                    .map_or(false, |provider| provider.show_completions_in_normal_mode()),
+                _ => false,
+            };
+            editor.set_inline_completions_enabled(enable_inline_completions);
         });
         cx.notify()
     }

crates/zeta/src/zeta.rs ๐Ÿ”—

@@ -979,6 +979,10 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
         true
     }
 
+    fn show_completions_in_normal_mode() -> bool {
+        true
+    }
+
     fn is_enabled(
         &self,
         buffer: &Model<Buffer>,