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