inlay_map.rs

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