inlay_map.rs

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