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, Dimensions, 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.reversed_chars_at(text.len()).next() != Some(' ') {
52 text.push(" ");
53 }
54 if hint.padding_left && text.chars_at(0).next() != Some(' ') {
55 text.push_front(" ");
56 }
57 Self {
58 id: InlayId::Hint(id),
59 position,
60 text,
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, Dimensions<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, Dimensions<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
555 .transforms
556 .cursor::<Dimensions<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 && cursor.end().0 == buffer_edit.old.start
562 {
563 push_isomorphic(&mut new_transforms, *transform);
564 cursor.next();
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 .is_none_or(|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.random_range(1..=5) {
723 if self.inlays.is_empty() || rng.random() {
724 let position = snapshot.buffer.random_byte_range(0, rng).start;
725 let bias = if rng.random() {
726 Bias::Left
727 } else {
728 Bias::Right
729 };
730 let len = if rng.random_bool(0.01) {
731 0
732 } else {
733 rng.random_range(1..=5)
734 };
735 let text = util::RandomCharIter::new(&mut *rng)
736 .filter(|ch| *ch != '\r')
737 .take(len)
738 .collect::<String>();
739
740 let next_inlay = if i % 2 == 0 {
741 Inlay::mock_hint(
742 post_inc(next_inlay_id),
743 snapshot.buffer.anchor_at(position, bias),
744 &text,
745 )
746 } else {
747 Inlay::edit_prediction(
748 post_inc(next_inlay_id),
749 snapshot.buffer.anchor_at(position, bias),
750 &text,
751 )
752 };
753 let inlay_id = next_inlay.id;
754 log::info!(
755 "creating inlay {inlay_id:?} at buffer offset {position} with bias {bias:?} and text {text:?}"
756 );
757 to_insert.push(next_inlay);
758 } else {
759 to_remove.push(
760 self.inlays
761 .iter()
762 .choose(rng)
763 .map(|inlay| inlay.id)
764 .unwrap(),
765 );
766 }
767 }
768 log::info!("removing inlays: {:?}", to_remove);
769
770 let (snapshot, edits) = self.splice(&to_remove, to_insert);
771 (snapshot, edits)
772 }
773}
774
775impl InlaySnapshot {
776 pub fn to_point(&self, offset: InlayOffset) -> InlayPoint {
777 let mut cursor = self
778 .transforms
779 .cursor::<Dimensions<InlayOffset, InlayPoint, usize>>(&());
780 cursor.seek(&offset, Bias::Right);
781 let overshoot = offset.0 - cursor.start().0.0;
782 match cursor.item() {
783 Some(Transform::Isomorphic(_)) => {
784 let buffer_offset_start = cursor.start().2;
785 let buffer_offset_end = buffer_offset_start + overshoot;
786 let buffer_start = self.buffer.offset_to_point(buffer_offset_start);
787 let buffer_end = self.buffer.offset_to_point(buffer_offset_end);
788 InlayPoint(cursor.start().1.0 + (buffer_end - buffer_start))
789 }
790 Some(Transform::Inlay(inlay)) => {
791 let overshoot = inlay.text.offset_to_point(overshoot);
792 InlayPoint(cursor.start().1.0 + overshoot)
793 }
794 None => self.max_point(),
795 }
796 }
797
798 pub fn len(&self) -> InlayOffset {
799 InlayOffset(self.transforms.summary().output.len)
800 }
801
802 pub fn max_point(&self) -> InlayPoint {
803 InlayPoint(self.transforms.summary().output.lines)
804 }
805
806 pub fn to_offset(&self, point: InlayPoint) -> InlayOffset {
807 let mut cursor = self
808 .transforms
809 .cursor::<Dimensions<InlayPoint, InlayOffset, Point>>(&());
810 cursor.seek(&point, Bias::Right);
811 let overshoot = point.0 - cursor.start().0.0;
812 match cursor.item() {
813 Some(Transform::Isomorphic(_)) => {
814 let buffer_point_start = cursor.start().2;
815 let buffer_point_end = buffer_point_start + overshoot;
816 let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start);
817 let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end);
818 InlayOffset(cursor.start().1.0 + (buffer_offset_end - buffer_offset_start))
819 }
820 Some(Transform::Inlay(inlay)) => {
821 let overshoot = inlay.text.point_to_offset(overshoot);
822 InlayOffset(cursor.start().1.0 + overshoot)
823 }
824 None => self.len(),
825 }
826 }
827 pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
828 let mut cursor = self.transforms.cursor::<Dimensions<InlayPoint, Point>>(&());
829 cursor.seek(&point, Bias::Right);
830 match cursor.item() {
831 Some(Transform::Isomorphic(_)) => {
832 let overshoot = point.0 - cursor.start().0.0;
833 cursor.start().1 + overshoot
834 }
835 Some(Transform::Inlay(_)) => cursor.start().1,
836 None => self.buffer.max_point(),
837 }
838 }
839 pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize {
840 let mut cursor = self
841 .transforms
842 .cursor::<Dimensions<InlayOffset, usize>>(&());
843 cursor.seek(&offset, Bias::Right);
844 match cursor.item() {
845 Some(Transform::Isomorphic(_)) => {
846 let overshoot = offset - cursor.start().0;
847 cursor.start().1 + overshoot.0
848 }
849 Some(Transform::Inlay(_)) => cursor.start().1,
850 None => self.buffer.len(),
851 }
852 }
853
854 pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset {
855 let mut cursor = self
856 .transforms
857 .cursor::<Dimensions<usize, InlayOffset>>(&());
858 cursor.seek(&offset, Bias::Left);
859 loop {
860 match cursor.item() {
861 Some(Transform::Isomorphic(_)) => {
862 if offset == cursor.end().0 {
863 while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
864 if inlay.position.bias() == Bias::Right {
865 break;
866 } else {
867 cursor.next();
868 }
869 }
870 return cursor.end().1;
871 } else {
872 let overshoot = offset - cursor.start().0;
873 return InlayOffset(cursor.start().1.0 + overshoot);
874 }
875 }
876 Some(Transform::Inlay(inlay)) => {
877 if inlay.position.bias() == Bias::Left {
878 cursor.next();
879 } else {
880 return cursor.start().1;
881 }
882 }
883 None => {
884 return self.len();
885 }
886 }
887 }
888 }
889 pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
890 let mut cursor = self.transforms.cursor::<Dimensions<Point, InlayPoint>>(&());
891 cursor.seek(&point, Bias::Left);
892 loop {
893 match cursor.item() {
894 Some(Transform::Isomorphic(_)) => {
895 if point == cursor.end().0 {
896 while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
897 if inlay.position.bias() == Bias::Right {
898 break;
899 } else {
900 cursor.next();
901 }
902 }
903 return cursor.end().1;
904 } else {
905 let overshoot = point - cursor.start().0;
906 return InlayPoint(cursor.start().1.0 + overshoot);
907 }
908 }
909 Some(Transform::Inlay(inlay)) => {
910 if inlay.position.bias() == Bias::Left {
911 cursor.next();
912 } else {
913 return cursor.start().1;
914 }
915 }
916 None => {
917 return self.max_point();
918 }
919 }
920 }
921 }
922
923 pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint {
924 let mut cursor = self.transforms.cursor::<Dimensions<InlayPoint, Point>>(&());
925 cursor.seek(&point, Bias::Left);
926 loop {
927 match cursor.item() {
928 Some(Transform::Isomorphic(transform)) => {
929 if cursor.start().0 == point {
930 if let Some(Transform::Inlay(inlay)) = cursor.prev_item() {
931 if inlay.position.bias() == Bias::Left {
932 return point;
933 } else if bias == Bias::Left {
934 cursor.prev();
935 } else if transform.first_line_chars == 0 {
936 point.0 += Point::new(1, 0);
937 } else {
938 point.0 += Point::new(0, 1);
939 }
940 } else {
941 return point;
942 }
943 } else if cursor.end().0 == point {
944 if let Some(Transform::Inlay(inlay)) = cursor.next_item() {
945 if inlay.position.bias() == Bias::Right {
946 return point;
947 } else if bias == Bias::Right {
948 cursor.next();
949 } else if point.0.column == 0 {
950 point.0.row -= 1;
951 point.0.column = self.line_len(point.0.row);
952 } else {
953 point.0.column -= 1;
954 }
955 } else {
956 return point;
957 }
958 } else {
959 let overshoot = point.0 - cursor.start().0.0;
960 let buffer_point = cursor.start().1 + overshoot;
961 let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias);
962 let clipped_overshoot = clipped_buffer_point - cursor.start().1;
963 let clipped_point = InlayPoint(cursor.start().0.0 + clipped_overshoot);
964 if clipped_point == point {
965 return clipped_point;
966 } else {
967 point = clipped_point;
968 }
969 }
970 }
971 Some(Transform::Inlay(inlay)) => {
972 if point == cursor.start().0 && inlay.position.bias() == Bias::Right {
973 match cursor.prev_item() {
974 Some(Transform::Inlay(inlay)) => {
975 if inlay.position.bias() == Bias::Left {
976 return point;
977 }
978 }
979 _ => return point,
980 }
981 } else if point == cursor.end().0 && inlay.position.bias() == Bias::Left {
982 match cursor.next_item() {
983 Some(Transform::Inlay(inlay)) => {
984 if inlay.position.bias() == Bias::Right {
985 return point;
986 }
987 }
988 _ => return point,
989 }
990 }
991
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 None => {
1001 bias = bias.invert();
1002 if bias == Bias::Left {
1003 point = cursor.start().0;
1004 cursor.prev();
1005 } else {
1006 cursor.next();
1007 point = cursor.start().0;
1008 }
1009 }
1010 }
1011 }
1012 }
1013
1014 pub fn text_summary(&self) -> TextSummary {
1015 self.transforms.summary().output
1016 }
1017
1018 pub fn text_summary_for_range(&self, range: Range<InlayOffset>) -> TextSummary {
1019 let mut summary = TextSummary::default();
1020
1021 let mut cursor = self
1022 .transforms
1023 .cursor::<Dimensions<InlayOffset, usize>>(&());
1024 cursor.seek(&range.start, Bias::Right);
1025
1026 let overshoot = range.start.0 - cursor.start().0.0;
1027 match cursor.item() {
1028 Some(Transform::Isomorphic(_)) => {
1029 let buffer_start = cursor.start().1;
1030 let suffix_start = buffer_start + overshoot;
1031 let suffix_end =
1032 buffer_start + (cmp::min(cursor.end().0, range.end).0 - cursor.start().0.0);
1033 summary = self.buffer.text_summary_for_range(suffix_start..suffix_end);
1034 cursor.next();
1035 }
1036 Some(Transform::Inlay(inlay)) => {
1037 let suffix_start = overshoot;
1038 let suffix_end = cmp::min(cursor.end().0, range.end).0 - cursor.start().0.0;
1039 summary = inlay.text.cursor(suffix_start).summary(suffix_end);
1040 cursor.next();
1041 }
1042 None => {}
1043 }
1044
1045 if range.end > cursor.start().0 {
1046 summary += cursor
1047 .summary::<_, TransformSummary>(&range.end, Bias::Right)
1048 .output;
1049
1050 let overshoot = range.end.0 - cursor.start().0.0;
1051 match cursor.item() {
1052 Some(Transform::Isomorphic(_)) => {
1053 let prefix_start = cursor.start().1;
1054 let prefix_end = prefix_start + overshoot;
1055 summary += self
1056 .buffer
1057 .text_summary_for_range::<TextSummary, _>(prefix_start..prefix_end);
1058 }
1059 Some(Transform::Inlay(inlay)) => {
1060 let prefix_end = overshoot;
1061 summary += inlay.text.cursor(0).summary::<TextSummary>(prefix_end);
1062 }
1063 None => {}
1064 }
1065 }
1066
1067 summary
1068 }
1069
1070 pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> {
1071 let mut cursor = self.transforms.cursor::<Dimensions<InlayPoint, Point>>(&());
1072 let inlay_point = InlayPoint::new(row, 0);
1073 cursor.seek(&inlay_point, Bias::Left);
1074
1075 let max_buffer_row = self.buffer.max_row();
1076 let mut buffer_point = cursor.start().1;
1077 let buffer_row = if row == 0 {
1078 MultiBufferRow(0)
1079 } else {
1080 match cursor.item() {
1081 Some(Transform::Isomorphic(_)) => {
1082 buffer_point += inlay_point.0 - cursor.start().0.0;
1083 MultiBufferRow(buffer_point.row)
1084 }
1085 _ => cmp::min(MultiBufferRow(buffer_point.row + 1), max_buffer_row),
1086 }
1087 };
1088
1089 InlayBufferRows {
1090 transforms: cursor,
1091 inlay_row: inlay_point.row(),
1092 buffer_rows: self.buffer.row_infos(buffer_row),
1093 max_buffer_row,
1094 }
1095 }
1096
1097 pub fn line_len(&self, row: u32) -> u32 {
1098 let line_start = self.to_offset(InlayPoint::new(row, 0)).0;
1099 let line_end = if row >= self.max_point().row() {
1100 self.len().0
1101 } else {
1102 self.to_offset(InlayPoint::new(row + 1, 0)).0 - 1
1103 };
1104 (line_end - line_start) as u32
1105 }
1106
1107 pub(crate) fn chunks<'a>(
1108 &'a self,
1109 range: Range<InlayOffset>,
1110 language_aware: bool,
1111 highlights: Highlights<'a>,
1112 ) -> InlayChunks<'a> {
1113 let mut cursor = self
1114 .transforms
1115 .cursor::<Dimensions<InlayOffset, usize>>(&());
1116 cursor.seek(&range.start, Bias::Right);
1117
1118 let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
1119 let buffer_chunks = CustomHighlightsChunks::new(
1120 buffer_range,
1121 language_aware,
1122 highlights.text_highlights,
1123 &self.buffer,
1124 );
1125
1126 InlayChunks {
1127 transforms: cursor,
1128 buffer_chunks,
1129 inlay_chunks: None,
1130 inlay_chunk: None,
1131 buffer_chunk: None,
1132 output_offset: range.start,
1133 max_output_offset: range.end,
1134 highlight_styles: highlights.styles,
1135 highlights,
1136 snapshot: self,
1137 }
1138 }
1139
1140 #[cfg(test)]
1141 pub fn text(&self) -> String {
1142 self.chunks(Default::default()..self.len(), false, Highlights::default())
1143 .map(|chunk| chunk.chunk.text)
1144 .collect()
1145 }
1146
1147 fn check_invariants(&self) {
1148 #[cfg(any(debug_assertions, feature = "test-support"))]
1149 {
1150 assert_eq!(self.transforms.summary().input, self.buffer.text_summary());
1151 let mut transforms = self.transforms.iter().peekable();
1152 while let Some(transform) = transforms.next() {
1153 let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_));
1154 if let Some(next_transform) = transforms.peek() {
1155 let next_transform_is_isomorphic =
1156 matches!(next_transform, Transform::Isomorphic(_));
1157 assert!(
1158 !transform_is_isomorphic || !next_transform_is_isomorphic,
1159 "two adjacent isomorphic transforms"
1160 );
1161 }
1162 }
1163 }
1164 }
1165}
1166
1167fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
1168 if summary.len == 0 {
1169 return;
1170 }
1171
1172 let mut summary = Some(summary);
1173 sum_tree.update_last(
1174 |transform| {
1175 if let Transform::Isomorphic(transform) = transform {
1176 *transform += summary.take().unwrap();
1177 }
1178 },
1179 &(),
1180 );
1181
1182 if let Some(summary) = summary {
1183 sum_tree.push(Transform::Isomorphic(summary), &());
1184 }
1185}
1186
1187/// Given a byte index that is NOT a UTF-8 boundary, find the next one.
1188/// Assumes: 0 < byte_index < text.len() and !text.is_char_boundary(byte_index)
1189#[inline(always)]
1190fn find_next_utf8_boundary(text: &str, byte_index: usize) -> usize {
1191 let bytes = text.as_bytes();
1192 let mut idx = byte_index + 1;
1193
1194 // Scan forward until we find a boundary
1195 while idx < text.len() {
1196 if is_utf8_char_boundary(bytes[idx]) {
1197 return idx;
1198 }
1199 idx += 1;
1200 }
1201
1202 // Hit the end, return the full length
1203 text.len()
1204}
1205
1206// Private helper function taken from Rust's core::num module (which is both Apache2 and MIT licensed)
1207const fn is_utf8_char_boundary(byte: u8) -> bool {
1208 // This is bit magic equivalent to: b < 128 || b >= 192
1209 (byte as i8) >= -0x40
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214 use super::*;
1215 use crate::{
1216 InlayId, MultiBuffer,
1217 display_map::{HighlightKey, InlayHighlights, TextHighlights},
1218 hover_links::InlayHighlight,
1219 };
1220 use gpui::{App, HighlightStyle};
1221 use project::{InlayHint, InlayHintLabel, ResolveState};
1222 use rand::prelude::*;
1223 use settings::SettingsStore;
1224 use std::{any::TypeId, cmp::Reverse, env, sync::Arc};
1225 use sum_tree::TreeMap;
1226 use text::Patch;
1227 use util::post_inc;
1228
1229 #[test]
1230 fn test_inlay_properties_label_padding() {
1231 assert_eq!(
1232 Inlay::hint(
1233 0,
1234 Anchor::min(),
1235 &InlayHint {
1236 label: InlayHintLabel::String("a".to_string()),
1237 position: text::Anchor::default(),
1238 padding_left: false,
1239 padding_right: false,
1240 tooltip: None,
1241 kind: None,
1242 resolve_state: ResolveState::Resolved,
1243 },
1244 )
1245 .text
1246 .to_string(),
1247 "a",
1248 "Should not pad label if not requested"
1249 );
1250
1251 assert_eq!(
1252 Inlay::hint(
1253 0,
1254 Anchor::min(),
1255 &InlayHint {
1256 label: InlayHintLabel::String("a".to_string()),
1257 position: text::Anchor::default(),
1258 padding_left: true,
1259 padding_right: true,
1260 tooltip: None,
1261 kind: None,
1262 resolve_state: ResolveState::Resolved,
1263 },
1264 )
1265 .text
1266 .to_string(),
1267 " a ",
1268 "Should pad label for every side requested"
1269 );
1270
1271 assert_eq!(
1272 Inlay::hint(
1273 0,
1274 Anchor::min(),
1275 &InlayHint {
1276 label: InlayHintLabel::String(" a ".to_string()),
1277 position: text::Anchor::default(),
1278 padding_left: false,
1279 padding_right: false,
1280 tooltip: None,
1281 kind: None,
1282 resolve_state: ResolveState::Resolved,
1283 },
1284 )
1285 .text
1286 .to_string(),
1287 " a ",
1288 "Should not change already padded label"
1289 );
1290
1291 assert_eq!(
1292 Inlay::hint(
1293 0,
1294 Anchor::min(),
1295 &InlayHint {
1296 label: InlayHintLabel::String(" a ".to_string()),
1297 position: text::Anchor::default(),
1298 padding_left: true,
1299 padding_right: true,
1300 tooltip: None,
1301 kind: None,
1302 resolve_state: ResolveState::Resolved,
1303 },
1304 )
1305 .text
1306 .to_string(),
1307 " a ",
1308 "Should not change already padded label"
1309 );
1310 }
1311
1312 #[gpui::test]
1313 fn test_inlay_hint_padding_with_multibyte_chars() {
1314 assert_eq!(
1315 Inlay::hint(
1316 0,
1317 Anchor::min(),
1318 &InlayHint {
1319 label: InlayHintLabel::String("🎨".to_string()),
1320 position: text::Anchor::default(),
1321 padding_left: true,
1322 padding_right: true,
1323 tooltip: None,
1324 kind: None,
1325 resolve_state: ResolveState::Resolved,
1326 },
1327 )
1328 .text
1329 .to_string(),
1330 " 🎨 ",
1331 "Should pad single emoji correctly"
1332 );
1333 }
1334
1335 #[gpui::test]
1336 fn test_basic_inlays(cx: &mut App) {
1337 let buffer = MultiBuffer::build_simple("abcdefghi", cx);
1338 let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
1339 let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
1340 assert_eq!(inlay_snapshot.text(), "abcdefghi");
1341 let mut next_inlay_id = 0;
1342
1343 let (inlay_snapshot, _) = inlay_map.splice(
1344 &[],
1345 vec![Inlay::mock_hint(
1346 post_inc(&mut next_inlay_id),
1347 buffer.read(cx).snapshot(cx).anchor_after(3),
1348 "|123|",
1349 )],
1350 );
1351 assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
1352 assert_eq!(
1353 inlay_snapshot.to_inlay_point(Point::new(0, 0)),
1354 InlayPoint::new(0, 0)
1355 );
1356 assert_eq!(
1357 inlay_snapshot.to_inlay_point(Point::new(0, 1)),
1358 InlayPoint::new(0, 1)
1359 );
1360 assert_eq!(
1361 inlay_snapshot.to_inlay_point(Point::new(0, 2)),
1362 InlayPoint::new(0, 2)
1363 );
1364 assert_eq!(
1365 inlay_snapshot.to_inlay_point(Point::new(0, 3)),
1366 InlayPoint::new(0, 3)
1367 );
1368 assert_eq!(
1369 inlay_snapshot.to_inlay_point(Point::new(0, 4)),
1370 InlayPoint::new(0, 9)
1371 );
1372 assert_eq!(
1373 inlay_snapshot.to_inlay_point(Point::new(0, 5)),
1374 InlayPoint::new(0, 10)
1375 );
1376 assert_eq!(
1377 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
1378 InlayPoint::new(0, 0)
1379 );
1380 assert_eq!(
1381 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
1382 InlayPoint::new(0, 0)
1383 );
1384 assert_eq!(
1385 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
1386 InlayPoint::new(0, 3)
1387 );
1388 assert_eq!(
1389 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
1390 InlayPoint::new(0, 3)
1391 );
1392 assert_eq!(
1393 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
1394 InlayPoint::new(0, 3)
1395 );
1396 assert_eq!(
1397 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
1398 InlayPoint::new(0, 9)
1399 );
1400
1401 // Edits before or after the inlay should not affect it.
1402 buffer.update(cx, |buffer, cx| {
1403 buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx)
1404 });
1405 let (inlay_snapshot, _) = inlay_map.sync(
1406 buffer.read(cx).snapshot(cx),
1407 buffer_edits.consume().into_inner(),
1408 );
1409 assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi");
1410
1411 // An edit surrounding the inlay should invalidate it.
1412 buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx));
1413 let (inlay_snapshot, _) = inlay_map.sync(
1414 buffer.read(cx).snapshot(cx),
1415 buffer_edits.consume().into_inner(),
1416 );
1417 assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
1418
1419 let (inlay_snapshot, _) = inlay_map.splice(
1420 &[],
1421 vec![
1422 Inlay::mock_hint(
1423 post_inc(&mut next_inlay_id),
1424 buffer.read(cx).snapshot(cx).anchor_before(3),
1425 "|123|",
1426 ),
1427 Inlay::edit_prediction(
1428 post_inc(&mut next_inlay_id),
1429 buffer.read(cx).snapshot(cx).anchor_after(3),
1430 "|456|",
1431 ),
1432 ],
1433 );
1434 assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
1435
1436 // Edits ending where the inlay starts should not move it if it has a left bias.
1437 buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx));
1438 let (inlay_snapshot, _) = inlay_map.sync(
1439 buffer.read(cx).snapshot(cx),
1440 buffer_edits.consume().into_inner(),
1441 );
1442 assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi");
1443
1444 assert_eq!(
1445 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
1446 InlayPoint::new(0, 0)
1447 );
1448 assert_eq!(
1449 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
1450 InlayPoint::new(0, 0)
1451 );
1452
1453 assert_eq!(
1454 inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Left),
1455 InlayPoint::new(0, 1)
1456 );
1457 assert_eq!(
1458 inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Right),
1459 InlayPoint::new(0, 1)
1460 );
1461
1462 assert_eq!(
1463 inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Left),
1464 InlayPoint::new(0, 2)
1465 );
1466 assert_eq!(
1467 inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Right),
1468 InlayPoint::new(0, 2)
1469 );
1470
1471 assert_eq!(
1472 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
1473 InlayPoint::new(0, 2)
1474 );
1475 assert_eq!(
1476 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
1477 InlayPoint::new(0, 8)
1478 );
1479
1480 assert_eq!(
1481 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
1482 InlayPoint::new(0, 2)
1483 );
1484 assert_eq!(
1485 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
1486 InlayPoint::new(0, 8)
1487 );
1488
1489 assert_eq!(
1490 inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Left),
1491 InlayPoint::new(0, 2)
1492 );
1493 assert_eq!(
1494 inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Right),
1495 InlayPoint::new(0, 8)
1496 );
1497
1498 assert_eq!(
1499 inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Left),
1500 InlayPoint::new(0, 2)
1501 );
1502 assert_eq!(
1503 inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Right),
1504 InlayPoint::new(0, 8)
1505 );
1506
1507 assert_eq!(
1508 inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Left),
1509 InlayPoint::new(0, 2)
1510 );
1511 assert_eq!(
1512 inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Right),
1513 InlayPoint::new(0, 8)
1514 );
1515
1516 assert_eq!(
1517 inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Left),
1518 InlayPoint::new(0, 8)
1519 );
1520 assert_eq!(
1521 inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Right),
1522 InlayPoint::new(0, 8)
1523 );
1524
1525 assert_eq!(
1526 inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Left),
1527 InlayPoint::new(0, 9)
1528 );
1529 assert_eq!(
1530 inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Right),
1531 InlayPoint::new(0, 9)
1532 );
1533
1534 assert_eq!(
1535 inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Left),
1536 InlayPoint::new(0, 10)
1537 );
1538 assert_eq!(
1539 inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Right),
1540 InlayPoint::new(0, 10)
1541 );
1542
1543 assert_eq!(
1544 inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Left),
1545 InlayPoint::new(0, 11)
1546 );
1547 assert_eq!(
1548 inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Right),
1549 InlayPoint::new(0, 11)
1550 );
1551
1552 assert_eq!(
1553 inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Left),
1554 InlayPoint::new(0, 11)
1555 );
1556 assert_eq!(
1557 inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Right),
1558 InlayPoint::new(0, 17)
1559 );
1560
1561 assert_eq!(
1562 inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Left),
1563 InlayPoint::new(0, 11)
1564 );
1565 assert_eq!(
1566 inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Right),
1567 InlayPoint::new(0, 17)
1568 );
1569
1570 assert_eq!(
1571 inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Left),
1572 InlayPoint::new(0, 11)
1573 );
1574 assert_eq!(
1575 inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Right),
1576 InlayPoint::new(0, 17)
1577 );
1578
1579 assert_eq!(
1580 inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Left),
1581 InlayPoint::new(0, 11)
1582 );
1583 assert_eq!(
1584 inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Right),
1585 InlayPoint::new(0, 17)
1586 );
1587
1588 assert_eq!(
1589 inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Left),
1590 InlayPoint::new(0, 11)
1591 );
1592 assert_eq!(
1593 inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Right),
1594 InlayPoint::new(0, 17)
1595 );
1596
1597 assert_eq!(
1598 inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Left),
1599 InlayPoint::new(0, 17)
1600 );
1601 assert_eq!(
1602 inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Right),
1603 InlayPoint::new(0, 17)
1604 );
1605
1606 assert_eq!(
1607 inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Left),
1608 InlayPoint::new(0, 18)
1609 );
1610 assert_eq!(
1611 inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Right),
1612 InlayPoint::new(0, 18)
1613 );
1614
1615 // The inlays can be manually removed.
1616 let (inlay_snapshot, _) = inlay_map.splice(
1617 &inlay_map
1618 .inlays
1619 .iter()
1620 .map(|inlay| inlay.id)
1621 .collect::<Vec<InlayId>>(),
1622 Vec::new(),
1623 );
1624 assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
1625 }
1626
1627 #[gpui::test]
1628 fn test_inlay_buffer_rows(cx: &mut App) {
1629 let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx);
1630 let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
1631 assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi");
1632 let mut next_inlay_id = 0;
1633
1634 let (inlay_snapshot, _) = inlay_map.splice(
1635 &[],
1636 vec![
1637 Inlay::mock_hint(
1638 post_inc(&mut next_inlay_id),
1639 buffer.read(cx).snapshot(cx).anchor_before(0),
1640 "|123|\n",
1641 ),
1642 Inlay::mock_hint(
1643 post_inc(&mut next_inlay_id),
1644 buffer.read(cx).snapshot(cx).anchor_before(4),
1645 "|456|",
1646 ),
1647 Inlay::edit_prediction(
1648 post_inc(&mut next_inlay_id),
1649 buffer.read(cx).snapshot(cx).anchor_before(7),
1650 "\n|567|\n",
1651 ),
1652 ],
1653 );
1654 assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
1655 assert_eq!(
1656 inlay_snapshot
1657 .row_infos(0)
1658 .map(|info| info.buffer_row)
1659 .collect::<Vec<_>>(),
1660 vec![Some(0), None, Some(1), None, None, Some(2)]
1661 );
1662 }
1663
1664 #[gpui::test(iterations = 100)]
1665 fn test_random_inlays(cx: &mut App, mut rng: StdRng) {
1666 init_test(cx);
1667
1668 let operations = env::var("OPERATIONS")
1669 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1670 .unwrap_or(10);
1671
1672 let len = rng.random_range(0..30);
1673 let buffer = if rng.random() {
1674 let text = util::RandomCharIter::new(&mut rng)
1675 .take(len)
1676 .collect::<String>();
1677 MultiBuffer::build_simple(&text, cx)
1678 } else {
1679 MultiBuffer::build_random(&mut rng, cx)
1680 };
1681 let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
1682 let mut next_inlay_id = 0;
1683 log::info!("buffer text: {:?}", buffer_snapshot.text());
1684 let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
1685 for _ in 0..operations {
1686 let mut inlay_edits = Patch::default();
1687
1688 let mut prev_inlay_text = inlay_snapshot.text();
1689 let mut buffer_edits = Vec::new();
1690 match rng.random_range(0..=100) {
1691 0..=50 => {
1692 let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
1693 log::info!("mutated text: {:?}", snapshot.text());
1694 inlay_edits = Patch::new(edits);
1695 }
1696 _ => buffer.update(cx, |buffer, cx| {
1697 let subscription = buffer.subscribe();
1698 let edit_count = rng.random_range(1..=5);
1699 buffer.randomly_mutate(&mut rng, edit_count, cx);
1700 buffer_snapshot = buffer.snapshot(cx);
1701 let edits = subscription.consume().into_inner();
1702 log::info!("editing {:?}", edits);
1703 buffer_edits.extend(edits);
1704 }),
1705 };
1706
1707 let (new_inlay_snapshot, new_inlay_edits) =
1708 inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
1709 inlay_snapshot = new_inlay_snapshot;
1710 inlay_edits = inlay_edits.compose(new_inlay_edits);
1711
1712 log::info!("buffer text: {:?}", buffer_snapshot.text());
1713 log::info!("inlay text: {:?}", inlay_snapshot.text());
1714
1715 let inlays = inlay_map
1716 .inlays
1717 .iter()
1718 .filter(|inlay| inlay.position.is_valid(&buffer_snapshot))
1719 .map(|inlay| {
1720 let offset = inlay.position.to_offset(&buffer_snapshot);
1721 (offset, inlay.clone())
1722 })
1723 .collect::<Vec<_>>();
1724 let mut expected_text = Rope::from(&buffer_snapshot.text());
1725 for (offset, inlay) in inlays.iter().rev() {
1726 expected_text.replace(*offset..*offset, &inlay.text.to_string());
1727 }
1728 assert_eq!(inlay_snapshot.text(), expected_text.to_string());
1729
1730 let expected_buffer_rows = inlay_snapshot.row_infos(0).collect::<Vec<_>>();
1731 assert_eq!(
1732 expected_buffer_rows.len() as u32,
1733 expected_text.max_point().row + 1
1734 );
1735 for row_start in 0..expected_buffer_rows.len() {
1736 assert_eq!(
1737 inlay_snapshot
1738 .row_infos(row_start as u32)
1739 .collect::<Vec<_>>(),
1740 &expected_buffer_rows[row_start..],
1741 "incorrect buffer rows starting at {}",
1742 row_start
1743 );
1744 }
1745
1746 let mut text_highlights = TextHighlights::default();
1747 let text_highlight_count = rng.random_range(0_usize..10);
1748 let mut text_highlight_ranges = (0..text_highlight_count)
1749 .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
1750 .collect::<Vec<_>>();
1751 text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
1752 log::info!("highlighting text ranges {text_highlight_ranges:?}");
1753 text_highlights.insert(
1754 HighlightKey::Type(TypeId::of::<()>()),
1755 Arc::new((
1756 HighlightStyle::default(),
1757 text_highlight_ranges
1758 .into_iter()
1759 .map(|range| {
1760 buffer_snapshot.anchor_before(range.start)
1761 ..buffer_snapshot.anchor_after(range.end)
1762 })
1763 .collect(),
1764 )),
1765 );
1766
1767 let mut inlay_highlights = InlayHighlights::default();
1768 if !inlays.is_empty() {
1769 let inlay_highlight_count = rng.random_range(0..inlays.len());
1770 let mut inlay_indices = BTreeSet::default();
1771 while inlay_indices.len() < inlay_highlight_count {
1772 inlay_indices.insert(rng.random_range(0..inlays.len()));
1773 }
1774 let new_highlights = TreeMap::from_ordered_entries(
1775 inlay_indices
1776 .into_iter()
1777 .filter_map(|i| {
1778 let (_, inlay) = &inlays[i];
1779 let inlay_text_len = inlay.text.len();
1780 match inlay_text_len {
1781 0 => None,
1782 1 => Some(InlayHighlight {
1783 inlay: inlay.id,
1784 inlay_position: inlay.position,
1785 range: 0..1,
1786 }),
1787 n => {
1788 let inlay_text = inlay.text.to_string();
1789 let mut highlight_end = rng.random_range(1..n);
1790 let mut highlight_start = rng.random_range(0..highlight_end);
1791 while !inlay_text.is_char_boundary(highlight_end) {
1792 highlight_end += 1;
1793 }
1794 while !inlay_text.is_char_boundary(highlight_start) {
1795 highlight_start -= 1;
1796 }
1797 Some(InlayHighlight {
1798 inlay: inlay.id,
1799 inlay_position: inlay.position,
1800 range: highlight_start..highlight_end,
1801 })
1802 }
1803 }
1804 })
1805 .map(|highlight| (highlight.inlay, (HighlightStyle::default(), highlight))),
1806 );
1807 log::info!("highlighting inlay ranges {new_highlights:?}");
1808 inlay_highlights.insert(TypeId::of::<()>(), new_highlights);
1809 }
1810
1811 for _ in 0..5 {
1812 let mut end = rng.random_range(0..=inlay_snapshot.len().0);
1813 end = expected_text.clip_offset(end, Bias::Right);
1814 let mut start = rng.random_range(0..=end);
1815 start = expected_text.clip_offset(start, Bias::Right);
1816
1817 let range = InlayOffset(start)..InlayOffset(end);
1818 log::info!("calling inlay_snapshot.chunks({range:?})");
1819 let actual_text = inlay_snapshot
1820 .chunks(
1821 range,
1822 false,
1823 Highlights {
1824 text_highlights: Some(&text_highlights),
1825 inlay_highlights: Some(&inlay_highlights),
1826 ..Highlights::default()
1827 },
1828 )
1829 .map(|chunk| chunk.chunk.text)
1830 .collect::<String>();
1831 assert_eq!(
1832 actual_text,
1833 expected_text.slice(start..end).to_string(),
1834 "incorrect text in range {:?}",
1835 start..end
1836 );
1837
1838 assert_eq!(
1839 inlay_snapshot.text_summary_for_range(InlayOffset(start)..InlayOffset(end)),
1840 expected_text.slice(start..end).summary()
1841 );
1842 }
1843
1844 for edit in inlay_edits {
1845 prev_inlay_text.replace_range(
1846 edit.new.start.0..edit.new.start.0 + edit.old_len().0,
1847 &inlay_snapshot.text()[edit.new.start.0..edit.new.end.0],
1848 );
1849 }
1850 assert_eq!(prev_inlay_text, inlay_snapshot.text());
1851
1852 assert_eq!(expected_text.max_point(), inlay_snapshot.max_point().0);
1853 assert_eq!(expected_text.len(), inlay_snapshot.len().0);
1854
1855 let mut buffer_point = Point::default();
1856 let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
1857 let mut buffer_chars = buffer_snapshot.chars_at(0);
1858 loop {
1859 // Ensure conversion from buffer coordinates to inlay coordinates
1860 // is consistent.
1861 let buffer_offset = buffer_snapshot.point_to_offset(buffer_point);
1862 assert_eq!(
1863 inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)),
1864 inlay_point
1865 );
1866
1867 // No matter which bias we clip an inlay point with, it doesn't move
1868 // because it was constructed from a buffer point.
1869 assert_eq!(
1870 inlay_snapshot.clip_point(inlay_point, Bias::Left),
1871 inlay_point,
1872 "invalid inlay point for buffer point {:?} when clipped left",
1873 buffer_point
1874 );
1875 assert_eq!(
1876 inlay_snapshot.clip_point(inlay_point, Bias::Right),
1877 inlay_point,
1878 "invalid inlay point for buffer point {:?} when clipped right",
1879 buffer_point
1880 );
1881
1882 if let Some(ch) = buffer_chars.next() {
1883 if ch == '\n' {
1884 buffer_point += Point::new(1, 0);
1885 } else {
1886 buffer_point += Point::new(0, ch.len_utf8() as u32);
1887 }
1888
1889 // Ensure that moving forward in the buffer always moves the inlay point forward as well.
1890 let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
1891 assert!(new_inlay_point > inlay_point);
1892 inlay_point = new_inlay_point;
1893 } else {
1894 break;
1895 }
1896 }
1897
1898 let mut inlay_point = InlayPoint::default();
1899 let mut inlay_offset = InlayOffset::default();
1900 for ch in expected_text.chars() {
1901 assert_eq!(
1902 inlay_snapshot.to_offset(inlay_point),
1903 inlay_offset,
1904 "invalid to_offset({:?})",
1905 inlay_point
1906 );
1907 assert_eq!(
1908 inlay_snapshot.to_point(inlay_offset),
1909 inlay_point,
1910 "invalid to_point({:?})",
1911 inlay_offset
1912 );
1913
1914 let mut bytes = [0; 4];
1915 for byte in ch.encode_utf8(&mut bytes).as_bytes() {
1916 inlay_offset.0 += 1;
1917 if *byte == b'\n' {
1918 inlay_point.0 += Point::new(1, 0);
1919 } else {
1920 inlay_point.0 += Point::new(0, 1);
1921 }
1922
1923 let clipped_left_point = inlay_snapshot.clip_point(inlay_point, Bias::Left);
1924 let clipped_right_point = inlay_snapshot.clip_point(inlay_point, Bias::Right);
1925 assert!(
1926 clipped_left_point <= clipped_right_point,
1927 "inlay point {:?} when clipped left is greater than when clipped right ({:?} > {:?})",
1928 inlay_point,
1929 clipped_left_point,
1930 clipped_right_point
1931 );
1932
1933 // Ensure the clipped points are at valid text locations.
1934 assert_eq!(
1935 clipped_left_point.0,
1936 expected_text.clip_point(clipped_left_point.0, Bias::Left)
1937 );
1938 assert_eq!(
1939 clipped_right_point.0,
1940 expected_text.clip_point(clipped_right_point.0, Bias::Right)
1941 );
1942
1943 // Ensure the clipped points never overshoot the end of the map.
1944 assert!(clipped_left_point <= inlay_snapshot.max_point());
1945 assert!(clipped_right_point <= inlay_snapshot.max_point());
1946
1947 // Ensure the clipped points are at valid buffer locations.
1948 assert_eq!(
1949 inlay_snapshot
1950 .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)),
1951 clipped_left_point,
1952 "to_buffer_point({:?}) = {:?}",
1953 clipped_left_point,
1954 inlay_snapshot.to_buffer_point(clipped_left_point),
1955 );
1956 assert_eq!(
1957 inlay_snapshot
1958 .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)),
1959 clipped_right_point,
1960 "to_buffer_point({:?}) = {:?}",
1961 clipped_right_point,
1962 inlay_snapshot.to_buffer_point(clipped_right_point),
1963 );
1964 }
1965 }
1966 }
1967 }
1968
1969 fn init_test(cx: &mut App) {
1970 let store = SettingsStore::test(cx);
1971 cx.set_global(store);
1972 theme::init(theme::LoadThemes::JustBase, cx);
1973 }
1974
1975 /// Helper to create test highlights for an inlay
1976 fn create_inlay_highlights(
1977 inlay_id: InlayId,
1978 highlight_range: Range<usize>,
1979 position: Anchor,
1980 ) -> TreeMap<TypeId, TreeMap<InlayId, (HighlightStyle, InlayHighlight)>> {
1981 let mut inlay_highlights = TreeMap::default();
1982 let mut type_highlights = TreeMap::default();
1983 type_highlights.insert(
1984 inlay_id,
1985 (
1986 HighlightStyle::default(),
1987 InlayHighlight {
1988 inlay: inlay_id,
1989 range: highlight_range,
1990 inlay_position: position,
1991 },
1992 ),
1993 );
1994 inlay_highlights.insert(TypeId::of::<()>(), type_highlights);
1995 inlay_highlights
1996 }
1997
1998 #[gpui::test]
1999 fn test_inlay_utf8_boundary_panic_fix(cx: &mut App) {
2000 init_test(cx);
2001
2002 // This test verifies that we handle UTF-8 character boundaries correctly
2003 // when splitting inlay text for highlighting. Previously, this would panic
2004 // when trying to split at byte 13, which is in the middle of the '…' character.
2005 //
2006 // See https://github.com/zed-industries/zed/issues/33641
2007 let buffer = MultiBuffer::build_simple("fn main() {}\n", cx);
2008 let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx));
2009
2010 // Create an inlay with text that contains a multi-byte character
2011 // The string "SortingDirec…" contains an ellipsis character '…' which is 3 bytes (E2 80 A6)
2012 let inlay_text = "SortingDirec…";
2013 let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 5));
2014
2015 let inlay = Inlay {
2016 id: InlayId::Hint(0),
2017 position,
2018 text: text::Rope::from(inlay_text),
2019 color: None,
2020 };
2021
2022 let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]);
2023
2024 // Create highlights that request a split at byte 13, which is in the middle
2025 // of the '…' character (bytes 12..15). We include the full character.
2026 let inlay_highlights = create_inlay_highlights(InlayId::Hint(0), 0..13, position);
2027
2028 let highlights = crate::display_map::Highlights {
2029 text_highlights: None,
2030 inlay_highlights: Some(&inlay_highlights),
2031 styles: crate::display_map::HighlightStyles::default(),
2032 };
2033
2034 // Collect chunks - this previously would panic
2035 let chunks: Vec<_> = inlay_snapshot
2036 .chunks(
2037 InlayOffset(0)..InlayOffset(inlay_snapshot.len().0),
2038 false,
2039 highlights,
2040 )
2041 .collect();
2042
2043 // Verify the chunks are correct
2044 let full_text: String = chunks.iter().map(|c| c.chunk.text).collect();
2045 assert_eq!(full_text, "fn maSortingDirec…in() {}\n");
2046
2047 // Verify the highlighted portion includes the complete ellipsis character
2048 let highlighted_chunks: Vec<_> = chunks
2049 .iter()
2050 .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay)
2051 .collect();
2052
2053 assert_eq!(highlighted_chunks.len(), 1);
2054 assert_eq!(highlighted_chunks[0].chunk.text, "SortingDirec…");
2055 }
2056
2057 #[gpui::test]
2058 fn test_inlay_utf8_boundaries(cx: &mut App) {
2059 init_test(cx);
2060
2061 struct TestCase {
2062 inlay_text: &'static str,
2063 highlight_range: Range<usize>,
2064 expected_highlighted: &'static str,
2065 description: &'static str,
2066 }
2067
2068 let test_cases = vec![
2069 TestCase {
2070 inlay_text: "Hello👋World",
2071 highlight_range: 0..7,
2072 expected_highlighted: "Hello👋",
2073 description: "Emoji boundary - rounds up to include full emoji",
2074 },
2075 TestCase {
2076 inlay_text: "Test→End",
2077 highlight_range: 0..5,
2078 expected_highlighted: "Test→",
2079 description: "Arrow boundary - rounds up to include full arrow",
2080 },
2081 TestCase {
2082 inlay_text: "café",
2083 highlight_range: 0..4,
2084 expected_highlighted: "café",
2085 description: "Accented char boundary - rounds up to include full é",
2086 },
2087 TestCase {
2088 inlay_text: "🎨🎭🎪",
2089 highlight_range: 0..5,
2090 expected_highlighted: "🎨🎭",
2091 description: "Multiple emojis - partial highlight",
2092 },
2093 TestCase {
2094 inlay_text: "普通话",
2095 highlight_range: 0..4,
2096 expected_highlighted: "普通",
2097 description: "Chinese characters - partial highlight",
2098 },
2099 TestCase {
2100 inlay_text: "Hello",
2101 highlight_range: 0..2,
2102 expected_highlighted: "He",
2103 description: "ASCII only - no adjustment needed",
2104 },
2105 TestCase {
2106 inlay_text: "👋",
2107 highlight_range: 0..1,
2108 expected_highlighted: "👋",
2109 description: "Single emoji - partial byte range includes whole char",
2110 },
2111 TestCase {
2112 inlay_text: "Test",
2113 highlight_range: 0..0,
2114 expected_highlighted: "",
2115 description: "Empty range",
2116 },
2117 TestCase {
2118 inlay_text: "🎨ABC",
2119 highlight_range: 2..5,
2120 expected_highlighted: "A",
2121 description: "Range starting mid-emoji skips the emoji",
2122 },
2123 ];
2124
2125 for test_case in test_cases {
2126 let buffer = MultiBuffer::build_simple("test", cx);
2127 let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx));
2128 let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 2));
2129
2130 let inlay = Inlay {
2131 id: InlayId::Hint(0),
2132 position,
2133 text: text::Rope::from(test_case.inlay_text),
2134 color: None,
2135 };
2136
2137 let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]);
2138 let inlay_highlights = create_inlay_highlights(
2139 InlayId::Hint(0),
2140 test_case.highlight_range.clone(),
2141 position,
2142 );
2143
2144 let highlights = crate::display_map::Highlights {
2145 text_highlights: None,
2146 inlay_highlights: Some(&inlay_highlights),
2147 styles: crate::display_map::HighlightStyles::default(),
2148 };
2149
2150 let chunks: Vec<_> = inlay_snapshot
2151 .chunks(
2152 InlayOffset(0)..InlayOffset(inlay_snapshot.len().0),
2153 false,
2154 highlights,
2155 )
2156 .collect();
2157
2158 // Verify we got chunks and they total to the expected text
2159 let full_text: String = chunks.iter().map(|c| c.chunk.text).collect();
2160 assert_eq!(
2161 full_text,
2162 format!("te{}st", test_case.inlay_text),
2163 "Full text mismatch for case: {}",
2164 test_case.description
2165 );
2166
2167 // Verify that the highlighted portion matches expectations
2168 let highlighted_text: String = chunks
2169 .iter()
2170 .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay)
2171 .map(|c| c.chunk.text)
2172 .collect();
2173 assert_eq!(
2174 highlighted_text, test_case.expected_highlighted,
2175 "Highlighted text mismatch for case: {} (text: '{}', range: {:?})",
2176 test_case.description, test_case.inlay_text, test_case.highlight_range
2177 );
2178 }
2179 }
2180}