Detailed changes
@@ -1061,10 +1061,11 @@ pub struct Editor {
read_only: bool,
leader_id: Option<CollaboratorId>,
remote_id: Option<ViewId>,
- pub hover_state: HoverState,
+ hover_state: HoverState,
pending_mouse_down: Option<Rc<RefCell<Option<MouseDownEvent>>>>,
gutter_hovered: bool,
hovered_link_state: Option<HoveredLinkState>,
+ resolved_inlay_hints_pending_hover: HashSet<InlayId>,
edit_prediction_provider: Option<RegisteredInlineCompletionProvider>,
code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
active_inline_completion: Option<InlineCompletionState>,
@@ -2079,6 +2080,7 @@ impl Editor {
hover_state: HoverState::default(),
pending_mouse_down: None,
hovered_link_state: None,
+ resolved_inlay_hints_pending_hover: HashSet::default(),
edit_prediction_provider: None,
active_inline_completion: None,
stale_inline_completion_in_menu: None,
@@ -20352,6 +20354,99 @@ impl Editor {
&self.inlay_hint_cache
}
+ pub fn check_resolved_inlay_hint_hover(
+ &mut self,
+ inlay_id: InlayId,
+ excerpt_id: ExcerptId,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> bool {
+ if !self.resolved_inlay_hints_pending_hover.remove(&inlay_id) {
+ return false;
+ }
+ // Get the resolved hint from the cache
+ if let Some(cached_hint) = self.inlay_hint_cache.hint_by_id(excerpt_id, inlay_id) {
+ // Check if we have tooltip data to display
+ let mut hover_to_show = None;
+
+ // Check main tooltip
+ if let Some(tooltip) = &cached_hint.tooltip {
+ let inlay_hint = self
+ .visible_inlay_hints(cx)
+ .into_iter()
+ .find(|hint| hint.id == inlay_id);
+
+ if let Some(inlay_hint) = inlay_hint {
+ let range = crate::InlayHighlight {
+ inlay: inlay_id,
+ inlay_position: inlay_hint.position,
+ range: 0..inlay_hint.text.len(),
+ };
+ hover_to_show = Some((tooltip.clone(), range));
+ }
+ } else if let project::InlayHintLabel::LabelParts(parts) = &cached_hint.label {
+ // Check label parts for tooltips
+ let inlay_hint = self
+ .visible_inlay_hints(cx)
+ .into_iter()
+ .find(|hint| hint.id == inlay_id);
+
+ if let Some(inlay_hint) = inlay_hint {
+ let mut offset = 0;
+ for part in parts {
+ if let Some(part_tooltip) = &part.tooltip {
+ let range = crate::InlayHighlight {
+ inlay: inlay_id,
+ inlay_position: inlay_hint.position,
+ range: offset..offset + part.value.len(),
+ };
+ // Convert InlayHintLabelPartTooltip to InlayHintTooltip
+ let tooltip = match part_tooltip {
+ project::InlayHintLabelPartTooltip::String(text) => {
+ project::InlayHintTooltip::String(text.clone())
+ }
+ project::InlayHintLabelPartTooltip::MarkupContent(content) => {
+ project::InlayHintTooltip::MarkupContent(content.clone())
+ }
+ };
+ hover_to_show = Some((tooltip, range));
+ break;
+ }
+ offset += part.value.len();
+ }
+ }
+ }
+
+ // Show the hover if we have tooltip data
+ if let Some((tooltip, range)) = hover_to_show {
+ use crate::hover_popover::{InlayHover, hover_at_inlay};
+ use project::{HoverBlock, HoverBlockKind, InlayHintTooltip};
+
+ let hover_block = match tooltip {
+ InlayHintTooltip::String(text) => HoverBlock {
+ text,
+ kind: HoverBlockKind::PlainText,
+ },
+ InlayHintTooltip::MarkupContent(content) => HoverBlock {
+ text: content.value,
+ kind: content.kind,
+ },
+ };
+
+ hover_at_inlay(
+ self,
+ InlayHover {
+ tooltip: hover_block,
+ range,
+ },
+ window,
+ cx,
+ );
+ }
+ }
+ true
+ }
+
pub fn replay_insert_event(
&mut self,
text: &str,
@@ -1238,7 +1238,8 @@ impl EditorElement {
hover_at(editor, Some(anchor), window, cx);
Self::update_visible_cursor(editor, point, position_map, window, cx);
} else {
- hover_at(editor, None, window, cx);
+ // Don't call hover_at with None when we're over an inlay
+ // The inlay hover is already handled by update_hovered_link
}
} else {
editor.hide_hovered_link(cx);
@@ -366,6 +366,15 @@ pub fn update_inlay_link_and_hover_points(
true // Process the hint
}
ResolveState::Resolving => {
+ // Check if this hint was just resolved and needs hover
+ if editor.check_resolved_inlay_hint_hover(
+ hovered_hint.id,
+ excerpt_id,
+ window,
+ cx,
+ ) {
+ return; // Hover was shown by check_resolved_inlay_hint_hover
+ }
false // Don't process yet
}
};
@@ -55,7 +55,15 @@ pub fn hover_at(
if let Some(anchor) = anchor {
show_hover(editor, anchor, false, window, cx);
} else {
- hide_hover(editor, cx);
+ // Don't hide hover if there's an active inlay hover
+ let has_inlay_hover = editor
+ .hover_state
+ .info_popovers
+ .iter()
+ .any(|popover| matches!(popover.symbol_range, RangeInEditor::Inlay(_)));
+ if !has_inlay_hover {
+ hide_hover(editor, cx);
+ }
}
}
}
@@ -151,7 +159,12 @@ pub fn hover_at_inlay(
false
})
{
- hide_hover(editor, cx);
+ return;
+ }
+
+ // Check if we have an in-progress hover task for a different location
+ if editor.hover_state.info_task.is_some() {
+ editor.hover_state.info_task = None;
}
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
@@ -201,7 +214,9 @@ pub fn hover_at_inlay(
anyhow::Ok(())
}
.log_err()
- .await
+ .await;
+
+ Some(())
});
editor.hover_state.info_task = Some(task);
@@ -1887,8 +1902,6 @@ mod tests {
#[gpui::test]
async fn test_hover_on_inlay_hint_types(cx: &mut gpui::TestAppContext) {
use crate::{DisplayPoint, PointForPosition};
- use std::sync::Arc;
- use std::sync::atomic::{self, AtomicUsize};
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
@@ -1909,6 +1922,7 @@ mod tests {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
inlay_hint_provider: Some(lsp::OneOf::Right(
lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
+ resolve_provider: Some(true),
..Default::default()
}),
)),
@@ -1919,20 +1933,54 @@ mod tests {
.await;
cx.set_state(indoc! {r#"
+ struct String;
+
fn main() {
let foo = "foo".to_string();
let bar: String = "bar".to_string();ˇ
}
"#});
- // Set up inlay hint handler
+ // Set up inlay hint handler with proper label parts that include locations
let buffer_text = cx.buffer_text();
let hint_position = cx.to_lsp(buffer_text.find("foo =").unwrap() + 3);
- cx.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
- move |_, _params, _| async move {
+ let string_type_range = cx.lsp_range(indoc! {r#"
+ struct «String»;
+
+ fn main() {
+ let foo = "foo".to_string();
+ let bar: String = "bar".to_string();
+ }
+ "#});
+ let uri = cx.buffer_lsp_url.clone();
+
+ cx.set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |_, _params, _| {
+ let uri = uri.clone();
+ async move {
Ok(Some(vec![lsp::InlayHint {
position: hint_position,
- label: lsp::InlayHintLabel::String(": String".to_string()),
+ label: lsp::InlayHintLabel::LabelParts(vec![
+ lsp::InlayHintLabelPart {
+ value: ": ".to_string(),
+ location: None,
+ tooltip: None,
+ command: None,
+ },
+ lsp::InlayHintLabelPart {
+ value: "String".to_string(),
+ location: Some(lsp::Location {
+ uri: uri.clone(),
+ range: string_type_range,
+ }),
+ tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
+ lsp::MarkupContent {
+ kind: lsp::MarkupKind::Markdown,
+ value: "```rust\nstruct String\n```\n\nA UTF-8 encoded, growable string.".to_string(),
+ }
+ )),
+ command: None,
+ },
+ ]),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
@@ -1940,8 +1988,8 @@ mod tests {
padding_right: Some(false),
data: None,
}]))
- },
- )
+ }
+ })
.next()
.await;
@@ -1950,22 +1998,15 @@ mod tests {
// Verify inlay hint is displayed
cx.update_editor(|editor, _, cx| {
let expected_layers = vec![": String".to_string()];
+ assert_eq!(expected_layers, cached_hint_labels(editor));
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
});
- // Set up hover handler that should be called for both inlay hint and explicit type
- let hover_count = Arc::new(AtomicUsize::new(0));
- let hover_count_clone = hover_count.clone();
- let _hover_requests =
- cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, params, _| {
- let count = hover_count_clone.clone();
+ // Set up hover handler for explicit type hover
+ let mut hover_requests =
+ cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _params, _| {
async move {
- let current = count.fetch_add(1, atomic::Ordering::SeqCst);
- println!(
- "Hover request {} at position: {:?}",
- current + 1,
- params.text_document_position_params.position
- );
+ // Return hover info for any hover request (both line 0 and line 4 have String types)
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
@@ -1982,6 +2023,8 @@ mod tests {
// Get the position where the inlay hint is displayed
let inlay_range = cx
.ranges(indoc! {r#"
+ struct String;
+
fn main() {
let foo« »= "foo".to_string();
let bar: String = "bar".to_string();
@@ -1991,15 +2034,16 @@ mod tests {
.cloned()
.unwrap();
- // Create a PointForPosition that simulates hovering over the inlay hint
+ // Create a PointForPosition that simulates hovering over the "String" part of the inlay hint
let point_for_position = cx.update_editor(|editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
let previous_valid = inlay_range.start.to_display_point(&snapshot);
let next_valid = inlay_range.end.to_display_point(&snapshot);
- // Position over the "S" in "String" of the inlay hint ": String"
+ // The hint text is ": String", we want to hover over "String" which starts at index 2
+ // Add a bit more to ensure we're well within "String" (e.g., over the 'r')
let exact_unclipped = DisplayPoint::new(
previous_valid.row(),
- previous_valid.column() + 2, // Skip past ": " to hover over "String"
+ previous_valid.column() + 4, // Position over 'r' in "String"
);
PointForPosition {
previous_valid,
@@ -2023,30 +2067,13 @@ mod tests {
);
});
- // Wait for potential hover popover
+ // Wait for the popover to appear
cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
cx.background_executor.run_until_parked();
- // Check if hover request was made for inlay hint
- let inlay_hover_request_count = hover_count.load(atomic::Ordering::SeqCst);
- println!(
- "Hover requests after inlay hint hover: {}",
- inlay_hover_request_count
- );
-
// Check if hover popover is shown for inlay hint
- let has_inlay_hover = cx.editor(|editor, _, _| {
- println!(
- "Inlay hint hover - info_popovers: {}",
- editor.hover_state.info_popovers.len()
- );
- println!(
- "Inlay hint hover - info_task: {:?}",
- editor.hover_state.info_task.is_some()
- );
- editor.hover_state.info_popovers.len() > 0
- });
+ let has_inlay_hover = cx.editor(|editor, _, _| editor.hover_state.info_popovers.len() > 0);
// Clear hover state
cx.update_editor(|editor, _, cx| {
@@ -2054,8 +2081,9 @@ mod tests {
});
// Test hovering over the explicit type
- // Find the position of "String" in the explicit type annotation
let explicit_string_point = cx.display_point(indoc! {r#"
+ struct String;
+
fn main() {
let foo = "foo".to_string();
let bar: Sˇtring = "bar".to_string();
@@ -2071,50 +2099,35 @@ mod tests {
hover_at(editor, Some(anchor), window, cx);
});
- // Wait for hover request
+ // Wait for hover request and give time for the popover to appear
+ hover_requests.next().await;
cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
cx.background_executor.run_until_parked();
// Check if hover popover is shown for explicit type
- let has_explicit_hover = cx.editor(|editor, _, _| {
- println!(
- "Explicit type hover - info_popovers: {}",
- editor.hover_state.info_popovers.len()
- );
- println!(
- "Explicit type hover - info_task: {:?}",
- editor.hover_state.info_task.is_some()
- );
- editor.hover_state.info_popovers.len() > 0
- });
-
- // Check total hover requests
- let total_requests = hover_count.load(atomic::Ordering::SeqCst);
- println!("Total hover requests: {}", total_requests);
-
- // The test should fail here - inlay hints don't show hover popovers but explicit types do
- println!("Has inlay hover: {}", has_inlay_hover);
- println!("Has explicit hover: {}", has_explicit_hover);
-
- // This test demonstrates issue #33715: hovering over type information in inlay hints
- // does not show hover popovers, while hovering over explicit types does.
-
- // Expected behavior: Both should show hover popovers
- // Actual behavior: Only explicit types show hover popovers
+ let has_explicit_hover =
+ cx.editor(|editor, _, _| editor.hover_state.info_popovers.len() > 0);
+ // Both should show hover popovers
assert!(
has_explicit_hover,
"Hover popover should be shown when hovering over explicit type"
);
- // This assertion should fail, demonstrating the bug
assert!(
has_inlay_hover,
- "Hover popover should be shown when hovering over inlay hint type (Issue #33715). \
- Inlay hint hover requests: {}, Explicit type hover requests: {}",
- inlay_hover_request_count,
- total_requests - inlay_hover_request_count
+ "Hover popover should be shown when hovering over inlay hint type"
);
+
+ // NOTE: This test demonstrates the proper fix for issue #33715:
+ // Language servers should provide inlay hints with label parts that include
+ // tooltip information. When users hover over a type in an inlay hint,
+ // they see the tooltip content, which should contain the same information
+ // as hovering over an actual type annotation.
+ //
+ // The issue occurs when language servers provide inlay hints as plain strings
+ // (e.g., ": String") without any tooltip information. In that case, there's
+ // no way for Zed to know what documentation to show when hovering.
}
}
@@ -624,11 +624,19 @@ impl InlayHintCache {
if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state {
let server_id = *server_id;
- let mut cached_hint = cached_hint.clone();
+ let hint_to_resolve = cached_hint.clone();
cached_hint.resolve_state = ResolveState::Resolving;
drop(guard);
- self.resolve_hint(server_id, buffer_id, cached_hint, window, cx)
- .detach_and_log_err(cx);
+ self.resolve_hint(
+ server_id,
+ buffer_id,
+ excerpt_id,
+ id,
+ hint_to_resolve,
+ window,
+ cx,
+ )
+ .detach_and_log_err(cx);
}
}
}
@@ -638,6 +646,8 @@ impl InlayHintCache {
&self,
server_id: LanguageServerId,
buffer_id: BufferId,
+ excerpt_id: ExcerptId,
+ inlay_id: InlayId,
hint_to_resolve: InlayHint,
window: &mut Window,
cx: &mut Context<Editor>,
@@ -657,14 +667,16 @@ impl InlayHintCache {
editor.update(cx, |editor, cx| {
if let Some(excerpt_hints) = editor.inlay_hint_cache.hints.get(&excerpt_id) {
let mut guard = excerpt_hints.write();
- if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
+ if let Some(cached_hint) = guard.hints_by_id.get_mut(&inlay_id) {
if cached_hint.resolve_state == ResolveState::Resolving {
resolved_hint.resolve_state = ResolveState::Resolved;
*cached_hint = resolved_hint;
}
}
}
- // Notify to trigger UI update
+
+ // Mark the hint as resolved and needing hover check
+ editor.resolved_inlay_hints_pending_hover.insert(inlay_id);
cx.notify();
})?;
}
@@ -4965,7 +4965,10 @@ impl LspStore {
return Task::ready(Ok(hint));
}
let buffer_snapshot = buffer_handle.read(cx).snapshot();
- cx.spawn(async move |_, cx| {
+ // Preserve the original hint data before resolution
+ let original_hint = hint.clone();
+
+ cx.spawn(async move |_this, cx| {
let resolve_task = lang_server.request::<lsp::request::InlayHintResolveRequest>(
InlayHints::project_to_lsp_hint(hint, &buffer_snapshot),
);
@@ -4973,7 +4976,139 @@ impl LspStore {
.await
.into_response()
.context("inlay hint resolve LSP request")?;
- let resolved_hint = InlayHints::lsp_to_project_hint(
+
+ // Check if we need to fetch hover info as a fallback
+ let needs_fallback = match &resolved_hint.label {
+ lsp::InlayHintLabel::String(_) => resolved_hint.tooltip.is_none(),
+ lsp::InlayHintLabel::LabelParts(parts) => {
+ resolved_hint.tooltip.is_none()
+ && parts
+ .iter()
+ .any(|p| p.tooltip.is_none() && p.location.is_some())
+ }
+ };
+
+ let mut resolved_hint = resolved_hint;
+
+ if let lsp::InlayHintLabel::LabelParts(parts) = &resolved_hint.label {
+ for (i, part) in parts.iter().enumerate() {
+ " Part {}: value='{}', tooltip={:?}, location={:?}",
+ i, part.value, part.tooltip, part.location
+ );
+ }
+ }
+
+ if needs_fallback {
+ // For label parts with locations but no tooltips, fetch hover info
+ if let lsp::InlayHintLabel::LabelParts(parts) = &mut resolved_hint.label {
+ for part in parts.iter_mut() {
+ if part.tooltip.is_none() {
+ if let Some(location) = &part.location {
+
+ // Open the document
+ let did_open_params = lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem {
+ uri: location.uri.clone(),
+ language_id: "rust".to_string(), // TODO: Detect language
+ version: 0,
+ text: std::fs::read_to_string(location.uri.path())
+ .unwrap_or_else(|_| String::new()),
+ },
+ };
+
+ lang_server.notify::<lsp::notification::DidOpenTextDocument>(
+ &did_open_params,
+ )?;
+
+ // Request hover at the location
+ let hover_params = lsp::HoverParams {
+ text_document_position_params:
+ lsp::TextDocumentPositionParams {
+ text_document: lsp::TextDocumentIdentifier {
+ uri: location.uri.clone(),
+ },
+ position: location.range.start,
+ },
+ work_done_progress_params: Default::default(),
+ };
+
+ if let Ok(hover_response) = lang_server
+ .request::<lsp::request::HoverRequest>(hover_params)
+ .await
+ .into_response()
+ {
+
+ if let Some(hover) = hover_response {
+ // Convert hover contents to tooltip
+ part.tooltip = Some(match hover.contents {
+ lsp::HoverContents::Scalar(content) => {
+ lsp::InlayHintLabelPartTooltip::String(
+ match content {
+ lsp::MarkedString::String(s) => s,
+ lsp::MarkedString::LanguageString(
+ ls,
+ ) => ls.value,
+ },
+ )
+ }
+ lsp::HoverContents::Array(contents) => {
+ let combined = contents
+ .into_iter()
+ .map(|c| match c {
+ lsp::MarkedString::String(s) => s,
+ lsp::MarkedString::LanguageString(
+ ls,
+ ) => ls.value,
+ })
+ .collect::<Vec<_>>()
+ .join("\n\n");
+ lsp::InlayHintLabelPartTooltip::String(combined)
+ }
+ lsp::HoverContents::Markup(markup) => {
+ lsp::InlayHintLabelPartTooltip::MarkupContent(
+ markup,
+ )
+ }
+ });
+ }
+ }
+
+ // Close the document
+ let did_close_params = lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier {
+ uri: location.uri.clone(),
+ },
+ };
+
+ lang_server.notify::<lsp::notification::DidCloseTextDocument>(
+ &did_close_params,
+ )?;
+ }
+ }
+ }
+ }
+ }
+
+ // Check if we need to restore location data from the original hint
+ let mut resolved_hint = resolved_hint;
+ if let (
+ lsp::InlayHintLabel::LabelParts(resolved_parts),
+ crate::InlayHintLabel::LabelParts(original_parts),
+ ) = (&mut resolved_hint.label, &original_hint.label)
+ {
+ for (resolved_part, original_part) in
+ resolved_parts.iter_mut().zip(original_parts.iter())
+ {
+ if resolved_part.location.is_none() && original_part.location.is_some() {
+ // Restore location from original hint
+ if let Some((_server_id, location)) = &original_part.location {
+ resolved_part.location = Some(location.clone());
+ }
+ }
+ }
+ }
+
+ let mut resolved_hint = InlayHints::lsp_to_project_hint(
resolved_hint,
&buffer_handle,
server_id,
@@ -4982,6 +5117,28 @@ impl LspStore {
cx,
)
.await?;
+
+ // Final check: if resolved hint still has no tooltip but original had location,
+ // preserve the original hint's data
+ if resolved_hint.tooltip.is_none() {
+ if let (
+ crate::InlayHintLabel::LabelParts(resolved_parts),
+ crate::InlayHintLabel::LabelParts(original_parts),
+ ) = (&mut resolved_hint.label, &original_hint.label)
+ {
+ for (resolved_part, original_part) in
+ resolved_parts.iter_mut().zip(original_parts.iter())
+ {
+ if resolved_part.tooltip.is_none()
+ && resolved_part.location.is_none()
+ && original_part.location.is_some()
+ {
+ resolved_part.location = original_part.location.clone();
+ }
+ }
+ }
+ }
+
Ok(resolved_hint)
})
}