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