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