inlay_hint_cache.rs

   1/// Stores and updates all data received from LSP <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint">textDocument/inlayHint</a> requests.
   2/// Has nothing to do with other inlays, e.g. copilot suggestions — those are stored elsewhere.
   3/// On every update, cache may query for more inlay hints and update inlays on the screen.
   4///
   5/// Inlays stored on screen are in [`crate::display_map::inlay_map`] and this cache is the only way to update any inlay hint data in the visible hints in the inlay map.
   6/// For determining the update to the `inlay_map`, the cache requires a list of visible inlay hints — all other hints are not relevant and their separate updates are not influencing the cache work.
   7///
   8/// Due to the way the data is stored for both visible inlays and the cache, every inlay (and inlay hint) collection is editor-specific, so a single buffer may have multiple sets of inlays of open on different panes.
   9use std::{
  10    cmp,
  11    ops::{ControlFlow, Range},
  12    sync::Arc,
  13    time::Duration,
  14};
  15
  16use crate::{
  17    Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot, display_map::Inlay,
  18};
  19use anyhow::Context as _;
  20use clock::Global;
  21use futures::future;
  22use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window};
  23use language::{
  24    Buffer, BufferSnapshot,
  25    language_settings::{InlayHintKind, InlayHintSettings},
  26};
  27use parking_lot::RwLock;
  28use project::{InlayHint, ResolveState};
  29
  30use collections::{HashMap, HashSet, hash_map};
  31use smol::lock::Semaphore;
  32use sum_tree::Bias;
  33use text::{BufferId, ToOffset, ToPoint};
  34use util::{ResultExt, post_inc};
  35
  36pub struct InlayHintCache {
  37    hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
  38    allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
  39    version: usize,
  40    pub(super) enabled: bool,
  41    modifiers_override: bool,
  42    enabled_in_settings: bool,
  43    update_tasks: HashMap<ExcerptId, TasksForRanges>,
  44    refresh_task: Task<()>,
  45    invalidate_debounce: Option<Duration>,
  46    append_debounce: Option<Duration>,
  47    lsp_request_limiter: Arc<Semaphore>,
  48}
  49
  50#[derive(Debug)]
  51struct TasksForRanges {
  52    tasks: Vec<Task<()>>,
  53    sorted_ranges: Vec<Range<language::Anchor>>,
  54}
  55
  56#[derive(Debug)]
  57struct CachedExcerptHints {
  58    version: usize,
  59    buffer_version: Global,
  60    buffer_id: BufferId,
  61    ordered_hints: Vec<InlayId>,
  62    hints_by_id: HashMap<InlayId, InlayHint>,
  63}
  64
  65/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts.
  66#[derive(Debug, Clone, Copy)]
  67pub(super) enum InvalidationStrategy {
  68    /// Hints reset is <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_inlayHint_refresh">requested</a> by the LSP server.
  69    /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation.
  70    ///
  71    /// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise.
  72    RefreshRequested,
  73    /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place.
  74    /// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence.
  75    BufferEdited,
  76    /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position.
  77    /// No invalidation should be done at all, all new hints are added to the cache.
  78    ///
  79    /// A special case is the settings change: in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other).
  80    /// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen.
  81    None,
  82}
  83
  84/// A splice to send into the `inlay_map` for updating the visible inlays on the screen.
  85/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes.
  86/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead.
  87/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.
  88#[derive(Debug, Default)]
  89pub(super) struct InlaySplice {
  90    pub to_remove: Vec<InlayId>,
  91    pub to_insert: Vec<Inlay>,
  92}
  93
  94#[derive(Debug)]
  95struct ExcerptHintsUpdate {
  96    excerpt_id: ExcerptId,
  97    remove_from_visible: HashSet<InlayId>,
  98    remove_from_cache: HashSet<InlayId>,
  99    add_to_cache: Vec<InlayHint>,
 100}
 101
 102#[derive(Debug, Clone, Copy)]
 103struct ExcerptQuery {
 104    buffer_id: BufferId,
 105    excerpt_id: ExcerptId,
 106    cache_version: usize,
 107    invalidate: InvalidationStrategy,
 108    reason: &'static str,
 109}
 110
 111impl InvalidationStrategy {
 112    fn should_invalidate(&self) -> bool {
 113        matches!(
 114            self,
 115            InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
 116        )
 117    }
 118}
 119
 120impl TasksForRanges {
 121    fn new(query_ranges: QueryRanges, task: Task<()>) -> Self {
 122        Self {
 123            tasks: vec![task],
 124            sorted_ranges: query_ranges.into_sorted_query_ranges(),
 125        }
 126    }
 127
 128    fn update_cached_tasks(
 129        &mut self,
 130        buffer_snapshot: &BufferSnapshot,
 131        query_ranges: QueryRanges,
 132        invalidate: InvalidationStrategy,
 133        spawn_task: impl FnOnce(QueryRanges) -> Task<()>,
 134    ) {
 135        let query_ranges = if invalidate.should_invalidate() {
 136            self.tasks.clear();
 137            self.sorted_ranges = query_ranges.clone().into_sorted_query_ranges();
 138            query_ranges
 139        } else {
 140            let mut non_cached_query_ranges = query_ranges;
 141            non_cached_query_ranges.before_visible = non_cached_query_ranges
 142                .before_visible
 143                .into_iter()
 144                .flat_map(|query_range| {
 145                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
 146                })
 147                .collect();
 148            non_cached_query_ranges.visible = non_cached_query_ranges
 149                .visible
 150                .into_iter()
 151                .flat_map(|query_range| {
 152                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
 153                })
 154                .collect();
 155            non_cached_query_ranges.after_visible = non_cached_query_ranges
 156                .after_visible
 157                .into_iter()
 158                .flat_map(|query_range| {
 159                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
 160                })
 161                .collect();
 162            non_cached_query_ranges
 163        };
 164
 165        if !query_ranges.is_empty() {
 166            self.tasks.push(spawn_task(query_ranges));
 167        }
 168    }
 169
 170    fn remove_cached_ranges_from_query(
 171        &mut self,
 172        buffer_snapshot: &BufferSnapshot,
 173        query_range: Range<language::Anchor>,
 174    ) -> Vec<Range<language::Anchor>> {
 175        let mut ranges_to_query = Vec::new();
 176        let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
 177        for cached_range in self
 178            .sorted_ranges
 179            .iter_mut()
 180            .skip_while(|cached_range| {
 181                cached_range
 182                    .end
 183                    .cmp(&query_range.start, buffer_snapshot)
 184                    .is_lt()
 185            })
 186            .take_while(|cached_range| {
 187                cached_range
 188                    .start
 189                    .cmp(&query_range.end, buffer_snapshot)
 190                    .is_le()
 191            })
 192        {
 193            match latest_cached_range {
 194                Some(latest_cached_range) => {
 195                    if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset
 196                    {
 197                        ranges_to_query.push(latest_cached_range.end..cached_range.start);
 198                        cached_range.start = latest_cached_range.end;
 199                    }
 200                }
 201                None => {
 202                    if query_range
 203                        .start
 204                        .cmp(&cached_range.start, buffer_snapshot)
 205                        .is_lt()
 206                    {
 207                        ranges_to_query.push(query_range.start..cached_range.start);
 208                        cached_range.start = query_range.start;
 209                    }
 210                }
 211            }
 212            latest_cached_range = Some(cached_range);
 213        }
 214
 215        match latest_cached_range {
 216            Some(latest_cached_range) => {
 217                if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset {
 218                    ranges_to_query.push(latest_cached_range.end..query_range.end);
 219                    latest_cached_range.end = query_range.end;
 220                }
 221            }
 222            None => {
 223                ranges_to_query.push(query_range.clone());
 224                self.sorted_ranges.push(query_range);
 225                self.sorted_ranges
 226                    .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot));
 227            }
 228        }
 229
 230        ranges_to_query
 231    }
 232
 233    fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range<language::Anchor>) {
 234        self.sorted_ranges = self
 235            .sorted_ranges
 236            .drain(..)
 237            .filter_map(|mut cached_range| {
 238                if cached_range.start.cmp(&range.end, buffer).is_gt()
 239                    || cached_range.end.cmp(&range.start, buffer).is_lt()
 240                {
 241                    Some(vec![cached_range])
 242                } else if cached_range.start.cmp(&range.start, buffer).is_ge()
 243                    && cached_range.end.cmp(&range.end, buffer).is_le()
 244                {
 245                    None
 246                } else if range.start.cmp(&cached_range.start, buffer).is_ge()
 247                    && range.end.cmp(&cached_range.end, buffer).is_le()
 248                {
 249                    Some(vec![
 250                        cached_range.start..range.start,
 251                        range.end..cached_range.end,
 252                    ])
 253                } else if cached_range.start.cmp(&range.start, buffer).is_ge() {
 254                    cached_range.start = range.end;
 255                    Some(vec![cached_range])
 256                } else {
 257                    cached_range.end = range.start;
 258                    Some(vec![cached_range])
 259                }
 260            })
 261            .flatten()
 262            .collect();
 263    }
 264}
 265
 266impl InlayHintCache {
 267    pub(super) fn new(inlay_hint_settings: InlayHintSettings) -> Self {
 268        Self {
 269            allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
 270            enabled: inlay_hint_settings.enabled,
 271            modifiers_override: false,
 272            enabled_in_settings: inlay_hint_settings.enabled,
 273            hints: HashMap::default(),
 274            update_tasks: HashMap::default(),
 275            refresh_task: Task::ready(()),
 276            invalidate_debounce: debounce_value(inlay_hint_settings.edit_debounce_ms),
 277            append_debounce: debounce_value(inlay_hint_settings.scroll_debounce_ms),
 278            version: 0,
 279            lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)),
 280        }
 281    }
 282
 283    /// Checks inlay hint settings for enabled hint kinds and general enabled state.
 284    /// Generates corresponding inlay_map splice updates on settings changes.
 285    /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries.
 286    pub(super) fn update_settings(
 287        &mut self,
 288        multi_buffer: &Entity<MultiBuffer>,
 289        new_hint_settings: InlayHintSettings,
 290        visible_hints: Vec<Inlay>,
 291        cx: &mut Context<Editor>,
 292    ) -> ControlFlow<Option<InlaySplice>> {
 293        let old_enabled = self.enabled;
 294        // If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay
 295        // hint visibility changes when other settings change (such as theme).
 296        //
 297        // Another option might be to store whether the user has manually toggled inlay hint
 298        // visibility, and prefer this. This could lead to confusion as it means inlay hint
 299        // visibility would not change when updating the setting if they were ever toggled.
 300        if new_hint_settings.enabled != self.enabled_in_settings {
 301            self.enabled = new_hint_settings.enabled;
 302            self.enabled_in_settings = new_hint_settings.enabled;
 303            self.modifiers_override = false;
 304        };
 305        self.invalidate_debounce = debounce_value(new_hint_settings.edit_debounce_ms);
 306        self.append_debounce = debounce_value(new_hint_settings.scroll_debounce_ms);
 307        let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
 308        match (old_enabled, self.enabled) {
 309            (false, false) => {
 310                self.allowed_hint_kinds = new_allowed_hint_kinds;
 311                ControlFlow::Break(None)
 312            }
 313            (true, true) => {
 314                if new_allowed_hint_kinds == self.allowed_hint_kinds {
 315                    ControlFlow::Break(None)
 316                } else {
 317                    let new_splice = self.new_allowed_hint_kinds_splice(
 318                        multi_buffer,
 319                        &visible_hints,
 320                        &new_allowed_hint_kinds,
 321                        cx,
 322                    );
 323                    if new_splice.is_some() {
 324                        self.version += 1;
 325                        self.allowed_hint_kinds = new_allowed_hint_kinds;
 326                    }
 327                    ControlFlow::Break(new_splice)
 328                }
 329            }
 330            (true, false) => {
 331                self.modifiers_override = false;
 332                self.allowed_hint_kinds = new_allowed_hint_kinds;
 333                if self.hints.is_empty() {
 334                    ControlFlow::Break(None)
 335                } else {
 336                    self.clear();
 337                    ControlFlow::Break(Some(InlaySplice {
 338                        to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
 339                        to_insert: Vec::new(),
 340                    }))
 341                }
 342            }
 343            (false, true) => {
 344                self.modifiers_override = false;
 345                self.allowed_hint_kinds = new_allowed_hint_kinds;
 346                ControlFlow::Continue(())
 347            }
 348        }
 349    }
 350
 351    pub(super) fn modifiers_override(&mut self, new_override: bool) -> Option<bool> {
 352        if self.modifiers_override == new_override {
 353            return None;
 354        }
 355        self.modifiers_override = new_override;
 356        if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
 357        {
 358            self.clear();
 359            Some(false)
 360        } else {
 361            Some(true)
 362        }
 363    }
 364
 365    pub(super) fn toggle(&mut self, enabled: bool) -> bool {
 366        if self.enabled == enabled {
 367            return false;
 368        }
 369        self.enabled = enabled;
 370        self.modifiers_override = false;
 371        if !enabled {
 372            self.clear();
 373        }
 374        true
 375    }
 376
 377    /// If needed, queries LSP for new inlay hints, using the invalidation strategy given.
 378    /// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first,
 379    /// followed by the delayed queries of the same range above and below the visible one.
 380    /// This way, subsequent refresh invocations are less likely to trigger LSP queries for the invisible ranges.
 381    pub(super) fn spawn_hint_refresh(
 382        &mut self,
 383        reason_description: &'static str,
 384        excerpts_to_query: HashMap<ExcerptId, (Entity<Buffer>, Global, Range<usize>)>,
 385        invalidate: InvalidationStrategy,
 386        ignore_debounce: bool,
 387        cx: &mut Context<Editor>,
 388    ) -> Option<InlaySplice> {
 389        if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
 390        {
 391            return None;
 392        }
 393        let mut invalidated_hints = Vec::new();
 394        if invalidate.should_invalidate() {
 395            self.update_tasks
 396                .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
 397            self.hints.retain(|cached_excerpt, cached_hints| {
 398                let retain = excerpts_to_query.contains_key(cached_excerpt);
 399                if !retain {
 400                    invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied());
 401                }
 402                retain
 403            });
 404        }
 405        if excerpts_to_query.is_empty() && invalidated_hints.is_empty() {
 406            return None;
 407        }
 408
 409        let cache_version = self.version + 1;
 410        let debounce_duration = if ignore_debounce {
 411            None
 412        } else if invalidate.should_invalidate() {
 413            self.invalidate_debounce
 414        } else {
 415            self.append_debounce
 416        };
 417        self.refresh_task = cx.spawn(async move |editor, cx| {
 418            if let Some(debounce_duration) = debounce_duration {
 419                cx.background_executor().timer(debounce_duration).await;
 420            }
 421
 422            editor
 423                .update(cx, |editor, cx| {
 424                    spawn_new_update_tasks(
 425                        editor,
 426                        reason_description,
 427                        excerpts_to_query,
 428                        invalidate,
 429                        cache_version,
 430                        cx,
 431                    )
 432                })
 433                .ok();
 434        });
 435
 436        if invalidated_hints.is_empty() {
 437            None
 438        } else {
 439            Some(InlaySplice {
 440                to_remove: invalidated_hints,
 441                to_insert: Vec::new(),
 442            })
 443        }
 444    }
 445
 446    fn new_allowed_hint_kinds_splice(
 447        &self,
 448        multi_buffer: &Entity<MultiBuffer>,
 449        visible_hints: &[Inlay],
 450        new_kinds: &HashSet<Option<InlayHintKind>>,
 451        cx: &mut Context<Editor>,
 452    ) -> Option<InlaySplice> {
 453        let old_kinds = &self.allowed_hint_kinds;
 454        if new_kinds == old_kinds {
 455            return None;
 456        }
 457
 458        let mut to_remove = Vec::new();
 459        let mut to_insert = Vec::new();
 460        let mut shown_hints_to_remove = visible_hints.iter().fold(
 461            HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
 462            |mut current_hints, inlay| {
 463                current_hints
 464                    .entry(inlay.position.excerpt_id)
 465                    .or_default()
 466                    .push((inlay.position, inlay.id));
 467                current_hints
 468            },
 469        );
 470
 471        let multi_buffer = multi_buffer.read(cx);
 472        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
 473
 474        for (excerpt_id, excerpt_cached_hints) in &self.hints {
 475            let shown_excerpt_hints_to_remove =
 476                shown_hints_to_remove.entry(*excerpt_id).or_default();
 477            let excerpt_cached_hints = excerpt_cached_hints.read();
 478            let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
 479            shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
 480                let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else {
 481                    return false;
 482                };
 483                let buffer_snapshot = buffer.read(cx).snapshot();
 484                loop {
 485                    match excerpt_cache.peek() {
 486                        Some(&cached_hint_id) => {
 487                            let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
 488                            if cached_hint_id == shown_hint_id {
 489                                excerpt_cache.next();
 490                                return !new_kinds.contains(&cached_hint.kind);
 491                            }
 492
 493                            match cached_hint
 494                                .position
 495                                .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
 496                            {
 497                                cmp::Ordering::Less | cmp::Ordering::Equal => {
 498                                    if !old_kinds.contains(&cached_hint.kind)
 499                                        && new_kinds.contains(&cached_hint.kind)
 500                                        && let Some(anchor) = multi_buffer_snapshot
 501                                            .anchor_in_excerpt(*excerpt_id, cached_hint.position)
 502                                    {
 503                                        to_insert.push(Inlay::hint(
 504                                            cached_hint_id.id(),
 505                                            anchor,
 506                                            cached_hint,
 507                                        ));
 508                                    }
 509                                    excerpt_cache.next();
 510                                }
 511                                cmp::Ordering::Greater => return true,
 512                            }
 513                        }
 514                        None => return true,
 515                    }
 516                }
 517            });
 518
 519            for cached_hint_id in excerpt_cache {
 520                let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
 521                let cached_hint_kind = maybe_missed_cached_hint.kind;
 522                if !old_kinds.contains(&cached_hint_kind)
 523                    && new_kinds.contains(&cached_hint_kind)
 524                    && let Some(anchor) = multi_buffer_snapshot
 525                        .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position)
 526                {
 527                    to_insert.push(Inlay::hint(
 528                        cached_hint_id.id(),
 529                        anchor,
 530                        maybe_missed_cached_hint,
 531                    ));
 532                }
 533            }
 534        }
 535
 536        to_remove.extend(
 537            shown_hints_to_remove
 538                .into_values()
 539                .flatten()
 540                .map(|(_, hint_id)| hint_id),
 541        );
 542        if to_remove.is_empty() && to_insert.is_empty() {
 543            None
 544        } else {
 545            Some(InlaySplice {
 546                to_remove,
 547                to_insert,
 548            })
 549        }
 550    }
 551
 552    /// Completely forget of certain excerpts that were removed from the multibuffer.
 553    pub(super) fn remove_excerpts(
 554        &mut self,
 555        excerpts_removed: &[ExcerptId],
 556    ) -> Option<InlaySplice> {
 557        let mut to_remove = Vec::new();
 558        for excerpt_to_remove in excerpts_removed {
 559            self.update_tasks.remove(excerpt_to_remove);
 560            if let Some(cached_hints) = self.hints.remove(excerpt_to_remove) {
 561                let cached_hints = cached_hints.read();
 562                to_remove.extend(cached_hints.ordered_hints.iter().copied());
 563            }
 564        }
 565        if to_remove.is_empty() {
 566            None
 567        } else {
 568            self.version += 1;
 569            Some(InlaySplice {
 570                to_remove,
 571                to_insert: Vec::new(),
 572            })
 573        }
 574    }
 575
 576    pub(super) fn clear(&mut self) {
 577        if !self.update_tasks.is_empty() || !self.hints.is_empty() {
 578            self.version += 1;
 579        }
 580        self.update_tasks.clear();
 581        self.refresh_task = Task::ready(());
 582        self.hints.clear();
 583    }
 584
 585    pub(super) fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option<InlayHint> {
 586        self.hints
 587            .get(&excerpt_id)?
 588            .read()
 589            .hints_by_id
 590            .get(&hint_id)
 591            .cloned()
 592    }
 593
 594    pub fn hints(&self) -> Vec<InlayHint> {
 595        let mut hints = Vec::new();
 596        for excerpt_hints in self.hints.values() {
 597            let excerpt_hints = excerpt_hints.read();
 598            hints.extend(
 599                excerpt_hints
 600                    .ordered_hints
 601                    .iter()
 602                    .map(|id| &excerpt_hints.hints_by_id[id])
 603                    .cloned(),
 604            );
 605        }
 606        hints
 607    }
 608
 609    /// Queries a certain hint from the cache for extra data via the LSP <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHint_resolve">resolve</a> request.
 610    pub(super) fn spawn_hint_resolve(
 611        &self,
 612        buffer_id: BufferId,
 613        excerpt_id: ExcerptId,
 614        id: InlayId,
 615        window: &mut Window,
 616        cx: &mut Context<Editor>,
 617    ) {
 618        if let Some(excerpt_hints) = self.hints.get(&excerpt_id) {
 619            let mut guard = excerpt_hints.write();
 620            if let Some(cached_hint) = guard.hints_by_id.get_mut(&id)
 621                && let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state
 622            {
 623                let hint_to_resolve = cached_hint.clone();
 624                let server_id = *server_id;
 625                cached_hint.resolve_state = ResolveState::Resolving;
 626                drop(guard);
 627                cx.spawn_in(window, async move |editor, cx| {
 628                    let resolved_hint_task = editor.update(cx, |editor, cx| {
 629                        let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
 630                        editor.semantics_provider.as_ref()?.resolve_inlay_hint(
 631                            hint_to_resolve,
 632                            buffer,
 633                            server_id,
 634                            cx,
 635                        )
 636                    })?;
 637                    if let Some(resolved_hint_task) = resolved_hint_task {
 638                        let mut resolved_hint =
 639                            resolved_hint_task.await.context("hint resolve task")?;
 640                        editor.read_with(cx, |editor, _| {
 641                            if let Some(excerpt_hints) =
 642                                editor.inlay_hint_cache.hints.get(&excerpt_id)
 643                            {
 644                                let mut guard = excerpt_hints.write();
 645                                if let Some(cached_hint) = guard.hints_by_id.get_mut(&id)
 646                                    && cached_hint.resolve_state == ResolveState::Resolving
 647                                {
 648                                    resolved_hint.resolve_state = ResolveState::Resolved;
 649                                    *cached_hint = resolved_hint;
 650                                }
 651                            }
 652                        })?;
 653                    }
 654
 655                    anyhow::Ok(())
 656                })
 657                .detach_and_log_err(cx);
 658            }
 659        }
 660    }
 661}
 662
 663fn debounce_value(debounce_ms: u64) -> Option<Duration> {
 664    if debounce_ms > 0 {
 665        Some(Duration::from_millis(debounce_ms))
 666    } else {
 667        None
 668    }
 669}
 670
 671fn spawn_new_update_tasks(
 672    editor: &mut Editor,
 673    reason: &'static str,
 674    excerpts_to_query: HashMap<ExcerptId, (Entity<Buffer>, Global, Range<usize>)>,
 675    invalidate: InvalidationStrategy,
 676    update_cache_version: usize,
 677    cx: &mut Context<Editor>,
 678) {
 679    for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in
 680        excerpts_to_query
 681    {
 682        if excerpt_visible_range.is_empty() {
 683            continue;
 684        }
 685        let buffer = excerpt_buffer.read(cx);
 686        let buffer_id = buffer.remote_id();
 687        let buffer_snapshot = buffer.snapshot();
 688        if buffer_snapshot
 689            .version()
 690            .changed_since(&new_task_buffer_version)
 691        {
 692            continue;
 693        }
 694
 695        if let Some(cached_excerpt_hints) = editor.inlay_hint_cache.hints.get(&excerpt_id) {
 696            let cached_excerpt_hints = cached_excerpt_hints.read();
 697            let cached_buffer_version = &cached_excerpt_hints.buffer_version;
 698            if cached_excerpt_hints.version > update_cache_version
 699                || cached_buffer_version.changed_since(&new_task_buffer_version)
 700            {
 701                continue;
 702            }
 703        };
 704
 705        let Some(query_ranges) = editor.buffer.update(cx, |multi_buffer, cx| {
 706            determine_query_ranges(
 707                multi_buffer,
 708                excerpt_id,
 709                &excerpt_buffer,
 710                excerpt_visible_range,
 711                cx,
 712            )
 713        }) else {
 714            return;
 715        };
 716        let query = ExcerptQuery {
 717            buffer_id,
 718            excerpt_id,
 719            cache_version: update_cache_version,
 720            invalidate,
 721            reason,
 722        };
 723
 724        let mut new_update_task =
 725            |query_ranges| new_update_task(query, query_ranges, excerpt_buffer.clone(), cx);
 726
 727        match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
 728            hash_map::Entry::Occupied(mut o) => {
 729                o.get_mut().update_cached_tasks(
 730                    &buffer_snapshot,
 731                    query_ranges,
 732                    invalidate,
 733                    new_update_task,
 734                );
 735            }
 736            hash_map::Entry::Vacant(v) => {
 737                v.insert(TasksForRanges::new(
 738                    query_ranges.clone(),
 739                    new_update_task(query_ranges),
 740                ));
 741            }
 742        }
 743    }
 744}
 745
 746#[derive(Debug, Clone)]
 747struct QueryRanges {
 748    before_visible: Vec<Range<language::Anchor>>,
 749    visible: Vec<Range<language::Anchor>>,
 750    after_visible: Vec<Range<language::Anchor>>,
 751}
 752
 753impl QueryRanges {
 754    fn is_empty(&self) -> bool {
 755        self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty()
 756    }
 757
 758    fn into_sorted_query_ranges(self) -> Vec<Range<text::Anchor>> {
 759        let mut sorted_ranges = Vec::with_capacity(
 760            self.before_visible.len() + self.visible.len() + self.after_visible.len(),
 761        );
 762        sorted_ranges.extend(self.before_visible);
 763        sorted_ranges.extend(self.visible);
 764        sorted_ranges.extend(self.after_visible);
 765        sorted_ranges
 766    }
 767}
 768
 769fn determine_query_ranges(
 770    multi_buffer: &mut MultiBuffer,
 771    excerpt_id: ExcerptId,
 772    excerpt_buffer: &Entity<Buffer>,
 773    excerpt_visible_range: Range<usize>,
 774    cx: &mut Context<MultiBuffer>,
 775) -> Option<QueryRanges> {
 776    let buffer = excerpt_buffer.read(cx);
 777    let full_excerpt_range = multi_buffer
 778        .excerpts_for_buffer(buffer.remote_id(), cx)
 779        .into_iter()
 780        .find(|(id, _)| id == &excerpt_id)
 781        .map(|(_, range)| range.context)?;
 782    let snapshot = buffer.snapshot();
 783    let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start;
 784
 785    let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end {
 786        return None;
 787    } else {
 788        vec![
 789            buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left))
 790                ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)),
 791        ]
 792    };
 793
 794    let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot);
 795    let after_visible_range_start = excerpt_visible_range
 796        .end
 797        .saturating_add(1)
 798        .min(full_excerpt_range_end_offset)
 799        .min(buffer.len());
 800    let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset {
 801        Vec::new()
 802    } else {
 803        let after_range_end_offset = after_visible_range_start
 804            .saturating_add(excerpt_visible_len)
 805            .min(full_excerpt_range_end_offset)
 806            .min(buffer.len());
 807        vec![
 808            buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left))
 809                ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)),
 810        ]
 811    };
 812
 813    let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot);
 814    let before_visible_range_end = excerpt_visible_range
 815        .start
 816        .saturating_sub(1)
 817        .max(full_excerpt_range_start_offset);
 818    let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset {
 819        Vec::new()
 820    } else {
 821        let before_range_start_offset = before_visible_range_end
 822            .saturating_sub(excerpt_visible_len)
 823            .max(full_excerpt_range_start_offset);
 824        vec![
 825            buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left))
 826                ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)),
 827        ]
 828    };
 829
 830    Some(QueryRanges {
 831        before_visible: before_visible_range,
 832        visible: visible_range,
 833        after_visible: after_visible_range,
 834    })
 835}
 836
 837const MAX_CONCURRENT_LSP_REQUESTS: usize = 5;
 838const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400;
 839
 840fn new_update_task(
 841    query: ExcerptQuery,
 842    query_ranges: QueryRanges,
 843    excerpt_buffer: Entity<Buffer>,
 844    cx: &mut Context<Editor>,
 845) -> Task<()> {
 846    cx.spawn(async move |editor, cx| {
 847        let visible_range_update_results = future::join_all(
 848            query_ranges
 849                .visible
 850                .into_iter()
 851                .filter_map(|visible_range| {
 852                    let fetch_task = editor
 853                        .update(cx, |_, cx| {
 854                            fetch_and_update_hints(
 855                                excerpt_buffer.clone(),
 856                                query,
 857                                visible_range.clone(),
 858                                query.invalidate.should_invalidate(),
 859                                cx,
 860                            )
 861                        })
 862                        .log_err()?;
 863                    Some(async move { (visible_range, fetch_task.await) })
 864                }),
 865        )
 866        .await;
 867
 868        let hint_delay = cx.background_executor().timer(Duration::from_millis(
 869            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS,
 870        ));
 871
 872        let query_range_failed =
 873            |range: &Range<language::Anchor>, e: anyhow::Error, cx: &mut AsyncApp| {
 874                log::error!("inlay hint update task for range failed: {e:#?}");
 875                editor
 876                    .update(cx, |editor, cx| {
 877                        if let Some(task_ranges) = editor
 878                            .inlay_hint_cache
 879                            .update_tasks
 880                            .get_mut(&query.excerpt_id)
 881                        {
 882                            let buffer_snapshot = excerpt_buffer.read(cx).snapshot();
 883                            task_ranges.invalidate_range(&buffer_snapshot, range);
 884                        }
 885                    })
 886                    .ok()
 887            };
 888
 889        for (range, result) in visible_range_update_results {
 890            if let Err(e) = result {
 891                query_range_failed(&range, e, cx);
 892            }
 893        }
 894
 895        hint_delay.await;
 896        let invisible_range_update_results = future::join_all(
 897            query_ranges
 898                .before_visible
 899                .into_iter()
 900                .chain(query_ranges.after_visible.into_iter())
 901                .filter_map(|invisible_range| {
 902                    let fetch_task = editor
 903                        .update(cx, |_, cx| {
 904                            fetch_and_update_hints(
 905                                excerpt_buffer.clone(),
 906                                query,
 907                                invisible_range.clone(),
 908                                false, // visible screen request already invalidated the entries
 909                                cx,
 910                            )
 911                        })
 912                        .log_err()?;
 913                    Some(async move { (invisible_range, fetch_task.await) })
 914                }),
 915        )
 916        .await;
 917        for (range, result) in invisible_range_update_results {
 918            if let Err(e) = result {
 919                query_range_failed(&range, e, cx);
 920            }
 921        }
 922    })
 923}
 924
 925fn fetch_and_update_hints(
 926    excerpt_buffer: Entity<Buffer>,
 927    query: ExcerptQuery,
 928    fetch_range: Range<language::Anchor>,
 929    invalidate: bool,
 930    cx: &mut Context<Editor>,
 931) -> Task<anyhow::Result<()>> {
 932    cx.spawn(async move |editor, cx|{
 933        let buffer_snapshot = excerpt_buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
 934        let (lsp_request_limiter, multi_buffer_snapshot) =
 935            editor.update(cx, |editor, cx| {
 936                let multi_buffer_snapshot =
 937                    editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
 938                let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter);
 939                (lsp_request_limiter, multi_buffer_snapshot)
 940            })?;
 941
 942        let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
 943            (None, false)
 944        } else {
 945            match lsp_request_limiter.try_acquire() {
 946                Some(guard) => (Some(guard), false),
 947                None => (Some(lsp_request_limiter.acquire().await), true),
 948            }
 949        };
 950        let fetch_range_to_log = fetch_range.start.to_point(&buffer_snapshot)
 951            ..fetch_range.end.to_point(&buffer_snapshot);
 952        let inlay_hints_fetch_task = editor
 953            .update(cx, |editor, cx| {
 954                if got_throttled {
 955                    let query_not_around_visible_range = match editor
 956                        .visible_excerpts(None, cx)
 957                        .remove(&query.excerpt_id)
 958                    {
 959                        Some((_, _, current_visible_range)) => {
 960                            let visible_offset_length = current_visible_range.len();
 961                            let double_visible_range = current_visible_range
 962                                .start
 963                                .saturating_sub(visible_offset_length)
 964                                ..current_visible_range
 965                                    .end
 966                                    .saturating_add(visible_offset_length)
 967                                    .min(buffer_snapshot.len());
 968                            !double_visible_range
 969                                .contains(&fetch_range.start.to_offset(&buffer_snapshot))
 970                                && !double_visible_range
 971                                    .contains(&fetch_range.end.to_offset(&buffer_snapshot))
 972                        }
 973                        None => true,
 974                    };
 975                    if query_not_around_visible_range {
 976                        log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
 977                        if let Some(task_ranges) = editor
 978                            .inlay_hint_cache
 979                            .update_tasks
 980                            .get_mut(&query.excerpt_id)
 981                        {
 982                            task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
 983                        }
 984                        return None;
 985                    }
 986                }
 987
 988                let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?;
 989
 990                if !editor.registered_buffers.contains_key(&query.buffer_id)
 991                    && let Some(project) = editor.project.as_ref() {
 992                        project.update(cx, |project, cx| {
 993                            editor.registered_buffers.insert(
 994                                query.buffer_id,
 995                                project.register_buffer_with_language_servers(&buffer, cx),
 996                            );
 997                        })
 998                    }
 999
1000                editor
1001                    .semantics_provider
1002                    .as_ref()?
1003                    .inlay_hints(buffer, fetch_range.clone(), cx)
1004            })
1005            .ok()
1006            .flatten();
1007
1008        let cached_excerpt_hints = editor.read_with(cx, |editor, _| {
1009            editor
1010                .inlay_hint_cache
1011                .hints
1012                .get(&query.excerpt_id)
1013                .cloned()
1014        })?;
1015
1016        let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx))?;
1017        let new_hints = match inlay_hints_fetch_task {
1018            Some(fetch_task) => {
1019                log::debug!(
1020                    "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
1021                    query_reason = query.reason,
1022                );
1023                log::trace!(
1024                    "Currently visible hints: {visible_hints:?}, cached hints present: {}",
1025                    cached_excerpt_hints.is_some(),
1026                );
1027                fetch_task.await.context("inlay hint fetch task")?
1028            }
1029            None => return Ok(()),
1030        };
1031        drop(lsp_request_guard);
1032        log::debug!(
1033            "Fetched {} hints for range {fetch_range_to_log:?}",
1034            new_hints.len()
1035        );
1036        log::trace!("Fetched hints: {new_hints:?}");
1037
1038        let background_task_buffer_snapshot = buffer_snapshot.clone();
1039        let background_fetch_range = fetch_range.clone();
1040        let new_update = cx.background_spawn(async move {
1041            calculate_hint_updates(
1042                query.excerpt_id,
1043                invalidate,
1044                background_fetch_range,
1045                new_hints,
1046                &background_task_buffer_snapshot,
1047                cached_excerpt_hints,
1048                &visible_hints,
1049            )
1050        })
1051            .await;
1052        if let Some(new_update) = new_update {
1053            log::debug!(
1054                "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
1055                new_update.remove_from_visible.len(),
1056                new_update.remove_from_cache.len(),
1057                new_update.add_to_cache.len()
1058            );
1059            log::trace!("New update: {new_update:?}");
1060            editor
1061                .update(cx, |editor,  cx| {
1062                    apply_hint_update(
1063                        editor,
1064                        new_update,
1065                        query,
1066                        invalidate,
1067                        buffer_snapshot,
1068                        multi_buffer_snapshot,
1069                        cx,
1070                    );
1071                })
1072                .ok();
1073        }
1074        anyhow::Ok(())
1075    })
1076}
1077
1078fn calculate_hint_updates(
1079    excerpt_id: ExcerptId,
1080    invalidate: bool,
1081    fetch_range: Range<language::Anchor>,
1082    new_excerpt_hints: Vec<InlayHint>,
1083    buffer_snapshot: &BufferSnapshot,
1084    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
1085    visible_hints: &[Inlay],
1086) -> Option<ExcerptHintsUpdate> {
1087    let mut add_to_cache = Vec::<InlayHint>::new();
1088    let mut excerpt_hints_to_persist = HashMap::default();
1089    for new_hint in new_excerpt_hints {
1090        if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
1091            continue;
1092        }
1093        let missing_from_cache = match &cached_excerpt_hints {
1094            Some(cached_excerpt_hints) => {
1095                let cached_excerpt_hints = cached_excerpt_hints.read();
1096                match cached_excerpt_hints
1097                    .ordered_hints
1098                    .binary_search_by(|probe| {
1099                        cached_excerpt_hints.hints_by_id[probe]
1100                            .position
1101                            .cmp(&new_hint.position, buffer_snapshot)
1102                    }) {
1103                    Ok(ix) => {
1104                        let mut missing_from_cache = true;
1105                        for id in &cached_excerpt_hints.ordered_hints[ix..] {
1106                            let cached_hint = &cached_excerpt_hints.hints_by_id[id];
1107                            if new_hint
1108                                .position
1109                                .cmp(&cached_hint.position, buffer_snapshot)
1110                                .is_gt()
1111                            {
1112                                break;
1113                            }
1114                            if cached_hint == &new_hint {
1115                                excerpt_hints_to_persist.insert(*id, cached_hint.kind);
1116                                missing_from_cache = false;
1117                            }
1118                        }
1119                        missing_from_cache
1120                    }
1121                    Err(_) => true,
1122                }
1123            }
1124            None => true,
1125        };
1126        if missing_from_cache {
1127            add_to_cache.push(new_hint);
1128        }
1129    }
1130
1131    let mut remove_from_visible = HashSet::default();
1132    let mut remove_from_cache = HashSet::default();
1133    if invalidate {
1134        remove_from_visible.extend(
1135            visible_hints
1136                .iter()
1137                .filter(|hint| hint.position.excerpt_id == excerpt_id)
1138                .map(|inlay_hint| inlay_hint.id)
1139                .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
1140        );
1141
1142        if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
1143            let cached_excerpt_hints = cached_excerpt_hints.read();
1144            remove_from_cache.extend(
1145                cached_excerpt_hints
1146                    .ordered_hints
1147                    .iter()
1148                    .filter(|cached_inlay_id| {
1149                        !excerpt_hints_to_persist.contains_key(cached_inlay_id)
1150                    })
1151                    .copied(),
1152            );
1153            remove_from_visible.extend(remove_from_cache.iter().cloned());
1154        }
1155    }
1156
1157    if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
1158        None
1159    } else {
1160        Some(ExcerptHintsUpdate {
1161            excerpt_id,
1162            remove_from_visible,
1163            remove_from_cache,
1164            add_to_cache,
1165        })
1166    }
1167}
1168
1169fn contains_position(
1170    range: &Range<language::Anchor>,
1171    position: language::Anchor,
1172    buffer_snapshot: &BufferSnapshot,
1173) -> bool {
1174    range.start.cmp(&position, buffer_snapshot).is_le()
1175        && range.end.cmp(&position, buffer_snapshot).is_ge()
1176}
1177
1178fn apply_hint_update(
1179    editor: &mut Editor,
1180    new_update: ExcerptHintsUpdate,
1181    query: ExcerptQuery,
1182    invalidate: bool,
1183    buffer_snapshot: BufferSnapshot,
1184    multi_buffer_snapshot: MultiBufferSnapshot,
1185    cx: &mut Context<Editor>,
1186) {
1187    let cached_excerpt_hints = editor
1188        .inlay_hint_cache
1189        .hints
1190        .entry(new_update.excerpt_id)
1191        .or_insert_with(|| {
1192            Arc::new(RwLock::new(CachedExcerptHints {
1193                version: query.cache_version,
1194                buffer_version: buffer_snapshot.version().clone(),
1195                buffer_id: query.buffer_id,
1196                ordered_hints: Vec::new(),
1197                hints_by_id: HashMap::default(),
1198            }))
1199        });
1200    let mut cached_excerpt_hints = cached_excerpt_hints.write();
1201    match query.cache_version.cmp(&cached_excerpt_hints.version) {
1202        cmp::Ordering::Less => return,
1203        cmp::Ordering::Greater | cmp::Ordering::Equal => {
1204            cached_excerpt_hints.version = query.cache_version;
1205        }
1206    }
1207
1208    let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
1209    cached_excerpt_hints
1210        .ordered_hints
1211        .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id));
1212    cached_excerpt_hints
1213        .hints_by_id
1214        .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
1215    let mut splice = InlaySplice::default();
1216    splice.to_remove.extend(new_update.remove_from_visible);
1217    for new_hint in new_update.add_to_cache {
1218        let insert_position = match cached_excerpt_hints
1219            .ordered_hints
1220            .binary_search_by(|probe| {
1221                cached_excerpt_hints.hints_by_id[probe]
1222                    .position
1223                    .cmp(&new_hint.position, &buffer_snapshot)
1224            }) {
1225            Ok(i) => {
1226                // When a hint is added to the same position where existing ones are present,
1227                // do not deduplicate it: we split hint queries into non-overlapping ranges
1228                // and each hint batch returned by the server should already contain unique hints.
1229                i + cached_excerpt_hints.ordered_hints[i..].len() + 1
1230            }
1231            Err(i) => i,
1232        };
1233
1234        let new_inlay_id = post_inc(&mut editor.next_inlay_id);
1235        if editor
1236            .inlay_hint_cache
1237            .allowed_hint_kinds
1238            .contains(&new_hint.kind)
1239            && let Some(new_hint_position) =
1240                multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position)
1241        {
1242            splice
1243                .to_insert
1244                .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
1245        }
1246        let new_id = InlayId::Hint(new_inlay_id);
1247        cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
1248        if cached_excerpt_hints.ordered_hints.len() <= insert_position {
1249            cached_excerpt_hints.ordered_hints.push(new_id);
1250        } else {
1251            cached_excerpt_hints
1252                .ordered_hints
1253                .insert(insert_position, new_id);
1254        }
1255
1256        cached_inlays_changed = true;
1257    }
1258    cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
1259    drop(cached_excerpt_hints);
1260
1261    if invalidate {
1262        let mut outdated_excerpt_caches = HashSet::default();
1263        for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
1264            let excerpt_hints = excerpt_hints.read();
1265            if excerpt_hints.buffer_id == query.buffer_id
1266                && excerpt_id != &query.excerpt_id
1267                && buffer_snapshot
1268                    .version()
1269                    .changed_since(&excerpt_hints.buffer_version)
1270            {
1271                outdated_excerpt_caches.insert(*excerpt_id);
1272                splice
1273                    .to_remove
1274                    .extend(excerpt_hints.ordered_hints.iter().copied());
1275            }
1276        }
1277        cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
1278        editor
1279            .inlay_hint_cache
1280            .hints
1281            .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
1282    }
1283
1284    let InlaySplice {
1285        to_remove,
1286        to_insert,
1287    } = splice;
1288    let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty();
1289    if cached_inlays_changed || displayed_inlays_changed {
1290        editor.inlay_hint_cache.version += 1;
1291    }
1292    if displayed_inlays_changed {
1293        editor.splice_inlays(&to_remove, to_insert, cx)
1294    }
1295}
1296
1297#[cfg(test)]
1298pub mod tests {
1299    use crate::SelectionEffects;
1300    use crate::editor_tests::update_test_language_settings;
1301    use crate::scroll::ScrollAmount;
1302    use crate::{ExcerptRange, scroll::Autoscroll, test::editor_lsp_test_context::rust_lang};
1303    use futures::StreamExt;
1304    use gpui::{AppContext as _, Context, SemanticVersion, TestAppContext, WindowHandle};
1305    use itertools::Itertools as _;
1306    use language::{Capability, FakeLspAdapter};
1307    use language::{Language, LanguageConfig, LanguageMatcher};
1308    use lsp::FakeLanguageServer;
1309    use parking_lot::Mutex;
1310    use project::{FakeFs, Project};
1311    use serde_json::json;
1312    use settings::{AllLanguageSettingsContent, InlayHintSettingsContent, SettingsStore};
1313    use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
1314    use text::Point;
1315    use util::path;
1316
1317    use super::*;
1318
1319    #[gpui::test]
1320    async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
1321        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1322        init_test(cx, |settings| {
1323            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1324                show_value_hints: Some(true),
1325                enabled: Some(true),
1326                edit_debounce_ms: Some(0),
1327                scroll_debounce_ms: Some(0),
1328                show_type_hints: Some(allowed_hint_kinds.contains(&Some(InlayHintKind::Type))),
1329                show_parameter_hints: Some(
1330                    allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1331                ),
1332                show_other_hints: Some(allowed_hint_kinds.contains(&None)),
1333                show_background: Some(false),
1334                toggle_on_modifiers_press: None,
1335            })
1336        });
1337        let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1338            let lsp_request_count = Arc::new(AtomicU32::new(0));
1339            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1340                move |params, _| {
1341                    let task_lsp_request_count = Arc::clone(&lsp_request_count);
1342                    async move {
1343                        let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1;
1344                        assert_eq!(
1345                            params.text_document.uri,
1346                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
1347                        );
1348                        Ok(Some(vec![lsp::InlayHint {
1349                            position: lsp::Position::new(0, i),
1350                            label: lsp::InlayHintLabel::String(i.to_string()),
1351                            kind: None,
1352                            text_edits: None,
1353                            tooltip: None,
1354                            padding_left: None,
1355                            padding_right: None,
1356                            data: None,
1357                        }]))
1358                    }
1359                },
1360            );
1361        })
1362        .await;
1363        cx.executor().run_until_parked();
1364
1365        editor
1366            .update(cx, |editor, _window, cx| {
1367                let expected_hints = vec!["1".to_string()];
1368                assert_eq!(
1369                    expected_hints,
1370                    cached_hint_labels(editor),
1371                    "Should get its first hints when opening the editor"
1372                );
1373                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1374                let inlay_cache = editor.inlay_hint_cache();
1375                assert_eq!(
1376                    inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1377                    "Cache should use editor settings to get the allowed hint kinds"
1378                );
1379            })
1380            .unwrap();
1381
1382        editor
1383            .update(cx, |editor, window, cx| {
1384                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1385                    s.select_ranges([13..13])
1386                });
1387                editor.handle_input("some change", window, cx);
1388            })
1389            .unwrap();
1390        cx.executor().run_until_parked();
1391        editor
1392            .update(cx, |editor, _window, cx| {
1393                let expected_hints = vec!["2".to_string()];
1394                assert_eq!(
1395                    expected_hints,
1396                    cached_hint_labels(editor),
1397                    "Should get new hints after an edit"
1398                );
1399                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1400                let inlay_cache = editor.inlay_hint_cache();
1401                assert_eq!(
1402                    inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1403                    "Cache should use editor settings to get the allowed hint kinds"
1404                );
1405            })
1406            .unwrap();
1407
1408        fake_server
1409            .request::<lsp::request::InlayHintRefreshRequest>(())
1410            .await
1411            .into_response()
1412            .expect("inlay refresh request failed");
1413        cx.executor().run_until_parked();
1414        editor
1415            .update(cx, |editor, _window, cx| {
1416                let expected_hints = vec!["3".to_string()];
1417                assert_eq!(
1418                    expected_hints,
1419                    cached_hint_labels(editor),
1420                    "Should get new hints after hint refresh/ request"
1421                );
1422                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1423                let inlay_cache = editor.inlay_hint_cache();
1424                assert_eq!(
1425                    inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1426                    "Cache should use editor settings to get the allowed hint kinds"
1427                );
1428            })
1429            .unwrap();
1430    }
1431
1432    #[gpui::test]
1433    async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
1434        init_test(cx, |settings| {
1435            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1436                show_value_hints: Some(true),
1437                enabled: Some(true),
1438                edit_debounce_ms: Some(0),
1439                scroll_debounce_ms: Some(0),
1440                show_type_hints: Some(true),
1441                show_parameter_hints: Some(true),
1442                show_other_hints: Some(true),
1443                show_background: Some(false),
1444                toggle_on_modifiers_press: None,
1445            })
1446        });
1447
1448        let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1449            let lsp_request_count = Arc::new(AtomicU32::new(0));
1450            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1451                move |params, _| {
1452                    let task_lsp_request_count = Arc::clone(&lsp_request_count);
1453                    async move {
1454                        assert_eq!(
1455                            params.text_document.uri,
1456                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
1457                        );
1458                        let current_call_id =
1459                            Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1460                        Ok(Some(vec![lsp::InlayHint {
1461                            position: lsp::Position::new(0, current_call_id),
1462                            label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1463                            kind: None,
1464                            text_edits: None,
1465                            tooltip: None,
1466                            padding_left: None,
1467                            padding_right: None,
1468                            data: None,
1469                        }]))
1470                    }
1471                },
1472            );
1473        })
1474        .await;
1475        cx.executor().run_until_parked();
1476
1477        editor
1478            .update(cx, |editor, _, cx| {
1479                let expected_hints = vec!["0".to_string()];
1480                assert_eq!(
1481                    expected_hints,
1482                    cached_hint_labels(editor),
1483                    "Should get its first hints when opening the editor"
1484                );
1485                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1486            })
1487            .unwrap();
1488
1489        let progress_token = "test_progress_token";
1490        fake_server
1491            .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
1492                token: lsp::ProgressToken::String(progress_token.to_string()),
1493            })
1494            .await
1495            .into_response()
1496            .expect("work done progress create request failed");
1497        cx.executor().run_until_parked();
1498        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1499            token: lsp::ProgressToken::String(progress_token.to_string()),
1500            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1501                lsp::WorkDoneProgressBegin::default(),
1502            )),
1503        });
1504        cx.executor().run_until_parked();
1505
1506        editor
1507            .update(cx, |editor, _, cx| {
1508                let expected_hints = vec!["0".to_string()];
1509                assert_eq!(
1510                    expected_hints,
1511                    cached_hint_labels(editor),
1512                    "Should not update hints while the work task is running"
1513                );
1514                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1515            })
1516            .unwrap();
1517
1518        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1519            token: lsp::ProgressToken::String(progress_token.to_string()),
1520            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1521                lsp::WorkDoneProgressEnd::default(),
1522            )),
1523        });
1524        cx.executor().run_until_parked();
1525
1526        editor
1527            .update(cx, |editor, _, cx| {
1528                let expected_hints = vec!["1".to_string()];
1529                assert_eq!(
1530                    expected_hints,
1531                    cached_hint_labels(editor),
1532                    "New hints should be queried after the work task is done"
1533                );
1534                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1535            })
1536            .unwrap();
1537    }
1538
1539    #[gpui::test]
1540    async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1541        init_test(cx, |settings| {
1542            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1543                show_value_hints: Some(true),
1544                enabled: Some(true),
1545                edit_debounce_ms: Some(0),
1546                scroll_debounce_ms: Some(0),
1547                show_type_hints: Some(true),
1548                show_parameter_hints: Some(true),
1549                show_other_hints: Some(true),
1550                show_background: Some(false),
1551                toggle_on_modifiers_press: None,
1552            })
1553        });
1554
1555        let fs = FakeFs::new(cx.background_executor.clone());
1556        fs.insert_tree(
1557            path!("/a"),
1558            json!({
1559                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1560                "other.md": "Test md file with some text",
1561            }),
1562        )
1563        .await;
1564
1565        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1566
1567        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1568        let mut rs_fake_servers = None;
1569        let mut md_fake_servers = None;
1570        for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1571            language_registry.add(Arc::new(Language::new(
1572                LanguageConfig {
1573                    name: name.into(),
1574                    matcher: LanguageMatcher {
1575                        path_suffixes: vec![path_suffix.to_string()],
1576                        ..Default::default()
1577                    },
1578                    ..Default::default()
1579                },
1580                Some(tree_sitter_rust::LANGUAGE.into()),
1581            )));
1582            let fake_servers = language_registry.register_fake_lsp(
1583                name,
1584                FakeLspAdapter {
1585                    name,
1586                    capabilities: lsp::ServerCapabilities {
1587                        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1588                        ..Default::default()
1589                    },
1590                    initializer: Some(Box::new({
1591                        move |fake_server| {
1592                            let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1593                            let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1594                            fake_server
1595                                .set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1596                                    move |params, _| {
1597                                        let i = match name {
1598                                            "Rust" => {
1599                                                assert_eq!(
1600                                                    params.text_document.uri,
1601                                                    lsp::Uri::from_file_path(path!("/a/main.rs"))
1602                                                        .unwrap(),
1603                                                );
1604                                                rs_lsp_request_count.fetch_add(1, Ordering::Release)
1605                                                    + 1
1606                                            }
1607                                            "Markdown" => {
1608                                                assert_eq!(
1609                                                    params.text_document.uri,
1610                                                    lsp::Uri::from_file_path(path!("/a/other.md"))
1611                                                        .unwrap(),
1612                                                );
1613                                                md_lsp_request_count.fetch_add(1, Ordering::Release)
1614                                                    + 1
1615                                            }
1616                                            unexpected => {
1617                                                panic!("Unexpected language: {unexpected}")
1618                                            }
1619                                        };
1620
1621                                        async move {
1622                                            let query_start = params.range.start;
1623                                            Ok(Some(vec![lsp::InlayHint {
1624                                                position: query_start,
1625                                                label: lsp::InlayHintLabel::String(i.to_string()),
1626                                                kind: None,
1627                                                text_edits: None,
1628                                                tooltip: None,
1629                                                padding_left: None,
1630                                                padding_right: None,
1631                                                data: None,
1632                                            }]))
1633                                        }
1634                                    },
1635                                );
1636                        }
1637                    })),
1638                    ..Default::default()
1639                },
1640            );
1641            match name {
1642                "Rust" => rs_fake_servers = Some(fake_servers),
1643                "Markdown" => md_fake_servers = Some(fake_servers),
1644                _ => unreachable!(),
1645            }
1646        }
1647
1648        let rs_buffer = project
1649            .update(cx, |project, cx| {
1650                project.open_local_buffer(path!("/a/main.rs"), cx)
1651            })
1652            .await
1653            .unwrap();
1654        let rs_editor = cx.add_window(|window, cx| {
1655            Editor::for_buffer(rs_buffer, Some(project.clone()), window, cx)
1656        });
1657        cx.executor().run_until_parked();
1658
1659        let _rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1660        cx.executor().run_until_parked();
1661        rs_editor
1662            .update(cx, |editor, _window, cx| {
1663                let expected_hints = vec!["1".to_string()];
1664                assert_eq!(
1665                    expected_hints,
1666                    cached_hint_labels(editor),
1667                    "Should get its first hints when opening the editor"
1668                );
1669                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1670            })
1671            .unwrap();
1672
1673        cx.executor().run_until_parked();
1674        let md_buffer = project
1675            .update(cx, |project, cx| {
1676                project.open_local_buffer(path!("/a/other.md"), cx)
1677            })
1678            .await
1679            .unwrap();
1680        let md_editor =
1681            cx.add_window(|window, cx| Editor::for_buffer(md_buffer, Some(project), window, cx));
1682        cx.executor().run_until_parked();
1683
1684        let _md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1685        cx.executor().run_until_parked();
1686        md_editor
1687            .update(cx, |editor, _window, cx| {
1688                let expected_hints = vec!["1".to_string()];
1689                assert_eq!(
1690                    expected_hints,
1691                    cached_hint_labels(editor),
1692                    "Markdown editor should have a separate version, repeating Rust editor rules"
1693                );
1694                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1695            })
1696            .unwrap();
1697
1698        rs_editor
1699            .update(cx, |editor, window, cx| {
1700                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1701                    s.select_ranges([13..13])
1702                });
1703                editor.handle_input("some rs change", window, cx);
1704            })
1705            .unwrap();
1706        cx.executor().run_until_parked();
1707        rs_editor
1708            .update(cx, |editor, _window, cx| {
1709                // TODO: Here, we do not get "2", because inserting another language server will trigger `RefreshInlayHints` event from the `LspStore`
1710                // A project is listened in every editor, so each of them will react to this event.
1711                //
1712                // We do not have language server IDs for remote projects, so cannot easily say on the editor level,
1713                // whether we should ignore a particular `RefreshInlayHints` event.
1714                let expected_hints = vec!["3".to_string()];
1715                assert_eq!(
1716                    expected_hints,
1717                    cached_hint_labels(editor),
1718                    "Rust inlay cache should change after the edit"
1719                );
1720                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1721            })
1722            .unwrap();
1723        md_editor
1724            .update(cx, |editor, _window, cx| {
1725                let expected_hints = vec!["1".to_string()];
1726                assert_eq!(
1727                    expected_hints,
1728                    cached_hint_labels(editor),
1729                    "Markdown editor should not be affected by Rust editor changes"
1730                );
1731                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1732            })
1733            .unwrap();
1734
1735        md_editor
1736            .update(cx, |editor, window, cx| {
1737                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1738                    s.select_ranges([13..13])
1739                });
1740                editor.handle_input("some md change", window, cx);
1741            })
1742            .unwrap();
1743        cx.executor().run_until_parked();
1744        md_editor
1745            .update(cx, |editor, _window, cx| {
1746                let expected_hints = vec!["2".to_string()];
1747                assert_eq!(
1748                    expected_hints,
1749                    cached_hint_labels(editor),
1750                    "Rust editor should not be affected by Markdown editor changes"
1751                );
1752                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1753            })
1754            .unwrap();
1755        rs_editor
1756            .update(cx, |editor, _window, cx| {
1757                let expected_hints = vec!["3".to_string()];
1758                assert_eq!(
1759                    expected_hints,
1760                    cached_hint_labels(editor),
1761                    "Markdown editor should also change independently"
1762                );
1763                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1764            })
1765            .unwrap();
1766    }
1767
1768    #[gpui::test]
1769    async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1770        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1771        init_test(cx, |settings| {
1772            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1773                show_value_hints: Some(true),
1774                enabled: Some(true),
1775                edit_debounce_ms: Some(0),
1776                scroll_debounce_ms: Some(0),
1777                show_type_hints: Some(allowed_hint_kinds.contains(&Some(InlayHintKind::Type))),
1778                show_parameter_hints: Some(
1779                    allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1780                ),
1781                show_other_hints: Some(allowed_hint_kinds.contains(&None)),
1782                show_background: Some(false),
1783                toggle_on_modifiers_press: None,
1784            })
1785        });
1786
1787        let lsp_request_count = Arc::new(AtomicUsize::new(0));
1788        let (_, editor, fake_server) = prepare_test_objects(cx, {
1789            let lsp_request_count = lsp_request_count.clone();
1790            move |fake_server, file_with_hints| {
1791                let lsp_request_count = lsp_request_count.clone();
1792                fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1793                    move |params, _| {
1794                        lsp_request_count.fetch_add(1, Ordering::Release);
1795                        async move {
1796                            assert_eq!(
1797                                params.text_document.uri,
1798                                lsp::Uri::from_file_path(file_with_hints).unwrap(),
1799                            );
1800                            Ok(Some(vec![
1801                                lsp::InlayHint {
1802                                    position: lsp::Position::new(0, 1),
1803                                    label: lsp::InlayHintLabel::String("type hint".to_string()),
1804                                    kind: Some(lsp::InlayHintKind::TYPE),
1805                                    text_edits: None,
1806                                    tooltip: None,
1807                                    padding_left: None,
1808                                    padding_right: None,
1809                                    data: None,
1810                                },
1811                                lsp::InlayHint {
1812                                    position: lsp::Position::new(0, 2),
1813                                    label: lsp::InlayHintLabel::String(
1814                                        "parameter hint".to_string(),
1815                                    ),
1816                                    kind: Some(lsp::InlayHintKind::PARAMETER),
1817                                    text_edits: None,
1818                                    tooltip: None,
1819                                    padding_left: None,
1820                                    padding_right: None,
1821                                    data: None,
1822                                },
1823                                lsp::InlayHint {
1824                                    position: lsp::Position::new(0, 3),
1825                                    label: lsp::InlayHintLabel::String("other hint".to_string()),
1826                                    kind: None,
1827                                    text_edits: None,
1828                                    tooltip: None,
1829                                    padding_left: None,
1830                                    padding_right: None,
1831                                    data: None,
1832                                },
1833                            ]))
1834                        }
1835                    },
1836                );
1837            }
1838        })
1839        .await;
1840        cx.executor().run_until_parked();
1841
1842        editor
1843            .update(cx, |editor, _, cx| {
1844                assert_eq!(
1845                    lsp_request_count.load(Ordering::Relaxed),
1846                    1,
1847                    "Should query new hints once"
1848                );
1849                assert_eq!(
1850                    vec![
1851                        "type hint".to_string(),
1852                        "parameter hint".to_string(),
1853                        "other hint".to_string(),
1854                    ],
1855                    cached_hint_labels(editor),
1856                    "Should get its first hints when opening the editor"
1857                );
1858                assert_eq!(
1859                    vec!["type hint".to_string(), "other hint".to_string()],
1860                    visible_hint_labels(editor, cx)
1861                );
1862                let inlay_cache = editor.inlay_hint_cache();
1863                assert_eq!(
1864                    inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1865                    "Cache should use editor settings to get the allowed hint kinds"
1866                );
1867            })
1868            .unwrap();
1869
1870        fake_server
1871            .request::<lsp::request::InlayHintRefreshRequest>(())
1872            .await
1873            .into_response()
1874            .expect("inlay refresh request failed");
1875        cx.executor().run_until_parked();
1876        editor
1877            .update(cx, |editor, _, cx| {
1878                assert_eq!(
1879                    lsp_request_count.load(Ordering::Relaxed),
1880                    2,
1881                    "Should load new hints twice"
1882                );
1883                assert_eq!(
1884                    vec![
1885                        "type hint".to_string(),
1886                        "parameter hint".to_string(),
1887                        "other hint".to_string(),
1888                    ],
1889                    cached_hint_labels(editor),
1890                    "Cached hints should not change due to allowed hint kinds settings update"
1891                );
1892                assert_eq!(
1893                    vec!["type hint".to_string(), "other hint".to_string()],
1894                    visible_hint_labels(editor, cx)
1895                );
1896            })
1897            .unwrap();
1898
1899        for (new_allowed_hint_kinds, expected_visible_hints) in [
1900            (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1901            (
1902                HashSet::from_iter([Some(InlayHintKind::Type)]),
1903                vec!["type hint".to_string()],
1904            ),
1905            (
1906                HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1907                vec!["parameter hint".to_string()],
1908            ),
1909            (
1910                HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1911                vec!["type hint".to_string(), "other hint".to_string()],
1912            ),
1913            (
1914                HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1915                vec!["parameter hint".to_string(), "other hint".to_string()],
1916            ),
1917            (
1918                HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1919                vec!["type hint".to_string(), "parameter hint".to_string()],
1920            ),
1921            (
1922                HashSet::from_iter([
1923                    None,
1924                    Some(InlayHintKind::Type),
1925                    Some(InlayHintKind::Parameter),
1926                ]),
1927                vec![
1928                    "type hint".to_string(),
1929                    "parameter hint".to_string(),
1930                    "other hint".to_string(),
1931                ],
1932            ),
1933        ] {
1934            update_test_language_settings(cx, |settings| {
1935                settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1936                    show_value_hints: Some(true),
1937                    enabled: Some(true),
1938                    edit_debounce_ms: Some(0),
1939                    scroll_debounce_ms: Some(0),
1940                    show_type_hints: Some(
1941                        new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1942                    ),
1943                    show_parameter_hints: Some(
1944                        new_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1945                    ),
1946                    show_other_hints: Some(new_allowed_hint_kinds.contains(&None)),
1947                    show_background: Some(false),
1948                    toggle_on_modifiers_press: None,
1949                })
1950            });
1951            cx.executor().run_until_parked();
1952            editor.update(cx, |editor, _, cx| {
1953                assert_eq!(
1954                    lsp_request_count.load(Ordering::Relaxed),
1955                    2,
1956                    "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1957                );
1958                assert_eq!(
1959                    vec![
1960                        "type hint".to_string(),
1961                        "parameter hint".to_string(),
1962                        "other hint".to_string(),
1963                    ],
1964                    cached_hint_labels(editor),
1965                    "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1966                );
1967                assert_eq!(
1968                    expected_visible_hints,
1969                    visible_hint_labels(editor, cx),
1970                    "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1971                );
1972                let inlay_cache = editor.inlay_hint_cache();
1973                assert_eq!(
1974                    inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1975                    "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1976                );
1977            }).unwrap();
1978        }
1979
1980        let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1981        update_test_language_settings(cx, |settings| {
1982            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1983                show_value_hints: Some(true),
1984                enabled: Some(false),
1985                edit_debounce_ms: Some(0),
1986                scroll_debounce_ms: Some(0),
1987                show_type_hints: Some(
1988                    another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1989                ),
1990                show_parameter_hints: Some(
1991                    another_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1992                ),
1993                show_other_hints: Some(another_allowed_hint_kinds.contains(&None)),
1994                show_background: Some(false),
1995                toggle_on_modifiers_press: None,
1996            })
1997        });
1998        cx.executor().run_until_parked();
1999        editor
2000            .update(cx, |editor, _, cx| {
2001                assert_eq!(
2002                    lsp_request_count.load(Ordering::Relaxed),
2003                    2,
2004                    "Should not load new hints when hints got disabled"
2005                );
2006                assert!(
2007                    cached_hint_labels(editor).is_empty(),
2008                    "Should clear the cache when hints got disabled"
2009                );
2010                assert!(
2011                    visible_hint_labels(editor, cx).is_empty(),
2012                    "Should clear visible hints when hints got disabled"
2013                );
2014                let inlay_cache = editor.inlay_hint_cache();
2015                assert_eq!(
2016                    inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
2017                    "Should update its allowed hint kinds even when hints got disabled"
2018                );
2019            })
2020            .unwrap();
2021
2022        fake_server
2023            .request::<lsp::request::InlayHintRefreshRequest>(())
2024            .await
2025            .into_response()
2026            .expect("inlay refresh request failed");
2027        cx.executor().run_until_parked();
2028        editor
2029            .update(cx, |editor, _window, cx| {
2030                assert_eq!(
2031                    lsp_request_count.load(Ordering::Relaxed),
2032                    2,
2033                    "Should not load new hints when they got disabled"
2034                );
2035                assert!(cached_hint_labels(editor).is_empty());
2036                assert!(visible_hint_labels(editor, cx).is_empty());
2037            })
2038            .unwrap();
2039
2040        let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
2041        update_test_language_settings(cx, |settings| {
2042            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2043                show_value_hints: Some(true),
2044                enabled: Some(true),
2045                edit_debounce_ms: Some(0),
2046                scroll_debounce_ms: Some(0),
2047                show_type_hints: Some(
2048                    final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
2049                ),
2050                show_parameter_hints: Some(
2051                    final_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
2052                ),
2053                show_other_hints: Some(final_allowed_hint_kinds.contains(&None)),
2054                show_background: Some(false),
2055                toggle_on_modifiers_press: None,
2056            })
2057        });
2058        cx.executor().run_until_parked();
2059        editor
2060            .update(cx, |editor, _, cx| {
2061                assert_eq!(
2062                    lsp_request_count.load(Ordering::Relaxed),
2063                    3,
2064                    "Should query for new hints when they got re-enabled"
2065                );
2066                assert_eq!(
2067                    vec![
2068                        "type hint".to_string(),
2069                        "parameter hint".to_string(),
2070                        "other hint".to_string(),
2071                    ],
2072                    cached_hint_labels(editor),
2073                    "Should get its cached hints fully repopulated after the hints got re-enabled"
2074                );
2075                assert_eq!(
2076                    vec!["parameter hint".to_string()],
2077                    visible_hint_labels(editor, cx),
2078                    "Should get its visible hints repopulated and filtered after the h"
2079                );
2080                let inlay_cache = editor.inlay_hint_cache();
2081                assert_eq!(
2082                    inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
2083                    "Cache should update editor settings when hints got re-enabled"
2084                );
2085            })
2086            .unwrap();
2087
2088        fake_server
2089            .request::<lsp::request::InlayHintRefreshRequest>(())
2090            .await
2091            .into_response()
2092            .expect("inlay refresh request failed");
2093        cx.executor().run_until_parked();
2094        editor
2095            .update(cx, |editor, _, cx| {
2096                assert_eq!(
2097                    lsp_request_count.load(Ordering::Relaxed),
2098                    4,
2099                    "Should query for new hints again"
2100                );
2101                assert_eq!(
2102                    vec![
2103                        "type hint".to_string(),
2104                        "parameter hint".to_string(),
2105                        "other hint".to_string(),
2106                    ],
2107                    cached_hint_labels(editor),
2108                );
2109                assert_eq!(
2110                    vec!["parameter hint".to_string()],
2111                    visible_hint_labels(editor, cx),
2112                );
2113            })
2114            .unwrap();
2115    }
2116
2117    #[gpui::test]
2118    async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
2119        init_test(cx, |settings| {
2120            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2121                show_value_hints: Some(true),
2122                enabled: Some(true),
2123                edit_debounce_ms: Some(0),
2124                scroll_debounce_ms: Some(0),
2125                show_type_hints: Some(true),
2126                show_parameter_hints: Some(true),
2127                show_other_hints: Some(true),
2128                show_background: Some(false),
2129                toggle_on_modifiers_press: None,
2130            })
2131        });
2132
2133        let lsp_request_count = Arc::new(AtomicU32::new(0));
2134        let (_, editor, _) = prepare_test_objects(cx, {
2135            let lsp_request_count = lsp_request_count.clone();
2136            move |fake_server, file_with_hints| {
2137                let lsp_request_count = lsp_request_count.clone();
2138                fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2139                    move |params, _| {
2140                        let lsp_request_count = lsp_request_count.clone();
2141                        async move {
2142                            let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
2143                            assert_eq!(
2144                                params.text_document.uri,
2145                                lsp::Uri::from_file_path(file_with_hints).unwrap(),
2146                            );
2147                            Ok(Some(vec![lsp::InlayHint {
2148                                position: lsp::Position::new(0, i),
2149                                label: lsp::InlayHintLabel::String(i.to_string()),
2150                                kind: None,
2151                                text_edits: None,
2152                                tooltip: None,
2153                                padding_left: None,
2154                                padding_right: None,
2155                                data: None,
2156                            }]))
2157                        }
2158                    },
2159                );
2160            }
2161        })
2162        .await;
2163
2164        let mut expected_changes = Vec::new();
2165        for change_after_opening in [
2166            "initial change #1",
2167            "initial change #2",
2168            "initial change #3",
2169        ] {
2170            editor
2171                .update(cx, |editor, window, cx| {
2172                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2173                        s.select_ranges([13..13])
2174                    });
2175                    editor.handle_input(change_after_opening, window, cx);
2176                })
2177                .unwrap();
2178            expected_changes.push(change_after_opening);
2179        }
2180
2181        cx.executor().run_until_parked();
2182
2183        editor
2184            .update(cx, |editor, _window, cx| {
2185                let current_text = editor.text(cx);
2186                for change in &expected_changes {
2187                    assert!(
2188                        current_text.contains(change),
2189                        "Should apply all changes made"
2190                    );
2191                }
2192                assert_eq!(
2193                    lsp_request_count.load(Ordering::Relaxed),
2194                    2,
2195                    "Should query new hints twice: for editor init and for the last edit that interrupted all others"
2196                );
2197                let expected_hints = vec!["2".to_string()];
2198                assert_eq!(
2199                    expected_hints,
2200                    cached_hint_labels(editor),
2201                    "Should get hints from the last edit landed only"
2202                );
2203                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2204            })
2205            .unwrap();
2206
2207        let mut edits = Vec::new();
2208        for async_later_change in [
2209            "another change #1",
2210            "another change #2",
2211            "another change #3",
2212        ] {
2213            expected_changes.push(async_later_change);
2214            let task_editor = editor;
2215            edits.push(cx.spawn(|mut cx| async move {
2216                task_editor
2217                    .update(&mut cx, |editor, window, cx| {
2218                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2219                            s.select_ranges([13..13])
2220                        });
2221                        editor.handle_input(async_later_change, window, cx);
2222                    })
2223                    .unwrap();
2224            }));
2225        }
2226        let _ = future::join_all(edits).await;
2227        cx.executor().run_until_parked();
2228
2229        editor
2230            .update(cx, |editor, _, cx| {
2231                let current_text = editor.text(cx);
2232                for change in &expected_changes {
2233                    assert!(
2234                        current_text.contains(change),
2235                        "Should apply all changes made"
2236                    );
2237                }
2238                assert_eq!(
2239                    lsp_request_count.load(Ordering::SeqCst),
2240                    3,
2241                    "Should query new hints one more time, for the last edit only"
2242                );
2243                let expected_hints = vec!["3".to_string()];
2244                assert_eq!(
2245                    expected_hints,
2246                    cached_hint_labels(editor),
2247                    "Should get hints from the last edit landed only"
2248                );
2249                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2250            })
2251            .unwrap();
2252    }
2253
2254    #[gpui::test(iterations = 4)]
2255    async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
2256        init_test(cx, |settings| {
2257            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2258                show_value_hints: Some(true),
2259                enabled: Some(true),
2260                edit_debounce_ms: Some(0),
2261                scroll_debounce_ms: Some(0),
2262                show_type_hints: Some(true),
2263                show_parameter_hints: Some(true),
2264                show_other_hints: Some(true),
2265                show_background: Some(false),
2266                toggle_on_modifiers_press: None,
2267            })
2268        });
2269
2270        let fs = FakeFs::new(cx.background_executor.clone());
2271        fs.insert_tree(
2272            path!("/a"),
2273            json!({
2274                "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
2275                "other.rs": "// Test file",
2276            }),
2277        )
2278        .await;
2279
2280        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2281
2282        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2283        language_registry.add(rust_lang());
2284
2285        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2286        let lsp_request_count = Arc::new(AtomicUsize::new(0));
2287        let mut fake_servers = language_registry.register_fake_lsp(
2288            "Rust",
2289            FakeLspAdapter {
2290                capabilities: lsp::ServerCapabilities {
2291                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2292                    ..Default::default()
2293                },
2294                initializer: Some(Box::new({
2295                    let lsp_request_ranges = lsp_request_ranges.clone();
2296                    let lsp_request_count = lsp_request_count.clone();
2297                    move |fake_server| {
2298                        let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
2299                        let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2300                        fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2301                            move |params, _| {
2302                                let task_lsp_request_ranges =
2303                                    Arc::clone(&closure_lsp_request_ranges);
2304                                let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2305                                async move {
2306                                    assert_eq!(
2307                                        params.text_document.uri,
2308                                        lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2309                                    );
2310
2311                                    task_lsp_request_ranges.lock().push(params.range);
2312                                    task_lsp_request_count.fetch_add(1, Ordering::Release);
2313                                    Ok(Some(vec![lsp::InlayHint {
2314                                        position: params.range.end,
2315                                        label: lsp::InlayHintLabel::String(
2316                                            params.range.end.line.to_string(),
2317                                        ),
2318                                        kind: None,
2319                                        text_edits: None,
2320                                        tooltip: None,
2321                                        padding_left: None,
2322                                        padding_right: None,
2323                                        data: None,
2324                                    }]))
2325                                }
2326                            },
2327                        );
2328                    }
2329                })),
2330                ..Default::default()
2331            },
2332        );
2333
2334        let buffer = project
2335            .update(cx, |project, cx| {
2336                project.open_local_buffer(path!("/a/main.rs"), cx)
2337            })
2338            .await
2339            .unwrap();
2340        let editor =
2341            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
2342
2343        cx.executor().run_until_parked();
2344
2345        let _fake_server = fake_servers.next().await.unwrap();
2346
2347        // in large buffers, requests are made for more than visible range of a buffer.
2348        // invisible parts are queried later, to avoid excessive requests on quick typing.
2349        // wait the timeout needed to get all requests.
2350        cx.executor().advance_clock(Duration::from_millis(
2351            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2352        ));
2353        cx.executor().run_until_parked();
2354        let initial_visible_range = editor_visible_range(&editor, cx);
2355        let lsp_initial_visible_range = lsp::Range::new(
2356            lsp::Position::new(
2357                initial_visible_range.start.row,
2358                initial_visible_range.start.column,
2359            ),
2360            lsp::Position::new(
2361                initial_visible_range.end.row,
2362                initial_visible_range.end.column,
2363            ),
2364        );
2365        let expected_initial_query_range_end =
2366            lsp::Position::new(initial_visible_range.end.row * 2, 2);
2367        let mut expected_invisible_query_start = lsp_initial_visible_range.end;
2368        expected_invisible_query_start.character += 1;
2369        editor.update(cx, |editor, _window, cx| {
2370            let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2371            assert_eq!(ranges.len(), 2,
2372                "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}");
2373            let visible_query_range = &ranges[0];
2374            assert_eq!(visible_query_range.start, lsp_initial_visible_range.start);
2375            assert_eq!(visible_query_range.end, lsp_initial_visible_range.end);
2376            let invisible_query_range = &ranges[1];
2377
2378            assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document");
2379            assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document");
2380
2381            let requests_count = lsp_request_count.load(Ordering::Acquire);
2382            assert_eq!(requests_count, 2, "Visible + invisible request");
2383            let expected_hints = vec!["47".to_string(), "94".to_string()];
2384            assert_eq!(
2385                expected_hints,
2386                cached_hint_labels(editor),
2387                "Should have hints from both LSP requests made for a big file"
2388            );
2389            assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
2390        }).unwrap();
2391
2392        editor
2393            .update(cx, |editor, window, cx| {
2394                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2395            })
2396            .unwrap();
2397        cx.executor().run_until_parked();
2398        editor
2399            .update(cx, |editor, window, cx| {
2400                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2401            })
2402            .unwrap();
2403        cx.executor().advance_clock(Duration::from_millis(
2404            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2405        ));
2406        cx.executor().run_until_parked();
2407        let visible_range_after_scrolls = editor_visible_range(&editor, cx);
2408        let visible_line_count = editor
2409            .update(cx, |editor, _window, _| {
2410                editor.visible_line_count().unwrap()
2411            })
2412            .unwrap();
2413        let selection_in_cached_range = editor
2414            .update(cx, |editor, _window, cx| {
2415                let ranges = lsp_request_ranges
2416                    .lock()
2417                    .drain(..)
2418                    .sorted_by_key(|r| r.start)
2419                    .collect::<Vec<_>>();
2420                assert_eq!(
2421                    ranges.len(),
2422                    2,
2423                    "Should query 2 ranges after both scrolls, but got: {ranges:?}"
2424                );
2425                let first_scroll = &ranges[0];
2426                let second_scroll = &ranges[1];
2427                assert_eq!(
2428                    first_scroll.end, second_scroll.start,
2429                    "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
2430                );
2431                assert_eq!(
2432                first_scroll.start, expected_initial_query_range_end,
2433                "First scroll should start the query right after the end of the original scroll",
2434            );
2435                assert_eq!(
2436                second_scroll.end,
2437                lsp::Position::new(
2438                    visible_range_after_scrolls.end.row
2439                        + visible_line_count.ceil() as u32,
2440                    1,
2441                ),
2442                "Second scroll should query one more screen down after the end of the visible range"
2443            );
2444
2445                let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2446                assert_eq!(lsp_requests, 4, "Should query for hints after every scroll");
2447                let expected_hints = vec![
2448                    "47".to_string(),
2449                    "94".to_string(),
2450                    "139".to_string(),
2451                    "184".to_string(),
2452                ];
2453                assert_eq!(
2454                    expected_hints,
2455                    cached_hint_labels(editor),
2456                    "Should have hints from the new LSP response after the edit"
2457                );
2458                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2459
2460                let mut selection_in_cached_range = visible_range_after_scrolls.end;
2461                selection_in_cached_range.row -= visible_line_count.ceil() as u32;
2462                selection_in_cached_range
2463            })
2464            .unwrap();
2465
2466        editor
2467            .update(cx, |editor, window, cx| {
2468                editor.change_selections(
2469                    SelectionEffects::scroll(Autoscroll::center()),
2470                    window,
2471                    cx,
2472                    |s| s.select_ranges([selection_in_cached_range..selection_in_cached_range]),
2473                );
2474            })
2475            .unwrap();
2476        cx.executor().advance_clock(Duration::from_millis(
2477            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2478        ));
2479        cx.executor().run_until_parked();
2480        editor.update(cx, |_, _, _| {
2481            let ranges = lsp_request_ranges
2482                .lock()
2483                .drain(..)
2484                .sorted_by_key(|r| r.start)
2485                .collect::<Vec<_>>();
2486            assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
2487            assert_eq!(lsp_request_count.load(Ordering::Acquire), 4);
2488        }).unwrap();
2489
2490        editor
2491            .update(cx, |editor, window, cx| {
2492                editor.handle_input("++++more text++++", window, cx);
2493            })
2494            .unwrap();
2495        cx.executor().advance_clock(Duration::from_millis(
2496            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2497        ));
2498        cx.executor().run_until_parked();
2499        editor.update(cx, |editor, _window, cx| {
2500            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2501            ranges.sort_by_key(|r| r.start);
2502
2503            assert_eq!(ranges.len(), 3,
2504                "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
2505            let above_query_range = &ranges[0];
2506            let visible_query_range = &ranges[1];
2507            let below_query_range = &ranges[2];
2508            assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
2509                "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");
2510            assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line  + 1 == below_query_range.start.line,
2511                "Visible range {visible_query_range:?} should be before below range {below_query_range:?}");
2512            assert!(above_query_range.start.line < selection_in_cached_range.row,
2513                "Hints should be queried with the selected range after the query range start");
2514            assert!(below_query_range.end.line > selection_in_cached_range.row,
2515                "Hints should be queried with the selected range before the query range end");
2516            assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32,
2517                "Hints query range should contain one more screen before");
2518            assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32,
2519                "Hints query range should contain one more screen after");
2520
2521            let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2522            assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried");
2523            let expected_hints = vec!["67".to_string(), "115".to_string(), "163".to_string()];
2524            assert_eq!(expected_hints, cached_hint_labels(editor),
2525                "Should have hints from the new LSP response after the edit");
2526            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2527        }).unwrap();
2528    }
2529
2530    fn editor_visible_range(
2531        editor: &WindowHandle<Editor>,
2532        cx: &mut gpui::TestAppContext,
2533    ) -> Range<Point> {
2534        let ranges = editor
2535            .update(cx, |editor, _window, cx| editor.visible_excerpts(None, cx))
2536            .unwrap();
2537        assert_eq!(
2538            ranges.len(),
2539            1,
2540            "Single buffer should produce a single excerpt with visible range"
2541        );
2542        let (_, (excerpt_buffer, _, excerpt_visible_range)) = ranges.into_iter().next().unwrap();
2543        excerpt_buffer.read_with(cx, |buffer, _| {
2544            let snapshot = buffer.snapshot();
2545            let start = buffer
2546                .anchor_before(excerpt_visible_range.start)
2547                .to_point(&snapshot);
2548            let end = buffer
2549                .anchor_after(excerpt_visible_range.end)
2550                .to_point(&snapshot);
2551            start..end
2552        })
2553    }
2554
2555    #[gpui::test]
2556    async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
2557        init_test(cx, |settings| {
2558            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2559                show_value_hints: Some(true),
2560                enabled: Some(true),
2561                edit_debounce_ms: Some(0),
2562                scroll_debounce_ms: Some(0),
2563                show_type_hints: Some(true),
2564                show_parameter_hints: Some(true),
2565                show_other_hints: Some(true),
2566                show_background: Some(false),
2567                toggle_on_modifiers_press: None,
2568            })
2569        });
2570
2571        let fs = FakeFs::new(cx.background_executor.clone());
2572        fs.insert_tree(
2573                path!("/a"),
2574                json!({
2575                    "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2576                    "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2577                }),
2578            )
2579            .await;
2580
2581        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2582
2583        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2584        let language = rust_lang();
2585        language_registry.add(language);
2586        let mut fake_servers = language_registry.register_fake_lsp(
2587            "Rust",
2588            FakeLspAdapter {
2589                capabilities: lsp::ServerCapabilities {
2590                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2591                    ..Default::default()
2592                },
2593                ..Default::default()
2594            },
2595        );
2596
2597        let (buffer_1, _handle1) = project
2598            .update(cx, |project, cx| {
2599                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2600            })
2601            .await
2602            .unwrap();
2603        let (buffer_2, _handle2) = project
2604            .update(cx, |project, cx| {
2605                project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2606            })
2607            .await
2608            .unwrap();
2609        let multibuffer = cx.new(|cx| {
2610            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2611            multibuffer.push_excerpts(
2612                buffer_1.clone(),
2613                [
2614                    ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
2615                    ExcerptRange::new(Point::new(4, 0)..Point::new(11, 0)),
2616                    ExcerptRange::new(Point::new(22, 0)..Point::new(33, 0)),
2617                    ExcerptRange::new(Point::new(44, 0)..Point::new(55, 0)),
2618                    ExcerptRange::new(Point::new(56, 0)..Point::new(66, 0)),
2619                    ExcerptRange::new(Point::new(67, 0)..Point::new(77, 0)),
2620                ],
2621                cx,
2622            );
2623            multibuffer.push_excerpts(
2624                buffer_2.clone(),
2625                [
2626                    ExcerptRange::new(Point::new(0, 1)..Point::new(2, 1)),
2627                    ExcerptRange::new(Point::new(4, 1)..Point::new(11, 1)),
2628                    ExcerptRange::new(Point::new(22, 1)..Point::new(33, 1)),
2629                    ExcerptRange::new(Point::new(44, 1)..Point::new(55, 1)),
2630                    ExcerptRange::new(Point::new(56, 1)..Point::new(66, 1)),
2631                    ExcerptRange::new(Point::new(67, 1)..Point::new(77, 1)),
2632                ],
2633                cx,
2634            );
2635            multibuffer
2636        });
2637
2638        cx.executor().run_until_parked();
2639        let editor = cx.add_window(|window, cx| {
2640            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2641        });
2642
2643        let editor_edited = Arc::new(AtomicBool::new(false));
2644        let fake_server = fake_servers.next().await.unwrap();
2645        let closure_editor_edited = Arc::clone(&editor_edited);
2646        fake_server
2647            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2648                let task_editor_edited = Arc::clone(&closure_editor_edited);
2649                async move {
2650                    let hint_text = if params.text_document.uri
2651                        == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
2652                    {
2653                        "main hint"
2654                    } else if params.text_document.uri
2655                        == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
2656                    {
2657                        "other hint"
2658                    } else {
2659                        panic!("unexpected uri: {:?}", params.text_document.uri);
2660                    };
2661
2662                    // one hint per excerpt
2663                    let positions = [
2664                        lsp::Position::new(0, 2),
2665                        lsp::Position::new(4, 2),
2666                        lsp::Position::new(22, 2),
2667                        lsp::Position::new(44, 2),
2668                        lsp::Position::new(56, 2),
2669                        lsp::Position::new(67, 2),
2670                    ];
2671                    let out_of_range_hint = lsp::InlayHint {
2672                        position: lsp::Position::new(
2673                            params.range.start.line + 99,
2674                            params.range.start.character + 99,
2675                        ),
2676                        label: lsp::InlayHintLabel::String(
2677                            "out of excerpt range, should be ignored".to_string(),
2678                        ),
2679                        kind: None,
2680                        text_edits: None,
2681                        tooltip: None,
2682                        padding_left: None,
2683                        padding_right: None,
2684                        data: None,
2685                    };
2686
2687                    let edited = task_editor_edited.load(Ordering::Acquire);
2688                    Ok(Some(
2689                        std::iter::once(out_of_range_hint)
2690                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
2691                                lsp::InlayHint {
2692                                    position,
2693                                    label: lsp::InlayHintLabel::String(format!(
2694                                        "{hint_text}{E} #{i}",
2695                                        E = if edited { "(edited)" } else { "" },
2696                                    )),
2697                                    kind: None,
2698                                    text_edits: None,
2699                                    tooltip: None,
2700                                    padding_left: None,
2701                                    padding_right: None,
2702                                    data: None,
2703                                }
2704                            }))
2705                            .collect(),
2706                    ))
2707                }
2708            })
2709            .next()
2710            .await;
2711        cx.executor().run_until_parked();
2712
2713        editor
2714            .update(cx, |editor, _window, cx| {
2715                let expected_hints = vec![
2716                    "main hint #0".to_string(),
2717                    "main hint #1".to_string(),
2718                    "main hint #2".to_string(),
2719                    "main hint #3".to_string(),
2720                    "main hint #4".to_string(),
2721                    "main hint #5".to_string(),
2722                ];
2723                assert_eq!(
2724                    expected_hints,
2725                    sorted_cached_hint_labels(editor),
2726                    "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2727                );
2728                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2729            })
2730            .unwrap();
2731
2732        editor
2733            .update(cx, |editor, window, cx| {
2734                editor.change_selections(
2735                    SelectionEffects::scroll(Autoscroll::Next),
2736                    window,
2737                    cx,
2738                    |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
2739                );
2740                editor.change_selections(
2741                    SelectionEffects::scroll(Autoscroll::Next),
2742                    window,
2743                    cx,
2744                    |s| s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]),
2745                );
2746                editor.change_selections(
2747                    SelectionEffects::scroll(Autoscroll::Next),
2748                    window,
2749                    cx,
2750                    |s| s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]),
2751                );
2752            })
2753            .unwrap();
2754        cx.executor().run_until_parked();
2755        editor
2756            .update(cx, |editor, _window, cx| {
2757                let expected_hints = vec![
2758                    "main hint #0".to_string(),
2759                    "main hint #1".to_string(),
2760                    "main hint #2".to_string(),
2761                    "main hint #3".to_string(),
2762                    "main hint #4".to_string(),
2763                    "main hint #5".to_string(),
2764                    "other hint #0".to_string(),
2765                    "other hint #1".to_string(),
2766                    "other hint #2".to_string(),
2767                ];
2768                assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2769                    "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2770                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2771            })
2772            .unwrap();
2773
2774        editor
2775            .update(cx, |editor, window, cx| {
2776                editor.change_selections(
2777                    SelectionEffects::scroll(Autoscroll::Next),
2778                    window,
2779                    cx,
2780                    |s| s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]),
2781                );
2782            })
2783            .unwrap();
2784        cx.executor().advance_clock(Duration::from_millis(
2785            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2786        ));
2787        cx.executor().run_until_parked();
2788        editor
2789            .update(cx, |editor, _window, cx| {
2790                let expected_hints = vec![
2791                    "main hint #0".to_string(),
2792                    "main hint #1".to_string(),
2793                    "main hint #2".to_string(),
2794                    "main hint #3".to_string(),
2795                    "main hint #4".to_string(),
2796                    "main hint #5".to_string(),
2797                    "other hint #0".to_string(),
2798                    "other hint #1".to_string(),
2799                    "other hint #2".to_string(),
2800                    "other hint #3".to_string(),
2801                    "other hint #4".to_string(),
2802                    "other hint #5".to_string(),
2803                ];
2804                assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2805                    "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2806                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2807            })
2808            .unwrap();
2809
2810        editor
2811            .update(cx, |editor, window, cx| {
2812                editor.change_selections(
2813                    SelectionEffects::scroll(Autoscroll::Next),
2814                    window,
2815                    cx,
2816                    |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
2817                );
2818            })
2819            .unwrap();
2820        cx.executor().advance_clock(Duration::from_millis(
2821            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2822        ));
2823        cx.executor().run_until_parked();
2824        editor
2825            .update(cx, |editor, _window, cx| {
2826                let expected_hints = vec![
2827                    "main hint #0".to_string(),
2828                    "main hint #1".to_string(),
2829                    "main hint #2".to_string(),
2830                    "main hint #3".to_string(),
2831                    "main hint #4".to_string(),
2832                    "main hint #5".to_string(),
2833                    "other hint #0".to_string(),
2834                    "other hint #1".to_string(),
2835                    "other hint #2".to_string(),
2836                    "other hint #3".to_string(),
2837                    "other hint #4".to_string(),
2838                    "other hint #5".to_string(),
2839                ];
2840                assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2841                    "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2842                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2843            })
2844            .unwrap();
2845
2846        editor_edited.store(true, Ordering::Release);
2847        editor
2848            .update(cx, |editor, window, cx| {
2849                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2850                    s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
2851                });
2852                editor.handle_input("++++more text++++", window, cx);
2853            })
2854            .unwrap();
2855        cx.executor().run_until_parked();
2856        editor
2857            .update(cx, |editor, _window, cx| {
2858                let expected_hints = vec![
2859                    "main hint #0".to_string(),
2860                    "main hint #1".to_string(),
2861                    "main hint #2".to_string(),
2862                    "main hint #3".to_string(),
2863                    "main hint #4".to_string(),
2864                    "main hint #5".to_string(),
2865                    "other hint(edited) #0".to_string(),
2866                    "other hint(edited) #1".to_string(),
2867                ];
2868                assert_eq!(
2869                    expected_hints,
2870                    sorted_cached_hint_labels(editor),
2871                    "After multibuffer edit, editor gets scrolled back to the last selection; \
2872                all hints should be invalidated and required for all of its visible excerpts"
2873                );
2874                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2875            })
2876            .unwrap();
2877    }
2878
2879    #[gpui::test]
2880    async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
2881        init_test(cx, |settings| {
2882            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2883                show_value_hints: Some(true),
2884                enabled: Some(true),
2885                edit_debounce_ms: Some(0),
2886                scroll_debounce_ms: Some(0),
2887                show_type_hints: Some(false),
2888                show_parameter_hints: Some(false),
2889                show_other_hints: Some(false),
2890                show_background: Some(false),
2891                toggle_on_modifiers_press: None,
2892            })
2893        });
2894
2895        let fs = FakeFs::new(cx.background_executor.clone());
2896        fs.insert_tree(
2897            path!("/a"),
2898            json!({
2899                "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2900                "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2901            }),
2902        )
2903        .await;
2904
2905        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2906
2907        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2908        language_registry.add(rust_lang());
2909        let mut fake_servers = language_registry.register_fake_lsp(
2910            "Rust",
2911            FakeLspAdapter {
2912                capabilities: lsp::ServerCapabilities {
2913                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2914                    ..Default::default()
2915                },
2916                ..Default::default()
2917            },
2918        );
2919
2920        let (buffer_1, _handle) = project
2921            .update(cx, |project, cx| {
2922                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2923            })
2924            .await
2925            .unwrap();
2926        let (buffer_2, _handle2) = project
2927            .update(cx, |project, cx| {
2928                project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2929            })
2930            .await
2931            .unwrap();
2932        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2933        let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2934            let buffer_1_excerpts = multibuffer.push_excerpts(
2935                buffer_1.clone(),
2936                [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
2937                cx,
2938            );
2939            let buffer_2_excerpts = multibuffer.push_excerpts(
2940                buffer_2.clone(),
2941                [ExcerptRange::new(Point::new(0, 1)..Point::new(2, 1))],
2942                cx,
2943            );
2944            (buffer_1_excerpts, buffer_2_excerpts)
2945        });
2946
2947        assert!(!buffer_1_excerpts.is_empty());
2948        assert!(!buffer_2_excerpts.is_empty());
2949
2950        cx.executor().run_until_parked();
2951        let editor = cx.add_window(|window, cx| {
2952            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2953        });
2954        let editor_edited = Arc::new(AtomicBool::new(false));
2955        let fake_server = fake_servers.next().await.unwrap();
2956        let closure_editor_edited = Arc::clone(&editor_edited);
2957        fake_server
2958            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2959                let task_editor_edited = Arc::clone(&closure_editor_edited);
2960                async move {
2961                    let hint_text = if params.text_document.uri
2962                        == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
2963                    {
2964                        "main hint"
2965                    } else if params.text_document.uri
2966                        == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
2967                    {
2968                        "other hint"
2969                    } else {
2970                        panic!("unexpected uri: {:?}", params.text_document.uri);
2971                    };
2972
2973                    let positions = [
2974                        lsp::Position::new(0, 2),
2975                        lsp::Position::new(4, 2),
2976                        lsp::Position::new(22, 2),
2977                        lsp::Position::new(44, 2),
2978                        lsp::Position::new(56, 2),
2979                        lsp::Position::new(67, 2),
2980                    ];
2981                    let out_of_range_hint = lsp::InlayHint {
2982                        position: lsp::Position::new(
2983                            params.range.start.line + 99,
2984                            params.range.start.character + 99,
2985                        ),
2986                        label: lsp::InlayHintLabel::String(
2987                            "out of excerpt range, should be ignored".to_string(),
2988                        ),
2989                        kind: None,
2990                        text_edits: None,
2991                        tooltip: None,
2992                        padding_left: None,
2993                        padding_right: None,
2994                        data: None,
2995                    };
2996
2997                    let edited = task_editor_edited.load(Ordering::Acquire);
2998                    Ok(Some(
2999                        std::iter::once(out_of_range_hint)
3000                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
3001                                lsp::InlayHint {
3002                                    position,
3003                                    label: lsp::InlayHintLabel::String(format!(
3004                                        "{hint_text}{} #{i}",
3005                                        if edited { "(edited)" } else { "" },
3006                                    )),
3007                                    kind: None,
3008                                    text_edits: None,
3009                                    tooltip: None,
3010                                    padding_left: None,
3011                                    padding_right: None,
3012                                    data: None,
3013                                }
3014                            }))
3015                            .collect(),
3016                    ))
3017                }
3018            })
3019            .next()
3020            .await;
3021        cx.executor().run_until_parked();
3022        editor
3023            .update(cx, |editor, _, cx| {
3024                assert_eq!(
3025                    vec!["main hint #0".to_string(), "other hint #0".to_string()],
3026                    sorted_cached_hint_labels(editor),
3027                    "Cache should update for both excerpts despite hints display was disabled"
3028                );
3029                assert!(
3030                visible_hint_labels(editor, cx).is_empty(),
3031                "All hints are disabled and should not be shown despite being present in the cache"
3032            );
3033            })
3034            .unwrap();
3035
3036        editor
3037            .update(cx, |editor, _, cx| {
3038                editor.buffer().update(cx, |multibuffer, cx| {
3039                    multibuffer.remove_excerpts(buffer_2_excerpts, cx)
3040                })
3041            })
3042            .unwrap();
3043        cx.executor().run_until_parked();
3044        editor
3045            .update(cx, |editor, _, cx| {
3046                assert_eq!(
3047                    vec!["main hint #0".to_string()],
3048                    cached_hint_labels(editor),
3049                    "For the removed excerpt, should clean corresponding cached hints"
3050                );
3051                assert!(
3052                visible_hint_labels(editor, cx).is_empty(),
3053                "All hints are disabled and should not be shown despite being present in the cache"
3054            );
3055            })
3056            .unwrap();
3057
3058        update_test_language_settings(cx, |settings| {
3059            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3060                show_value_hints: Some(true),
3061                enabled: Some(true),
3062                edit_debounce_ms: Some(0),
3063                scroll_debounce_ms: Some(0),
3064                show_type_hints: Some(true),
3065                show_parameter_hints: Some(true),
3066                show_other_hints: Some(true),
3067                show_background: Some(false),
3068                toggle_on_modifiers_press: None,
3069            })
3070        });
3071        cx.executor().run_until_parked();
3072        editor
3073            .update(cx, |editor, _, cx| {
3074                let expected_hints = vec!["main hint #0".to_string()];
3075                assert_eq!(
3076                    expected_hints,
3077                    cached_hint_labels(editor),
3078                    "Hint display settings change should not change the cache"
3079                );
3080                assert_eq!(
3081                    expected_hints,
3082                    visible_hint_labels(editor, cx),
3083                    "Settings change should make cached hints visible"
3084                );
3085            })
3086            .unwrap();
3087    }
3088
3089    #[gpui::test]
3090    async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
3091        init_test(cx, |settings| {
3092            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3093                show_value_hints: Some(true),
3094                enabled: Some(true),
3095                edit_debounce_ms: Some(0),
3096                scroll_debounce_ms: Some(0),
3097                show_type_hints: Some(true),
3098                show_parameter_hints: Some(true),
3099                show_other_hints: Some(true),
3100                show_background: Some(false),
3101                toggle_on_modifiers_press: None,
3102            })
3103        });
3104
3105        let fs = FakeFs::new(cx.background_executor.clone());
3106        fs.insert_tree(
3107            path!("/a"),
3108            json!({
3109                "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "".repeat(10)).repeat(500)),
3110                "other.rs": "// Test file",
3111            }),
3112        )
3113        .await;
3114
3115        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3116
3117        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3118        language_registry.add(rust_lang());
3119        language_registry.register_fake_lsp(
3120            "Rust",
3121            FakeLspAdapter {
3122                capabilities: lsp::ServerCapabilities {
3123                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3124                    ..Default::default()
3125                },
3126                initializer: Some(Box::new(move |fake_server| {
3127                    let lsp_request_count = Arc::new(AtomicU32::new(0));
3128                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3129                        move |params, _| {
3130                            let i = lsp_request_count.fetch_add(1, Ordering::Release) + 1;
3131                            async move {
3132                                assert_eq!(
3133                                    params.text_document.uri,
3134                                    lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3135                                );
3136                                let query_start = params.range.start;
3137                                Ok(Some(vec![lsp::InlayHint {
3138                                    position: query_start,
3139                                    label: lsp::InlayHintLabel::String(i.to_string()),
3140                                    kind: None,
3141                                    text_edits: None,
3142                                    tooltip: None,
3143                                    padding_left: None,
3144                                    padding_right: None,
3145                                    data: None,
3146                                }]))
3147                            }
3148                        },
3149                    );
3150                })),
3151                ..Default::default()
3152            },
3153        );
3154
3155        let buffer = project
3156            .update(cx, |project, cx| {
3157                project.open_local_buffer(path!("/a/main.rs"), cx)
3158            })
3159            .await
3160            .unwrap();
3161        let editor =
3162            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3163
3164        cx.executor().run_until_parked();
3165        editor
3166            .update(cx, |editor, window, cx| {
3167                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3168                    s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3169                })
3170            })
3171            .unwrap();
3172        cx.executor().run_until_parked();
3173        editor
3174            .update(cx, |editor, _, cx| {
3175                let expected_hints = vec!["1".to_string()];
3176                assert_eq!(expected_hints, cached_hint_labels(editor));
3177                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3178            })
3179            .unwrap();
3180    }
3181
3182    #[gpui::test]
3183    async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
3184        init_test(cx, |settings| {
3185            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3186                show_value_hints: Some(true),
3187                enabled: Some(false),
3188                edit_debounce_ms: Some(0),
3189                scroll_debounce_ms: Some(0),
3190                show_type_hints: Some(true),
3191                show_parameter_hints: Some(true),
3192                show_other_hints: Some(true),
3193                show_background: Some(false),
3194                toggle_on_modifiers_press: None,
3195            })
3196        });
3197
3198        let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3199            let lsp_request_count = Arc::new(AtomicU32::new(0));
3200            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3201                move |params, _| {
3202                    let lsp_request_count = lsp_request_count.clone();
3203                    async move {
3204                        assert_eq!(
3205                            params.text_document.uri,
3206                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
3207                        );
3208
3209                        let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
3210                        Ok(Some(vec![lsp::InlayHint {
3211                            position: lsp::Position::new(0, i),
3212                            label: lsp::InlayHintLabel::String(i.to_string()),
3213                            kind: None,
3214                            text_edits: None,
3215                            tooltip: None,
3216                            padding_left: None,
3217                            padding_right: None,
3218                            data: None,
3219                        }]))
3220                    }
3221                },
3222            );
3223        })
3224        .await;
3225
3226        editor
3227            .update(cx, |editor, window, cx| {
3228                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3229            })
3230            .unwrap();
3231
3232        cx.executor().run_until_parked();
3233        editor
3234            .update(cx, |editor, _, cx| {
3235                let expected_hints = vec!["1".to_string()];
3236                assert_eq!(
3237                    expected_hints,
3238                    cached_hint_labels(editor),
3239                    "Should display inlays after toggle despite them disabled in settings"
3240                );
3241                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3242            })
3243            .unwrap();
3244
3245        editor
3246            .update(cx, |editor, window, cx| {
3247                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3248            })
3249            .unwrap();
3250        cx.executor().run_until_parked();
3251        editor
3252            .update(cx, |editor, _, cx| {
3253                assert!(
3254                    cached_hint_labels(editor).is_empty(),
3255                    "Should clear hints after 2nd toggle"
3256                );
3257                assert!(visible_hint_labels(editor, cx).is_empty());
3258            })
3259            .unwrap();
3260
3261        update_test_language_settings(cx, |settings| {
3262            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3263                show_value_hints: Some(true),
3264                enabled: Some(true),
3265                edit_debounce_ms: Some(0),
3266                scroll_debounce_ms: Some(0),
3267                show_type_hints: Some(true),
3268                show_parameter_hints: Some(true),
3269                show_other_hints: Some(true),
3270                show_background: Some(false),
3271                toggle_on_modifiers_press: None,
3272            })
3273        });
3274        cx.executor().run_until_parked();
3275        editor
3276            .update(cx, |editor, _, cx| {
3277                let expected_hints = vec!["2".to_string()];
3278                assert_eq!(
3279                    expected_hints,
3280                    cached_hint_labels(editor),
3281                    "Should query LSP hints for the 2nd time after enabling hints in settings"
3282                );
3283                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3284            })
3285            .unwrap();
3286
3287        editor
3288            .update(cx, |editor, window, cx| {
3289                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3290            })
3291            .unwrap();
3292        cx.executor().run_until_parked();
3293        editor
3294            .update(cx, |editor, _, cx| {
3295                assert!(
3296                    cached_hint_labels(editor).is_empty(),
3297                    "Should clear hints after enabling in settings and a 3rd toggle"
3298                );
3299                assert!(visible_hint_labels(editor, cx).is_empty());
3300            })
3301            .unwrap();
3302
3303        editor
3304            .update(cx, |editor, window, cx| {
3305                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3306            })
3307            .unwrap();
3308        cx.executor().run_until_parked();
3309        editor.update(cx, |editor, _, cx| {
3310            let expected_hints = vec!["3".to_string()];
3311            assert_eq!(
3312                expected_hints,
3313                cached_hint_labels(editor),
3314                "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
3315            );
3316            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3317        }).unwrap();
3318    }
3319
3320    #[gpui::test]
3321    async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) {
3322        init_test(cx, |settings| {
3323            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3324                show_value_hints: Some(true),
3325                enabled: Some(true),
3326                edit_debounce_ms: Some(0),
3327                scroll_debounce_ms: Some(0),
3328                show_type_hints: Some(true),
3329                show_parameter_hints: Some(true),
3330                show_other_hints: Some(true),
3331                show_background: Some(false),
3332                toggle_on_modifiers_press: None,
3333            })
3334        });
3335
3336        let fs = FakeFs::new(cx.background_executor.clone());
3337        fs.insert_tree(
3338            path!("/a"),
3339            json!({
3340                "main.rs": "fn main() {
3341                    let x = 42;
3342                    std::thread::scope(|s| {
3343                        s.spawn(|| {
3344                            let _x = x;
3345                        });
3346                    });
3347                }",
3348                "other.rs": "// Test file",
3349            }),
3350        )
3351        .await;
3352
3353        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3354
3355        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3356        language_registry.add(rust_lang());
3357        language_registry.register_fake_lsp(
3358            "Rust",
3359            FakeLspAdapter {
3360                capabilities: lsp::ServerCapabilities {
3361                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3362                    ..Default::default()
3363                },
3364                initializer: Some(Box::new(move |fake_server| {
3365                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3366                        move |params, _| async move {
3367                            assert_eq!(
3368                                params.text_document.uri,
3369                                lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3370                            );
3371                            Ok(Some(
3372                                serde_json::from_value(json!([
3373                                    {
3374                                        "position": {
3375                                            "line": 3,
3376                                            "character": 16
3377                                        },
3378                                        "label": "move",
3379                                        "paddingLeft": false,
3380                                        "paddingRight": false
3381                                    },
3382                                    {
3383                                        "position": {
3384                                            "line": 3,
3385                                            "character": 16
3386                                        },
3387                                        "label": "(",
3388                                        "paddingLeft": false,
3389                                        "paddingRight": false
3390                                    },
3391                                    {
3392                                        "position": {
3393                                            "line": 3,
3394                                            "character": 16
3395                                        },
3396                                        "label": [
3397                                            {
3398                                                "value": "&x"
3399                                            }
3400                                        ],
3401                                        "paddingLeft": false,
3402                                        "paddingRight": false,
3403                                        "data": {
3404                                            "file_id": 0
3405                                        }
3406                                    },
3407                                    {
3408                                        "position": {
3409                                            "line": 3,
3410                                            "character": 16
3411                                        },
3412                                        "label": ")",
3413                                        "paddingLeft": false,
3414                                        "paddingRight": true
3415                                    },
3416                                    // not a correct syntax, but checks that same symbols at the same place
3417                                    // are not deduplicated
3418                                    {
3419                                        "position": {
3420                                            "line": 3,
3421                                            "character": 16
3422                                        },
3423                                        "label": ")",
3424                                        "paddingLeft": false,
3425                                        "paddingRight": true
3426                                    },
3427                                ]))
3428                                .unwrap(),
3429                            ))
3430                        },
3431                    );
3432                })),
3433                ..FakeLspAdapter::default()
3434            },
3435        );
3436
3437        let buffer = project
3438            .update(cx, |project, cx| {
3439                project.open_local_buffer(path!("/a/main.rs"), cx)
3440            })
3441            .await
3442            .unwrap();
3443        let editor =
3444            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3445
3446        cx.executor().run_until_parked();
3447        editor
3448            .update(cx, |editor, window, cx| {
3449                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3450                    s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3451                })
3452            })
3453            .unwrap();
3454        cx.executor().run_until_parked();
3455        editor
3456            .update(cx, |editor, _window, cx| {
3457                let expected_hints = vec![
3458                    "move".to_string(),
3459                    "(".to_string(),
3460                    "&x".to_string(),
3461                    ") ".to_string(),
3462                    ") ".to_string(),
3463                ];
3464                assert_eq!(
3465                    expected_hints,
3466                    cached_hint_labels(editor),
3467                    "Editor inlay hints should repeat server's order when placed at the same spot"
3468                );
3469                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3470            })
3471            .unwrap();
3472    }
3473
3474    pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
3475        cx.update(|cx| {
3476            let settings_store = SettingsStore::test(cx);
3477            cx.set_global(settings_store);
3478            theme::init(theme::LoadThemes::JustBase, cx);
3479            release_channel::init(SemanticVersion::default(), cx);
3480            client::init_settings(cx);
3481            language::init(cx);
3482            Project::init_settings(cx);
3483            workspace::init_settings(cx);
3484            crate::init(cx);
3485        });
3486
3487        update_test_language_settings(cx, f);
3488    }
3489
3490    async fn prepare_test_objects(
3491        cx: &mut TestAppContext,
3492        initialize: impl 'static + Send + Fn(&mut FakeLanguageServer, &'static str) + Send + Sync,
3493    ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
3494        let fs = FakeFs::new(cx.background_executor.clone());
3495        fs.insert_tree(
3496            path!("/a"),
3497            json!({
3498                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
3499                "other.rs": "// Test file",
3500            }),
3501        )
3502        .await;
3503
3504        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3505        let file_path = path!("/a/main.rs");
3506
3507        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3508        language_registry.add(rust_lang());
3509        let mut fake_servers = language_registry.register_fake_lsp(
3510            "Rust",
3511            FakeLspAdapter {
3512                capabilities: lsp::ServerCapabilities {
3513                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3514                    ..Default::default()
3515                },
3516                initializer: Some(Box::new(move |server| initialize(server, file_path))),
3517                ..Default::default()
3518            },
3519        );
3520
3521        let buffer = project
3522            .update(cx, |project, cx| {
3523                project.open_local_buffer(path!("/a/main.rs"), cx)
3524            })
3525            .await
3526            .unwrap();
3527        let editor =
3528            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3529
3530        editor
3531            .update(cx, |editor, _, cx| {
3532                assert!(cached_hint_labels(editor).is_empty());
3533                assert!(visible_hint_labels(editor, cx).is_empty());
3534            })
3535            .unwrap();
3536
3537        cx.executor().run_until_parked();
3538        let fake_server = fake_servers.next().await.unwrap();
3539        (file_path, editor, fake_server)
3540    }
3541
3542    // 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.
3543    // Ensure a stable order for testing.
3544    fn sorted_cached_hint_labels(editor: &Editor) -> Vec<String> {
3545        let mut labels = cached_hint_labels(editor);
3546        labels.sort();
3547        labels
3548    }
3549
3550    pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
3551        let mut labels = Vec::new();
3552        for excerpt_hints in editor.inlay_hint_cache().hints.values() {
3553            let excerpt_hints = excerpt_hints.read();
3554            for id in &excerpt_hints.ordered_hints {
3555                let hint = &excerpt_hints.hints_by_id[id];
3556                let mut label = hint.text().to_string();
3557                if hint.padding_left {
3558                    label.insert(0, ' ');
3559                }
3560                if hint.padding_right {
3561                    label.push_str(" ");
3562                }
3563                labels.push(label);
3564            }
3565        }
3566
3567        labels
3568    }
3569
3570    pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
3571        editor
3572            .visible_inlay_hints(cx)
3573            .into_iter()
3574            .map(|hint| hint.text().to_string())
3575            .collect()
3576    }
3577}