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 {
252 chunk.text.ceil_char_boundary(desired_bytes)
253 };
254
255 let (prefix, suffix) = chunk.text.split_at(split_index);
256 self.output_offset.0 += prefix.len();
257
258 let mask = 1u128.unbounded_shl(split_index as u32).wrapping_sub(1);
259 let chars = chunk.chars & mask;
260 let tabs = chunk.tabs & mask;
261
262 chunk.chars = chunk.chars.unbounded_shr(split_index as u32);
263 chunk.tabs = chunk.tabs.unbounded_shr(split_index as u32);
264 chunk.text = suffix;
265
266 InlayChunk {
267 chunk: Chunk {
268 text: prefix,
269 chars,
270 tabs,
271 ..chunk.clone()
272 },
273 renderer: None,
274 }
275 }
276 Transform::Inlay(inlay) => {
277 let mut inlay_style_and_highlight = None;
278 if let Some(inlay_highlights) = self.highlights.inlay_highlights {
279 for (_, inlay_id_to_data) in inlay_highlights.iter() {
280 let style_and_highlight = inlay_id_to_data.get(&inlay.id);
281 if style_and_highlight.is_some() {
282 inlay_style_and_highlight = style_and_highlight;
283 break;
284 }
285 }
286 }
287
288 let mut renderer = None;
289 let mut highlight_style = match inlay.id {
290 InlayId::EditPrediction(_) => self.highlight_styles.edit_prediction.map(|s| {
291 if inlay.text().chars().all(|c| c.is_whitespace()) {
292 s.whitespace
293 } else {
294 s.insertion
295 }
296 }),
297 InlayId::Hint(_) => self.highlight_styles.inlay_hint,
298 InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
299 InlayId::Color(_) => {
300 if let InlayContent::Color(color) = inlay.content {
301 renderer = Some(ChunkRenderer {
302 id: ChunkRendererId::Inlay(inlay.id),
303 render: Arc::new(move |cx| {
304 div()
305 .relative()
306 .size_3p5()
307 .child(
308 div()
309 .absolute()
310 .right_1()
311 .size_3()
312 .border_1()
313 .border_color(
314 if cx.theme().appearance().is_light() {
315 gpui::black().opacity(0.5)
316 } else {
317 gpui::white().opacity(0.5)
318 },
319 )
320 .bg(color),
321 )
322 .into_any_element()
323 }),
324 constrain_width: false,
325 measured_width: None,
326 });
327 }
328 self.highlight_styles.inlay_hint
329 }
330 };
331 let next_inlay_highlight_endpoint;
332 let offset_in_inlay = self.output_offset - self.transforms.start().0;
333 if let Some((style, highlight)) = inlay_style_and_highlight {
334 let range = &highlight.range;
335 if offset_in_inlay.0 < range.start {
336 next_inlay_highlight_endpoint = range.start - offset_in_inlay.0;
337 } else if offset_in_inlay.0 >= range.end {
338 next_inlay_highlight_endpoint = usize::MAX;
339 } else {
340 next_inlay_highlight_endpoint = range.end - offset_in_inlay.0;
341 highlight_style = highlight_style
342 .map(|highlight| highlight.highlight(*style))
343 .or_else(|| Some(*style));
344 }
345 } else {
346 next_inlay_highlight_endpoint = usize::MAX;
347 }
348
349 let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
350 let start = offset_in_inlay;
351 let end = cmp::min(self.max_output_offset, self.transforms.end().0)
352 - self.transforms.start().0;
353 let chunks = inlay.text().chunks_in_range(start.0..end.0);
354 text::ChunkWithBitmaps(chunks)
355 });
356 let ChunkBitmaps {
357 text: inlay_chunk,
358 chars,
359 tabs,
360 } = self
361 .inlay_chunk
362 .get_or_insert_with(|| inlay_chunks.next().unwrap());
363
364 // Determine split index handling edge cases
365 let split_index = if next_inlay_highlight_endpoint >= inlay_chunk.len() {
366 inlay_chunk.len()
367 } else if next_inlay_highlight_endpoint == 0 {
368 // Need to take at least one character to make progress
369 inlay_chunk
370 .chars()
371 .next()
372 .map(|c| c.len_utf8())
373 .unwrap_or(1)
374 } else {
375 inlay_chunk.ceil_char_boundary(next_inlay_highlight_endpoint)
376 };
377
378 let (chunk, remainder) = inlay_chunk.split_at(split_index);
379 *inlay_chunk = remainder;
380
381 let mask = 1u128.unbounded_shl(split_index as u32).wrapping_sub(1);
382 let new_chars = *chars & mask;
383 let new_tabs = *tabs & mask;
384
385 *chars = chars.unbounded_shr(split_index as u32);
386 *tabs = tabs.unbounded_shr(split_index as u32);
387
388 if inlay_chunk.is_empty() {
389 self.inlay_chunk = None;
390 }
391
392 self.output_offset.0 += chunk.len();
393
394 InlayChunk {
395 chunk: Chunk {
396 text: chunk,
397 chars: new_chars,
398 tabs: new_tabs,
399 highlight_style,
400 is_inlay: true,
401 ..Chunk::default()
402 },
403 renderer,
404 }
405 }
406 };
407
408 if self.output_offset >= self.transforms.end().0 {
409 self.inlay_chunks = None;
410 self.transforms.next();
411 }
412
413 Some(chunk)
414 }
415}
416
417impl InlayBufferRows<'_> {
418 pub fn seek(&mut self, row: u32) {
419 let inlay_point = InlayPoint::new(row, 0);
420 self.transforms.seek(&inlay_point, Bias::Left);
421
422 let mut buffer_point = self.transforms.start().1;
423 let buffer_row = MultiBufferRow(if row == 0 {
424 0
425 } else {
426 match self.transforms.item() {
427 Some(Transform::Isomorphic(_)) => {
428 buffer_point += inlay_point.0 - self.transforms.start().0.0;
429 buffer_point.row
430 }
431 _ => cmp::min(buffer_point.row + 1, self.max_buffer_row.0),
432 }
433 });
434 self.inlay_row = inlay_point.row();
435 self.buffer_rows.seek(buffer_row);
436 }
437}
438
439impl Iterator for InlayBufferRows<'_> {
440 type Item = RowInfo;
441
442 fn next(&mut self) -> Option<Self::Item> {
443 let buffer_row = if self.inlay_row == 0 {
444 self.buffer_rows.next().unwrap()
445 } else {
446 match self.transforms.item()? {
447 Transform::Inlay(_) => Default::default(),
448 Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(),
449 }
450 };
451
452 self.inlay_row += 1;
453 self.transforms
454 .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left);
455
456 Some(buffer_row)
457 }
458}
459
460impl InlayPoint {
461 pub fn new(row: u32, column: u32) -> Self {
462 Self(Point::new(row, column))
463 }
464
465 pub fn row(self) -> u32 {
466 self.0.row
467 }
468}
469
470impl InlayMap {
471 pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) {
472 let version = 0;
473 let snapshot = InlaySnapshot {
474 buffer: buffer.clone(),
475 transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), ()),
476 version,
477 };
478
479 (
480 Self {
481 snapshot: snapshot.clone(),
482 inlays: Vec::new(),
483 },
484 snapshot,
485 )
486 }
487
488 pub fn sync(
489 &mut self,
490 buffer_snapshot: MultiBufferSnapshot,
491 mut buffer_edits: Vec<text::Edit<usize>>,
492 ) -> (InlaySnapshot, Vec<InlayEdit>) {
493 let snapshot = &mut self.snapshot;
494
495 if buffer_edits.is_empty()
496 && snapshot.buffer.trailing_excerpt_update_count()
497 != buffer_snapshot.trailing_excerpt_update_count()
498 {
499 buffer_edits.push(Edit {
500 old: snapshot.buffer.len()..snapshot.buffer.len(),
501 new: buffer_snapshot.len()..buffer_snapshot.len(),
502 });
503 }
504
505 if buffer_edits.is_empty() {
506 if snapshot.buffer.edit_count() != buffer_snapshot.edit_count()
507 || snapshot.buffer.non_text_state_update_count()
508 != buffer_snapshot.non_text_state_update_count()
509 || snapshot.buffer.trailing_excerpt_update_count()
510 != buffer_snapshot.trailing_excerpt_update_count()
511 {
512 snapshot.version += 1;
513 }
514
515 snapshot.buffer = buffer_snapshot;
516 (snapshot.clone(), Vec::new())
517 } else {
518 let mut inlay_edits = Patch::default();
519 let mut new_transforms = SumTree::default();
520 let mut cursor = snapshot
521 .transforms
522 .cursor::<Dimensions<usize, InlayOffset>>(());
523 let mut buffer_edits_iter = buffer_edits.iter().peekable();
524 while let Some(buffer_edit) = buffer_edits_iter.next() {
525 new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left), ());
526 if let Some(Transform::Isomorphic(transform)) = cursor.item()
527 && cursor.end().0 == buffer_edit.old.start
528 {
529 push_isomorphic(&mut new_transforms, *transform);
530 cursor.next();
531 }
532
533 // Remove all the inlays and transforms contained by the edit.
534 let old_start =
535 cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0);
536 cursor.seek(&buffer_edit.old.end, Bias::Right);
537 let old_end =
538 cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0);
539
540 // Push the unchanged prefix.
541 let prefix_start = new_transforms.summary().input.len;
542 let prefix_end = buffer_edit.new.start;
543 push_isomorphic(
544 &mut new_transforms,
545 buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
546 );
547 let new_start = InlayOffset(new_transforms.summary().output.len);
548
549 let start_ix = match self.inlays.binary_search_by(|probe| {
550 probe
551 .position
552 .to_offset(&buffer_snapshot)
553 .cmp(&buffer_edit.new.start)
554 .then(std::cmp::Ordering::Greater)
555 }) {
556 Ok(ix) | Err(ix) => ix,
557 };
558
559 for inlay in &self.inlays[start_ix..] {
560 if !inlay.position.is_valid(&buffer_snapshot) {
561 continue;
562 }
563 let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
564 if buffer_offset > buffer_edit.new.end {
565 break;
566 }
567
568 let prefix_start = new_transforms.summary().input.len;
569 let prefix_end = buffer_offset;
570 push_isomorphic(
571 &mut new_transforms,
572 buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
573 );
574
575 new_transforms.push(Transform::Inlay(inlay.clone()), ());
576 }
577
578 // Apply the rest of the edit.
579 let transform_start = new_transforms.summary().input.len;
580 push_isomorphic(
581 &mut new_transforms,
582 buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end),
583 );
584 let new_end = InlayOffset(new_transforms.summary().output.len);
585 inlay_edits.push(Edit {
586 old: old_start..old_end,
587 new: new_start..new_end,
588 });
589
590 // If the next edit doesn't intersect the current isomorphic transform, then
591 // we can push its remainder.
592 if buffer_edits_iter
593 .peek()
594 .is_none_or(|edit| edit.old.start >= cursor.end().0)
595 {
596 let transform_start = new_transforms.summary().input.len;
597 let transform_end =
598 buffer_edit.new.end + (cursor.end().0 - buffer_edit.old.end);
599 push_isomorphic(
600 &mut new_transforms,
601 buffer_snapshot.text_summary_for_range(transform_start..transform_end),
602 );
603 cursor.next();
604 }
605 }
606
607 new_transforms.append(cursor.suffix(), ());
608 if new_transforms.is_empty() {
609 new_transforms.push(Transform::Isomorphic(Default::default()), ());
610 }
611
612 drop(cursor);
613 snapshot.transforms = new_transforms;
614 snapshot.version += 1;
615 snapshot.buffer = buffer_snapshot;
616 snapshot.check_invariants();
617
618 (snapshot.clone(), inlay_edits.into_inner())
619 }
620 }
621
622 pub fn splice(
623 &mut self,
624 to_remove: &[InlayId],
625 to_insert: Vec<Inlay>,
626 ) -> (InlaySnapshot, Vec<InlayEdit>) {
627 let snapshot = &mut self.snapshot;
628 let mut edits = BTreeSet::new();
629
630 self.inlays.retain(|inlay| {
631 let retain = !to_remove.contains(&inlay.id);
632 if !retain {
633 let offset = inlay.position.to_offset(&snapshot.buffer);
634 edits.insert(offset);
635 }
636 retain
637 });
638
639 for inlay_to_insert in to_insert {
640 // Avoid inserting empty inlays.
641 if inlay_to_insert.text().is_empty() {
642 continue;
643 }
644
645 let offset = inlay_to_insert.position.to_offset(&snapshot.buffer);
646 match self.inlays.binary_search_by(|probe| {
647 probe
648 .position
649 .cmp(&inlay_to_insert.position, &snapshot.buffer)
650 .then(std::cmp::Ordering::Less)
651 }) {
652 Ok(ix) | Err(ix) => {
653 self.inlays.insert(ix, inlay_to_insert);
654 }
655 }
656
657 edits.insert(offset);
658 }
659
660 let buffer_edits = edits
661 .into_iter()
662 .map(|offset| Edit {
663 old: offset..offset,
664 new: offset..offset,
665 })
666 .collect();
667 let buffer_snapshot = snapshot.buffer.clone();
668 let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
669 (snapshot, edits)
670 }
671
672 pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
673 self.inlays.iter()
674 }
675
676 #[cfg(test)]
677 pub(crate) fn randomly_mutate(
678 &mut self,
679 next_inlay_id: &mut usize,
680 rng: &mut rand::rngs::StdRng,
681 ) -> (InlaySnapshot, Vec<InlayEdit>) {
682 use rand::prelude::*;
683 use util::post_inc;
684
685 let mut to_remove = Vec::new();
686 let mut to_insert = Vec::new();
687 let snapshot = &mut self.snapshot;
688 for i in 0..rng.random_range(1..=5) {
689 if self.inlays.is_empty() || rng.random() {
690 let position = snapshot.buffer.random_byte_range(0, rng).start;
691 let bias = if rng.random() {
692 Bias::Left
693 } else {
694 Bias::Right
695 };
696 let len = if rng.random_bool(0.01) {
697 0
698 } else {
699 rng.random_range(1..=5)
700 };
701 let text = util::RandomCharIter::new(&mut *rng)
702 .filter(|ch| *ch != '\r')
703 .take(len)
704 .collect::<String>();
705
706 let next_inlay = if i % 2 == 0 {
707 Inlay::mock_hint(
708 post_inc(next_inlay_id),
709 snapshot.buffer.anchor_at(position, bias),
710 &text,
711 )
712 } else {
713 Inlay::edit_prediction(
714 post_inc(next_inlay_id),
715 snapshot.buffer.anchor_at(position, bias),
716 &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#[cfg(test)]
1146mod tests {
1147 use super::*;
1148 use crate::{
1149 MultiBuffer,
1150 display_map::{HighlightKey, InlayHighlights, TextHighlights},
1151 hover_links::InlayHighlight,
1152 };
1153 use gpui::{App, HighlightStyle};
1154 use multi_buffer::Anchor;
1155 use project::{InlayHint, InlayHintLabel, ResolveState};
1156 use rand::prelude::*;
1157 use settings::SettingsStore;
1158 use std::{any::TypeId, cmp::Reverse, env, sync::Arc};
1159 use sum_tree::TreeMap;
1160 use text::{Patch, Rope};
1161 use util::RandomCharIter;
1162 use util::post_inc;
1163
1164 #[test]
1165 fn test_inlay_properties_label_padding() {
1166 assert_eq!(
1167 Inlay::hint(
1168 InlayId::Hint(0),
1169 Anchor::min(),
1170 &InlayHint {
1171 label: InlayHintLabel::String("a".to_string()),
1172 position: text::Anchor::MIN,
1173 padding_left: false,
1174 padding_right: false,
1175 tooltip: None,
1176 kind: None,
1177 resolve_state: ResolveState::Resolved,
1178 },
1179 )
1180 .text()
1181 .to_string(),
1182 "a",
1183 "Should not pad label if not requested"
1184 );
1185
1186 assert_eq!(
1187 Inlay::hint(
1188 InlayId::Hint(0),
1189 Anchor::min(),
1190 &InlayHint {
1191 label: InlayHintLabel::String("a".to_string()),
1192 position: text::Anchor::MIN,
1193 padding_left: true,
1194 padding_right: true,
1195 tooltip: None,
1196 kind: None,
1197 resolve_state: ResolveState::Resolved,
1198 },
1199 )
1200 .text()
1201 .to_string(),
1202 " a ",
1203 "Should pad label for every side requested"
1204 );
1205
1206 assert_eq!(
1207 Inlay::hint(
1208 InlayId::Hint(0),
1209 Anchor::min(),
1210 &InlayHint {
1211 label: InlayHintLabel::String(" a ".to_string()),
1212 position: text::Anchor::MIN,
1213 padding_left: false,
1214 padding_right: false,
1215 tooltip: None,
1216 kind: None,
1217 resolve_state: ResolveState::Resolved,
1218 },
1219 )
1220 .text()
1221 .to_string(),
1222 " a ",
1223 "Should not change already padded label"
1224 );
1225
1226 assert_eq!(
1227 Inlay::hint(
1228 InlayId::Hint(0),
1229 Anchor::min(),
1230 &InlayHint {
1231 label: InlayHintLabel::String(" a ".to_string()),
1232 position: text::Anchor::MIN,
1233 padding_left: true,
1234 padding_right: true,
1235 tooltip: None,
1236 kind: None,
1237 resolve_state: ResolveState::Resolved,
1238 },
1239 )
1240 .text()
1241 .to_string(),
1242 " a ",
1243 "Should not change already padded label"
1244 );
1245 }
1246
1247 #[gpui::test]
1248 fn test_inlay_hint_padding_with_multibyte_chars() {
1249 assert_eq!(
1250 Inlay::hint(
1251 InlayId::Hint(0),
1252 Anchor::min(),
1253 &InlayHint {
1254 label: InlayHintLabel::String("🎨".to_string()),
1255 position: text::Anchor::MIN,
1256 padding_left: true,
1257 padding_right: true,
1258 tooltip: None,
1259 kind: None,
1260 resolve_state: ResolveState::Resolved,
1261 },
1262 )
1263 .text()
1264 .to_string(),
1265 " 🎨 ",
1266 "Should pad single emoji correctly"
1267 );
1268 }
1269
1270 #[gpui::test]
1271 fn test_basic_inlays(cx: &mut App) {
1272 let buffer = MultiBuffer::build_simple("abcdefghi", cx);
1273 let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
1274 let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
1275 assert_eq!(inlay_snapshot.text(), "abcdefghi");
1276 let mut next_inlay_id = 0;
1277
1278 let (inlay_snapshot, _) = inlay_map.splice(
1279 &[],
1280 vec![Inlay::mock_hint(
1281 post_inc(&mut next_inlay_id),
1282 buffer.read(cx).snapshot(cx).anchor_after(3),
1283 "|123|",
1284 )],
1285 );
1286 assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
1287 assert_eq!(
1288 inlay_snapshot.to_inlay_point(Point::new(0, 0)),
1289 InlayPoint::new(0, 0)
1290 );
1291 assert_eq!(
1292 inlay_snapshot.to_inlay_point(Point::new(0, 1)),
1293 InlayPoint::new(0, 1)
1294 );
1295 assert_eq!(
1296 inlay_snapshot.to_inlay_point(Point::new(0, 2)),
1297 InlayPoint::new(0, 2)
1298 );
1299 assert_eq!(
1300 inlay_snapshot.to_inlay_point(Point::new(0, 3)),
1301 InlayPoint::new(0, 3)
1302 );
1303 assert_eq!(
1304 inlay_snapshot.to_inlay_point(Point::new(0, 4)),
1305 InlayPoint::new(0, 9)
1306 );
1307 assert_eq!(
1308 inlay_snapshot.to_inlay_point(Point::new(0, 5)),
1309 InlayPoint::new(0, 10)
1310 );
1311 assert_eq!(
1312 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
1313 InlayPoint::new(0, 0)
1314 );
1315 assert_eq!(
1316 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
1317 InlayPoint::new(0, 0)
1318 );
1319 assert_eq!(
1320 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
1321 InlayPoint::new(0, 3)
1322 );
1323 assert_eq!(
1324 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
1325 InlayPoint::new(0, 3)
1326 );
1327 assert_eq!(
1328 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
1329 InlayPoint::new(0, 3)
1330 );
1331 assert_eq!(
1332 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
1333 InlayPoint::new(0, 9)
1334 );
1335
1336 // Edits before or after the inlay should not affect it.
1337 buffer.update(cx, |buffer, cx| {
1338 buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx)
1339 });
1340 let (inlay_snapshot, _) = inlay_map.sync(
1341 buffer.read(cx).snapshot(cx),
1342 buffer_edits.consume().into_inner(),
1343 );
1344 assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi");
1345
1346 // An edit surrounding the inlay should invalidate it.
1347 buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx));
1348 let (inlay_snapshot, _) = inlay_map.sync(
1349 buffer.read(cx).snapshot(cx),
1350 buffer_edits.consume().into_inner(),
1351 );
1352 assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
1353
1354 let (inlay_snapshot, _) = inlay_map.splice(
1355 &[],
1356 vec![
1357 Inlay::mock_hint(
1358 post_inc(&mut next_inlay_id),
1359 buffer.read(cx).snapshot(cx).anchor_before(3),
1360 "|123|",
1361 ),
1362 Inlay::edit_prediction(
1363 post_inc(&mut next_inlay_id),
1364 buffer.read(cx).snapshot(cx).anchor_after(3),
1365 "|456|",
1366 ),
1367 ],
1368 );
1369 assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
1370
1371 // Edits ending where the inlay starts should not move it if it has a left bias.
1372 buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], 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(), "abx|123|JKL|456|yDzefghi");
1378
1379 assert_eq!(
1380 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
1381 InlayPoint::new(0, 0)
1382 );
1383 assert_eq!(
1384 inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
1385 InlayPoint::new(0, 0)
1386 );
1387
1388 assert_eq!(
1389 inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Left),
1390 InlayPoint::new(0, 1)
1391 );
1392 assert_eq!(
1393 inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Right),
1394 InlayPoint::new(0, 1)
1395 );
1396
1397 assert_eq!(
1398 inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Left),
1399 InlayPoint::new(0, 2)
1400 );
1401 assert_eq!(
1402 inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Right),
1403 InlayPoint::new(0, 2)
1404 );
1405
1406 assert_eq!(
1407 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
1408 InlayPoint::new(0, 2)
1409 );
1410 assert_eq!(
1411 inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
1412 InlayPoint::new(0, 8)
1413 );
1414
1415 assert_eq!(
1416 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
1417 InlayPoint::new(0, 2)
1418 );
1419 assert_eq!(
1420 inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
1421 InlayPoint::new(0, 8)
1422 );
1423
1424 assert_eq!(
1425 inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Left),
1426 InlayPoint::new(0, 2)
1427 );
1428 assert_eq!(
1429 inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Right),
1430 InlayPoint::new(0, 8)
1431 );
1432
1433 assert_eq!(
1434 inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Left),
1435 InlayPoint::new(0, 2)
1436 );
1437 assert_eq!(
1438 inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Right),
1439 InlayPoint::new(0, 8)
1440 );
1441
1442 assert_eq!(
1443 inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Left),
1444 InlayPoint::new(0, 2)
1445 );
1446 assert_eq!(
1447 inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Right),
1448 InlayPoint::new(0, 8)
1449 );
1450
1451 assert_eq!(
1452 inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Left),
1453 InlayPoint::new(0, 8)
1454 );
1455 assert_eq!(
1456 inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Right),
1457 InlayPoint::new(0, 8)
1458 );
1459
1460 assert_eq!(
1461 inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Left),
1462 InlayPoint::new(0, 9)
1463 );
1464 assert_eq!(
1465 inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Right),
1466 InlayPoint::new(0, 9)
1467 );
1468
1469 assert_eq!(
1470 inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Left),
1471 InlayPoint::new(0, 10)
1472 );
1473 assert_eq!(
1474 inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Right),
1475 InlayPoint::new(0, 10)
1476 );
1477
1478 assert_eq!(
1479 inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Left),
1480 InlayPoint::new(0, 11)
1481 );
1482 assert_eq!(
1483 inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Right),
1484 InlayPoint::new(0, 11)
1485 );
1486
1487 assert_eq!(
1488 inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Left),
1489 InlayPoint::new(0, 11)
1490 );
1491 assert_eq!(
1492 inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Right),
1493 InlayPoint::new(0, 17)
1494 );
1495
1496 assert_eq!(
1497 inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Left),
1498 InlayPoint::new(0, 11)
1499 );
1500 assert_eq!(
1501 inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Right),
1502 InlayPoint::new(0, 17)
1503 );
1504
1505 assert_eq!(
1506 inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Left),
1507 InlayPoint::new(0, 11)
1508 );
1509 assert_eq!(
1510 inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Right),
1511 InlayPoint::new(0, 17)
1512 );
1513
1514 assert_eq!(
1515 inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Left),
1516 InlayPoint::new(0, 11)
1517 );
1518 assert_eq!(
1519 inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Right),
1520 InlayPoint::new(0, 17)
1521 );
1522
1523 assert_eq!(
1524 inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Left),
1525 InlayPoint::new(0, 11)
1526 );
1527 assert_eq!(
1528 inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Right),
1529 InlayPoint::new(0, 17)
1530 );
1531
1532 assert_eq!(
1533 inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Left),
1534 InlayPoint::new(0, 17)
1535 );
1536 assert_eq!(
1537 inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Right),
1538 InlayPoint::new(0, 17)
1539 );
1540
1541 assert_eq!(
1542 inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Left),
1543 InlayPoint::new(0, 18)
1544 );
1545 assert_eq!(
1546 inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Right),
1547 InlayPoint::new(0, 18)
1548 );
1549
1550 // The inlays can be manually removed.
1551 let (inlay_snapshot, _) = inlay_map.splice(
1552 &inlay_map
1553 .inlays
1554 .iter()
1555 .map(|inlay| inlay.id)
1556 .collect::<Vec<InlayId>>(),
1557 Vec::new(),
1558 );
1559 assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
1560 }
1561
1562 #[gpui::test]
1563 fn test_inlay_buffer_rows(cx: &mut App) {
1564 let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx);
1565 let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
1566 assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi");
1567 let mut next_inlay_id = 0;
1568
1569 let (inlay_snapshot, _) = inlay_map.splice(
1570 &[],
1571 vec![
1572 Inlay::mock_hint(
1573 post_inc(&mut next_inlay_id),
1574 buffer.read(cx).snapshot(cx).anchor_before(0),
1575 "|123|\n",
1576 ),
1577 Inlay::mock_hint(
1578 post_inc(&mut next_inlay_id),
1579 buffer.read(cx).snapshot(cx).anchor_before(4),
1580 "|456|",
1581 ),
1582 Inlay::edit_prediction(
1583 post_inc(&mut next_inlay_id),
1584 buffer.read(cx).snapshot(cx).anchor_before(7),
1585 "\n|567|\n",
1586 ),
1587 ],
1588 );
1589 assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
1590 assert_eq!(
1591 inlay_snapshot
1592 .row_infos(0)
1593 .map(|info| info.buffer_row)
1594 .collect::<Vec<_>>(),
1595 vec![Some(0), None, Some(1), None, None, Some(2)]
1596 );
1597 }
1598
1599 #[gpui::test(iterations = 100)]
1600 fn test_random_inlays(cx: &mut App, mut rng: StdRng) {
1601 init_test(cx);
1602
1603 let operations = env::var("OPERATIONS")
1604 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1605 .unwrap_or(10);
1606
1607 let len = rng.random_range(0..30);
1608 let buffer = if rng.random() {
1609 let text = util::RandomCharIter::new(&mut rng)
1610 .take(len)
1611 .collect::<String>();
1612 MultiBuffer::build_simple(&text, cx)
1613 } else {
1614 MultiBuffer::build_random(&mut rng, cx)
1615 };
1616 let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
1617 let mut next_inlay_id = 0;
1618 log::info!("buffer text: {:?}", buffer_snapshot.text());
1619 let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
1620 for _ in 0..operations {
1621 let mut inlay_edits = Patch::default();
1622
1623 let mut prev_inlay_text = inlay_snapshot.text();
1624 let mut buffer_edits = Vec::new();
1625 match rng.random_range(0..=100) {
1626 0..=50 => {
1627 let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
1628 log::info!("mutated text: {:?}", snapshot.text());
1629 inlay_edits = Patch::new(edits);
1630 }
1631 _ => buffer.update(cx, |buffer, cx| {
1632 let subscription = buffer.subscribe();
1633 let edit_count = rng.random_range(1..=5);
1634 buffer.randomly_mutate(&mut rng, edit_count, cx);
1635 buffer_snapshot = buffer.snapshot(cx);
1636 let edits = subscription.consume().into_inner();
1637 log::info!("editing {:?}", edits);
1638 buffer_edits.extend(edits);
1639 }),
1640 };
1641
1642 let (new_inlay_snapshot, new_inlay_edits) =
1643 inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
1644 inlay_snapshot = new_inlay_snapshot;
1645 inlay_edits = inlay_edits.compose(new_inlay_edits);
1646
1647 log::info!("buffer text: {:?}", buffer_snapshot.text());
1648 log::info!("inlay text: {:?}", inlay_snapshot.text());
1649
1650 let inlays = inlay_map
1651 .inlays
1652 .iter()
1653 .filter(|inlay| inlay.position.is_valid(&buffer_snapshot))
1654 .map(|inlay| {
1655 let offset = inlay.position.to_offset(&buffer_snapshot);
1656 (offset, inlay.clone())
1657 })
1658 .collect::<Vec<_>>();
1659 let mut expected_text = Rope::from(&buffer_snapshot.text());
1660 for (offset, inlay) in inlays.iter().rev() {
1661 expected_text.replace(*offset..*offset, &inlay.text().to_string());
1662 }
1663 assert_eq!(inlay_snapshot.text(), expected_text.to_string());
1664
1665 let expected_buffer_rows = inlay_snapshot.row_infos(0).collect::<Vec<_>>();
1666 assert_eq!(
1667 expected_buffer_rows.len() as u32,
1668 expected_text.max_point().row + 1
1669 );
1670 for row_start in 0..expected_buffer_rows.len() {
1671 assert_eq!(
1672 inlay_snapshot
1673 .row_infos(row_start as u32)
1674 .collect::<Vec<_>>(),
1675 &expected_buffer_rows[row_start..],
1676 "incorrect buffer rows starting at {}",
1677 row_start
1678 );
1679 }
1680
1681 let mut text_highlights = TextHighlights::default();
1682 let text_highlight_count = rng.random_range(0_usize..10);
1683 let mut text_highlight_ranges = (0..text_highlight_count)
1684 .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
1685 .collect::<Vec<_>>();
1686 text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
1687 log::info!("highlighting text ranges {text_highlight_ranges:?}");
1688 text_highlights.insert(
1689 HighlightKey::Type(TypeId::of::<()>()),
1690 Arc::new((
1691 HighlightStyle::default(),
1692 text_highlight_ranges
1693 .into_iter()
1694 .map(|range| {
1695 buffer_snapshot.anchor_before(range.start)
1696 ..buffer_snapshot.anchor_after(range.end)
1697 })
1698 .collect(),
1699 )),
1700 );
1701
1702 let mut inlay_highlights = InlayHighlights::default();
1703 if !inlays.is_empty() {
1704 let inlay_highlight_count = rng.random_range(0..inlays.len());
1705 let mut inlay_indices = BTreeSet::default();
1706 while inlay_indices.len() < inlay_highlight_count {
1707 inlay_indices.insert(rng.random_range(0..inlays.len()));
1708 }
1709 let new_highlights = TreeMap::from_ordered_entries(
1710 inlay_indices
1711 .into_iter()
1712 .filter_map(|i| {
1713 let (_, inlay) = &inlays[i];
1714 let inlay_text_len = inlay.text().len();
1715 match inlay_text_len {
1716 0 => None,
1717 1 => Some(InlayHighlight {
1718 inlay: inlay.id,
1719 inlay_position: inlay.position,
1720 range: 0..1,
1721 }),
1722 n => {
1723 let inlay_text = inlay.text().to_string();
1724 let mut highlight_end = rng.random_range(1..n);
1725 let mut highlight_start = rng.random_range(0..highlight_end);
1726 while !inlay_text.is_char_boundary(highlight_end) {
1727 highlight_end += 1;
1728 }
1729 while !inlay_text.is_char_boundary(highlight_start) {
1730 highlight_start -= 1;
1731 }
1732 Some(InlayHighlight {
1733 inlay: inlay.id,
1734 inlay_position: inlay.position,
1735 range: highlight_start..highlight_end,
1736 })
1737 }
1738 }
1739 })
1740 .map(|highlight| (highlight.inlay, (HighlightStyle::default(), highlight))),
1741 );
1742 log::info!("highlighting inlay ranges {new_highlights:?}");
1743 inlay_highlights.insert(TypeId::of::<()>(), new_highlights);
1744 }
1745
1746 for _ in 0..5 {
1747 let mut end = rng.random_range(0..=inlay_snapshot.len().0);
1748 end = expected_text.clip_offset(end, Bias::Right);
1749 let mut start = rng.random_range(0..=end);
1750 start = expected_text.clip_offset(start, Bias::Right);
1751
1752 let range = InlayOffset(start)..InlayOffset(end);
1753 log::info!("calling inlay_snapshot.chunks({range:?})");
1754 let actual_text = inlay_snapshot
1755 .chunks(
1756 range,
1757 false,
1758 Highlights {
1759 text_highlights: Some(&text_highlights),
1760 inlay_highlights: Some(&inlay_highlights),
1761 ..Highlights::default()
1762 },
1763 )
1764 .map(|chunk| chunk.chunk.text)
1765 .collect::<String>();
1766 assert_eq!(
1767 actual_text,
1768 expected_text.slice(start..end).to_string(),
1769 "incorrect text in range {:?}",
1770 start..end
1771 );
1772
1773 assert_eq!(
1774 inlay_snapshot.text_summary_for_range(InlayOffset(start)..InlayOffset(end)),
1775 expected_text.slice(start..end).summary()
1776 );
1777 }
1778
1779 for edit in inlay_edits {
1780 prev_inlay_text.replace_range(
1781 edit.new.start.0..edit.new.start.0 + edit.old_len().0,
1782 &inlay_snapshot.text()[edit.new.start.0..edit.new.end.0],
1783 );
1784 }
1785 assert_eq!(prev_inlay_text, inlay_snapshot.text());
1786
1787 assert_eq!(expected_text.max_point(), inlay_snapshot.max_point().0);
1788 assert_eq!(expected_text.len(), inlay_snapshot.len().0);
1789
1790 let mut buffer_point = Point::default();
1791 let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
1792 let mut buffer_chars = buffer_snapshot.chars_at(0);
1793 loop {
1794 // Ensure conversion from buffer coordinates to inlay coordinates
1795 // is consistent.
1796 let buffer_offset = buffer_snapshot.point_to_offset(buffer_point);
1797 assert_eq!(
1798 inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)),
1799 inlay_point
1800 );
1801
1802 // No matter which bias we clip an inlay point with, it doesn't move
1803 // because it was constructed from a buffer point.
1804 assert_eq!(
1805 inlay_snapshot.clip_point(inlay_point, Bias::Left),
1806 inlay_point,
1807 "invalid inlay point for buffer point {:?} when clipped left",
1808 buffer_point
1809 );
1810 assert_eq!(
1811 inlay_snapshot.clip_point(inlay_point, Bias::Right),
1812 inlay_point,
1813 "invalid inlay point for buffer point {:?} when clipped right",
1814 buffer_point
1815 );
1816
1817 if let Some(ch) = buffer_chars.next() {
1818 if ch == '\n' {
1819 buffer_point += Point::new(1, 0);
1820 } else {
1821 buffer_point += Point::new(0, ch.len_utf8() as u32);
1822 }
1823
1824 // Ensure that moving forward in the buffer always moves the inlay point forward as well.
1825 let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
1826 assert!(new_inlay_point > inlay_point);
1827 inlay_point = new_inlay_point;
1828 } else {
1829 break;
1830 }
1831 }
1832
1833 let mut inlay_point = InlayPoint::default();
1834 let mut inlay_offset = InlayOffset::default();
1835 for ch in expected_text.chars() {
1836 assert_eq!(
1837 inlay_snapshot.to_offset(inlay_point),
1838 inlay_offset,
1839 "invalid to_offset({:?})",
1840 inlay_point
1841 );
1842 assert_eq!(
1843 inlay_snapshot.to_point(inlay_offset),
1844 inlay_point,
1845 "invalid to_point({:?})",
1846 inlay_offset
1847 );
1848
1849 let mut bytes = [0; 4];
1850 for byte in ch.encode_utf8(&mut bytes).as_bytes() {
1851 inlay_offset.0 += 1;
1852 if *byte == b'\n' {
1853 inlay_point.0 += Point::new(1, 0);
1854 } else {
1855 inlay_point.0 += Point::new(0, 1);
1856 }
1857
1858 let clipped_left_point = inlay_snapshot.clip_point(inlay_point, Bias::Left);
1859 let clipped_right_point = inlay_snapshot.clip_point(inlay_point, Bias::Right);
1860 assert!(
1861 clipped_left_point <= clipped_right_point,
1862 "inlay point {:?} when clipped left is greater than when clipped right ({:?} > {:?})",
1863 inlay_point,
1864 clipped_left_point,
1865 clipped_right_point
1866 );
1867
1868 // Ensure the clipped points are at valid text locations.
1869 assert_eq!(
1870 clipped_left_point.0,
1871 expected_text.clip_point(clipped_left_point.0, Bias::Left)
1872 );
1873 assert_eq!(
1874 clipped_right_point.0,
1875 expected_text.clip_point(clipped_right_point.0, Bias::Right)
1876 );
1877
1878 // Ensure the clipped points never overshoot the end of the map.
1879 assert!(clipped_left_point <= inlay_snapshot.max_point());
1880 assert!(clipped_right_point <= inlay_snapshot.max_point());
1881
1882 // Ensure the clipped points are at valid buffer locations.
1883 assert_eq!(
1884 inlay_snapshot
1885 .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)),
1886 clipped_left_point,
1887 "to_buffer_point({:?}) = {:?}",
1888 clipped_left_point,
1889 inlay_snapshot.to_buffer_point(clipped_left_point),
1890 );
1891 assert_eq!(
1892 inlay_snapshot
1893 .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)),
1894 clipped_right_point,
1895 "to_buffer_point({:?}) = {:?}",
1896 clipped_right_point,
1897 inlay_snapshot.to_buffer_point(clipped_right_point),
1898 );
1899 }
1900 }
1901 }
1902 }
1903
1904 #[gpui::test(iterations = 100)]
1905 fn test_random_chunk_bitmaps(cx: &mut gpui::App, mut rng: StdRng) {
1906 init_test(cx);
1907
1908 // Generate random buffer using existing test infrastructure
1909 let text_len = rng.random_range(0..10000);
1910 let buffer = if rng.random() {
1911 let text = RandomCharIter::new(&mut rng)
1912 .take(text_len)
1913 .collect::<String>();
1914 MultiBuffer::build_simple(&text, cx)
1915 } else {
1916 MultiBuffer::build_random(&mut rng, cx)
1917 };
1918
1919 let buffer_snapshot = buffer.read(cx).snapshot(cx);
1920 let (mut inlay_map, _) = InlayMap::new(buffer_snapshot.clone());
1921
1922 // Perform random mutations to add inlays
1923 let mut next_inlay_id = 0;
1924 let mutation_count = rng.random_range(1..10);
1925 for _ in 0..mutation_count {
1926 inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
1927 }
1928
1929 let (snapshot, _) = inlay_map.sync(buffer_snapshot, vec![]);
1930
1931 // Get all chunks and verify their bitmaps
1932 let chunks = snapshot.chunks(
1933 InlayOffset(0)..InlayOffset(snapshot.len().0),
1934 false,
1935 Highlights::default(),
1936 );
1937
1938 for chunk in chunks.into_iter().map(|inlay_chunk| inlay_chunk.chunk) {
1939 let chunk_text = chunk.text;
1940 let chars_bitmap = chunk.chars;
1941 let tabs_bitmap = chunk.tabs;
1942
1943 // Check empty chunks have empty bitmaps
1944 if chunk_text.is_empty() {
1945 assert_eq!(
1946 chars_bitmap, 0,
1947 "Empty chunk should have empty chars bitmap"
1948 );
1949 assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
1950 continue;
1951 }
1952
1953 // Verify that chunk text doesn't exceed 128 bytes
1954 assert!(
1955 chunk_text.len() <= 128,
1956 "Chunk text length {} exceeds 128 bytes",
1957 chunk_text.len()
1958 );
1959
1960 // Verify chars bitmap
1961 let char_indices = chunk_text
1962 .char_indices()
1963 .map(|(i, _)| i)
1964 .collect::<Vec<_>>();
1965
1966 for byte_idx in 0..chunk_text.len() {
1967 let should_have_bit = char_indices.contains(&byte_idx);
1968 let has_bit = chars_bitmap & (1 << byte_idx) != 0;
1969
1970 if has_bit != should_have_bit {
1971 eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
1972 eprintln!("Char indices: {:?}", char_indices);
1973 eprintln!("Chars bitmap: {:#b}", chars_bitmap);
1974 assert_eq!(
1975 has_bit, should_have_bit,
1976 "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
1977 byte_idx, chunk_text, should_have_bit, has_bit
1978 );
1979 }
1980 }
1981
1982 // Verify tabs bitmap
1983 for (byte_idx, byte) in chunk_text.bytes().enumerate() {
1984 let is_tab = byte == b'\t';
1985 let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
1986
1987 if has_bit != is_tab {
1988 eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
1989 eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
1990 assert_eq!(
1991 has_bit, is_tab,
1992 "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
1993 byte_idx, chunk_text, byte as char, is_tab, has_bit
1994 );
1995 }
1996 }
1997 }
1998 }
1999
2000 fn init_test(cx: &mut App) {
2001 let store = SettingsStore::test(cx);
2002 cx.set_global(store);
2003 theme::init(theme::LoadThemes::JustBase, cx);
2004 }
2005
2006 /// Helper to create test highlights for an inlay
2007 fn create_inlay_highlights(
2008 inlay_id: InlayId,
2009 highlight_range: Range<usize>,
2010 position: Anchor,
2011 ) -> TreeMap<TypeId, TreeMap<InlayId, (HighlightStyle, InlayHighlight)>> {
2012 let mut inlay_highlights = TreeMap::default();
2013 let mut type_highlights = TreeMap::default();
2014 type_highlights.insert(
2015 inlay_id,
2016 (
2017 HighlightStyle::default(),
2018 InlayHighlight {
2019 inlay: inlay_id,
2020 range: highlight_range,
2021 inlay_position: position,
2022 },
2023 ),
2024 );
2025 inlay_highlights.insert(TypeId::of::<()>(), type_highlights);
2026 inlay_highlights
2027 }
2028
2029 #[gpui::test]
2030 fn test_inlay_utf8_boundary_panic_fix(cx: &mut App) {
2031 init_test(cx);
2032
2033 // This test verifies that we handle UTF-8 character boundaries correctly
2034 // when splitting inlay text for highlighting. Previously, this would panic
2035 // when trying to split at byte 13, which is in the middle of the '…' character.
2036 //
2037 // See https://github.com/zed-industries/zed/issues/33641
2038 let buffer = MultiBuffer::build_simple("fn main() {}\n", cx);
2039 let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx));
2040
2041 // Create an inlay with text that contains a multi-byte character
2042 // The string "SortingDirec…" contains an ellipsis character '…' which is 3 bytes (E2 80 A6)
2043 let inlay_text = "SortingDirec…";
2044 let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 5));
2045
2046 let inlay = Inlay {
2047 id: InlayId::Hint(0),
2048 position,
2049 content: InlayContent::Text(text::Rope::from(inlay_text)),
2050 };
2051
2052 let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]);
2053
2054 // Create highlights that request a split at byte 13, which is in the middle
2055 // of the '…' character (bytes 12..15). We include the full character.
2056 let inlay_highlights = create_inlay_highlights(InlayId::Hint(0), 0..13, position);
2057
2058 let highlights = crate::display_map::Highlights {
2059 text_highlights: None,
2060 inlay_highlights: Some(&inlay_highlights),
2061 styles: crate::display_map::HighlightStyles::default(),
2062 };
2063
2064 // Collect chunks - this previously would panic
2065 let chunks: Vec<_> = inlay_snapshot
2066 .chunks(
2067 InlayOffset(0)..InlayOffset(inlay_snapshot.len().0),
2068 false,
2069 highlights,
2070 )
2071 .collect();
2072
2073 // Verify the chunks are correct
2074 let full_text: String = chunks.iter().map(|c| c.chunk.text).collect();
2075 assert_eq!(full_text, "fn maSortingDirec…in() {}\n");
2076
2077 // Verify the highlighted portion includes the complete ellipsis character
2078 let highlighted_chunks: Vec<_> = chunks
2079 .iter()
2080 .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay)
2081 .collect();
2082
2083 assert_eq!(highlighted_chunks.len(), 1);
2084 assert_eq!(highlighted_chunks[0].chunk.text, "SortingDirec…");
2085 }
2086
2087 #[gpui::test]
2088 fn test_inlay_utf8_boundaries(cx: &mut App) {
2089 init_test(cx);
2090
2091 struct TestCase {
2092 inlay_text: &'static str,
2093 highlight_range: Range<usize>,
2094 expected_highlighted: &'static str,
2095 description: &'static str,
2096 }
2097
2098 let test_cases = vec![
2099 TestCase {
2100 inlay_text: "Hello👋World",
2101 highlight_range: 0..7,
2102 expected_highlighted: "Hello👋",
2103 description: "Emoji boundary - rounds up to include full emoji",
2104 },
2105 TestCase {
2106 inlay_text: "Test→End",
2107 highlight_range: 0..5,
2108 expected_highlighted: "Test→",
2109 description: "Arrow boundary - rounds up to include full arrow",
2110 },
2111 TestCase {
2112 inlay_text: "café",
2113 highlight_range: 0..4,
2114 expected_highlighted: "café",
2115 description: "Accented char boundary - rounds up to include full é",
2116 },
2117 TestCase {
2118 inlay_text: "🎨🎭🎪",
2119 highlight_range: 0..5,
2120 expected_highlighted: "🎨🎭",
2121 description: "Multiple emojis - partial highlight",
2122 },
2123 TestCase {
2124 inlay_text: "普通话",
2125 highlight_range: 0..4,
2126 expected_highlighted: "普通",
2127 description: "Chinese characters - partial highlight",
2128 },
2129 TestCase {
2130 inlay_text: "Hello",
2131 highlight_range: 0..2,
2132 expected_highlighted: "He",
2133 description: "ASCII only - no adjustment needed",
2134 },
2135 TestCase {
2136 inlay_text: "👋",
2137 highlight_range: 0..1,
2138 expected_highlighted: "👋",
2139 description: "Single emoji - partial byte range includes whole char",
2140 },
2141 TestCase {
2142 inlay_text: "Test",
2143 highlight_range: 0..0,
2144 expected_highlighted: "",
2145 description: "Empty range",
2146 },
2147 TestCase {
2148 inlay_text: "🎨ABC",
2149 highlight_range: 2..5,
2150 expected_highlighted: "A",
2151 description: "Range starting mid-emoji skips the emoji",
2152 },
2153 ];
2154
2155 for test_case in test_cases {
2156 let buffer = MultiBuffer::build_simple("test", cx);
2157 let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx));
2158 let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 2));
2159
2160 let inlay = Inlay {
2161 id: InlayId::Hint(0),
2162 position,
2163 content: InlayContent::Text(text::Rope::from(test_case.inlay_text)),
2164 };
2165
2166 let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]);
2167 let inlay_highlights = create_inlay_highlights(
2168 InlayId::Hint(0),
2169 test_case.highlight_range.clone(),
2170 position,
2171 );
2172
2173 let highlights = crate::display_map::Highlights {
2174 text_highlights: None,
2175 inlay_highlights: Some(&inlay_highlights),
2176 styles: crate::display_map::HighlightStyles::default(),
2177 };
2178
2179 let chunks: Vec<_> = inlay_snapshot
2180 .chunks(
2181 InlayOffset(0)..InlayOffset(inlay_snapshot.len().0),
2182 false,
2183 highlights,
2184 )
2185 .collect();
2186
2187 // Verify we got chunks and they total to the expected text
2188 let full_text: String = chunks.iter().map(|c| c.chunk.text).collect();
2189 assert_eq!(
2190 full_text,
2191 format!("te{}st", test_case.inlay_text),
2192 "Full text mismatch for case: {}",
2193 test_case.description
2194 );
2195
2196 // Verify that the highlighted portion matches expectations
2197 let highlighted_text: String = chunks
2198 .iter()
2199 .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay)
2200 .map(|c| c.chunk.text)
2201 .collect();
2202 assert_eq!(
2203 highlighted_text, test_case.expected_highlighted,
2204 "Highlighted text mismatch for case: {} (text: '{}', range: {:?})",
2205 test_case.description, test_case.inlay_text, test_case.highlight_range
2206 );
2207 }
2208 }
2209}