This commit is contained in:
Richard Feldman 2025-08-26 21:09:11 +02:00 committed by GitHub
commit ea4907a769
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<Self>,
) {
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<Project>,
) {
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,