diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 8d580087de..96c38e9ad5 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -337,15 +337,15 @@ pub fn update_inlay_link_and_hover_points( window, cx, ); - // Don't clear hover while resolution is starting - hover_updated = true; + // 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 clear hover while resolving - hover_updated = true; + // Don't set hover_updated during resolution to prevent empty tooltip + // hover_updated = true; false // Don't process further } }; @@ -448,195 +448,159 @@ pub fn update_inlay_link_and_hover_points( .unwrap_or("unknown") .to_string(); - // Prepare data needed for the async task - let project = editor.project.clone().unwrap(); - let hint_value = part.value.clone(); - let highlight = highlight.clone(); - let filename = filename.clone(); + hover_popover::hover_at_inlay( + editor, + InlayHover { + tooltip: HoverBlock { + text: "Loading documentation...".to_string(), + kind: HoverBlockKind::PlainText, + }, + range: highlight.clone(), + }, + window, + cx, + ); + hover_updated = true; - // Spawn async task to fetch documentation - cx.spawn_in(window, async move |editor, cx| { - // Convert LSP URL to file path - let file_path = location.uri.to_file_path() - .map_err(|_| anyhow::anyhow!("Invalid file URL"))?; + // 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 = part.value.clone(); + let location_uri = location.uri.clone(); - // Open the definition file - let definition_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer(file_path, cx) - })? - .await?; + cx.spawn_in(window, async move |editor, cx| { + async move { + eprintln!("Starting async documentation fetch for {}", hint_value); - // Register the buffer with language servers - let _lsp_handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&definition_buffer, cx) - })?; + // Small delay to show the loading message first + cx.background_executor() + .timer(std::time::Duration::from_millis(50)) + .await; - // Give LSP a moment to process the didOpen notification - cx.background_executor() - .timer(std::time::Duration::from_millis(100)) - .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); - // Try to get hover documentation from LSP - let hover_position = location.range.start; + // 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"); - // Convert LSP position to a point - let hover_point = definition_buffer.update(cx, |buffer, _| { - let point_utf16 = point_from_lsp(hover_position); - let snapshot = buffer.snapshot(); - let point = snapshot.clip_point_utf16(point_utf16, Bias::Left); - snapshot.anchor_after(point) - })?; + // 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); - let hover_response = project - .update(cx, |project, cx| { - project.hover(&definition_buffer, hover_point, cx) - })? - .await; + // Get the text of the buffer + let text = buffer.text(); + let lines: Vec<&str> = text.lines().collect(); - 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(); + // 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); - // 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"); - } + // 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); } - } - 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, + // 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()); + + 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::()); + 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( + editor, + InlayHover { + tooltip: HoverBlock { + text: formatted_docs, + kind: HoverBlockKind::Markdown, + }, + range: highlight, }, - range: highlight, - }, - window, - cx, + window, + cx, + ); + }).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 {}", + hint_value.trim(), + filename, + location.range.start.line + 1 ); - }).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; - - // 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; - } + 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(); } - current_line = current_line.saturating_sub(1); + + eprintln!("Documentation fetch complete"); + anyhow::Ok(()) } - - // 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, - 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(()) - }).detach(); + .log_err() + .await + }).detach(); + } } if let Some((language_server_id, location)) = &part.location {