Propagate inlay background highlights to data storage

Kirill Bulatov created

Change summary

crates/editor/src/display_map/inlay_map.rs |   2 
crates/editor/src/display_map/tab_map.rs   |   1 
crates/editor/src/editor.rs                |  18 +++
crates/editor/src/element.rs               |  87 ++++++++++++++----
crates/editor/src/hover_popover.rs         | 109 ++++++++++++++++++++++-
crates/project/src/lsp_command.rs          |   1 
6 files changed, 185 insertions(+), 33 deletions(-)

Detailed changes

crates/editor/src/display_map/inlay_map.rs 🔗

@@ -1596,7 +1596,7 @@ mod tests {
             .map(|range| {
                 buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end)
             })
-            // TODO add inlay highlight tests
+            // TODO kb add inlay highlight tests
             .map(DocumentRange::Text)
             .collect::<Vec<_>>();
 

crates/editor/src/display_map/tab_map.rs 🔗

@@ -223,7 +223,6 @@ impl TabSnapshot {
         &'a self,
         range: Range<TabPoint>,
         language_aware: bool,
-        // TODO kb extract into one struct?
         text_highlights: Option<&'a TextHighlights>,
         inlay_highlight_style: Option<HighlightStyle>,
         suggestion_highlight_style: Option<HighlightStyle>,

crates/editor/src/editor.rs 🔗

@@ -7511,6 +7511,22 @@ impl Editor {
         cx.notify();
     }
 
+    pub fn highlight_inlay_background<T: 'static>(
+        &mut self,
+        ranges: Vec<InlayRange>,
+        color_fetcher: fn(&Theme) -> Color,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.background_highlights.insert(
+            TypeId::of::<T>(),
+            (
+                color_fetcher,
+                ranges.into_iter().map(DocumentRange::Inlay).collect(),
+            ),
+        );
+        cx.notify();
+    }
+
     pub fn clear_background_highlights<T: 'static>(
         &mut self,
         cx: &mut ViewContext<Self>,
