From aa5e2aef46034b2954affb7c39a11aac7bf4180b Mon Sep 17 00:00:00 2001 From: Ramon <55579979+van-sprundel@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:29:18 +0200 Subject: [PATCH] Fix inlay hint cursor (#54048) https://github.com/user-attachments/assets/e7a7903b-e133-4fbf-9267-3ebb17f867ff Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #43132 Release Notes: - Fixed inlay hints navigating to the wrong position --- crates/editor/src/display_map.rs | 15 ++++ crates/editor/src/display_map/inlay_map.rs | 9 +++ crates/editor/src/editor_tests.rs | 90 ++++++++++++++++++++++ crates/editor/src/element.rs | 49 ++++++++---- crates/editor/src/hover_popover.rs | 2 + 5 files changed, 152 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 402a293aed4db99b38f080bc6c85880637adbc4f..d1f8428f3167d5ad9d77035a9336ea947ac1f2e4 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -2050,6 +2050,21 @@ impl DisplaySnapshot { DisplayPoint(self.block_snapshot.clip_point(point.0, bias)) } + pub fn inlay_bias_at(&self, point: DisplayPoint) -> Option { + let wrap_point = self.block_snapshot.to_wrap_point(point.0, Bias::Left); + let tab_point = self.block_snapshot.to_tab_point(wrap_point); + let (fold_point, _, _) = self + .block_snapshot + .tab_snapshot + .tab_point_to_fold_point(tab_point, Bias::Left); + let inlay_point = + fold_point.to_inlay_point(&self.block_snapshot.tab_snapshot.fold_snapshot); + self.block_snapshot + .tab_snapshot + .fold_snapshot + .inlay_bias_at_point(inlay_point) + } + pub fn clip_at_line_end(&self, display_point: DisplayPoint) -> DisplayPoint { let mut point = self.display_point_to_point(display_point, Bias::Left); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 698b58682d7ef7682094e7728f419348fd5d32d9..016417ceed56cae2ca3f6f6cb35eb69a79a4aa38 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1094,6 +1094,15 @@ impl InlaySnapshot { } } + pub fn inlay_bias_at_point(&self, point: InlayPoint) -> Option { + let mut cursor = self.transforms.cursor::>(()); + cursor.seek(&point, Bias::Left); + match cursor.item() { + Some(Transform::Inlay(inlay)) => Some(inlay.position.bias()), + _ => None, + } + } + #[ztracing::instrument(skip_all)] pub fn text_summary(&self) -> MBTextSummary { self.transforms.summary().output diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 66c97276c34221f11cc2ddbab338f49626714f89..b18f734b50b58396056ff62456ef2d76bed88276 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -31506,6 +31506,96 @@ async fn test_inlay_hints_request_timeout(cx: &mut TestAppContext) { .unwrap(); } +#[gpui::test] +async fn test_click_on_parameter_inlay_hint_places_cursor_correctly(cx: &mut TestAppContext) { + use crate::inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels}; + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.update(|_, cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, &|settings: &mut SettingsContent| { + settings.project.all_languages.defaults.inlay_hints = + Some(InlayHintSettingsContent { + enabled: Some(true), + show_parameter_hints: Some(true), + show_type_hints: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + ..Default::default() + }) + }); + }); + }); + + cx.set_state("fn foo(value: i32) {} fn main() { foo(ˇ42); }"); + + // Buffer: `fn foo(value: i32) {} fn main() { foo(42); }` + // The parameter hint "value:" appears before "42" + let hint_start_offset = cx.ranges("fn foo(value: i32) {} fn main() { foo(ˇ42); }")[0].start; + let hint_position = cx.to_lsp(MultiBufferOffset(hint_start_offset)); + let hint_label = "value:"; + let expected_uri = cx.buffer_lsp_url.clone(); + cx.lsp + .set_request_handler::(move |params, _| { + let expected_uri = expected_uri.clone(); + async move { + assert_eq!(params.text_document.uri, expected_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::String(hint_label.to_string()), + kind: Some(lsp::InlayHintKind::PARAMETER), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: Some(true), + data: None, + }])) + } + }) + .next() + .await; + cx.background_executor.run_until_parked(); + + cx.update_editor(|editor, _window, cx| { + let expected_labels = vec!["value: ".to_string()]; + assert_eq!(expected_labels, cached_hint_labels(editor, cx)); + assert_eq!(expected_labels, visible_hint_labels(editor, cx)); + }); + + // The cursor is at `4` in `42`. The parameter hint "value: " appears just + // before it in display space. We'll click a few characters to the left of + // the cursor position to land inside the inlay hint text. + let cursor_display_point = cx.update_editor(|editor, _window, cx| { + editor + .selections + .newest_display(&editor.display_snapshot(cx)) + .head() + }); + let cursor_pixel = cx.pixel_position_for(cursor_display_point); + let em_width = + cx.update_editor(|editor, _, _| editor.last_position_map.as_ref().unwrap().em_layout_width); + // Click 3 characters to the left of the cursor, which lands inside the + // "value: " inlay hint text. + let click_position = gpui::Point { + x: cursor_pixel.x - em_width * 3.0, + y: cursor_pixel.y, + }; + cx.simulate_click(click_position, Modifiers::none()); + cx.background_executor.run_until_parked(); + + // The cursor should be placed after the `(`, at the `4` in `42`, + // NOT before the `(`. + cx.assert_editor_state("fn foo(value: i32) {} fn main() { foo(ˇ42); }"); +} + #[gpui::test] async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 78e70b0ae20d595dbec062994bbcae409b1bdefa..993641a4a7b77b08ec0c18469d003ede7092d93d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -843,7 +843,7 @@ impl EditorElement { } } - let position = point_for_position.previous_valid; + let position = point_for_position.nearest_valid; if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) { editor.select( SelectPhase::BeginColumnar { @@ -898,7 +898,7 @@ impl EditorElement { { let point_for_position = position_map.point_for_position(event.position); editor.set_gutter_context_menu( - point_for_position.previous_valid.row(), + point_for_position.nearest_valid.row(), None, event.position, window, @@ -916,7 +916,7 @@ impl EditorElement { mouse_context_menu::deploy_context_menu( editor, Some(event.position), - point_for_position.previous_valid, + point_for_position.nearest_valid, window, cx, ); @@ -935,7 +935,7 @@ impl EditorElement { } let point_for_position = position_map.point_for_position(event.position); - let position = point_for_position.previous_valid; + let position = point_for_position.nearest_valid; editor.select( SelectPhase::BeginColumnar { @@ -977,7 +977,7 @@ impl EditorElement { if event.position == *click_position { editor.select( SelectPhase::Begin { - position: point_for_position.previous_valid, + position: point_for_position.nearest_valid, add: false, click_count: 1, // ready to drag state only occurs on click count 1 }, @@ -1001,7 +1001,7 @@ impl EditorElement { || cfg!(not(target_os = "macos")) && event.modifiers.control); editor.move_selection_on_drop( &selection.clone(), - point_for_position.previous_valid, + point_for_position.nearest_valid, is_cut, window, cx, @@ -1037,7 +1037,7 @@ impl EditorElement { if EditorSettings::get_global(cx).middle_click_paste { if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) { let point_for_position = position_map.point_for_position(event.position); - let position = point_for_position.previous_valid; + let position = point_for_position.nearest_valid; editor.select( SelectPhase::Begin { @@ -1166,7 +1166,7 @@ impl EditorElement { if !editor.has_pending_selection() { let drop_anchor = position_map .snapshot - .display_point_to_anchor(point_for_position.previous_valid, Bias::Left); + .display_point_to_anchor(point_for_position.nearest_valid, Bias::Left); match editor.selection_drag_state { SelectionDragState::Dragging { ref mut drop_cursor, @@ -1210,7 +1210,7 @@ impl EditorElement { editor.selection_drag_state = SelectionDragState::None; editor.select( SelectPhase::Begin { - position: click_point.previous_valid, + position: click_point.nearest_valid, add: false, click_count: 1, }, @@ -1219,7 +1219,7 @@ impl EditorElement { ); editor.select( SelectPhase::Update { - position: point_for_position.previous_valid, + position: point_for_position.nearest_valid, goal_column: point_for_position.exact_unclipped.column(), scroll_delta, }, @@ -1233,7 +1233,7 @@ impl EditorElement { } else { editor.select( SelectPhase::Update { - position: point_for_position.previous_valid, + position: point_for_position.nearest_valid, goal_column: point_for_position.exact_unclipped.column(), scroll_delta, }, @@ -1260,7 +1260,7 @@ impl EditorElement { editor.show_mouse_cursor(cx); let point_for_position = position_map.point_for_position(event.position); - let valid_point = point_for_position.previous_valid; + let valid_point = point_for_position.nearest_valid; // Update diff review drag state if we're dragging if editor.diff_review_drag_state.is_some() { @@ -6688,7 +6688,7 @@ impl EditorElement { let snapshot = editor.snapshot(window, cx); let anchor = snapshot .display_snapshot - .display_point_to_anchor(point_for_position.previous_valid, Bias::Left); + .display_point_to_anchor(point_for_position.nearest_valid, Bias::Left); editor.change_selections( SelectionEffects::scroll(Autoscroll::top_relative(line_index)), window, @@ -11902,6 +11902,7 @@ pub(crate) struct PositionMap { pub struct PointForPosition { pub previous_valid: DisplayPoint, pub next_valid: DisplayPoint, + pub nearest_valid: DisplayPoint, pub exact_unclipped: DisplayPoint, pub column_overshoot_after_line_end: u32, } @@ -11971,12 +11972,23 @@ impl PositionMap { let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left); let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right); + let nearest_valid = if previous_valid == next_valid { + previous_valid + } else { + match self.snapshot.inlay_bias_at(exact_unclipped) { + Some(Bias::Left) => next_valid, + Some(Bias::Right) => previous_valid, + None => previous_valid, + } + }; + let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_layout_width) as u32; *exact_unclipped.column_mut() += column_overshoot_after_line_end; PointForPosition { previous_valid, next_valid, + nearest_valid, exact_unclipped, column_overshoot_after_line_end, } @@ -12006,12 +12018,23 @@ impl PositionMap { let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left); let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right); + let nearest_valid = if previous_valid == next_valid { + previous_valid + } else { + match self.snapshot.inlay_bias_at(exact_unclipped) { + Some(Bias::Left) => next_valid, + Some(Bias::Right) => previous_valid, + None => previous_valid, + } + }; + let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_layout_width) as u32; *exact_unclipped.column_mut() += column_overshoot_after_line_end; PointForPosition { previous_valid, next_valid, + nearest_valid, exact_unclipped, column_overshoot_after_line_end, } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 21177ad27b5886739ce5f57421412226ae4b1123..90d57d478712fe20cf975f2726c282cc01dd70e0 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1952,6 +1952,7 @@ mod tests { PointForPosition { previous_valid, next_valid, + nearest_valid: previous_valid, exact_unclipped, column_overshoot_after_line_end: 0, } @@ -2079,6 +2080,7 @@ mod tests { PointForPosition { previous_valid, next_valid, + nearest_valid: previous_valid, exact_unclipped, column_overshoot_after_line_end: 0, }