Rework inlay hover model (#2969)

Kirill Bulatov created

Fixes
```
thread 'main' panicked at 'byte index 2 is not a char boundary; it is inside '…' (bytes 0..3) of `…)`'
```
panics like
https://zed-industries.slack.com/archives/C04S6T1T7TQ/p1694535396473329

by reworking the inlay hover model:
* avoid storing "hardcoded" coordinates of hovered inlay labels (such as
`InlayOffset`), instead, remember `inlay_id` and apply required
highlights there when hint with the same id is handled
* add randomized tests on inlay highlights
* sped up inlay hint cache lookup by inlay_id

As a downside, background highlights are no long appearing on inlay
hints, but Zed does not receive any tooltips for inlays anyway (r-a does
not send them for some reason, other LSP seem to have no such feature?),
so it does not matter now.
Nontheless, if the logic for displaying hint pop-ups is present and
works for harcoded tooltips in r-a, only background highlight is missing
now.

Release Notes:

- Fixed inlay hint highlights causing panic for certain cases with
"large" characters

Change summary

crates/ai/src/assistant.rs                    |   2 
crates/editor/src/display_map.rs              |  96 +++--
crates/editor/src/display_map/block_map.rs    |  20 
crates/editor/src/display_map/fold_map.rs     |  22 
crates/editor/src/display_map/inlay_map.rs    | 318 ++++++++++++--------
crates/editor/src/display_map/tab_map.rs      |  40 +-
crates/editor/src/display_map/wrap_map.rs     |  24 -
crates/editor/src/editor.rs                   | 152 +++------
crates/editor/src/hover_popover.rs            |  71 +---
crates/editor/src/inlay_hint_cache.rs         | 101 +++--
crates/editor/src/link_go_to_definition.rs    | 207 ++++++-------
crates/editor/src/test/editor_test_context.rs |   4 
crates/search/src/buffer_search.rs            |  12 
crates/search/src/project_search.rs           |   2 
crates/vim/src/normal/search.rs               |   2 
crates/vim/src/state.rs                       |   1 
crates/vim/src/test.rs                        |   2 
17 files changed, 522 insertions(+), 554 deletions(-)

Detailed changes

crates/ai/src/assistant.rs 🔗

@@ -704,7 +704,7 @@ impl AssistantPanel {
             }
 
             if foreground_ranges.is_empty() {
-                editor.clear_text_highlights::<PendingInlineAssist>(cx);
+                editor.clear_highlights::<PendingInlineAssist>(cx);
             } else {
                 editor.highlight_text::<PendingInlineAssist>(
                     foreground_ranges,

crates/editor/src/display_map.rs 🔗

@@ -5,11 +5,11 @@ mod tab_map;
 mod wrap_map;
 
 use crate::{
-    link_go_to_definition::{DocumentRange, InlayRange},
-    Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+    link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer,
+    MultiBufferSnapshot, ToOffset, ToPoint,
 };
 pub use block_map::{BlockMap, BlockPoint};
-use collections::{HashMap, HashSet};
+use collections::{BTreeMap, HashMap, HashSet};
 use fold_map::FoldMap;
 use gpui::{
     color::Color,
@@ -43,7 +43,8 @@ pub trait ToDisplayPoint {
     fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
 }
 
-type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<DocumentRange>)>>;
+type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
+type InlayHighlights = BTreeMap<TypeId, HashMap<InlayId, (HighlightStyle, InlayHighlight)>>;
 
 pub struct DisplayMap {
     buffer: ModelHandle<MultiBuffer>,
@@ -54,6 +55,7 @@ pub struct DisplayMap {
     wrap_map: ModelHandle<WrapMap>,
     block_map: BlockMap,
     text_highlights: TextHighlights,
+    inlay_highlights: InlayHighlights,
     pub clip_at_line_ends: bool,
 }
 
@@ -89,6 +91,7 @@ impl DisplayMap {
             wrap_map,
             block_map,
             text_highlights: Default::default(),
+            inlay_highlights: Default::default(),
             clip_at_line_ends: false,
         }
     }
@@ -113,6 +116,7 @@ impl DisplayMap {
             wrap_snapshot,
             block_snapshot,
             text_highlights: self.text_highlights.clone(),
+            inlay_highlights: self.inlay_highlights.clone(),
             clip_at_line_ends: self.clip_at_line_ends,
         }
     }
@@ -215,37 +219,32 @@ impl DisplayMap {
         ranges: Vec<Range<Anchor>>,
         style: HighlightStyle,
     ) {
-        self.text_highlights.insert(
-            Some(type_id),
-            Arc::new((style, ranges.into_iter().map(DocumentRange::Text).collect())),
-        );
+        self.text_highlights
+            .insert(Some(type_id), Arc::new((style, ranges)));
     }
 
     pub fn highlight_inlays(
         &mut self,
         type_id: TypeId,
-        ranges: Vec<InlayRange>,
+        highlights: Vec<InlayHighlight>,
         style: HighlightStyle,
     ) {
-        self.text_highlights.insert(
-            Some(type_id),
-            Arc::new((
-                style,
-                ranges.into_iter().map(DocumentRange::Inlay).collect(),
-            )),
-        );
+        for highlight in highlights {
+            self.inlay_highlights
+                .entry(type_id)
+                .or_default()
+                .insert(highlight.inlay, (style, highlight));
+        }
     }
 
-    pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[DocumentRange])> {
+    pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
         let highlights = self.text_highlights.get(&Some(type_id))?;
         Some((highlights.0, &highlights.1))
     }
-
-    pub fn clear_text_highlights(
-        &mut self,
-        type_id: TypeId,
-    ) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
-        self.text_highlights.remove(&Some(type_id))
+    pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
+        let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some();
+        cleared |= self.inlay_highlights.remove(&type_id).is_none();
+        cleared
     }
 
     pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
@@ -309,6 +308,14 @@ impl DisplayMap {
     }
 }
 
+#[derive(Debug, Default)]
+pub struct Highlights<'a> {
+    pub text_highlights: Option<&'a TextHighlights>,
+    pub inlay_highlights: Option<&'a InlayHighlights>,
+    pub inlay_highlight_style: Option<HighlightStyle>,
+    pub suggestion_highlight_style: Option<HighlightStyle>,
+}
+
 pub struct DisplaySnapshot {
     pub buffer_snapshot: MultiBufferSnapshot,
     pub fold_snapshot: fold_map::FoldSnapshot,
@@ -317,6 +324,7 @@ pub struct DisplaySnapshot {
     wrap_snapshot: wrap_map::WrapSnapshot,
     block_snapshot: block_map::BlockSnapshot,
     text_highlights: TextHighlights,
+    inlay_highlights: InlayHighlights,
     clip_at_line_ends: bool,
 }
 
@@ -422,15 +430,6 @@ impl DisplaySnapshot {
             .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
     }
 
-    pub fn inlay_offset_to_display_point(&self, offset: InlayOffset, bias: Bias) -> DisplayPoint {
-        let inlay_point = self.inlay_snapshot.to_point(offset);
-        let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
-        let tab_point = self.tab_snapshot.to_tab_point(fold_point);
-        let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
-        let block_point = self.block_snapshot.to_block_point(wrap_point);
-        DisplayPoint(block_point)
-    }
-
     fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
         let block_point = point.0;
         let wrap_point = self.block_snapshot.to_wrap_point(block_point);
@@ -463,9 +462,7 @@ impl DisplaySnapshot {
             .chunks(
                 display_row..self.max_point().row() + 1,
                 false,
-                None,
-                None,
-                None,
+                Highlights::default(),
             )
             .map(|h| h.text)
     }
@@ -474,7 +471,7 @@ impl DisplaySnapshot {
     pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
         (0..=display_row).into_iter().rev().flat_map(|row| {
             self.block_snapshot
-                .chunks(row..row + 1, false, None, None, None)
+                .chunks(row..row + 1, false, Highlights::default())
                 .map(|h| h.text)
                 .collect::<Vec<_>>()
                 .into_iter()
@@ -482,19 +479,22 @@ impl DisplaySnapshot {
         })
     }
 
-    pub fn chunks(
-        &self,
+    pub fn chunks<'a>(
+        &'a self,
         display_rows: Range<u32>,
         language_aware: bool,
-        hint_highlight_style: Option<HighlightStyle>,
+        inlay_highlight_style: Option<HighlightStyle>,
         suggestion_highlight_style: Option<HighlightStyle>,
     ) -> DisplayChunks<'_> {
         self.block_snapshot.chunks(
             display_rows,
             language_aware,
-            Some(&self.text_highlights),
-            hint_highlight_style,
-            suggestion_highlight_style,
+            Highlights {
+                text_highlights: Some(&self.text_highlights),
+                inlay_highlights: Some(&self.inlay_highlights),
+                inlay_highlight_style,
+                suggestion_highlight_style,
+            },
         )
     }
 
@@ -752,12 +752,20 @@ impl DisplaySnapshot {
     }
 
     #[cfg(any(test, feature = "test-support"))]
-    pub fn highlight_ranges<Tag: ?Sized + 'static>(
+    pub fn text_highlight_ranges<Tag: ?Sized + 'static>(
         &self,
-    ) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
+    ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
         let type_id = TypeId::of::<Tag>();
         self.text_highlights.get(&Some(type_id)).cloned()
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn inlay_highlights<Tag: ?Sized + 'static>(
+        &self,
+    ) -> Option<&HashMap<InlayId, (HighlightStyle, InlayHighlight)>> {
+        let type_id = TypeId::of::<Tag>();
+        self.inlay_highlights.get(&type_id)
+    }
 }
 
 #[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]

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

@@ -1,10 +1,10 @@
 use super::{
     wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
-    TextHighlights,
+    Highlights,
 };
 use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _};
 use collections::{Bound, HashMap, HashSet};
