inlay_hint_cache.rs

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