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