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