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