-use gpui::{fonts::HighlightStyle, AnyElement, ViewContext};
+use gpui::{AnyElement, ViewContext};
 use language::{BufferSnapshot, Chunk, Patch, Point};
 use parking_lot::Mutex;
 use std::{
@@ -576,9 +576,7 @@ impl BlockSnapshot {
         self.chunks(
             0..self.transforms.summary().output_rows,
             false,
-            None,
-            None,
-            None,
+            Highlights::default(),
         )
         .map(|chunk| chunk.text)
         .collect()
@@ -588,9 +586,7 @@ impl BlockSnapshot {
         &'a self,
         rows: Range<u32>,
         language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        hint_highlight_style: Option<HighlightStyle>,
-        suggestion_highlight_style: Option<HighlightStyle>,
+        highlights: Highlights<'a>,
     ) -> BlockChunks<'a> {
         let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
         let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
@@ -622,9 +618,7 @@ impl BlockSnapshot {
             input_chunks: self.wrap_snapshot.chunks(
                 input_start..input_end,
                 language_aware,
-                text_highlights,
-                hint_highlight_style,
-                suggestion_highlight_style,
+                highlights,
             ),
             input_chunk: Default::default(),
             transforms: cursor,
@@ -1501,9 +1495,7 @@ mod tests {
                     .chunks(
                         start_row as u32..blocks_snapshot.max_point().row + 1,
                         false,
-                        None,
-                        None,
-                        None,
+                        Highlights::default(),
                     )
                     .map(|chunk| chunk.text)
                     .collect::<String>();

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

@@ -1,6 +1,6 @@
 use super::{
     inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
-    TextHighlights,
+    Highlights,
 };
 use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
 use gpui::{color::Color, fonts::HighlightStyle};
@@ -475,7 +475,7 @@ pub struct FoldSnapshot {
 impl FoldSnapshot {
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(FoldOffset(0)..self.len(), false, None, None, None)
+        self.chunks(FoldOffset(0)..self.len(), false, Highlights::default())
             .map(|c| c.text)
             .collect()
     }
@@ -651,9 +651,7 @@ impl FoldSnapshot {
         &'a self,
         range: Range<FoldOffset>,
         language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        hint_highlight_style: Option<HighlightStyle>,
-        suggestion_highlight_style: Option<HighlightStyle>,
+        highlights: Highlights<'a>,
     ) -> FoldChunks<'a> {
         let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
 
@@ -674,9 +672,7 @@ impl FoldSnapshot {
             inlay_chunks: self.inlay_snapshot.chunks(
                 inlay_start..inlay_end,
                 language_aware,
-                text_highlights,
-                hint_highlight_style,
-                suggestion_highlight_style,
+                highlights,
             ),
             inlay_chunk: None,
             inlay_offset: inlay_start,
@@ -687,8 +683,12 @@ impl FoldSnapshot {
     }
 
     pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
-        self.chunks(start.to_offset(self)..self.len(), false, None, None, None)
-            .flat_map(|chunk| chunk.text.chars())
+        self.chunks(
+            start.to_offset(self)..self.len(),
+            false,
+            Highlights::default(),
+        )
+        .flat_map(|chunk| chunk.text.chars())
     }
 
     #[cfg(test)]
@@ -1496,7 +1496,7 @@ mod tests {
                 let text = &expected_text[start.0..end.0];
                 assert_eq!(
                     snapshot
-                        .chunks(start..end, false, None, None, None)
+                        .chunks(start..end, false, Highlights::default())
                         .map(|c| c.text)
                         .collect::<String>(),
                     text,

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

@@ -1,5 +1,4 @@
 use crate::{
-    link_go_to_definition::DocumentRange,
     multi_buffer::{MultiBufferChunks, MultiBufferRows},
     Anchor, InlayId, MultiBufferSnapshot, ToOffset,
 };
@@ -11,12 +10,13 @@ use std::{
     cmp,
     iter::Peekable,
     ops::{Add, AddAssign, Range, Sub, SubAssign},
+    sync::Arc,
     vec,
 };
-use sum_tree::{Bias, Cursor, SumTree};
+use sum_tree::{Bias, Cursor, SumTree, TreeMap};
 use text::{Patch, Rope};
 
-use super::TextHighlights;
+use super::Highlights;
 
 pub struct InlayMap {
     snapshot: InlaySnapshot,
@@ -214,10 +214,11 @@ pub struct InlayChunks<'a> {
     inlay_chunk: Option<&'a str>,
     output_offset: InlayOffset,
     max_output_offset: InlayOffset,
-    hint_highlight_style: Option<HighlightStyle>,
+    inlay_highlight_style: Option<HighlightStyle>,
     suggestion_highlight_style: Option<HighlightStyle>,
     highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
     active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
+    highlights: Highlights<'a>,
     snapshot: &'a InlaySnapshot,
 }
 
@@ -293,8 +294,41 @@ impl<'a> Iterator for InlayChunks<'a> {
                 prefix
             }
             Transform::Inlay(inlay) => {
+                let mut inlay_style_and_highlight = None;
+                if let Some(inlay_highlights) = self.highlights.inlay_highlights {
+                    for (_, inlay_id_to_data) in inlay_highlights.iter() {
+                        let style_and_highlight = inlay_id_to_data.get(&inlay.id);
+                        if style_and_highlight.is_some() {
+                            inlay_style_and_highlight = style_and_highlight;
+                            break;
+                        }
+                    }
+                }
+
+                let mut highlight_style = match inlay.id {
+                    InlayId::Suggestion(_) => self.suggestion_highlight_style,
+                    InlayId::Hint(_) => self.inlay_highlight_style,
+                };
+                let next_inlay_highlight_endpoint;
+                let offset_in_inlay = self.output_offset - self.transforms.start().0;
+                if let Some((style, highlight)) = inlay_style_and_highlight {
+                    let range = &highlight.range;
+                    if offset_in_inlay.0 < range.start {
+                        next_inlay_highlight_endpoint = range.start - offset_in_inlay.0;
+                    } else if offset_in_inlay.0 >= range.end {
+                        next_inlay_highlight_endpoint = usize::MAX;
+                    } else {
+                        next_inlay_highlight_endpoint = range.end - offset_in_inlay.0;
+                        highlight_style
+                            .get_or_insert_with(|| Default::default())
+                            .highlight(style.clone());
+                    }
+                } else {
+                    next_inlay_highlight_endpoint = usize::MAX;
+                }
+
                 let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
-                    let start = self.output_offset - self.transforms.start().0;
+                    let start = offset_in_inlay;
                     let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0)
                         - self.transforms.start().0;
                     inlay.text.chunks_in_range(start.0..end.0)
@@ -302,21 +336,15 @@ impl<'a> Iterator for InlayChunks<'a> {
                 let inlay_chunk = self
                     .inlay_chunk
                     .get_or_insert_with(|| inlay_chunks.next().unwrap());
-                let (chunk, remainder) = inlay_chunk.split_at(
-                    inlay_chunk
-                        .len()
-                        .min(next_highlight_endpoint.0 - self.output_offset.0),
-                );
+                let (chunk, remainder) =
+                    inlay_chunk.split_at(inlay_chunk.len().min(next_inlay_highlight_endpoint));
                 *inlay_chunk = remainder;
                 if inlay_chunk.is_empty() {
                     self.inlay_chunk = None;
                 }
 
                 self.output_offset.0 += chunk.len();
-                let mut highlight_style = match inlay.id {
-                    InlayId::Suggestion(_) => self.suggestion_highlight_style,
-                    InlayId::Hint(_) => self.hint_highlight_style,
-                };
+
                 if !self.active_highlights.is_empty() {
                     for active_highlight in self.active_highlights.values() {
                         highlight_style
@@ -625,18 +653,20 @@ impl InlayMap {
                     .filter(|ch| *ch != '\r')
                     .take(len)
                     .collect::<String>();
-                log::info!(
-                    "creating inlay at buffer offset {} with bias {:?} and text {:?}",
-                    position,
-                    bias,
-                    text
-                );
 
                 let inlay_id = if i % 2 == 0 {
                     InlayId::Hint(post_inc(next_inlay_id))
                 } else {
                     InlayId::Suggestion(post_inc(next_inlay_id))
                 };
+                log::info!(
+                    "creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}",
+                    inlay_id,
+                    position,
+                    bias,
+                    text
+                );
+
                 to_insert.push(Inlay {
                     id: inlay_id,
                     position: snapshot.buffer.anchor_at(position, bias),
@@ -992,77 +1022,24 @@ impl InlaySnapshot {
         &'a self,
         range: Range<InlayOffset>,
         language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        hint_highlight_style: Option<HighlightStyle>,
-        suggestion_highlight_style: Option<HighlightStyle>,
+        highlights: Highlights<'a>,
     ) -> InlayChunks<'a> {
         let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
         cursor.seek(&range.start, Bias::Right, &());
 
         let mut highlight_endpoints = Vec::new();
-        if let Some(text_highlights) = text_highlights {
+        if let Some(text_highlights) = highlights.text_highlights {
             if !text_highlights.is_empty() {
-                while cursor.start().0 < range.end {
-                    let transform_start = self.buffer.anchor_after(
-                        self.to_buffer_offset(cmp::max(range.start, cursor.start().0)),
-                    );
-                    let transform_start =
-                        self.to_inlay_offset(transform_start.to_offset(&self.buffer));
-
-                    let transform_end = {
-                        let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
-                        self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
-                            cursor.end(&()).0,
-                            cursor.start().0 + overshoot,
-                        )))
-                    };
-                    let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer));
-
-                    for (tag, text_highlights) in text_highlights.iter() {
-                        let style = text_highlights.0;
-                        let ranges = &text_highlights.1;
-
-                        let start_ix = match ranges.binary_search_by(|probe| {
-                            let cmp = self
-                                .document_to_inlay_range(probe)
-                                .end
-                                .cmp(&transform_start);
-                            if cmp.is_gt() {
-                                cmp::Ordering::Greater
-                            } else {
-                                cmp::Ordering::Less
-                            }
-                        }) {
-                            Ok(i) | Err(i) => i,
-                        };
-                        for range in &ranges[start_ix..] {
-                            let range = self.document_to_inlay_range(range);
-                            if range.start.cmp(&transform_end).is_ge() {
-                                break;
-                            }
-
-                            highlight_endpoints.push(HighlightEndpoint {
-                                offset: range.start,
-                                is_start: true,
-                                tag: *tag,
-                                style,
-                            });
-                            highlight_endpoints.push(HighlightEndpoint {
-                                offset: range.end,
-                                is_start: false,
-                                tag: *tag,
-                                style,
-                            });
-                        }
-                    }
-
-                    cursor.next(&());
-                }
-                highlight_endpoints.sort();
+                self.apply_text_highlights(
+                    &mut cursor,
+                    &range,
+                    text_highlights,
+                    &mut highlight_endpoints,
+                );
                 cursor.seek(&range.start, Bias::Right, &());
             }
         }
-
+        highlight_endpoints.sort();
         let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
         let buffer_chunks = self.buffer.chunks(buffer_range, language_aware);
 
@@ -1074,29 +1051,76 @@ impl InlaySnapshot {
             buffer_chunk: None,
             output_offset: range.start,
             max_output_offset: range.end,
-            hint_highlight_style,
-            suggestion_highlight_style,
+            inlay_highlight_style: highlights.inlay_highlight_style,
+            suggestion_highlight_style: highlights.suggestion_highlight_style,
             highlight_endpoints: highlight_endpoints.into_iter().peekable(),
             active_highlights: Default::default(),
+            highlights,
             snapshot: self,
         }
     }
 
-    fn document_to_inlay_range(&self, range: &DocumentRange) -> Range<InlayOffset> {
-        match range {
-            DocumentRange::Text(text_range) => {
-                self.to_inlay_offset(text_range.start.to_offset(&self.buffer))
-                    ..self.to_inlay_offset(text_range.end.to_offset(&self.buffer))
-            }
-            DocumentRange::Inlay(inlay_range) => {
-                inlay_range.highlight_start..inlay_range.highlight_end
+    fn apply_text_highlights(
+        &self,
+        cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>,
+        range: &Range<InlayOffset>,
+        text_highlights: &TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
+        highlight_endpoints: &mut Vec<HighlightEndpoint>,
+    ) {
+        while cursor.start().0 < range.end {
+            let transform_start = self
+                .buffer
+                .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0)));
+            let transform_end =
+                {
+                    let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
+                    self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
+                        cursor.end(&()).0,
+                        cursor.start().0 + overshoot,
+                    )))
+                };
+
+            for (tag, text_highlights) in text_highlights.iter() {
+                let style = text_highlights.0;
+                let ranges = &text_highlights.1;
+
+                let start_ix = match ranges.binary_search_by(|probe| {
+                    let cmp = probe.end.cmp(&transform_start, &self.buffer);
+                    if cmp.is_gt() {
+                        cmp::Ordering::Greater
+                    } else {
+                        cmp::Ordering::Less
+                    }
+                }) {
+                    Ok(i) | Err(i) => i,
+                };
+                for range in &ranges[start_ix..] {
+                    if range.start.cmp(&transform_end, &self.buffer).is_ge() {
+                        break;
+                    }
+
+                    highlight_endpoints.push(HighlightEndpoint {
+                        offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)),
+                        is_start: true,
+                        tag: *tag,
+                        style,
+                    });
+                    highlight_endpoints.push(HighlightEndpoint {
+                        offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
+                        is_start: false,
+                        tag: *tag,
+                        style,
+                    });
+                }
             }
+
+            cursor.next(&());
         }
     }
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(Default::default()..self.len(), false, None, None, None)
+        self.chunks(Default::default()..self.len(), false, Highlights::default())
             .map(|chunk| chunk.text)
             .collect()
     }
@@ -1144,7 +1168,11 @@ fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{link_go_to_definition::InlayRange, InlayId, MultiBuffer};
+    use crate::{
+        display_map::{InlayHighlights, TextHighlights},
+        link_go_to_definition::InlayHighlight,
+        InlayId, MultiBuffer,
+    };
     use gpui::AppContext;
     use project::{InlayHint, InlayHintLabel, ResolveState};
     use rand::prelude::*;
@@ -1619,8 +1647,8 @@ mod tests {
                 })
                 .collect::<Vec<_>>();
             let mut expected_text = Rope::from(buffer_snapshot.text());
-            for (offset, inlay) in inlays.into_iter().rev() {
-                expected_text.replace(offset..offset, &inlay.text.to_string());
+            for (offset, inlay) in inlays.iter().rev() {
+                expected_text.replace(*offset..*offset, &inlay.text.to_string());
             }
             assert_eq!(inlay_snapshot.text(), expected_text.to_string());
 
@@ -1640,51 +1668,87 @@ mod tests {
                 );
             }
 
-            let mut highlights = TextHighlights::default();
-            let highlight_count = rng.gen_range(0_usize..10);
-            let mut highlight_ranges = (0..highlight_count)
+            let mut text_highlights = TextHighlights::default();
+            let text_highlight_count = rng.gen_range(0_usize..10);
+            let mut text_highlight_ranges = (0..text_highlight_count)
                 .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
                 .collect::<Vec<_>>();
-            highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
-            log::info!("highlighting ranges {:?}", highlight_ranges);
-            let highlight_ranges = if rng.gen_bool(0.5) {
-                highlight_ranges
-                    .into_iter()
-                    .map(|range| InlayRange {
-                        inlay_position: buffer_snapshot.anchor_before(range.start),
-                        highlight_start: inlay_snapshot.to_inlay_offset(range.start),
-                        highlight_end: inlay_snapshot.to_inlay_offset(range.end),
-                    })
-                    .map(DocumentRange::Inlay)
-                    .collect::<Vec<_>>()
-            } else {
-                highlight_ranges
-                    .into_iter()
-                    .map(|range| {
-                        buffer_snapshot.anchor_before(range.start)
-                            ..buffer_snapshot.anchor_after(range.end)
-                    })
-                    .map(DocumentRange::Text)
-                    .collect::<Vec<_>>()
-            };
-            highlights.insert(
+            text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
+            log::info!("highlighting text ranges {text_highlight_ranges:?}");
+            text_highlights.insert(
                 Some(TypeId::of::<()>()),
-                Arc::new((HighlightStyle::default(), highlight_ranges)),
+                Arc::new((
+                    HighlightStyle::default(),
+                    text_highlight_ranges
+                        .into_iter()
+                        .map(|range| {
+                            buffer_snapshot.anchor_before(range.start)
+                                ..buffer_snapshot.anchor_after(range.end)
+                        })
+                        .collect(),
+                )),
             );
 
+            let mut inlay_highlights = InlayHighlights::default();
+            if !inlays.is_empty() {
+                let inlay_highlight_count = rng.gen_range(0..inlays.len());
+                let mut inlay_indices = BTreeSet::default();
+                while inlay_indices.len() < inlay_highlight_count {
+                    inlay_indices.insert(rng.gen_range(0..inlays.len()));
+                }
+                let new_highlights = inlay_indices
+                    .into_iter()
+                    .filter_map(|i| {
+                        let (_, inlay) = &inlays[i];
+                        let inlay_text_len = inlay.text.len();
+                        match inlay_text_len {
+                            0 => None,
+                            1 => Some(InlayHighlight {
+                                inlay: inlay.id,
+                                inlay_position: inlay.position,
+                                range: 0..1,
+                            }),
+                            n => {
+                                let inlay_text = inlay.text.to_string();
+                                let mut highlight_end = rng.gen_range(1..n);
+                                let mut highlight_start = rng.gen_range(0..highlight_end);
+                                while !inlay_text.is_char_boundary(highlight_end) {
+                                    highlight_end += 1;
+                                }
+                                while !inlay_text.is_char_boundary(highlight_start) {
+                                    highlight_start -= 1;
+                                }
+                                Some(InlayHighlight {
+                                    inlay: inlay.id,
+                                    inlay_position: inlay.position,
+                                    range: highlight_start..highlight_end,
+                                })
+                            }
+                        }
+                    })
+                    .map(|highlight| (highlight.inlay, (HighlightStyle::default(), highlight)))
+                    .collect();
+                log::info!("highlighting inlay ranges {new_highlights:?}");
+                inlay_highlights.insert(TypeId::of::<()>(), new_highlights);
+            }
+
             for _ in 0..5 {
                 let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
                 end = expected_text.clip_offset(end, Bias::Right);
                 let mut start = rng.gen_range(0..=end);
                 start = expected_text.clip_offset(start, Bias::Right);
 
+                let range = InlayOffset(start)..InlayOffset(end);
+                log::info!("calling inlay_snapshot.chunks({range:?})");
                 let actual_text = inlay_snapshot
                     .chunks(
-                        InlayOffset(start)..InlayOffset(end),
+                        range,
                         false,
-                        Some(&highlights),
-                        None,
-                        None,
+                        Highlights {
+                            text_highlights: Some(&text_highlights),
+                            inlay_highlights: Some(&inlay_highlights),
+                            ..Highlights::default()
+                        },
                     )
                     .map(|chunk| chunk.text)
                     .collect::<String>();

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

@@ -1,9 +1,8 @@
 use super::{
     fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
-    TextHighlights,
+    Highlights,
 };
 use crate::MultiBufferSnapshot;
-use gpui::fonts::HighlightStyle;
 use language::{Chunk, Point};
 use std::{cmp, mem, num::NonZeroU32, ops::Range};
 use sum_tree::Bias;
@@ -68,9 +67,7 @@ impl TabMap {
                 'outer: for chunk in old_snapshot.fold_snapshot.chunks(
                     fold_edit.old.end..old_end_row_successor_offset,
                     false,
-                    None,
-                    None,
-                    None,
+                    Highlights::default(),
                 ) {
                     for (ix, _) in chunk.text.match_indices('\t') {
                         let offset_from_edit = offset_from_edit + (ix as u32);
@@ -183,7 +180,7 @@ impl TabSnapshot {
             self.max_point()
         };
         for c in self
-            .chunks(range.start..line_end, false, None, None, None)
+            .chunks(range.start..line_end, false, Highlights::default())
             .flat_map(|chunk| chunk.text.chars())
         {
             if c == '\n' {
@@ -200,9 +197,7 @@ impl TabSnapshot {
                 .chunks(
                     TabPoint::new(range.end.row(), 0)..range.end,
                     false,
-                    None,
-                    None,
-                    None,
+                    Highlights::default(),
                 )
                 .flat_map(|chunk| chunk.text.chars())
             {
@@ -223,9 +218,7 @@ impl TabSnapshot {
         &'a self,
         range: Range<TabPoint>,
         language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        hint_highlight_style: Option<HighlightStyle>,
-        suggestion_highlight_style: Option<HighlightStyle>,
+        highlights: Highlights<'a>,
     ) -> TabChunks<'a> {
         let (input_start, expanded_char_column, to_next_stop) =
             self.to_fold_point(range.start, Bias::Left);
@@ -245,9 +238,7 @@ impl TabSnapshot {
             fold_chunks: self.fold_snapshot.chunks(
                 input_start..input_end,
                 language_aware,
-                text_highlights,
-                hint_highlight_style,
-                suggestion_highlight_style,
+                highlights,
             ),
             input_column,
             column: expanded_char_column,
@@ -270,9 +261,13 @@ impl TabSnapshot {
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None)
-            .map(|chunk| chunk.text)
-            .collect()
+        self.chunks(
+            TabPoint::zero()..self.max_point(),
+            false,
+            Highlights::default(),
+        )
+        .map(|chunk| chunk.text)
+        .collect()
     }
 
     pub fn max_point(&self) -> TabPoint {
@@ -597,9 +592,7 @@ mod tests {
                     .chunks(
                         TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
                         false,
-                        None,
-                        None,
-                        None,
+                        Highlights::default(),
                     )
                     .map(|c| c.text)
                     .collect::<String>(),
@@ -674,7 +667,8 @@ mod tests {
             let mut chunks = Vec::new();
             let mut was_tab = false;
             let mut text = String::new();
-            for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) {
+            for chunk in snapshot.chunks(start..snapshot.max_point(), false, Highlights::default())
+            {
                 if chunk.is_tab != was_tab {
                     if !text.is_empty() {
                         chunks.push((mem::take(&mut text), was_tab));
@@ -743,7 +737,7 @@ mod tests {
             let expected_summary = TextSummary::from(expected_text.as_str());
             assert_eq!(
                 tabs_snapshot
-                    .chunks(start..end, false, None, None, None)
+                    .chunks(start..end, false, Highlights::default())
                     .map(|c| c.text)
                     .collect::<String>(),
                 expected_text,

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

@@ -1,13 +1,11 @@
 use super::{
     fold_map::FoldBufferRows,
     tab_map::{self, TabEdit, TabPoint, TabSnapshot},
-    TextHighlights,
+    Highlights,
 };
 use crate::MultiBufferSnapshot;
 use gpui::{
-    fonts::{FontId, HighlightStyle},
-    text_layout::LineWrapper,
-    AppContext, Entity, ModelContext, ModelHandle, Task,
+    fonts::FontId, text_layout::LineWrapper, AppContext, Entity, ModelContext, ModelHandle, Task,
 };
 use language::{Chunk, Point};
 use lazy_static::lazy_static;
@@ -444,9 +442,7 @@ impl WrapSnapshot {
                 let mut chunks = new_tab_snapshot.chunks(
                     TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
                     false,
-                    None,
-                    None,
-                    None,
+                    Highlights::default(),
                 );
                 let mut edit_transforms = Vec::<Transform>::new();
                 for _ in edit.new_rows.start..edit.new_rows.end {
@@ -575,9 +571,7 @@ impl WrapSnapshot {
         &'a self,
         rows: Range<u32>,
         language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        hint_highlight_style: Option<HighlightStyle>,
-        suggestion_highlight_style: Option<HighlightStyle>,
+        highlights: Highlights<'a>,
     ) -> WrapChunks<'a> {
         let output_start = WrapPoint::new(rows.start, 0);
         let output_end = WrapPoint::new(rows.end, 0);
@@ -594,9 +588,7 @@ impl WrapSnapshot {
             input_chunks: self.tab_snapshot.chunks(
                 input_start..input_end,
                 language_aware,
-                text_highlights,
-                hint_highlight_style,
-                suggestion_highlight_style,
+                highlights,
             ),
             input_chunk: Default::default(),
             output_position: output_start,
@@ -1323,9 +1315,7 @@ mod tests {
             self.chunks(
                 wrap_row..self.max_point().row() + 1,
                 false,
-                None,
-                None,
-                None,
+                Highlights::default(),
             )
             .map(|h| h.text)
         }
@@ -1350,7 +1340,7 @@ mod tests {
                 }
 
                 let actual_text = self
-                    .chunks(start_row..end_row, true, None, None, None)
+                    .chunks(start_row..end_row, true, Highlights::default())
                     .map(|c| c.text)
                     .collect::<String>();
                 assert_eq!(

crates/editor/src/editor.rs 🔗

@@ -66,7 +66,7 @@ use language::{
     TransactionId,
 };
 use link_go_to_definition::{
-    hide_link_definition, show_link_definition, DocumentRange, GoToDefinitionLink, InlayRange,
+    hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight,
     LinkGoToDefinitionState,
 };
 use log::error;
@@ -99,6 +99,7 @@ use std::{
     time::{Duration, Instant},
 };
 pub use sum_tree::Bias;
+use sum_tree::TreeMap;
 use text::Rope;
 use theme::{DiagnosticStyle, Theme, ThemeSettings};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
@@ -548,7 +549,8 @@ type CompletionId = usize;
 type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
 type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
 
-type BackgroundHighlight = (fn(&Theme) -> Color, Vec<DocumentRange>);
+type BackgroundHighlight = (fn(&Theme) -> Color, Vec<Range<Anchor>>);
+type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec<InlayHighlight>);
 
 pub struct Editor {
     handle: WeakViewHandle<Self>,
@@ -580,6 +582,7 @@ pub struct Editor {
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
     background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
+    inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
     nav_history: Option<ItemNavHistory>,
     context_menu: Option<ContextMenu>,
     mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
@@ -1523,6 +1526,7 @@ impl Editor {
             placeholder_text: None,
             highlighted_rows: None,
             background_highlights: Default::default(),
+            inlay_background_highlights: Default::default(),
             nav_history: None,
             context_menu: None,
             mouse_context_menu: cx
@@ -7066,16 +7070,8 @@ impl Editor {
             } else {
                 this.update(&mut cx, |this, cx| {
                     let buffer = this.buffer.read(cx).snapshot(cx);
-                    let display_snapshot = this
-                        .display_map
-                        .update(cx, |display_map, cx| display_map.snapshot(cx));
                     let mut buffer_highlights = this
-                        .document_highlights_for_position(
-                            selection.head(),
-                            &buffer,
-                            &display_snapshot,
-                        )
-                        .filter_map(|highlight| highlight.as_text_range())
+                        .document_highlights_for_position(selection.head(), &buffer)
                         .filter(|highlight| {
                             highlight.start.excerpt_id() == selection.head().excerpt_id()
                                 && highlight.end.excerpt_id() == selection.head().excerpt_id()
@@ -7130,15 +7126,11 @@ impl Editor {
                     let ranges = this
                         .clear_background_highlights::<DocumentHighlightWrite>(cx)
                         .into_iter()
-                        .flat_map(|(_, ranges)| {
-                            ranges.into_iter().filter_map(|range| range.as_text_range())
-                        })
+                        .flat_map(|(_, ranges)| ranges.into_iter())
                         .chain(
                             this.clear_background_highlights::<DocumentHighlightRead>(cx)
                                 .into_iter()
-                                .flat_map(|(_, ranges)| {
-                                    ranges.into_iter().filter_map(|range| range.as_text_range())
-                                }),
+                                .flat_map(|(_, ranges)| ranges.into_iter()),
                         )
                         .collect();
 
@@ -7239,7 +7231,7 @@ impl Editor {
             Some(Autoscroll::fit()),
             cx,
         );
-        self.clear_text_highlights::<Rename>(cx);
+        self.clear_highlights::<Rename>(cx);
         self.show_local_selections = true;
 
         if moving_cursor {
@@ -7816,29 +7808,20 @@ impl Editor {
         color_fetcher: fn(&Theme) -> Color,
         cx: &mut ViewContext<Self>,
     ) {
-        self.background_highlights.insert(
-            TypeId::of::<T>(),
-            (
-                color_fetcher,
-                ranges.into_iter().map(DocumentRange::Text).collect(),
-            ),
-        );
+        self.background_highlights
+            .insert(TypeId::of::<T>(), (color_fetcher, ranges));
         cx.notify();
     }
 
     pub fn highlight_inlay_background<T: 'static>(
         &mut self,
-        ranges: Vec<InlayRange>,
+        ranges: Vec<InlayHighlight>,
         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(),
-            ),
-        );
+        // TODO: no actual highlights happen for inlays currently, find a way to do that
+        self.inlay_background_highlights
+            .insert(Some(TypeId::of::<T>()), (color_fetcher, ranges));
         cx.notify();
     }
 
@@ -7846,15 +7829,18 @@ impl Editor {
         &mut self,
         cx: &mut ViewContext<Self>,
     ) -> Option<BackgroundHighlight> {
-        let highlights = self.background_highlights.remove(&TypeId::of::<T>());
-        if highlights.is_some() {
+        let text_highlights = self.background_highlights.remove(&TypeId::of::<T>());
+        let inlay_highlights = self
+            .inlay_background_highlights
+            .remove(&Some(TypeId::of::<T>()));
+        if text_highlights.is_some() || inlay_highlights.is_some() {
             cx.notify();
         }
-        highlights
+        text_highlights
     }
 
     #[cfg(feature = "test-support")]
-    pub fn all_background_highlights(
+    pub fn all_text_background_highlights(
         &mut self,
         cx: &mut ViewContext<Self>,
     ) -> Vec<(Range<DisplayPoint>, Color)> {
@@ -7870,8 +7856,7 @@ impl Editor {
         &'a self,
         position: Anchor,
         buffer: &'a MultiBufferSnapshot,
-        display_snapshot: &'a DisplaySnapshot,
-    ) -> impl 'a + Iterator<Item = &DocumentRange> {
+    ) -> impl 'a + Iterator<Item = &Range<Anchor>> {
         let read_highlights = self
             .background_highlights
             .get(&TypeId::of::<DocumentHighlightRead>())
@@ -7880,16 +7865,14 @@ impl Editor {
             .background_highlights
             .get(&TypeId::of::<DocumentHighlightWrite>())
             .map(|h| &h.1);
-        let left_position = display_snapshot.anchor_to_inlay_offset(position.bias_left(buffer));
-        let right_position = display_snapshot.anchor_to_inlay_offset(position.bias_right(buffer));
+        let left_position = position.bias_left(buffer);
+        let right_position = position.bias_right(buffer);
         read_highlights
             .into_iter()
             .chain(write_highlights)
             .flat_map(move |ranges| {
                 let start_ix = match ranges.binary_search_by(|probe| {
-                    let cmp = document_to_inlay_range(probe, display_snapshot)
-                        .end
-                        .cmp(&left_position);
+                    let cmp = probe.end.cmp(&left_position, buffer);
                     if cmp.is_ge() {
                         Ordering::Greater
                     } else {
@@ -7900,12 +7883,9 @@ impl Editor {
                 };
 
                 let right_position = right_position.clone();
-                ranges[start_ix..].iter().take_while(move |range| {
-                    document_to_inlay_range(range, display_snapshot)
-                        .start
-                        .cmp(&right_position)
-                        .is_le()
-                })
+                ranges[start_ix..]
+                    .iter()
+                    .take_while(move |range| range.start.cmp(&right_position, buffer).is_le())
             })
     }
 
@@ -7915,15 +7895,13 @@ impl Editor {
         display_snapshot: &DisplaySnapshot,
         theme: &Theme,
     ) -> Vec<(Range<DisplayPoint>, Color)> {
-        let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start)
-            ..display_snapshot.anchor_to_inlay_offset(search_range.end);
         let mut results = Vec::new();
         for (color_fetcher, ranges) in self.background_highlights.values() {
             let color = color_fetcher(theme);
             let start_ix = match ranges.binary_search_by(|probe| {
-                let cmp = document_to_inlay_range(probe, display_snapshot)
+                let cmp = probe
                     .end
-                    .cmp(&search_range.start);
+                    .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
                 if cmp.is_gt() {
                     Ordering::Greater
                 } else {
@@ -7933,13 +7911,16 @@ impl Editor {
                 Ok(i) | Err(i) => i,
             };
             for range in &ranges[start_ix..] {
-                let range = document_to_inlay_range(range, display_snapshot);
-                if range.start.cmp(&search_range.end).is_ge() {
+                if range
+                    .start
+                    .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
+                    .is_ge()
+                {
                     break;
                 }
 
-                let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left);
-                let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right);
+                let start = range.start.to_display_point(&display_snapshot);
+                let end = range.end.to_display_point(&display_snapshot);
                 results.push((start..end, color))
             }
         }
@@ -7952,17 +7933,15 @@ impl Editor {
         display_snapshot: &DisplaySnapshot,
         count: usize,
     ) -> Vec<RangeInclusive<DisplayPoint>> {
-        let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start)
-            ..display_snapshot.anchor_to_inlay_offset(search_range.end);
         let mut results = Vec::new();
         let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::<T>()) else {
             return vec![];
         };
 
         let start_ix = match ranges.binary_search_by(|probe| {
-            let cmp = document_to_inlay_range(probe, display_snapshot)
+            let cmp = probe
                 .end
-                .cmp(&search_range.start);
+                .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
             if cmp.is_gt() {
                 Ordering::Greater
             } else {
@@ -7985,22 +7964,20 @@ impl Editor {
             return Vec::new();
         }
         for range in &ranges[start_ix..] {
-            let range = document_to_inlay_range(range, display_snapshot);
-            if range.start.cmp(&search_range.end).is_ge() {
+            if range
+                .start
+                .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
+                .is_ge()
+            {
                 break;
             }
-            let end = display_snapshot
-                .inlay_offset_to_display_point(range.end, Bias::Right)
-                .to_point(display_snapshot);
+            let end = range.end.to_point(&display_snapshot.buffer_snapshot);
             if let Some(current_row) = &end_row {
                 if end.row == current_row.row {
                     continue;
                 }
             }
-            let start = display_snapshot
-                .inlay_offset_to_display_point(range.start, Bias::Left)
-                .to_point(display_snapshot);
-
+            let start = range.start.to_point(&display_snapshot.buffer_snapshot);
             if start_row.is_none() {
                 assert_eq!(end_row, None);
                 start_row = Some(start);
@@ -8039,12 +8016,12 @@ impl Editor {
 
     pub fn highlight_inlays<T: 'static>(
         &mut self,
-        ranges: Vec<InlayRange>,
+        highlights: Vec<InlayHighlight>,
         style: HighlightStyle,
         cx: &mut ViewContext<Self>,
     ) {
         self.display_map.update(cx, |map, _| {
-            map.highlight_inlays(TypeId::of::<T>(), ranges, style)
+            map.highlight_inlays(TypeId::of::<T>(), highlights, style)
         });
         cx.notify();
     }
@@ -8052,15 +8029,15 @@ impl Editor {
     pub fn text_highlights<'a, T: 'static>(
         &'a self,
         cx: &'a AppContext,
-    ) -> Option<(HighlightStyle, &'a [DocumentRange])> {
+    ) -> Option<(HighlightStyle, &'a [Range<Anchor>])> {
         self.display_map.read(cx).text_highlights(TypeId::of::<T>())
     }
 
-    pub fn clear_text_highlights<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
-        let text_highlights = self
+    pub fn clear_highlights<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
+        let cleared = self
             .display_map
-            .update(cx, |map, _| map.clear_text_highlights(TypeId::of::<T>()));
-        if text_highlights.is_some() {
+            .update(cx, |map, _| map.clear_highlights(TypeId::of::<T>()));
+        if cleared {
             cx.notify();
         }
     }
@@ -8277,7 +8254,6 @@ impl Editor {
         Some(
             ranges
                 .iter()
-                .filter_map(|range| range.as_text_range())
                 .map(move |range| {
                     range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot)
                 })
@@ -8492,19 +8468,6 @@ impl Editor {
     }
 }
 
-fn document_to_inlay_range(
-    range: &DocumentRange,
-    snapshot: &DisplaySnapshot,
-) -> Range<InlayOffset> {
-    match range {
-        DocumentRange::Text(text_range) => {
-            snapshot.anchor_to_inlay_offset(text_range.start)
-                ..snapshot.anchor_to_inlay_offset(text_range.end)
-        }
-        DocumentRange::Inlay(inlay_range) => inlay_range.highlight_start..inlay_range.highlight_end,
-    }
-}
-
 fn inlay_hint_settings(
     location: Anchor,
     snapshot: &MultiBufferSnapshot,
@@ -8715,7 +8678,7 @@ impl View for Editor {
 
             self.link_go_to_definition_state.task = None;
 
-            self.clear_text_highlights::<LinkGoToDefinitionState>(cx);
+            self.clear_highlights::<LinkGoToDefinitionState>(cx);
         }
 
         false
@@ -8784,12 +8747,11 @@ 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)?;
-        let range = range.as_text_range()?;
         Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0)
     }
 
     fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
-        self.clear_text_highlights::<InputComposition>(cx);
+        self.clear_highlights::<InputComposition>(cx);
         self.ime_transaction.take();
     }
 

crates/editor/src/hover_popover.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     display_map::{InlayOffset, ToDisplayPoint},
-    link_go_to_definition::{DocumentRange, InlayRange},
+    link_go_to_definition::{InlayHighlight, RangeInEditor},
     Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
     ExcerptId, RangeToAnchorExt,
 };
@@ -50,19 +50,18 @@ 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 range: InlayHighlight,
     pub tooltip: HoverBlock,
 }
 
 pub fn find_hovered_hint_part(
     label_parts: Vec<InlayHintLabelPart>,
-    hint_range: Range<InlayOffset>,
+    hint_start: InlayOffset,
     hovered_offset: InlayOffset,
 ) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
-    if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end {
-        let mut hovered_character = (hovered_offset - hint_range.start).0;
-        let mut part_start = hint_range.start;
+    if hovered_offset >= hint_start {
+        let mut hovered_character = (hovered_offset - hint_start).0;
+        let mut part_start = hint_start;
         for part in label_parts {
             let part_len = part.value.chars().count();
             if hovered_character > part_len {
@@ -88,10 +87,8 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
         };
 
         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)
-                {
+            if let RangeInEditor::Inlay(range) = symbol_range {
+                if range == &inlay_hover.range {
                     // Hover triggered from same location as last time. Don't show again.
                     return;
                 }
@@ -99,18 +96,6 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
             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()
@@ -122,7 +107,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
 
                 let hover_popover = InfoPopover {
                     project: project.clone(),
-                    symbol_range: DocumentRange::Inlay(inlay_hover.range),
+                    symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
                     blocks: vec![inlay_hover.tooltip],
                     language: None,
                     rendered_content: None,
@@ -326,7 +311,7 @@ fn show_hover(
 
                 Some(InfoPopover {
                     project: project.clone(),
-                    symbol_range: DocumentRange::Text(range),
+                    symbol_range: RangeInEditor::Text(range),
                     blocks: hover_result.contents,
                     language: hover_result.language,
                     rendered_content: None,
@@ -608,8 +593,8 @@ impl HoverState {
                 self.info_popover
                     .as_ref()
                     .map(|info_popover| match &info_popover.symbol_range {
-                        DocumentRange::Text(range) => &range.start,
-                        DocumentRange::Inlay(range) => &range.inlay_position,
+                        RangeInEditor::Text(range) => &range.start,
+                        RangeInEditor::Inlay(range) => &range.inlay_position,
                     })
             })?;
         let point = anchor.to_display_point(&snapshot.display_snapshot);
@@ -635,7 +620,7 @@ impl HoverState {
 #[derive(Debug, Clone)]
 pub struct InfoPopover {
     pub project: ModelHandle<Project>,
-    symbol_range: DocumentRange,
+    symbol_range: RangeInEditor,
     pub blocks: Vec<HoverBlock>,
     language: Option<Arc<Language>>,
     rendered_content: Option<RenderedInfo>,
@@ -811,6 +796,7 @@ mod tests {
         inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
         link_go_to_definition::update_inlay_link_and_hover_points,
         test::editor_lsp_test_context::EditorLspTestContext,
+        InlayId,
     };
     use collections::BTreeSet;
     use gpui::fonts::Weight;
@@ -1477,25 +1463,16 @@ mod tests {
             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
         cx.foreground().run_until_parked();
         cx.update_editor(|editor, cx| {
-            let snapshot = editor.snapshot(cx);
             let hover_state = &editor.hover_state;
             assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
             let popover = hover_state.info_popover.as_ref().unwrap();
             let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-            let entire_inlay_start = snapshot.display_point_to_inlay_offset(
-                inlay_range.start.to_display_point(&snapshot),
-                Bias::Left,
-            );
-
-            let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len());
             assert_eq!(
                 popover.symbol_range,
-                DocumentRange::Inlay(InlayRange {
+                RangeInEditor::Inlay(InlayHighlight {
+                    inlay: InlayId::Hint(0),
                     inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-                    highlight_start: expected_new_type_label_start,
-                    highlight_end: InlayOffset(
-                        expected_new_type_label_start.0 + new_type_label.len()
-                    ),
+                    range: ": ".len()..": ".len() + new_type_label.len(),
                 }),
                 "Popover range should match the new type label part"
             );
@@ -1543,23 +1520,17 @@ mod tests {
             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
         cx.foreground().run_until_parked();
         cx.update_editor(|editor, cx| {
-            let snapshot = editor.snapshot(cx);
             let hover_state = &editor.hover_state;
             assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
             let popover = hover_state.info_popover.as_ref().unwrap();
             let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-            let entire_inlay_start = snapshot.display_point_to_inlay_offset(
-                inlay_range.start.to_display_point(&snapshot),
-                Bias::Left,
-            );
-            let expected_struct_label_start =
-                InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len());
             assert_eq!(
                 popover.symbol_range,
-                DocumentRange::Inlay(InlayRange {
+                RangeInEditor::Inlay(InlayHighlight {
+                    inlay: InlayId::Hint(0),
                     inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-                    highlight_start: expected_struct_label_start,
-                    highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()),
+                    range: ": ".len() + new_type_label.len() + "<".len()
+                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
                 }),
                 "Popover range should match the struct label part"
             );

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -43,7 +43,8 @@ pub struct CachedExcerptHints {
     version: usize,
     buffer_version: Global,
     buffer_id: u64,
-    hints: Vec<(InlayId, InlayHint)>,
+    ordered_hints: Vec<InlayId>,
+    hints_by_id: HashMap<InlayId, InlayHint>,
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -316,7 +317,7 @@ impl InlayHintCache {
             self.hints.retain(|cached_excerpt, cached_hints| {
                 let retain = excerpts_to_query.contains_key(cached_excerpt);
                 if !retain {
-                    invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id));
+                    invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied());
                 }
                 retain
             });
@@ -384,7 +385,7 @@ impl InlayHintCache {
             let shown_excerpt_hints_to_remove =
                 shown_hints_to_remove.entry(*excerpt_id).or_default();
             let excerpt_cached_hints = excerpt_cached_hints.read();
-            let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable();
+            let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
             shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
                 let Some(buffer) = shown_anchor
                     .buffer_id
@@ -395,7 +396,8 @@ impl InlayHintCache {
                 let buffer_snapshot = buffer.read(cx).snapshot();
                 loop {
                     match excerpt_cache.peek() {
-                        Some((cached_hint_id, cached_hint)) => {
+                        Some(&cached_hint_id) => {
+                            let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
                             if cached_hint_id == shown_hint_id {
                                 excerpt_cache.next();
                                 return !new_kinds.contains(&cached_hint.kind);
@@ -428,7 +430,8 @@ impl InlayHintCache {
                 }
             });
 
-            for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
+            for cached_hint_id in excerpt_cache {
+                let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
                 let cached_hint_kind = maybe_missed_cached_hint.kind;
                 if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
                     to_insert.push(Inlay::hint(
@@ -463,7 +466,7 @@ impl InlayHintCache {
             self.update_tasks.remove(&excerpt_to_remove);
             if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) {
                 let cached_hints = cached_hints.read();
-                to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id));
+                to_remove.extend(cached_hints.ordered_hints.iter().copied());
             }
         }
         if to_remove.is_empty() {
@@ -489,10 +492,8 @@ impl InlayHintCache {
         self.hints
             .get(&excerpt_id)?
             .read()
-            .hints
-            .iter()
-            .find(|&(id, _)| id == &hint_id)
-            .map(|(_, hint)| hint)
+            .hints_by_id
+            .get(&hint_id)
             .cloned()
     }
 
@@ -500,7 +501,13 @@ impl InlayHintCache {
         let mut hints = Vec::new();
         for excerpt_hints in self.hints.values() {
             let excerpt_hints = excerpt_hints.read();
-            hints.extend(excerpt_hints.hints.iter().map(|(_, hint)| hint).cloned());
+            hints.extend(
+                excerpt_hints
+                    .ordered_hints
+                    .iter()
+                    .map(|id| &excerpt_hints.hints_by_id[id])
+                    .cloned(),
+            );
         }
         hints
     }
@@ -518,12 +525,7 @@ impl InlayHintCache {
     ) {
         if let Some(excerpt_hints) = self.hints.get(&excerpt_id) {
             let mut guard = excerpt_hints.write();
-            if let Some(cached_hint) = guard
-                .hints
-                .iter_mut()
-                .find(|(hint_id, _)| hint_id == &id)
-                .map(|(_, hint)| hint)
-            {
+            if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
                 if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state {
                     let hint_to_resolve = cached_hint.clone();
                     let server_id = *server_id;
@@ -555,12 +557,7 @@ impl InlayHintCache {
                                     editor.inlay_hint_cache.hints.get(&excerpt_id)
                                 {
                                     let mut guard = excerpt_hints.write();
-                                    if let Some(cached_hint) = guard
-                                        .hints
-                                        .iter_mut()
-                                        .find(|(hint_id, _)| hint_id == &id)
-                                        .map(|(_, hint)| hint)
-                                    {
+                                    if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
                                         if cached_hint.resolve_state == ResolveState::Resolving {
                                             resolved_hint.resolve_state = ResolveState::Resolved;
                                             *cached_hint = resolved_hint;
@@ -986,12 +983,17 @@ fn calculate_hint_updates(
         let missing_from_cache = match &cached_excerpt_hints {
             Some(cached_excerpt_hints) => {
                 let cached_excerpt_hints = cached_excerpt_hints.read();
-                match cached_excerpt_hints.hints.binary_search_by(|probe| {
-                    probe.1.position.cmp(&new_hint.position, buffer_snapshot)
-                }) {
+                match cached_excerpt_hints
+                    .ordered_hints
+                    .binary_search_by(|probe| {
+                        cached_excerpt_hints.hints_by_id[probe]
+                            .position
+                            .cmp(&new_hint.position, buffer_snapshot)
+                    }) {
                     Ok(ix) => {
                         let mut missing_from_cache = true;
-                        for (cached_inlay_id, cached_hint) in &cached_excerpt_hints.hints[ix..] {
+                        for id in &cached_excerpt_hints.ordered_hints[ix..] {
+                            let cached_hint = &cached_excerpt_hints.hints_by_id[id];
                             if new_hint
                                 .position
                                 .cmp(&cached_hint.position, buffer_snapshot)
@@ -1000,7 +1002,7 @@ fn calculate_hint_updates(
                                 break;
                             }
                             if cached_hint == &new_hint {
-                                excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
+                                excerpt_hints_to_persist.insert(*id, cached_hint.kind);
                                 missing_from_cache = false;
                             }
                         }
@@ -1031,12 +1033,12 @@ fn calculate_hint_updates(
             let cached_excerpt_hints = cached_excerpt_hints.read();
             remove_from_cache.extend(
                 cached_excerpt_hints
-                    .hints
+                    .ordered_hints
                     .iter()
-                    .filter(|(cached_inlay_id, _)| {
+                    .filter(|cached_inlay_id| {
                         !excerpt_hints_to_persist.contains_key(cached_inlay_id)
                     })
-                    .map(|(cached_inlay_id, _)| *cached_inlay_id),
+                    .copied(),
             );
         }
     }
@@ -1080,7 +1082,8 @@ fn apply_hint_update(
                 version: query.cache_version,
                 buffer_version: buffer_snapshot.version().clone(),
                 buffer_id: query.buffer_id,
-                hints: Vec::new(),
+                ordered_hints: Vec::new(),
+                hints_by_id: HashMap::default(),
             }))
         });
     let mut cached_excerpt_hints = cached_excerpt_hints.write();
@@ -1093,20 +1096,27 @@ fn apply_hint_update(
 
     let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
     cached_excerpt_hints
-        .hints
-        .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
+        .ordered_hints
+        .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id));
+    cached_excerpt_hints
+        .hints_by_id
+        .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
     let mut splice = InlaySplice {
         to_remove: new_update.remove_from_visible,
         to_insert: Vec::new(),
     };
     for new_hint in new_update.add_to_cache {
-        let cached_hints = &mut cached_excerpt_hints.hints;
-        let insert_position = match cached_hints
-            .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot))
-        {
+        let insert_position = match cached_excerpt_hints
+            .ordered_hints
+            .binary_search_by(|probe| {
+                cached_excerpt_hints.hints_by_id[probe]
+                    .position
+                    .cmp(&new_hint.position, &buffer_snapshot)
+            }) {
             Ok(i) => {
                 let mut insert_position = Some(i);
-                for (_, cached_hint) in &cached_hints[i..] {
+                for id in &cached_excerpt_hints.ordered_hints[i..] {
+                    let cached_hint = &cached_excerpt_hints.hints_by_id[id];
                     if new_hint
                         .position
                         .cmp(&cached_hint.position, &buffer_snapshot)
@@ -1137,7 +1147,11 @@ fn apply_hint_update(
                     .to_insert
                     .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
             }
-            cached_hints.insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint));
+            let new_id = InlayId::Hint(new_inlay_id);
+            cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
+            cached_excerpt_hints
+                .ordered_hints
+                .insert(insert_position, new_id);
             cached_inlays_changed = true;
         }
     }
@@ -1157,7 +1171,7 @@ fn apply_hint_update(
                 outdated_excerpt_caches.insert(*excerpt_id);
                 splice
                     .to_remove
-                    .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
+                    .extend(excerpt_hints.ordered_hints.iter().copied());
             }
         }
         cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
@@ -3311,8 +3325,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
     pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
         let mut labels = Vec::new();
         for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
-            for (_, inlay) in &excerpt_hints.read().hints {
-                labels.push(inlay.text());
+            let excerpt_hints = excerpt_hints.read();
+            for id in &excerpt_hints.ordered_hints {
+                labels.push(excerpt_hints.hints_by_id[id].text());
             }
         }
 
@@ -1,8 +1,8 @@
 use crate::{
-    display_map::{DisplaySnapshot, InlayOffset},
+    display_map::DisplaySnapshot,
     element::PointForPosition,
     hover_popover::{self, InlayHover},
-    Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase,
+    Anchor, DisplayPoint, Editor, EditorSnapshot, InlayId, SelectPhase,
 };
 use gpui::{Task, ViewContext};
 use language::{Bias, ToOffset};
@@ -17,44 +17,19 @@ use util::TryFutureExt;
 #[derive(Debug, Default)]
 pub struct LinkGoToDefinitionState {
     pub last_trigger_point: Option<TriggerPoint>,
-    pub symbol_range: Option<DocumentRange>,
+    pub symbol_range: Option<RangeInEditor>,
     pub kind: Option<LinkDefinitionKind>,
     pub definitions: Vec<GoToDefinitionLink>,
     pub task: Option<Task<Option<()>>>,
 }
 
-#[derive(Debug)]
-pub enum GoToDefinitionTrigger {
-    Text(DisplayPoint),
-    InlayHint(InlayRange, lsp::Location, LanguageServerId),
-}
-
-#[derive(Debug, Clone)]
-pub enum GoToDefinitionLink {
-    Text(LocationLink),
-    InlayHint(lsp::Location, LanguageServerId),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct InlayRange {
-    pub inlay_position: Anchor,
-    pub highlight_start: InlayOffset,
-    pub highlight_end: InlayOffset,
-}
-
-#[derive(Debug, Clone)]
-pub enum TriggerPoint {
-    Text(Anchor),
-    InlayHint(InlayRange, lsp::Location, LanguageServerId),
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum DocumentRange {
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub enum RangeInEditor {
     Text(Range<Anchor>),
-    Inlay(InlayRange),
+    Inlay(InlayHighlight),
 }
 
-impl DocumentRange {
+impl RangeInEditor {
     pub fn as_text_range(&self) -> Option<Range<Anchor>> {
         match self {
             Self::Text(range) => Some(range.clone()),
@@ -64,28 +39,47 @@ impl DocumentRange {
 
     fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
         match (self, trigger_point) {
-            (DocumentRange::Text(range), TriggerPoint::Text(point)) => {
+            (Self::Text(range), TriggerPoint::Text(point)) => {
                 let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
                 point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge()
             }
-            (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _, _)) => {
-                range.highlight_start.cmp(&point.highlight_end).is_le()
-                    && range.highlight_end.cmp(&point.highlight_end).is_ge()
+            (Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => {
+                highlight.inlay == point.inlay
+                    && highlight.range.contains(&point.range.start)
+                    && highlight.range.contains(&point.range.end)
             }
-            (DocumentRange::Inlay(_), TriggerPoint::Text(_))
-            | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
+            (Self::Inlay(_), TriggerPoint::Text(_))
+            | (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
         }
     }
 }
 
-impl TriggerPoint {
-    fn anchor(&self) -> &Anchor {
-        match self {
-            TriggerPoint::Text(anchor) => anchor,
-            TriggerPoint::InlayHint(range, _, _) => &range.inlay_position,
-        }
-    }
+#[derive(Debug)]
+pub enum GoToDefinitionTrigger {
+    Text(DisplayPoint),
+    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
+}
+
+#[derive(Debug, Clone)]
+pub enum GoToDefinitionLink {
+    Text(LocationLink),
+    InlayHint(lsp::Location, LanguageServerId),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct InlayHighlight {
+    pub inlay: InlayId,
+    pub inlay_position: Anchor,
+    pub range: Range<usize>,
+}
+
+#[derive(Debug, Clone)]
+pub enum TriggerPoint {
+    Text(Anchor),
+    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
+}
 
+impl TriggerPoint {
     pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind {
         match self {
             TriggerPoint::Text(_) => {
@@ -98,6 +92,13 @@ impl TriggerPoint {
             TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type,
         }
     }
+
+    fn anchor(&self) -> &Anchor {
+        match self {
+            TriggerPoint::Text(anchor) => anchor,
+            TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position,
+        }
+    }
 }
 
 pub fn update_go_to_definition_link(
@@ -135,11 +136,7 @@ pub fn update_go_to_definition_link(
                 }
             }
             (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => {
-                if range_a
-                    .inlay_position
-                    .cmp(&range_b.inlay_position, &snapshot.buffer_snapshot)
-                    .is_eq()
-                {
+                if range_a == range_b {
                     return;
                 }
             }
@@ -173,10 +170,6 @@ pub fn update_inlay_link_and_hover_points(
     shift_held: bool,
     cx: &mut ViewContext<'_, '_, Editor>,
 ) {
-    let hint_start_offset =
-        snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left);
-    let hint_end_offset =
-        snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right);
     let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
         Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
     } else {
@@ -224,15 +217,14 @@ pub fn update_inlay_link_and_hover_points(
                         }
                     }
                     ResolveState::Resolved => {
-                        let mut actual_hint_start = hint_start_offset;
-                        let mut actual_hint_end = hint_end_offset;
+                        let mut extra_shift_left = 0;
+                        let mut extra_shift_right = 0;
                         if cached_hint.padding_left {
-                            actual_hint_start.0 += 1;
-                            actual_hint_end.0 += 1;
+                            extra_shift_left += 1;
+                            extra_shift_right += 1;
                         }
                         if cached_hint.padding_right {
-                            actual_hint_start.0 += 1;
-                            actual_hint_end.0 += 1;
+                            extra_shift_right += 1;
                         }
                         match cached_hint.label {
                             project::InlayHintLabel::String(_) => {
@@ -253,11 +245,11 @@ pub fn update_inlay_link_and_hover_points(
                                                     }
                                                 }
                                             },
-                                            triggered_from: hovered_offset,
-                                            range: InlayRange {
+                                            range: InlayHighlight {
+                                                inlay: hovered_hint.id,
                                                 inlay_position: hovered_hint.position,
-                                                highlight_start: actual_hint_start,
-                                                highlight_end: actual_hint_end,
+                                                range: extra_shift_left
+                                                    ..hovered_hint.text.len() + extra_shift_right,
                                             },
                                         },
                                         cx,
@@ -266,13 +258,24 @@ pub fn update_inlay_link_and_hover_points(
                                 }
                             }
                             project::InlayHintLabel::LabelParts(label_parts) => {
+                                let hint_start =
+                                    snapshot.anchor_to_inlay_offset(hovered_hint.position);
                                 if let Some((hovered_hint_part, part_range)) =
                                     hover_popover::find_hovered_hint_part(
                                         label_parts,
-                                        actual_hint_start..actual_hint_end,
+                                        hint_start,
                                         hovered_offset,
                                     )
                                 {
+                                    let highlight_start =
+                                        (part_range.start - hint_start).0 + extra_shift_left;
+                                    let highlight_end =
+                                        (part_range.end - hint_start).0 + extra_shift_right;
+                                    let highlight = InlayHighlight {
+                                        inlay: hovered_hint.id,
+                                        inlay_position: hovered_hint.position,
+                                        range: highlight_start..highlight_end,
+                                    };
                                     if let Some(tooltip) = hovered_hint_part.tooltip {
                                         hover_popover::hover_at_inlay(
                                             editor,
@@ -292,12 +295,7 @@ pub fn update_inlay_link_and_hover_points(
                                                         kind: content.kind,
                                                     },
                                                 },
-                                                triggered_from: hovered_offset,
-                                                range: InlayRange {
-                                                    inlay_position: hovered_hint.position,
-                                                    highlight_start: part_range.start,
-                                                    highlight_end: part_range.end,
-                                                },
+                                                range: highlight.clone(),
                                             },
                                             cx,
                                         );
@@ -310,11 +308,7 @@ pub fn update_inlay_link_and_hover_points(
                                         update_go_to_definition_link(
                                             editor,
                                             Some(GoToDefinitionTrigger::InlayHint(
-                                                InlayRange {
-                                                    inlay_position: hovered_hint.position,
-                                                    highlight_start: part_range.start,
-                                                    highlight_end: part_range.end,
-                                                },
+                                                highlight,
                                                 location,
                                                 language_server_id,
                                             )),
@@ -425,7 +419,7 @@ pub fn show_link_definition(
                                     let end = snapshot
                                         .buffer_snapshot
                                         .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
-                                    DocumentRange::Text(start..end)
+                                    RangeInEditor::Text(start..end)
                                 })
                             }),
                             definition_result
@@ -435,8 +429,8 @@ pub fn show_link_definition(
                         )
                     })
                 }
-                TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => Some((
-                    Some(DocumentRange::Inlay(*trigger_source)),
+                TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
+                    Some(RangeInEditor::Inlay(highlight.clone())),
                     vec![GoToDefinitionLink::InlayHint(
                         lsp_location.clone(),
                         *server_id,
@@ -446,7 +440,7 @@ pub fn show_link_definition(
 
             this.update(&mut cx, |this, cx| {
                 // Clear any existing highlights
-                this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
+                this.clear_highlights::<LinkGoToDefinitionState>(cx);
                 this.link_go_to_definition_state.kind = Some(definition_kind);
                 this.link_go_to_definition_state.symbol_range = result
                     .as_ref()
@@ -498,26 +492,26 @@ pub fn show_link_definition(
                                     // If no symbol range returned from language server, use the surrounding word.
                                     let (offset_range, _) =
                                         snapshot.surrounding_word(*trigger_anchor);
-                                    DocumentRange::Text(
+                                    RangeInEditor::Text(
                                         snapshot.anchor_before(offset_range.start)
                                             ..snapshot.anchor_after(offset_range.end),
                                     )
                                 }
-                                TriggerPoint::InlayHint(inlay_coordinates, _, _) => {
-                                    DocumentRange::Inlay(*inlay_coordinates)
+                                TriggerPoint::InlayHint(highlight, _, _) => {
+                                    RangeInEditor::Inlay(highlight.clone())
                                 }
                             });
 
                         match highlight_range {
-                            DocumentRange::Text(text_range) => this
+                            RangeInEditor::Text(text_range) => this
                                 .highlight_text::<LinkGoToDefinitionState>(
                                     vec![text_range],
                                     style,
                                     cx,
                                 ),
-                            DocumentRange::Inlay(inlay_coordinates) => this
+                            RangeInEditor::Inlay(highlight) => this
                                 .highlight_inlays::<LinkGoToDefinitionState>(
-                                    vec![inlay_coordinates],
+                                    vec![highlight],
                                     style,
                                     cx,
                                 ),
@@ -547,7 +541,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
 
     editor.link_go_to_definition_state.task = None;
 
-    editor.clear_text_highlights::<LinkGoToDefinitionState>(cx);
+    editor.clear_highlights::<LinkGoToDefinitionState>(cx);
 }
 
 pub fn go_to_fetched_definition(
@@ -1199,30 +1193,19 @@ mod tests {
         cx.foreground().run_until_parked();
         cx.update_editor(|editor, cx| {
             let snapshot = editor.snapshot(cx);
-            let actual_ranges = snapshot
-                .highlight_ranges::<LinkGoToDefinitionState>()
-                .map(|ranges| ranges.as_ref().clone().1)
-                .unwrap_or_default()
+            let actual_highlights = snapshot
+                .inlay_highlights::<LinkGoToDefinitionState>()
                 .into_iter()
-                .map(|range| match range {
-                    DocumentRange::Text(range) => {
-                        panic!("Unexpected regular text selection range {range:?}")
-                    }
-                    DocumentRange::Inlay(inlay_range) => inlay_range,
-                })
+                .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
                 .collect::<Vec<_>>();
 
             let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-            let expected_highlight_start = snapshot.display_point_to_inlay_offset(
-                inlay_range.start.to_display_point(&snapshot),
-                Bias::Left,
-            );
-            let expected_ranges = vec![InlayRange {
+            let expected_highlight = InlayHighlight {
+                inlay: InlayId::Hint(0),
                 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-                highlight_start: expected_highlight_start,
-                highlight_end: InlayOffset(expected_highlight_start.0 + hint_label.len()),
-            }];
-            assert_set_eq!(actual_ranges, expected_ranges);
+                range: 0..hint_label.len(),
+            };
+            assert_set_eq!(actual_highlights, vec![&expected_highlight]);
         });
 
         // Unpress cmd causes highlight to go away
@@ -1242,17 +1225,9 @@ mod tests {
         cx.update_editor(|editor, cx| {
             let snapshot = editor.snapshot(cx);
             let actual_ranges = snapshot
-                .highlight_ranges::<LinkGoToDefinitionState>()
+                .text_highlight_ranges::<LinkGoToDefinitionState>()
                 .map(|ranges| ranges.as_ref().clone().1)
-                .unwrap_or_default()
-                .into_iter()
-                .map(|range| match range {
-                    DocumentRange::Text(range) => {
-                        panic!("Unexpected regular text selection range {range:?}")
-                    }
-                    DocumentRange::Inlay(inlay_range) => inlay_range,
-                })
-                .collect::<Vec<_>>();
+                .unwrap_or_default();
 
             assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
         });

crates/editor/src/test/editor_test_context.rs 🔗

@@ -225,7 +225,6 @@ impl<'a> EditorTestContext<'a> {
                 .map(|h| h.1.clone())
                 .unwrap_or_default()
                 .into_iter()
-                .filter_map(|range| range.as_text_range())
                 .map(|range| range.to_offset(&snapshot.buffer_snapshot))
                 .collect()
         });
@@ -237,11 +236,10 @@ impl<'a> EditorTestContext<'a> {
         let expected_ranges = self.ranges(marked_text);
         let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
         let actual_ranges: Vec<Range<usize>> = snapshot
-            .highlight_ranges::<Tag>()
+            .text_highlight_ranges::<Tag>()
             .map(|ranges| ranges.as_ref().clone().1)
             .unwrap_or_default()
             .into_iter()
-            .filter_map(|range| range.as_text_range())
             .map(|range| range.to_offset(&snapshot.buffer_snapshot))
             .collect();
         assert_set_eq!(actual_ranges, expected_ranges);

crates/search/src/buffer_search.rs 🔗

@@ -1012,7 +1012,7 @@ mod tests {
             .unwrap();
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[
                     (
                         DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
@@ -1033,7 +1033,7 @@ mod tests {
         editor.next_notification(cx).await;
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[(
                     DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
                     Color::red(),
@@ -1049,7 +1049,7 @@ mod tests {
             .unwrap();
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[
                     (
                         DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
@@ -1090,7 +1090,7 @@ mod tests {
         editor.next_notification(cx).await;
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[
                     (
                         DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
@@ -1301,7 +1301,7 @@ mod tests {
             .unwrap();
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[(
                     DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
                     Color::red(),
@@ -1328,7 +1328,7 @@ mod tests {
         editor.next_notification(cx).await;
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[(
                     DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
                     Color::red(),

crates/search/src/project_search.rs 🔗

@@ -1725,7 +1725,7 @@ pub mod tests {
             assert_eq!(
                 search_view
                     .results_editor
-                    .update(cx, |editor, cx| editor.all_background_highlights(cx)),
+                    .update(cx, |editor, cx| editor.all_text_background_highlights(cx)),
                 &[
                     (
                         DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35),

crates/vim/src/normal/search.rs 🔗

@@ -227,7 +227,7 @@ mod test {
         deterministic.run_until_parked();
 
         cx.update_editor(|editor, cx| {
-            let highlights = editor.all_background_highlights(cx);
+            let highlights = editor.all_text_background_highlights(cx);
             assert_eq!(3, highlights.len());
             assert_eq!(
                 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),

crates/vim/src/state.rs 🔗

@@ -186,7 +186,6 @@ impl EditorState {
         if self.active_operator().is_none() && self.pre_count.is_some()
             || self.active_operator().is_some() && self.post_count.is_some()
         {
-            dbg!("VimCount");
             context.add_identifier("VimCount");
         }
 

crates/vim/src/test.rs 🔗

@@ -190,7 +190,7 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
     search_bar.next_notification(&cx).await;
 
     cx.update_editor(|editor, cx| {
-        let highlights = editor.all_background_highlights(cx);
+        let highlights = editor.all_text_background_highlights(cx);
         assert_eq!(3, highlights.len());
         assert_eq!(
             DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),