inlay_map.rs

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