Add hover tests

This commit is contained in:
Kirill Bulatov 2023-08-25 14:26:04 +03:00
parent f19c659ed6
commit e44516cc6c
4 changed files with 344 additions and 24 deletions

View File

@ -804,10 +804,17 @@ impl DiagnosticPopover {
#[cfg(test)]
mod tests {
use super::*;
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use crate::{
editor_tests::init_test,
element::PointForPosition,
inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
link_go_to_definition::update_inlay_link_and_hover_points,
test::editor_lsp_test_context::EditorLspTestContext,
};
use collections::BTreeSet;
use gpui::fonts::Weight;
use indoc::indoc;
use language::{Diagnostic, DiagnosticSet};
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
use lsp::LanguageServerId;
use project::{HoverBlock, HoverBlockKind};
use smol::stream::StreamExt;
@ -1243,4 +1250,311 @@ mod tests {
editor
});
}
#[gpui::test]
async fn test_hover_inlay_label_parts(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::Right(
lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
resolve_provider: Some(true),
..Default::default()
}),
)),
..Default::default()
},
cx,
)
.await;
cx.set_state(indoc! {"
struct TestStruct;
// ==================
struct TestNewType<T>(T);
fn main() {
let variableˇ = TestNewType(TestStruct);
}
"});
let hint_start_offset = cx.ranges(indoc! {"
struct TestStruct;
// ==================
struct TestNewType<T>(T);
fn main() {
let variableˇ = TestNewType(TestStruct);
}
"})[0]
.start;
let hint_position = cx.to_lsp(hint_start_offset);
let new_type_target_range = cx.lsp_range(indoc! {"
struct TestStruct;
// ==================
struct «TestNewType»<T>(T);
fn main() {
let variable = TestNewType(TestStruct);
}
"});
let struct_target_range = cx.lsp_range(indoc! {"
struct «TestStruct»;
// ==================
struct TestNewType<T>(T);
fn main() {
let variable = TestNewType(TestStruct);
}
"});
let uri = cx.buffer_lsp_url.clone();
let new_type_label = "TestNewType";
let struct_label = "TestStruct";
let entire_hint_label = ": TestNewType<TestStruct>";
let closure_uri = uri.clone();
cx.lsp
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let task_uri = closure_uri.clone();
async move {
assert_eq!(params.text_document.uri, task_uri);
Ok(Some(vec![lsp::InlayHint {
position: hint_position,
label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
value: entire_hint_label.to_string(),
..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![entire_hint_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;
// ==================
struct TestNewType<T>(T);
fn main() {
let variable« »= TestNewType(TestStruct);
}
"})
.get(0)
.cloned()
.unwrap();
let new_type_hint_part_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: (entire_hint_label.find(new_type_label).unwrap()
+ new_type_label.len() / 2)
as u32,
}
});
cx.update_editor(|editor, cx| {
update_inlay_link_and_hover_points(
&editor.snapshot(cx),
new_type_hint_part_hover_position,
editor,
true,
false,
cx,
);
});
let resolve_closure_uri = uri.clone();
cx.lsp
.handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
move |mut hint_to_resolve, _| {
let mut resolved_hint_positions = BTreeSet::new();
let task_uri = resolve_closure_uri.clone();
async move {
let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
// `: TestNewType<TestStruct>`
hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
lsp::InlayHintLabelPart {
value: ": ".to_string(),
..Default::default()
},
lsp::InlayHintLabelPart {
value: new_type_label.to_string(),
location: Some(lsp::Location {
uri: task_uri.clone(),
range: new_type_target_range,
}),
tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
"A tooltip for `{new_type_label}`"
))),
..Default::default()
},
lsp::InlayHintLabelPart {
value: "<".to_string(),
..Default::default()
},
lsp::InlayHintLabelPart {
value: struct_label.to_string(),
location: Some(lsp::Location {
uri: task_uri,
range: struct_target_range,
}),
tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: format!("A tooltip for `{struct_label}`"),
},
)),
..Default::default()
},
lsp::InlayHintLabelPart {
value: ">".to_string(),
..Default::default()
},
]);
Ok(hint_to_resolve)
}
},
)
.next()
.await;
cx.foreground().run_until_parked();
cx.update_editor(|editor, cx| {
update_inlay_link_and_hover_points(
&editor.snapshot(cx),
new_type_hint_part_hover_position,
editor,
true,
false,
cx,
);
});
cx.foreground()
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
cx.foreground().run_until_parked();
cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let hover_state = &editor.hover_state;
assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
let popover = hover_state.info_popover.as_ref().unwrap();
let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
let entire_inlay_start = snapshot.display_point_to_inlay_offset(
inlay_range.start.to_display_point(&snapshot),
Bias::Left,
);
let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len());
assert_eq!(
popover.symbol_range,
DocumentRange::Inlay(InlayRange {
inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
highlight_start: expected_new_type_label_start,
highlight_end: InlayOffset(
expected_new_type_label_start.0 + new_type_label.len()
),
}),
"Popover range should match the new type label part"
);
assert_eq!(
popover
.rendered_content
.as_ref()
.expect("should have label text for new type hint")
.text,
format!("A tooltip for `{new_type_label}`"),
"Rendered text should not anyhow alter backticks"
);
});
let struct_hint_part_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: (entire_hint_label.find(struct_label).unwrap()
+ struct_label.len() / 2)
as u32,
}
});
cx.update_editor(|editor, cx| {
update_inlay_link_and_hover_points(
&editor.snapshot(cx),
struct_hint_part_hover_position,
editor,
true,
false,
cx,
);
});
cx.foreground()
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
cx.foreground().run_until_parked();
cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let hover_state = &editor.hover_state;
assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
let popover = hover_state.info_popover.as_ref().unwrap();
let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
let entire_inlay_start = snapshot.display_point_to_inlay_offset(
inlay_range.start.to_display_point(&snapshot),
Bias::Left,
);
let expected_struct_label_start =
InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len());
assert_eq!(
popover.symbol_range,
DocumentRange::Inlay(InlayRange {
inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
highlight_start: expected_struct_label_start,
highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()),
}),
"Popover range should match the struct label part"
);
assert_eq!(
popover
.rendered_content
.as_ref()
.expect("should have label text for struct hint")
.text,
format!("A tooltip for {struct_label}"),
"Rendered markdown element should remove backticks from text"
);
});
}
}

