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