Add hover tests

Kirill Bulatov created

Change summary

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(-)

Detailed changes

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>(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"
+            );
+        });
+    }
 }
@@ -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);
         });

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();

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));
             }