View File

@ -41,7 +41,7 @@ pub enum TriggerPoint {
InlayHint(InlayRange, LocationLink),
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DocumentRange {
Text(Range<Anchor>),
Inlay(InlayRange),
@ -1096,7 +1096,7 @@ mod tests {
"});
let expected_uri = cx.buffer_lsp_url.clone();
let inlay_label = ": TestStruct";
let hint_label = ": TestStruct";
cx.lsp
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let expected_uri = expected_uri.clone();
@ -1105,7 +1105,7 @@ mod tests {
Ok(Some(vec![lsp::InlayHint {
position: hint_position,
label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
value: inlay_label.to_string(),
value: hint_label.to_string(),
location: Some(lsp::Location {
uri: params.text_document.uri,
range: target_range,
@ -1125,7 +1125,7 @@ mod tests {
.await;
cx.foreground().run_until_parked();
cx.update_editor(|editor, cx| {
let expected_layers = vec![inlay_label.to_string()];
let expected_layers = vec![hint_label.to_string()];
assert_eq!(expected_layers, cached_hint_labels(editor));
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
});
@ -1147,7 +1147,7 @@ mod tests {
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,
column_overshoot_after_line_end: (hint_label.len() / 2) as u32,
}
});
// Press cmd to trigger highlight
@ -1185,7 +1185,7 @@ mod tests {
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()),
highlight_end: InlayOffset(expected_highlight_start.0 + hint_label.len()),
}];
assert_set_eq!(actual_ranges, expected_ranges);
});

View File

@ -17,7 +17,7 @@ use language::{
CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16,
Transaction, Unclipped,
};
use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities};
use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities};
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
@ -2213,6 +2213,22 @@ impl InlayHints {
},
}
}
pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool {
capabilities
.inlay_hint_provider
.as_ref()
.and_then(|options| match options {
OneOf::Left(_is_supported) => None,
OneOf::Right(capabilities) => match capabilities {
lsp::InlayHintServerCapabilities::Options(o) => o.resolve_provider,
lsp::InlayHintServerCapabilities::RegistrationOptions(o) => {
o.inlay_hint_options.resolve_provider
}
},
})
.unwrap_or(false)
}
}
#[async_trait(?Send)]
@ -2269,14 +2285,10 @@ impl LspCommand for InlayHints {
lsp_adapter.name.0.as_ref() == "typescript-language-server";
let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| {
let resolve_state = match lsp_server.capabilities().inlay_hint_provider {
Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(
lsp::InlayHintOptions {
resolve_provider: Some(true),
..
},
))) => ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()),
_ => ResolveState::Resolved,
let resolve_state = if InlayHints::can_resolve_inlays(lsp_server.capabilities()) {
ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone())
} else {
ResolveState::Resolved
};
let project = project.clone();

View File

@ -5043,13 +5043,7 @@ impl Project {
} else {
return Task::ready(Ok(hint));
};
let can_resolve = lang_server
.capabilities()
.completion_provider
.as_ref()
.and_then(|options| options.resolve_provider)
.unwrap_or(false);
if !can_resolve {
if !InlayHints::can_resolve_inlays(lang_server.capabilities()) {
return Task::ready(Ok(hint));
}