Add a hover when hovering over inlays

Richard Feldman created

Change summary

crates/editor/src/element.rs       |   4 
crates/editor/src/hover_links.rs   | 108 +++++++++++++++++++++++--------
crates/editor/src/hover_popover.rs |  14 +++
3 files changed, 94 insertions(+), 32 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -1238,8 +1238,8 @@ impl EditorElement {
                 hover_at(editor, Some(anchor), window, cx);
                 Self::update_visible_cursor(editor, point, position_map, window, cx);
             } else {
-                // Don't call hover_at with None when we're over an inlay
-                // The inlay hover is already handled by update_hovered_link
+                // When over an inlay or invalid position, clear any existing hover
+                hover_at(editor, None, window, cx);
             }
         } else {
             editor.hide_hovered_link(cx);

crates/editor/src/hover_links.rs 🔗

@@ -3,11 +3,11 @@ use crate::{
     GoToTypeDefinition, GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase,
     display_map::InlayOffset,
     editor_settings::GoToDefinitionFallback,
-    hover_popover::{self, InlayHover},
+    hover_popover::{self, HoverState, InlayHover},
     scroll::ScrollAmount,
 };
 use gpui::{App, AsyncWindowContext, Context, Entity, Modifiers, Task, Window, px};
-use language::{Bias, ToOffset, point_from_lsp};
+use language::{Bias, ToOffset};
 use linkify::{LinkFinder, LinkKind};
 use lsp::LanguageServerId;
 use project::{
@@ -18,6 +18,7 @@ use settings::Settings;
 use std::ops::Range;
 use text;
 use theme::ActiveTheme as _;
+
 use util::{ResultExt, TryFutureExt as _, maybe};
 
 #[derive(Debug)]
@@ -164,6 +165,7 @@ impl Editor {
     pub(crate) fn hide_hovered_link(&mut self, cx: &mut Context<Self>) {
         self.hovered_link_state.take();
         self.clear_highlights::<HoveredLinkState>(cx);
+        self.clear_background_highlights::<HoverState>(cx);
     }
 
     pub(crate) fn handle_click_hovered_link(
@@ -337,15 +339,11 @@ pub fn update_inlay_link_and_hover_points(
                                 window,
                                 cx,
                             );
-                            // Don't set hover_updated during resolution to prevent empty tooltip
-                            // hover_updated = true;
                         }
                         false // Don't process unresolved hints
                     }
                     ResolveState::Resolved => true,
                     ResolveState::Resolving => {
-                        // Don't set hover_updated during resolution to prevent empty tooltip
-                        // hover_updated = true;
                         false // Don't process further
                     }
                 };
@@ -466,36 +464,96 @@ pub fn update_inlay_link_and_hover_points(
                                         if let Some(project) = editor.project.clone() {
                                             let highlight = highlight.clone();
                                             let hint_value = part.value.clone();
-                                            let location_uri = location.uri.clone();
 
                                             cx.spawn_in(window, async move |editor, cx| {
                                                 async move {
-                                                    eprintln!("Starting async documentation fetch for {}", hint_value);
-
-                                                    // Small delay to show the loading message first
-                                                    cx.background_executor()
-                                                        .timer(std::time::Duration::from_millis(50))
-                                                        .await;
-
                                                     // Convert LSP URL to file path
-                                                    eprintln!("Converting LSP URI to file path: {}", location_uri);
                                                     let file_path = location.uri.to_file_path()
                                                         .map_err(|_| anyhow::anyhow!("Invalid file URL"))?;
-                                                    eprintln!("File path: {:?}", file_path);
 
                                                     // Open the definition file
-                                                    eprintln!("Opening definition file via project.open_local_buffer");
                                                     let definition_buffer = project
                                                         .update(cx, |project, cx| {
                                                             project.open_local_buffer(file_path, cx)
                                                         })?
                                                         .await?;
-                                                    eprintln!("Successfully opened definition buffer");
 
-                                                    // Extract documentation directly from the source
+                                                    // Register the buffer with language servers
+                                                    let _lsp_handle = project.update(cx, |project, cx| {
+                                                        project.register_buffer_with_language_servers(&definition_buffer, cx)
+                                                    })?;
+
+                                                    // Give LSP a moment to process the didOpen notification
+                                                    cx.background_executor()
+                                                        .timer(std::time::Duration::from_millis(100))
+                                                        .await;
+
+                                                    // Try to get hover documentation from LSP
+                                                    let hover_position = location.range.start;
+
+                                                    // Convert LSP position to a point
+                                                    let hover_point = definition_buffer.update(cx, |buffer, _| {
+                                                        let point_utf16 = language::point_from_lsp(hover_position);
+                                                        let snapshot = buffer.snapshot();
+                                                        let point = snapshot.clip_point_utf16(point_utf16, Bias::Left);
+                                                        snapshot.anchor_after(point)
+                                                    })?;
+
+                                                    let hover_response = project
+                                                        .update(cx, |project, cx| {
+                                                            project.hover(&definition_buffer, hover_point, cx)
+                                                        })?
+                                                        .await;
+
+                                                    if !hover_response.is_empty() {
+                                                        // Get the first hover response
+                                                        let hover = &hover_response[0];
+                                                        if !hover.contents.is_empty() {
+                                                            // Format the hover blocks as markdown
+                                                            let mut formatted_docs = String::new();
+
+                                                            // Add the type signature first
+                                                            formatted_docs.push_str(&format!("```rust\n{}\n```\n\n", hint_value.trim()));
+
+                                                            // Add all the hover content
+                                                            for block in &hover.contents {
+                                                                match &block.kind {
+                                                                    HoverBlockKind::Markdown => {
+                                                                        formatted_docs.push_str(&block.text);
+                                                                        formatted_docs.push_str("\n\n");
+                                                                    }
+                                                                    HoverBlockKind::Code { language } => {
+                                                                        formatted_docs.push_str(&format!("```{}\n{}\n```\n\n", language, block.text));
+                                                                    }
+                                                                    HoverBlockKind::PlainText => {
+                                                                        formatted_docs.push_str(&block.text);
+                                                                        formatted_docs.push_str("\n\n");
+                                                                    }
+                                                                }
+                                                            }
+
+                                                            editor.update_in(cx, |editor, window, cx| {
+                                                                hover_popover::hover_at_inlay(
+                                                                    editor,
+                                                                    InlayHover {
+                                                                        tooltip: HoverBlock {
+                                                                            text: formatted_docs.trim().to_string(),
+                                                                            kind: HoverBlockKind::Markdown,
+                                                                        },
+                                                                        range: highlight,
+                                                                    },
+                                                                    window,
+                                                                    cx,
+                                                                );
+                                                            }).log_err();
+
+                                                            return Ok(());
+                                                        }
+                                                    }
+
+                                                    // Fallback: Extract documentation directly from the source
                                                     let documentation = definition_buffer.update(cx, |buffer, _| {
                                                         let line_number = location.range.start.line as usize;
-                                                        eprintln!("Looking for documentation at line {}", line_number);
 
                                                         // Get the text of the buffer
                                                         let text = buffer.text();
@@ -538,25 +596,20 @@ pub fn update_inlay_link_and_hover_points(
                                                             .map(|s| s.trim().to_string())
                                                             .unwrap_or_else(|| hint_value.clone());
 
-                                                        eprintln!("Found {} doc lines", doc_lines.len());
-
                                                         if doc_lines.is_empty() {
                                                             None
                                                         } else {
                                                             let docs = doc_lines.join("\n");
-                                                            eprintln!("Extracted docs: {}", docs.chars().take(100).collect::<String>());
                                                             Some((definition, docs))
                                                         }
                                                     })?;
 
                                                     if let Some((definition, docs)) = documentation {
-                                                        eprintln!("Got documentation from source!");
-
                                                         // Format as markdown with the definition as a code block
                                                         let formatted_docs = format!("```rust\n{}\n```\n\n{}", definition, docs);
 
                                                         editor.update_in(cx, |editor, window, cx| {
-                                                            hover_popover::hover_at_inlay(
+                                                                hover_popover::hover_at_inlay(
                                                                 editor,
                                                                 InlayHover {
                                                                     tooltip: HoverBlock {
@@ -570,7 +623,6 @@ pub fn update_inlay_link_and_hover_points(
                                                             );
                                                         }).log_err();
                                                     } else {
-                                                        eprintln!("No documentation found in source, falling back to location info");
                                                         // Fallback to showing just the location info
                                                         let fallback_text = format!(
                                                             "{}\n\nDefined in {} at line {}",
@@ -594,7 +646,6 @@ pub fn update_inlay_link_and_hover_points(
                                                         }).log_err();
                                                     }
 
-                                                    eprintln!("Documentation fetch complete");
                                                     anyhow::Ok(())
                                                 }
                                                 .log_err()
@@ -641,6 +692,7 @@ pub fn update_inlay_link_and_hover_points(
     }
     if !hover_updated {
         hover_popover::hover_at(editor, None, window, cx);
+        editor.clear_background_highlights::<HoverState>(cx);
     }
 }
 

crates/editor/src/hover_popover.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     ActiveDiagnostic, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings,
-    EditorSnapshot, GlobalDiagnosticRenderer, Hover,
+    EditorSnapshot, GlobalDiagnosticRenderer, HighlightStyle, Hover,
     display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible},
     hover_links::{InlayHighlight, RangeInEditor},
     scroll::ScrollAmount,
@@ -193,7 +193,17 @@ pub fn hover_at_inlay(
                 };
 
                 this.update(cx, |this, cx| {
-                    // TODO: no background highlights happen for inlays currently
+                    // Highlight the inlay using background highlighting
+                    let highlight_range = inlay_hover.range.clone();
+                    this.highlight_inlays::<HoverState>(
+                        vec![highlight_range],
+                        HighlightStyle {
+                            background_color: Some(cx.theme().colors().element_hover),
+                            ..Default::default()
+                        },
+                        cx,
+                    );
+
                     this.hover_state.info_popovers = vec![hover_popover];
                     cx.notify();
                 })?;