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