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