Fix inlay hint caching

Richard Feldman created

Change summary

crates/editor/src/editor.rs           |  97 ++++++++++++++++
crates/editor/src/element.rs          |   3 
crates/editor/src/hover_links.rs      |   9 +
crates/editor/src/hover_popover.rs    | 167 +++++++++++++++-------------
crates/editor/src/inlay_hint_cache.rs |  22 ++
crates/project/src/lsp_store.rs       | 161 +++++++++++++++++++++++++++
6 files changed, 373 insertions(+), 86 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -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,

crates/editor/src/element.rs 🔗

@@ -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);

crates/editor/src/hover_links.rs 🔗

@@ -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
                     }
                 };

crates/editor/src/hover_popover.rs 🔗

@@ -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.
     }
 }

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -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();
                 })?;
             }

crates/project/src/lsp_store.rs 🔗

@@ -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)
             })
         }