From e44516cc6c8197f401934394d939d89fd414a634 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:26:04 +0300 Subject: [PATCH] Add hover tests --- crates/editor/src/hover_popover.rs | 318 ++++++++++++++++++++- crates/editor/src/link_go_to_definition.rs | 12 +- crates/project/src/lsp_command.rs | 30 +- crates/project/src/project.rs | 8 +- 4 files changed, 344 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6eae470badc812f7d4889e6ff705244b46ae2300..19020b643ace141a38812cb95c1a462cbf1feadb 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -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); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "}); + + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(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); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + let struct_target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + // ================== + + struct TestNewType(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"; + let closure_uri = uri.clone(); + cx.lsp + .handle_request::(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); + + 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::( + 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` + 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" + ); + }); + } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ea22ea5eae15ee686067d4f1119cb6edd0a5ac0a..926c0d6ddeb588bf133d032e9492c2249e9711eb 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -41,7 +41,7 @@ pub enum TriggerPoint { InlayHint(InlayRange, LocationLink), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum DocumentRange { Text(Range), 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::(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); }); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 9f7799c555940607a78b4faedd63bf89b16ddada..292f9a5226dfb4897c4faa94ab35e6108d4ec136 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -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(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0bbb61dfcb44065b105ac331a414f54ae12ed17d..c7765bf55a70b4829cf43575b7679db94ff44065 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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)); }