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