diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 94f49f601a..9971fcc66a 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -1,6 +1,7 @@ use crate::{ Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase, + display_map::InlayOffset, editor_settings::GoToDefinitionFallback, hover_popover::{self, InlayHover}, scroll::ScrollAmount, @@ -15,6 +16,7 @@ use project::{ }; use settings::Settings; use std::ops::Range; +use text; use theme::ActiveTheme as _; use util::{ResultExt, TryFutureExt as _, maybe}; @@ -121,13 +123,25 @@ impl Editor { cx: &mut Context, ) { let hovered_link_modifier = Editor::multi_cursor_modifier(false, &modifiers, cx); - if !hovered_link_modifier || self.has_pending_selection() { + + // When you're dragging to select, and you release the drag to create the selection, + // if you happened to end over something hoverable (including an inlay hint), don't + // have the hovered link appear. That would be annoying, because all you're trying + // to do is to create a selection, not hover to see a hovered link. + if self.has_pending_selection() { self.hide_hovered_link(cx); return; } match point_for_position.as_valid() { Some(point) => { + // Hide the underline unless you're holding the modifier key on the keyboard + // which will perform a goto definition. + if !hovered_link_modifier { + self.hide_hovered_link(cx); + return; + } + let trigger_point = TriggerPoint::Text( snapshot .buffer_snapshot @@ -319,6 +333,7 @@ pub fn update_inlay_link_and_hover_points( let inlay_hint_cache = editor.inlay_hint_cache(); let excerpt_id = previous_valid_anchor.excerpt_id; if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { + // Check if we should process this hint for hover match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { if let Some(buffer_id) = snapshot @@ -419,31 +434,46 @@ pub fn update_inlay_link_and_hover_points( ); hover_updated = true; } + if let Some((language_server_id, location)) = hovered_hint_part.location - && secondary_held - && !editor.has_pending_nonempty_selection() { - go_to_definition_updated = true; - show_link_definition( - shift_held, - editor, - TriggerPoint::InlayHint( - highlight, - location, - language_server_id, - ), - snapshot, - window, - cx, - ); + // Now perform the "Go to Definition" flow to get hover documentation + if let Some(project) = editor.project.clone() { + let highlight = highlight.clone(); + let hint_value = hovered_hint_part.value.clone(); + let location = location.clone(); + + get_docs_then_show_hover( + window, cx, highlight, hint_value, location, + project, + ); + } + + if secondary_held + && !editor.has_pending_nonempty_selection() + { + go_to_definition_updated = true; + show_link_definition( + shift_held, + editor, + TriggerPoint::InlayHint( + highlight, + location, + language_server_id, + ), + snapshot, + window, + cx, + ); + } } } } - }; + } } ResolveState::Resolving => {} - } + }; } } } @@ -456,6 +486,145 @@ pub fn update_inlay_link_and_hover_points( } } +fn get_docs_then_show_hover( + window: &mut Window, + cx: &mut Context<'_, Editor>, + highlight: InlayHighlight, + hint_value: String, + location: lsp::Location, + project: Entity, +) { + cx.spawn_in(window, async move |editor, cx| { + async move { + // 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 + let file_path = location + .uri + .to_file_path() + .map_err(|_| anyhow::anyhow!("Invalid file URL"))?; + + // Open the definition file + let definition_buffer = project + .update(cx, |project, cx| project.open_local_buffer(file_path, cx))? + .await?; + + // Extract documentation directly from the source + let documentation = definition_buffer.update(cx, |buffer, _| { + let line_number = location.range.start.line as usize; + + // Get the text of the buffer + let text = buffer.text(); + let lines: Vec<&str> = text.lines().collect(); + + // Look backwards from the definition line to find doc comments + let mut doc_lines = Vec::new(); + let mut current_line = line_number.saturating_sub(1); + + // Skip any attributes like #[derive(...)] + while current_line > 0 + && lines.get(current_line).map_or(false, |line| { + let trimmed = line.trim(); + trimmed.starts_with("#[") || trimmed.is_empty() + }) + { + current_line = current_line.saturating_sub(1); + } + + // Collect doc comments + while current_line > 0 { + if let Some(line) = lines.get(current_line) { + let trimmed = line.trim(); + if trimmed.starts_with("///") { + // Remove the /// and any leading space + let doc_text = trimmed + .strip_prefix("///") + .unwrap_or("") + .strip_prefix(" ") + .unwrap_or_else(|| trimmed.strip_prefix("///").unwrap_or("")); + doc_lines.push(doc_text.to_string()); + } else if !trimmed.is_empty() { + // Stop at the first non-doc, non-empty line + break; + } + } + current_line = current_line.saturating_sub(1); + } + + // Reverse to get correct order + doc_lines.reverse(); + + // Also get the actual definition line + let definition = lines + .get(line_number) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| hint_value.clone()); + + if doc_lines.is_empty() { + None + } else { + let docs = doc_lines.join("\n"); + Some((definition, docs)) + } + })?; + + if let Some((definition, docs)) = documentation { + // 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( + editor, + InlayHover { + tooltip: HoverBlock { + text: formatted_docs, + kind: HoverBlockKind::Markdown, + }, + range: highlight, + }, + window, + cx, + ); + }) + .log_err(); + } else { + // Fallback to showing just the location info + let fallback_text = format!( + "{}\n\nDefined in at line {}", + hint_value.trim(), + // filename, // TODO + location.range.start.line + 1 + ); + editor + .update_in(cx, |editor, window, cx| { + hover_popover::hover_at_inlay( + editor, + InlayHover { + tooltip: HoverBlock { + text: fallback_text, + kind: HoverBlockKind::PlainText, + }, + range: highlight, + }, + window, + cx, + ); + }) + .log_err(); + } + + anyhow::Ok(()) + } + .log_err() + .await + }) + .detach(); +} + pub fn show_link_definition( shift_held: bool, editor: &mut Editor,