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