mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
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:
parent
90c8629077
commit
aa5e2aef46
5 changed files with 152 additions and 13 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, |_| {});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue