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