@@ -7932,7 +7948,6 @@ impl Editor {
         Some(
             ranges
                 .iter()
-                // TODO kb mark inlays too
                 .filter_map(|range| range.as_text_range())
                 .map(move |range| {
                     range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot)
@@ -8400,7 +8415,6 @@ impl View for Editor {
     fn marked_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
         let snapshot = self.buffer.read(cx).read(cx);
         let range = self.text_highlights::<InputComposition>(cx)?.1.get(0)?;
-        // TODO kb mark inlays too
         let range = range.as_text_range()?;
         Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0)
     }

crates/editor/src/element.rs 🔗

@@ -8,8 +8,8 @@ use crate::{
     editor_settings::ShowScrollbar,
     git::{diff_hunk_to_display, DisplayDiffHunk},
     hover_popover::{
-        hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
-        MIN_POPOVER_LINE_HEIGHT,
+        hide_hover, hover_at, hover_at_inlay, InlayHover, HOVER_POPOVER_GAP,
+        MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
     },
     link_go_to_definition::{
         go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
@@ -43,7 +43,8 @@ use language::{
 };
 use project::{
     project_settings::{GitGutterSetting, ProjectSettings},
-    InlayHintLabelPart, Location, LocationLink, ProjectPath, ResolveState,
+    HoverBlock, HoverBlockKind, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip,
+    Location, LocationLink, ProjectPath, ResolveState,
 };
 use smallvec::SmallVec;
 use std::{
@@ -1860,8 +1861,7 @@ fn update_inlay_link_and_hover_points(
         None
     };
     if let Some(hovered_offset) = hovered_offset {
-        let buffer = editor.buffer().read(cx);
-        let snapshot = buffer.snapshot(cx);
+        let snapshot = editor.buffer().read(cx).snapshot(cx);
         let previous_valid_anchor = snapshot.anchor_at(
             point_for_position
                 .previous_valid
@@ -1885,15 +1885,14 @@ fn update_inlay_link_and_hover_points(
             .max_by_key(|hint| hint.id)
         {
             let inlay_hint_cache = editor.inlay_hint_cache();
-            if let Some(cached_hint) =
-                inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id)
-            {
+            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 {
                     ResolveState::CanResolve(_, _) => {
                         if let Some(buffer_id) = previous_valid_anchor.buffer_id {
                             inlay_hint_cache.spawn_hint_resolve(
                                 buffer_id,
-                                previous_valid_anchor.excerpt_id,
+                                excerpt_id,
                                 hovered_hint.id,
                                 cx,
                             );
@@ -1902,9 +1901,33 @@ fn update_inlay_link_and_hover_points(
                     ResolveState::Resolved => {
                         match cached_hint.label {
                             project::InlayHintLabel::String(_) => {
-                                if cached_hint.tooltip.is_some() {
-                                    dbg!(&cached_hint.tooltip); // TODO kb
-                                                                // hover_at_point = Some(hovered_offset);
+                                if let Some(tooltip) = cached_hint.tooltip {
+                                    hover_at_inlay(
+                                        editor,
+                                        InlayHover {
+                                            excerpt: excerpt_id,
+                                            tooltip: match tooltip {
+                                                InlayHintTooltip::String(text) => HoverBlock {
+                                                    text,
+                                                    kind: HoverBlockKind::PlainText,
+                                                },
+                                                InlayHintTooltip::MarkupContent(content) => {
+                                                    HoverBlock {
+                                                        text: content.value,
+                                                        kind: content.kind,
+                                                    }
+                                                }
+                                            },
+                                            triggered_from: hovered_offset,
+                                            range: InlayRange {
+                                                inlay_position: hovered_hint.position,
+                                                highlight_start: hint_start_offset,
+                                                highlight_end: hint_end_offset,
+                                            },
+                                        },
+                                        cx,
+                                    );
+                                    hover_updated = true;
                                 }
                             }
                             project::InlayHintLabel::LabelParts(label_parts) => {
@@ -1915,15 +1938,41 @@ fn update_inlay_link_and_hover_points(
                                         hovered_offset,
                                     )
                                 {
-                                    if hovered_hint_part.tooltip.is_some() {
-                                        dbg!(&hovered_hint_part.tooltip); // TODO kb
-                                                                          // hover_at_point = Some(hovered_offset);
+                                    if let Some(tooltip) = hovered_hint_part.tooltip {
+                                        hover_at_inlay(
+                                            editor,
+                                            InlayHover {
+                                                excerpt: excerpt_id,
+                                                tooltip: match tooltip {
+                                                    InlayHintLabelPartTooltip::String(text) => {
+                                                        HoverBlock {
+                                                            text,
+                                                            kind: HoverBlockKind::PlainText,
+                                                        }
+                                                    }
+                                                    InlayHintLabelPartTooltip::MarkupContent(
+                                                        content,
+                                                    ) => HoverBlock {
+                                                        text: content.value,
+                                                        kind: content.kind,
+                                                    },
+                                                },
+                                                triggered_from: hovered_offset,
+                                                range: InlayRange {
+                                                    inlay_position: hovered_hint.position,
+                                                    highlight_start: part_range.start,
+                                                    highlight_end: part_range.end,
+                                                },
+                                            },
+                                            cx,
+                                        );
+                                        hover_updated = true;
                                     }
                                     if let Some(location) = hovered_hint_part.location {
-                                        if let Some(buffer) = cached_hint
-                                            .position
-                                            .buffer_id
-                                            .and_then(|buffer_id| buffer.buffer(buffer_id))
+                                        if let Some(buffer) =
+                                            cached_hint.position.buffer_id.and_then(|buffer_id| {
+                                                editor.buffer().read(cx).buffer(buffer_id)
+                                            })
                                         {
                                             go_to_definition_updated = true;
                                             update_go_to_definition_link(

crates/editor/src/hover_popover.rs 🔗

@@ -1,6 +1,8 @@
 use crate::{
-    display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings,
-    EditorSnapshot, EditorStyle, RangeToAnchorExt,
+    display_map::{InlayOffset, ToDisplayPoint},
+    link_go_to_definition::{DocumentRange, InlayRange},
+    Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
+    ExcerptId, RangeToAnchorExt,
 };
 use futures::FutureExt;
 use gpui::{
@@ -46,6 +48,84 @@ pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewC
     }
 }
 
+pub struct InlayHover {
+    pub excerpt: ExcerptId,
+    pub triggered_from: InlayOffset,
+    pub range: InlayRange,
+    pub tooltip: HoverBlock,
+}
+
+pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
+    if settings::get::<EditorSettings>(cx).hover_popover_enabled {
+        if editor.pending_rename.is_some() {
+            return;
+        }
+
+        let Some(project) = editor.project.clone() else {
+            return;
+        };
+
+        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
+            if let DocumentRange::Inlay(range) = symbol_range {
+                if (range.highlight_start..=range.highlight_end)
+                    .contains(&inlay_hover.triggered_from)
+                {
+                    // Hover triggered from same location as last time. Don't show again.
+                    return;
+                }
+            }
+            hide_hover(editor, cx);
+        }
+
+        let snapshot = editor.snapshot(cx);
+        // Don't request again if the location is the same as the previous request
+        if let Some(triggered_from) = editor.hover_state.triggered_from {
+            if inlay_hover.triggered_from
+                == snapshot
+                    .display_snapshot
+                    .anchor_to_inlay_offset(triggered_from)
+            {
+                return;
+            }
+        }
+
+        let task = cx.spawn(|this, mut cx| {
+            async move {
+                cx.background()
+                    .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
+                    .await;
+                this.update(&mut cx, |this, _| {
+                    this.hover_state.diagnostic_popover = None;
+                })?;
+
+                let hover_popover = InfoPopover {
+                    project: project.clone(),
+                    symbol_range: DocumentRange::Inlay(inlay_hover.range),
+                    blocks: vec![inlay_hover.tooltip],
+                    language: None,
+                    rendered_content: None,
+                };
+
+                this.update(&mut cx, |this, cx| {
+                    // Highlight the selected symbol using a background highlight
+                    this.highlight_inlay_background::<HoverState>(
+                        vec![inlay_hover.range],
+                        |theme| theme.editor.hover_popover.highlight,
+                        cx,
+                    );
+                    this.hover_state.info_popover = Some(hover_popover);
+                    cx.notify();
+                })?;
+
+                anyhow::Ok(())
+            }
+            .log_err()
+        });
+
+        editor.hover_state.info_task = Some(task);
+    }
+}
+
 /// Hides the type information popup.
 /// Triggered by the `Hover` action when the cursor is not over a symbol or when the
 /// selections changed.
@@ -110,8 +190,13 @@ fn show_hover(
     if !ignore_timeout {
         if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
             if symbol_range
-                .to_offset(&snapshot.buffer_snapshot)
-                .contains(&multibuffer_offset)
+                .as_text_range()
+                .map(|range| {
+                    range
+                        .to_offset(&snapshot.buffer_snapshot)
+                        .contains(&multibuffer_offset)
+                })
+                .unwrap_or(false)
             {
                 // Hover triggered from same location as last time. Don't show again.
                 return;
@@ -219,7 +304,7 @@ fn show_hover(
 
                 Some(InfoPopover {
                     project: project.clone(),
-                    symbol_range: range,
+                    symbol_range: DocumentRange::Text(range),
                     blocks: hover_result.contents,
                     language: hover_result.language,
                     rendered_content: None,
@@ -227,10 +312,13 @@ fn show_hover(
             });
 
             this.update(&mut cx, |this, cx| {
-                if let Some(hover_popover) = hover_popover.as_ref() {
+                if let Some(symbol_range) = hover_popover
+                    .as_ref()
+                    .and_then(|hover_popover| hover_popover.symbol_range.as_text_range())
+                {
                     // Highlight the selected symbol using a background highlight
                     this.highlight_background::<HoverState>(
-                        vec![hover_popover.symbol_range.clone()],
+                        vec![symbol_range],
                         |theme| theme.editor.hover_popover.highlight,
                         cx,
                     );
@@ -497,7 +585,10 @@ impl HoverState {
             .or_else(|| {
                 self.info_popover
                     .as_ref()
-                    .map(|info_popover| &info_popover.symbol_range.start)
+                    .map(|info_popover| match &info_popover.symbol_range {
+                        DocumentRange::Text(range) => &range.start,
+                        DocumentRange::Inlay(range) => &range.inlay_position,
+                    })
             })?;
         let point = anchor.to_display_point(&snapshot.display_snapshot);
 
@@ -522,7 +613,7 @@ impl HoverState {
 #[derive(Debug, Clone)]
 pub struct InfoPopover {
     pub project: ModelHandle<Project>,
-    pub symbol_range: Range<Anchor>,
+    symbol_range: DocumentRange,
     pub blocks: Vec<HoverBlock>,
     language: Option<Arc<Language>>,
     rendered_content: Option<RenderedInfo>,

crates/project/src/lsp_command.rs 🔗

@@ -2126,7 +2126,6 @@ impl InlayHints {
         })
     }
 
-    // TODO kb instead, store all LSP data inside the project::InlayHint?
     pub fn project_to_lsp_hint(
         hint: InlayHint,
         project: &ModelHandle<Project>,