inlay_hints.rs

   1use std::{
   2    collections::hash_map,
   3    ops::{ControlFlow, Range},
   4    time::Duration,
   5};
   6
   7use clock::Global;
   8use collections::{HashMap, HashSet};
   9use futures::future::join_all;
  10use gpui::{App, Entity, Task};
  11use itertools::Itertools;
  12use language::{
  13    BufferRow,
  14    language_settings::{InlayHintKind, InlayHintSettings, language_settings},
  15};
  16use lsp::LanguageServerId;
  17use multi_buffer::{Anchor, ExcerptId, MultiBufferSnapshot};
  18use project::{
  19    HoverBlock, HoverBlockKind, InlayHintLabel, InlayHintLabelPartTooltip, InlayHintTooltip,
  20    InvalidationStrategy, ResolveState,
  21    lsp_store::{CacheInlayHints, ResolvedHint},
  22};
  23use text::{Bias, BufferId};
  24use ui::{Context, Window};
  25use util::debug_panic;
  26
  27use super::{Inlay, InlayId};
  28use crate::{
  29    Editor, EditorSnapshot, PointForPosition, ToggleInlayHints, ToggleInlineValues, debounce_value,
  30    display_map::{DisplayMap, InlayOffset},
  31    hover_links::{InlayHighlight, TriggerPoint, show_link_definition},
  32    hover_popover::{self, InlayHover},
  33    inlays::InlaySplice,
  34};
  35
  36pub fn inlay_hint_settings(
  37    location: Anchor,
  38    snapshot: &MultiBufferSnapshot,
  39    cx: &mut Context<Editor>,
  40) -> InlayHintSettings {
  41    let file = snapshot.file_at(location);
  42    let language = snapshot.language_at(location).map(|l| l.name());
  43    language_settings(language, file, cx).inlay_hints
  44}
  45
  46#[derive(Debug)]
  47pub struct LspInlayHintData {
  48    enabled: bool,
  49    modifiers_override: bool,
  50    enabled_in_settings: bool,
  51    allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
  52    invalidate_debounce: Option<Duration>,
  53    append_debounce: Option<Duration>,
  54    hint_refresh_tasks: HashMap<BufferId, Vec<Task<()>>>,
  55    hint_chunk_fetching: HashMap<BufferId, (Global, HashSet<Range<BufferRow>>)>,
  56    invalidate_hints_for_buffers: HashSet<BufferId>,
  57    pub added_hints: HashMap<InlayId, Option<InlayHintKind>>,
  58}
  59
  60impl LspInlayHintData {
  61    pub fn new(settings: InlayHintSettings) -> Self {
  62        Self {
  63            modifiers_override: false,
  64            enabled: settings.enabled,
  65            enabled_in_settings: settings.enabled,
  66            hint_refresh_tasks: HashMap::default(),
  67            added_hints: HashMap::default(),
  68            hint_chunk_fetching: HashMap::default(),
  69            invalidate_hints_for_buffers: HashSet::default(),
  70            invalidate_debounce: debounce_value(settings.edit_debounce_ms),
  71            append_debounce: debounce_value(settings.scroll_debounce_ms),
  72            allowed_hint_kinds: settings.enabled_inlay_hint_kinds(),
  73        }
  74    }
  75
  76    pub fn modifiers_override(&mut self, new_override: bool) -> Option<bool> {
  77        if self.modifiers_override == new_override {
  78            return None;
  79        }
  80        self.modifiers_override = new_override;
  81        if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
  82        {
  83            self.clear();
  84            Some(false)
  85        } else {
  86            Some(true)
  87        }
  88    }
  89
  90    pub fn toggle(&mut self, enabled: bool) -> bool {
  91        if self.enabled == enabled {
  92            return false;
  93        }
  94        self.enabled = enabled;
  95        self.modifiers_override = false;
  96        if !enabled {
  97            self.clear();
  98        }
  99        true
 100    }
 101
 102    pub fn clear(&mut self) {
 103        self.hint_refresh_tasks.clear();
 104        self.hint_chunk_fetching.clear();
 105        self.added_hints.clear();
 106    }
 107
 108    /// Like `clear`, but only wipes tracking state for the given buffer IDs.
 109    /// Hints belonging to other buffers are left intact so they are neither
 110    /// re-fetched nor duplicated on the next `NewLinesShown`.
 111    pub fn clear_for_buffers(
 112        &mut self,
 113        buffer_ids: &HashSet<BufferId>,
 114        current_hints: impl IntoIterator<Item = Inlay>,
 115    ) {
 116        for buffer_id in buffer_ids {
 117            self.hint_refresh_tasks.remove(buffer_id);
 118            self.hint_chunk_fetching.remove(buffer_id);
 119        }
 120        for hint in current_hints {
 121            if let Some(buffer_id) = hint.position.text_anchor.buffer_id {
 122                if buffer_ids.contains(&buffer_id) {
 123                    self.added_hints.remove(&hint.id);
 124                }
 125            }
 126        }
 127    }
 128
 129    /// Checks inlay hint settings for enabled hint kinds and general enabled state.
 130    /// Generates corresponding inlay_map splice updates on settings changes.
 131    /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries.
 132    fn update_settings(
 133        &mut self,
 134        new_hint_settings: InlayHintSettings,
 135        visible_hints: impl IntoIterator<Item = Inlay>,
 136    ) -> ControlFlow<Option<InlaySplice>, Option<InlaySplice>> {
 137        let old_enabled = self.enabled;
 138        // If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay
 139        // hint visibility changes when other settings change (such as theme).
 140        //
 141        // Another option might be to store whether the user has manually toggled inlay hint
 142        // visibility, and prefer this. This could lead to confusion as it means inlay hint
 143        // visibility would not change when updating the setting if they were ever toggled.
 144        if new_hint_settings.enabled != self.enabled_in_settings {
 145            self.enabled = new_hint_settings.enabled;
 146            self.enabled_in_settings = new_hint_settings.enabled;
 147            self.modifiers_override = false;
 148        };
 149        self.invalidate_debounce = debounce_value(new_hint_settings.edit_debounce_ms);
 150        self.append_debounce = debounce_value(new_hint_settings.scroll_debounce_ms);
 151        let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
 152        match (old_enabled, self.enabled) {
 153            (false, false) => {
 154                self.allowed_hint_kinds = new_allowed_hint_kinds;
 155                ControlFlow::Break(None)
 156            }
 157            (true, true) => {
 158                if new_allowed_hint_kinds == self.allowed_hint_kinds {
 159                    ControlFlow::Break(None)
 160                } else {
 161                    self.allowed_hint_kinds = new_allowed_hint_kinds;
 162                    ControlFlow::Continue(
 163                        Some(InlaySplice {
 164                            to_remove: visible_hints
 165                                .into_iter()
 166                                .filter_map(|inlay| {
 167                                    let inlay_kind = self.added_hints.get(&inlay.id).copied()?;
 168                                    if !self.allowed_hint_kinds.contains(&inlay_kind) {
 169                                        Some(inlay.id)
 170                                    } else {
 171                                        None
 172                                    }
 173                                })
 174                                .collect(),
 175                            to_insert: Vec::new(),
 176                        })
 177                        .filter(|splice| !splice.is_empty()),
 178                    )
 179                }
 180            }
 181            (true, false) => {
 182                self.modifiers_override = false;
 183                self.allowed_hint_kinds = new_allowed_hint_kinds;
 184                let mut visible_hints = visible_hints.into_iter().peekable();
 185                if visible_hints.peek().is_none() {
 186                    ControlFlow::Break(None)
 187                } else {
 188                    self.clear();
 189                    ControlFlow::Break(Some(InlaySplice {
 190                        to_remove: visible_hints.map(|inlay| inlay.id).collect(),
 191                        to_insert: Vec::new(),
 192                    }))
 193                }
 194            }
 195            (false, true) => {
 196                self.modifiers_override = false;
 197                self.allowed_hint_kinds = new_allowed_hint_kinds;
 198                ControlFlow::Continue(
 199                    Some(InlaySplice {
 200                        to_remove: visible_hints
 201                            .into_iter()
 202                            .filter_map(|inlay| {
 203                                let inlay_kind = self.added_hints.get(&inlay.id).copied()?;
 204                                if !self.allowed_hint_kinds.contains(&inlay_kind) {
 205                                    Some(inlay.id)
 206                                } else {
 207                                    None
 208                                }
 209                            })
 210                            .collect(),
 211                        to_insert: Vec::new(),
 212                    })
 213                    .filter(|splice| !splice.is_empty()),
 214                )
 215            }
 216        }
 217    }
 218
 219    pub(crate) fn remove_inlay_chunk_data<'a>(
 220        &'a mut self,
 221        removed_buffer_ids: impl IntoIterator<Item = &'a BufferId> + 'a,
 222    ) {
 223        for buffer_id in removed_buffer_ids {
 224            self.hint_refresh_tasks.remove(buffer_id);
 225            self.hint_chunk_fetching.remove(buffer_id);
 226        }
 227    }
 228}
 229
 230#[derive(Debug, Clone)]
 231pub enum InlayHintRefreshReason {
 232    ModifiersChanged(bool),
 233    Toggle(bool),
 234    SettingsChange(InlayHintSettings),
 235    NewLinesShown,
 236    BufferEdited(BufferId),
 237    ServerRemoved,
 238    RefreshRequested {
 239        server_id: LanguageServerId,
 240        request_id: Option<usize>,
 241    },
 242    ExcerptsRemoved(Vec<ExcerptId>),
 243}
 244
 245impl Editor {
 246    pub fn supports_inlay_hints(&self, cx: &mut App) -> bool {
 247        let Some(provider) = self.semantics_provider.as_ref() else {
 248            return false;
 249        };
 250
 251        let mut supports = false;
 252        self.buffer().update(cx, |this, cx| {
 253            this.for_each_buffer(&mut |buffer| {
 254                supports |= provider.supports_inlay_hints(buffer, cx);
 255            });
 256        });
 257
 258        supports
 259    }
 260
 261    pub fn toggle_inline_values(
 262        &mut self,
 263        _: &ToggleInlineValues,
 264        _: &mut Window,
 265        cx: &mut Context<Self>,
 266    ) {
 267        self.inline_value_cache.enabled = !self.inline_value_cache.enabled;
 268
 269        self.refresh_inline_values(cx);
 270    }
 271
 272    pub fn toggle_inlay_hints(
 273        &mut self,
 274        _: &ToggleInlayHints,
 275        _: &mut Window,
 276        cx: &mut Context<Self>,
 277    ) {
 278        self.refresh_inlay_hints(
 279            InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()),
 280            cx,
 281        );
 282    }
 283
 284    pub fn inlay_hints_enabled(&self) -> bool {
 285        self.inlay_hints.as_ref().is_some_and(|cache| cache.enabled)
 286    }
 287
 288    /// Updates inlay hints for the visible ranges of the singleton buffer(s).
 289    /// Based on its parameters, either invalidates the previous data, or appends to it.
 290    pub(crate) fn refresh_inlay_hints(
 291        &mut self,
 292        reason: InlayHintRefreshReason,
 293        cx: &mut Context<Self>,
 294    ) {
 295        if !self.mode().is_full() || self.inlay_hints.is_none() {
 296            return;
 297        }
 298        let Some(semantics_provider) = self.semantics_provider() else {
 299            return;
 300        };
 301        let Some(invalidate_cache) = self.refresh_editor_data(&reason, cx) else {
 302            return;
 303        };
 304
 305        let debounce = match &reason {
 306            InlayHintRefreshReason::SettingsChange(_)
 307            | InlayHintRefreshReason::Toggle(_)
 308            | InlayHintRefreshReason::ExcerptsRemoved(_)
 309            | InlayHintRefreshReason::ModifiersChanged(_) => None,
 310            _may_need_lsp_call => self.inlay_hints.as_ref().and_then(|inlay_hints| {
 311                if invalidate_cache.should_invalidate() {
 312                    inlay_hints.invalidate_debounce
 313                } else {
 314                    inlay_hints.append_debounce
 315                }
 316            }),
 317        };
 318
 319        let mut visible_excerpts = self.visible_excerpts(true, cx);
 320
 321        let mut invalidate_hints_for_buffers = HashSet::default();
 322        let ignore_previous_fetches = match reason {
 323            InlayHintRefreshReason::ModifiersChanged(_)
 324            | InlayHintRefreshReason::Toggle(_)
 325            | InlayHintRefreshReason::SettingsChange(_)
 326            | InlayHintRefreshReason::ServerRemoved => true,
 327            InlayHintRefreshReason::NewLinesShown
 328            | InlayHintRefreshReason::RefreshRequested { .. }
 329            | InlayHintRefreshReason::ExcerptsRemoved(_) => false,
 330            InlayHintRefreshReason::BufferEdited(buffer_id) => {
 331                let Some(affected_language) = self
 332                    .buffer()
 333                    .read(cx)
 334                    .buffer(buffer_id)
 335                    .and_then(|buffer| buffer.read(cx).language().cloned())
 336                else {
 337                    return;
 338                };
 339
 340                invalidate_hints_for_buffers.extend(
 341                    self.buffer()
 342                        .read(cx)
 343                        .all_buffers()
 344                        .into_iter()
 345                        .filter_map(|buffer| {
 346                            let buffer = buffer.read(cx);
 347                            if buffer.language() == Some(&affected_language) {
 348                                Some(buffer.remote_id())
 349                            } else {
 350                                None
 351                            }
 352                        }),
 353                );
 354
 355                semantics_provider.invalidate_inlay_hints(&invalidate_hints_for_buffers, cx);
 356                visible_excerpts.retain(|_, (visible_buffer, _, _)| {
 357                    visible_buffer.read(cx).language() == Some(&affected_language)
 358                });
 359                false
 360            }
 361        };
 362
 363        let multi_buffer = self.buffer().clone();
 364
 365        let Some(inlay_hints) = self.inlay_hints.as_mut() else {
 366            return;
 367        };
 368
 369        if invalidate_cache.should_invalidate() {
 370            if invalidate_hints_for_buffers.is_empty() {
 371                inlay_hints.clear();
 372            } else {
 373                inlay_hints.clear_for_buffers(
 374                    &invalidate_hints_for_buffers,
 375                    Self::visible_inlay_hints(self.display_map.read(cx)),
 376                );
 377            }
 378        }
 379        inlay_hints
 380            .invalidate_hints_for_buffers
 381            .extend(invalidate_hints_for_buffers);
 382
 383        let mut buffers_to_query = HashMap::default();
 384        for (_, (buffer, buffer_version, visible_range)) in visible_excerpts {
 385            let buffer_id = buffer.read(cx).remote_id();
 386
 387            if !self.registered_buffers.contains_key(&buffer_id) {
 388                continue;
 389            }
 390
 391            let buffer_snapshot = buffer.read(cx).snapshot();
 392            let buffer_anchor_range = buffer_snapshot.anchor_before(visible_range.start)
 393                ..buffer_snapshot.anchor_after(visible_range.end);
 394
 395            let visible_excerpts =
 396                buffers_to_query
 397                    .entry(buffer_id)
 398                    .or_insert_with(|| VisibleExcerpts {
 399                        ranges: Vec::new(),
 400                        buffer_version: buffer_version.clone(),
 401                        buffer: buffer.clone(),
 402                    });
 403            visible_excerpts.buffer_version = buffer_version;
 404            visible_excerpts.ranges.push(buffer_anchor_range);
 405        }
 406
 407        for (buffer_id, visible_excerpts) in buffers_to_query {
 408            let Some(buffer) = multi_buffer.read(cx).buffer(buffer_id) else {
 409                continue;
 410            };
 411
 412            let (fetched_for_version, fetched_chunks) = inlay_hints
 413                .hint_chunk_fetching
 414                .entry(buffer_id)
 415                .or_default();
 416            if visible_excerpts
 417                .buffer_version
 418                .changed_since(fetched_for_version)
 419            {
 420                *fetched_for_version = visible_excerpts.buffer_version.clone();
 421                fetched_chunks.clear();
 422                inlay_hints.hint_refresh_tasks.remove(&buffer_id);
 423            }
 424
 425            let known_chunks = if ignore_previous_fetches {
 426                None
 427            } else {
 428                Some((fetched_for_version.clone(), fetched_chunks.clone()))
 429            };
 430
 431            let mut applicable_chunks =
 432                semantics_provider.applicable_inlay_chunks(&buffer, &visible_excerpts.ranges, cx);
 433            applicable_chunks.retain(|chunk| fetched_chunks.insert(chunk.clone()));
 434            if applicable_chunks.is_empty() && !ignore_previous_fetches {
 435                continue;
 436            }
 437            inlay_hints
 438                .hint_refresh_tasks
 439                .entry(buffer_id)
 440                .or_default()
 441                .push(spawn_editor_hints_refresh(
 442                    buffer_id,
 443                    invalidate_cache,
 444                    debounce,
 445                    visible_excerpts,
 446                    known_chunks,
 447                    applicable_chunks,
 448                    cx,
 449                ));
 450        }
 451    }
 452
 453    pub fn clear_inlay_hints(&mut self, cx: &mut Context<Self>) {
 454        let to_remove = Self::visible_inlay_hints(self.display_map.read(cx))
 455            .map(|inlay| inlay.id)
 456            .collect::<Vec<_>>();
 457        self.splice_inlays(&to_remove, Vec::new(), cx);
 458    }
 459
 460    fn refresh_editor_data(
 461        &mut self,
 462        reason: &InlayHintRefreshReason,
 463        cx: &mut Context<'_, Editor>,
 464    ) -> Option<InvalidationStrategy> {
 465        let Some(inlay_hints) = self.inlay_hints.as_mut() else {
 466            return None;
 467        };
 468
 469        let invalidate_cache = match reason {
 470            InlayHintRefreshReason::ModifiersChanged(enabled) => {
 471                match inlay_hints.modifiers_override(*enabled) {
 472                    Some(enabled) => {
 473                        if enabled {
 474                            InvalidationStrategy::None
 475                        } else {
 476                            self.clear_inlay_hints(cx);
 477                            return None;
 478                        }
 479                    }
 480                    None => return None,
 481                }
 482            }
 483            InlayHintRefreshReason::Toggle(enabled) => {
 484                if inlay_hints.toggle(*enabled) {
 485                    if *enabled {
 486                        InvalidationStrategy::None
 487                    } else {
 488                        self.clear_inlay_hints(cx);
 489                        return None;
 490                    }
 491                } else {
 492                    return None;
 493                }
 494            }
 495            InlayHintRefreshReason::SettingsChange(new_settings) => {
 496                let visible_inlay_hints =
 497                    Self::visible_inlay_hints(self.display_map.read(cx)).collect::<Vec<_>>();
 498                match inlay_hints.update_settings(*new_settings, visible_inlay_hints) {
 499                    ControlFlow::Break(Some(InlaySplice {
 500                        to_remove,
 501                        to_insert,
 502                    })) => {
 503                        self.splice_inlays(&to_remove, to_insert, cx);
 504                        return None;
 505                    }
 506                    ControlFlow::Break(None) => return None,
 507                    ControlFlow::Continue(splice) => {
 508                        if let Some(InlaySplice {
 509                            to_remove,
 510                            to_insert,
 511                        }) = splice
 512                        {
 513                            self.splice_inlays(&to_remove, to_insert, cx);
 514                        }
 515                        InvalidationStrategy::None
 516                    }
 517                }
 518            }
 519            InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => {
 520                let to_remove = self
 521                    .display_map
 522                    .read(cx)
 523                    .current_inlays()
 524                    .filter_map(|inlay| {
 525                        if excerpts_removed.contains(&inlay.position.excerpt_id) {
 526                            Some(inlay.id)
 527                        } else {
 528                            None
 529                        }
 530                    })
 531                    .collect::<Vec<_>>();
 532                self.splice_inlays(&to_remove, Vec::new(), cx);
 533                return None;
 534            }
 535            InlayHintRefreshReason::ServerRemoved => InvalidationStrategy::BufferEdited,
 536            InlayHintRefreshReason::NewLinesShown => InvalidationStrategy::None,
 537            InlayHintRefreshReason::BufferEdited(_) => InvalidationStrategy::BufferEdited,
 538            InlayHintRefreshReason::RefreshRequested {
 539                server_id,
 540                request_id,
 541            } => InvalidationStrategy::RefreshRequested {
 542                server_id: *server_id,
 543                request_id: *request_id,
 544            },
 545        };
 546
 547        match &mut self.inlay_hints {
 548            Some(inlay_hints) => {
 549                if !inlay_hints.enabled
 550                    && !matches!(reason, InlayHintRefreshReason::ModifiersChanged(_))
 551                {
 552                    return None;
 553                }
 554            }
 555            None => return None,
 556        }
 557
 558        Some(invalidate_cache)
 559    }
 560
 561    fn visible_inlay_hints(display_map: &DisplayMap) -> impl Iterator<Item = Inlay> + use<'_> {
 562        display_map
 563            .current_inlays()
 564            .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
 565            .cloned()
 566    }
 567
 568    pub fn update_inlay_link_and_hover_points(
 569        &mut self,
 570        snapshot: &EditorSnapshot,
 571        point_for_position: PointForPosition,
 572        secondary_held: bool,
 573        shift_held: bool,
 574        window: &mut Window,
 575        cx: &mut Context<Self>,
 576    ) {
 577        let Some(lsp_store) = self.project().map(|project| project.read(cx).lsp_store()) else {
 578            return;
 579        };
 580        let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
 581            Some(
 582                snapshot
 583                    .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left),
 584            )
 585        } else {
 586            None
 587        };
 588        let mut go_to_definition_updated = false;
 589        let mut hover_updated = false;
 590        if let Some(hovered_offset) = hovered_offset {
 591            let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
 592            let previous_valid_anchor = buffer_snapshot.anchor_at(
 593                point_for_position.previous_valid.to_point(snapshot),
 594                Bias::Left,
 595            );
 596            let next_valid_anchor = buffer_snapshot.anchor_at(
 597                point_for_position.next_valid.to_point(snapshot),
 598                Bias::Right,
 599            );
 600            if let Some(hovered_hint) = Self::visible_inlay_hints(self.display_map.read(cx))
 601                .filter(|hint| snapshot.can_resolve(&hint.position))
 602                .skip_while(|hint| {
 603                    hint.position
 604                        .cmp(&previous_valid_anchor, &buffer_snapshot)
 605                        .is_lt()
 606                })
 607                .take_while(|hint| {
 608                    hint.position
 609                        .cmp(&next_valid_anchor, &buffer_snapshot)
 610                        .is_le()
 611                })
 612                .max_by_key(|hint| hint.id)
 613            {
 614                if let Some(ResolvedHint::Resolved(cached_hint)) = hovered_hint
 615                    .position
 616                    .text_anchor
 617                    .buffer_id
 618                    .and_then(|buffer_id| {
 619                        lsp_store.update(cx, |lsp_store, cx| {
 620                            lsp_store.resolved_hint(buffer_id, hovered_hint.id, cx)
 621                        })
 622                    })
 623                {
 624                    match cached_hint.resolve_state {
 625                        ResolveState::Resolved => {
 626                            let original_text = cached_hint.text();
 627                            let actual_left_padding =
 628                                if cached_hint.padding_left && !original_text.starts_with(" ") {
 629                                    1
 630                                } else {
 631                                    0
 632                                };
 633                            let actual_right_padding =
 634                                if cached_hint.padding_right && !original_text.ends_with(" ") {
 635                                    1
 636                                } else {
 637                                    0
 638                                };
 639                            match cached_hint.label {
 640                                InlayHintLabel::String(_) => {
 641                                    if let Some(tooltip) = cached_hint.tooltip {
 642                                        hover_popover::hover_at_inlay(
 643                                            self,
 644                                            InlayHover {
 645                                                tooltip: match tooltip {
 646                                                    InlayHintTooltip::String(text) => HoverBlock {
 647                                                        text,
 648                                                        kind: HoverBlockKind::PlainText,
 649                                                    },
 650                                                    InlayHintTooltip::MarkupContent(content) => {
 651                                                        HoverBlock {
 652                                                            text: content.value,
 653                                                            kind: content.kind,
 654                                                        }
 655                                                    }
 656                                                },
 657                                                range: InlayHighlight {
 658                                                    inlay: hovered_hint.id,
 659                                                    inlay_position: hovered_hint.position,
 660                                                    range: actual_left_padding
 661                                                        ..hovered_hint.text().len()
 662                                                            - actual_right_padding,
 663                                                },
 664                                            },
 665                                            window,
 666                                            cx,
 667                                        );
 668                                        hover_updated = true;
 669                                    }
 670                                }
 671                                InlayHintLabel::LabelParts(label_parts) => {
 672                                    let hint_start =
 673                                        snapshot.anchor_to_inlay_offset(hovered_hint.position);
 674                                    let content_start =
 675                                        InlayOffset(hint_start.0 + actual_left_padding);
 676                                    if let Some((hovered_hint_part, part_range)) =
 677                                        hover_popover::find_hovered_hint_part(
 678                                            label_parts,
 679                                            content_start,
 680                                            hovered_offset,
 681                                        )
 682                                    {
 683                                        let highlight_start = part_range.start - hint_start;
 684                                        let highlight_end = part_range.end - hint_start;
 685                                        let highlight = InlayHighlight {
 686                                            inlay: hovered_hint.id,
 687                                            inlay_position: hovered_hint.position,
 688                                            range: highlight_start..highlight_end,
 689                                        };
 690                                        if let Some(tooltip) = hovered_hint_part.tooltip {
 691                                            hover_popover::hover_at_inlay(
 692                                                self,
 693                                                InlayHover {
 694                                                    tooltip: match tooltip {
 695                                                        InlayHintLabelPartTooltip::String(text) => {
 696                                                            HoverBlock {
 697                                                                text,
 698                                                                kind: HoverBlockKind::PlainText,
 699                                                            }
 700                                                        }
 701                                                        InlayHintLabelPartTooltip::MarkupContent(
 702                                                            content,
 703                                                        ) => HoverBlock {
 704                                                            text: content.value,
 705                                                            kind: content.kind,
 706                                                        },
 707                                                    },
 708                                                    range: highlight.clone(),
 709                                                },
 710                                                window,
 711                                                cx,
 712                                            );
 713                                            hover_updated = true;
 714                                        }
 715                                        if let Some((language_server_id, location)) =
 716                                            hovered_hint_part.location
 717                                            && secondary_held
 718                                            && !self.has_pending_nonempty_selection()
 719                                        {
 720                                            go_to_definition_updated = true;
 721                                            show_link_definition(
 722                                                shift_held,
 723                                                self,
 724                                                TriggerPoint::InlayHint(
 725                                                    highlight,
 726                                                    location,
 727                                                    language_server_id,
 728                                                ),
 729                                                snapshot,
 730                                                window,
 731                                                cx,
 732                                            );
 733                                        }
 734                                    }
 735                                }
 736                            };
 737                        }
 738                        ResolveState::CanResolve(_, _) => debug_panic!(
 739                            "Expected resolved_hint retrieval to return a resolved hint"
 740                        ),
 741                        ResolveState::Resolving => {}
 742                    }
 743                }
 744            }
 745        }
 746
 747        if !go_to_definition_updated {
 748            self.hide_hovered_link(cx)
 749        }
 750        if !hover_updated {
 751            hover_popover::hover_at(self, None, window, cx);
 752        }
 753    }
 754
 755    fn inlay_hints_for_buffer(
 756        &mut self,
 757        invalidate_cache: InvalidationStrategy,
 758        buffer_excerpts: VisibleExcerpts,
 759        known_chunks: Option<(Global, HashSet<Range<BufferRow>>)>,
 760        cx: &mut Context<Self>,
 761    ) -> Option<Vec<Task<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>>> {
 762        let semantics_provider = self.semantics_provider()?;
 763
 764        let new_hint_tasks = semantics_provider
 765            .inlay_hints(
 766                invalidate_cache,
 767                buffer_excerpts.buffer,
 768                buffer_excerpts.ranges,
 769                known_chunks,
 770                cx,
 771            )
 772            .unwrap_or_default();
 773
 774        let mut hint_tasks = None;
 775        for (row_range, new_hints_task) in new_hint_tasks {
 776            hint_tasks
 777                .get_or_insert_with(Vec::new)
 778                .push(cx.spawn(async move |_, _| (row_range, new_hints_task.await)));
 779        }
 780        hint_tasks
 781    }
 782
 783    fn apply_fetched_hints(
 784        &mut self,
 785        buffer_id: BufferId,
 786        query_version: Global,
 787        invalidate_cache: InvalidationStrategy,
 788        new_hints: Vec<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>,
 789        cx: &mut Context<Self>,
 790    ) {
 791        let visible_inlay_hint_ids = Self::visible_inlay_hints(self.display_map.read(cx))
 792            .filter(|inlay| inlay.position.text_anchor.buffer_id == Some(buffer_id))
 793            .map(|inlay| inlay.id)
 794            .collect::<Vec<_>>();
 795        let Some(inlay_hints) = &mut self.inlay_hints else {
 796            return;
 797        };
 798
 799        let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
 800        let Some(buffer_snapshot) = self
 801            .buffer
 802            .read(cx)
 803            .buffer(buffer_id)
 804            .map(|buffer| buffer.read(cx).snapshot())
 805        else {
 806            return;
 807        };
 808
 809        let mut hints_to_remove = Vec::new();
 810
 811        // If we've received hints from the cache, it means `invalidate_cache` had invalidated whatever possible there,
 812        // and most probably there are no more hints with IDs from `visible_inlay_hint_ids` in the cache.
 813        // So, if we hover such hints, no resolve will happen.
 814        //
 815        // Another issue is in the fact that changing one buffer may lead to other buffers' hints changing, so more cache entries may be removed.
 816        // Hence, clear all excerpts' hints in the multi buffer: later, the invalidated ones will re-trigger the LSP query, the rest will be restored
 817        // from the cache.
 818        if invalidate_cache.should_invalidate() {
 819            hints_to_remove.extend(visible_inlay_hint_ids);
 820
 821            // When invalidating, this task removes ALL visible hints for the buffer
 822            // but only adds back hints for its own chunk ranges. Chunks fetched by
 823            // other concurrent tasks (e.g., a scroll task that completed before this
 824            // edit task) would have their hints removed but remain marked as "already
 825            // fetched" in hint_chunk_fetching, preventing re-fetch on the next
 826            // NewLinesShown. Fix: retain only chunks that this task has results for.
 827            let task_chunk_ranges: HashSet<&Range<BufferRow>> =
 828                new_hints.iter().map(|(range, _)| range).collect();
 829            if let Some((_, fetched_chunks)) = inlay_hints.hint_chunk_fetching.get_mut(&buffer_id) {
 830                fetched_chunks.retain(|chunk| task_chunk_ranges.contains(chunk));
 831            }
 832        }
 833
 834        let mut inserted_hint_text = HashMap::default();
 835        let new_hints = new_hints
 836            .into_iter()
 837            .filter_map(|(chunk_range, hints_result)| {
 838                let chunks_fetched = inlay_hints.hint_chunk_fetching.get_mut(&buffer_id);
 839                match hints_result {
 840                    Ok(new_hints) => {
 841                        if new_hints.is_empty() {
 842                            if let Some((_, chunks_fetched)) = chunks_fetched {
 843                                chunks_fetched.remove(&chunk_range);
 844                            }
 845                        }
 846                        Some(new_hints)
 847                    }
 848                    Err(e) => {
 849                        log::error!(
 850                            "Failed to query inlays for buffer row range {chunk_range:?}, {e:#}"
 851                        );
 852                        if let Some((for_version, chunks_fetched)) = chunks_fetched {
 853                            if for_version == &query_version {
 854                                chunks_fetched.remove(&chunk_range);
 855                            }
 856                        }
 857                        None
 858                    }
 859                }
 860            })
 861            .flat_map(|new_hints| {
 862                let mut hints_deduplicated = Vec::new();
 863
 864                if new_hints.len() > 1 {
 865                    for (server_id, new_hints) in new_hints {
 866                        for (new_id, new_hint) in new_hints {
 867                            let hints_text_for_position = inserted_hint_text
 868                                .entry(new_hint.position)
 869                                .or_insert_with(HashMap::default);
 870                            let insert =
 871                                match hints_text_for_position.entry(new_hint.text().to_string()) {
 872                                    hash_map::Entry::Occupied(o) => o.get() == &server_id,
 873                                    hash_map::Entry::Vacant(v) => {
 874                                        v.insert(server_id);
 875                                        true
 876                                    }
 877                                };
 878
 879                            if insert {
 880                                hints_deduplicated.push((new_id, new_hint));
 881                            }
 882                        }
 883                    }
 884                } else {
 885                    hints_deduplicated.extend(new_hints.into_values().flatten());
 886                }
 887
 888                hints_deduplicated
 889            })
 890            .filter(|(hint_id, lsp_hint)| {
 891                inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
 892                    && inlay_hints
 893                        .added_hints
 894                        .insert(*hint_id, lsp_hint.kind)
 895                        .is_none()
 896            })
 897            .sorted_by(|(_, a), (_, b)| a.position.cmp(&b.position, &buffer_snapshot))
 898            .collect::<Vec<_>>();
 899
 900        let hints_to_insert = multi_buffer_snapshot
 901            .text_anchors_to_visible_anchors(
 902                new_hints.iter().map(|(_, lsp_hint)| lsp_hint.position),
 903            )
 904            .into_iter()
 905            .zip(&new_hints)
 906            .filter_map(|(position, (hint_id, hint))| Some(Inlay::hint(*hint_id, position?, &hint)))
 907            .collect();
 908        let invalidate_hints_for_buffers =
 909            std::mem::take(&mut inlay_hints.invalidate_hints_for_buffers);
 910        if !invalidate_hints_for_buffers.is_empty() {
 911            hints_to_remove.extend(
 912                Self::visible_inlay_hints(self.display_map.read(cx))
 913                    .filter(|inlay| {
 914                        inlay
 915                            .position
 916                            .text_anchor
 917                            .buffer_id
 918                            .is_none_or(|buffer_id| {
 919                                invalidate_hints_for_buffers.contains(&buffer_id)
 920                            })
 921                    })
 922                    .map(|inlay| inlay.id),
 923            );
 924        }
 925
 926        self.splice_inlays(&hints_to_remove, hints_to_insert, cx);
 927    }
 928}
 929
 930#[derive(Debug)]
 931struct VisibleExcerpts {
 932    ranges: Vec<Range<text::Anchor>>,
 933    buffer_version: Global,
 934    buffer: Entity<language::Buffer>,
 935}
 936
 937fn spawn_editor_hints_refresh(
 938    buffer_id: BufferId,
 939    invalidate_cache: InvalidationStrategy,
 940    debounce: Option<Duration>,
 941    buffer_excerpts: VisibleExcerpts,
 942    known_chunks: Option<(Global, HashSet<Range<BufferRow>>)>,
 943    applicable_chunks: Vec<Range<BufferRow>>,
 944    cx: &mut Context<'_, Editor>,
 945) -> Task<()> {
 946    cx.spawn(async move |editor, cx| {
 947        if let Some(debounce) = debounce {
 948            cx.background_executor().timer(debounce).await;
 949        }
 950
 951        let query_version = buffer_excerpts.buffer_version.clone();
 952        let Some(hint_tasks) = editor
 953            .update(cx, |editor, cx| {
 954                editor.inlay_hints_for_buffer(invalidate_cache, buffer_excerpts, known_chunks, cx)
 955            })
 956            .ok()
 957        else {
 958            return;
 959        };
 960        let hint_tasks = hint_tasks.unwrap_or_default();
 961        if hint_tasks.is_empty() {
 962            editor
 963                .update(cx, |editor, _| {
 964                    if let Some((_, hint_chunk_fetching)) = editor
 965                        .inlay_hints
 966                        .as_mut()
 967                        .and_then(|inlay_hints| inlay_hints.hint_chunk_fetching.get_mut(&buffer_id))
 968                    {
 969                        for applicable_chunks in &applicable_chunks {
 970                            hint_chunk_fetching.remove(applicable_chunks);
 971                        }
 972                    }
 973                })
 974                .ok();
 975            return;
 976        }
 977        let new_hints = join_all(hint_tasks).await;
 978        editor
 979            .update(cx, |editor, cx| {
 980                editor.apply_fetched_hints(
 981                    buffer_id,
 982                    query_version,
 983                    invalidate_cache,
 984                    new_hints,
 985                    cx,
 986                );
 987            })
 988            .ok();
 989    })
 990}
 991
 992#[cfg(test)]
 993pub mod tests {
 994    use crate::editor_tests::update_test_language_settings;
 995    use crate::inlays::inlay_hints::InlayHintRefreshReason;
 996    use crate::scroll::Autoscroll;
 997    use crate::scroll::ScrollAmount;
 998    use crate::{Editor, SelectionEffects};
 999    use collections::HashSet;
1000    use futures::{StreamExt, future};
1001    use gpui::{AppContext as _, Context, TestAppContext, WindowHandle};
1002    use itertools::Itertools as _;
1003    use language::language_settings::InlayHintKind;
1004    use language::{Capability, FakeLspAdapter};
1005    use language::{Language, LanguageConfig, LanguageMatcher};
1006    use languages::rust_lang;
1007    use lsp::{DEFAULT_LSP_REQUEST_TIMEOUT, FakeLanguageServer};
1008    use multi_buffer::{MultiBuffer, MultiBufferOffset, PathKey};
1009    use parking_lot::Mutex;
1010    use pretty_assertions::assert_eq;
1011    use project::{FakeFs, Project};
1012    use serde_json::json;
1013    use settings::{AllLanguageSettingsContent, InlayHintSettingsContent, SettingsStore};
1014    use std::ops::Range;
1015    use std::sync::Arc;
1016    use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
1017    use std::time::Duration;
1018    use text::{OffsetRangeExt, Point};
1019    use ui::App;
1020    use util::path;
1021    use util::paths::natural_sort;
1022
1023    #[gpui::test]
1024    async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
1025        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1026        init_test(cx, &|settings| {
1027            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1028                show_value_hints: Some(true),
1029                enabled: Some(true),
1030                edit_debounce_ms: Some(0),
1031                scroll_debounce_ms: Some(0),
1032                show_type_hints: Some(allowed_hint_kinds.contains(&Some(InlayHintKind::Type))),
1033                show_parameter_hints: Some(
1034                    allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1035                ),
1036                show_other_hints: Some(allowed_hint_kinds.contains(&None)),
1037                show_background: Some(false),
1038                toggle_on_modifiers_press: None,
1039            })
1040        });
1041        let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1042            let lsp_request_count = Arc::new(AtomicU32::new(0));
1043            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1044                move |params, _| {
1045                    let task_lsp_request_count = Arc::clone(&lsp_request_count);
1046                    async move {
1047                        let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1;
1048                        assert_eq!(
1049                            params.text_document.uri,
1050                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
1051                        );
1052                        Ok(Some(vec![lsp::InlayHint {
1053                            position: lsp::Position::new(0, i),
1054                            label: lsp::InlayHintLabel::String(i.to_string()),
1055                            kind: None,
1056                            text_edits: None,
1057                            tooltip: None,
1058                            padding_left: None,
1059                            padding_right: None,
1060                            data: None,
1061                        }]))
1062                    }
1063                },
1064            );
1065        })
1066        .await;
1067        cx.executor().run_until_parked();
1068
1069        editor
1070            .update(cx, |editor, _window, cx| {
1071                let expected_hints = vec!["1".to_string()];
1072                assert_eq!(
1073                    expected_hints,
1074                    cached_hint_labels(editor, cx),
1075                    "Should get its first hints when opening the editor"
1076                );
1077                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1078                assert_eq!(
1079                    allowed_hint_kinds_for_editor(editor),
1080                    allowed_hint_kinds,
1081                    "Cache should use editor settings to get the allowed hint kinds"
1082                );
1083            })
1084            .unwrap();
1085
1086        editor
1087            .update(cx, |editor, window, cx| {
1088                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1089                    s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1090                });
1091                editor.handle_input("some change", window, cx);
1092            })
1093            .unwrap();
1094        cx.executor().run_until_parked();
1095        editor
1096            .update(cx, |editor, _window, cx| {
1097                let expected_hints = vec!["2".to_string()];
1098                assert_eq!(
1099                    expected_hints,
1100                    cached_hint_labels(editor, cx),
1101                    "Should get new hints after an edit"
1102                );
1103                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1104                assert_eq!(
1105                    allowed_hint_kinds_for_editor(editor),
1106                    allowed_hint_kinds,
1107                    "Cache should use editor settings to get the allowed hint kinds"
1108                );
1109            })
1110            .unwrap();
1111
1112        fake_server
1113            .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1114            .await
1115            .into_response()
1116            .expect("inlay refresh request failed");
1117        cx.executor().run_until_parked();
1118        editor
1119            .update(cx, |editor, _window, cx| {
1120                let expected_hints = vec!["3".to_string()];
1121                assert_eq!(
1122                    expected_hints,
1123                    cached_hint_labels(editor, cx),
1124                    "Should get new hints after hint refresh/ request"
1125                );
1126                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1127                assert_eq!(
1128                    allowed_hint_kinds_for_editor(editor),
1129                    allowed_hint_kinds,
1130                    "Cache should use editor settings to get the allowed hint kinds"
1131                );
1132            })
1133            .unwrap();
1134    }
1135
1136    #[gpui::test]
1137    async fn test_racy_cache_updates(cx: &mut gpui::TestAppContext) {
1138        init_test(cx, &|settings| {
1139            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1140                enabled: Some(true),
1141                ..InlayHintSettingsContent::default()
1142            })
1143        });
1144        let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1145            let lsp_request_count = Arc::new(AtomicU32::new(0));
1146            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1147                move |params, _| {
1148                    let task_lsp_request_count = Arc::clone(&lsp_request_count);
1149                    async move {
1150                        let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1;
1151                        assert_eq!(
1152                            params.text_document.uri,
1153                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
1154                        );
1155                        Ok(Some(vec![lsp::InlayHint {
1156                            position: lsp::Position::new(0, i),
1157                            label: lsp::InlayHintLabel::String(i.to_string()),
1158                            kind: Some(lsp::InlayHintKind::TYPE),
1159                            text_edits: None,
1160                            tooltip: None,
1161                            padding_left: None,
1162                            padding_right: None,
1163                            data: None,
1164                        }]))
1165                    }
1166                },
1167            );
1168        })
1169        .await;
1170        cx.executor().advance_clock(Duration::from_secs(1));
1171        cx.executor().run_until_parked();
1172
1173        editor
1174            .update(cx, |editor, _window, cx| {
1175                let expected_hints = vec!["1".to_string()];
1176                assert_eq!(
1177                    expected_hints,
1178                    cached_hint_labels(editor, cx),
1179                    "Should get its first hints when opening the editor"
1180                );
1181                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1182            })
1183            .unwrap();
1184
1185        // Emulate simultaneous events: both editing, refresh and, slightly after, scroll updates are triggered.
1186        editor
1187            .update(cx, |editor, window, cx| {
1188                editor.handle_input("foo", window, cx);
1189            })
1190            .unwrap();
1191        cx.executor().advance_clock(Duration::from_millis(5));
1192        editor
1193            .update(cx, |editor, _window, cx| {
1194                editor.refresh_inlay_hints(
1195                    InlayHintRefreshReason::RefreshRequested {
1196                        server_id: fake_server.server.server_id(),
1197                        request_id: Some(1),
1198                    },
1199                    cx,
1200                );
1201            })
1202            .unwrap();
1203        cx.executor().advance_clock(Duration::from_millis(5));
1204        editor
1205            .update(cx, |editor, _window, cx| {
1206                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
1207            })
1208            .unwrap();
1209        cx.executor().advance_clock(Duration::from_secs(1));
1210        cx.executor().run_until_parked();
1211        editor
1212            .update(cx, |editor, _window, cx| {
1213                let expected_hints = vec!["2".to_string()];
1214                assert_eq!(expected_hints, cached_hint_labels(editor, cx), "Despite multiple simultaneous refreshes, only one inlay hint query should be issued");
1215                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1216            })
1217            .unwrap();
1218    }
1219
1220    #[gpui::test]
1221    async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
1222        init_test(cx, &|settings| {
1223            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1224                show_value_hints: Some(true),
1225                enabled: Some(true),
1226                edit_debounce_ms: Some(0),
1227                scroll_debounce_ms: Some(0),
1228                show_type_hints: Some(true),
1229                show_parameter_hints: Some(true),
1230                show_other_hints: Some(true),
1231                show_background: Some(false),
1232                toggle_on_modifiers_press: None,
1233            })
1234        });
1235
1236        let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1237            let lsp_request_count = Arc::new(AtomicU32::new(0));
1238            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1239                move |params, _| {
1240                    let task_lsp_request_count = Arc::clone(&lsp_request_count);
1241                    async move {
1242                        assert_eq!(
1243                            params.text_document.uri,
1244                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
1245                        );
1246                        let current_call_id =
1247                            Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1248                        Ok(Some(vec![lsp::InlayHint {
1249                            position: lsp::Position::new(0, current_call_id),
1250                            label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1251                            kind: None,
1252                            text_edits: None,
1253                            tooltip: None,
1254                            padding_left: None,
1255                            padding_right: None,
1256                            data: None,
1257                        }]))
1258                    }
1259                },
1260            );
1261        })
1262        .await;
1263        cx.executor().run_until_parked();
1264
1265        editor
1266            .update(cx, |editor, _, cx| {
1267                let expected_hints = vec!["0".to_string()];
1268                assert_eq!(
1269                    expected_hints,
1270                    cached_hint_labels(editor, cx),
1271                    "Should get its first hints when opening the editor"
1272                );
1273                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1274            })
1275            .unwrap();
1276
1277        let progress_token = 42;
1278        fake_server
1279            .request::<lsp::request::WorkDoneProgressCreate>(
1280                lsp::WorkDoneProgressCreateParams {
1281                    token: lsp::ProgressToken::Number(progress_token),
1282                },
1283                DEFAULT_LSP_REQUEST_TIMEOUT,
1284            )
1285            .await
1286            .into_response()
1287            .expect("work done progress create request failed");
1288        cx.executor().run_until_parked();
1289        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1290            token: lsp::ProgressToken::Number(progress_token),
1291            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1292                lsp::WorkDoneProgressBegin::default(),
1293            )),
1294        });
1295        cx.executor().run_until_parked();
1296
1297        editor
1298            .update(cx, |editor, _, cx| {
1299                let expected_hints = vec!["0".to_string()];
1300                assert_eq!(
1301                    expected_hints,
1302                    cached_hint_labels(editor, cx),
1303                    "Should not update hints while the work task is running"
1304                );
1305                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1306            })
1307            .unwrap();
1308
1309        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1310            token: lsp::ProgressToken::Number(progress_token),
1311            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1312                lsp::WorkDoneProgressEnd::default(),
1313            )),
1314        });
1315        cx.executor().run_until_parked();
1316
1317        editor
1318            .update(cx, |editor, _, cx| {
1319                let expected_hints = vec!["1".to_string()];
1320                assert_eq!(
1321                    expected_hints,
1322                    cached_hint_labels(editor, cx),
1323                    "New hints should be queried after the work task is done"
1324                );
1325                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1326            })
1327            .unwrap();
1328    }
1329
1330    #[gpui::test]
1331    async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1332        init_test(cx, &|settings| {
1333            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1334                show_value_hints: Some(true),
1335                enabled: Some(true),
1336                edit_debounce_ms: Some(0),
1337                scroll_debounce_ms: Some(0),
1338                show_type_hints: Some(true),
1339                show_parameter_hints: Some(true),
1340                show_other_hints: Some(true),
1341                show_background: Some(false),
1342                toggle_on_modifiers_press: None,
1343            })
1344        });
1345
1346        let fs = FakeFs::new(cx.background_executor.clone());
1347        fs.insert_tree(
1348            path!("/a"),
1349            json!({
1350                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1351                "other.md": "Test md file with some text",
1352            }),
1353        )
1354        .await;
1355
1356        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1357
1358        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1359        let mut rs_fake_servers = None;
1360        let mut md_fake_servers = None;
1361        for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1362            language_registry.add(Arc::new(Language::new(
1363                LanguageConfig {
1364                    name: name.into(),
1365                    matcher: LanguageMatcher {
1366                        path_suffixes: vec![path_suffix.to_string()],
1367                        ..Default::default()
1368                    },
1369                    ..Default::default()
1370                },
1371                Some(tree_sitter_rust::LANGUAGE.into()),
1372            )));
1373            let fake_servers = language_registry.register_fake_lsp(
1374                name,
1375                FakeLspAdapter {
1376                    name,
1377                    capabilities: lsp::ServerCapabilities {
1378                        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1379                        ..Default::default()
1380                    },
1381                    initializer: Some(Box::new({
1382                        move |fake_server| {
1383                            let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1384                            let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1385                            fake_server
1386                                .set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1387                                    move |params, _| {
1388                                        let i = match name {
1389                                            "Rust" => {
1390                                                assert_eq!(
1391                                                    params.text_document.uri,
1392                                                    lsp::Uri::from_file_path(path!("/a/main.rs"))
1393                                                        .unwrap(),
1394                                                );
1395                                                rs_lsp_request_count.fetch_add(1, Ordering::Release)
1396                                                    + 1
1397                                            }
1398                                            "Markdown" => {
1399                                                assert_eq!(
1400                                                    params.text_document.uri,
1401                                                    lsp::Uri::from_file_path(path!("/a/other.md"))
1402                                                        .unwrap(),
1403                                                );
1404                                                md_lsp_request_count.fetch_add(1, Ordering::Release)
1405                                                    + 1
1406                                            }
1407                                            unexpected => {
1408                                                panic!("Unexpected language: {unexpected}")
1409                                            }
1410                                        };
1411
1412                                        async move {
1413                                            let query_start = params.range.start;
1414                                            Ok(Some(vec![lsp::InlayHint {
1415                                                position: query_start,
1416                                                label: lsp::InlayHintLabel::String(i.to_string()),
1417                                                kind: None,
1418                                                text_edits: None,
1419                                                tooltip: None,
1420                                                padding_left: None,
1421                                                padding_right: None,
1422                                                data: None,
1423                                            }]))
1424                                        }
1425                                    },
1426                                );
1427                        }
1428                    })),
1429                    ..Default::default()
1430                },
1431            );
1432            match name {
1433                "Rust" => rs_fake_servers = Some(fake_servers),
1434                "Markdown" => md_fake_servers = Some(fake_servers),
1435                _ => unreachable!(),
1436            }
1437        }
1438
1439        let rs_buffer = project
1440            .update(cx, |project, cx| {
1441                project.open_local_buffer(path!("/a/main.rs"), cx)
1442            })
1443            .await
1444            .unwrap();
1445        let rs_editor = cx.add_window(|window, cx| {
1446            Editor::for_buffer(rs_buffer, Some(project.clone()), window, cx)
1447        });
1448        cx.executor().run_until_parked();
1449
1450        let _rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1451        cx.executor().run_until_parked();
1452
1453        // Establish a viewport so the editor considers itself visible and the hint refresh
1454        // pipeline runs. Then explicitly trigger a refresh.
1455        rs_editor
1456            .update(cx, |editor, window, cx| {
1457                editor.set_visible_line_count(50.0, window, cx);
1458                editor.set_visible_column_count(120.0);
1459                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
1460            })
1461            .unwrap();
1462        cx.executor().run_until_parked();
1463        rs_editor
1464            .update(cx, |editor, _window, cx| {
1465                let expected_hints = vec!["1".to_string()];
1466                assert_eq!(
1467                    expected_hints,
1468                    cached_hint_labels(editor, cx),
1469                    "Should get its first hints when opening the editor"
1470                );
1471                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1472            })
1473            .unwrap();
1474
1475        cx.executor().run_until_parked();
1476        let md_buffer = project
1477            .update(cx, |project, cx| {
1478                project.open_local_buffer(path!("/a/other.md"), cx)
1479            })
1480            .await
1481            .unwrap();
1482        let md_editor =
1483            cx.add_window(|window, cx| Editor::for_buffer(md_buffer, Some(project), window, cx));
1484        cx.executor().run_until_parked();
1485
1486        let _md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1487        cx.executor().run_until_parked();
1488
1489        // Establish a viewport so the editor considers itself visible and the hint refresh
1490        // pipeline runs. Then explicitly trigger a refresh.
1491        md_editor
1492            .update(cx, |editor, window, cx| {
1493                editor.set_visible_line_count(50.0, window, cx);
1494                editor.set_visible_column_count(120.0);
1495                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
1496            })
1497            .unwrap();
1498        cx.executor().run_until_parked();
1499        md_editor
1500            .update(cx, |editor, _window, cx| {
1501                let expected_hints = vec!["1".to_string()];
1502                assert_eq!(
1503                    expected_hints,
1504                    cached_hint_labels(editor, cx),
1505                    "Markdown editor should have a separate version, repeating Rust editor rules"
1506                );
1507                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1508            })
1509            .unwrap();
1510
1511        rs_editor
1512            .update(cx, |editor, window, cx| {
1513                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1514                    s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1515                });
1516                editor.handle_input("some rs change", window, cx);
1517            })
1518            .unwrap();
1519        cx.executor().run_until_parked();
1520        rs_editor
1521            .update(cx, |editor, _window, cx| {
1522                let expected_hints = vec!["2".to_string()];
1523                assert_eq!(
1524                    expected_hints,
1525                    cached_hint_labels(editor, cx),
1526                    "Rust inlay cache should change after the edit"
1527                );
1528                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1529            })
1530            .unwrap();
1531        md_editor
1532            .update(cx, |editor, _window, cx| {
1533                let expected_hints = vec!["1".to_string()];
1534                assert_eq!(
1535                    expected_hints,
1536                    cached_hint_labels(editor, cx),
1537                    "Markdown editor should not be affected by Rust editor changes"
1538                );
1539                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1540            })
1541            .unwrap();
1542
1543        md_editor
1544            .update(cx, |editor, window, cx| {
1545                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1546                    s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1547                });
1548                editor.handle_input("some md change", window, cx);
1549            })
1550            .unwrap();
1551        cx.executor().run_until_parked();
1552        md_editor
1553            .update(cx, |editor, _window, cx| {
1554                let expected_hints = vec!["2".to_string()];
1555                assert_eq!(
1556                    expected_hints,
1557                    cached_hint_labels(editor, cx),
1558                    "Rust editor should not be affected by Markdown editor changes"
1559                );
1560                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1561            })
1562            .unwrap();
1563        rs_editor
1564            .update(cx, |editor, _window, cx| {
1565                let expected_hints = vec!["2".to_string()];
1566                assert_eq!(
1567                    expected_hints,
1568                    cached_hint_labels(editor, cx),
1569                    "Markdown editor should also change independently"
1570                );
1571                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1572            })
1573            .unwrap();
1574    }
1575
1576    #[gpui::test]
1577    async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1578        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1579        init_test(cx, &|settings| {
1580            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1581                show_value_hints: Some(true),
1582                enabled: Some(true),
1583                edit_debounce_ms: Some(0),
1584                scroll_debounce_ms: Some(0),
1585                show_type_hints: Some(allowed_hint_kinds.contains(&Some(InlayHintKind::Type))),
1586                show_parameter_hints: Some(
1587                    allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1588                ),
1589                show_other_hints: Some(allowed_hint_kinds.contains(&None)),
1590                show_background: Some(false),
1591                toggle_on_modifiers_press: None,
1592            })
1593        });
1594
1595        let lsp_request_count = Arc::new(AtomicUsize::new(0));
1596        let (_, editor, fake_server) = prepare_test_objects(cx, {
1597            let lsp_request_count = lsp_request_count.clone();
1598            move |fake_server, file_with_hints| {
1599                let lsp_request_count = lsp_request_count.clone();
1600                fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1601                    move |params, _| {
1602                        lsp_request_count.fetch_add(1, Ordering::Release);
1603                        async move {
1604                            assert_eq!(
1605                                params.text_document.uri,
1606                                lsp::Uri::from_file_path(file_with_hints).unwrap(),
1607                            );
1608                            Ok(Some(vec![
1609                                lsp::InlayHint {
1610                                    position: lsp::Position::new(0, 1),
1611                                    label: lsp::InlayHintLabel::String("type hint".to_string()),
1612                                    kind: Some(lsp::InlayHintKind::TYPE),
1613                                    text_edits: None,
1614                                    tooltip: None,
1615                                    padding_left: None,
1616                                    padding_right: None,
1617                                    data: None,
1618                                },
1619                                lsp::InlayHint {
1620                                    position: lsp::Position::new(0, 2),
1621                                    label: lsp::InlayHintLabel::String(
1622                                        "parameter hint".to_string(),
1623                                    ),
1624                                    kind: Some(lsp::InlayHintKind::PARAMETER),
1625                                    text_edits: None,
1626                                    tooltip: None,
1627                                    padding_left: None,
1628                                    padding_right: None,
1629                                    data: None,
1630                                },
1631                                lsp::InlayHint {
1632                                    position: lsp::Position::new(0, 3),
1633                                    label: lsp::InlayHintLabel::String("other hint".to_string()),
1634                                    kind: None,
1635                                    text_edits: None,
1636                                    tooltip: None,
1637                                    padding_left: None,
1638                                    padding_right: None,
1639                                    data: None,
1640                                },
1641                            ]))
1642                        }
1643                    },
1644                );
1645            }
1646        })
1647        .await;
1648        cx.executor().run_until_parked();
1649
1650        editor
1651            .update(cx, |editor, _, cx| {
1652                assert_eq!(
1653                    lsp_request_count.load(Ordering::Relaxed),
1654                    1,
1655                    "Should query new hints once"
1656                );
1657                assert_eq!(
1658                    vec![
1659                        "type hint".to_string(),
1660                        "parameter hint".to_string(),
1661                        "other hint".to_string(),
1662                    ],
1663                    cached_hint_labels(editor, cx),
1664                    "Should get its first hints when opening the editor"
1665                );
1666                assert_eq!(
1667                    vec!["type hint".to_string(), "other hint".to_string()],
1668                    visible_hint_labels(editor, cx)
1669                );
1670                assert_eq!(
1671                    allowed_hint_kinds_for_editor(editor),
1672                    allowed_hint_kinds,
1673                    "Cache should use editor settings to get the allowed hint kinds"
1674                );
1675            })
1676            .unwrap();
1677
1678        fake_server
1679            .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1680            .await
1681            .into_response()
1682            .expect("inlay refresh request failed");
1683        cx.executor().run_until_parked();
1684        editor
1685            .update(cx, |editor, _, cx| {
1686                assert_eq!(
1687                    lsp_request_count.load(Ordering::Relaxed),
1688                    2,
1689                    "Should load new hints twice"
1690                );
1691                assert_eq!(
1692                    vec![
1693                        "type hint".to_string(),
1694                        "parameter hint".to_string(),
1695                        "other hint".to_string(),
1696                    ],
1697                    cached_hint_labels(editor, cx),
1698                    "Cached hints should not change due to allowed hint kinds settings update"
1699                );
1700                assert_eq!(
1701                    vec!["type hint".to_string(), "other hint".to_string()],
1702                    visible_hint_labels(editor, cx)
1703                );
1704            })
1705            .unwrap();
1706
1707        for (new_allowed_hint_kinds, expected_visible_hints) in [
1708            (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1709            (
1710                HashSet::from_iter([Some(InlayHintKind::Type)]),
1711                vec!["type hint".to_string()],
1712            ),
1713            (
1714                HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1715                vec!["parameter hint".to_string()],
1716            ),
1717            (
1718                HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1719                vec!["type hint".to_string(), "other hint".to_string()],
1720            ),
1721            (
1722                HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1723                vec!["parameter hint".to_string(), "other hint".to_string()],
1724            ),
1725            (
1726                HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1727                vec!["type hint".to_string(), "parameter hint".to_string()],
1728            ),
1729            (
1730                HashSet::from_iter([
1731                    None,
1732                    Some(InlayHintKind::Type),
1733                    Some(InlayHintKind::Parameter),
1734                ]),
1735                vec![
1736                    "type hint".to_string(),
1737                    "parameter hint".to_string(),
1738                    "other hint".to_string(),
1739                ],
1740            ),
1741        ] {
1742            update_test_language_settings(cx, &|settings| {
1743                settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1744                    show_value_hints: Some(true),
1745                    enabled: Some(true),
1746                    edit_debounce_ms: Some(0),
1747                    scroll_debounce_ms: Some(0),
1748                    show_type_hints: Some(
1749                        new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1750                    ),
1751                    show_parameter_hints: Some(
1752                        new_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1753                    ),
1754                    show_other_hints: Some(new_allowed_hint_kinds.contains(&None)),
1755                    show_background: Some(false),
1756                    toggle_on_modifiers_press: None,
1757                })
1758            });
1759            cx.executor().run_until_parked();
1760            editor.update(cx, |editor, _, cx| {
1761                assert_eq!(
1762                    lsp_request_count.load(Ordering::Relaxed),
1763                    2,
1764                    "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1765                );
1766                assert_eq!(
1767                    vec![
1768                        "type hint".to_string(),
1769                        "parameter hint".to_string(),
1770                        "other hint".to_string(),
1771                    ],
1772                    cached_hint_labels(editor, cx),
1773                    "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1774                );
1775                assert_eq!(
1776                    expected_visible_hints,
1777                    visible_hint_labels(editor, cx),
1778                    "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1779                );
1780                assert_eq!(
1781                    allowed_hint_kinds_for_editor(editor),
1782                    new_allowed_hint_kinds,
1783                    "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1784                );
1785            }).unwrap();
1786        }
1787
1788        let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1789        update_test_language_settings(cx, &|settings| {
1790            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1791                show_value_hints: Some(true),
1792                enabled: Some(false),
1793                edit_debounce_ms: Some(0),
1794                scroll_debounce_ms: Some(0),
1795                show_type_hints: Some(
1796                    another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1797                ),
1798                show_parameter_hints: Some(
1799                    another_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1800                ),
1801                show_other_hints: Some(another_allowed_hint_kinds.contains(&None)),
1802                show_background: Some(false),
1803                toggle_on_modifiers_press: None,
1804            })
1805        });
1806        cx.executor().run_until_parked();
1807        editor
1808            .update(cx, |editor, _, cx| {
1809                assert_eq!(
1810                    lsp_request_count.load(Ordering::Relaxed),
1811                    2,
1812                    "Should not load new hints when hints got disabled"
1813                );
1814                assert_eq!(
1815                    vec![
1816                        "type hint".to_string(),
1817                        "parameter hint".to_string(),
1818                        "other hint".to_string(),
1819                    ],
1820                    cached_hint_labels(editor, cx),
1821                    "Should not clear the cache when hints got disabled"
1822                );
1823                assert_eq!(
1824                    Vec::<String>::new(),
1825                    visible_hint_labels(editor, cx),
1826                    "Should clear visible hints when hints got disabled"
1827                );
1828                assert_eq!(
1829                    allowed_hint_kinds_for_editor(editor),
1830                    another_allowed_hint_kinds,
1831                    "Should update its allowed hint kinds even when hints got disabled"
1832                );
1833            })
1834            .unwrap();
1835
1836        fake_server
1837            .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1838            .await
1839            .into_response()
1840            .expect("inlay refresh request failed");
1841        cx.executor().run_until_parked();
1842        editor
1843            .update(cx, |editor, _window, cx| {
1844                assert_eq!(
1845                    lsp_request_count.load(Ordering::Relaxed),
1846                    2,
1847                    "Should not load new hints when they got disabled"
1848                );
1849                assert_eq!(
1850                    vec![
1851                        "type hint".to_string(),
1852                        "parameter hint".to_string(),
1853                        "other hint".to_string(),
1854                    ],
1855                    cached_hint_labels(editor, cx)
1856                );
1857                assert_eq!(Vec::<String>::new(), visible_hint_labels(editor, cx));
1858            })
1859            .unwrap();
1860
1861        let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1862        update_test_language_settings(cx, &|settings| {
1863            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1864                show_value_hints: Some(true),
1865                enabled: Some(true),
1866                edit_debounce_ms: Some(0),
1867                scroll_debounce_ms: Some(0),
1868                show_type_hints: Some(
1869                    final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1870                ),
1871                show_parameter_hints: Some(
1872                    final_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1873                ),
1874                show_other_hints: Some(final_allowed_hint_kinds.contains(&None)),
1875                show_background: Some(false),
1876                toggle_on_modifiers_press: None,
1877            })
1878        });
1879        cx.executor().run_until_parked();
1880        editor
1881            .update(cx, |editor, _, cx| {
1882                assert_eq!(
1883                    lsp_request_count.load(Ordering::Relaxed),
1884                    2,
1885                    "Should not query for new hints when they got re-enabled, as the file version did not change"
1886                );
1887                assert_eq!(
1888                    vec![
1889                        "type hint".to_string(),
1890                        "parameter hint".to_string(),
1891                        "other hint".to_string(),
1892                    ],
1893                    cached_hint_labels(editor, cx),
1894                    "Should get its cached hints fully repopulated after the hints got re-enabled"
1895                );
1896                assert_eq!(
1897                    vec!["parameter hint".to_string()],
1898                    visible_hint_labels(editor, cx),
1899                    "Should get its visible hints repopulated and filtered after the h"
1900                );
1901                assert_eq!(
1902                    allowed_hint_kinds_for_editor(editor),
1903                    final_allowed_hint_kinds,
1904                    "Cache should update editor settings when hints got re-enabled"
1905                );
1906            })
1907            .unwrap();
1908
1909        fake_server
1910            .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1911            .await
1912            .into_response()
1913            .expect("inlay refresh request failed");
1914        cx.executor().run_until_parked();
1915        editor
1916            .update(cx, |editor, _, cx| {
1917                assert_eq!(
1918                    lsp_request_count.load(Ordering::Relaxed),
1919                    3,
1920                    "Should query for new hints again"
1921                );
1922                assert_eq!(
1923                    vec![
1924                        "type hint".to_string(),
1925                        "parameter hint".to_string(),
1926                        "other hint".to_string(),
1927                    ],
1928                    cached_hint_labels(editor, cx),
1929                );
1930                assert_eq!(
1931                    vec!["parameter hint".to_string()],
1932                    visible_hint_labels(editor, cx),
1933                );
1934            })
1935            .unwrap();
1936    }
1937
1938    #[gpui::test]
1939    async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
1940        init_test(cx, &|settings| {
1941            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1942                show_value_hints: Some(true),
1943                enabled: Some(true),
1944                edit_debounce_ms: Some(0),
1945                scroll_debounce_ms: Some(0),
1946                show_type_hints: Some(true),
1947                show_parameter_hints: Some(true),
1948                show_other_hints: Some(true),
1949                show_background: Some(false),
1950                toggle_on_modifiers_press: None,
1951            })
1952        });
1953
1954        let lsp_request_count = Arc::new(AtomicU32::new(0));
1955        let (_, editor, _) = prepare_test_objects(cx, {
1956            let lsp_request_count = lsp_request_count.clone();
1957            move |fake_server, file_with_hints| {
1958                let lsp_request_count = lsp_request_count.clone();
1959                fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1960                    move |params, _| {
1961                        let lsp_request_count = lsp_request_count.clone();
1962                        async move {
1963                            let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
1964                            assert_eq!(
1965                                params.text_document.uri,
1966                                lsp::Uri::from_file_path(file_with_hints).unwrap(),
1967                            );
1968                            Ok(Some(vec![lsp::InlayHint {
1969                                position: lsp::Position::new(0, i),
1970                                label: lsp::InlayHintLabel::String(i.to_string()),
1971                                kind: None,
1972                                text_edits: None,
1973                                tooltip: None,
1974                                padding_left: None,
1975                                padding_right: None,
1976                                data: None,
1977                            }]))
1978                        }
1979                    },
1980                );
1981            }
1982        })
1983        .await;
1984
1985        let mut expected_changes = Vec::new();
1986        for change_after_opening in [
1987            "initial change #1",
1988            "initial change #2",
1989            "initial change #3",
1990        ] {
1991            editor
1992                .update(cx, |editor, window, cx| {
1993                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1994                        s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1995                    });
1996                    editor.handle_input(change_after_opening, window, cx);
1997                })
1998                .unwrap();
1999            expected_changes.push(change_after_opening);
2000        }
2001
2002        cx.executor().run_until_parked();
2003
2004        editor
2005            .update(cx, |editor, _window, cx| {
2006                let current_text = editor.text(cx);
2007                for change in &expected_changes {
2008                    assert!(
2009                        current_text.contains(change),
2010                        "Should apply all changes made"
2011                    );
2012                }
2013                assert_eq!(
2014                    lsp_request_count.load(Ordering::Relaxed),
2015                    2,
2016                    "Should query new hints twice: for editor init and for the last edit that interrupted all others"
2017                );
2018                let expected_hints = vec!["2".to_string()];
2019                assert_eq!(
2020                    expected_hints,
2021                    cached_hint_labels(editor, cx),
2022                    "Should get hints from the last edit landed only"
2023                );
2024                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2025            })
2026            .unwrap();
2027
2028        let mut edits = Vec::new();
2029        for async_later_change in [
2030            "another change #1",
2031            "another change #2",
2032            "another change #3",
2033        ] {
2034            expected_changes.push(async_later_change);
2035            let task_editor = editor;
2036            edits.push(cx.spawn(|mut cx| async move {
2037                task_editor
2038                    .update(&mut cx, |editor, window, cx| {
2039                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2040                            s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
2041                        });
2042                        editor.handle_input(async_later_change, window, cx);
2043                    })
2044                    .unwrap();
2045            }));
2046        }
2047        let _ = future::join_all(edits).await;
2048        cx.executor().run_until_parked();
2049
2050        editor
2051            .update(cx, |editor, _, cx| {
2052                let current_text = editor.text(cx);
2053                for change in &expected_changes {
2054                    assert!(
2055                        current_text.contains(change),
2056                        "Should apply all changes made"
2057                    );
2058                }
2059                assert_eq!(
2060                    lsp_request_count.load(Ordering::SeqCst),
2061                    3,
2062                    "Should query new hints one more time, for the last edit only"
2063                );
2064                let expected_hints = vec!["3".to_string()];
2065                assert_eq!(
2066                    expected_hints,
2067                    cached_hint_labels(editor, cx),
2068                    "Should get hints from the last edit landed only"
2069                );
2070                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2071            })
2072            .unwrap();
2073    }
2074
2075    #[gpui::test(iterations = 4)]
2076    async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
2077        init_test(cx, &|settings| {
2078            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2079                enabled: Some(true),
2080                ..InlayHintSettingsContent::default()
2081            })
2082        });
2083
2084        let fs = FakeFs::new(cx.background_executor.clone());
2085        fs.insert_tree(
2086            path!("/a"),
2087            json!({
2088                "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
2089                "other.rs": "// Test file",
2090            }),
2091        )
2092        .await;
2093
2094        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2095
2096        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2097        language_registry.add(rust_lang());
2098
2099        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2100        let lsp_request_count = Arc::new(AtomicUsize::new(0));
2101        let mut fake_servers = language_registry.register_fake_lsp(
2102            "Rust",
2103            FakeLspAdapter {
2104                capabilities: lsp::ServerCapabilities {
2105                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2106                    ..lsp::ServerCapabilities::default()
2107                },
2108                initializer: Some(Box::new({
2109                    let lsp_request_ranges = lsp_request_ranges.clone();
2110                    let lsp_request_count = lsp_request_count.clone();
2111                    move |fake_server| {
2112                        let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
2113                        let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2114                        fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2115                            move |params, _| {
2116                                let task_lsp_request_ranges =
2117                                    Arc::clone(&closure_lsp_request_ranges);
2118                                let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2119                                async move {
2120                                    assert_eq!(
2121                                        params.text_document.uri,
2122                                        lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2123                                    );
2124
2125                                    task_lsp_request_ranges.lock().push(params.range);
2126                                    task_lsp_request_count.fetch_add(1, Ordering::Release);
2127                                    Ok(Some(vec![lsp::InlayHint {
2128                                        position: params.range.start,
2129                                        label: lsp::InlayHintLabel::String(
2130                                            params.range.end.line.to_string(),
2131                                        ),
2132                                        kind: None,
2133                                        text_edits: None,
2134                                        tooltip: None,
2135                                        padding_left: None,
2136                                        padding_right: None,
2137                                        data: None,
2138                                    }]))
2139                                }
2140                            },
2141                        );
2142                    }
2143                })),
2144                ..FakeLspAdapter::default()
2145            },
2146        );
2147
2148        let buffer = project
2149            .update(cx, |project, cx| {
2150                project.open_local_buffer(path!("/a/main.rs"), cx)
2151            })
2152            .await
2153            .unwrap();
2154        let editor =
2155            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
2156        cx.executor().run_until_parked();
2157        let _fake_server = fake_servers.next().await.unwrap();
2158        cx.executor().advance_clock(Duration::from_millis(100));
2159        cx.executor().run_until_parked();
2160
2161        let ranges = lsp_request_ranges
2162            .lock()
2163            .drain(..)
2164            .sorted_by_key(|r| r.start)
2165            .collect::<Vec<_>>();
2166        assert_eq!(
2167            ranges.len(),
2168            1,
2169            "Should query 1 range initially, but got: {ranges:?}"
2170        );
2171
2172        editor
2173            .update(cx, |editor, window, cx| {
2174                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2175            })
2176            .unwrap();
2177        // Wait for the first hints request to fire off
2178        cx.executor().advance_clock(Duration::from_millis(100));
2179        cx.executor().run_until_parked();
2180        editor
2181            .update(cx, |editor, window, cx| {
2182                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2183            })
2184            .unwrap();
2185        cx.executor().advance_clock(Duration::from_millis(100));
2186        cx.executor().run_until_parked();
2187        let visible_range_after_scrolls = editor_visible_range(&editor, cx);
2188        let visible_line_count = editor
2189            .update(cx, |editor, _window, _| {
2190                editor.visible_line_count().unwrap()
2191            })
2192            .unwrap();
2193        let selection_in_cached_range = editor
2194            .update(cx, |editor, _window, cx| {
2195                let ranges = lsp_request_ranges
2196                    .lock()
2197                    .drain(..)
2198                    .sorted_by_key(|r| r.start)
2199                    .collect::<Vec<_>>();
2200                assert_eq!(
2201                    ranges.len(),
2202                    2,
2203                    "Should query 2 ranges after both scrolls, but got: {ranges:?}"
2204                );
2205                let first_scroll = &ranges[0];
2206                let second_scroll = &ranges[1];
2207                assert_eq!(
2208                    first_scroll.end.line, second_scroll.start.line,
2209                    "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
2210                );
2211
2212                let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2213                assert_eq!(
2214                    lsp_requests, 3,
2215                    "Should query hints initially, and after each scroll (2 times)"
2216                );
2217                assert_eq!(
2218                    vec!["50".to_string(), "100".to_string(), "150".to_string()],
2219                    cached_hint_labels(editor, cx),
2220                    "Chunks of 50 line width should have been queried each time"
2221                );
2222                assert_eq!(
2223                    vec!["50".to_string(), "100".to_string(), "150".to_string()],
2224                    visible_hint_labels(editor, cx),
2225                    "Editor should show only hints that it's scrolled to"
2226                );
2227
2228                let mut selection_in_cached_range = visible_range_after_scrolls.end;
2229                selection_in_cached_range.row -= visible_line_count.ceil() as u32;
2230                selection_in_cached_range
2231            })
2232            .unwrap();
2233
2234        editor
2235            .update(cx, |editor, window, cx| {
2236                editor.change_selections(
2237                    SelectionEffects::scroll(Autoscroll::center()),
2238                    window,
2239                    cx,
2240                    |s| s.select_ranges([selection_in_cached_range..selection_in_cached_range]),
2241                );
2242            })
2243            .unwrap();
2244        cx.executor().advance_clock(Duration::from_millis(100));
2245        cx.executor().run_until_parked();
2246        editor.update(cx, |_, _, _| {
2247            let ranges = lsp_request_ranges
2248                .lock()
2249                .drain(..)
2250                .sorted_by_key(|r| r.start)
2251                .collect::<Vec<_>>();
2252            assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
2253            assert_eq!(lsp_request_count.load(Ordering::Acquire), 3, "No new requests should be made when selecting within cached chunks");
2254        }).unwrap();
2255
2256        editor
2257            .update(cx, |editor, window, cx| {
2258                editor.handle_input("++++more text++++", window, cx);
2259            })
2260            .unwrap();
2261        cx.executor().advance_clock(Duration::from_secs(1));
2262        cx.executor().run_until_parked();
2263        editor.update(cx, |editor, _window, cx| {
2264            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2265            ranges.sort_by_key(|r| r.start);
2266
2267            assert_eq!(ranges.len(), 2,
2268                "On edit, should scroll to selection and query a range around it: that range should split into 2 50 rows wide chunks. Instead, got query ranges {ranges:?}");
2269            let first_chunk = &ranges[0];
2270            let second_chunk = &ranges[1];
2271            assert!(first_chunk.end.line == second_chunk.start.line,
2272                "First chunk {first_chunk:?} should be before second chunk {second_chunk:?}");
2273            assert!(first_chunk.start.line < selection_in_cached_range.row,
2274                "Hints should be queried with the selected range after the query range start");
2275
2276            let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2277            assert_eq!(lsp_requests, 5, "Two chunks should be re-queried");
2278            assert_eq!(vec!["100".to_string(), "150".to_string()], cached_hint_labels(editor, cx),
2279                "Should have (less) hints from the new LSP response after the edit");
2280            assert_eq!(vec!["100".to_string(), "150".to_string()], visible_hint_labels(editor, cx), "Should show only visible hints (in the center) from the new cached set");
2281        }).unwrap();
2282    }
2283
2284    fn editor_visible_range(
2285        editor: &WindowHandle<Editor>,
2286        cx: &mut gpui::TestAppContext,
2287    ) -> Range<Point> {
2288        let ranges = editor
2289            .update(cx, |editor, _window, cx| editor.visible_excerpts(true, cx))
2290            .unwrap();
2291        assert_eq!(
2292            ranges.len(),
2293            1,
2294            "Single buffer should produce a single excerpt with visible range"
2295        );
2296        let (_, (excerpt_buffer, _, excerpt_visible_range)) = ranges.into_iter().next().unwrap();
2297        excerpt_buffer.read_with(cx, |buffer, _| {
2298            excerpt_visible_range.to_point(&buffer.snapshot())
2299        })
2300    }
2301
2302    #[gpui::test]
2303    async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
2304        init_test(cx, &|settings| {
2305            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2306                show_value_hints: Some(true),
2307                enabled: Some(true),
2308                edit_debounce_ms: Some(0),
2309                scroll_debounce_ms: Some(0),
2310                show_type_hints: Some(true),
2311                show_parameter_hints: Some(true),
2312                show_other_hints: Some(true),
2313                show_background: Some(false),
2314                toggle_on_modifiers_press: None,
2315            })
2316        });
2317
2318        let fs = FakeFs::new(cx.background_executor.clone());
2319        fs.insert_tree(
2320                path!("/a"),
2321                json!({
2322                    "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2323                    "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2324                }),
2325            )
2326            .await;
2327
2328        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2329
2330        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2331        let language = rust_lang();
2332        language_registry.add(language);
2333        let mut fake_servers = language_registry.register_fake_lsp(
2334            "Rust",
2335            FakeLspAdapter {
2336                capabilities: lsp::ServerCapabilities {
2337                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2338                    ..lsp::ServerCapabilities::default()
2339                },
2340                ..FakeLspAdapter::default()
2341            },
2342        );
2343
2344        let (buffer_1, _handle1) = project
2345            .update(cx, |project, cx| {
2346                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2347            })
2348            .await
2349            .unwrap();
2350        let (buffer_2, _handle2) = project
2351            .update(cx, |project, cx| {
2352                project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2353            })
2354            .await
2355            .unwrap();
2356        let multibuffer = cx.new(|cx| {
2357            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2358            multibuffer.set_excerpts_for_path(
2359                PathKey::sorted(0),
2360                buffer_1.clone(),
2361                [
2362                    Point::new(0, 0)..Point::new(2, 0),
2363                    Point::new(4, 0)..Point::new(11, 0),
2364                    Point::new(22, 0)..Point::new(33, 0),
2365                    Point::new(44, 0)..Point::new(55, 0),
2366                    Point::new(56, 0)..Point::new(66, 0),
2367                    Point::new(67, 0)..Point::new(77, 0),
2368                ],
2369                0,
2370                cx,
2371            );
2372            multibuffer.set_excerpts_for_path(
2373                PathKey::sorted(1),
2374                buffer_2.clone(),
2375                [
2376                    Point::new(0, 1)..Point::new(2, 1),
2377                    Point::new(4, 1)..Point::new(11, 1),
2378                    Point::new(22, 1)..Point::new(33, 1),
2379                    Point::new(44, 1)..Point::new(55, 1),
2380                    Point::new(56, 1)..Point::new(66, 1),
2381                    Point::new(67, 1)..Point::new(77, 1),
2382                ],
2383                0,
2384                cx,
2385            );
2386            multibuffer
2387        });
2388
2389        cx.executor().run_until_parked();
2390        let editor = cx.add_window(|window, cx| {
2391            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2392        });
2393
2394        let editor_edited = Arc::new(AtomicBool::new(false));
2395        let fake_server = fake_servers.next().await.unwrap();
2396        let closure_editor_edited = Arc::clone(&editor_edited);
2397        fake_server
2398            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2399                let task_editor_edited = Arc::clone(&closure_editor_edited);
2400                async move {
2401                    let hint_text = if params.text_document.uri
2402                        == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
2403                    {
2404                        "main hint"
2405                    } else if params.text_document.uri
2406                        == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
2407                    {
2408                        "other hint"
2409                    } else {
2410                        panic!("unexpected uri: {:?}", params.text_document.uri);
2411                    };
2412
2413                    // one hint per excerpt
2414                    let positions = [
2415                        lsp::Position::new(0, 2),
2416                        lsp::Position::new(4, 2),
2417                        lsp::Position::new(22, 2),
2418                        lsp::Position::new(44, 2),
2419                        lsp::Position::new(56, 2),
2420                        lsp::Position::new(67, 2),
2421                    ];
2422                    let out_of_range_hint = lsp::InlayHint {
2423                        position: lsp::Position::new(
2424                            params.range.start.line + 99,
2425                            params.range.start.character + 99,
2426                        ),
2427                        label: lsp::InlayHintLabel::String(
2428                            "out of excerpt range, should be ignored".to_string(),
2429                        ),
2430                        kind: None,
2431                        text_edits: None,
2432                        tooltip: None,
2433                        padding_left: None,
2434                        padding_right: None,
2435                        data: None,
2436                    };
2437
2438                    let edited = task_editor_edited.load(Ordering::Acquire);
2439                    Ok(Some(
2440                        std::iter::once(out_of_range_hint)
2441                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
2442                                lsp::InlayHint {
2443                                    position,
2444                                    label: lsp::InlayHintLabel::String(format!(
2445                                        "{hint_text}{E} #{i}",
2446                                        E = if edited { "(edited)" } else { "" },
2447                                    )),
2448                                    kind: None,
2449                                    text_edits: None,
2450                                    tooltip: None,
2451                                    padding_left: None,
2452                                    padding_right: None,
2453                                    data: None,
2454                                }
2455                            }))
2456                            .collect(),
2457                    ))
2458                }
2459            })
2460            .next()
2461            .await;
2462        cx.executor().run_until_parked();
2463
2464        editor
2465            .update(cx, |editor, _window, cx| {
2466                let expected_hints = vec![
2467                    "main hint #0".to_string(),
2468                    "main hint #1".to_string(),
2469                    "main hint #2".to_string(),
2470                    "main hint #3".to_string(),
2471                    "main hint #4".to_string(),
2472                    "main hint #5".to_string(),
2473                ];
2474                assert_eq!(
2475                    expected_hints,
2476                    sorted_cached_hint_labels(editor, cx),
2477                    "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2478                );
2479                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2480            })
2481            .unwrap();
2482
2483        editor
2484            .update(cx, |editor, window, cx| {
2485                editor.change_selections(
2486                    SelectionEffects::scroll(Autoscroll::Next),
2487                    window,
2488                    cx,
2489                    |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
2490                );
2491                editor.change_selections(
2492                    SelectionEffects::scroll(Autoscroll::Next),
2493                    window,
2494                    cx,
2495                    |s| s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]),
2496                );
2497                editor.change_selections(
2498                    SelectionEffects::scroll(Autoscroll::Next),
2499                    window,
2500                    cx,
2501                    |s| s.select_ranges([Point::new(57, 0)..Point::new(57, 0)]),
2502                );
2503            })
2504            .unwrap();
2505        cx.executor().run_until_parked();
2506        editor
2507            .update(cx, |editor, _window, cx| {
2508                let expected_hints = vec![
2509                    "main hint #0".to_string(),
2510                    "main hint #1".to_string(),
2511                    "main hint #2".to_string(),
2512                    "main hint #3".to_string(),
2513                    "main hint #4".to_string(),
2514                    "main hint #5".to_string(),
2515                ];
2516                assert_eq!(expected_hints, sorted_cached_hint_labels(editor, cx),
2517                    "New hints are not shown right after scrolling, we need to wait for the buffer to be registered");
2518                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2519            })
2520            .unwrap();
2521        cx.executor().advance_clock(Duration::from_millis(100));
2522        cx.executor().run_until_parked();
2523        editor
2524            .update(cx, |editor, _window, cx| {
2525                let expected_hints = vec![
2526                    "main hint #0".to_string(),
2527                    "main hint #1".to_string(),
2528                    "main hint #2".to_string(),
2529                    "main hint #3".to_string(),
2530                    "main hint #4".to_string(),
2531                    "main hint #5".to_string(),
2532                    "other hint #0".to_string(),
2533                    "other hint #1".to_string(),
2534                    "other hint #2".to_string(),
2535                    "other hint #3".to_string(),
2536                ];
2537                assert_eq!(
2538                    expected_hints,
2539                    sorted_cached_hint_labels(editor, cx),
2540                    "After scrolling to the new buffer and waiting for it to be registered, new hints should appear");
2541                assert_eq!(
2542                    expected_hints,
2543                    visible_hint_labels(editor, cx),
2544                    "Editor should show only visible hints",
2545                );
2546            })
2547            .unwrap();
2548
2549        editor
2550            .update(cx, |editor, window, cx| {
2551                editor.change_selections(
2552                    SelectionEffects::scroll(Autoscroll::Next),
2553                    window,
2554                    cx,
2555                    |s| s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]),
2556                );
2557            })
2558            .unwrap();
2559        cx.executor().advance_clock(Duration::from_millis(100));
2560        cx.executor().run_until_parked();
2561        editor
2562            .update(cx, |editor, _window, cx| {
2563                let expected_hints = vec![
2564                    "main hint #0".to_string(),
2565                    "main hint #1".to_string(),
2566                    "main hint #2".to_string(),
2567                    "main hint #3".to_string(),
2568                    "main hint #4".to_string(),
2569                    "main hint #5".to_string(),
2570                    "other hint #0".to_string(),
2571                    "other hint #1".to_string(),
2572                    "other hint #2".to_string(),
2573                    "other hint #3".to_string(),
2574                    "other hint #4".to_string(),
2575                    "other hint #5".to_string(),
2576                ];
2577                assert_eq!(
2578                    expected_hints,
2579                    sorted_cached_hint_labels(editor, cx),
2580                    "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"
2581                );
2582                assert_eq!(
2583                    expected_hints,
2584                    visible_hint_labels(editor, cx),
2585                    "Editor shows only hints for excerpts that were visible when scrolling"
2586                );
2587            })
2588            .unwrap();
2589
2590        editor
2591            .update(cx, |editor, window, cx| {
2592                editor.change_selections(
2593                    SelectionEffects::scroll(Autoscroll::Next),
2594                    window,
2595                    cx,
2596                    |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
2597                );
2598            })
2599            .unwrap();
2600        cx.executor().run_until_parked();
2601        editor
2602            .update(cx, |editor, _window, cx| {
2603                let expected_hints = vec![
2604                    "main hint #0".to_string(),
2605                    "main hint #1".to_string(),
2606                    "main hint #2".to_string(),
2607                    "main hint #3".to_string(),
2608                    "main hint #4".to_string(),
2609                    "main hint #5".to_string(),
2610                    "other hint #0".to_string(),
2611                    "other hint #1".to_string(),
2612                    "other hint #2".to_string(),
2613                    "other hint #3".to_string(),
2614                    "other hint #4".to_string(),
2615                    "other hint #5".to_string(),
2616                ];
2617                assert_eq!(
2618                    expected_hints,
2619                    sorted_cached_hint_labels(editor, cx),
2620                    "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"
2621                );
2622                assert_eq!(
2623                    expected_hints,
2624                    visible_hint_labels(editor, cx),
2625                );
2626            })
2627            .unwrap();
2628
2629        // We prepare to change the scrolling on edit, but do not scroll yet
2630        editor
2631            .update(cx, |editor, window, cx| {
2632                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2633                    s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
2634                });
2635            })
2636            .unwrap();
2637        cx.executor().run_until_parked();
2638        // Edit triggers the scrolling too
2639        editor_edited.store(true, Ordering::Release);
2640        editor
2641            .update(cx, |editor, window, cx| {
2642                editor.handle_input("++++more text++++", window, cx);
2643            })
2644            .unwrap();
2645        cx.executor().run_until_parked();
2646        // Wait again to trigger the inlay hints fetch on scroll
2647        cx.executor().advance_clock(Duration::from_millis(100));
2648        cx.executor().run_until_parked();
2649        editor
2650            .update(cx, |editor, _window, cx| {
2651                let expected_hints = vec![
2652                    "main hint(edited) #0".to_string(),
2653                    "main hint(edited) #1".to_string(),
2654                    "main hint(edited) #2".to_string(),
2655                    "main hint(edited) #3".to_string(),
2656                    "main hint(edited) #4".to_string(),
2657                    "main hint(edited) #5".to_string(),
2658                    "other hint(edited) #0".to_string(),
2659                    "other hint(edited) #1".to_string(),
2660                    "other hint(edited) #2".to_string(),
2661                    "other hint(edited) #3".to_string(),
2662                ];
2663                assert_eq!(
2664                    expected_hints,
2665                    sorted_cached_hint_labels(editor, cx),
2666                    "After multibuffer edit, editor gets scrolled back to the last selection; \
2667                all hints should be invalidated and required for all of its visible excerpts"
2668                );
2669                assert_eq!(
2670                    expected_hints,
2671                    visible_hint_labels(editor, cx),
2672                    "All excerpts should get their hints"
2673                );
2674            })
2675            .unwrap();
2676    }
2677
2678    #[gpui::test]
2679    async fn test_editing_in_multi_buffer(cx: &mut gpui::TestAppContext) {
2680        init_test(cx, &|settings| {
2681            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2682                enabled: Some(true),
2683                ..InlayHintSettingsContent::default()
2684            })
2685        });
2686
2687        let fs = FakeFs::new(cx.background_executor.clone());
2688        fs.insert_tree(
2689            path!("/a"),
2690            json!({
2691                "main.rs": format!("fn main() {{\n{}\n}}", (0..200).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2692                "lib.rs": r#"let a = 1;
2693let b = 2;
2694let c = 3;"#
2695            }),
2696        )
2697        .await;
2698
2699        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2700
2701        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2702        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2703        let language = rust_lang();
2704        language_registry.add(language);
2705
2706        let closure_ranges_fetched = lsp_request_ranges.clone();
2707        let mut fake_servers = language_registry.register_fake_lsp(
2708            "Rust",
2709            FakeLspAdapter {
2710                capabilities: lsp::ServerCapabilities {
2711                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2712                    ..lsp::ServerCapabilities::default()
2713                },
2714                initializer: Some(Box::new(move |fake_server| {
2715                    let closure_ranges_fetched = closure_ranges_fetched.clone();
2716                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2717                        move |params, _| {
2718                            let closure_ranges_fetched = closure_ranges_fetched.clone();
2719                            async move {
2720                                let prefix = if params.text_document.uri
2721                                    == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
2722                                {
2723                                    closure_ranges_fetched
2724                                        .lock()
2725                                        .push(("main.rs", params.range));
2726                                    "main.rs"
2727                                } else if params.text_document.uri
2728                                    == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
2729                                {
2730                                    closure_ranges_fetched.lock().push(("lib.rs", params.range));
2731                                    "lib.rs"
2732                                } else {
2733                                    panic!("Unexpected file path {:?}", params.text_document.uri);
2734                                };
2735                                Ok(Some(
2736                                    (params.range.start.line..params.range.end.line)
2737                                        .map(|row| lsp::InlayHint {
2738                                            position: lsp::Position::new(row, 0),
2739                                            label: lsp::InlayHintLabel::String(format!(
2740                                                "{prefix} Inlay hint #{row}"
2741                                            )),
2742                                            kind: Some(lsp::InlayHintKind::TYPE),
2743                                            text_edits: None,
2744                                            tooltip: None,
2745                                            padding_left: None,
2746                                            padding_right: None,
2747                                            data: None,
2748                                        })
2749                                        .collect(),
2750                                ))
2751                            }
2752                        },
2753                    );
2754                })),
2755                ..FakeLspAdapter::default()
2756            },
2757        );
2758
2759        let (buffer_1, _handle_1) = project
2760            .update(cx, |project, cx| {
2761                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2762            })
2763            .await
2764            .unwrap();
2765        let (buffer_2, _handle_2) = project
2766            .update(cx, |project, cx| {
2767                project.open_local_buffer_with_lsp(path!("/a/lib.rs"), cx)
2768            })
2769            .await
2770            .unwrap();
2771        let multi_buffer = cx.new(|cx| {
2772            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2773            multibuffer.set_excerpts_for_path(
2774                PathKey::sorted(0),
2775                buffer_1.clone(),
2776                [
2777                    Point::new(49, 0)..Point::new(53, 0),
2778                    Point::new(70, 0)..Point::new(73, 0),
2779                ],
2780                0,
2781                cx,
2782            );
2783            multibuffer.set_excerpts_for_path(
2784                PathKey::sorted(1),
2785                buffer_2.clone(),
2786                [Point::new(0, 0)..Point::new(4, 0)],
2787                0,
2788                cx,
2789            );
2790            multibuffer
2791        });
2792
2793        let editor = cx.add_window(|window, cx| {
2794            let mut editor =
2795                Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx);
2796            editor.change_selections(SelectionEffects::default(), window, cx, |s| {
2797                s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
2798            });
2799            editor
2800        });
2801
2802        let _fake_server = fake_servers.next().await.unwrap();
2803        cx.executor().advance_clock(Duration::from_millis(100));
2804        cx.executor().run_until_parked();
2805
2806        assert_eq!(
2807            vec![
2808                (
2809                    "lib.rs",
2810                    lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(2, 10))
2811                ),
2812                (
2813                    "main.rs",
2814                    lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(50, 0))
2815                ),
2816                (
2817                    "main.rs",
2818                    lsp::Range::new(lsp::Position::new(50, 0), lsp::Position::new(100, 0))
2819                ),
2820            ],
2821            lsp_request_ranges
2822                .lock()
2823                .drain(..)
2824                .sorted_by_key(|(prefix, r)| (prefix.to_owned(), r.start))
2825                .collect::<Vec<_>>(),
2826            "For large buffers, should query chunks that cover both visible excerpt"
2827        );
2828        editor
2829            .update(cx, |editor, _window, cx| {
2830                assert_eq!(
2831                    (0..2)
2832                        .map(|i| format!("lib.rs Inlay hint #{i}"))
2833                        .chain((0..100).map(|i| format!("main.rs Inlay hint #{i}")))
2834                        .collect::<Vec<_>>(),
2835                    sorted_cached_hint_labels(editor, cx),
2836                    "Both chunks should provide their inlay hints"
2837                );
2838                assert_eq!(
2839                    vec![
2840                        "main.rs Inlay hint #49".to_owned(),
2841                        "main.rs Inlay hint #50".to_owned(),
2842                        "main.rs Inlay hint #51".to_owned(),
2843                        "main.rs Inlay hint #52".to_owned(),
2844                        "main.rs Inlay hint #53".to_owned(),
2845                        "main.rs Inlay hint #70".to_owned(),
2846                        "main.rs Inlay hint #71".to_owned(),
2847                        "main.rs Inlay hint #72".to_owned(),
2848                        "main.rs Inlay hint #73".to_owned(),
2849                        "lib.rs Inlay hint #0".to_owned(),
2850                        "lib.rs Inlay hint #1".to_owned(),
2851                    ],
2852                    visible_hint_labels(editor, cx),
2853                    "Only hints from visible excerpt should be added into the editor"
2854                );
2855            })
2856            .unwrap();
2857
2858        editor
2859            .update(cx, |editor, window, cx| {
2860                editor.handle_input("a", window, cx);
2861            })
2862            .unwrap();
2863        cx.executor().advance_clock(Duration::from_millis(1000));
2864        cx.executor().run_until_parked();
2865        assert_eq!(
2866            vec![
2867                (
2868                    "lib.rs",
2869                    lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(2, 10))
2870                ),
2871                (
2872                    "main.rs",
2873                    lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(50, 0))
2874                ),
2875                (
2876                    "main.rs",
2877                    lsp::Range::new(lsp::Position::new(50, 0), lsp::Position::new(100, 0))
2878                ),
2879            ],
2880            lsp_request_ranges
2881                .lock()
2882                .drain(..)
2883                .sorted_by_key(|(prefix, r)| (prefix.to_owned(), r.start))
2884                .collect::<Vec<_>>(),
2885            "Same chunks should be re-queried on edit"
2886        );
2887        editor
2888            .update(cx, |editor, _window, cx| {
2889                assert_eq!(
2890                    (0..2)
2891                        .map(|i| format!("lib.rs Inlay hint #{i}"))
2892                        .chain((0..100).map(|i| format!("main.rs Inlay hint #{i}")))
2893                        .collect::<Vec<_>>(),
2894                    sorted_cached_hint_labels(editor, cx),
2895                    "Same hints should be re-inserted after the edit"
2896                );
2897                assert_eq!(
2898                    vec![
2899                        "main.rs Inlay hint #49".to_owned(),
2900                        "main.rs Inlay hint #50".to_owned(),
2901                        "main.rs Inlay hint #51".to_owned(),
2902                        "main.rs Inlay hint #52".to_owned(),
2903                        "main.rs Inlay hint #53".to_owned(),
2904                        "main.rs Inlay hint #70".to_owned(),
2905                        "main.rs Inlay hint #71".to_owned(),
2906                        "main.rs Inlay hint #72".to_owned(),
2907                        "main.rs Inlay hint #73".to_owned(),
2908                        "lib.rs Inlay hint #0".to_owned(),
2909                        "lib.rs Inlay hint #1".to_owned(),
2910                    ],
2911                    visible_hint_labels(editor, cx),
2912                    "Same hints should be re-inserted into the editor after the edit"
2913                );
2914            })
2915            .unwrap();
2916    }
2917
2918    #[gpui::test]
2919    async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
2920        init_test(cx, &|settings| {
2921            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2922                show_value_hints: Some(true),
2923                enabled: Some(true),
2924                edit_debounce_ms: Some(0),
2925                scroll_debounce_ms: Some(0),
2926                show_type_hints: Some(false),
2927                show_parameter_hints: Some(false),
2928                show_other_hints: Some(false),
2929                show_background: Some(false),
2930                toggle_on_modifiers_press: None,
2931            })
2932        });
2933
2934        let fs = FakeFs::new(cx.background_executor.clone());
2935        fs.insert_tree(
2936            path!("/a"),
2937            json!({
2938                "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2939                "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2940            }),
2941        )
2942        .await;
2943
2944        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2945
2946        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2947        language_registry.add(rust_lang());
2948        let mut fake_servers = language_registry.register_fake_lsp(
2949            "Rust",
2950            FakeLspAdapter {
2951                capabilities: lsp::ServerCapabilities {
2952                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2953                    ..lsp::ServerCapabilities::default()
2954                },
2955                ..FakeLspAdapter::default()
2956            },
2957        );
2958
2959        let (buffer_1, _handle) = project
2960            .update(cx, |project, cx| {
2961                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2962            })
2963            .await
2964            .unwrap();
2965        let (buffer_2, _handle2) = project
2966            .update(cx, |project, cx| {
2967                project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2968            })
2969            .await
2970            .unwrap();
2971        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2972        let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2973            multibuffer.set_excerpts_for_path(
2974                PathKey::sorted(0),
2975                buffer_1.clone(),
2976                [Point::new(0, 0)..Point::new(2, 0)],
2977                0,
2978                cx,
2979            );
2980            multibuffer.set_excerpts_for_path(
2981                PathKey::sorted(1),
2982                buffer_2.clone(),
2983                [Point::new(0, 1)..Point::new(2, 1)],
2984                0,
2985                cx,
2986            );
2987            let excerpt_ids = multibuffer.excerpt_ids();
2988            let buffer_1_excerpts = vec![excerpt_ids[0]];
2989            let buffer_2_excerpts = vec![excerpt_ids[1]];
2990            (buffer_1_excerpts, buffer_2_excerpts)
2991        });
2992
2993        assert!(!buffer_1_excerpts.is_empty());
2994        assert!(!buffer_2_excerpts.is_empty());
2995
2996        cx.executor().run_until_parked();
2997        let editor = cx.add_window(|window, cx| {
2998            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2999        });
3000        let editor_edited = Arc::new(AtomicBool::new(false));
3001        let fake_server = fake_servers.next().await.unwrap();
3002        let closure_editor_edited = Arc::clone(&editor_edited);
3003        fake_server
3004            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
3005                let task_editor_edited = Arc::clone(&closure_editor_edited);
3006                async move {
3007                    let hint_text = if params.text_document.uri
3008                        == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
3009                    {
3010                        "main hint"
3011                    } else if params.text_document.uri
3012                        == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
3013                    {
3014                        "other hint"
3015                    } else {
3016                        panic!("unexpected uri: {:?}", params.text_document.uri);
3017                    };
3018
3019                    let positions = [
3020                        lsp::Position::new(0, 2),
3021                        lsp::Position::new(4, 2),
3022                        lsp::Position::new(22, 2),
3023                        lsp::Position::new(44, 2),
3024                        lsp::Position::new(56, 2),
3025                        lsp::Position::new(67, 2),
3026                    ];
3027                    let out_of_range_hint = lsp::InlayHint {
3028                        position: lsp::Position::new(
3029                            params.range.start.line + 99,
3030                            params.range.start.character + 99,
3031                        ),
3032                        label: lsp::InlayHintLabel::String(
3033                            "out of excerpt range, should be ignored".to_string(),
3034                        ),
3035                        kind: None,
3036                        text_edits: None,
3037                        tooltip: None,
3038                        padding_left: None,
3039                        padding_right: None,
3040                        data: None,
3041                    };
3042
3043                    let edited = task_editor_edited.load(Ordering::Acquire);
3044                    Ok(Some(
3045                        std::iter::once(out_of_range_hint)
3046                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
3047                                lsp::InlayHint {
3048                                    position,
3049                                    label: lsp::InlayHintLabel::String(format!(
3050                                        "{hint_text}{} #{i}",
3051                                        if edited { "(edited)" } else { "" },
3052                                    )),
3053                                    kind: None,
3054                                    text_edits: None,
3055                                    tooltip: None,
3056                                    padding_left: None,
3057                                    padding_right: None,
3058                                    data: None,
3059                                }
3060                            }))
3061                            .collect(),
3062                    ))
3063                }
3064            })
3065            .next()
3066            .await;
3067        cx.executor().advance_clock(Duration::from_millis(100));
3068        cx.executor().run_until_parked();
3069        editor
3070            .update(cx, |editor, _, cx| {
3071                assert_eq!(
3072                    vec![
3073                        "main hint #0".to_string(),
3074                        "main hint #1".to_string(),
3075                        "main hint #2".to_string(),
3076                        "main hint #3".to_string(),
3077                        "other hint #0".to_string(),
3078                        "other hint #1".to_string(),
3079                        "other hint #2".to_string(),
3080                        "other hint #3".to_string(),
3081                    ],
3082                    sorted_cached_hint_labels(editor, cx),
3083                    "Cache should update for both excerpts despite hints display was disabled; after selecting 2nd buffer, it's now registered with the langserever and should get its hints"
3084                );
3085                assert_eq!(
3086                    Vec::<String>::new(),
3087                    visible_hint_labels(editor, cx),
3088                    "All hints are disabled and should not be shown despite being present in the cache"
3089                );
3090            })
3091            .unwrap();
3092
3093        editor
3094            .update(cx, |editor, _, cx| {
3095                editor.buffer().update(cx, |multibuffer, cx| {
3096                    multibuffer.remove_excerpts_for_path(PathKey::sorted(1), cx);
3097                })
3098            })
3099            .unwrap();
3100        cx.executor().run_until_parked();
3101        editor
3102            .update(cx, |editor, _, cx| {
3103                assert_eq!(
3104                    vec![
3105                        "main hint #0".to_string(),
3106                        "main hint #1".to_string(),
3107                        "main hint #2".to_string(),
3108                        "main hint #3".to_string(),
3109                    ],
3110                    cached_hint_labels(editor, cx),
3111                    "For the removed excerpt, should clean corresponding cached hints as its buffer was dropped"
3112                );
3113                assert!(
3114                visible_hint_labels(editor, cx).is_empty(),
3115                "All hints are disabled and should not be shown despite being present in the cache"
3116            );
3117            })
3118            .unwrap();
3119
3120        update_test_language_settings(cx, &|settings| {
3121            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3122                show_value_hints: Some(true),
3123                enabled: Some(true),
3124                edit_debounce_ms: Some(0),
3125                scroll_debounce_ms: Some(0),
3126                show_type_hints: Some(true),
3127                show_parameter_hints: Some(true),
3128                show_other_hints: Some(true),
3129                show_background: Some(false),
3130                toggle_on_modifiers_press: None,
3131            })
3132        });
3133        cx.executor().run_until_parked();
3134        editor
3135            .update(cx, |editor, _, cx| {
3136                assert_eq!(
3137                    vec![
3138                        "main hint #0".to_string(),
3139                        "main hint #1".to_string(),
3140                        "main hint #2".to_string(),
3141                        "main hint #3".to_string(),
3142                    ],
3143                    cached_hint_labels(editor, cx),
3144                    "Hint display settings change should not change the cache"
3145                );
3146                assert_eq!(
3147                    vec![
3148                        "main hint #0".to_string(),
3149                    ],
3150                    visible_hint_labels(editor, cx),
3151                    "Settings change should make cached hints visible, but only the visible ones, from the remaining excerpt"
3152                );
3153            })
3154            .unwrap();
3155    }
3156
3157    #[gpui::test]
3158    async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
3159        init_test(cx, &|settings| {
3160            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3161                show_value_hints: Some(true),
3162                enabled: Some(true),
3163                edit_debounce_ms: Some(0),
3164                scroll_debounce_ms: Some(0),
3165                show_type_hints: Some(true),
3166                show_parameter_hints: Some(true),
3167                show_other_hints: Some(true),
3168                show_background: Some(false),
3169                toggle_on_modifiers_press: None,
3170            })
3171        });
3172
3173        let fs = FakeFs::new(cx.background_executor.clone());
3174        fs.insert_tree(
3175            path!("/a"),
3176            json!({
3177                "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "".repeat(10)).repeat(500)),
3178                "other.rs": "// Test file",
3179            }),
3180        )
3181        .await;
3182
3183        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3184
3185        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3186        language_registry.add(rust_lang());
3187        language_registry.register_fake_lsp(
3188            "Rust",
3189            FakeLspAdapter {
3190                capabilities: lsp::ServerCapabilities {
3191                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3192                    ..lsp::ServerCapabilities::default()
3193                },
3194                initializer: Some(Box::new(move |fake_server| {
3195                    let lsp_request_count = Arc::new(AtomicU32::new(0));
3196                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3197                        move |params, _| {
3198                            let i = lsp_request_count.fetch_add(1, Ordering::Release) + 1;
3199                            async move {
3200                                assert_eq!(
3201                                    params.text_document.uri,
3202                                    lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3203                                );
3204                                let query_start = params.range.start;
3205                                Ok(Some(vec![lsp::InlayHint {
3206                                    position: query_start,
3207                                    label: lsp::InlayHintLabel::String(i.to_string()),
3208                                    kind: None,
3209                                    text_edits: None,
3210                                    tooltip: None,
3211                                    padding_left: None,
3212                                    padding_right: None,
3213                                    data: None,
3214                                }]))
3215                            }
3216                        },
3217                    );
3218                })),
3219                ..FakeLspAdapter::default()
3220            },
3221        );
3222
3223        let buffer = project
3224            .update(cx, |project, cx| {
3225                project.open_local_buffer(path!("/a/main.rs"), cx)
3226            })
3227            .await
3228            .unwrap();
3229        let editor =
3230            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3231
3232        // Allow LSP to initialize
3233        cx.executor().run_until_parked();
3234
3235        // Establish a viewport and explicitly trigger hint refresh.
3236        // This ensures we control exactly when hints are requested.
3237        editor
3238            .update(cx, |editor, window, cx| {
3239                editor.set_visible_line_count(50.0, window, cx);
3240                editor.set_visible_column_count(120.0);
3241                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
3242            })
3243            .unwrap();
3244
3245        // Allow LSP initialization and hint request/response to complete.
3246        // Use multiple advance_clock + run_until_parked cycles to ensure all async work completes.
3247        for _ in 0..5 {
3248            cx.executor().advance_clock(Duration::from_millis(100));
3249            cx.executor().run_until_parked();
3250        }
3251
3252        // At this point we should have exactly one hint from our explicit refresh.
3253        // The test verifies that hints at character boundaries are handled correctly.
3254        editor
3255            .update(cx, |editor, _, cx| {
3256                assert!(
3257                    !cached_hint_labels(editor, cx).is_empty(),
3258                    "Should have at least one hint after refresh"
3259                );
3260                assert!(
3261                    !visible_hint_labels(editor, cx).is_empty(),
3262                    "Should have at least one visible hint"
3263                );
3264            })
3265            .unwrap();
3266    }
3267
3268    #[gpui::test]
3269    async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
3270        init_test(cx, &|settings| {
3271            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3272                show_value_hints: Some(true),
3273                enabled: Some(false),
3274                edit_debounce_ms: Some(0),
3275                scroll_debounce_ms: Some(0),
3276                show_type_hints: Some(true),
3277                show_parameter_hints: Some(true),
3278                show_other_hints: Some(true),
3279                show_background: Some(false),
3280                toggle_on_modifiers_press: None,
3281            })
3282        });
3283
3284        let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3285            let lsp_request_count = Arc::new(AtomicU32::new(0));
3286            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3287                move |params, _| {
3288                    let lsp_request_count = lsp_request_count.clone();
3289                    async move {
3290                        assert_eq!(
3291                            params.text_document.uri,
3292                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
3293                        );
3294
3295                        let i = lsp_request_count.fetch_add(1, Ordering::AcqRel) + 1;
3296                        Ok(Some(vec![lsp::InlayHint {
3297                            position: lsp::Position::new(0, i),
3298                            label: lsp::InlayHintLabel::String(i.to_string()),
3299                            kind: None,
3300                            text_edits: None,
3301                            tooltip: None,
3302                            padding_left: None,
3303                            padding_right: None,
3304                            data: None,
3305                        }]))
3306                    }
3307                },
3308            );
3309        })
3310        .await;
3311
3312        editor
3313            .update(cx, |editor, window, cx| {
3314                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3315            })
3316            .unwrap();
3317
3318        cx.executor().run_until_parked();
3319        editor
3320            .update(cx, |editor, _, cx| {
3321                let expected_hints = vec!["1".to_string()];
3322                assert_eq!(
3323                    expected_hints,
3324                    cached_hint_labels(editor, cx),
3325                    "Should display inlays after toggle despite them disabled in settings"
3326                );
3327                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3328            })
3329            .unwrap();
3330
3331        editor
3332            .update(cx, |editor, window, cx| {
3333                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3334            })
3335            .unwrap();
3336        cx.executor().run_until_parked();
3337        editor
3338            .update(cx, |editor, _, cx| {
3339                assert_eq!(
3340                    vec!["1".to_string()],
3341                    cached_hint_labels(editor, cx),
3342                    "Cache does not change because of toggles in the editor"
3343                );
3344                assert_eq!(
3345                    Vec::<String>::new(),
3346                    visible_hint_labels(editor, cx),
3347                    "Should clear hints after 2nd toggle"
3348                );
3349            })
3350            .unwrap();
3351
3352        update_test_language_settings(cx, &|settings| {
3353            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3354                show_value_hints: Some(true),
3355                enabled: Some(true),
3356                edit_debounce_ms: Some(0),
3357                scroll_debounce_ms: Some(0),
3358                show_type_hints: Some(true),
3359                show_parameter_hints: Some(true),
3360                show_other_hints: Some(true),
3361                show_background: Some(false),
3362                toggle_on_modifiers_press: None,
3363            })
3364        });
3365        cx.executor().run_until_parked();
3366        editor
3367            .update(cx, |editor, _, cx| {
3368                let expected_hints = vec!["1".to_string()];
3369                assert_eq!(
3370                    expected_hints,
3371                    cached_hint_labels(editor, cx),
3372                    "Should not query LSP hints after enabling hints in settings, as file version is the same"
3373                );
3374                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3375            })
3376            .unwrap();
3377
3378        editor
3379            .update(cx, |editor, window, cx| {
3380                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3381            })
3382            .unwrap();
3383        cx.executor().run_until_parked();
3384        editor
3385            .update(cx, |editor, _, cx| {
3386                assert_eq!(
3387                    vec!["1".to_string()],
3388                    cached_hint_labels(editor, cx),
3389                    "Cache does not change because of toggles in the editor"
3390                );
3391                assert_eq!(
3392                    Vec::<String>::new(),
3393                    visible_hint_labels(editor, cx),
3394                    "Should clear hints after enabling in settings and a 3rd toggle"
3395                );
3396            })
3397            .unwrap();
3398
3399        editor
3400            .update(cx, |editor, window, cx| {
3401                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3402            })
3403            .unwrap();
3404        cx.executor().run_until_parked();
3405        editor.update(cx, |editor, _, cx| {
3406            let expected_hints = vec!["1".to_string()];
3407            assert_eq!(
3408                expected_hints,
3409                cached_hint_labels(editor,cx),
3410                "Should not query LSP hints after enabling hints in settings and toggling them back on"
3411            );
3412            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3413        }).unwrap();
3414    }
3415
3416    #[gpui::test]
3417    async fn test_modifiers_change(cx: &mut gpui::TestAppContext) {
3418        init_test(cx, &|settings| {
3419            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3420                show_value_hints: Some(true),
3421                enabled: Some(true),
3422                edit_debounce_ms: Some(0),
3423                scroll_debounce_ms: Some(0),
3424                show_type_hints: Some(true),
3425                show_parameter_hints: Some(true),
3426                show_other_hints: Some(true),
3427                show_background: Some(false),
3428                toggle_on_modifiers_press: None,
3429            })
3430        });
3431
3432        let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3433            let lsp_request_count = Arc::new(AtomicU32::new(0));
3434            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3435                move |params, _| {
3436                    let lsp_request_count = lsp_request_count.clone();
3437                    async move {
3438                        assert_eq!(
3439                            params.text_document.uri,
3440                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
3441                        );
3442
3443                        let i = lsp_request_count.fetch_add(1, Ordering::AcqRel) + 1;
3444                        Ok(Some(vec![lsp::InlayHint {
3445                            position: lsp::Position::new(0, i),
3446                            label: lsp::InlayHintLabel::String(i.to_string()),
3447                            kind: None,
3448                            text_edits: None,
3449                            tooltip: None,
3450                            padding_left: None,
3451                            padding_right: None,
3452                            data: None,
3453                        }]))
3454                    }
3455                },
3456            );
3457        })
3458        .await;
3459
3460        cx.executor().run_until_parked();
3461        editor
3462            .update(cx, |editor, _, cx| {
3463                assert_eq!(
3464                    vec!["1".to_string()],
3465                    cached_hint_labels(editor, cx),
3466                    "Should display inlays after toggle despite them disabled in settings"
3467                );
3468                assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx));
3469            })
3470            .unwrap();
3471
3472        editor
3473            .update(cx, |editor, _, cx| {
3474                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3475            })
3476            .unwrap();
3477        cx.executor().run_until_parked();
3478        editor
3479            .update(cx, |editor, _, cx| {
3480                assert_eq!(
3481                    vec!["1".to_string()],
3482                    cached_hint_labels(editor, cx),
3483                    "Nothing happens with the cache on modifiers change"
3484                );
3485                assert_eq!(
3486                    Vec::<String>::new(),
3487                    visible_hint_labels(editor, cx),
3488                    "On modifiers change and hints toggled on, should hide editor inlays"
3489                );
3490            })
3491            .unwrap();
3492        editor
3493            .update(cx, |editor, _, cx| {
3494                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3495            })
3496            .unwrap();
3497        cx.executor().run_until_parked();
3498        editor
3499            .update(cx, |editor, _, cx| {
3500                assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3501                assert_eq!(
3502                    Vec::<String>::new(),
3503                    visible_hint_labels(editor, cx),
3504                    "Nothing changes on consequent modifiers change of the same kind"
3505                );
3506            })
3507            .unwrap();
3508
3509        editor
3510            .update(cx, |editor, _, cx| {
3511                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3512            })
3513            .unwrap();
3514        cx.executor().run_until_parked();
3515        editor
3516            .update(cx, |editor, _, cx| {
3517                assert_eq!(
3518                    vec!["1".to_string()],
3519                    cached_hint_labels(editor, cx),
3520                    "When modifiers change is off, no extra requests are sent"
3521                );
3522                assert_eq!(
3523                    vec!["1".to_string()],
3524                    visible_hint_labels(editor, cx),
3525                    "When modifiers change is off, hints are back into the editor"
3526                );
3527            })
3528            .unwrap();
3529        editor
3530            .update(cx, |editor, _, cx| {
3531                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3532            })
3533            .unwrap();
3534        cx.executor().run_until_parked();
3535        editor
3536            .update(cx, |editor, _, cx| {
3537                assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3538                assert_eq!(
3539                    vec!["1".to_string()],
3540                    visible_hint_labels(editor, cx),
3541                    "Nothing changes on consequent modifiers change of the same kind (2)"
3542                );
3543            })
3544            .unwrap();
3545
3546        editor
3547            .update(cx, |editor, window, cx| {
3548                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3549            })
3550            .unwrap();
3551        cx.executor().run_until_parked();
3552        editor
3553            .update(cx, |editor, _, cx| {
3554                assert_eq!(
3555                    vec!["1".to_string()],
3556                    cached_hint_labels(editor, cx),
3557                    "Nothing happens with the cache on modifiers change"
3558                );
3559                assert_eq!(
3560                    Vec::<String>::new(),
3561                    visible_hint_labels(editor, cx),
3562                    "When toggled off, should hide editor inlays"
3563                );
3564            })
3565            .unwrap();
3566
3567        editor
3568            .update(cx, |editor, _, cx| {
3569                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3570            })
3571            .unwrap();
3572        cx.executor().run_until_parked();
3573        editor
3574            .update(cx, |editor, _, cx| {
3575                assert_eq!(
3576                    vec!["1".to_string()],
3577                    cached_hint_labels(editor, cx),
3578                    "Nothing happens with the cache on modifiers change"
3579                );
3580                assert_eq!(
3581                    vec!["1".to_string()],
3582                    visible_hint_labels(editor, cx),
3583                    "On modifiers change & hints toggled off, should show editor inlays"
3584                );
3585            })
3586            .unwrap();
3587        editor
3588            .update(cx, |editor, _, cx| {
3589                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3590            })
3591            .unwrap();
3592        cx.executor().run_until_parked();
3593        editor
3594            .update(cx, |editor, _, cx| {
3595                assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3596                assert_eq!(
3597                    vec!["1".to_string()],
3598                    visible_hint_labels(editor, cx),
3599                    "Nothing changes on consequent modifiers change of the same kind"
3600                );
3601            })
3602            .unwrap();
3603
3604        editor
3605            .update(cx, |editor, _, cx| {
3606                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3607            })
3608            .unwrap();
3609        cx.executor().run_until_parked();
3610        editor
3611            .update(cx, |editor, _, cx| {
3612                assert_eq!(
3613                    vec!["1".to_string()],
3614                    cached_hint_labels(editor, cx),
3615                    "When modifiers change is off, no extra requests are sent"
3616                );
3617                assert_eq!(
3618                    Vec::<String>::new(),
3619                    visible_hint_labels(editor, cx),
3620                    "When modifiers change is off, editor hints are back into their toggled off state"
3621                );
3622            })
3623            .unwrap();
3624        editor
3625            .update(cx, |editor, _, cx| {
3626                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3627            })
3628            .unwrap();
3629        cx.executor().run_until_parked();
3630        editor
3631            .update(cx, |editor, _, cx| {
3632                assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3633                assert_eq!(
3634                    Vec::<String>::new(),
3635                    visible_hint_labels(editor, cx),
3636                    "Nothing changes on consequent modifiers change of the same kind (3)"
3637                );
3638            })
3639            .unwrap();
3640    }
3641
3642    #[gpui::test]
3643    async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) {
3644        init_test(cx, &|settings| {
3645            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3646                show_value_hints: Some(true),
3647                enabled: Some(true),
3648                edit_debounce_ms: Some(0),
3649                scroll_debounce_ms: Some(0),
3650                show_type_hints: Some(true),
3651                show_parameter_hints: Some(true),
3652                show_other_hints: Some(true),
3653                show_background: Some(false),
3654                toggle_on_modifiers_press: None,
3655            })
3656        });
3657
3658        let fs = FakeFs::new(cx.background_executor.clone());
3659        fs.insert_tree(
3660            path!("/a"),
3661            json!({
3662                "main.rs": "fn main() {
3663                    let x = 42;
3664                    std::thread::scope(|s| {
3665                        s.spawn(|| {
3666                            let _x = x;
3667                        });
3668                    });
3669                }",
3670                "other.rs": "// Test file",
3671            }),
3672        )
3673        .await;
3674
3675        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3676
3677        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3678        language_registry.add(rust_lang());
3679        language_registry.register_fake_lsp(
3680            "Rust",
3681            FakeLspAdapter {
3682                capabilities: lsp::ServerCapabilities {
3683                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3684                    ..Default::default()
3685                },
3686                initializer: Some(Box::new(move |fake_server| {
3687                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3688                        move |params, _| async move {
3689                            assert_eq!(
3690                                params.text_document.uri,
3691                                lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3692                            );
3693                            Ok(Some(
3694                                serde_json::from_value(json!([
3695                                    {
3696                                        "position": {
3697                                            "line": 3,
3698                                            "character": 16
3699                                        },
3700                                        "label": "move",
3701                                        "paddingLeft": false,
3702                                        "paddingRight": false
3703                                    },
3704                                    {
3705                                        "position": {
3706                                            "line": 3,
3707                                            "character": 16
3708                                        },
3709                                        "label": "(",
3710                                        "paddingLeft": false,
3711                                        "paddingRight": false
3712                                    },
3713                                    {
3714                                        "position": {
3715                                            "line": 3,
3716                                            "character": 16
3717                                        },
3718                                        "label": [
3719                                            {
3720                                                "value": "&x"
3721                                            }
3722                                        ],
3723                                        "paddingLeft": false,
3724                                        "paddingRight": false,
3725                                        "data": {
3726                                            "file_id": 0
3727                                        }
3728                                    },
3729                                    {
3730                                        "position": {
3731                                            "line": 3,
3732                                            "character": 16
3733                                        },
3734                                        "label": ")",
3735                                        "paddingLeft": false,
3736                                        "paddingRight": true
3737                                    },
3738                                    // not a correct syntax, but checks that same symbols at the same place
3739                                    // are not deduplicated
3740                                    {
3741                                        "position": {
3742                                            "line": 3,
3743                                            "character": 16
3744                                        },
3745                                        "label": ")",
3746                                        "paddingLeft": false,
3747                                        "paddingRight": true
3748                                    },
3749                                ]))
3750                                .unwrap(),
3751                            ))
3752                        },
3753                    );
3754                })),
3755                ..FakeLspAdapter::default()
3756            },
3757        );
3758
3759        let buffer = project
3760            .update(cx, |project, cx| {
3761                project.open_local_buffer(path!("/a/main.rs"), cx)
3762            })
3763            .await
3764            .unwrap();
3765
3766        // Use a VisualTestContext and explicitly establish a viewport on the editor (the production
3767        // trigger for `NewLinesShown` / inlay hint refresh) by setting visible line/column counts.
3768        let (editor_entity, cx) =
3769            cx.add_window_view(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3770
3771        editor_entity.update_in(cx, |editor, window, cx| {
3772            // Establish a viewport. The exact values are not important for this test; we just need
3773            // the editor to consider itself visible so the refresh pipeline runs.
3774            editor.set_visible_line_count(50.0, window, cx);
3775            editor.set_visible_column_count(120.0);
3776
3777            // Explicitly trigger a refresh now that the viewport exists.
3778            editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
3779        });
3780        cx.executor().run_until_parked();
3781
3782        editor_entity.update_in(cx, |editor, window, cx| {
3783            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3784                s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3785            });
3786        });
3787        cx.executor().run_until_parked();
3788
3789        // Allow any async inlay hint request/response work to complete.
3790        cx.executor().advance_clock(Duration::from_millis(100));
3791        cx.executor().run_until_parked();
3792
3793        editor_entity.update(cx, |editor, cx| {
3794            let expected_hints = vec![
3795                "move".to_string(),
3796                "(".to_string(),
3797                "&x".to_string(),
3798                ") ".to_string(),
3799                ") ".to_string(),
3800            ];
3801            assert_eq!(
3802                expected_hints,
3803                cached_hint_labels(editor, cx),
3804                "Editor inlay hints should repeat server's order when placed at the same spot"
3805            );
3806            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3807        });
3808    }
3809
3810    #[gpui::test]
3811    async fn test_invalidation_and_addition_race(cx: &mut gpui::TestAppContext) {
3812        init_test(cx, &|settings| {
3813            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3814                enabled: Some(true),
3815                ..InlayHintSettingsContent::default()
3816            })
3817        });
3818
3819        let fs = FakeFs::new(cx.background_executor.clone());
3820        fs.insert_tree(
3821            path!("/a"),
3822            json!({
3823                "main.rs": r#"fn main() {
3824                    let x = 1;
3825                    ////
3826                    ////
3827                    ////
3828                    ////
3829                    ////
3830                    ////
3831                    ////
3832                    ////
3833                    ////
3834                    ////
3835                    ////
3836                    ////
3837                    ////
3838                    ////
3839                    ////
3840                    ////
3841                    ////
3842                    let x = "2";
3843                }
3844"#,
3845                "lib.rs": r#"fn aaa() {
3846                    let aa = 22;
3847                }
3848                //
3849                //
3850                //
3851                //
3852                //
3853                //
3854                //
3855                //
3856                //
3857                //
3858                //
3859                //
3860                //
3861                //
3862                //
3863                //
3864                //
3865                //
3866                //
3867                //
3868                //
3869                //
3870                //
3871                //
3872
3873                fn bb() {
3874                    let bb = 33;
3875                }
3876"#
3877            }),
3878        )
3879        .await;
3880
3881        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3882        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3883        let language = rust_lang();
3884        language_registry.add(language);
3885
3886        let requests_count = Arc::new(AtomicUsize::new(0));
3887        let closure_requests_count = requests_count.clone();
3888        let mut fake_servers = language_registry.register_fake_lsp(
3889            "Rust",
3890            FakeLspAdapter {
3891                name: "rust-analyzer",
3892                capabilities: lsp::ServerCapabilities {
3893                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3894                    ..lsp::ServerCapabilities::default()
3895                },
3896                initializer: Some(Box::new(move |fake_server| {
3897                    let requests_count = closure_requests_count.clone();
3898                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3899                        move |params, _| {
3900                            let requests_count = requests_count.clone();
3901                            async move {
3902                                requests_count.fetch_add(1, Ordering::Release);
3903                                if params.text_document.uri
3904                                    == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
3905                                {
3906                                    Ok(Some(vec![
3907                                        lsp::InlayHint {
3908                                            position: lsp::Position::new(1, 9),
3909                                            label: lsp::InlayHintLabel::String(": i32".to_owned()),
3910                                            kind: Some(lsp::InlayHintKind::TYPE),
3911                                            text_edits: None,
3912                                            tooltip: None,
3913                                            padding_left: None,
3914                                            padding_right: None,
3915                                            data: None,
3916                                        },
3917                                        lsp::InlayHint {
3918                                            position: lsp::Position::new(19, 9),
3919                                            label: lsp::InlayHintLabel::String(": i33".to_owned()),
3920                                            kind: Some(lsp::InlayHintKind::TYPE),
3921                                            text_edits: None,
3922                                            tooltip: None,
3923                                            padding_left: None,
3924                                            padding_right: None,
3925                                            data: None,
3926                                        },
3927                                    ]))
3928                                } else if params.text_document.uri
3929                                    == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
3930                                {
3931                                    Ok(Some(vec![
3932                                        lsp::InlayHint {
3933                                            position: lsp::Position::new(1, 10),
3934                                            label: lsp::InlayHintLabel::String(": i34".to_owned()),
3935                                            kind: Some(lsp::InlayHintKind::TYPE),
3936                                            text_edits: None,
3937                                            tooltip: None,
3938                                            padding_left: None,
3939                                            padding_right: None,
3940                                            data: None,
3941                                        },
3942                                        lsp::InlayHint {
3943                                            position: lsp::Position::new(29, 10),
3944                                            label: lsp::InlayHintLabel::String(": i35".to_owned()),
3945                                            kind: Some(lsp::InlayHintKind::TYPE),
3946                                            text_edits: None,
3947                                            tooltip: None,
3948                                            padding_left: None,
3949                                            padding_right: None,
3950                                            data: None,
3951                                        },
3952                                    ]))
3953                                } else {
3954                                    panic!("Unexpected file path {:?}", params.text_document.uri);
3955                                }
3956                            }
3957                        },
3958                    );
3959                })),
3960                ..FakeLspAdapter::default()
3961            },
3962        );
3963
3964        // Add another server that does send the same, duplicate hints back
3965        let mut fake_servers_2 = language_registry.register_fake_lsp(
3966            "Rust",
3967            FakeLspAdapter {
3968                name: "CrabLang-ls",
3969                capabilities: lsp::ServerCapabilities {
3970                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3971                    ..lsp::ServerCapabilities::default()
3972                },
3973                initializer: Some(Box::new(move |fake_server| {
3974                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3975                        move |params, _| async move {
3976                            if params.text_document.uri
3977                                == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
3978                            {
3979                                Ok(Some(vec![
3980                                    lsp::InlayHint {
3981                                        position: lsp::Position::new(1, 9),
3982                                        label: lsp::InlayHintLabel::String(": i32".to_owned()),
3983                                        kind: Some(lsp::InlayHintKind::TYPE),
3984                                        text_edits: None,
3985                                        tooltip: None,
3986                                        padding_left: None,
3987                                        padding_right: None,
3988                                        data: None,
3989                                    },
3990                                    lsp::InlayHint {
3991                                        position: lsp::Position::new(19, 9),
3992                                        label: lsp::InlayHintLabel::String(": i33".to_owned()),
3993                                        kind: Some(lsp::InlayHintKind::TYPE),
3994                                        text_edits: None,
3995                                        tooltip: None,
3996                                        padding_left: None,
3997                                        padding_right: None,
3998                                        data: None,
3999                                    },
4000                                ]))
4001                            } else if params.text_document.uri
4002                                == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
4003                            {
4004                                Ok(Some(vec![
4005                                    lsp::InlayHint {
4006                                        position: lsp::Position::new(1, 10),
4007                                        label: lsp::InlayHintLabel::String(": i34".to_owned()),
4008                                        kind: Some(lsp::InlayHintKind::TYPE),
4009                                        text_edits: None,
4010                                        tooltip: None,
4011                                        padding_left: None,
4012                                        padding_right: None,
4013                                        data: None,
4014                                    },
4015                                    lsp::InlayHint {
4016                                        position: lsp::Position::new(29, 10),
4017                                        label: lsp::InlayHintLabel::String(": i35".to_owned()),
4018                                        kind: Some(lsp::InlayHintKind::TYPE),
4019                                        text_edits: None,
4020                                        tooltip: None,
4021                                        padding_left: None,
4022                                        padding_right: None,
4023                                        data: None,
4024                                    },
4025                                ]))
4026                            } else {
4027                                panic!("Unexpected file path {:?}", params.text_document.uri);
4028                            }
4029                        },
4030                    );
4031                })),
4032                ..FakeLspAdapter::default()
4033            },
4034        );
4035
4036        let (buffer_1, _handle_1) = project
4037            .update(cx, |project, cx| {
4038                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
4039            })
4040            .await
4041            .unwrap();
4042        let (buffer_2, _handle_2) = project
4043            .update(cx, |project, cx| {
4044                project.open_local_buffer_with_lsp(path!("/a/lib.rs"), cx)
4045            })
4046            .await
4047            .unwrap();
4048        let multi_buffer = cx.new(|cx| {
4049            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
4050            multibuffer.set_excerpts_for_path(
4051                PathKey::sorted(0),
4052                buffer_2.clone(),
4053                [
4054                    Point::new(0, 0)..Point::new(10, 0),
4055                    Point::new(23, 0)..Point::new(34, 0),
4056                ],
4057                0,
4058                cx,
4059            );
4060            multibuffer.set_excerpts_for_path(
4061                PathKey::sorted(1),
4062                buffer_1.clone(),
4063                [
4064                    Point::new(0, 0)..Point::new(10, 0),
4065                    Point::new(13, 0)..Point::new(23, 0),
4066                ],
4067                0,
4068                cx,
4069            );
4070            multibuffer
4071        });
4072
4073        let editor = cx.add_window(|window, cx| {
4074            let mut editor =
4075                Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx);
4076            editor.change_selections(SelectionEffects::default(), window, cx, |s| {
4077                s.select_ranges([Point::new(3, 3)..Point::new(3, 3)])
4078            });
4079            editor
4080        });
4081
4082        let fake_server = fake_servers.next().await.unwrap();
4083        let _fake_server_2 = fake_servers_2.next().await.unwrap();
4084        cx.executor().advance_clock(Duration::from_millis(100));
4085        cx.executor().run_until_parked();
4086
4087        editor
4088            .update(cx, |editor, _window, cx| {
4089                assert_eq!(
4090                    vec![
4091                        ": i32".to_string(),
4092                        ": i32".to_string(),
4093                        ": i33".to_string(),
4094                        ": i33".to_string(),
4095                        ": i34".to_string(),
4096                        ": i34".to_string(),
4097                        ": i35".to_string(),
4098                        ": i35".to_string(),
4099                    ],
4100                    sorted_cached_hint_labels(editor, cx),
4101                    "We receive duplicate hints from 2 servers and cache them all"
4102                );
4103                assert_eq!(
4104                    vec![
4105                        ": i34".to_string(),
4106                        ": i35".to_string(),
4107                        ": i32".to_string(),
4108                        ": i33".to_string(),
4109                    ],
4110                    visible_hint_labels(editor, cx),
4111                    "lib.rs is added before main.rs , so its excerpts should be visible first; hints should be deduplicated per label"
4112                );
4113            })
4114            .unwrap();
4115        assert_eq!(
4116            requests_count.load(Ordering::Acquire),
4117            2,
4118            "Should have queried hints once per each file"
4119        );
4120
4121        // Scroll all the way down so the 1st buffer is out of sight.
4122        // The selection is on the 1st buffer still.
4123        editor
4124            .update(cx, |editor, window, cx| {
4125                editor.scroll_screen(&ScrollAmount::Line(88.0), window, cx);
4126            })
4127            .unwrap();
4128        // Emulate a language server refresh request, coming in the background..
4129        editor
4130            .update(cx, |editor, _, cx| {
4131                editor.refresh_inlay_hints(
4132                    InlayHintRefreshReason::RefreshRequested {
4133                        server_id: fake_server.server.server_id(),
4134                        request_id: Some(1),
4135                    },
4136                    cx,
4137                );
4138            })
4139            .unwrap();
4140        // Edit the 1st buffer while scrolled down and not seeing that.
4141        // The edit will auto scroll to the edit (1st buffer).
4142        editor
4143            .update(cx, |editor, window, cx| {
4144                editor.handle_input("a", window, cx);
4145            })
4146            .unwrap();
4147        // Add more racy additive hint tasks.
4148        editor
4149            .update(cx, |editor, window, cx| {
4150                editor.scroll_screen(&ScrollAmount::Line(0.2), window, cx);
4151            })
4152            .unwrap();
4153
4154        cx.executor().advance_clock(Duration::from_millis(1000));
4155        cx.executor().run_until_parked();
4156        editor
4157            .update(cx, |editor, _window, cx| {
4158                assert_eq!(
4159                    vec![
4160                        ": i32".to_string(),
4161                        ": i32".to_string(),
4162                        ": i33".to_string(),
4163                        ": i33".to_string(),
4164                        ": i34".to_string(),
4165                        ": i34".to_string(),
4166                        ": i35".to_string(),
4167                        ": i35".to_string(),
4168                    ],
4169                    sorted_cached_hint_labels(editor, cx),
4170                    "No hint changes/duplicates should occur in the cache",
4171                );
4172                assert_eq!(
4173                    vec![
4174                        ": i34".to_string(),
4175                        ": i35".to_string(),
4176                        ": i32".to_string(),
4177                        ": i33".to_string(),
4178                    ],
4179                    visible_hint_labels(editor, cx),
4180                    "No hint changes/duplicates should occur in the editor excerpts",
4181                );
4182            })
4183            .unwrap();
4184        assert_eq!(
4185            requests_count.load(Ordering::Acquire),
4186            4,
4187            "Should have queried hints once more per each file, after editing the file once"
4188        );
4189    }
4190
4191    #[gpui::test]
4192    async fn test_edit_then_scroll_race(cx: &mut gpui::TestAppContext) {
4193        // Bug 1: An edit fires with a long debounce, and a scroll brings new lines
4194        // before that debounce elapses. The edit task's apply_fetched_hints removes
4195        // ALL visible hints (including the scroll-added ones) but only adds back
4196        // hints for its own chunks. The scroll chunk remains in hint_chunk_fetching,
4197        // so it is never re-queried, leaving it permanently empty.
4198        init_test(cx, &|settings| {
4199            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
4200                enabled: Some(true),
4201                edit_debounce_ms: Some(700),
4202                scroll_debounce_ms: Some(50),
4203                show_type_hints: Some(true),
4204                show_parameter_hints: Some(true),
4205                show_other_hints: Some(true),
4206                ..InlayHintSettingsContent::default()
4207            })
4208        });
4209
4210        let fs = FakeFs::new(cx.background_executor.clone());
4211        let mut file_content = String::from("fn main() {\n");
4212        for i in 0..150 {
4213            file_content.push_str(&format!("    let v{i} = {i};\n"));
4214        }
4215        file_content.push_str("}\n");
4216        fs.insert_tree(
4217            path!("/a"),
4218            json!({
4219                "main.rs": file_content,
4220                "other.rs": "// Test file",
4221            }),
4222        )
4223        .await;
4224
4225        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
4226        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4227        language_registry.add(rust_lang());
4228
4229        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
4230        let mut fake_servers = language_registry.register_fake_lsp(
4231            "Rust",
4232            FakeLspAdapter {
4233                capabilities: lsp::ServerCapabilities {
4234                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4235                    ..lsp::ServerCapabilities::default()
4236                },
4237                initializer: Some(Box::new({
4238                    let lsp_request_ranges = lsp_request_ranges.clone();
4239                    move |fake_server| {
4240                        let lsp_request_ranges = lsp_request_ranges.clone();
4241                        fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
4242                            move |params, _| {
4243                                let lsp_request_ranges = lsp_request_ranges.clone();
4244                                async move {
4245                                    lsp_request_ranges.lock().push(params.range);
4246                                    let start_line = params.range.start.line;
4247                                    Ok(Some(vec![lsp::InlayHint {
4248                                        position: lsp::Position::new(start_line + 1, 9),
4249                                        label: lsp::InlayHintLabel::String(format!(
4250                                            "chunk_{start_line}"
4251                                        )),
4252                                        kind: Some(lsp::InlayHintKind::TYPE),
4253                                        text_edits: None,
4254                                        tooltip: None,
4255                                        padding_left: None,
4256                                        padding_right: None,
4257                                        data: None,
4258                                    }]))
4259                                }
4260                            },
4261                        );
4262                    }
4263                })),
4264                ..FakeLspAdapter::default()
4265            },
4266        );
4267
4268        let buffer = project
4269            .update(cx, |project, cx| {
4270                project.open_local_buffer(path!("/a/main.rs"), cx)
4271            })
4272            .await
4273            .unwrap();
4274        let editor =
4275            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
4276        cx.executor().run_until_parked();
4277        let _fake_server = fake_servers.next().await.unwrap();
4278
4279        editor
4280            .update(cx, |editor, window, cx| {
4281                editor.set_visible_line_count(50.0, window, cx);
4282                editor.set_visible_column_count(120.0);
4283                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4284            })
4285            .unwrap();
4286        cx.executor().advance_clock(Duration::from_millis(100));
4287        cx.executor().run_until_parked();
4288
4289        editor
4290            .update(cx, |editor, _window, cx| {
4291                let visible = visible_hint_labels(editor, cx);
4292                assert!(
4293                    visible.iter().any(|h| h.starts_with("chunk_0")),
4294                    "Should have chunk_0 hints initially, got: {visible:?}"
4295                );
4296            })
4297            .unwrap();
4298
4299        lsp_request_ranges.lock().clear();
4300
4301        // Step 1: Make an edit → triggers BufferEdited with 700ms debounce.
4302        editor
4303            .update(cx, |editor, window, cx| {
4304                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4305                    s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
4306                });
4307                editor.handle_input("x", window, cx);
4308            })
4309            .unwrap();
4310        // Let the BufferEdited event propagate and the edit task get spawned.
4311        cx.executor().run_until_parked();
4312
4313        // Step 2: Scroll down to reveal a new chunk, then trigger NewLinesShown.
4314        // This spawns a scroll task with the shorter 50ms debounce.
4315        editor
4316            .update(cx, |editor, window, cx| {
4317                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
4318            })
4319            .unwrap();
4320        // Explicitly trigger NewLinesShown for the new visible range.
4321        editor
4322            .update(cx, |editor, _window, cx| {
4323                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4324            })
4325            .unwrap();
4326
4327        // Step 3: Advance clock past scroll debounce (50ms) but NOT past edit
4328        // debounce (700ms). The scroll task completes and adds hints for the
4329        // new chunk.
4330        cx.executor().advance_clock(Duration::from_millis(100));
4331        cx.executor().run_until_parked();
4332
4333        // The scroll task's apply_fetched_hints also processes
4334        // invalidate_hints_for_buffers (set by the earlier BufferEdited), which
4335        // removes the old chunk_0 hint. Only the scroll chunk's hint remains.
4336        editor
4337            .update(cx, |editor, _window, cx| {
4338                let visible = visible_hint_labels(editor, cx);
4339                assert!(
4340                    visible.iter().any(|h| h.starts_with("chunk_50")),
4341                    "After scroll task completes, the scroll chunk's hints should be \
4342                     present, got: {visible:?}"
4343                );
4344            })
4345            .unwrap();
4346
4347        // Step 4: Advance clock past the edit debounce (700ms). The edit task
4348        // completes, calling apply_fetched_hints with should_invalidate()=true,
4349        // which removes ALL visible hints (including the scroll chunk's) but only
4350        // adds back hints for its own chunks (chunk_0).
4351        cx.executor().advance_clock(Duration::from_millis(700));
4352        cx.executor().run_until_parked();
4353
4354        // At this point the edit task has:
4355        //   - removed chunk_50's hint (via should_invalidate removing all visible)
4356        //   - added chunk_0's hint (from its own fetch)
4357        //   - (with fix) cleared chunk_50 from hint_chunk_fetching
4358        // Without the fix, chunk_50 is stuck in hint_chunk_fetching and will
4359        // never be re-queried by NewLinesShown.
4360
4361        // Step 5: Trigger NewLinesShown to give the system a chance to re-fetch
4362        // any chunks whose hints were lost.
4363        editor
4364            .update(cx, |editor, _window, cx| {
4365                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4366            })
4367            .unwrap();
4368        cx.executor().advance_clock(Duration::from_millis(100));
4369        cx.executor().run_until_parked();
4370
4371        editor
4372            .update(cx, |editor, _window, cx| {
4373                let visible = visible_hint_labels(editor, cx);
4374                assert!(
4375                    visible.iter().any(|h| h.starts_with("chunk_0")),
4376                    "chunk_0 hints (from edit task) should be present. Got: {visible:?}"
4377                );
4378                assert!(
4379                    visible.iter().any(|h| h.starts_with("chunk_50")),
4380                    "chunk_50 hints should have been re-fetched after NewLinesShown. \
4381                     Bug 1: the scroll chunk's hints were removed by the edit task \
4382                     and the chunk was stuck in hint_chunk_fetching, preventing \
4383                     re-fetch. Got: {visible:?}"
4384                );
4385            })
4386            .unwrap();
4387    }
4388
4389    #[gpui::test]
4390    async fn test_refresh_requested_multi_server(cx: &mut gpui::TestAppContext) {
4391        // Bug 2: When one LSP server sends workspace/inlayHint/refresh, the editor
4392        // wipes all tracking state via clear(), then spawns tasks that call
4393        // LspStore::inlay_hints with for_server=Some(requesting_server). The LspStore
4394        // filters out other servers' cached hints via the for_server guard, so only
4395        // the requesting server's hints are returned. apply_fetched_hints removes ALL
4396        // visible hints (should_invalidate()=true) but only adds back the requesting
4397        // server's hints. Other servers' hints disappear permanently.
4398        init_test(cx, &|settings| {
4399            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
4400                enabled: Some(true),
4401                edit_debounce_ms: Some(0),
4402                scroll_debounce_ms: Some(0),
4403                show_type_hints: Some(true),
4404                show_parameter_hints: Some(true),
4405                show_other_hints: Some(true),
4406                ..InlayHintSettingsContent::default()
4407            })
4408        });
4409
4410        let fs = FakeFs::new(cx.background_executor.clone());
4411        fs.insert_tree(
4412            path!("/a"),
4413            json!({
4414                "main.rs": "fn main() { let x = 1; } // padding to keep hints from being trimmed",
4415                "other.rs": "// Test file",
4416            }),
4417        )
4418        .await;
4419
4420        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
4421        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4422        language_registry.add(rust_lang());
4423
4424        // Server A returns a hint labeled "server_a".
4425        let server_a_request_count = Arc::new(AtomicU32::new(0));
4426        let mut fake_servers_a = language_registry.register_fake_lsp(
4427            "Rust",
4428            FakeLspAdapter {
4429                name: "rust-analyzer",
4430                capabilities: lsp::ServerCapabilities {
4431                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4432                    ..lsp::ServerCapabilities::default()
4433                },
4434                initializer: Some(Box::new({
4435                    let server_a_request_count = server_a_request_count.clone();
4436                    move |fake_server| {
4437                        let server_a_request_count = server_a_request_count.clone();
4438                        fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
4439                            move |_params, _| {
4440                                let count =
4441                                    server_a_request_count.fetch_add(1, Ordering::Release) + 1;
4442                                async move {
4443                                    Ok(Some(vec![lsp::InlayHint {
4444                                        position: lsp::Position::new(0, 9),
4445                                        label: lsp::InlayHintLabel::String(format!(
4446                                            "server_a_{count}"
4447                                        )),
4448                                        kind: Some(lsp::InlayHintKind::TYPE),
4449                                        text_edits: None,
4450                                        tooltip: None,
4451                                        padding_left: None,
4452                                        padding_right: None,
4453                                        data: None,
4454                                    }]))
4455                                }
4456                            },
4457                        );
4458                    }
4459                })),
4460                ..FakeLspAdapter::default()
4461            },
4462        );
4463
4464        // Server B returns a hint labeled "server_b" at a different position.
4465        let server_b_request_count = Arc::new(AtomicU32::new(0));
4466        let mut fake_servers_b = language_registry.register_fake_lsp(
4467            "Rust",
4468            FakeLspAdapter {
4469                name: "secondary-ls",
4470                capabilities: lsp::ServerCapabilities {
4471                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4472                    ..lsp::ServerCapabilities::default()
4473                },
4474                initializer: Some(Box::new({
4475                    let server_b_request_count = server_b_request_count.clone();
4476                    move |fake_server| {
4477                        let server_b_request_count = server_b_request_count.clone();
4478                        fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
4479                            move |_params, _| {
4480                                let count =
4481                                    server_b_request_count.fetch_add(1, Ordering::Release) + 1;
4482                                async move {
4483                                    Ok(Some(vec![lsp::InlayHint {
4484                                        position: lsp::Position::new(0, 22),
4485                                        label: lsp::InlayHintLabel::String(format!(
4486                                            "server_b_{count}"
4487                                        )),
4488                                        kind: Some(lsp::InlayHintKind::TYPE),
4489                                        text_edits: None,
4490                                        tooltip: None,
4491                                        padding_left: None,
4492                                        padding_right: None,
4493                                        data: None,
4494                                    }]))
4495                                }
4496                            },
4497                        );
4498                    }
4499                })),
4500                ..FakeLspAdapter::default()
4501            },
4502        );
4503
4504        let (buffer, _buffer_handle) = project
4505            .update(cx, |project, cx| {
4506                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
4507            })
4508            .await
4509            .unwrap();
4510        let editor =
4511            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
4512        cx.executor().run_until_parked();
4513
4514        let fake_server_a = fake_servers_a.next().await.unwrap();
4515        let _fake_server_b = fake_servers_b.next().await.unwrap();
4516
4517        editor
4518            .update(cx, |editor, window, cx| {
4519                editor.set_visible_line_count(50.0, window, cx);
4520                editor.set_visible_column_count(120.0);
4521                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4522            })
4523            .unwrap();
4524        cx.executor().advance_clock(Duration::from_millis(100));
4525        cx.executor().run_until_parked();
4526
4527        // Verify both servers' hints are present initially.
4528        editor
4529            .update(cx, |editor, _window, cx| {
4530                let visible = visible_hint_labels(editor, cx);
4531                let has_a = visible.iter().any(|h| h.starts_with("server_a"));
4532                let has_b = visible.iter().any(|h| h.starts_with("server_b"));
4533                assert!(
4534                    has_a && has_b,
4535                    "Both servers should have hints initially. Got: {visible:?}"
4536                );
4537            })
4538            .unwrap();
4539
4540        // Trigger RefreshRequested from server A. This should re-fetch server A's
4541        // hints while keeping server B's hints intact.
4542        editor
4543            .update(cx, |editor, _window, cx| {
4544                editor.refresh_inlay_hints(
4545                    InlayHintRefreshReason::RefreshRequested {
4546                        server_id: fake_server_a.server.server_id(),
4547                        request_id: Some(1),
4548                    },
4549                    cx,
4550                );
4551            })
4552            .unwrap();
4553        cx.executor().advance_clock(Duration::from_millis(100));
4554        cx.executor().run_until_parked();
4555
4556        // Also trigger NewLinesShown to give the system a chance to recover
4557        // any chunks that might have been cleared.
4558        editor
4559            .update(cx, |editor, _window, cx| {
4560                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4561            })
4562            .unwrap();
4563        cx.executor().advance_clock(Duration::from_millis(100));
4564        cx.executor().run_until_parked();
4565
4566        editor
4567            .update(cx, |editor, _window, cx| {
4568                let visible = visible_hint_labels(editor, cx);
4569                let has_a = visible.iter().any(|h| h.starts_with("server_a"));
4570                let has_b = visible.iter().any(|h| h.starts_with("server_b"));
4571                assert!(
4572                    has_a,
4573                    "Server A hints should be present after its own refresh. Got: {visible:?}"
4574                );
4575                assert!(
4576                    has_b,
4577                    "Server B hints should NOT be lost when server A triggers \
4578                     RefreshRequested. Bug 2: clear() wipes all tracking, then \
4579                     LspStore filters out server B's cached hints via the for_server \
4580                     guard, and apply_fetched_hints removes all visible hints but only \
4581                     adds back server A's. Got: {visible:?}"
4582                );
4583            })
4584            .unwrap();
4585    }
4586
4587    #[gpui::test]
4588    async fn test_multi_language_multibuffer_no_duplicate_hints(cx: &mut gpui::TestAppContext) {
4589        init_test(cx, &|settings| {
4590            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
4591                show_value_hints: Some(true),
4592                enabled: Some(true),
4593                edit_debounce_ms: Some(0),
4594                scroll_debounce_ms: Some(0),
4595                show_type_hints: Some(true),
4596                show_parameter_hints: Some(true),
4597                show_other_hints: Some(true),
4598                show_background: Some(false),
4599                toggle_on_modifiers_press: None,
4600            })
4601        });
4602
4603        let fs = FakeFs::new(cx.background_executor.clone());
4604        fs.insert_tree(
4605            path!("/a"),
4606            json!({
4607                "main.rs": "fn main() { let x = 1; } // padding to keep hints from being trimmed",
4608                "index.ts": "const y = 2; // padding to keep hints from being trimmed in typescript",
4609            }),
4610        )
4611        .await;
4612
4613        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
4614        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4615
4616        let mut rs_fake_servers = None;
4617        let mut ts_fake_servers = None;
4618        for (name, path_suffix) in [("Rust", "rs"), ("TypeScript", "ts")] {
4619            language_registry.add(Arc::new(Language::new(
4620                LanguageConfig {
4621                    name: name.into(),
4622                    matcher: LanguageMatcher {
4623                        path_suffixes: vec![path_suffix.to_string()],
4624                        ..Default::default()
4625                    },
4626                    ..Default::default()
4627                },
4628                Some(tree_sitter_rust::LANGUAGE.into()),
4629            )));
4630            let fake_servers = language_registry.register_fake_lsp(
4631                name,
4632                FakeLspAdapter {
4633                    name,
4634                    capabilities: lsp::ServerCapabilities {
4635                        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4636                        ..Default::default()
4637                    },
4638                    initializer: Some(Box::new({
4639                        move |fake_server| {
4640                            let request_count = Arc::new(AtomicU32::new(0));
4641                            fake_server
4642                                .set_request_handler::<lsp::request::InlayHintRequest, _, _>(
4643                                    move |params, _| {
4644                                        let count =
4645                                            request_count.fetch_add(1, Ordering::Release) + 1;
4646                                        let prefix = match name {
4647                                            "Rust" => "rs_hint",
4648                                            "TypeScript" => "ts_hint",
4649                                            other => panic!("Unexpected language: {other}"),
4650                                        };
4651                                        async move {
4652                                            Ok(Some(vec![lsp::InlayHint {
4653                                                position: params.range.start,
4654                                                label: lsp::InlayHintLabel::String(format!(
4655                                                    "{prefix}_{count}"
4656                                                )),
4657                                                kind: None,
4658                                                text_edits: None,
4659                                                tooltip: None,
4660                                                padding_left: None,
4661                                                padding_right: None,
4662                                                data: None,
4663                                            }]))
4664                                        }
4665                                    },
4666                                );
4667                        }
4668                    })),
4669                    ..Default::default()
4670                },
4671            );
4672            match name {
4673                "Rust" => rs_fake_servers = Some(fake_servers),
4674                "TypeScript" => ts_fake_servers = Some(fake_servers),
4675                _ => unreachable!(),
4676            }
4677        }
4678
4679        let (rs_buffer, _rs_handle) = project
4680            .update(cx, |project, cx| {
4681                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
4682            })
4683            .await
4684            .unwrap();
4685        let (ts_buffer, _ts_handle) = project
4686            .update(cx, |project, cx| {
4687                project.open_local_buffer_with_lsp(path!("/a/index.ts"), cx)
4688            })
4689            .await
4690            .unwrap();
4691
4692        let multi_buffer = cx.new(|cx| {
4693            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
4694            multibuffer.set_excerpts_for_path(
4695                PathKey::sorted(0),
4696                rs_buffer.clone(),
4697                [Point::new(0, 0)..Point::new(1, 0)],
4698                0,
4699                cx,
4700            );
4701            multibuffer.set_excerpts_for_path(
4702                PathKey::sorted(1),
4703                ts_buffer.clone(),
4704                [Point::new(0, 0)..Point::new(1, 0)],
4705                0,
4706                cx,
4707            );
4708            multibuffer
4709        });
4710
4711        cx.executor().run_until_parked();
4712        let editor = cx.add_window(|window, cx| {
4713            Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
4714        });
4715
4716        let _rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
4717        let _ts_fake_server = ts_fake_servers.unwrap().next().await.unwrap();
4718        cx.executor().advance_clock(Duration::from_millis(100));
4719        cx.executor().run_until_parked();
4720
4721        // Verify initial state: both languages have exactly one hint each
4722        editor
4723            .update(cx, |editor, _window, cx| {
4724                let visible = visible_hint_labels(editor, cx);
4725                let rs_hints: Vec<_> = visible
4726                    .iter()
4727                    .filter(|h| h.starts_with("rs_hint"))
4728                    .collect();
4729                let ts_hints: Vec<_> = visible
4730                    .iter()
4731                    .filter(|h| h.starts_with("ts_hint"))
4732                    .collect();
4733                assert_eq!(
4734                    rs_hints.len(),
4735                    1,
4736                    "Should have exactly 1 Rust hint initially, got: {rs_hints:?}"
4737                );
4738                assert_eq!(
4739                    ts_hints.len(),
4740                    1,
4741                    "Should have exactly 1 TypeScript hint initially, got: {ts_hints:?}"
4742                );
4743            })
4744            .unwrap();
4745
4746        // Edit the Rust buffer — triggers BufferEdited(rust_buffer_id).
4747        // The language filter in refresh_inlay_hints excludes TypeScript excerpts
4748        // from processing, but the global clear() wipes added_hints for ALL buffers.
4749        editor
4750            .update(cx, |editor, window, cx| {
4751                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4752                    s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
4753                });
4754                editor.handle_input("x", window, cx);
4755            })
4756            .unwrap();
4757        cx.executor().run_until_parked();
4758
4759        // Trigger NewLinesShown — this causes TypeScript chunks to be re-fetched
4760        // because hint_chunk_fetching was wiped by clear(). The cached hints pass
4761        // the added_hints.insert(...).is_none() filter (also wiped) and get inserted
4762        // alongside the still-displayed copies, causing duplicates.
4763        editor
4764            .update(cx, |editor, _window, cx| {
4765                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4766            })
4767            .unwrap();
4768        cx.executor().run_until_parked();
4769
4770        // Assert: TypeScript hints must NOT be duplicated
4771        editor
4772            .update(cx, |editor, _window, cx| {
4773                let visible = visible_hint_labels(editor, cx);
4774                let ts_hints: Vec<_> = visible
4775                    .iter()
4776                    .filter(|h| h.starts_with("ts_hint"))
4777                    .collect();
4778                assert_eq!(
4779                    ts_hints.len(),
4780                    1,
4781                    "TypeScript hints should NOT be duplicated after editing Rust buffer \
4782                     and triggering NewLinesShown. Got: {ts_hints:?}"
4783                );
4784
4785                let rs_hints: Vec<_> = visible
4786                    .iter()
4787                    .filter(|h| h.starts_with("rs_hint"))
4788                    .collect();
4789                assert_eq!(
4790                    rs_hints.len(),
4791                    1,
4792                    "Rust hints should still be present after editing. Got: {rs_hints:?}"
4793                );
4794            })
4795            .unwrap();
4796    }
4797
4798    pub(crate) fn init_test(cx: &mut TestAppContext, f: &dyn Fn(&mut AllLanguageSettingsContent)) {
4799        cx.update(|cx| {
4800            let settings_store = SettingsStore::test(cx);
4801            cx.set_global(settings_store);
4802            theme::init(theme::LoadThemes::JustBase, cx);
4803            release_channel::init(semver::Version::new(0, 0, 0), cx);
4804            crate::init(cx);
4805        });
4806
4807        update_test_language_settings(cx, f);
4808    }
4809
4810    async fn prepare_test_objects(
4811        cx: &mut TestAppContext,
4812        initialize: impl 'static + Send + Fn(&mut FakeLanguageServer, &'static str) + Send + Sync,
4813    ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
4814        let fs = FakeFs::new(cx.background_executor.clone());
4815        fs.insert_tree(
4816            path!("/a"),
4817            json!({
4818                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
4819                "other.rs": "// Test file",
4820            }),
4821        )
4822        .await;
4823
4824        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
4825        let file_path = path!("/a/main.rs");
4826
4827        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4828        language_registry.add(rust_lang());
4829        let mut fake_servers = language_registry.register_fake_lsp(
4830            "Rust",
4831            FakeLspAdapter {
4832                capabilities: lsp::ServerCapabilities {
4833                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4834                    ..lsp::ServerCapabilities::default()
4835                },
4836                initializer: Some(Box::new(move |server| initialize(server, file_path))),
4837                ..FakeLspAdapter::default()
4838            },
4839        );
4840
4841        let buffer = project
4842            .update(cx, |project, cx| {
4843                project.open_local_buffer(path!("/a/main.rs"), cx)
4844            })
4845            .await
4846            .unwrap();
4847        let editor =
4848            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
4849
4850        editor
4851            .update(cx, |editor, _, cx| {
4852                assert!(cached_hint_labels(editor, cx).is_empty());
4853                assert!(visible_hint_labels(editor, cx).is_empty());
4854            })
4855            .unwrap();
4856
4857        cx.executor().run_until_parked();
4858        let fake_server = fake_servers.next().await.unwrap();
4859
4860        // Establish a viewport so the editor considers itself visible and the hint refresh
4861        // pipeline runs. Then explicitly trigger a refresh.
4862        editor
4863            .update(cx, |editor, window, cx| {
4864                editor.set_visible_line_count(50.0, window, cx);
4865                editor.set_visible_column_count(120.0);
4866                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4867            })
4868            .unwrap();
4869        cx.executor().run_until_parked();
4870        (file_path, editor, fake_server)
4871    }
4872
4873    // Inlay hints in the cache are stored per excerpt as a key, and those keys are guaranteed to be ordered same as in the multi buffer.
4874    // Ensure a stable order for testing.
4875    fn sorted_cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4876        let mut labels = cached_hint_labels(editor, cx);
4877        labels.sort_by(|a, b| natural_sort(a, b));
4878        labels
4879    }
4880
4881    pub fn cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4882        let lsp_store = editor.project().unwrap().read(cx).lsp_store();
4883
4884        let mut all_cached_labels = Vec::new();
4885        let mut all_fetched_hints = Vec::new();
4886        for buffer in editor.buffer.read(cx).all_buffers() {
4887            lsp_store.update(cx, |lsp_store, cx| {
4888                let hints = lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
4889                all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
4890                    let mut label = hint.text().to_string();
4891                    if hint.padding_left {
4892                        label.insert(0, ' ');
4893                    }
4894                    if hint.padding_right {
4895                        label.push_str(" ");
4896                    }
4897                    label
4898                }));
4899                all_fetched_hints.extend(hints.all_fetched_hints());
4900            });
4901        }
4902
4903        all_cached_labels
4904    }
4905
4906    pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
4907        Editor::visible_inlay_hints(editor.display_map.read(cx))
4908            .map(|hint| hint.text().to_string())
4909            .collect()
4910    }
4911
4912    fn allowed_hint_kinds_for_editor(editor: &Editor) -> HashSet<Option<InlayHintKind>> {
4913        editor
4914            .inlay_hints
4915            .as_ref()
4916            .unwrap()
4917            .allowed_hint_kinds
4918            .clone()
4919    }
4920}