@@ -105,6 +105,7 @@ use multi_buffer::{
use project::project_settings::DiagnosticSeverity;
use project::{InlayId, lsp_store::LspFoldingRange, lsp_store::TokenType};
use serde::Deserialize;
+use smallvec::SmallVec;
use sum_tree::{Bias, TreeMap};
use text::{BufferId, LineIndent, Patch, ToOffset as _};
use ui::{SharedString, px};
@@ -1694,6 +1695,38 @@ impl DisplaySnapshot {
DisplayPoint(block_point)
}
+ /// Converts a buffer offset range into one or more `DisplayPoint` ranges
+ /// that cover only actual buffer text, excluding any inlay hint text that
+ /// falls within the range.
+ pub fn isomorphic_display_point_ranges_for_buffer_range(
+ &self,
+ range: Range<MultiBufferOffset>,
+ ) -> SmallVec<[Range<DisplayPoint>; 1]> {
+ let inlay_snapshot = self.inlay_snapshot();
+ inlay_snapshot
+ .buffer_offset_to_inlay_ranges(range)
+ .map(|inlay_range| {
+ let inlay_point_to_display_point = |inlay_point: InlayPoint, bias: Bias| {
+ let fold_point = self.fold_snapshot().to_fold_point(inlay_point, bias);
+ let tab_point = self.tab_snapshot().fold_point_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)
+ };
+
+ let start = inlay_point_to_display_point(
+ inlay_snapshot.to_point(inlay_range.start),
+ Bias::Left,
+ );
+ let end = inlay_point_to_display_point(
+ inlay_snapshot.to_point(inlay_range.end),
+ Bias::Left,
+ );
+ start..end
+ })
+ .collect()
+ }
+
pub fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
self.inlay_snapshot()
.to_buffer_point(self.display_point_to_inlay_point(point, bias))
@@ -3956,4 +3989,88 @@ pub mod tests {
store.update_user_settings(cx, f);
});
}
+
+ #[gpui::test]
+ fn test_isomorphic_display_point_ranges_for_buffer_range(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| init_test(cx, |_| {}));
+
+ let buffer = cx.new(|cx| Buffer::local("let x = 5;\n", cx));
+ let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+ let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+
+ let font_size = px(14.0);
+ let map = cx.new(|cx| {
+ DisplayMap::new(
+ buffer.clone(),
+ font("Helvetica"),
+ font_size,
+ None,
+ 1,
+ 1,
+ FoldPlaceholder::test(),
+ DiagnosticSeverity::Warning,
+ cx,
+ )
+ });
+
+ // Without inlays, a buffer range maps to a single display range.
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ let ranges = snapshot.isomorphic_display_point_ranges_for_buffer_range(
+ MultiBufferOffset(4)..MultiBufferOffset(9),
+ );
+ assert_eq!(ranges.len(), 1);
+ // "x = 5" is columns 4..9 with no inlays shifting anything.
+ assert_eq!(ranges[0].start, DisplayPoint::new(DisplayRow(0), 4));
+ assert_eq!(ranges[0].end, DisplayPoint::new(DisplayRow(0), 9));
+
+ // Insert a 4-char inlay hint ": i32" at buffer offset 5 (after "x").
+ map.update(cx, |map, cx| {
+ map.splice_inlays(
+ &[],
+ vec![Inlay::mock_hint(
+ 0,
+ buffer_snapshot.anchor_after(MultiBufferOffset(5)),
+ ": i32",
+ )],
+ cx,
+ );
+ });
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ assert_eq!(snapshot.text(), "let x: i32 = 5;\n");
+
+ // A buffer range [4..9] ("x = 5") now spans across the inlay.
+ // It should be split into two display ranges that skip the inlay text.
+ let ranges = snapshot.isomorphic_display_point_ranges_for_buffer_range(
+ MultiBufferOffset(4)..MultiBufferOffset(9),
+ );
+ assert_eq!(
+ ranges.len(),
+ 2,
+ "expected the range to be split around the inlay, got: {:?}",
+ ranges,
+ );
+ // First sub-range: buffer [4, 5) → "x" at display columns 4..5
+ assert_eq!(ranges[0].start, DisplayPoint::new(DisplayRow(0), 4));
+ assert_eq!(ranges[0].end, DisplayPoint::new(DisplayRow(0), 5));
+ // Second sub-range: buffer [5, 9) → " = 5" at display columns 10..14
+ // (shifted right by the 5-char ": i32" inlay)
+ assert_eq!(ranges[1].start, DisplayPoint::new(DisplayRow(0), 10));
+ assert_eq!(ranges[1].end, DisplayPoint::new(DisplayRow(0), 14));
+
+ // A range entirely before the inlay is not split.
+ let ranges = snapshot.isomorphic_display_point_ranges_for_buffer_range(
+ MultiBufferOffset(0)..MultiBufferOffset(5),
+ );
+ assert_eq!(ranges.len(), 1);
+ assert_eq!(ranges[0].start, DisplayPoint::new(DisplayRow(0), 0));
+ assert_eq!(ranges[0].end, DisplayPoint::new(DisplayRow(0), 5));
+
+ // A range entirely after the inlay is not split.
+ let ranges = snapshot.isomorphic_display_point_ranges_for_buffer_range(
+ MultiBufferOffset(5)..MultiBufferOffset(9),
+ );
+ assert_eq!(ranges.len(), 1);
+ assert_eq!(ranges[0].start, DisplayPoint::new(DisplayRow(0), 10));
+ assert_eq!(ranges[0].end, DisplayPoint::new(DisplayRow(0), 14));
+ }
}
@@ -938,6 +938,51 @@ impl InlaySnapshot {
self.inlay_point_cursor().map(point)
}
+ /// Converts a buffer offset range into one or more `InlayOffset` ranges that
+ /// cover only the actual buffer text, skipping any inlay hint text that falls
+ /// within the range. When there are no inlays the returned vec contains a
+ /// single element identical to the input mapped into inlay-offset space.
+ pub fn buffer_offset_to_inlay_ranges(
+ &self,
+ range: Range<MultiBufferOffset>,
+ ) -> impl Iterator<Item = Range<InlayOffset>> {
+ let mut cursor = self
+ .transforms
+ .cursor::<Dimensions<MultiBufferOffset, InlayOffset>>(());
+ cursor.seek(&range.start, Bias::Right);
+
+ std::iter::from_fn(move || {
+ loop {
+ match cursor.item()? {
+ Transform::Isomorphic(_) => {
+ let seg_buffer_start = cursor.start().0;
+ let seg_buffer_end = cursor.end().0;
+ let seg_inlay_start = cursor.start().1;
+
+ let overlap_start = cmp::max(range.start, seg_buffer_start);
+ let overlap_end = cmp::min(range.end, seg_buffer_end);
+
+ let past_end = seg_buffer_end >= range.end;
+ cursor.next();
+
+ if overlap_start < overlap_end {
+ let inlay_start =
+ InlayOffset(seg_inlay_start.0 + (overlap_start - seg_buffer_start));
+ let inlay_end =
+ InlayOffset(seg_inlay_start.0 + (overlap_end - seg_buffer_start));
+ return Some(inlay_start..inlay_end);
+ }
+
+ if past_end {
+ return None;
+ }
+ }
+ Transform::Inlay(_) => cursor.next(),
+ }
+ }
+ })
+ }
+
#[ztracing::instrument(skip_all)]
pub fn inlay_point_cursor(&self) -> InlayPointCursor<'_> {
let cursor = self.transforms.cursor::<Dimensions<Point, InlayPoint>>(());
@@ -5504,25 +5504,31 @@ impl EditorElement {
})
.filter(|(_, status)| status.is_modified())
.flat_map(|(word_diffs, _)| word_diffs)
- .filter_map(|word_diff| {
- let start_point = word_diff.start.to_display_point(&snapshot.display_snapshot);
- let end_point = word_diff.end.to_display_point(&snapshot.display_snapshot);
- let start_row_offset = start_point.row().0.saturating_sub(start_row.0) as usize;
-
- row_infos
- .get(start_row_offset)
- .and_then(|row_info| row_info.diff_status)
- .and_then(|diff_status| {
- let background_color = match diff_status.kind {
- DiffHunkStatusKind::Added => colors.version_control_word_added,
- DiffHunkStatusKind::Deleted => colors.version_control_word_deleted,
- DiffHunkStatusKind::Modified => {
- debug_panic!("modified diff status for row info");
- return None;
- }
- };
- Some((start_point..end_point, background_color))
- })
+ .flat_map(|word_diff| {
+ let display_ranges = snapshot
+ .display_snapshot
+ .isomorphic_display_point_ranges_for_buffer_range(
+ word_diff.start..word_diff.end,
+ );
+
+ display_ranges.into_iter().filter_map(|range| {
+ let start_row_offset = range.start.row().0.saturating_sub(start_row.0) as usize;
+
+ let diff_status = row_infos
+ .get(start_row_offset)
+ .and_then(|row_info| row_info.diff_status)?;
+
+ let background_color = match diff_status.kind {
+ DiffHunkStatusKind::Added => colors.version_control_word_added,
+ DiffHunkStatusKind::Deleted => colors.version_control_word_deleted,
+ DiffHunkStatusKind::Modified => {
+ debug_panic!("modified diff status for row info");
+ return None;
+ }
+ };
+
+ Some((range, background_color))
+ })
});
highlighted_ranges.extend(word_highlights);