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