inlay_map.rs

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