Skip to repository content2575 lines · 96.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:33:21.442Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
inlay_map.rs
1//! The inlay map. See the [`display_map`][super] docs for an overview of how the inlay map fits
2//! into the rest of the [`DisplayMap`][super::DisplayMap]. Much of the documentation for this
3//! module generalizes to other layers.
4//!
5//! The core of this module is the [`InlayMap`] struct, which maintains a vec of [`Inlay`]s, and
6//! [`InlaySnapshot`], which holds a sum tree of [`Transform`]s.
7
8use crate::{
9 ChunkRenderer, HighlightStyles,
10 inlays::{Inlay, InlayContent},
11};
12use collections::BTreeSet;
13use language::{Chunk, Edit, LanguageAwareStyling, Point, TextSummary};
14use multi_buffer::{
15 MBTextSummary, MultiBufferOffset, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot,
16 RowInfo, ToOffset,
17};
18use project::InlayId;
19use smallvec::SmallVec;
20use std::{
21 cmp, iter,
22 ops::{Add, AddAssign, Range, Sub, SubAssign},
23 sync::Arc,
24};
25use sum_tree::{Bias, Cursor, Dimensions, SumTree};
26use text::{ChunkBitmaps, Patch};
27use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
28
29use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
30
31/// Decides where the [`Inlay`]s should be displayed.
32///
33/// See the [`display_map` module documentation](crate::display_map) for more information.
34pub struct InlayMap {
35 snapshot: InlaySnapshot,
36 inlays: Vec<Inlay>,
37}
38
39#[derive(Clone)]
40pub struct InlaySnapshot {
41 pub buffer: MultiBufferSnapshot,
42 transforms: SumTree<Transform>,
43 pub version: usize,
44}
45
46impl std::ops::Deref for InlaySnapshot {
47 type Target = MultiBufferSnapshot;
48
49 fn deref(&self) -> &Self::Target {
50 &self.buffer
51 }
52}
53
54#[derive(Clone, Debug)]
55enum Transform {
56 Isomorphic(MBTextSummary),
57 Inlay(Inlay),
58}
59
60impl sum_tree::Item for Transform {
61 type Summary = TransformSummary;
62
63 #[ztracing::instrument(skip_all)]
64 fn summary(&self, _: ()) -> Self::Summary {
65 match self {
66 Transform::Isomorphic(summary) => TransformSummary {
67 input: *summary,
68 output: *summary,
69 },
70 Transform::Inlay(inlay) => TransformSummary {
71 input: MBTextSummary::default(),
72 output: MBTextSummary::from(inlay.text().summary()),
73 },
74 }
75 }
76}
77
78#[derive(Clone, Debug, Default)]
79struct TransformSummary {
80 /// Summary of the text before inlays have been applied.
81 input: MBTextSummary,
82 /// Summary of the text after inlays have been applied.
83 output: MBTextSummary,
84}
85
86impl TransformSummary {
87 fn has_inlays(&self) -> bool {
88 self.input.len != self.output.len
89 }
90}
91
92impl sum_tree::ContextLessSummary for TransformSummary {
93 fn zero() -> Self {
94 Default::default()
95 }
96
97 fn add_summary(&mut self, other: &Self) {
98 self.input += other.input;
99 self.output += other.output;
100 }
101}
102
103pub type InlayEdit = Edit<InlayOffset>;
104
105#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
106pub struct InlayOffset(pub MultiBufferOffset);
107
108impl Add for InlayOffset {
109 type Output = Self;
110
111 fn add(self, rhs: Self) -> Self::Output {
112 Self(self.0 + rhs.0)
113 }
114}
115
116impl Sub for InlayOffset {
117 type Output = <MultiBufferOffset as Sub>::Output;
118
119 fn sub(self, rhs: Self) -> Self::Output {
120 self.0 - rhs.0
121 }
122}
123
124impl<T> SubAssign<T> for InlayOffset
125where
126 MultiBufferOffset: SubAssign<T>,
127{
128 fn sub_assign(&mut self, rhs: T) {
129 self.0 -= rhs;
130 }
131}
132
133impl<T> Add<T> for InlayOffset
134where
135 MultiBufferOffset: Add<T, Output = MultiBufferOffset>,
136{
137 type Output = Self;
138
139 fn add(self, rhs: T) -> Self::Output {
140 Self(self.0 + rhs)
141 }
142}
143
144impl AddAssign for InlayOffset {
145 fn add_assign(&mut self, rhs: Self) {
146 self.0 += rhs.0;
147 }
148}
149
150impl<T> AddAssign<T> for InlayOffset
151where
152 MultiBufferOffset: AddAssign<T>,
153{
154 fn add_assign(&mut self, rhs: T) {
155 self.0 += rhs;
156 }
157}
158
159impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset {
160 fn zero(_cx: ()) -> Self {
161 Default::default()
162 }
163
164 fn add_summary(&mut self, summary: &'a TransformSummary, _: ()) {
165 self.0 += summary.output.len;
166 }
167}
168
169#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
170pub struct InlayPoint(pub Point);
171
172impl Add for InlayPoint {
173 type Output = Self;
174
175 fn add(self, rhs: Self) -> Self::Output {
176 Self(self.0 + rhs.0)
177 }
178}
179
180impl Sub for InlayPoint {
181 type Output = Self;
182
183 fn sub(self, rhs: Self) -> Self::Output {
184 Self(self.0 - rhs.0)
185 }
186}
187
188impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint {
189 fn zero(_cx: ()) -> Self {
190 Default::default()
191 }
192
193 fn add_summary(&mut self, summary: &'a TransformSummary, _: ()) {
194 self.0 += &summary.output.lines;
195 }
196}
197
198impl<'a> sum_tree::Dimension<'a, TransformSummary> for MultiBufferOffset {
199 fn zero(_cx: ()) -> Self {
200 Default::default()
201 }
202
203 fn add_summary(&mut self, summary: &'a TransformSummary, _: ()) {
204 *self += summary.input.len;
205 }
206}
207
208impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point {
209 fn zero(_cx: ()) -> Self {
210 Default::default()
211 }
212
213 fn add_summary(&mut self, summary: &'a TransformSummary, _: ()) {
214 *self += &summary.input.lines;
215 }
216}
217
218#[derive(Clone)]
219pub struct InlayBufferRows<'a> {
220 transforms: Cursor<'a, 'static, Transform, Dimensions<InlayPoint, Point>>,
221 buffer_rows: MultiBufferRows<'a>,
222 inlay_row: u32,
223 max_buffer_row: MultiBufferRow,
224}
225
226pub struct InlayChunks<'a> {
227 transforms: Cursor<'a, 'static, Transform, Dimensions<InlayOffset, MultiBufferOffset>>,
228 buffer_chunks: CustomHighlightsChunks<'a>,
229 buffer_chunk: Option<Chunk<'a>>,
230 inlay_chunks: Option<text::ChunkWithBitmaps<'a>>,
231 /// text, char bitmap, tabs bitmap
232 inlay_chunk: Option<ChunkBitmaps<'a>>,
233 output_offset: InlayOffset,
234 max_output_offset: InlayOffset,
235 highlight_styles: HighlightStyles,
236 highlights: Highlights<'a>,
237 snapshot: &'a InlaySnapshot,
238}
239
240#[derive(Clone)]
241pub struct InlayChunk<'a> {
242 pub chunk: Chunk<'a>,
243 /// Whether the inlay should be customly rendered.
244 pub renderer: Option<ChunkRenderer>,
245}
246
247impl InlayChunks<'_> {
248 #[ztracing::instrument(skip_all)]
249 pub fn seek(&mut self, new_range: Range<InlayOffset>) {
250 self.transforms.seek(&new_range.start, Bias::Right);
251
252 let buffer_range = self.snapshot.to_buffer_offset(new_range.start)
253 ..self.snapshot.to_buffer_offset(new_range.end);
254 self.buffer_chunks.seek(buffer_range);
255 self.inlay_chunks = None;
256 self.buffer_chunk = None;
257 self.output_offset = new_range.start;
258 self.max_output_offset = new_range.end;
259 }
260
261 pub fn offset(&self) -> InlayOffset {
262 self.output_offset
263 }
264}
265
266impl<'a> Iterator for InlayChunks<'a> {
267 type Item = InlayChunk<'a>;
268
269 #[ztracing::instrument(skip_all)]
270 fn next(&mut self) -> Option<Self::Item> {
271 if self.output_offset == self.max_output_offset {
272 return None;
273 }
274
275 let chunk = match self.transforms.item()? {
276 Transform::Isomorphic(_) => {
277 let chunk = self
278 .buffer_chunk
279 .get_or_insert_with(|| self.buffer_chunks.next().unwrap());
280 if chunk.text.is_empty() {
281 *chunk = self.buffer_chunks.next().unwrap();
282 }
283
284 let desired_bytes = self.transforms.end().0.0 - self.output_offset.0;
285
286 // If we're already at the transform boundary, skip to the next transform
287 if desired_bytes == 0 {
288 self.inlay_chunks = None;
289 self.transforms.next();
290 return self.next();
291 }
292
293 // Determine split index handling edge cases
294 let split_index = if desired_bytes >= chunk.text.len() {
295 chunk.text.len()
296 } else {
297 chunk.text.ceil_char_boundary(desired_bytes)
298 };
299
300 let (prefix, suffix) = chunk.text.split_at(split_index);
301 self.output_offset.0 += prefix.len();
302
303 let mask = 1u128.unbounded_shl(split_index as u32).wrapping_sub(1);
304 let chars = chunk.chars & mask;
305 let tabs = chunk.tabs & mask;
306 let newlines = chunk.newlines & mask;
307
308 chunk.chars = chunk.chars.unbounded_shr(split_index as u32);
309 chunk.tabs = chunk.tabs.unbounded_shr(split_index as u32);
310 chunk.newlines = chunk.newlines.unbounded_shr(split_index as u32);
311 chunk.text = suffix;
312
313 InlayChunk {
314 chunk: Chunk {
315 text: prefix,
316 chars,
317 tabs,
318 newlines,
319 ..chunk.clone()
320 },
321 renderer: None,
322 }
323 }
324 Transform::Inlay(inlay) => {
325 let mut inlay_style_and_highlight = None;
326 if let Some(inlay_highlights) = self.highlights.inlay_highlights {
327 for (_, inlay_id_to_data) in inlay_highlights.iter() {
328 let style_and_highlight = inlay_id_to_data.get(&inlay.id);
329 if style_and_highlight.is_some() {
330 inlay_style_and_highlight = style_and_highlight;
331 break;
332 }
333 }
334 }
335
336 let mut renderer = None;
337 let mut highlight_style = match inlay.id {
338 InlayId::EditPrediction(_) => self.highlight_styles.edit_prediction.map(|s| {
339 if inlay.text().chars().all(|c| c.is_whitespace()) {
340 s.whitespace
341 } else {
342 s.insertion
343 }
344 }),
345 InlayId::Hint(_) => self.highlight_styles.inlay_hint,
346 InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
347 InlayId::ReplResult(_) => {
348 let text = inlay.text().to_string();
349 renderer = Some(ChunkRenderer {
350 id: ChunkRendererId::Inlay(inlay.id),
351 render: Arc::new(move |cx| {
352 let colors = cx.theme().colors();
353 div()
354 .flex()
355 .flex_row()
356 .items_center()
357 .child(div().w_4())
358 .child(
359 div()
360 .px_1()
361 .rounded_sm()
362 .bg(colors.surface_background)
363 .text_color(colors.text_muted)
364 .text_xs()
365 .child(text.trim().to_string()),
366 )
367 .into_any_element()
368 }),
369 constrain_width: false,
370 measured_width: None,
371 });
372 self.highlight_styles.inlay_hint
373 }
374 InlayId::Color(_) => {
375 if let InlayContent::Color(color) = inlay.content {
376 renderer = Some(ChunkRenderer {
377 id: ChunkRendererId::Inlay(inlay.id),
378 render: Arc::new(move |cx| {
379 div()
380 .relative()
381 .size_3p5()
382 .child(
383 div()
384 .absolute()
385 .right_1()
386 .size_3()
387 .border_1()
388 .border_color(
389 if cx.theme().appearance().is_light() {
390 gpui::black().opacity(0.5)
391 } else {
392 gpui::white().opacity(0.5)
393 },
394 )
395 .bg(color),
396 )
397 .into_any_element()
398 }),
399 constrain_width: false,
400 measured_width: None,
401 });
402 }
403 self.highlight_styles.inlay_hint
404 }
405 };
406 let next_inlay_highlight_endpoint;
407 let offset_in_inlay = self.output_offset - self.transforms.start().0;
408 if let Some((style, highlight)) = inlay_style_and_highlight {
409 let range = &highlight.range;
410 if offset_in_inlay < range.start {
411 next_inlay_highlight_endpoint = range.start - offset_in_inlay;
412 } else if offset_in_inlay >= range.end {
413 next_inlay_highlight_endpoint = usize::MAX;
414 } else {
415 next_inlay_highlight_endpoint = range.end - offset_in_inlay;
416 highlight_style = highlight_style
417 .map(|highlight| highlight.highlight(*style))
418 .or_else(|| Some(*style));
419 }
420 } else {
421 next_inlay_highlight_endpoint = usize::MAX;
422 }
423
424 let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
425 let start = offset_in_inlay;
426 let end = cmp::min(self.max_output_offset, self.transforms.end().0)
427 - self.transforms.start().0;
428 let chunks = inlay.text().chunks_in_range(start..end);
429 text::ChunkWithBitmaps(chunks)
430 });
431 let ChunkBitmaps {
432 text: inlay_chunk,
433 chars,
434 tabs,
435 newlines,
436 } = self
437 .inlay_chunk
438 .get_or_insert_with(|| inlay_chunks.next().unwrap());
439
440 // Determine split index handling edge cases
441 let split_index = if next_inlay_highlight_endpoint >= inlay_chunk.len() {
442 inlay_chunk.len()
443 } else if next_inlay_highlight_endpoint == 0 {
444 // Need to take at least one character to make progress
445 inlay_chunk
446 .chars()
447 .next()
448 .map(|c| c.len_utf8())
449 .unwrap_or(1)
450 } else {
451 inlay_chunk.ceil_char_boundary(next_inlay_highlight_endpoint)
452 };
453
454 let (chunk, remainder) = inlay_chunk.split_at(split_index);
455 *inlay_chunk = remainder;
456
457 let mask = 1u128.unbounded_shl(split_index as u32).wrapping_sub(1);
458 let new_chars = *chars & mask;
459 let new_tabs = *tabs & mask;
460 let new_newlines = *newlines & mask;
461
462 *chars = chars.unbounded_shr(split_index as u32);
463 *tabs = tabs.unbounded_shr(split_index as u32);
464 *newlines = newlines.unbounded_shr(split_index as u32);
465
466 if inlay_chunk.is_empty() {
467 self.inlay_chunk = None;
468 }
469
470 self.output_offset.0 += chunk.len();
471
472 InlayChunk {
473 chunk: Chunk {
474 text: chunk,
475 chars: new_chars,
476 tabs: new_tabs,
477 newlines: new_newlines,
478 highlight_style,
479 is_inlay: true,
480 ..Chunk::default()
481 },
482 renderer,
483 }
484 }
485 };
486
487 if self.output_offset >= self.transforms.end().0 {
488 self.inlay_chunks = None;
489 self.transforms.next();
490 }
491
492 Some(chunk)
493 }
494}
495
496impl InlayBufferRows<'_> {
497 #[ztracing::instrument(skip_all)]
498 pub fn seek(&mut self, row: u32) {
499 let inlay_point = InlayPoint::new(row, 0);
500 self.transforms.seek(&inlay_point, Bias::Left);
501
502 let mut buffer_point = self.transforms.start().1;
503 let buffer_row = MultiBufferRow(if row == 0 {
504 0
505 } else {
506 match self.transforms.item() {
507 Some(Transform::Isomorphic(_)) => {
508 buffer_point += inlay_point.0 - self.transforms.start().0.0;
509 buffer_point.row
510 }
511 _ => cmp::min(buffer_point.row + 1, self.max_buffer_row.0),
512 }
513 });
514 self.inlay_row = inlay_point.row();
515 self.buffer_rows.seek(buffer_row);
516 }
517}
518
519impl Iterator for InlayBufferRows<'_> {
520 type Item = RowInfo;
521
522 #[ztracing::instrument(skip_all)]
523 fn next(&mut self) -> Option<Self::Item> {
524 let buffer_row = if self.inlay_row == 0 {
525 self.buffer_rows.next().unwrap()
526 } else {
527 match self.transforms.item()? {
528 Transform::Inlay(_) => Default::default(),
529 Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(),
530 }
531 };
532
533 self.inlay_row += 1;
534 self.transforms
535 .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left);
536
537 Some(buffer_row)
538 }
539}
540
541impl InlayPoint {
542 pub fn new(row: u32, column: u32) -> Self {
543 Self(Point::new(row, column))
544 }
545
546 pub fn row(self) -> u32 {
547 self.0.row
548 }
549}
550
551impl InlayMap {
552 #[ztracing::instrument(skip_all)]
553 pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) {
554 let version = 0;
555 let snapshot = InlaySnapshot {
556 transforms: SumTree::from_iter(
557 iter::once(Transform::Isomorphic(buffer.text_summary())),
558 (),
559 ),
560 buffer,
561 version,
562 };
563
564 (
565 Self {
566 snapshot: snapshot.clone(),
567 inlays: Vec::new(),
568 },
569 snapshot,
570 )
571 }
572
573 #[ztracing::instrument(skip_all)]
574 pub fn sync(
575 &mut self,
576 buffer_snapshot: MultiBufferSnapshot,
577 mut buffer_edits: Vec<text::Edit<MultiBufferOffset>>,
578 ) -> (InlaySnapshot, Vec<InlayEdit>) {
579 let snapshot = &mut self.snapshot;
580
581 if buffer_edits.is_empty()
582 && snapshot.buffer.trailing_excerpt_update_count()
583 != buffer_snapshot.trailing_excerpt_update_count()
584 {
585 buffer_edits.push(Edit {
586 old: snapshot.buffer.len()..snapshot.buffer.len(),
587 new: buffer_snapshot.len()..buffer_snapshot.len(),
588 });
589 }
590
591 if buffer_edits.is_empty() {
592 if snapshot.buffer.edit_count() != buffer_snapshot.edit_count()
593 || snapshot.buffer.non_text_state_update_count()
594 != buffer_snapshot.non_text_state_update_count()
595 || snapshot.buffer.trailing_excerpt_update_count()
596 != buffer_snapshot.trailing_excerpt_update_count()
597 {
598 snapshot.version += 1;
599 }
600
601 snapshot.buffer = buffer_snapshot;
602 (snapshot.clone(), Vec::new())
603 } else if self.inlays.is_empty() && !snapshot.transforms.summary().has_inlays() {
604 // Fast path: without inlays, the InlayMap is a passthrough, so rebuild a single
605 // isomorphic transform and forward buffer edits as inlay edits verbatim.
606 let mut new_transforms = SumTree::default();
607 push_isomorphic(&mut new_transforms, buffer_snapshot.text_summary());
608 if new_transforms.is_empty() {
609 new_transforms.push(Transform::Isomorphic(Default::default()), ());
610 }
611
612 let mut inlay_edits = Patch::default();
613 for buffer_edit in &buffer_edits {
614 inlay_edits.push(Edit {
615 old: InlayOffset(buffer_edit.old.start)..InlayOffset(buffer_edit.old.end),
616 new: InlayOffset(buffer_edit.new.start)..InlayOffset(buffer_edit.new.end),
617 });
618 }
619
620 snapshot.transforms = new_transforms;
621 snapshot.version += 1;
622 snapshot.buffer = buffer_snapshot;
623 snapshot.check_invariants();
624
625 (snapshot.clone(), inlay_edits.into_inner())
626 } else {
627 let mut inlay_edits = Patch::default();
628 let mut new_transforms = SumTree::default();
629 let mut cursor = snapshot
630 .transforms
631 .cursor::<Dimensions<MultiBufferOffset, InlayOffset>>(());
632 let mut buffer_edits_iter = buffer_edits.iter().peekable();
633 while let Some(buffer_edit) = buffer_edits_iter.next() {
634 new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left), ());
635 if let Some(Transform::Isomorphic(transform)) = cursor.item()
636 && cursor.end().0 == buffer_edit.old.start
637 {
638 push_isomorphic(&mut new_transforms, *transform);
639 cursor.next();
640 }
641
642 // Remove all the inlays and transforms contained by the edit.
643 let old_start = cursor.start().1 + (buffer_edit.old.start - cursor.start().0);
644 cursor.seek(&buffer_edit.old.end, Bias::Right);
645 let old_end = cursor.start().1 + (buffer_edit.old.end - cursor.start().0);
646
647 // Push the unchanged prefix.
648 let prefix_start = new_transforms.summary().input.len;
649 let prefix_end = buffer_edit.new.start;
650 push_isomorphic(
651 &mut new_transforms,
652 buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
653 );
654 let new_start = InlayOffset(new_transforms.summary().output.len);
655
656 let start_ix = match self.inlays.binary_search_by(|probe| {
657 probe
658 .position
659 .to_offset(&buffer_snapshot)
660 .cmp(&buffer_edit.new.start)
661 .then(std::cmp::Ordering::Greater)
662 }) {
663 Ok(ix) | Err(ix) => ix,
664 };
665
666 for inlay in &self.inlays[start_ix..] {
667 if !inlay.position.is_valid(&buffer_snapshot) {
668 continue;
669 }
670 let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
671 if buffer_offset > buffer_edit.new.end {
672 break;
673 }
674
675 let prefix_start = new_transforms.summary().input.len;
676 let prefix_end = buffer_offset;
677 push_isomorphic(
678 &mut new_transforms,
679 buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
680 );
681
682 new_transforms.push(Transform::Inlay(inlay.clone()), ());
683 }
684
685 // Apply the rest of the edit.
686 let transform_start = new_transforms.summary().input.len;
687 push_isomorphic(
688 &mut new_transforms,
689 buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end),
690 );
691 let new_end = InlayOffset(new_transforms.summary().output.len);
692 inlay_edits.push(Edit {
693 old: old_start..old_end,
694 new: new_start..new_end,
695 });
696
697 // If the next edit doesn't intersect the current isomorphic transform, then
698 // we can push its remainder.
699 if buffer_edits_iter
700 .peek()
701 .is_none_or(|edit| edit.old.start >= cursor.end().0)
702 {
703 let transform_start = new_transforms.summary().input.len;
704 let transform_end =
705 buffer_edit.new.end + (cursor.end().0 - buffer_edit.old.end);
706 push_isomorphic(
707 &mut new_transforms,
708 buffer_snapshot.text_summary_for_range(transform_start..transform_end),
709 );
710 cursor.next();
711 }
712 }
713
714 new_transforms.append(cursor.suffix(), ());
715 if new_transforms.is_empty() {
716 new_transforms.push(Transform::Isomorphic(Default::default()), ());
717 }
718
719 drop(cursor);
720 snapshot.transforms = new_transforms;
721 snapshot.version += 1;
722 snapshot.buffer = buffer_snapshot;
723 snapshot.check_invariants();
724
725 (snapshot.clone(), inlay_edits.into_inner())
726 }
727 }
728
729 #[ztracing::instrument(skip_all)]
730 pub fn splice(
731 &mut self,
732 to_remove: &[InlayId],
733 to_insert: Vec<Inlay>,
734 ) -> (InlaySnapshot, Vec<InlayEdit>) {
735 let snapshot = &mut self.snapshot;
736 let mut edits = BTreeSet::new();
737
738 self.inlays.retain(|inlay| {
739 let retain = !to_remove.contains(&inlay.id);
740 if !retain {
741 let offset = inlay.position.to_offset(&snapshot.buffer);
742 edits.insert(offset);
743 }
744 retain
745 });
746
747 for inlay_to_insert in to_insert {
748 // Avoid inserting empty inlays.
749 if inlay_to_insert.text().is_empty() {
750 continue;
751 }
752
753 let offset = inlay_to_insert.position.to_offset(&snapshot.buffer);
754 match self.inlays.binary_search_by(|probe| {
755 probe
756 .position
757 .cmp(&inlay_to_insert.position, &snapshot.buffer)
758 .then(std::cmp::Ordering::Less)
759 }) {
760 Ok(ix) | Err(ix) => {
761 self.inlays.insert(ix, inlay_to_insert);
762 }
763 }
764
765 edits.insert(offset);
766 }
767
768 let buffer_edits = edits
769 .into_iter()
770 .map(|offset| Edit {
771 old: offset..offset,
772 new: offset..offset,
773 })
774 .collect();
775 let buffer_snapshot = snapshot.buffer.clone();
776 let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
777 (snapshot, edits)
778 }
779
780 #[ztracing::instrument(skip_all)]
781 pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> + Default {
782 self.inlays.iter()
783 }
784
785 #[cfg(test)]
786 #[ztracing::instrument(skip_all)]
787 pub(crate) fn randomly_mutate(
788 &mut self,
789 next_inlay_id: &mut usize,
790 rng: &mut rand::rngs::StdRng,
791 ) -> (InlaySnapshot, Vec<InlayEdit>) {
792 use rand::prelude::*;
793 use util::post_inc;
794
795 let mut to_remove = Vec::new();
796 let mut to_insert = Vec::new();
797 let snapshot = &mut self.snapshot;
798 for i in 0..rng.random_range(1..=5) {
799 if self.inlays.is_empty() || rng.random() {
800 let position = snapshot
801 .buffer
802 .random_byte_range(MultiBufferOffset(0), rng)
803 .start;
804 let bias = if rng.random() {
805 Bias::Left
806 } else {
807 Bias::Right
808 };
809 let len = if rng.random_bool(0.01) {
810 0
811 } else {
812 rng.random_range(1..=5)
813 };
814 let text = util::RandomCharIter::new(&mut *rng)
815 .filter(|ch| *ch != '\r')
816 .take(len)
817 .collect::<String>();
818
819 let next_inlay = if i % 2 == 0 {
820 Inlay::mock_hint(
821 post_inc(next_inlay_id),
822 snapshot.buffer.anchor_at(position, bias),
823 &text,
824 )
825 } else {
826 Inlay::edit_prediction(
827 post_inc(next_inlay_id),
828 snapshot.buffer.anchor_at(position, bias),
829 &text,
830 )
831 };
832 let inlay_id = next_inlay.id;
833 log::info!(
834 "creating inlay {inlay_id:?} at buffer offset {position} with bias {bias:?} and text {text:?}"
835 );
836 to_insert.push(next_inlay);
837 } else {
838 to_remove.push(
839 self.inlays
840 .iter()
841 .choose(rng)
842 .map(|inlay| inlay.id)
843 .unwrap(),
844 );
845 }
846 }
847 log::info!("removing inlays: {:?}", to_remove);
848
849 let (snapshot, edits) = self.splice(&to_remove, to_insert);
850 (snapshot, edits)
851 }
852}
853
854impl InlaySnapshot {
855 #[ztracing::instrument(skip_all)]
856 pub fn to_point(&self, offset: InlayOffset) -> InlayPoint {
857 let (start, _, item) = self.transforms.find::<Dimensions<
858 InlayOffset,
859 InlayPoint,
860 MultiBufferOffset,
861 >, _>((), &offset, Bias::Right);
862 let overshoot = offset.0 - start.0.0;
863 match item {
864 Some(Transform::Isomorphic(_)) => {
865 let buffer_offset_start = start.2;
866 let buffer_offset_end = buffer_offset_start + overshoot;
867 let buffer_start = self.buffer.offset_to_point(buffer_offset_start);
868 let buffer_end = self.buffer.offset_to_point(buffer_offset_end);
869 InlayPoint(start.1.0 + (buffer_end - buffer_start))
870 }
871 Some(Transform::Inlay(inlay)) => {
872 let overshoot = inlay.text().offset_to_point(overshoot);
873 InlayPoint(start.1.0 + overshoot)
874 }
875 None => self.max_point(),
876 }
877 }
878
879 #[ztracing::instrument(skip_all)]
880 pub fn len(&self) -> InlayOffset {
881 InlayOffset(self.transforms.summary().output.len)
882 }
883
884 #[ztracing::instrument(skip_all)]
885 pub fn max_point(&self) -> InlayPoint {
886 InlayPoint(self.transforms.summary().output.lines)
887 }
888
889 #[ztracing::instrument(skip_all, fields(point))]
890 pub fn to_offset(&self, point: InlayPoint) -> InlayOffset {
891 let (start, _, item) = self
892 .transforms
893 .find::<Dimensions<InlayPoint, InlayOffset, Point>, _>((), &point, Bias::Right);
894 let overshoot = point.0 - start.0.0;
895 match item {
896 Some(Transform::Isomorphic(_)) => {
897 let buffer_point_start = start.2;
898 let buffer_point_end = buffer_point_start + overshoot;
899 let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start);
900 let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end);
901 InlayOffset(start.1.0 + (buffer_offset_end - buffer_offset_start))
902 }
903 Some(Transform::Inlay(inlay)) => {
904 let overshoot = inlay.text().point_to_offset(overshoot);
905 InlayOffset(start.1.0 + overshoot)
906 }
907 None => self.len(),
908 }
909 }
910 #[ztracing::instrument(skip_all)]
911 pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
912 let (start, _, item) =
913 self.transforms
914 .find::<Dimensions<InlayPoint, Point>, _>((), &point, Bias::Right);
915 match item {
916 Some(Transform::Isomorphic(_)) => {
917 let overshoot = point.0 - start.0.0;
918 start.1 + overshoot
919 }
920 Some(Transform::Inlay(_)) => start.1,
921 None => self.buffer.max_point(),
922 }
923 }
924 #[ztracing::instrument(skip_all)]
925 pub fn to_buffer_offset(&self, offset: InlayOffset) -> MultiBufferOffset {
926 let (start, _, item) = self
927 .transforms
928 .find::<Dimensions<InlayOffset, MultiBufferOffset>, _>((), &offset, Bias::Right);
929 match item {
930 Some(Transform::Isomorphic(_)) => {
931 let overshoot = offset - start.0;
932 start.1 + overshoot
933 }
934 Some(Transform::Inlay(_)) => start.1,
935 None => self.buffer.len(),
936 }
937 }
938
939 #[ztracing::instrument(skip_all)]
940 pub fn to_inlay_offset(&self, offset: MultiBufferOffset) -> InlayOffset {
941 let mut cursor = self
942 .transforms
943 .cursor::<Dimensions<MultiBufferOffset, InlayOffset>>(());
944 cursor.seek(&offset, Bias::Left);
945 loop {
946 match cursor.item() {
947 Some(Transform::Isomorphic(_)) => {
948 if offset == cursor.end().0 {
949 while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
950 if inlay.position.bias() == Bias::Right {
951 break;
952 } else {
953 cursor.next();
954 }
955 }
956 return cursor.end().1;
957 } else {
958 let overshoot = offset - cursor.start().0;
959 return InlayOffset(cursor.start().1.0 + overshoot);
960 }
961 }
962 Some(Transform::Inlay(inlay)) => {
963 if inlay.position.bias() == Bias::Left {
964 cursor.next();
965 } else {
966 return cursor.start().1;
967 }
968 }
969 None => {
970 return self.len();
971 }
972 }
973 }
974 }
975
976 #[ztracing::instrument(skip_all)]
977 pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
978 self.inlay_point_cursor().map(point, Bias::Left)
979 }
980
981 /// Converts a buffer offset range into one or more `InlayOffset` ranges that
982 /// cover only the actual buffer text, skipping any inlay hint text that falls
983 /// within the range. When there are no inlays the returned vec contains a
984 /// single element identical to the input mapped into inlay-offset space.
985 pub fn buffer_offset_to_inlay_ranges(
986 &self,
987 range: Range<MultiBufferOffset>,
988 ) -> impl Iterator<Item = Range<InlayOffset>> {
989 let mut cursor = self
990 .transforms
991 .cursor::<Dimensions<MultiBufferOffset, InlayOffset>>(());
992 cursor.seek(&range.start, Bias::Right);
993
994 std::iter::from_fn(move || {
995 loop {
996 match cursor.item()? {
997 Transform::Isomorphic(_) => {
998 let seg_buffer_start = cursor.start().0;
999 let seg_buffer_end = cursor.end().0;
1000 let seg_inlay_start = cursor.start().1;
1001
1002 let overlap_start = cmp::max(range.start, seg_buffer_start);
1003 let overlap_end = cmp::min(range.end, seg_buffer_end);
1004
1005 let past_end = seg_buffer_end >= range.end;
1006 cursor.next();
1007
1008 if overlap_start < overlap_end {
1009 let inlay_start =
1010 InlayOffset(seg_inlay_start.0 + (overlap_start - seg_buffer_start));
1011 let inlay_end =
1012 InlayOffset(seg_inlay_start.0 + (overlap_end - seg_buffer_start));
1013 return Some(inlay_start..inlay_end);
1014 }
1015
1016 if past_end {
1017 return None;
1018 }
1019 }
1020 Transform::Inlay(_) => cursor.next(),
1021 }
1022 }
1023 })
1024 }
1025
1026 pub fn buffer_offset_to_inlay_point_cursor(&self) -> BufferOffsetToInlayPointCursor<'_> {
1027 BufferOffsetToInlayPointCursor {
1028 snapshot: self,
1029 cursor: self
1030 .transforms
1031 .cursor::<Dimensions<MultiBufferOffset, InlayPoint>>(()),
1032 }
1033 }
1034
1035 #[ztracing::instrument(skip_all)]
1036 pub fn inlay_point_cursor(&self) -> InlayPointCursor<'_> {
1037 let cursor = self.transforms.cursor::<Dimensions<Point, InlayPoint>>(());
1038 InlayPointCursor {
1039 cursor,
1040 transforms: &self.transforms,
1041 }
1042 }
1043
1044 #[ztracing::instrument(skip_all)]
1045 pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint {
1046 let mut cursor = self.transforms.cursor::<Dimensions<InlayPoint, Point>>(());
1047 cursor.seek(&point, Bias::Left);
1048 loop {
1049 match cursor.item() {
1050 Some(Transform::Isomorphic(transform)) => {
1051 if cursor.start().0 == point {
1052 if let Some(Transform::Inlay(inlay)) = cursor.prev_item() {
1053 if inlay.position.bias() == Bias::Left {
1054 return point;
1055 } else if bias == Bias::Left {
1056 cursor.prev();
1057 } else if transform.first_line_chars == 0 {
1058 point.0 += Point::new(1, 0);
1059 } else {
1060 point.0 += Point::new(0, 1);
1061 }
1062 } else {
1063 return point;
1064 }
1065 } else if cursor.end().0 == point {
1066 if let Some(Transform::Inlay(inlay)) = cursor.next_item() {
1067 if inlay.position.bias() == Bias::Right {
1068 return point;
1069 } else if bias == Bias::Right {
1070 cursor.next();
1071 } else if point.0.column == 0 {
1072 point.0.row -= 1;
1073 point.0.column = self.line_len(point.0.row);
1074 } else {
1075 point.0.column -= 1;
1076 }
1077 } else {
1078 return point;
1079 }
1080 } else {
1081 let overshoot = point.0 - cursor.start().0.0;
1082 let buffer_point = cursor.start().1 + overshoot;
1083 let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias);
1084 let clipped_overshoot = clipped_buffer_point - cursor.start().1;
1085 let clipped_point = InlayPoint(cursor.start().0.0 + clipped_overshoot);
1086 if clipped_point == point {
1087 return clipped_point;
1088 } else {
1089 point = clipped_point;
1090 }
1091 }
1092 }
1093 Some(Transform::Inlay(inlay)) => {
1094 if point == cursor.start().0 && inlay.position.bias() == Bias::Right {
1095 match cursor.prev_item() {
1096 Some(Transform::Inlay(inlay)) => {
1097 if inlay.position.bias() == Bias::Left {
1098 return point;
1099 }
1100 }
1101 _ => return point,
1102 }
1103 } else if point == cursor.end().0 && inlay.position.bias() == Bias::Left {
1104 match cursor.next_item() {
1105 Some(Transform::Inlay(inlay)) => {
1106 if inlay.position.bias() == Bias::Right {
1107 return point;
1108 }
1109 }
1110 _ => return point,
1111 }
1112 }
1113
1114 if bias == Bias::Left {
1115 point = cursor.start().0;
1116 cursor.prev();
1117 } else {
1118 cursor.next();
1119 point = cursor.start().0;
1120 }
1121 }
1122 None => {
1123 bias = bias.invert();
1124 if bias == Bias::Left {
1125 point = cursor.start().0;
1126 cursor.prev();
1127 } else {
1128 cursor.next();
1129 point = cursor.start().0;
1130 }
1131 }
1132 }
1133 }
1134 }
1135
1136 pub fn inlay_bias_at_point(&self, point: InlayPoint) -> Option<Bias> {
1137 let mut cursor = self.transforms.cursor::<Dimensions<InlayPoint, Point>>(());
1138 cursor.seek(&point, Bias::Left);
1139 match cursor.item() {
1140 Some(Transform::Inlay(inlay)) => Some(inlay.position.bias()),
1141 _ => None,
1142 }
1143 }
1144
1145 #[ztracing::instrument(skip_all)]
1146 pub fn text_summary(&self) -> MBTextSummary {
1147 self.transforms.summary().output
1148 }
1149
1150 #[ztracing::instrument(skip_all)]
1151 pub fn text_summary_for_range(&self, range: Range<InlayOffset>) -> MBTextSummary {
1152 let mut summary = MBTextSummary::default();
1153
1154 let mut cursor = self
1155 .transforms
1156 .cursor::<Dimensions<InlayOffset, MultiBufferOffset>>(());
1157 cursor.seek(&range.start, Bias::Right);
1158
1159 let overshoot = range.start.0 - cursor.start().0.0;
1160 match cursor.item() {
1161 Some(Transform::Isomorphic(_)) => {
1162 let buffer_start = cursor.start().1;
1163 let suffix_start = buffer_start + overshoot;
1164 let suffix_end =
1165 buffer_start + (cmp::min(cursor.end().0, range.end).0 - cursor.start().0.0);
1166 summary = self.buffer.text_summary_for_range(suffix_start..suffix_end);
1167 cursor.next();
1168 }
1169 Some(Transform::Inlay(inlay)) => {
1170 let suffix_start = overshoot;
1171 let suffix_end = cmp::min(cursor.end().0, range.end).0 - cursor.start().0.0;
1172 summary = MBTextSummary::from(
1173 inlay
1174 .text()
1175 .cursor(suffix_start)
1176 .summary::<TextSummary>(suffix_end),
1177 );
1178 cursor.next();
1179 }
1180 None => {}
1181 }
1182
1183 if range.end > cursor.start().0 {
1184 summary += cursor
1185 .summary::<_, TransformSummary>(&range.end, Bias::Right)
1186 .output;
1187
1188 let overshoot = range.end.0 - cursor.start().0.0;
1189 match cursor.item() {
1190 Some(Transform::Isomorphic(_)) => {
1191 let prefix_start = cursor.start().1;
1192 let prefix_end = prefix_start + overshoot;
1193 summary += self
1194 .buffer
1195 .text_summary_for_range::<MBTextSummary, _>(prefix_start..prefix_end);
1196 }
1197 Some(Transform::Inlay(inlay)) => {
1198 let prefix_end = overshoot;
1199 summary += inlay.text().cursor(0).summary::<TextSummary>(prefix_end);
1200 }
1201 None => {}
1202 }
1203 }
1204
1205 summary
1206 }
1207
1208 #[ztracing::instrument(skip_all)]
1209 pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> {
1210 let mut cursor = self.transforms.cursor::<Dimensions<InlayPoint, Point>>(());
1211 let inlay_point = InlayPoint::new(row, 0);
1212 cursor.seek(&inlay_point, Bias::Left);
1213
1214 let max_buffer_row = self.buffer.max_row();
1215 let mut buffer_point = cursor.start().1;
1216 let buffer_row = if row == 0 {
1217 MultiBufferRow(0)
1218 } else {
1219 match cursor.item() {
1220 Some(Transform::Isomorphic(_)) => {
1221 buffer_point += inlay_point.0 - cursor.start().0.0;
1222 MultiBufferRow(buffer_point.row)
1223 }
1224 _ => cmp::min(MultiBufferRow(buffer_point.row + 1), max_buffer_row),
1225 }
1226 };
1227
1228 InlayBufferRows {
1229 transforms: cursor,
1230 inlay_row: inlay_point.row(),
1231 buffer_rows: self.buffer.row_infos(buffer_row),
1232 max_buffer_row,
1233 }
1234 }
1235
1236 #[ztracing::instrument(skip_all)]
1237 pub fn line_len(&self, row: u32) -> u32 {
1238 let line_start = self.to_offset(InlayPoint::new(row, 0)).0;
1239 let line_end = if row >= self.max_point().row() {
1240 self.len().0
1241 } else {
1242 self.to_offset(InlayPoint::new(row + 1, 0)).0 - 1
1243 };
1244 (line_end - line_start) as u32
1245 }
1246
1247 #[ztracing::instrument(skip_all)]
1248 pub(crate) fn chunks<'a>(
1249 &'a self,
1250 range: Range<InlayOffset>,
1251 language_aware: LanguageAwareStyling,
1252 highlights: Highlights<'a>,
1253 ) -> InlayChunks<'a> {
1254 let mut cursor = self
1255 .transforms
1256 .cursor::<Dimensions<InlayOffset, MultiBufferOffset>>(());
1257 cursor.seek(&range.start, Bias::Right);
1258
1259 let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
1260 let buffer_chunks = CustomHighlightsChunks::new(
1261 buffer_range,
1262 language_aware,
1263 highlights.text_highlights,
1264 highlights.semantic_token_highlights,
1265 &self.buffer,
1266 );
1267
1268 InlayChunks {
1269 transforms: cursor,
1270 buffer_chunks,
1271 inlay_chunks: None,
1272 inlay_chunk: None,
1273 buffer_chunk: None,
1274 output_offset: range.start,
1275 max_output_offset: range.end,
1276 highlight_styles: highlights.styles,
1277 highlights,
1278 snapshot: self,
1279 }
1280 }
1281
1282 #[cfg(test)]
1283 #[ztracing::instrument(skip_all)]
1284 pub fn text(&self) -> String {
1285 self.chunks(
1286 Default::default()..self.len(),
1287 LanguageAwareStyling {
1288 tree_sitter: false,
1289 diagnostics: false,
1290 },
1291 Highlights::default(),
1292 )
1293 .map(|chunk| chunk.chunk.text)
1294 .collect()
1295 }
1296
1297 #[ztracing::instrument(skip_all)]
1298 fn check_invariants(&self) {
1299 #[cfg(any(debug_assertions, feature = "test-support"))]
1300 {
1301 assert_eq!(self.transforms.summary().input, self.buffer.text_summary());
1302 let mut transforms = self.transforms.iter().peekable();
1303 while let Some(transform) = transforms.next() {
1304 let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_));
1305 if let Some(next_transform) = transforms.peek() {
1306 let next_transform_is_isomorphic =
1307 matches!(next_transform, Transform::Isomorphic(_));
1308 assert!(
1309 !transform_is_isomorphic || !next_transform_is_isomorphic,
1310 "two adjacent isomorphic transforms"
1311 );
1312 }
1313 }
1314 }
1315 }
1316}
1317
1318pub struct InlayPointCursor<'transforms> {
1319 cursor: Cursor<'transforms, 'static, Transform, Dimensions<Point, InlayPoint>>,
1320 transforms: &'transforms SumTree<Transform>,
1321}
1322
1323impl InlayPointCursor<'_> {
1324 #[ztracing::instrument(skip_all)]
1325 pub fn map(&mut self, point: Point, bias: Bias) -> InlayPoint {
1326 let cursor = &mut self.cursor;
1327 if cursor.did_seek() {
1328 cursor.seek_forward(&point, Bias::Left);
1329 } else {
1330 cursor.seek(&point, Bias::Left);
1331 }
1332 loop {
1333 match cursor.item() {
1334 Some(Transform::Isomorphic(_)) => {
1335 if point == cursor.end().0 {
1336 while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
1337 if bias == Bias::Left && inlay.position.bias() == Bias::Right {
1338 break;
1339 } else {
1340 cursor.next();
1341 }
1342 }
1343 return cursor.end().1;
1344 } else {
1345 let overshoot = point - cursor.start().0;
1346 return InlayPoint(cursor.start().1.0 + overshoot);
1347 }
1348 }
1349 Some(Transform::Inlay(inlay)) => {
1350 if inlay.position.bias() == Bias::Left || bias == Bias::Right {
1351 cursor.next();
1352 } else {
1353 return cursor.start().1;
1354 }
1355 }
1356 None => {
1357 return InlayPoint(self.transforms.summary().output.lines);
1358 }
1359 }
1360 }
1361 }
1362}
1363
1364/// Forward-only cursor that maps buffer-offset ranges to the inlay-point ranges
1365/// covering only actual buffer text (excluding inlay text), reusing its tree
1366/// position across calls.
1367///
1368/// This is the streaming equivalent of
1369/// [`InlaySnapshot::buffer_offset_to_inlay_ranges`] composed with
1370/// [`InlaySnapshot::to_point`]. Because the cursor only seeks forward, callers
1371/// must provide ranges with non-decreasing offsets.
1372pub struct BufferOffsetToInlayPointCursor<'a> {
1373 snapshot: &'a InlaySnapshot,
1374 cursor: Cursor<'a, 'static, Transform, Dimensions<MultiBufferOffset, InlayPoint>>,
1375}
1376
1377impl BufferOffsetToInlayPointCursor<'_> {
1378 /// Resets the cursor to the start so it can seek backward again.
1379 pub fn reset(&mut self) {
1380 self.cursor.reset();
1381 }
1382
1383 pub fn map(&mut self, range: Range<MultiBufferOffset>) -> SmallVec<[Range<InlayPoint>; 1]> {
1384 let buffer = &self.snapshot.buffer;
1385 let cursor = &mut self.cursor;
1386 if cursor.did_seek() {
1387 cursor.seek_forward(&range.start, Bias::Right);
1388 } else {
1389 cursor.seek(&range.start, Bias::Right);
1390 }
1391
1392 let mut result = SmallVec::new();
1393 loop {
1394 match cursor.item() {
1395 Some(Transform::Isomorphic(_)) => {
1396 let seg_buffer_start = cursor.start().0;
1397 let seg_buffer_end = cursor.end().0;
1398 let seg_inlay_point_start = cursor.start().1;
1399
1400 let overlap_start = cmp::max(range.start, seg_buffer_start);
1401 let overlap_end = cmp::min(range.end, seg_buffer_end);
1402
1403 if overlap_start < overlap_end {
1404 let seg_point_start = buffer.offset_to_point(seg_buffer_start);
1405 let start = InlayPoint(
1406 seg_inlay_point_start.0
1407 + (buffer.offset_to_point(overlap_start) - seg_point_start),
1408 );
1409 let end = InlayPoint(
1410 seg_inlay_point_start.0
1411 + (buffer.offset_to_point(overlap_end) - seg_point_start),
1412 );
1413 result.push(start..end);
1414 }
1415
1416 // Leave the cursor on the transform containing `range.end`
1417 // rather than advancing past it, so a subsequent call with a
1418 // larger (but possibly same-transform) start does not seek
1419 // backward.
1420 if seg_buffer_end >= range.end {
1421 break;
1422 }
1423 cursor.next();
1424 }
1425 Some(Transform::Inlay(_)) => cursor.next(),
1426 None => break,
1427 }
1428 }
1429 result
1430 }
1431}
1432
1433fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: MBTextSummary) {
1434 if summary.len == MultiBufferOffset(0) {
1435 return;
1436 }
1437
1438 let mut summary = Some(summary);
1439 sum_tree.update_last(
1440 |transform| {
1441 if let Transform::Isomorphic(transform) = transform {
1442 *transform += summary.take().unwrap();
1443 }
1444 },
1445 (),
1446 );
1447
1448 if let Some(summary) = summary {
1449 sum_tree.push(Transform::Isomorphic(summary), ());
1450 }
1451}
1452
1453#[cfg(test)]
1454mod tests {
1455 use super::*;
1456 use crate::{
1457 MultiBuffer,
1458 display_map::{HighlightKey, InlayHighlights},
1459 hover_links::InlayHighlight,
1460 };
1461 use collections::HashMap;
1462 use gpui::{App, HighlightStyle};
1463 use multi_buffer::Anchor;
1464 use project::{InlayHint, InlayHintLabel, ResolveState};
1465 use rand::prelude::*;
1466 use settings::SettingsStore;
1467 use std::{cmp::Reverse, env, sync::Arc};
1468 use sum_tree::TreeMap;
1469 use text::{BufferId, Patch, Rope};
1470 use util::RandomCharIter;
1471 use util::post_inc;
1472
1473 #[test]
1474 fn test_inlay_properties_label_padding() {
1475 assert_eq!(
1476 Inlay::hint(
1477 InlayId::Hint(0),
1478 Anchor::Min,
1479 &InlayHint {
1480 label: InlayHintLabel::String("a".to_string()),
1481 position: text::Anchor::min_for_buffer(BufferId::new(1).unwrap()),
1482 padding_left: false,
1483 padding_right: false,
1484 tooltip: None,
1485 kind: None,
1486 resolve_state: ResolveState::Resolved,
1487 },
1488 )
1489 .text()
1490 .to_string(),
1491 "a",
1492 "Should not pad label if not requested"
1493 );
1494
1495 assert_eq!(
1496 Inlay::hint(
1497 InlayId::Hint(0),
1498 Anchor::Min,
1499 &InlayHint {
1500 label: InlayHintLabel::String("a".to_string()),
1501 position: text::Anchor::min_for_buffer(BufferId::new(1).unwrap()),
1502 padding_left: true,
1503 padding_right: true,
1504 tooltip: None,
1505 kind: None,
1506 resolve_state: ResolveState::Resolved,
1507 },
1508 )
1509 .text()
1510 .to_string(),
1511 " a ",
1512 "Should pad label for every side requested"
1513 );
1514
1515 assert_eq!(
1516 Inlay::hint(
1517 InlayId::Hint(0),
1518 Anchor::Min,
1519 &InlayHint {
1520 label: InlayHintLabel::String(" a ".to_string()),
1521 position: text::Anchor::min_for_buffer(BufferId::new(1).unwrap()),
1522 padding_left: false,
1523 padding_right: false,
1524 tooltip: None,
1525 kind: None,
1526 resolve_state: ResolveState::Resolved,
1527 },
1528 )
1529 .text()
1530 .to_string(),
1531 " a ",
1532 "Should not change already padded label"
1533 );
1534
1535 assert_eq!(
1536 Inlay::hint(
1537 InlayId::Hint(0),
1538 Anchor::Min,
1539 &InlayHint {
1540 label: InlayHintLabel::String(" a ".to_string()),
1541 position: text::Anchor::min_for_buffer(BufferId::new(1).unwrap()),
1542 padding_left: true,
1543 padding_right: true,
1544 tooltip: None,
1545 kind: None,
1546 resolve_state: ResolveState::Resolved,
1547 },
1548 )
1549 .text()
1550 .to_string(),
1551 " a ",
1552 "Should not change already padded label"
1553 );
1554 }
1555
1556 #[gpui::test]
1557 fn test_inlay_hint_padding_with_multibyte_chars() {
1558 assert_eq!(
1559 Inlay::hint(
1560 InlayId::Hint(0),
1561 Anchor::Min,
1562 &InlayHint {
1563 label: InlayHintLabel::String("🎨".to_string()),
1564 position: text::Anchor::min_for_buffer(BufferId::new(1).unwrap()),
1565 padding_left: true,
1566 padding_right: true,
1567 tooltip: None,
1568 kind: None,
1569 resolve_state: ResolveState::Resolved,
1570 },
1571 )
1572 .text()
1573 .to_string(),
1574 " 🎨 ",
1575 "Should pad single emoji correctly"
1576 );
1577 }
1578
1579 #[gpui::test]
1580 fn test_basic_inlays(cx: &mut App) {
1581 let buffer = MultiBuffer::build_simple("abcdefghi", cx);
1582 let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
1583 let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
1584 assert_eq!(inlay_snapshot.text(), "abcdefghi");
1585 let mut next_inlay_id = 0;
1586
1587 let (inlay_snapshot, _) = inlay_map.splice(
1588 &[],
1589 vec![Inlay::mock_hint(
1590 post_inc(&mut next_inlay_id),
1591 buffer
1592 .read(cx)
1593 .snapshot(cx)
1594 .anchor_after(MultiBufferOffset(3)),
1595 "|123|",
1596 )],
1597 );
1598 assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
1599 assert_eq!(
1600 inlay_snapshot.to_inlay_point(Point::new(0, 0)),
1601 InlayPoint::new(0, 0)
1602 );
1603 assert_eq!(
1604 inlay_snapshot.to_inlay_point(Point::new(0, 1)),
1605 InlayPoint::new(0, 1)
1606 );
1607 assert_eq!(
1608 inlay_snapshot.to_inlay_point(Point::new(0, 2)),
1609 InlayPoint::new(0, 2)
1610 );
1611 assert_eq!(
1612 inlay_snapshot.to_inlay_point(Point::new(0, 3)),
1613 InlayPoint::new(0, 3)
1614 );
1615 assert_eq!(
1616 inlay_snapshot.to_inlay_point(Point::new(0, 4)),
1617 InlayPoint::new(0, 9)
1618 );
1619 assert_eq!(
1620 inlay_snapshot.to_inlay_point(Point::new(0, 5)),
1621 InlayPoint::new(0, 10)
1622 );
1623 assert_eq!(
1624 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
1625 InlayPoint::new(0, 0)
1626 );
1627 assert_eq!(
1628 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
1629 InlayPoint::new(0, 0)
1630 );
1631 assert_eq!(
1632 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
1633 InlayPoint::new(0, 3)
1634 );
1635 assert_eq!(
1636 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
1637 InlayPoint::new(0, 3)
1638 );
1639 assert_eq!(
1640 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
1641 InlayPoint::new(0, 3)
1642 );
1643 assert_eq!(
1644 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
1645 InlayPoint::new(0, 9)
1646 );
1647
1648 // Edits before or after the inlay should not affect it.
1649 buffer.update(cx, |buffer, cx| {
1650 buffer.edit(
1651 [
1652 (MultiBufferOffset(2)..MultiBufferOffset(3), "x"),
1653 (MultiBufferOffset(3)..MultiBufferOffset(3), "y"),
1654 (MultiBufferOffset(4)..MultiBufferOffset(4), "z"),
1655 ],
1656 None,
1657 cx,
1658 )
1659 });
1660 let (inlay_snapshot, _) = inlay_map.sync(
1661 buffer.read(cx).snapshot(cx),
1662 buffer_edits.consume().into_inner(),
1663 );
1664 assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi");
1665
1666 // An edit surrounding the inlay should invalidate it.
1667 buffer.update(cx, |buffer, cx| {
1668 buffer.edit(
1669 [(MultiBufferOffset(4)..MultiBufferOffset(5), "D")],
1670 None,
1671 cx,
1672 )
1673 });
1674 let (inlay_snapshot, _) = inlay_map.sync(
1675 buffer.read(cx).snapshot(cx),
1676 buffer_edits.consume().into_inner(),
1677 );
1678 assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
1679
1680 let (inlay_snapshot, _) = inlay_map.splice(
1681 &[],
1682 vec![
1683 Inlay::mock_hint(
1684 post_inc(&mut next_inlay_id),
1685 buffer
1686 .read(cx)
1687 .snapshot(cx)
1688 .anchor_before(MultiBufferOffset(3)),
1689 "|123|",
1690 ),
1691 Inlay::edit_prediction(
1692 post_inc(&mut next_inlay_id),
1693 buffer
1694 .read(cx)
1695 .snapshot(cx)
1696 .anchor_after(MultiBufferOffset(3)),
1697 "|456|",
1698 ),
1699 ],
1700 );
1701 assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
1702
1703 // Edits ending where the inlay starts should not move it if it has a left bias.
1704 buffer.update(cx, |buffer, cx| {
1705 buffer.edit(
1706 [(MultiBufferOffset(3)..MultiBufferOffset(3), "JKL")],
1707 None,
1708 cx,
1709 )
1710 });
1711 let (inlay_snapshot, _) = inlay_map.sync(
1712 buffer.read(cx).snapshot(cx),
1713 buffer_edits.consume().into_inner(),
1714 );
1715 assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi");
1716
1717 assert_eq!(
1718 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
1719 InlayPoint::new(0, 0)
1720 );
1721 assert_eq!(
1722 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
1723 InlayPoint::new(0, 0)
1724 );
1725
1726 assert_eq!(
1727 inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Left),
1728 InlayPoint::new(0, 1)
1729 );
1730 assert_eq!(
1731 inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Right),
1732 InlayPoint::new(0, 1)
1733 );
1734
1735 assert_eq!(
1736 inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Left),
1737 InlayPoint::new(0, 2)
1738 );
1739 assert_eq!(
1740 inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Right),
1741 InlayPoint::new(0, 2)
1742 );
1743
1744 assert_eq!(
1745 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
1746 InlayPoint::new(0, 2)
1747 );
1748 assert_eq!(
1749 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
1750 InlayPoint::new(0, 8)
1751 );
1752
1753 assert_eq!(
1754 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
1755 InlayPoint::new(0, 2)
1756 );
1757 assert_eq!(
1758 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
1759 InlayPoint::new(0, 8)
1760 );
1761
1762 assert_eq!(
1763 inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Left),
1764 InlayPoint::new(0, 2)
1765 );
1766 assert_eq!(
1767 inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Right),
1768 InlayPoint::new(0, 8)
1769 );
1770
1771 assert_eq!(
1772 inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Left),
1773 InlayPoint::new(0, 2)
1774 );
1775 assert_eq!(
1776 inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Right),
1777 InlayPoint::new(0, 8)
1778 );
1779
1780 assert_eq!(
1781 inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Left),
1782 InlayPoint::new(0, 2)
1783 );
1784 assert_eq!(
1785 inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Right),
1786 InlayPoint::new(0, 8)
1787 );
1788
1789 assert_eq!(
1790 inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Left),
1791 InlayPoint::new(0, 8)
1792 );
1793 assert_eq!(
1794 inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Right),
1795 InlayPoint::new(0, 8)
1796 );
1797
1798 assert_eq!(
1799 inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Left),
1800 InlayPoint::new(0, 9)
1801 );
1802 assert_eq!(
1803 inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Right),
1804 InlayPoint::new(0, 9)
1805 );
1806
1807 assert_eq!(
1808 inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Left),
1809 InlayPoint::new(0, 10)
1810 );
1811 assert_eq!(
1812 inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Right),
1813 InlayPoint::new(0, 10)
1814 );
1815
1816 assert_eq!(
1817 inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Left),
1818 InlayPoint::new(0, 11)
1819 );
1820 assert_eq!(
1821 inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Right),
1822 InlayPoint::new(0, 11)
1823 );
1824
1825 assert_eq!(
1826 inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Left),
1827 InlayPoint::new(0, 11)
1828 );
1829 assert_eq!(
1830 inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Right),
1831 InlayPoint::new(0, 17)
1832 );
1833
1834 assert_eq!(
1835 inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Left),
1836 InlayPoint::new(0, 11)
1837 );
1838 assert_eq!(
1839 inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Right),
1840 InlayPoint::new(0, 17)
1841 );
1842
1843 assert_eq!(
1844 inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Left),
1845 InlayPoint::new(0, 11)
1846 );
1847 assert_eq!(
1848 inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Right),
1849 InlayPoint::new(0, 17)
1850 );
1851
1852 assert_eq!(
1853 inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Left),
1854 InlayPoint::new(0, 11)
1855 );
1856 assert_eq!(
1857 inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Right),
1858 InlayPoint::new(0, 17)
1859 );
1860
1861 assert_eq!(
1862 inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Left),
1863 InlayPoint::new(0, 11)
1864 );
1865 assert_eq!(
1866 inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Right),
1867 InlayPoint::new(0, 17)
1868 );
1869
1870 assert_eq!(
1871 inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Left),
1872 InlayPoint::new(0, 17)
1873 );
1874 assert_eq!(
1875 inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Right),
1876 InlayPoint::new(0, 17)
1877 );
1878
1879 assert_eq!(
1880 inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Left),
1881 InlayPoint::new(0, 18)
1882 );
1883 assert_eq!(
1884 inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Right),
1885 InlayPoint::new(0, 18)
1886 );
1887
1888 // The inlays can be manually removed.
1889 let (inlay_snapshot, _) = inlay_map.splice(
1890 &inlay_map
1891 .inlays
1892 .iter()
1893 .map(|inlay| inlay.id)
1894 .collect::<Vec<InlayId>>(),
1895 Vec::new(),
1896 );
1897 assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
1898 }
1899
1900 #[gpui::test]
1901 fn test_inlay_buffer_rows(cx: &mut App) {
1902 let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx);
1903 let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
1904 assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi");
1905 let mut next_inlay_id = 0;
1906
1907 let (inlay_snapshot, _) = inlay_map.splice(
1908 &[],
1909 vec![
1910 Inlay::mock_hint(
1911 post_inc(&mut next_inlay_id),
1912 buffer
1913 .read(cx)
1914 .snapshot(cx)
1915 .anchor_before(MultiBufferOffset(0)),
1916 "|123|\n",
1917 ),
1918 Inlay::mock_hint(
1919 post_inc(&mut next_inlay_id),
1920 buffer
1921 .read(cx)
1922 .snapshot(cx)
1923 .anchor_before(MultiBufferOffset(4)),
1924 "|456|",
1925 ),
1926 Inlay::edit_prediction(
1927 post_inc(&mut next_inlay_id),
1928 buffer
1929 .read(cx)
1930 .snapshot(cx)
1931 .anchor_before(MultiBufferOffset(7)),
1932 "\n|567|\n",
1933 ),
1934 ],
1935 );
1936 assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
1937 assert_eq!(
1938 inlay_snapshot
1939 .row_infos(0)
1940 .map(|info| info.buffer_row)
1941 .collect::<Vec<_>>(),
1942 vec![Some(0), None, Some(1), None, None, Some(2)]
1943 );
1944 }
1945
1946 #[gpui::test(iterations = 100)]
1947 fn test_random_inlays(cx: &mut App, mut rng: StdRng) {
1948 init_test(cx);
1949
1950 let operations = env::var("OPERATIONS")
1951 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1952 .unwrap_or(10);
1953
1954 let len = rng.random_range(0..30);
1955 let buffer = if rng.random() {
1956 let text = util::RandomCharIter::new(&mut rng)
1957 .take(len)
1958 .collect::<String>();
1959 MultiBuffer::build_simple(&text, cx)
1960 } else {
1961 MultiBuffer::build_random(&mut rng, cx)
1962 };
1963 let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
1964 let mut next_inlay_id = 0;
1965 log::info!("buffer text: {:?}", buffer_snapshot.text());
1966 let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
1967 for _ in 0..operations {
1968 let mut inlay_edits = Patch::default();
1969
1970 let mut prev_inlay_text = inlay_snapshot.text();
1971 let mut buffer_edits = Vec::new();
1972 match rng.random_range(0..=100) {
1973 0..=50 => {
1974 let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
1975 log::info!("mutated text: {:?}", snapshot.text());
1976 inlay_edits = Patch::new(edits);
1977 }
1978 _ => buffer.update(cx, |buffer, cx| {
1979 let subscription = buffer.subscribe();
1980 let edit_count = rng.random_range(1..=5);
1981 buffer.randomly_mutate(&mut rng, edit_count, cx);
1982 buffer_snapshot = buffer.snapshot(cx);
1983 let edits = subscription.consume().into_inner();
1984 log::info!("editing {:?}", edits);
1985 buffer_edits.extend(edits);
1986 }),
1987 };
1988
1989 let (new_inlay_snapshot, new_inlay_edits) =
1990 inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
1991 inlay_snapshot = new_inlay_snapshot;
1992 inlay_edits = inlay_edits.compose(new_inlay_edits);
1993
1994 log::info!("buffer text: {:?}", buffer_snapshot.text());
1995 log::info!("inlay text: {:?}", inlay_snapshot.text());
1996
1997 let inlays = inlay_map
1998 .inlays
1999 .iter()
2000 .filter(|inlay| inlay.position.is_valid(&buffer_snapshot))
2001 .map(|inlay| {
2002 let offset = inlay.position.to_offset(&buffer_snapshot);
2003 (offset, inlay.clone())
2004 })
2005 .collect::<Vec<_>>();
2006 let mut expected_text = Rope::from(&buffer_snapshot.text());
2007 for (offset, inlay) in inlays.iter().rev() {
2008 expected_text.replace(offset.0..offset.0, &inlay.text().to_string());
2009 }
2010 assert_eq!(inlay_snapshot.text(), expected_text.to_string());
2011
2012 let expected_buffer_rows = inlay_snapshot.row_infos(0).collect::<Vec<_>>();
2013 assert_eq!(
2014 expected_buffer_rows.len() as u32,
2015 expected_text.max_point().row + 1
2016 );
2017 for row_start in 0..expected_buffer_rows.len() {
2018 assert_eq!(
2019 inlay_snapshot
2020 .row_infos(row_start as u32)
2021 .collect::<Vec<_>>(),
2022 &expected_buffer_rows[row_start..],
2023 "incorrect buffer rows starting at {}",
2024 row_start
2025 );
2026 }
2027
2028 let mut text_highlights = HashMap::default();
2029 let text_highlight_count = rng.random_range(0_usize..10);
2030 let mut text_highlight_ranges = (0..text_highlight_count)
2031 .map(|_| buffer_snapshot.random_byte_range(MultiBufferOffset(0), &mut rng))
2032 .collect::<Vec<_>>();
2033 text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
2034 log::info!("highlighting text ranges {text_highlight_ranges:?}");
2035 text_highlights.insert(
2036 HighlightKey::ColorizeBracket(0),
2037 Arc::new((
2038 HighlightStyle::default(),
2039 text_highlight_ranges
2040 .into_iter()
2041 .map(|range| {
2042 buffer_snapshot.anchor_before(range.start)
2043 ..buffer_snapshot.anchor_after(range.end)
2044 })
2045 .collect(),
2046 )),
2047 );
2048 let text_highlights = Arc::new(text_highlights);
2049
2050 let mut inlay_highlights = InlayHighlights::default();
2051 if !inlays.is_empty() {
2052 let inlay_highlight_count = rng.random_range(0..inlays.len());
2053 let mut inlay_indices = BTreeSet::default();
2054 while inlay_indices.len() < inlay_highlight_count {
2055 inlay_indices.insert(rng.random_range(0..inlays.len()));
2056 }
2057 let new_highlights = TreeMap::from_ordered_entries(
2058 inlay_indices
2059 .into_iter()
2060 .filter_map(|i| {
2061 let (_, inlay) = &inlays[i];
2062 let inlay_text_len = inlay.text().len();
2063 match inlay_text_len {
2064 0 => None,
2065 1 => Some(InlayHighlight {
2066 inlay: inlay.id,
2067 inlay_position: inlay.position,
2068 range: 0..1,
2069 }),
2070 n => {
2071 let inlay_text = inlay.text().to_string();
2072 let mut highlight_end = rng.random_range(1..n);
2073 let mut highlight_start = rng.random_range(0..highlight_end);
2074 while !inlay_text.is_char_boundary(highlight_end) {
2075 highlight_end += 1;
2076 }
2077 while !inlay_text.is_char_boundary(highlight_start) {
2078 highlight_start -= 1;
2079 }
2080 Some(InlayHighlight {
2081 inlay: inlay.id,
2082 inlay_position: inlay.position,
2083 range: highlight_start..highlight_end,
2084 })
2085 }
2086 }
2087 })
2088 .map(|highlight| (highlight.inlay, (HighlightStyle::default(), highlight))),
2089 );
2090 log::info!("highlighting inlay ranges {new_highlights:?}");
2091 inlay_highlights.insert(HighlightKey::Editor, new_highlights);
2092 }
2093
2094 for _ in 0..5 {
2095 let mut end = rng.random_range(0..=inlay_snapshot.len().0.0);
2096 end = expected_text.clip_offset(end, Bias::Right);
2097 let mut start = rng.random_range(0..=end);
2098 start = expected_text.clip_offset(start, Bias::Right);
2099
2100 let range =
2101 InlayOffset(MultiBufferOffset(start))..InlayOffset(MultiBufferOffset(end));
2102 log::info!("calling inlay_snapshot.chunks({range:?})");
2103 let actual_text = inlay_snapshot
2104 .chunks(
2105 range,
2106 LanguageAwareStyling {
2107 tree_sitter: false,
2108 diagnostics: false,
2109 },
2110 Highlights {
2111 text_highlights: Some(&text_highlights),
2112 inlay_highlights: Some(&inlay_highlights),
2113 ..Highlights::default()
2114 },
2115 )
2116 .map(|chunk| chunk.chunk.text)
2117 .collect::<String>();
2118 assert_eq!(
2119 actual_text,
2120 expected_text.slice(start..end).to_string(),
2121 "incorrect text in range {:?}",
2122 start..end
2123 );
2124
2125 assert_eq!(
2126 inlay_snapshot.text_summary_for_range(
2127 InlayOffset(MultiBufferOffset(start))..InlayOffset(MultiBufferOffset(end))
2128 ),
2129 MBTextSummary::from(expected_text.slice(start..end).summary())
2130 );
2131 }
2132
2133 for edit in inlay_edits {
2134 prev_inlay_text.replace_range(
2135 edit.new.start.0.0..edit.new.start.0.0 + edit.old_len(),
2136 &inlay_snapshot.text()[edit.new.start.0.0..edit.new.end.0.0],
2137 );
2138 }
2139 assert_eq!(prev_inlay_text, inlay_snapshot.text());
2140
2141 assert_eq!(expected_text.max_point(), inlay_snapshot.max_point().0);
2142 assert_eq!(expected_text.len(), inlay_snapshot.len().0.0);
2143
2144 let mut buffer_point = Point::default();
2145 let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
2146 let mut buffer_chars = buffer_snapshot.chars_at(MultiBufferOffset(0));
2147 loop {
2148 // Ensure conversion from buffer coordinates to inlay coordinates
2149 // is consistent.
2150 let buffer_offset = buffer_snapshot.point_to_offset(buffer_point);
2151 assert_eq!(
2152 inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)),
2153 inlay_point
2154 );
2155
2156 // No matter which bias we clip an inlay point with, it doesn't move
2157 // because it was constructed from a buffer point.
2158 assert_eq!(
2159 inlay_snapshot.clip_point(inlay_point, Bias::Left),
2160 inlay_point,
2161 "invalid inlay point for buffer point {:?} when clipped left",
2162 buffer_point
2163 );
2164 assert_eq!(
2165 inlay_snapshot.clip_point(inlay_point, Bias::Right),
2166 inlay_point,
2167 "invalid inlay point for buffer point {:?} when clipped right",
2168 buffer_point
2169 );
2170
2171 if let Some(ch) = buffer_chars.next() {
2172 if ch == '\n' {
2173 buffer_point += Point::new(1, 0);
2174 } else {
2175 buffer_point += Point::new(0, ch.len_utf8() as u32);
2176 }
2177
2178 // Ensure that moving forward in the buffer always moves the inlay point forward as well.
2179 let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
2180 assert!(new_inlay_point > inlay_point);
2181 inlay_point = new_inlay_point;
2182 } else {
2183 break;
2184 }
2185 }
2186
2187 let mut inlay_point = InlayPoint::default();
2188 let mut inlay_offset = InlayOffset::default();
2189 for ch in expected_text.chars() {
2190 assert_eq!(
2191 inlay_snapshot.to_offset(inlay_point),
2192 inlay_offset,
2193 "invalid to_offset({:?})",
2194 inlay_point
2195 );
2196 assert_eq!(
2197 inlay_snapshot.to_point(inlay_offset),
2198 inlay_point,
2199 "invalid to_point({:?})",
2200 inlay_offset
2201 );
2202
2203 let mut bytes = [0; 4];
2204 for byte in ch.encode_utf8(&mut bytes).as_bytes() {
2205 inlay_offset.0 += 1;
2206 if *byte == b'\n' {
2207 inlay_point.0 += Point::new(1, 0);
2208 } else {
2209 inlay_point.0 += Point::new(0, 1);
2210 }
2211
2212 let clipped_left_point = inlay_snapshot.clip_point(inlay_point, Bias::Left);
2213 let clipped_right_point = inlay_snapshot.clip_point(inlay_point, Bias::Right);
2214 assert!(
2215 clipped_left_point <= clipped_right_point,
2216 "inlay point {:?} when clipped left is greater than when clipped right ({:?} > {:?})",
2217 inlay_point,
2218 clipped_left_point,
2219 clipped_right_point
2220 );
2221
2222 // Ensure the clipped points are at valid text locations.
2223 assert_eq!(
2224 clipped_left_point.0,
2225 expected_text.clip_point(clipped_left_point.0, Bias::Left)
2226 );
2227 assert_eq!(
2228 clipped_right_point.0,
2229 expected_text.clip_point(clipped_right_point.0, Bias::Right)
2230 );
2231
2232 // Ensure the clipped points never overshoot the end of the map.
2233 assert!(clipped_left_point <= inlay_snapshot.max_point());
2234 assert!(clipped_right_point <= inlay_snapshot.max_point());
2235
2236 // Ensure the clipped points are at valid buffer locations.
2237 assert_eq!(
2238 inlay_snapshot
2239 .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)),
2240 clipped_left_point,
2241 "to_buffer_point({:?}) = {:?}",
2242 clipped_left_point,
2243 inlay_snapshot.to_buffer_point(clipped_left_point),
2244 );
2245 assert_eq!(
2246 inlay_snapshot
2247 .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)),
2248 clipped_right_point,
2249 "to_buffer_point({:?}) = {:?}",
2250 clipped_right_point,
2251 inlay_snapshot.to_buffer_point(clipped_right_point),
2252 );
2253 }
2254 }
2255 }
2256 }
2257
2258 #[gpui::test(iterations = 100)]
2259 fn test_random_chunk_bitmaps(cx: &mut gpui::App, mut rng: StdRng) {
2260 init_test(cx);
2261
2262 // Generate random buffer using existing test infrastructure
2263 let text_len = rng.random_range(0..10000);
2264 let buffer = if rng.random() {
2265 let text = RandomCharIter::new(&mut rng)
2266 .take(text_len)
2267 .collect::<String>();
2268 MultiBuffer::build_simple(&text, cx)
2269 } else {
2270 MultiBuffer::build_random(&mut rng, cx)
2271 };
2272
2273 let buffer_snapshot = buffer.read(cx).snapshot(cx);
2274 let (mut inlay_map, _) = InlayMap::new(buffer_snapshot.clone());
2275
2276 // Perform random mutations to add inlays
2277 let mut next_inlay_id = 0;
2278 let mutation_count = rng.random_range(1..10);
2279 for _ in 0..mutation_count {
2280 inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
2281 }
2282
2283 let (snapshot, _) = inlay_map.sync(buffer_snapshot, vec![]);
2284
2285 // Get all chunks and verify their bitmaps
2286 let chunks = snapshot.chunks(
2287 InlayOffset(MultiBufferOffset(0))..snapshot.len(),
2288 LanguageAwareStyling {
2289 tree_sitter: false,
2290 diagnostics: false,
2291 },
2292 Highlights::default(),
2293 );
2294
2295 for chunk in chunks.into_iter().map(|inlay_chunk| inlay_chunk.chunk) {
2296 let chunk_text = chunk.text;
2297 let chars_bitmap = chunk.chars;
2298 let tabs_bitmap = chunk.tabs;
2299
2300 // Check empty chunks have empty bitmaps
2301 if chunk_text.is_empty() {
2302 assert_eq!(
2303 chars_bitmap, 0,
2304 "Empty chunk should have empty chars bitmap"
2305 );
2306 assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
2307 continue;
2308 }
2309
2310 // Verify that chunk text doesn't exceed 128 bytes
2311 assert!(
2312 chunk_text.len() <= 128,
2313 "Chunk text length {} exceeds 128 bytes",
2314 chunk_text.len()
2315 );
2316
2317 // Verify chars bitmap
2318 let char_indices = chunk_text
2319 .char_indices()
2320 .map(|(i, _)| i)
2321 .collect::<Vec<_>>();
2322
2323 for byte_idx in 0..chunk_text.len() {
2324 let should_have_bit = char_indices.contains(&byte_idx);
2325 let has_bit = chars_bitmap & (1 << byte_idx) != 0;
2326
2327 if has_bit != should_have_bit {
2328 eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
2329 eprintln!("Char indices: {:?}", char_indices);
2330 eprintln!("Chars bitmap: {:#b}", chars_bitmap);
2331 assert_eq!(
2332 has_bit, should_have_bit,
2333 "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
2334 byte_idx, chunk_text, should_have_bit, has_bit
2335 );
2336 }
2337 }
2338
2339 // Verify tabs bitmap
2340 for (byte_idx, byte) in chunk_text.bytes().enumerate() {
2341 let is_tab = byte == b'\t';
2342 let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
2343
2344 if has_bit != is_tab {
2345 eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
2346 eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
2347 assert_eq!(
2348 has_bit, is_tab,
2349 "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
2350 byte_idx, chunk_text, byte as char, is_tab, has_bit
2351 );
2352 }
2353 }
2354 }
2355 }
2356
2357 fn init_test(cx: &mut App) {
2358 let store = SettingsStore::test(cx);
2359 cx.set_global(store);
2360 theme_settings::init(theme::LoadThemes::JustBase, cx);
2361 }
2362
2363 /// Helper to create test highlights for an inlay
2364 fn create_inlay_highlights(
2365 inlay_id: InlayId,
2366 highlight_range: Range<usize>,
2367 position: Anchor,
2368 ) -> TreeMap<HighlightKey, TreeMap<InlayId, (HighlightStyle, InlayHighlight)>> {
2369 let mut inlay_highlights = TreeMap::default();
2370 let mut type_highlights = TreeMap::default();
2371 type_highlights.insert(
2372 inlay_id,
2373 (
2374 HighlightStyle::default(),
2375 InlayHighlight {
2376 inlay: inlay_id,
2377 range: highlight_range,
2378 inlay_position: position,
2379 },
2380 ),
2381 );
2382 inlay_highlights.insert(HighlightKey::Editor, type_highlights);
2383 inlay_highlights
2384 }
2385
2386 #[gpui::test]
2387 fn test_inlay_utf8_boundary_panic_fix(cx: &mut App) {
2388 init_test(cx);
2389
2390 // This test verifies that we handle UTF-8 character boundaries correctly
2391 // when splitting inlay text for highlighting. Previously, this would panic
2392 // when trying to split at byte 13, which is in the middle of the '…' character.
2393 //
2394 // See https://github.com/zed-industries/zed/issues/33641
2395 let buffer = MultiBuffer::build_simple("fn main() {}\n", cx);
2396 let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx));
2397
2398 // Create an inlay with text that contains a multi-byte character
2399 // The string "SortingDirec…" contains an ellipsis character '…' which is 3 bytes (E2 80 A6)
2400 let inlay_text = "SortingDirec…";
2401 let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 5));
2402
2403 let inlay = Inlay {
2404 id: InlayId::Hint(0),
2405 position,
2406 content: InlayContent::Text(text::Rope::from(inlay_text)),
2407 };
2408
2409 let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]);
2410
2411 // Create highlights that request a split at byte 13, which is in the middle
2412 // of the '…' character (bytes 12..15). We include the full character.
2413 let inlay_highlights = create_inlay_highlights(InlayId::Hint(0), 0..13, position);
2414
2415 let highlights = crate::display_map::Highlights {
2416 text_highlights: None,
2417 inlay_highlights: Some(&inlay_highlights),
2418 semantic_token_highlights: None,
2419 styles: crate::display_map::HighlightStyles::default(),
2420 };
2421
2422 // Collect chunks - this previously would panic
2423 let chunks: Vec<_> = inlay_snapshot
2424 .chunks(
2425 InlayOffset(MultiBufferOffset(0))..inlay_snapshot.len(),
2426 LanguageAwareStyling {
2427 tree_sitter: false,
2428 diagnostics: false,
2429 },
2430 highlights,
2431 )
2432 .collect();
2433
2434 // Verify the chunks are correct
2435 let full_text: String = chunks.iter().map(|c| c.chunk.text).collect();
2436 assert_eq!(full_text, "fn maSortingDirec…in() {}\n");
2437
2438 // Verify the highlighted portion includes the complete ellipsis character
2439 let highlighted_chunks: Vec<_> = chunks
2440 .iter()
2441 .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay)
2442 .collect();
2443
2444 assert_eq!(highlighted_chunks.len(), 1);
2445 assert_eq!(highlighted_chunks[0].chunk.text, "SortingDirec…");
2446 }
2447
2448 #[gpui::test]
2449 fn test_inlay_utf8_boundaries(cx: &mut App) {
2450 init_test(cx);
2451
2452 struct TestCase {
2453 inlay_text: &'static str,
2454 highlight_range: Range<usize>,
2455 expected_highlighted: &'static str,
2456 description: &'static str,
2457 }
2458
2459 let test_cases = vec![
2460 TestCase {
2461 inlay_text: "Hello👋World",
2462 highlight_range: 0..7,
2463 expected_highlighted: "Hello👋",
2464 description: "Emoji boundary - rounds up to include full emoji",
2465 },
2466 TestCase {
2467 inlay_text: "Test→End",
2468 highlight_range: 0..5,
2469 expected_highlighted: "Test→",
2470 description: "Arrow boundary - rounds up to include full arrow",
2471 },
2472 TestCase {
2473 inlay_text: "café",
2474 highlight_range: 0..4,
2475 expected_highlighted: "café",
2476 description: "Accented char boundary - rounds up to include full é",
2477 },
2478 TestCase {
2479 inlay_text: "🎨🎭🎪",
2480 highlight_range: 0..5,
2481 expected_highlighted: "🎨🎭",
2482 description: "Multiple emojis - partial highlight",
2483 },
2484 TestCase {
2485 inlay_text: "普通话",
2486 highlight_range: 0..4,
2487 expected_highlighted: "普通",
2488 description: "Chinese characters - partial highlight",
2489 },
2490 TestCase {
2491 inlay_text: "Hello",
2492 highlight_range: 0..2,
2493 expected_highlighted: "He",
2494 description: "ASCII only - no adjustment needed",
2495 },
2496 TestCase {
2497 inlay_text: "👋",
2498 highlight_range: 0..1,
2499 expected_highlighted: "👋",
2500 description: "Single emoji - partial byte range includes whole char",
2501 },
2502 TestCase {
2503 inlay_text: "Test",
2504 highlight_range: 0..0,
2505 expected_highlighted: "",
2506 description: "Empty range",
2507 },
2508 TestCase {
2509 inlay_text: "🎨ABC",
2510 highlight_range: 2..5,
2511 expected_highlighted: "A",
2512 description: "Range starting mid-emoji skips the emoji",
2513 },
2514 ];
2515
2516 for test_case in test_cases {
2517 let buffer = MultiBuffer::build_simple("test", cx);
2518 let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx));
2519 let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 2));
2520
2521 let inlay = Inlay {
2522 id: InlayId::Hint(0),
2523 position,
2524 content: InlayContent::Text(text::Rope::from(test_case.inlay_text)),
2525 };
2526
2527 let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]);
2528 let inlay_highlights = create_inlay_highlights(
2529 InlayId::Hint(0),
2530 test_case.highlight_range.clone(),
2531 position,
2532 );
2533
2534 let highlights = crate::display_map::Highlights {
2535 text_highlights: None,
2536 inlay_highlights: Some(&inlay_highlights),
2537 semantic_token_highlights: None,
2538 styles: crate::display_map::HighlightStyles::default(),
2539 };
2540
2541 let chunks: Vec<_> = inlay_snapshot
2542 .chunks(
2543 InlayOffset(MultiBufferOffset(0))..inlay_snapshot.len(),
2544 LanguageAwareStyling {
2545 tree_sitter: false,
2546 diagnostics: false,
2547 },
2548 highlights,
2549 )
2550 .collect();
2551
2552 // Verify we got chunks and they total to the expected text
2553 let full_text: String = chunks.iter().map(|c| c.chunk.text).collect();
2554 assert_eq!(
2555 full_text,
2556 format!("te{}st", test_case.inlay_text),
2557 "Full text mismatch for case: {}",
2558 test_case.description
2559 );
2560
2561 // Verify that the highlighted portion matches expectations
2562 let highlighted_text: String = chunks
2563 .iter()
2564 .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay)
2565 .map(|c| c.chunk.text)
2566 .collect();
2567 assert_eq!(
2568 highlighted_text, test_case.expected_highlighted,
2569 "Highlighted text mismatch for case: {} (text: '{}', range: {:?})",
2570 test_case.description, test_case.inlay_text, test_case.highlight_range
2571 );
2572 }
2573 }
2574}
2575