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