crates/editor/src/hover_links.rs 🔗
@@ -287,601 +287,320 @@ pub fn update_inlay_link_and_hover_points(
window: &mut Window,
cx: &mut Context<Editor>,
) {
- let mut go_to_definition_updated = false;
- let mut hover_updated = false;
-
// For inlay hints, we need to use the exact position where the mouse is
// But we must clip it to valid bounds to avoid panics
let clipped_point = snapshot.clip_point(point_for_position.exact_unclipped, Bias::Left);
let hovered_offset = snapshot.display_point_to_inlay_offset(clipped_point, Bias::Left);
+ let mut go_to_definition_updated = false;
+ let mut hover_updated = false;
+
// Get all visible inlay hints
let visible_hints = editor.visible_inlay_hints(cx);
- if point_for_position.column_overshoot_after_line_end == 0 {
- let hovered_offset =
- snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left);
- let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
- let previous_valid_anchor = buffer_snapshot.anchor_at(
- point_for_position.previous_valid.to_point(snapshot),
- Bias::Left,
- );
- let next_valid_anchor = buffer_snapshot.anchor_at(
- point_for_position.next_valid.to_point(snapshot),
- Bias::Right,
- );
- if let Some(hovered_hint) = editor
- .visible_inlay_hints(cx)
- .into_iter()
- .skip_while(|hint| {
- hint.position
- .cmp(&previous_valid_anchor, &buffer_snapshot)
- .is_lt()
- })
- .take_while(|hint| {
- hint.position
- .cmp(&next_valid_anchor, &buffer_snapshot)
- .is_le()
- })
- .max_by_key(|hint| hint.id)
- {
- 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) {
- match cached_hint.resolve_state {
+ // Find if we're hovering over an inlay hint
+ if let Some(hovered_inlay) = visible_hints.into_iter().find(|inlay| {
+ // Only process hint inlays
+ if !matches!(inlay.id, InlayId::Hint(_)) {
+ return false;
+ }
+
+ // Check if the hovered position falls within this inlay's display range
+ let inlay_start = snapshot.anchor_to_inlay_offset(inlay.position);
+ let inlay_end = InlayOffset(inlay_start.0 + inlay.text.len());
+
+ hovered_offset >= inlay_start && hovered_offset < inlay_end
+ }) {
+ let inlay_hint_cache = editor.inlay_hint_cache();
+ let excerpt_id = hovered_inlay.position.excerpt_id;
+
+ // Extract the hint ID from the inlay
+ if let InlayId::Hint(_hint_id) = hovered_inlay.id {
+ if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_inlay.id) {
+ // Check if we should process this hint for hover
+ let should_process_hint = match cached_hint.resolve_state {
ResolveState::CanResolve(_, _) => {
- if let Some(buffer_id) = previous_valid_anchor.buffer_id {
+ // For unresolved hints, spawn resolution
+ if let Some(buffer_id) = hovered_inlay.position.buffer_id {
inlay_hint_cache.spawn_hint_resolve(
buffer_id,
excerpt_id,
- hovered_hint.id,
+ hovered_inlay.id,
window,
cx,
);
}
+ false // Don't process unresolved hints
+ }
+ ResolveState::Resolved => true,
+ ResolveState::Resolving => false,
+ };
- if cached_hint.resolve_state.is_resolved() {
- let mut extra_shift_left = 0;
- let mut extra_shift_right = 0;
- if cached_hint.padding_left {
- extra_shift_left += 1;
- extra_shift_right += 1;
- }
- if cached_hint.padding_right {
- extra_shift_right += 1;
+ if should_process_hint {
+ let mut extra_shift_left = 0;
+ let mut extra_shift_right = 0;
+ if cached_hint.padding_left {
+ extra_shift_left += 1;
+ extra_shift_right += 1;
+ }
+ if cached_hint.padding_right {
+ extra_shift_right += 1;
+ }
+ match cached_hint.label {
+ project::InlayHintLabel::String(_) => {
+ if let Some(tooltip) = cached_hint.tooltip {
+ hover_popover::hover_at_inlay(
+ editor,
+ InlayHover {
+ tooltip: match tooltip {
+ InlayHintTooltip::String(text) => HoverBlock {
+ text,
+ kind: HoverBlockKind::PlainText,
+ },
+ InlayHintTooltip::MarkupContent(content) => {
+ HoverBlock {
+ text: content.value,
+ kind: content.kind,
+ }
+ }
+ },
+ range: InlayHighlight {
+ inlay: hovered_inlay.id,
+ inlay_position: hovered_inlay.position,
+ range: extra_shift_left
+ ..hovered_inlay.text.len() + extra_shift_right,
+ },
+ },
+ window,
+ cx,
+ );
+ hover_updated = true;
}
- match cached_hint.label {
- project::InlayHintLabel::String(_) => {
- if let Some(tooltip) = cached_hint.tooltip {
+ }
+ project::InlayHintLabel::LabelParts(label_parts) => {
+ // Find the first part with actual hover information (tooltip or location)
+ let _hint_start =
+ snapshot.anchor_to_inlay_offset(hovered_inlay.position);
+ let mut part_offset = 0;
+
+ for part in label_parts {
+ let part_len = part.value.chars().count();
+
+ if part.tooltip.is_some() || part.location.is_some() {
+ // Found the meaningful part - show hover for it
+ let highlight_start = part_offset + extra_shift_left;
+ let highlight_end = part_offset + part_len + extra_shift_right;
+
+ let highlight = InlayHighlight {
+ inlay: hovered_inlay.id,
+ inlay_position: hovered_inlay.position,
+ range: highlight_start..highlight_end,
+ };
+
+ if let Some(tooltip) = part.tooltip {
hover_popover::hover_at_inlay(
editor,
InlayHover {
tooltip: match tooltip {
- InlayHintTooltip::String(text) => HoverBlock {
- text,
- kind: HoverBlockKind::PlainText,
- },
- InlayHintTooltip::MarkupContent(content) => {
+ InlayHintLabelPartTooltip::String(text) => {
HoverBlock {
- text: content.value,
- kind: content.kind,
+ text,
+ kind: HoverBlockKind::PlainText,
}
}
+ InlayHintLabelPartTooltip::MarkupContent(
+ content,
+ ) => HoverBlock {
+ text: content.value,
+ kind: content.kind,
+ },
},
- range: InlayHighlight {
- inlay: hovered_hint.id,
- inlay_position: hovered_hint.position,
- range: extra_shift_left
- ..hovered_hint.text.len()
- + extra_shift_right,
- },
+ range: highlight.clone(),
},
window,
cx,
);
hover_updated = true;
- }
- }
- project::InlayHintLabel::LabelParts(label_parts) => {
- // Find the first part with actual hover information (tooltip or location)
- let _hint_start =
- snapshot.anchor_to_inlay_offset(hovered_hint.position);
- let mut part_offset = 0;
-
- for part in label_parts {
- let part_len = part.value.chars().count();
-
- if part.tooltip.is_some() || part.location.is_some() {
- // Found the meaningful part - show hover for it
- let highlight_start = part_offset + extra_shift_left;
- let highlight_end =
- part_offset + part_len + extra_shift_right;
-
- let highlight = InlayHighlight {
- inlay: hovered_hint.id,
- inlay_position: hovered_hint.position,
- range: highlight_start..highlight_end,
- };
-
- if let Some(tooltip) = part.tooltip {
- hover_popover::hover_at_inlay(
- editor,
- InlayHover {
- tooltip: match tooltip {
- InlayHintLabelPartTooltip::String(text) => {
- HoverBlock {
- text,
- kind: HoverBlockKind::PlainText,
- }
- }
- InlayHintLabelPartTooltip::MarkupContent(
- content,
- ) => HoverBlock {
- text: content.value,
- kind: content.kind,
- },
- },
- range: highlight.clone(),
- },
- window,
- cx,
- );
- hover_updated = true;
- } else if let Some((_language_server_id, location)) =
- part.location.clone()
- {
- // When there's no tooltip but we have a location, perform a "Go to Definition" style operation
- let filename = location
- .uri
- .path()
- .split('/')
- .next_back()
- .unwrap_or("unknown")
- .to_string();
-
- 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;
-
- // 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();
-
- 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());
+ } else if let Some((_language_server_id, location)) =
+ part.location.clone()
+ {
+ // When there's no tooltip but we have a location, perform a "Go to Definition" style operation
+ let filename = location
+ .uri
+ .path()
+ .split('/')
+ .next_back()
+ .unwrap_or("unknown")
+ .to_string();
- 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(())
- }
- .log_err()
- .await
- }).detach();
- }
- }
-
- if let Some((language_server_id, location)) =
- &part.location
- {
- if secondary_held
- && !editor.has_pending_nonempty_selection()
- {
- go_to_definition_updated = true;
- show_link_definition(
- shift_held,
- editor,
- TriggerPoint::InlayHint(
- highlight,
- location.clone(),
- *language_server_id,
- ),
- snapshot,
- window,
- cx,
- );
- }
- }
-
- break;
- }
-
- part_offset += part_len;
- }
- }
- };
- }
- }
- ResolveState::Resolved => {
- let mut extra_shift_left = 0;
- let mut extra_shift_right = 0;
- if cached_hint.padding_left {
- extra_shift_left += 1;
- extra_shift_right += 1;
- }
- if cached_hint.padding_right {
- extra_shift_right += 1;
- }
- match cached_hint.label {
- project::InlayHintLabel::String(_) => {
- if let Some(tooltip) = cached_hint.tooltip {
- hover_popover::hover_at_inlay(
- editor,
- InlayHover {
- tooltip: match tooltip {
- InlayHintTooltip::String(text) => HoverBlock {
- text,
+ hover_popover::hover_at_inlay(
+ editor,
+ InlayHover {
+ tooltip: HoverBlock {
+ text: "Loading documentation...".to_string(),
kind: HoverBlockKind::PlainText,
},
- InlayHintTooltip::MarkupContent(content) => {
- HoverBlock {
- text: content.value,
- kind: content.kind,
- }
- }
- },
- range: InlayHighlight {
- inlay: hovered_hint.id,
- inlay_position: hovered_hint.position,
- range: extra_shift_left
- ..hovered_hint.text.len() + extra_shift_right,
+ range: highlight.clone(),
},
- },
- window,
- cx,
- );
- hover_updated = true;
- }
- }
- project::InlayHintLabel::LabelParts(label_parts) => {
- // VS Code shows hover for the meaningful part regardless of where you hover
- // Find the first part with actual hover information (tooltip or location)
- let _hint_start =
- snapshot.anchor_to_inlay_offset(hovered_hint.position);
- let mut part_offset = 0;
-
- for part in label_parts {
- let part_len = part.value.chars().count();
-
- if part.tooltip.is_some() || part.location.is_some() {
- // Found the meaningful part - show hover for it
- let highlight_start = part_offset + extra_shift_left;
- let highlight_end =
- part_offset + part_len + extra_shift_right;
-
- let highlight = InlayHighlight {
- inlay: hovered_hint.id,
- inlay_position: hovered_hint.position,
- range: highlight_start..highlight_end,
- };
-
- if let Some(tooltip) = part.tooltip {
- hover_popover::hover_at_inlay(
- editor,
- InlayHover {
- tooltip: match tooltip {
- InlayHintLabelPartTooltip::String(text) => {
- HoverBlock {
- text,
- kind: HoverBlockKind::PlainText,
- }
+ window,
+ cx,
+ );
+ hover_updated = true;
+
+ // 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();
+
+ 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);
}
- InlayHintLabelPartTooltip::MarkupContent(
- content,
- ) => HoverBlock {
- text: content.value,
- kind: content.kind,
- },
- },
- range: highlight.clone(),
- },
- window,
- cx,
- );
- hover_updated = true;
- } else if let Some((_language_server_id, location)) =
- part.location.clone()
- {
- // When there's no tooltip but we have a location, perform a "Go to Definition" style operation
- let filename = location
- .uri
- .path()
- .split('/')
- .next_back()
- .unwrap_or("unknown")
- .to_string();
-
- 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;
-
- // 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();
-
- 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;
- }
+ // 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);
}
+ current_line = current_line.saturating_sub(1);
+ }
- // Reverse to get correct order
- doc_lines.reverse();
+ // 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());
+ // 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();
+ if doc_lines.is_empty() {
+ None
} 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
+ 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,
);
- editor.update_in(cx, |editor, window, cx| {
- hover_popover::hover_at_inlay(
- editor,
- InlayHover {
- tooltip: HoverBlock {
- text: fallback_text,
- kind: HoverBlockKind::PlainText,
- },
- range: highlight,
+ }).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,
},
- window,
- cx,
- );
- }).log_err();
- }
-
- anyhow::Ok(())
+ range: highlight,
+ },
+ window,
+ cx,
+ );
+ }).log_err();
}
- .log_err()
- .await
- }).detach();
- }
+
+ anyhow::Ok(())
+ }
+ .log_err()
+ .await
+ }).detach();
}
+ }
- if let Some((language_server_id, location)) = &part.location
+ if let Some((language_server_id, location)) = &part.location {
+ if secondary_held
+ && !editor.has_pending_nonempty_selection()
{
- if secondary_held
- && !editor.has_pending_nonempty_selection()
- {
- go_to_definition_updated = true;
- show_link_definition(
- shift_held,
- editor,
- TriggerPoint::InlayHint(
- highlight,
- location.clone(),
- *language_server_id,
- ),
- snapshot,
- window,
- cx,
- );
- }
+ go_to_definition_updated = true;
+ show_link_definition(
+ shift_held,
+ editor,
+ TriggerPoint::InlayHint(
+ highlight,
+ location.clone(),
+ *language_server_id,
+ ),
+ snapshot,
+ window,
+ cx,
+ );
}
-
- break;
}
- part_offset += part_len;
+ break;
}
+
+ part_offset += part_len;
}
- };
- }
+ }
+ };
}
}
}