inlay_map.rs

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