Omit empty hovers (#9967)

Kirill Bulatov created

Closes https://github.com/zed-industries/zed/issues/9962

Release Notes:

- N/A

Change summary

crates/project/src/project.rs       | 18 +++++++
crates/project/src/project_tests.rs | 70 ++++++++++++++++++++++++++++++
2 files changed, 87 insertions(+), 1 deletion(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -5193,6 +5193,17 @@ impl Project {
         position: PointUtf16,
         cx: &mut ModelContext<Self>,
     ) -> Task<Vec<Hover>> {
+        fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
+            hover
+                .contents
+                .retain(|hover_block| !hover_block.text.trim().is_empty());
+            if hover.contents.is_empty() {
+                None
+            } else {
+                Some(hover)
+            }
+        }
+
         if self.is_local() {
             let snapshot = buffer.read(cx).snapshot();
             let offset = position.to_offset(&snapshot);
@@ -5225,7 +5236,11 @@ impl Project {
             cx.spawn(|_, _| async move {
                 let mut hovers = Vec::with_capacity(hover_responses.len());
                 while let Some(hover_response) = hover_responses.next().await {
-                    if let Some(hover) = hover_response.log_err().flatten() {
+                    if let Some(hover) = hover_response
+                        .log_err()
+                        .flatten()
+                        .and_then(remove_empty_hover_blocks)
+                    {
                         hovers.push(hover);
                     }
                 }
@@ -5243,6 +5258,7 @@ impl Project {
                     .await
                     .log_err()
                     .flatten()
+                    .and_then(remove_empty_hover_blocks)
                     .map(|hover| vec![hover])
                     .unwrap_or_default()
             })

crates/project/src/project_tests.rs 🔗

@@ -4556,6 +4556,76 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "a.ts": "a",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+
+    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+    language_registry.add(typescript_lang());
+    let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
+        "TypeScript",
+        FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..lsp::ServerCapabilities::default()
+            },
+            ..FakeLspAdapter::default()
+        },
+    );
+
+    let buffer = project
+        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
+        .await
+        .unwrap();
+    cx.executor().run_until_parked();
+
+    let fake_server = fake_language_servers
+        .next()
+        .await
+        .expect("failed to get the language server");
+
+    let mut request_handled =
+        fake_server.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Array(vec![
+                    lsp::MarkedString::String("".to_string()),
+                    lsp::MarkedString::String("      ".to_string()),
+                    lsp::MarkedString::String("\n\n\n".to_string()),
+                ]),
+                range: None,
+            }))
+        });
+
+    let hover_task = project.update(cx, |project, cx| {
+        project.hover(&buffer, Point::new(0, 0), cx)
+    });
+    let () = request_handled
+        .next()
+        .await
+        .expect("All hover requests should have been triggered");
+    assert_eq!(
+        Vec::<String>::new(),
+        hover_task
+            .await
+            .into_iter()
+            .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
+            .sorted()
+            .collect::<Vec<_>>(),
+        "Empty hover parts should be ignored"
+    );
+}
+
 #[gpui::test]
 async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
     init_test(cx);