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