Add link_go_to_definition test for inlays

This commit is contained in:
Kirill Bulatov 2023-08-25 11:45:07 +03:00
parent abd2d012b1
commit f19c659ed6
4 changed files with 447 additions and 235 deletions

View File

@ -4,16 +4,16 @@ use super::{
MAX_LINE_LEN,
};
use crate::{
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayOffset, TransformBlock},
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
editor_settings::ShowScrollbar,
git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{
hide_hover, hover_at, hover_at_inlay, InlayHover, HOVER_POPOVER_GAP,
MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
MIN_POPOVER_LINE_HEIGHT,
},
link_go_to_definition::{
go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
GoToDefinitionTrigger, InlayRange,
update_inlay_link_and_hover_points, GoToDefinitionTrigger,
},
mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
};
@ -43,8 +43,7 @@ use language::{
};
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
HoverBlock, HoverBlockKind, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip,
Location, LocationLink, ProjectPath, ResolveState,
ProjectPath,
};
use smallvec::SmallVec;
use std::{
@ -478,10 +477,11 @@ impl EditorElement {
}
None => {
update_inlay_link_and_hover_points(
position_map,
&position_map.snapshot,
point_for_position,
editor,
(cmd, shift),
cmd,
shift,
cx,
);
}
@ -1835,214 +1835,6 @@ impl EditorElement {
}
}
fn update_inlay_link_and_hover_points(
position_map: &PositionMap,
point_for_position: PointForPosition,
editor: &mut Editor,
(cmd_held, shift_held): (bool, bool),
cx: &mut ViewContext<'_, '_, Editor>,
) {
let hint_start_offset = position_map
.snapshot
.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left);
let hint_end_offset = position_map
.snapshot
.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right);
let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize;
let hovered_offset = if offset_overshoot == 0 {
Some(
position_map
.snapshot
.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left),
)
} else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot {
Some(InlayOffset(hint_start_offset.0 + offset_overshoot))
} else {
None
};
if let Some(hovered_offset) = hovered_offset {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let previous_valid_anchor = snapshot.anchor_at(
point_for_position
.previous_valid
.to_point(&position_map.snapshot.display_snapshot),
Bias::Left,
);
let next_valid_anchor = snapshot.anchor_at(
point_for_position
.next_valid
.to_point(&position_map.snapshot.display_snapshot),
Bias::Right,
);
let mut go_to_definition_updated = false;
let mut hover_updated = false;
if let Some(hovered_hint) = editor
.visible_inlay_hints(cx)
.into_iter()
.skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt())
.take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le())
.max_by_key(|hint| hint.id)
{
let inlay_hint_cache = editor.inlay_hint_cache();
let excerpt_id = previous_valid_anchor.excerpt_id;
if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
match cached_hint.resolve_state {
ResolveState::CanResolve(_, _) => {
if let Some(buffer_id) = previous_valid_anchor.buffer_id {
inlay_hint_cache.spawn_hint_resolve(
buffer_id,
excerpt_id,
hovered_hint.id,
cx,
);
}
}
ResolveState::Resolved => {
match cached_hint.label {
project::InlayHintLabel::String(_) => {
if let Some(tooltip) = cached_hint.tooltip {
hover_at_inlay(
editor,
InlayHover {
excerpt: excerpt_id,
tooltip: match tooltip {
InlayHintTooltip::String(text) => HoverBlock {
text,
kind: HoverBlockKind::PlainText,
},
InlayHintTooltip::MarkupContent(content) => {
HoverBlock {
text: content.value,
kind: content.kind,
}
}
},
triggered_from: hovered_offset,
range: InlayRange {
inlay_position: hovered_hint.position,
highlight_start: hint_start_offset,
highlight_end: hint_end_offset,
},
},
cx,
);
hover_updated = true;
}
}
project::InlayHintLabel::LabelParts(label_parts) => {
if let Some((hovered_hint_part, part_range)) =
find_hovered_hint_part(
label_parts,
hint_start_offset..hint_end_offset,
hovered_offset,
)
{
if let Some(tooltip) = hovered_hint_part.tooltip {
hover_at_inlay(
editor,
InlayHover {
excerpt: excerpt_id,
tooltip: match tooltip {
InlayHintLabelPartTooltip::String(text) => {
HoverBlock {
text,
kind: HoverBlockKind::PlainText,
}
}
InlayHintLabelPartTooltip::MarkupContent(
content,
) => HoverBlock {
text: content.value,
kind: content.kind,
},
},
triggered_from: hovered_offset,
range: InlayRange {
inlay_position: hovered_hint.position,
highlight_start: part_range.start,
highlight_end: part_range.end,
},
},
cx,
);
hover_updated = true;
}
if let Some(location) = hovered_hint_part.location {
if let Some(buffer) =
cached_hint.position.buffer_id.and_then(|buffer_id| {
editor.buffer().read(cx).buffer(buffer_id)
})
{
go_to_definition_updated = true;
update_go_to_definition_link(
editor,
GoToDefinitionTrigger::InlayHint(
InlayRange {
inlay_position: hovered_hint.position,
highlight_start: part_range.start,
highlight_end: part_range.end,
},
LocationLink {
origin: Some(Location {
buffer,
range: cached_hint.position
..cached_hint.position,
}),
target: location,
},
),
cmd_held,
shift_held,
cx,
);
}
}
}
}
};
}
ResolveState::Resolving => {}
}
}
}
if !go_to_definition_updated {
update_go_to_definition_link(
editor,
GoToDefinitionTrigger::None,
cmd_held,
shift_held,
cx,
);
}
if !hover_updated {
hover_at(editor, None, cx);
}
}
}
fn find_hovered_hint_part(
label_parts: Vec<InlayHintLabelPart>,
hint_range: Range<InlayOffset>,
hovered_offset: InlayOffset,
) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end {
let mut hovered_character = (hovered_offset - hint_range.start).0;
let mut part_start = hint_range.start;
for part in label_parts {
let part_len = part.value.chars().count();
if hovered_character >= part_len {
hovered_character -= part_len;
part_start.0 += part_len;
} else {
return Some((part, part_start..InlayOffset(part_start.0 + part_len)));
}
}
}
None
}
struct HighlightedChunk<'a> {
chunk: &'a str,
style: Option<HighlightStyle>,
@ -2871,12 +2663,12 @@ struct PositionMap {
snapshot: EditorSnapshot,
}
#[derive(Debug)]
#[derive(Debug, Copy, Clone)]
pub struct PointForPosition {
previous_valid: DisplayPoint,
pub previous_valid: DisplayPoint,
pub next_valid: DisplayPoint,
exact_unclipped: DisplayPoint,
column_overshoot_after_line_end: u32,
pub exact_unclipped: DisplayPoint,
pub column_overshoot_after_line_end: u32,
}
impl PointForPosition {

View File

@ -13,7 +13,7 @@ use gpui::{
AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
};
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
use project::{HoverBlock, HoverBlockKind, Project};
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
use std::{ops::Range, sync::Arc, time::Duration};
use util::TryFutureExt;
@ -55,6 +55,27 @@ pub struct InlayHover {
pub tooltip: HoverBlock,
}
pub fn find_hovered_hint_part(
label_parts: Vec<InlayHintLabelPart>,
hint_range: Range<InlayOffset>,
hovered_offset: InlayOffset,
) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end {
let mut hovered_character = (hovered_offset - hint_range.start).0;
let mut part_start = hint_range.start;
for part in label_parts {
let part_len = part.value.chars().count();
if hovered_character >= part_len {
hovered_character -= part_len;
part_start.0 += part_len;
} else {
return Some((part, part_start..InlayOffset(part_start.0 + part_len)));
}
}
}
None
}
pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
if settings::get::<EditorSettings>(cx).hover_popover_enabled {
if editor.pending_rename.is_some() {

View File

@ -904,7 +904,7 @@ fn apply_hint_update(
}
#[cfg(test)]
mod tests {
pub mod tests {
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use crate::{
@ -2989,15 +2989,11 @@ all hints should be invalidated and requeried for all of its visible excerpts"
("/a/main.rs", editor, fake_server)
}
fn cached_hint_labels(editor: &Editor) -> Vec<String> {
pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
let mut labels = Vec::new();
for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
let excerpt_hints = excerpt_hints.read();
for (_, inlay) in excerpt_hints.hints.iter() {
match &inlay.label {
project::InlayHintLabel::String(s) => labels.push(s.to_string()),
_ => unreachable!(),
}
for (_, inlay) in &excerpt_hints.read().hints {
labels.push(inlay.text());
}
}
@ -3005,7 +3001,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
labels
}
fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
let mut hints = editor
.visible_inlay_hints(cx)
.into_iter()

View File

@ -1,10 +1,15 @@
use crate::{
display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor,
EditorSnapshot, SelectPhase,
display_map::{DisplaySnapshot, InlayOffset},
element::PointForPosition,
hover_popover::{self, InlayHover},
Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase,
};
use gpui::{Task, ViewContext};
use language::{Bias, ToOffset};
use project::LocationLink;
use project::{
HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, Location,
LocationLink, ResolveState,
};
use std::ops::Range;
use util::TryFutureExt;
@ -23,7 +28,7 @@ pub enum GoToDefinitionTrigger {
None,
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InlayRange {
pub inlay_position: Anchor,
pub highlight_start: InlayOffset,
@ -140,6 +145,192 @@ pub fn update_go_to_definition_link(
hide_link_definition(editor, cx);
}
pub fn update_inlay_link_and_hover_points(
snapshot: &DisplaySnapshot,
point_for_position: PointForPosition,
editor: &mut Editor,
cmd_held: bool,
shift_held: bool,
cx: &mut ViewContext<'_, '_, Editor>,
) {
let hint_start_offset =
snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left);
let hint_end_offset =
snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right);
let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize;
let hovered_offset = if offset_overshoot == 0 {
Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
} else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot {
Some(InlayOffset(hint_start_offset.0 + offset_overshoot))
} else {
None
};
if let Some(hovered_offset) = hovered_offset {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let previous_valid_anchor = buffer_snapshot.anchor_at(
point_for_position.previous_valid.to_point(snapshot),
Bias::Left,
);
let next_valid_anchor = buffer_snapshot.anchor_at(
point_for_position.next_valid.to_point(snapshot),
Bias::Right,
);
let mut go_to_definition_updated = false;
let mut hover_updated = false;
if let Some(hovered_hint) = editor
.visible_inlay_hints(cx)
.into_iter()
.skip_while(|hint| {
hint.position
.cmp(&previous_valid_anchor, &buffer_snapshot)
.is_lt()
})
.take_while(|hint| {
hint.position
.cmp(&next_valid_anchor, &buffer_snapshot)
.is_le()
})
.max_by_key(|hint| hint.id)
{
let inlay_hint_cache = editor.inlay_hint_cache();
let excerpt_id = previous_valid_anchor.excerpt_id;
if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
match cached_hint.resolve_state {
ResolveState::CanResolve(_, _) => {
if let Some(buffer_id) = previous_valid_anchor.buffer_id {
inlay_hint_cache.spawn_hint_resolve(
buffer_id,
excerpt_id,
hovered_hint.id,
cx,
);
}
}
ResolveState::Resolved => {
match cached_hint.label {
project::InlayHintLabel::String(_) => {
if let Some(tooltip) = cached_hint.tooltip {
hover_popover::hover_at_inlay(
editor,
InlayHover {
excerpt: excerpt_id,
tooltip: match tooltip {
InlayHintTooltip::String(text) => HoverBlock {
text,
kind: HoverBlockKind::PlainText,
},
InlayHintTooltip::MarkupContent(content) => {
HoverBlock {
text: content.value,
kind: content.kind,
}
}
},
triggered_from: hovered_offset,
range: InlayRange {
inlay_position: hovered_hint.position,
highlight_start: hint_start_offset,
highlight_end: hint_end_offset,
},
},
cx,
);
hover_updated = true;
}
}
project::InlayHintLabel::LabelParts(label_parts) => {
if let Some((hovered_hint_part, part_range)) =
hover_popover::find_hovered_hint_part(
label_parts,
hint_start_offset..hint_end_offset,
hovered_offset,
)
{
if let Some(tooltip) = hovered_hint_part.tooltip {
hover_popover::hover_at_inlay(
editor,
InlayHover {
excerpt: excerpt_id,
tooltip: match tooltip {
InlayHintLabelPartTooltip::String(text) => {
HoverBlock {
text,
kind: HoverBlockKind::PlainText,
}
}
InlayHintLabelPartTooltip::MarkupContent(
content,
) => HoverBlock {
text: content.value,
kind: content.kind,
},
},
triggered_from: hovered_offset,
range: InlayRange {
inlay_position: hovered_hint.position,
highlight_start: part_range.start,
highlight_end: part_range.end,
},
},
cx,
);
hover_updated = true;
}
if let Some(location) = hovered_hint_part.location {
if let Some(buffer) =
cached_hint.position.buffer_id.and_then(|buffer_id| {
editor.buffer().read(cx).buffer(buffer_id)
})
{
go_to_definition_updated = true;
update_go_to_definition_link(
editor,
GoToDefinitionTrigger::InlayHint(
InlayRange {
inlay_position: hovered_hint.position,
highlight_start: part_range.start,
highlight_end: part_range.end,
},
LocationLink {
origin: Some(Location {
buffer,
range: cached_hint.position
..cached_hint.position,
}),
target: location,
},
),
cmd_held,
shift_held,
cx,
);
}
}
}
}
};
}
ResolveState::Resolving => {}
}
}
}
if !go_to_definition_updated {
update_go_to_definition_link(
editor,
GoToDefinitionTrigger::None,
cmd_held,
shift_held,
cx,
);
}
if !hover_updated {
hover_popover::hover_at(editor, None, cx);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LinkDefinitionKind {
Symbol,
@ -391,14 +582,21 @@ fn go_to_fetched_definition_of_kind(
#[cfg(test)]
mod tests {
use super::*;
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use crate::{
display_map::ToDisplayPoint,
editor_tests::init_test,
inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
test::editor_lsp_test_context::EditorLspTestContext,
};
use futures::StreamExt;
use gpui::{
platform::{self, Modifiers, ModifiersChangedEvent},
View,
};
use indoc::indoc;
use language::language_settings::InlayHintSettings;
use lsp::request::{GotoDefinition, GotoTypeDefinition};
use util::assert_set_eq;
#[gpui::test]
async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
@ -853,4 +1051,209 @@ mod tests {
"});
cx.foreground().run_until_parked();
}
#[gpui::test]
async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_type_hints: true,
show_parameter_hints: true,
show_other_hints: true,
})
});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..Default::default()
},
cx,
)
.await;
cx.set_state(indoc! {"
struct TestStruct;
fn main() {
let variableˇ = TestStruct;
}
"});
let hint_start_offset = cx.ranges(indoc! {"
struct TestStruct;
fn main() {
let variableˇ = TestStruct;
}
"})[0]
.start;
let hint_position = cx.to_lsp(hint_start_offset);
let target_range = cx.lsp_range(indoc! {"
struct «TestStruct»;
fn main() {
let variable = TestStruct;
}
"});
let expected_uri = cx.buffer_lsp_url.clone();
let inlay_label = ": TestStruct";
cx.lsp
.handle_request::<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::LabelParts(vec![lsp::InlayHintLabelPart {
value: inlay_label.to_string(),
location: Some(lsp::Location {
uri: params.text_document.uri,
range: target_range,
}),
..Default::default()
}]),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: Some(false),
padding_right: Some(false),
data: None,
}]))
}
})
.next()
.await;
cx.foreground().run_until_parked();
cx.update_editor(|editor, cx| {
let expected_layers = vec![inlay_label.to_string()];
assert_eq!(expected_layers, cached_hint_labels(editor));
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
});
let inlay_range = cx
.ranges(indoc! {"
struct TestStruct;
fn main() {
let variable« »= TestStruct;
}
"})
.get(0)
.cloned()
.unwrap();
let hint_hover_position = cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
PointForPosition {
previous_valid: inlay_range.start.to_display_point(&snapshot),
next_valid: inlay_range.end.to_display_point(&snapshot),
exact_unclipped: inlay_range.end.to_display_point(&snapshot),
column_overshoot_after_line_end: (inlay_label.len() / 2) as u32,
}
});
// Press cmd to trigger highlight
cx.update_editor(|editor, cx| {
update_inlay_link_and_hover_points(
&editor.snapshot(cx),
hint_hover_position,
editor,
true,
false,
cx,
);
});
cx.foreground().run_until_parked();
cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let actual_ranges = snapshot
.highlight_ranges::<LinkGoToDefinitionState>()
.map(|ranges| ranges.as_ref().clone().1)
.unwrap_or_default()
.into_iter()
.map(|range| match range {
DocumentRange::Text(range) => {
panic!("Unexpected regular text selection range {range:?}")
}
DocumentRange::Inlay(inlay_range) => inlay_range,
})
.collect::<Vec<_>>();
let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
let expected_highlight_start = snapshot.display_point_to_inlay_offset(
inlay_range.start.to_display_point(&snapshot),
Bias::Left,
);
let expected_ranges = vec![InlayRange {
inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
highlight_start: expected_highlight_start,
highlight_end: InlayOffset(expected_highlight_start.0 + inlay_label.len()),
}];
assert_set_eq!(actual_ranges, expected_ranges);
});
// Unpress cmd causes highlight to go away
cx.update_editor(|editor, cx| {
editor.modifiers_changed(
&platform::ModifiersChangedEvent {
modifiers: Modifiers {
cmd: false,
..Default::default()
},
..Default::default()
},
cx,
);
});
// Assert no link highlights
cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let actual_ranges = snapshot
.highlight_ranges::<LinkGoToDefinitionState>()
.map(|ranges| ranges.as_ref().clone().1)
.unwrap_or_default()
.into_iter()
.map(|range| match range {
DocumentRange::Text(range) => {
panic!("Unexpected regular text selection range {range:?}")
}
DocumentRange::Inlay(inlay_range) => inlay_range,
})
.collect::<Vec<_>>();
assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
});
// Cmd+click without existing definition requests and jumps
cx.update_editor(|editor, cx| {
editor.modifiers_changed(
&platform::ModifiersChangedEvent {
modifiers: Modifiers {
cmd: true,
..Default::default()
},
..Default::default()
},
cx,
);
update_inlay_link_and_hover_points(
&editor.snapshot(cx),
hint_hover_position,
editor,
true,
false,
cx,
);
});
cx.foreground().run_until_parked();
cx.update_editor(|editor, cx| {
go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
});
cx.foreground().run_until_parked();
cx.assert_editor_state(indoc! {"
struct «TestStructˇ»;
fn main() {
let variable = TestStruct;
}
"});
}
}