inlay_map.rs

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