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
This commit is contained in:
Ramon 2026-04-21 12:29:18 +02:00 committed by GitHub
parent 90c8629077
commit aa5e2aef46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 152 additions and 13 deletions

View file

@ -2050,6 +2050,21 @@ impl DisplaySnapshot {
DisplayPoint(self.block_snapshot.clip_point(point.0, bias))
}
pub fn inlay_bias_at(&self, point: DisplayPoint) -> Option<Bias> {
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);

View file

@ -1094,6 +1094,15 @@ impl InlaySnapshot {
}
}
pub fn inlay_bias_at_point(&self, point: InlayPoint) -> Option<Bias> {
let mut cursor = self.transforms.cursor::<Dimensions<InlayPoint, Point>>(());
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

View file

@ -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::<lsp::request::InlayHintRequest, _, _>(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, |_| {});

View file

@ -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,
}

View file

@ -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,
}