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.update(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.update(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.update(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            .expect("inlay refresh request failed");
1413        cx.executor().run_until_parked();
1414        editor
1415            .update(cx, |editor, _window, cx| {
1416                let expected_hints = vec!["3".to_string()];
1417                assert_eq!(
1418                    expected_hints,
1419                    cached_hint_labels(editor),
1420                    "Should get new hints after hint refresh/ request"
1421                );
1422                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1423                let inlay_cache = editor.inlay_hint_cache();
1424                assert_eq!(
1425                    inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1426                    "Cache should use editor settings to get the allowed hint kinds"
1427                );
1428            })
1429            .unwrap();
1430    }
1431
1432    #[gpui::test]
1433    async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
1434        init_test(cx, |settings| {
1435            settings.defaults.inlay_hints = Some(InlayHintSettings {
1436                show_value_hints: true,
1437                enabled: true,
1438                edit_debounce_ms: 0,
1439                scroll_debounce_ms: 0,
1440                show_type_hints: true,
1441                show_parameter_hints: true,
1442                show_other_hints: true,
1443                show_background: false,
1444                toggle_on_modifiers_press: None,
1445            })
1446        });
1447
1448        let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1449            let lsp_request_count = Arc::new(AtomicU32::new(0));
1450            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1451                move |params, _| {
1452                    let task_lsp_request_count = Arc::clone(&lsp_request_count);
1453                    async move {
1454                        assert_eq!(
1455                            params.text_document.uri,
1456                            lsp::Url::from_file_path(file_with_hints).unwrap(),
1457                        );
1458                        let current_call_id =
1459                            Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1460                        Ok(Some(vec![lsp::InlayHint {
1461                            position: lsp::Position::new(0, current_call_id),
1462                            label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1463                            kind: None,
1464                            text_edits: None,
1465                            tooltip: None,
1466                            padding_left: None,
1467                            padding_right: None,
1468                            data: None,
1469                        }]))
1470                    }
1471                },
1472            );
1473        })
1474        .await;
1475        cx.executor().run_until_parked();
1476
1477        editor
1478            .update(cx, |editor, _, cx| {
1479                let expected_hints = vec!["0".to_string()];
1480                assert_eq!(
1481                    expected_hints,
1482                    cached_hint_labels(editor),
1483                    "Should get its first hints when opening the editor"
1484                );
1485                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1486            })
1487            .unwrap();
1488
1489        let progress_token = "test_progress_token";
1490        fake_server
1491            .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
1492                token: lsp::ProgressToken::String(progress_token.to_string()),
1493            })
1494            .await
1495            .expect("work done progress create request failed");
1496        cx.executor().run_until_parked();
1497        fake_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1498            token: lsp::ProgressToken::String(progress_token.to_string()),
1499            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1500                lsp::WorkDoneProgressBegin::default(),
1501            )),
1502        });
1503        cx.executor().run_until_parked();
1504
1505        editor
1506            .update(cx, |editor, _, cx| {
1507                let expected_hints = vec!["0".to_string()];
1508                assert_eq!(
1509                    expected_hints,
1510                    cached_hint_labels(editor),
1511                    "Should not update hints while the work task is running"
1512                );
1513                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1514            })
1515            .unwrap();
1516
1517        fake_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1518            token: lsp::ProgressToken::String(progress_token.to_string()),
1519            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1520                lsp::WorkDoneProgressEnd::default(),
1521            )),
1522        });
1523        cx.executor().run_until_parked();
1524
1525        editor
1526            .update(cx, |editor, _, cx| {
1527                let expected_hints = vec!["1".to_string()];
1528                assert_eq!(
1529                    expected_hints,
1530                    cached_hint_labels(editor),
1531                    "New hints should be queried after the work task is done"
1532                );
1533                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1534            })
1535            .unwrap();
1536    }
1537
1538    #[gpui::test]
1539    async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1540        init_test(cx, |settings| {
1541            settings.defaults.inlay_hints = Some(InlayHintSettings {
1542                show_value_hints: true,
1543                enabled: true,
1544                edit_debounce_ms: 0,
1545                scroll_debounce_ms: 0,
1546                show_type_hints: true,
1547                show_parameter_hints: true,
1548                show_other_hints: true,
1549                show_background: false,
1550                toggle_on_modifiers_press: None,
1551            })
1552        });
1553
1554        let fs = FakeFs::new(cx.background_executor.clone());
1555        fs.insert_tree(
1556            path!("/a"),
1557            json!({
1558                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1559                "other.md": "Test md file with some text",
1560            }),
1561        )
1562        .await;
1563
1564        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1565
1566        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1567        let mut rs_fake_servers = None;
1568        let mut md_fake_servers = None;
1569        for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1570            language_registry.add(Arc::new(Language::new(
1571                LanguageConfig {
1572                    name: name.into(),
1573                    matcher: LanguageMatcher {
1574                        path_suffixes: vec![path_suffix.to_string()],
1575                        ..Default::default()
1576                    },
1577                    ..Default::default()
1578                },
1579                Some(tree_sitter_rust::LANGUAGE.into()),
1580            )));
1581            let fake_servers = language_registry.register_fake_lsp(
1582                name,
1583                FakeLspAdapter {
1584                    name,
1585                    capabilities: lsp::ServerCapabilities {
1586                        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1587                        ..Default::default()
1588                    },
1589                    initializer: Some(Box::new({
1590                        move |fake_server| {
1591                            let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1592                            let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1593                            fake_server
1594                                .set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1595                                    move |params, _| {
1596                                        let i = match name {
1597                                            "Rust" => {
1598                                                assert_eq!(
1599                                                    params.text_document.uri,
1600                                                    lsp::Url::from_file_path(path!("/a/main.rs"))
1601                                                        .unwrap(),
1602                                                );
1603                                                rs_lsp_request_count.fetch_add(1, Ordering::Release)
1604                                                    + 1
1605                                            }
1606                                            "Markdown" => {
1607                                                assert_eq!(
1608                                                    params.text_document.uri,
1609                                                    lsp::Url::from_file_path(path!("/a/other.md"))
1610                                                        .unwrap(),
1611                                                );
1612                                                md_lsp_request_count.fetch_add(1, Ordering::Release)
1613                                                    + 1
1614                                            }
1615                                            unexpected => {
1616                                                panic!("Unexpected language: {unexpected}")
1617                                            }
1618                                        };
1619
1620                                        async move {
1621                                            let query_start = params.range.start;
1622                                            Ok(Some(vec![lsp::InlayHint {
1623                                                position: query_start,
1624                                                label: lsp::InlayHintLabel::String(i.to_string()),
1625                                                kind: None,
1626                                                text_edits: None,
1627                                                tooltip: None,
1628                                                padding_left: None,
1629                                                padding_right: None,
1630                                                data: None,
1631                                            }]))
1632                                        }
1633                                    },
1634                                );
1635                        }
1636                    })),
1637                    ..Default::default()
1638                },
1639            );
1640            match name {
1641                "Rust" => rs_fake_servers = Some(fake_servers),
1642                "Markdown" => md_fake_servers = Some(fake_servers),
1643                _ => unreachable!(),
1644            }
1645        }
1646
1647        let rs_buffer = project
1648            .update(cx, |project, cx| {
1649                project.open_local_buffer(path!("/a/main.rs"), cx)
1650            })
1651            .await
1652            .unwrap();
1653        let rs_editor = cx.add_window(|window, cx| {
1654            Editor::for_buffer(rs_buffer, Some(project.clone()), window, cx)
1655        });
1656        cx.executor().run_until_parked();
1657
1658        let _rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1659        cx.executor().run_until_parked();
1660        rs_editor
1661            .update(cx, |editor, _window, cx| {
1662                let expected_hints = vec!["1".to_string()];
1663                assert_eq!(
1664                    expected_hints,
1665                    cached_hint_labels(editor),
1666                    "Should get its first hints when opening the editor"
1667                );
1668                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1669            })
1670            .unwrap();
1671
1672        cx.executor().run_until_parked();
1673        let md_buffer = project
1674            .update(cx, |project, cx| {
1675                project.open_local_buffer(path!("/a/other.md"), cx)
1676            })
1677            .await
1678            .unwrap();
1679        let md_editor =
1680            cx.add_window(|window, cx| Editor::for_buffer(md_buffer, Some(project), window, cx));
1681        cx.executor().run_until_parked();
1682
1683        let _md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1684        cx.executor().run_until_parked();
1685        md_editor
1686            .update(cx, |editor, _window, cx| {
1687                let expected_hints = vec!["1".to_string()];
1688                assert_eq!(
1689                    expected_hints,
1690                    cached_hint_labels(editor),
1691                    "Markdown editor should have a separate version, repeating Rust editor rules"
1692                );
1693                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1694            })
1695            .unwrap();
1696
1697        rs_editor
1698            .update(cx, |editor, window, cx| {
1699                editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1700                editor.handle_input("some rs change", window, cx);
1701            })
1702            .unwrap();
1703        cx.executor().run_until_parked();
1704        rs_editor
1705            .update(cx, |editor, _window, cx| {
1706                // TODO: Here, we do not get "2", because inserting another language server will trigger `RefreshInlayHints` event from the `LspStore`
1707                // A project is listened in every editor, so each of them will react to this event.
1708                //
1709                // We do not have language server IDs for remote projects, so cannot easily say on the editor level,
1710                // whether we should ignore a particular `RefreshInlayHints` event.
1711                let expected_hints = vec!["3".to_string()];
1712                assert_eq!(
1713                    expected_hints,
1714                    cached_hint_labels(editor),
1715                    "Rust inlay cache should change after the edit"
1716                );
1717                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1718            })
1719            .unwrap();
1720        md_editor
1721            .update(cx, |editor, _window, cx| {
1722                let expected_hints = vec!["1".to_string()];
1723                assert_eq!(
1724                    expected_hints,
1725                    cached_hint_labels(editor),
1726                    "Markdown editor should not be affected by Rust editor changes"
1727                );
1728                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1729            })
1730            .unwrap();
1731
1732        md_editor
1733            .update(cx, |editor, window, cx| {
1734                editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1735                editor.handle_input("some md change", window, cx);
1736            })
1737            .unwrap();
1738        cx.executor().run_until_parked();
1739        md_editor
1740            .update(cx, |editor, _window, cx| {
1741                let expected_hints = vec!["2".to_string()];
1742                assert_eq!(
1743                    expected_hints,
1744                    cached_hint_labels(editor),
1745                    "Rust editor should not be affected by Markdown editor changes"
1746                );
1747                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1748            })
1749            .unwrap();
1750        rs_editor
1751            .update(cx, |editor, _window, cx| {
1752                let expected_hints = vec!["3".to_string()];
1753                assert_eq!(
1754                    expected_hints,
1755                    cached_hint_labels(editor),
1756                    "Markdown editor should also change independently"
1757                );
1758                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1759            })
1760            .unwrap();
1761    }
1762
1763    #[gpui::test]
1764    async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1765        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1766        init_test(cx, |settings| {
1767            settings.defaults.inlay_hints = Some(InlayHintSettings {
1768                show_value_hints: true,
1769                enabled: true,
1770                edit_debounce_ms: 0,
1771                scroll_debounce_ms: 0,
1772                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1773                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1774                show_other_hints: allowed_hint_kinds.contains(&None),
1775                show_background: false,
1776                toggle_on_modifiers_press: None,
1777            })
1778        });
1779
1780        let lsp_request_count = Arc::new(AtomicUsize::new(0));
1781        let (_, editor, fake_server) = prepare_test_objects(cx, {
1782            let lsp_request_count = lsp_request_count.clone();
1783            move |fake_server, file_with_hints| {
1784                let lsp_request_count = lsp_request_count.clone();
1785                fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1786                    move |params, _| {
1787                        lsp_request_count.fetch_add(1, Ordering::Release);
1788                        async move {
1789                            assert_eq!(
1790                                params.text_document.uri,
1791                                lsp::Url::from_file_path(file_with_hints).unwrap(),
1792                            );
1793                            Ok(Some(vec![
1794                                lsp::InlayHint {
1795                                    position: lsp::Position::new(0, 1),
1796                                    label: lsp::InlayHintLabel::String("type hint".to_string()),
1797                                    kind: Some(lsp::InlayHintKind::TYPE),
1798                                    text_edits: None,
1799                                    tooltip: None,
1800                                    padding_left: None,
1801                                    padding_right: None,
1802                                    data: None,
1803                                },
1804                                lsp::InlayHint {
1805                                    position: lsp::Position::new(0, 2),
1806                                    label: lsp::InlayHintLabel::String(
1807                                        "parameter hint".to_string(),
1808                                    ),
1809                                    kind: Some(lsp::InlayHintKind::PARAMETER),
1810                                    text_edits: None,
1811                                    tooltip: None,
1812                                    padding_left: None,
1813                                    padding_right: None,
1814                                    data: None,
1815                                },
1816                                lsp::InlayHint {
1817                                    position: lsp::Position::new(0, 3),
1818                                    label: lsp::InlayHintLabel::String("other hint".to_string()),
1819                                    kind: None,
1820                                    text_edits: None,
1821                                    tooltip: None,
1822                                    padding_left: None,
1823                                    padding_right: None,
1824                                    data: None,
1825                                },
1826                            ]))
1827                        }
1828                    },
1829                );
1830            }
1831        })
1832        .await;
1833        cx.executor().run_until_parked();
1834
1835        editor
1836            .update(cx, |editor, _, cx| {
1837                assert_eq!(
1838                    lsp_request_count.load(Ordering::Relaxed),
1839                    1,
1840                    "Should query new hints once"
1841                );
1842                assert_eq!(
1843                    vec![
1844                        "type hint".to_string(),
1845                        "parameter hint".to_string(),
1846                        "other hint".to_string(),
1847                    ],
1848                    cached_hint_labels(editor),
1849                    "Should get its first hints when opening the editor"
1850                );
1851                assert_eq!(
1852                    vec!["type hint".to_string(), "other hint".to_string()],
1853                    visible_hint_labels(editor, cx)
1854                );
1855                let inlay_cache = editor.inlay_hint_cache();
1856                assert_eq!(
1857                    inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1858                    "Cache should use editor settings to get the allowed hint kinds"
1859                );
1860            })
1861            .unwrap();
1862
1863        fake_server
1864            .request::<lsp::request::InlayHintRefreshRequest>(())
1865            .await
1866            .expect("inlay refresh request failed");
1867        cx.executor().run_until_parked();
1868        editor
1869            .update(cx, |editor, _, cx| {
1870                assert_eq!(
1871                    lsp_request_count.load(Ordering::Relaxed),
1872                    2,
1873                    "Should load new hints twice"
1874                );
1875                assert_eq!(
1876                    vec![
1877                        "type hint".to_string(),
1878                        "parameter hint".to_string(),
1879                        "other hint".to_string(),
1880                    ],
1881                    cached_hint_labels(editor),
1882                    "Cached hints should not change due to allowed hint kinds settings update"
1883                );
1884                assert_eq!(
1885                    vec!["type hint".to_string(), "other hint".to_string()],
1886                    visible_hint_labels(editor, cx)
1887                );
1888            })
1889            .unwrap();
1890
1891        for (new_allowed_hint_kinds, expected_visible_hints) in [
1892            (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1893            (
1894                HashSet::from_iter([Some(InlayHintKind::Type)]),
1895                vec!["type hint".to_string()],
1896            ),
1897            (
1898                HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1899                vec!["parameter hint".to_string()],
1900            ),
1901            (
1902                HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1903                vec!["type hint".to_string(), "other hint".to_string()],
1904            ),
1905            (
1906                HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1907                vec!["parameter hint".to_string(), "other hint".to_string()],
1908            ),
1909            (
1910                HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1911                vec!["type hint".to_string(), "parameter hint".to_string()],
1912            ),
1913            (
1914                HashSet::from_iter([
1915                    None,
1916                    Some(InlayHintKind::Type),
1917                    Some(InlayHintKind::Parameter),
1918                ]),
1919                vec![
1920                    "type hint".to_string(),
1921                    "parameter hint".to_string(),
1922                    "other hint".to_string(),
1923                ],
1924            ),
1925        ] {
1926            update_test_language_settings(cx, |settings| {
1927                settings.defaults.inlay_hints = Some(InlayHintSettings {
1928                    show_value_hints: true,
1929                    enabled: true,
1930                    edit_debounce_ms: 0,
1931                    scroll_debounce_ms: 0,
1932                    show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1933                    show_parameter_hints: new_allowed_hint_kinds
1934                        .contains(&Some(InlayHintKind::Parameter)),
1935                    show_other_hints: new_allowed_hint_kinds.contains(&None),
1936                    show_background: false,
1937                    toggle_on_modifiers_press: None,
1938                })
1939            });
1940            cx.executor().run_until_parked();
1941            editor.update(cx, |editor, _, cx| {
1942                assert_eq!(
1943                    lsp_request_count.load(Ordering::Relaxed),
1944                    2,
1945                    "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1946                );
1947                assert_eq!(
1948                    vec![
1949                        "type hint".to_string(),
1950                        "parameter hint".to_string(),
1951                        "other hint".to_string(),
1952                    ],
1953                    cached_hint_labels(editor),
1954                    "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1955                );
1956                assert_eq!(
1957                    expected_visible_hints,
1958                    visible_hint_labels(editor, cx),
1959                    "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1960                );
1961                let inlay_cache = editor.inlay_hint_cache();
1962                assert_eq!(
1963                    inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1964                    "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1965                );
1966            }).unwrap();
1967        }
1968
1969        let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1970        update_test_language_settings(cx, |settings| {
1971            settings.defaults.inlay_hints = Some(InlayHintSettings {
1972                show_value_hints: true,
1973                enabled: false,
1974                edit_debounce_ms: 0,
1975                scroll_debounce_ms: 0,
1976                show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1977                show_parameter_hints: another_allowed_hint_kinds
1978                    .contains(&Some(InlayHintKind::Parameter)),
1979                show_other_hints: another_allowed_hint_kinds.contains(&None),
1980                show_background: false,
1981                toggle_on_modifiers_press: None,
1982            })
1983        });
1984        cx.executor().run_until_parked();
1985        editor
1986            .update(cx, |editor, _, cx| {
1987                assert_eq!(
1988                    lsp_request_count.load(Ordering::Relaxed),
1989                    2,
1990                    "Should not load new hints when hints got disabled"
1991                );
1992                assert!(
1993                    cached_hint_labels(editor).is_empty(),
1994                    "Should clear the cache when hints got disabled"
1995                );
1996                assert!(
1997                    visible_hint_labels(editor, cx).is_empty(),
1998                    "Should clear visible hints when hints got disabled"
1999                );
2000                let inlay_cache = editor.inlay_hint_cache();
2001                assert_eq!(
2002                    inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
2003                    "Should update its allowed hint kinds even when hints got disabled"
2004                );
2005            })
2006            .unwrap();
2007
2008        fake_server
2009            .request::<lsp::request::InlayHintRefreshRequest>(())
2010            .await
2011            .expect("inlay refresh request failed");
2012        cx.executor().run_until_parked();
2013        editor
2014            .update(cx, |editor, _window, cx| {
2015                assert_eq!(
2016                    lsp_request_count.load(Ordering::Relaxed),
2017                    2,
2018                    "Should not load new hints when they got disabled"
2019                );
2020                assert!(cached_hint_labels(editor).is_empty());
2021                assert!(visible_hint_labels(editor, cx).is_empty());
2022            })
2023            .unwrap();
2024
2025        let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
2026        update_test_language_settings(cx, |settings| {
2027            settings.defaults.inlay_hints = Some(InlayHintSettings {
2028                show_value_hints: true,
2029                enabled: true,
2030                edit_debounce_ms: 0,
2031                scroll_debounce_ms: 0,
2032                show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
2033                show_parameter_hints: final_allowed_hint_kinds
2034                    .contains(&Some(InlayHintKind::Parameter)),
2035                show_other_hints: final_allowed_hint_kinds.contains(&None),
2036                show_background: false,
2037                toggle_on_modifiers_press: None,
2038            })
2039        });
2040        cx.executor().run_until_parked();
2041        editor
2042            .update(cx, |editor, _, cx| {
2043                assert_eq!(
2044                    lsp_request_count.load(Ordering::Relaxed),
2045                    3,
2046                    "Should query for new hints when they got re-enabled"
2047                );
2048                assert_eq!(
2049                    vec![
2050                        "type hint".to_string(),
2051                        "parameter hint".to_string(),
2052                        "other hint".to_string(),
2053                    ],
2054                    cached_hint_labels(editor),
2055                    "Should get its cached hints fully repopulated after the hints got re-enabled"
2056                );
2057                assert_eq!(
2058                    vec!["parameter hint".to_string()],
2059                    visible_hint_labels(editor, cx),
2060                    "Should get its visible hints repopulated and filtered after the h"
2061                );
2062                let inlay_cache = editor.inlay_hint_cache();
2063                assert_eq!(
2064                    inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
2065                    "Cache should update editor settings when hints got re-enabled"
2066                );
2067            })
2068            .unwrap();
2069
2070        fake_server
2071            .request::<lsp::request::InlayHintRefreshRequest>(())
2072            .await
2073            .expect("inlay refresh request failed");
2074        cx.executor().run_until_parked();
2075        editor
2076            .update(cx, |editor, _, cx| {
2077                assert_eq!(
2078                    lsp_request_count.load(Ordering::Relaxed),
2079                    4,
2080                    "Should query for new hints again"
2081                );
2082                assert_eq!(
2083                    vec![
2084                        "type hint".to_string(),
2085                        "parameter hint".to_string(),
2086                        "other hint".to_string(),
2087                    ],
2088                    cached_hint_labels(editor),
2089                );
2090                assert_eq!(
2091                    vec!["parameter hint".to_string()],
2092                    visible_hint_labels(editor, cx),
2093                );
2094            })
2095            .unwrap();
2096    }
2097
2098    #[gpui::test]
2099    async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
2100        init_test(cx, |settings| {
2101            settings.defaults.inlay_hints = Some(InlayHintSettings {
2102                show_value_hints: true,
2103                enabled: true,
2104                edit_debounce_ms: 0,
2105                scroll_debounce_ms: 0,
2106                show_type_hints: true,
2107                show_parameter_hints: true,
2108                show_other_hints: true,
2109                show_background: false,
2110                toggle_on_modifiers_press: None,
2111            })
2112        });
2113
2114        let lsp_request_count = Arc::new(AtomicU32::new(0));
2115        let (_, editor, _) = prepare_test_objects(cx, {
2116            let lsp_request_count = lsp_request_count.clone();
2117            move |fake_server, file_with_hints| {
2118                let lsp_request_count = lsp_request_count.clone();
2119                fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2120                    move |params, _| {
2121                        let lsp_request_count = lsp_request_count.clone();
2122                        async move {
2123                            let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
2124                            assert_eq!(
2125                                params.text_document.uri,
2126                                lsp::Url::from_file_path(file_with_hints).unwrap(),
2127                            );
2128                            Ok(Some(vec![lsp::InlayHint {
2129                                position: lsp::Position::new(0, i),
2130                                label: lsp::InlayHintLabel::String(i.to_string()),
2131                                kind: None,
2132                                text_edits: None,
2133                                tooltip: None,
2134                                padding_left: None,
2135                                padding_right: None,
2136                                data: None,
2137                            }]))
2138                        }
2139                    },
2140                );
2141            }
2142        })
2143        .await;
2144
2145        let mut expected_changes = Vec::new();
2146        for change_after_opening in [
2147            "initial change #1",
2148            "initial change #2",
2149            "initial change #3",
2150        ] {
2151            editor
2152                .update(cx, |editor, window, cx| {
2153                    editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
2154                    editor.handle_input(change_after_opening, window, cx);
2155                })
2156                .unwrap();
2157            expected_changes.push(change_after_opening);
2158        }
2159
2160        cx.executor().run_until_parked();
2161
2162        editor
2163            .update(cx, |editor, _window, cx| {
2164                let current_text = editor.text(cx);
2165                for change in &expected_changes {
2166                    assert!(
2167                        current_text.contains(change),
2168                        "Should apply all changes made"
2169                    );
2170                }
2171                assert_eq!(
2172                    lsp_request_count.load(Ordering::Relaxed),
2173                    2,
2174                    "Should query new hints twice: for editor init and for the last edit that interrupted all others"
2175                );
2176                let expected_hints = vec!["2".to_string()];
2177                assert_eq!(
2178                    expected_hints,
2179                    cached_hint_labels(editor),
2180                    "Should get hints from the last edit landed only"
2181                );
2182                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2183            })
2184            .unwrap();
2185
2186        let mut edits = Vec::new();
2187        for async_later_change in [
2188            "another change #1",
2189            "another change #2",
2190            "another change #3",
2191        ] {
2192            expected_changes.push(async_later_change);
2193            let task_editor = editor;
2194            edits.push(cx.spawn(|mut cx| async move {
2195                task_editor
2196                    .update(&mut cx, |editor, window, cx| {
2197                        editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
2198                        editor.handle_input(async_later_change, window, cx);
2199                    })
2200                    .unwrap();
2201            }));
2202        }
2203        let _ = future::join_all(edits).await;
2204        cx.executor().run_until_parked();
2205
2206        editor
2207            .update(cx, |editor, _, cx| {
2208                let current_text = editor.text(cx);
2209                for change in &expected_changes {
2210                    assert!(
2211                        current_text.contains(change),
2212                        "Should apply all changes made"
2213                    );
2214                }
2215                assert_eq!(
2216                    lsp_request_count.load(Ordering::SeqCst),
2217                    3,
2218                    "Should query new hints one more time, for the last edit only"
2219                );
2220                let expected_hints = vec!["3".to_string()];
2221                assert_eq!(
2222                    expected_hints,
2223                    cached_hint_labels(editor),
2224                    "Should get hints from the last edit landed only"
2225                );
2226                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2227            })
2228            .unwrap();
2229    }
2230
2231    #[gpui::test(iterations = 10)]
2232    async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
2233        init_test(cx, |settings| {
2234            settings.defaults.inlay_hints = Some(InlayHintSettings {
2235                show_value_hints: true,
2236                enabled: true,
2237                edit_debounce_ms: 0,
2238                scroll_debounce_ms: 0,
2239                show_type_hints: true,
2240                show_parameter_hints: true,
2241                show_other_hints: true,
2242                show_background: false,
2243                toggle_on_modifiers_press: None,
2244            })
2245        });
2246
2247        let fs = FakeFs::new(cx.background_executor.clone());
2248        fs.insert_tree(
2249            path!("/a"),
2250            json!({
2251                "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
2252                "other.rs": "// Test file",
2253            }),
2254        )
2255        .await;
2256
2257        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2258
2259        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2260        language_registry.add(rust_lang());
2261
2262        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2263        let lsp_request_count = Arc::new(AtomicUsize::new(0));
2264        let mut fake_servers = language_registry.register_fake_lsp(
2265            "Rust",
2266            FakeLspAdapter {
2267                capabilities: lsp::ServerCapabilities {
2268                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2269                    ..Default::default()
2270                },
2271                initializer: Some(Box::new({
2272                    let lsp_request_ranges = lsp_request_ranges.clone();
2273                    let lsp_request_count = lsp_request_count.clone();
2274                    move |fake_server| {
2275                        let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
2276                        let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2277                        fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2278                            move |params, _| {
2279                                let task_lsp_request_ranges =
2280                                    Arc::clone(&closure_lsp_request_ranges);
2281                                let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2282                                async move {
2283                                    assert_eq!(
2284                                        params.text_document.uri,
2285                                        lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
2286                                    );
2287
2288                                    task_lsp_request_ranges.lock().push(params.range);
2289                                    task_lsp_request_count.fetch_add(1, Ordering::Release);
2290                                    Ok(Some(vec![lsp::InlayHint {
2291                                        position: params.range.end,
2292                                        label: lsp::InlayHintLabel::String(
2293                                            params.range.end.line.to_string(),
2294                                        ),
2295                                        kind: None,
2296                                        text_edits: None,
2297                                        tooltip: None,
2298                                        padding_left: None,
2299                                        padding_right: None,
2300                                        data: None,
2301                                    }]))
2302                                }
2303                            },
2304                        );
2305                    }
2306                })),
2307                ..Default::default()
2308            },
2309        );
2310
2311        let buffer = project
2312            .update(cx, |project, cx| {
2313                project.open_local_buffer(path!("/a/main.rs"), cx)
2314            })
2315            .await
2316            .unwrap();
2317        let editor =
2318            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
2319
2320        cx.executor().run_until_parked();
2321
2322        let _fake_server = fake_servers.next().await.unwrap();
2323
2324        // in large buffers, requests are made for more than visible range of a buffer.
2325        // invisible parts are queried later, to avoid excessive requests on quick typing.
2326        // wait the timeout needed to get all requests.
2327        cx.executor().advance_clock(Duration::from_millis(
2328            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2329        ));
2330        cx.executor().run_until_parked();
2331        let initial_visible_range = editor_visible_range(&editor, cx);
2332        let lsp_initial_visible_range = lsp::Range::new(
2333            lsp::Position::new(
2334                initial_visible_range.start.row,
2335                initial_visible_range.start.column,
2336            ),
2337            lsp::Position::new(
2338                initial_visible_range.end.row,
2339                initial_visible_range.end.column,
2340            ),
2341        );
2342        let expected_initial_query_range_end =
2343            lsp::Position::new(initial_visible_range.end.row * 2, 2);
2344        let mut expected_invisible_query_start = lsp_initial_visible_range.end;
2345        expected_invisible_query_start.character += 1;
2346        editor.update(cx, |editor, _window, cx| {
2347            let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2348            assert_eq!(ranges.len(), 2,
2349                "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:?}");
2350            let visible_query_range = &ranges[0];
2351            assert_eq!(visible_query_range.start, lsp_initial_visible_range.start);
2352            assert_eq!(visible_query_range.end, lsp_initial_visible_range.end);
2353            let invisible_query_range = &ranges[1];
2354
2355            assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document");
2356            assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document");
2357
2358            let requests_count = lsp_request_count.load(Ordering::Acquire);
2359            assert_eq!(requests_count, 2, "Visible + invisible request");
2360            let expected_hints = vec!["47".to_string(), "94".to_string()];
2361            assert_eq!(
2362                expected_hints,
2363                cached_hint_labels(editor),
2364                "Should have hints from both LSP requests made for a big file"
2365            );
2366            assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
2367        }).unwrap();
2368
2369        editor
2370            .update(cx, |editor, window, cx| {
2371                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2372            })
2373            .unwrap();
2374        cx.executor().run_until_parked();
2375        editor
2376            .update(cx, |editor, window, cx| {
2377                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2378            })
2379            .unwrap();
2380        cx.executor().advance_clock(Duration::from_millis(
2381            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2382        ));
2383        cx.executor().run_until_parked();
2384        let visible_range_after_scrolls = editor_visible_range(&editor, cx);
2385        let visible_line_count = editor
2386            .update(cx, |editor, _window, _| {
2387                editor.visible_line_count().unwrap()
2388            })
2389            .unwrap();
2390        let selection_in_cached_range = editor
2391            .update(cx, |editor, _window, cx| {
2392                let ranges = lsp_request_ranges
2393                    .lock()
2394                    .drain(..)
2395                    .sorted_by_key(|r| r.start)
2396                    .collect::<Vec<_>>();
2397                assert_eq!(
2398                    ranges.len(),
2399                    2,
2400                    "Should query 2 ranges after both scrolls, but got: {ranges:?}"
2401                );
2402                let first_scroll = &ranges[0];
2403                let second_scroll = &ranges[1];
2404                assert_eq!(
2405                    first_scroll.end, second_scroll.start,
2406                    "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
2407                );
2408                assert_eq!(
2409                first_scroll.start, expected_initial_query_range_end,
2410                "First scroll should start the query right after the end of the original scroll",
2411            );
2412                assert_eq!(
2413                second_scroll.end,
2414                lsp::Position::new(
2415                    visible_range_after_scrolls.end.row
2416                        + visible_line_count.ceil() as u32,
2417                    1,
2418                ),
2419                "Second scroll should query one more screen down after the end of the visible range"
2420            );
2421
2422                let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2423                assert_eq!(lsp_requests, 4, "Should query for hints after every scroll");
2424                let expected_hints = vec![
2425                    "47".to_string(),
2426                    "94".to_string(),
2427                    "139".to_string(),
2428                    "184".to_string(),
2429                ];
2430                assert_eq!(
2431                    expected_hints,
2432                    cached_hint_labels(editor),
2433                    "Should have hints from the new LSP response after the edit"
2434                );
2435                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2436
2437                let mut selection_in_cached_range = visible_range_after_scrolls.end;
2438                selection_in_cached_range.row -= visible_line_count.ceil() as u32;
2439                selection_in_cached_range
2440            })
2441            .unwrap();
2442
2443        editor
2444            .update(cx, |editor, window, cx| {
2445                editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
2446                    s.select_ranges([selection_in_cached_range..selection_in_cached_range])
2447                });
2448            })
2449            .unwrap();
2450        cx.executor().advance_clock(Duration::from_millis(
2451            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2452        ));
2453        cx.executor().run_until_parked();
2454        editor.update(cx, |_, _, _| {
2455            let ranges = lsp_request_ranges
2456                .lock()
2457                .drain(..)
2458                .sorted_by_key(|r| r.start)
2459                .collect::<Vec<_>>();
2460            assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
2461            assert_eq!(lsp_request_count.load(Ordering::Acquire), 4);
2462        }).unwrap();
2463
2464        editor
2465            .update(cx, |editor, window, cx| {
2466                editor.handle_input("++++more text++++", window, cx);
2467            })
2468            .unwrap();
2469        cx.executor().advance_clock(Duration::from_millis(
2470            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2471        ));
2472        cx.executor().run_until_parked();
2473        editor.update(cx, |editor, _window, cx| {
2474            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2475            ranges.sort_by_key(|r| r.start);
2476
2477            assert_eq!(ranges.len(), 3,
2478                "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
2479            let above_query_range = &ranges[0];
2480            let visible_query_range = &ranges[1];
2481            let below_query_range = &ranges[2];
2482            assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
2483                "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");
2484            assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line  + 1 == below_query_range.start.line,
2485                "Visible range {visible_query_range:?} should be before below range {below_query_range:?}");
2486            assert!(above_query_range.start.line < selection_in_cached_range.row,
2487                "Hints should be queried with the selected range after the query range start");
2488            assert!(below_query_range.end.line > selection_in_cached_range.row,
2489                "Hints should be queried with the selected range before the query range end");
2490            assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32,
2491                "Hints query range should contain one more screen before");
2492            assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32,
2493                "Hints query range should contain one more screen after");
2494
2495            let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2496            assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried");
2497            let expected_hints = vec!["67".to_string(), "115".to_string(), "163".to_string()];
2498            assert_eq!(expected_hints, cached_hint_labels(editor),
2499                "Should have hints from the new LSP response after the edit");
2500            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2501        }).unwrap();
2502    }
2503
2504    fn editor_visible_range(
2505        editor: &WindowHandle<Editor>,
2506        cx: &mut gpui::TestAppContext,
2507    ) -> Range<Point> {
2508        let ranges = editor
2509            .update(cx, |editor, _window, cx| {
2510                editor.excerpts_for_inlay_hints_query(None, cx)
2511            })
2512            .unwrap();
2513        assert_eq!(
2514            ranges.len(),
2515            1,
2516            "Single buffer should produce a single excerpt with visible range"
2517        );
2518        let (_, (excerpt_buffer, _, excerpt_visible_range)) = ranges.into_iter().next().unwrap();
2519        excerpt_buffer.update(cx, |buffer, _| {
2520            let snapshot = buffer.snapshot();
2521            let start = buffer
2522                .anchor_before(excerpt_visible_range.start)
2523                .to_point(&snapshot);
2524            let end = buffer
2525                .anchor_after(excerpt_visible_range.end)
2526                .to_point(&snapshot);
2527            start..end
2528        })
2529    }
2530
2531    #[gpui::test]
2532    async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
2533        init_test(cx, |settings| {
2534            settings.defaults.inlay_hints = Some(InlayHintSettings {
2535                show_value_hints: true,
2536                enabled: true,
2537                edit_debounce_ms: 0,
2538                scroll_debounce_ms: 0,
2539                show_type_hints: true,
2540                show_parameter_hints: true,
2541                show_other_hints: true,
2542                show_background: false,
2543                toggle_on_modifiers_press: None,
2544            })
2545        });
2546
2547        let fs = FakeFs::new(cx.background_executor.clone());
2548        fs.insert_tree(
2549                path!("/a"),
2550                json!({
2551                    "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2552                    "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2553                }),
2554            )
2555            .await;
2556
2557        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2558
2559        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2560        let language = rust_lang();
2561        language_registry.add(language);
2562        let mut fake_servers = language_registry.register_fake_lsp(
2563            "Rust",
2564            FakeLspAdapter {
2565                capabilities: lsp::ServerCapabilities {
2566                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2567                    ..Default::default()
2568                },
2569                ..Default::default()
2570            },
2571        );
2572
2573        let (buffer_1, _handle1) = project
2574            .update(cx, |project, cx| {
2575                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2576            })
2577            .await
2578            .unwrap();
2579        let (buffer_2, _handle2) = project
2580            .update(cx, |project, cx| {
2581                project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2582            })
2583            .await
2584            .unwrap();
2585        let multibuffer = cx.new(|cx| {
2586            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2587            multibuffer.push_excerpts(
2588                buffer_1.clone(),
2589                [
2590                    ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
2591                    ExcerptRange::new(Point::new(4, 0)..Point::new(11, 0)),
2592                    ExcerptRange::new(Point::new(22, 0)..Point::new(33, 0)),
2593                    ExcerptRange::new(Point::new(44, 0)..Point::new(55, 0)),
2594                    ExcerptRange::new(Point::new(56, 0)..Point::new(66, 0)),
2595                    ExcerptRange::new(Point::new(67, 0)..Point::new(77, 0)),
2596                ],
2597                cx,
2598            );
2599            multibuffer.push_excerpts(
2600                buffer_2.clone(),
2601                [
2602                    ExcerptRange::new(Point::new(0, 1)..Point::new(2, 1)),
2603                    ExcerptRange::new(Point::new(4, 1)..Point::new(11, 1)),
2604                    ExcerptRange::new(Point::new(22, 1)..Point::new(33, 1)),
2605                    ExcerptRange::new(Point::new(44, 1)..Point::new(55, 1)),
2606                    ExcerptRange::new(Point::new(56, 1)..Point::new(66, 1)),
2607                    ExcerptRange::new(Point::new(67, 1)..Point::new(77, 1)),
2608                ],
2609                cx,
2610            );
2611            multibuffer
2612        });
2613
2614        cx.executor().run_until_parked();
2615        let editor = cx.add_window(|window, cx| {
2616            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2617        });
2618
2619        let editor_edited = Arc::new(AtomicBool::new(false));
2620        let fake_server = fake_servers.next().await.unwrap();
2621        let closure_editor_edited = Arc::clone(&editor_edited);
2622        fake_server
2623            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2624                let task_editor_edited = Arc::clone(&closure_editor_edited);
2625                async move {
2626                    let hint_text = if params.text_document.uri
2627                        == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
2628                    {
2629                        "main hint"
2630                    } else if params.text_document.uri
2631                        == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
2632                    {
2633                        "other hint"
2634                    } else {
2635                        panic!("unexpected uri: {:?}", params.text_document.uri);
2636                    };
2637
2638                    // one hint per excerpt
2639                    let positions = [
2640                        lsp::Position::new(0, 2),
2641                        lsp::Position::new(4, 2),
2642                        lsp::Position::new(22, 2),
2643                        lsp::Position::new(44, 2),
2644                        lsp::Position::new(56, 2),
2645                        lsp::Position::new(67, 2),
2646                    ];
2647                    let out_of_range_hint = lsp::InlayHint {
2648                        position: lsp::Position::new(
2649                            params.range.start.line + 99,
2650                            params.range.start.character + 99,
2651                        ),
2652                        label: lsp::InlayHintLabel::String(
2653                            "out of excerpt range, should be ignored".to_string(),
2654                        ),
2655                        kind: None,
2656                        text_edits: None,
2657                        tooltip: None,
2658                        padding_left: None,
2659                        padding_right: None,
2660                        data: None,
2661                    };
2662
2663                    let edited = task_editor_edited.load(Ordering::Acquire);
2664                    Ok(Some(
2665                        std::iter::once(out_of_range_hint)
2666                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
2667                                lsp::InlayHint {
2668                                    position,
2669                                    label: lsp::InlayHintLabel::String(format!(
2670                                        "{hint_text}{E} #{i}",
2671                                        E = if edited { "(edited)" } else { "" },
2672                                    )),
2673                                    kind: None,
2674                                    text_edits: None,
2675                                    tooltip: None,
2676                                    padding_left: None,
2677                                    padding_right: None,
2678                                    data: None,
2679                                }
2680                            }))
2681                            .collect(),
2682                    ))
2683                }
2684            })
2685            .next()
2686            .await;
2687        cx.executor().run_until_parked();
2688
2689        editor
2690            .update(cx, |editor, _window, cx| {
2691                let expected_hints = vec![
2692                    "main hint #0".to_string(),
2693                    "main hint #1".to_string(),
2694                    "main hint #2".to_string(),
2695                    "main hint #3".to_string(),
2696                    "main hint #4".to_string(),
2697                    "main hint #5".to_string(),
2698                ];
2699                assert_eq!(
2700                    expected_hints,
2701                    sorted_cached_hint_labels(editor),
2702                    "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2703                );
2704                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2705            })
2706            .unwrap();
2707
2708        editor
2709            .update(cx, |editor, window, cx| {
2710                editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2711                    s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2712                });
2713                editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2714                    s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2715                });
2716                editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2717                    s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
2718                });
2719            })
2720            .unwrap();
2721        cx.executor().run_until_parked();
2722        editor
2723            .update(cx, |editor, _window, cx| {
2724                let expected_hints = vec![
2725                    "main hint #0".to_string(),
2726                    "main hint #1".to_string(),
2727                    "main hint #2".to_string(),
2728                    "main hint #3".to_string(),
2729                    "main hint #4".to_string(),
2730                    "main hint #5".to_string(),
2731                    "other hint #0".to_string(),
2732                    "other hint #1".to_string(),
2733                    "other hint #2".to_string(),
2734                ];
2735                assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2736                    "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2737                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2738            })
2739            .unwrap();
2740
2741        editor
2742            .update(cx, |editor, window, cx| {
2743                editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2744                    s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2745                });
2746            })
2747            .unwrap();
2748        cx.executor().advance_clock(Duration::from_millis(
2749            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2750        ));
2751        cx.executor().run_until_parked();
2752        editor
2753            .update(cx, |editor, _window, cx| {
2754                let expected_hints = vec![
2755                    "main hint #0".to_string(),
2756                    "main hint #1".to_string(),
2757                    "main hint #2".to_string(),
2758                    "main hint #3".to_string(),
2759                    "main hint #4".to_string(),
2760                    "main hint #5".to_string(),
2761                    "other hint #0".to_string(),
2762                    "other hint #1".to_string(),
2763                    "other hint #2".to_string(),
2764                    "other hint #3".to_string(),
2765                    "other hint #4".to_string(),
2766                    "other hint #5".to_string(),
2767                ];
2768                assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2769                    "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2770                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2771            })
2772            .unwrap();
2773
2774        editor
2775            .update(cx, |editor, window, cx| {
2776                editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2777                    s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2778                });
2779            })
2780            .unwrap();
2781        cx.executor().advance_clock(Duration::from_millis(
2782            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2783        ));
2784        cx.executor().run_until_parked();
2785        editor
2786            .update(cx, |editor, _window, cx| {
2787                let expected_hints = vec![
2788                    "main hint #0".to_string(),
2789                    "main hint #1".to_string(),
2790                    "main hint #2".to_string(),
2791                    "main hint #3".to_string(),
2792                    "main hint #4".to_string(),
2793                    "main hint #5".to_string(),
2794                    "other hint #0".to_string(),
2795                    "other hint #1".to_string(),
2796                    "other hint #2".to_string(),
2797                    "other hint #3".to_string(),
2798                    "other hint #4".to_string(),
2799                    "other hint #5".to_string(),
2800                ];
2801                assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2802                    "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2803                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2804            })
2805            .unwrap();
2806
2807        editor_edited.store(true, Ordering::Release);
2808        editor
2809            .update(cx, |editor, window, cx| {
2810                editor.change_selections(None, window, cx, |s| {
2811                    s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
2812                });
2813                editor.handle_input("++++more text++++", window, cx);
2814            })
2815            .unwrap();
2816        cx.executor().run_until_parked();
2817        editor
2818            .update(cx, |editor, _window, cx| {
2819                let expected_hints = vec![
2820                    "main hint #0".to_string(),
2821                    "main hint #1".to_string(),
2822                    "main hint #2".to_string(),
2823                    "main hint #3".to_string(),
2824                    "main hint #4".to_string(),
2825                    "main hint #5".to_string(),
2826                    "other hint(edited) #0".to_string(),
2827                    "other hint(edited) #1".to_string(),
2828                ];
2829                assert_eq!(
2830                    expected_hints,
2831                    sorted_cached_hint_labels(editor),
2832                    "After multibuffer edit, editor gets scrolled back to the last selection; \
2833                all hints should be invalidated and required for all of its visible excerpts"
2834                );
2835                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2836            })
2837            .unwrap();
2838    }
2839
2840    #[gpui::test]
2841    async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
2842        init_test(cx, |settings| {
2843            settings.defaults.inlay_hints = Some(InlayHintSettings {
2844                show_value_hints: true,
2845                enabled: true,
2846                edit_debounce_ms: 0,
2847                scroll_debounce_ms: 0,
2848                show_type_hints: false,
2849                show_parameter_hints: false,
2850                show_other_hints: false,
2851                show_background: false,
2852                toggle_on_modifiers_press: None,
2853            })
2854        });
2855
2856        let fs = FakeFs::new(cx.background_executor.clone());
2857        fs.insert_tree(
2858            path!("/a"),
2859            json!({
2860                "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2861                "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2862            }),
2863        )
2864        .await;
2865
2866        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2867
2868        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2869        language_registry.add(rust_lang());
2870        let mut fake_servers = language_registry.register_fake_lsp(
2871            "Rust",
2872            FakeLspAdapter {
2873                capabilities: lsp::ServerCapabilities {
2874                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2875                    ..Default::default()
2876                },
2877                ..Default::default()
2878            },
2879        );
2880
2881        let (buffer_1, _handle) = project
2882            .update(cx, |project, cx| {
2883                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2884            })
2885            .await
2886            .unwrap();
2887        let (buffer_2, _handle2) = project
2888            .update(cx, |project, cx| {
2889                project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2890            })
2891            .await
2892            .unwrap();
2893        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2894        let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2895            let buffer_1_excerpts = multibuffer.push_excerpts(
2896                buffer_1.clone(),
2897                [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
2898                cx,
2899            );
2900            let buffer_2_excerpts = multibuffer.push_excerpts(
2901                buffer_2.clone(),
2902                [ExcerptRange::new(Point::new(0, 1)..Point::new(2, 1))],
2903                cx,
2904            );
2905            (buffer_1_excerpts, buffer_2_excerpts)
2906        });
2907
2908        assert!(!buffer_1_excerpts.is_empty());
2909        assert!(!buffer_2_excerpts.is_empty());
2910
2911        cx.executor().run_until_parked();
2912        let editor = cx.add_window(|window, cx| {
2913            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2914        });
2915        let editor_edited = Arc::new(AtomicBool::new(false));
2916        let fake_server = fake_servers.next().await.unwrap();
2917        let closure_editor_edited = Arc::clone(&editor_edited);
2918        fake_server
2919            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2920                let task_editor_edited = Arc::clone(&closure_editor_edited);
2921                async move {
2922                    let hint_text = if params.text_document.uri
2923                        == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
2924                    {
2925                        "main hint"
2926                    } else if params.text_document.uri
2927                        == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
2928                    {
2929                        "other hint"
2930                    } else {
2931                        panic!("unexpected uri: {:?}", params.text_document.uri);
2932                    };
2933
2934                    let positions = [
2935                        lsp::Position::new(0, 2),
2936                        lsp::Position::new(4, 2),
2937                        lsp::Position::new(22, 2),
2938                        lsp::Position::new(44, 2),
2939                        lsp::Position::new(56, 2),
2940                        lsp::Position::new(67, 2),
2941                    ];
2942                    let out_of_range_hint = lsp::InlayHint {
2943                        position: lsp::Position::new(
2944                            params.range.start.line + 99,
2945                            params.range.start.character + 99,
2946                        ),
2947                        label: lsp::InlayHintLabel::String(
2948                            "out of excerpt range, should be ignored".to_string(),
2949                        ),
2950                        kind: None,
2951                        text_edits: None,
2952                        tooltip: None,
2953                        padding_left: None,
2954                        padding_right: None,
2955                        data: None,
2956                    };
2957
2958                    let edited = task_editor_edited.load(Ordering::Acquire);
2959                    Ok(Some(
2960                        std::iter::once(out_of_range_hint)
2961                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
2962                                lsp::InlayHint {
2963                                    position,
2964                                    label: lsp::InlayHintLabel::String(format!(
2965                                        "{hint_text}{} #{i}",
2966                                        if edited { "(edited)" } else { "" },
2967                                    )),
2968                                    kind: None,
2969                                    text_edits: None,
2970                                    tooltip: None,
2971                                    padding_left: None,
2972                                    padding_right: None,
2973                                    data: None,
2974                                }
2975                            }))
2976                            .collect(),
2977                    ))
2978                }
2979            })
2980            .next()
2981            .await;
2982        cx.executor().run_until_parked();
2983        editor
2984            .update(cx, |editor, _, cx| {
2985                assert_eq!(
2986                    vec!["main hint #0".to_string(), "other hint #0".to_string()],
2987                    sorted_cached_hint_labels(editor),
2988                    "Cache should update for both excerpts despite hints display was disabled"
2989                );
2990                assert!(
2991                visible_hint_labels(editor, cx).is_empty(),
2992                "All hints are disabled and should not be shown despite being present in the cache"
2993            );
2994            })
2995            .unwrap();
2996
2997        editor
2998            .update(cx, |editor, _, cx| {
2999                editor.buffer().update(cx, |multibuffer, cx| {
3000                    multibuffer.remove_excerpts(buffer_2_excerpts, cx)
3001                })
3002            })
3003            .unwrap();
3004        cx.executor().run_until_parked();
3005        editor
3006            .update(cx, |editor, _, cx| {
3007                assert_eq!(
3008                    vec!["main hint #0".to_string()],
3009                    cached_hint_labels(editor),
3010                    "For the removed excerpt, should clean corresponding cached hints"
3011                );
3012                assert!(
3013                visible_hint_labels(editor, cx).is_empty(),
3014                "All hints are disabled and should not be shown despite being present in the cache"
3015            );
3016            })
3017            .unwrap();
3018
3019        update_test_language_settings(cx, |settings| {
3020            settings.defaults.inlay_hints = Some(InlayHintSettings {
3021                show_value_hints: true,
3022                enabled: true,
3023                edit_debounce_ms: 0,
3024                scroll_debounce_ms: 0,
3025                show_type_hints: true,
3026                show_parameter_hints: true,
3027                show_other_hints: true,
3028                show_background: false,
3029                toggle_on_modifiers_press: None,
3030            })
3031        });
3032        cx.executor().run_until_parked();
3033        editor
3034            .update(cx, |editor, _, cx| {
3035                let expected_hints = vec!["main hint #0".to_string()];
3036                assert_eq!(
3037                    expected_hints,
3038                    cached_hint_labels(editor),
3039                    "Hint display settings change should not change the cache"
3040                );
3041                assert_eq!(
3042                    expected_hints,
3043                    visible_hint_labels(editor, cx),
3044                    "Settings change should make cached hints visible"
3045                );
3046            })
3047            .unwrap();
3048    }
3049
3050    #[gpui::test]
3051    async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
3052        init_test(cx, |settings| {
3053            settings.defaults.inlay_hints = Some(InlayHintSettings {
3054                show_value_hints: true,
3055                enabled: true,
3056                edit_debounce_ms: 0,
3057                scroll_debounce_ms: 0,
3058                show_type_hints: true,
3059                show_parameter_hints: true,
3060                show_other_hints: true,
3061                show_background: false,
3062                toggle_on_modifiers_press: None,
3063            })
3064        });
3065
3066        let fs = FakeFs::new(cx.background_executor.clone());
3067        fs.insert_tree(
3068            path!("/a"),
3069            json!({
3070                "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "".repeat(10)).repeat(500)),
3071                "other.rs": "// Test file",
3072            }),
3073        )
3074        .await;
3075
3076        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3077
3078        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3079        language_registry.add(rust_lang());
3080        language_registry.register_fake_lsp(
3081            "Rust",
3082            FakeLspAdapter {
3083                capabilities: lsp::ServerCapabilities {
3084                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3085                    ..Default::default()
3086                },
3087                initializer: Some(Box::new(move |fake_server| {
3088                    let lsp_request_count = Arc::new(AtomicU32::new(0));
3089                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3090                        move |params, _| {
3091                            let i = lsp_request_count.fetch_add(1, Ordering::Release) + 1;
3092                            async move {
3093                                assert_eq!(
3094                                    params.text_document.uri,
3095                                    lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3096                                );
3097                                let query_start = params.range.start;
3098                                Ok(Some(vec![lsp::InlayHint {
3099                                    position: query_start,
3100                                    label: lsp::InlayHintLabel::String(i.to_string()),
3101                                    kind: None,
3102                                    text_edits: None,
3103                                    tooltip: None,
3104                                    padding_left: None,
3105                                    padding_right: None,
3106                                    data: None,
3107                                }]))
3108                            }
3109                        },
3110                    );
3111                })),
3112                ..Default::default()
3113            },
3114        );
3115
3116        let buffer = project
3117            .update(cx, |project, cx| {
3118                project.open_local_buffer(path!("/a/main.rs"), cx)
3119            })
3120            .await
3121            .unwrap();
3122        let editor =
3123            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3124
3125        cx.executor().run_until_parked();
3126        editor
3127            .update(cx, |editor, window, cx| {
3128                editor.change_selections(None, window, cx, |s| {
3129                    s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3130                })
3131            })
3132            .unwrap();
3133        cx.executor().run_until_parked();
3134        editor
3135            .update(cx, |editor, _, cx| {
3136                let expected_hints = vec!["1".to_string()];
3137                assert_eq!(expected_hints, cached_hint_labels(editor));
3138                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3139            })
3140            .unwrap();
3141    }
3142
3143    #[gpui::test]
3144    async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
3145        init_test(cx, |settings| {
3146            settings.defaults.inlay_hints = Some(InlayHintSettings {
3147                show_value_hints: true,
3148                enabled: false,
3149                edit_debounce_ms: 0,
3150                scroll_debounce_ms: 0,
3151                show_type_hints: true,
3152                show_parameter_hints: true,
3153                show_other_hints: true,
3154                show_background: false,
3155                toggle_on_modifiers_press: None,
3156            })
3157        });
3158
3159        let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3160            let lsp_request_count = Arc::new(AtomicU32::new(0));
3161            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3162                move |params, _| {
3163                    let lsp_request_count = lsp_request_count.clone();
3164                    async move {
3165                        assert_eq!(
3166                            params.text_document.uri,
3167                            lsp::Url::from_file_path(file_with_hints).unwrap(),
3168                        );
3169
3170                        let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
3171                        Ok(Some(vec![lsp::InlayHint {
3172                            position: lsp::Position::new(0, i),
3173                            label: lsp::InlayHintLabel::String(i.to_string()),
3174                            kind: None,
3175                            text_edits: None,
3176                            tooltip: None,
3177                            padding_left: None,
3178                            padding_right: None,
3179                            data: None,
3180                        }]))
3181                    }
3182                },
3183            );
3184        })
3185        .await;
3186
3187        editor
3188            .update(cx, |editor, window, cx| {
3189                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3190            })
3191            .unwrap();
3192
3193        cx.executor().run_until_parked();
3194        editor
3195            .update(cx, |editor, _, cx| {
3196                let expected_hints = vec!["1".to_string()];
3197                assert_eq!(
3198                    expected_hints,
3199                    cached_hint_labels(editor),
3200                    "Should display inlays after toggle despite them disabled in settings"
3201                );
3202                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3203            })
3204            .unwrap();
3205
3206        editor
3207            .update(cx, |editor, window, cx| {
3208                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3209            })
3210            .unwrap();
3211        cx.executor().run_until_parked();
3212        editor
3213            .update(cx, |editor, _, cx| {
3214                assert!(
3215                    cached_hint_labels(editor).is_empty(),
3216                    "Should clear hints after 2nd toggle"
3217                );
3218                assert!(visible_hint_labels(editor, cx).is_empty());
3219            })
3220            .unwrap();
3221
3222        update_test_language_settings(cx, |settings| {
3223            settings.defaults.inlay_hints = Some(InlayHintSettings {
3224                show_value_hints: true,
3225                enabled: true,
3226                edit_debounce_ms: 0,
3227                scroll_debounce_ms: 0,
3228                show_type_hints: true,
3229                show_parameter_hints: true,
3230                show_other_hints: true,
3231                show_background: false,
3232                toggle_on_modifiers_press: None,
3233            })
3234        });
3235        cx.executor().run_until_parked();
3236        editor
3237            .update(cx, |editor, _, cx| {
3238                let expected_hints = vec!["2".to_string()];
3239                assert_eq!(
3240                    expected_hints,
3241                    cached_hint_labels(editor),
3242                    "Should query LSP hints for the 2nd time after enabling hints in settings"
3243                );
3244                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3245            })
3246            .unwrap();
3247
3248        editor
3249            .update(cx, |editor, window, cx| {
3250                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3251            })
3252            .unwrap();
3253        cx.executor().run_until_parked();
3254        editor
3255            .update(cx, |editor, _, cx| {
3256                assert!(
3257                    cached_hint_labels(editor).is_empty(),
3258                    "Should clear hints after enabling in settings and a 3rd toggle"
3259                );
3260                assert!(visible_hint_labels(editor, cx).is_empty());
3261            })
3262            .unwrap();
3263
3264        editor
3265            .update(cx, |editor, window, cx| {
3266                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3267            })
3268            .unwrap();
3269        cx.executor().run_until_parked();
3270        editor.update(cx, |editor, _, cx| {
3271            let expected_hints = vec!["3".to_string()];
3272            assert_eq!(
3273                expected_hints,
3274                cached_hint_labels(editor),
3275                "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
3276            );
3277            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3278        }).unwrap();
3279    }
3280
3281    #[gpui::test]
3282    async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) {
3283        init_test(cx, |settings| {
3284            settings.defaults.inlay_hints = Some(InlayHintSettings {
3285                show_value_hints: true,
3286                enabled: true,
3287                edit_debounce_ms: 0,
3288                scroll_debounce_ms: 0,
3289                show_type_hints: true,
3290                show_parameter_hints: true,
3291                show_other_hints: true,
3292                show_background: false,
3293                toggle_on_modifiers_press: None,
3294            })
3295        });
3296
3297        let fs = FakeFs::new(cx.background_executor.clone());
3298        fs.insert_tree(
3299            path!("/a"),
3300            json!({
3301                "main.rs": "fn main() {
3302                    let x = 42;
3303                    std::thread::scope(|s| {
3304                        s.spawn(|| {
3305                            let _x = x;
3306                        });
3307                    });
3308                }",
3309                "other.rs": "// Test file",
3310            }),
3311        )
3312        .await;
3313
3314        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3315
3316        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3317        language_registry.add(rust_lang());
3318        language_registry.register_fake_lsp(
3319            "Rust",
3320            FakeLspAdapter {
3321                capabilities: lsp::ServerCapabilities {
3322                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3323                    ..Default::default()
3324                },
3325                initializer: Some(Box::new(move |fake_server| {
3326                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3327                        move |params, _| async move {
3328                            assert_eq!(
3329                                params.text_document.uri,
3330                                lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3331                            );
3332                            Ok(Some(
3333                                serde_json::from_value(json!([
3334                                    {
3335                                        "position": {
3336                                            "line": 3,
3337                                            "character": 16
3338                                        },
3339                                        "label": "move",
3340                                        "paddingLeft": false,
3341                                        "paddingRight": false
3342                                    },
3343                                    {
3344                                        "position": {
3345                                            "line": 3,
3346                                            "character": 16
3347                                        },
3348                                        "label": "(",
3349                                        "paddingLeft": false,
3350                                        "paddingRight": false
3351                                    },
3352                                    {
3353                                        "position": {
3354                                            "line": 3,
3355                                            "character": 16
3356                                        },
3357                                        "label": [
3358                                            {
3359                                                "value": "&x"
3360                                            }
3361                                        ],
3362                                        "paddingLeft": false,
3363                                        "paddingRight": false,
3364                                        "data": {
3365                                            "file_id": 0
3366                                        }
3367                                    },
3368                                    {
3369                                        "position": {
3370                                            "line": 3,
3371                                            "character": 16
3372                                        },
3373                                        "label": ")",
3374                                        "paddingLeft": false,
3375                                        "paddingRight": true
3376                                    },
3377                                    // not a correct syntax, but checks that same symbols at the same place
3378                                    // are not deduplicated
3379                                    {
3380                                        "position": {
3381                                            "line": 3,
3382                                            "character": 16
3383                                        },
3384                                        "label": ")",
3385                                        "paddingLeft": false,
3386                                        "paddingRight": true
3387                                    },
3388                                ]))
3389                                .unwrap(),
3390                            ))
3391                        },
3392                    );
3393                })),
3394                ..FakeLspAdapter::default()
3395            },
3396        );
3397
3398        let buffer = project
3399            .update(cx, |project, cx| {
3400                project.open_local_buffer(path!("/a/main.rs"), cx)
3401            })
3402            .await
3403            .unwrap();
3404        let editor =
3405            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3406
3407        cx.executor().run_until_parked();
3408        editor
3409            .update(cx, |editor, window, cx| {
3410                editor.change_selections(None, window, cx, |s| {
3411                    s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3412                })
3413            })
3414            .unwrap();
3415        cx.executor().run_until_parked();
3416        editor
3417            .update(cx, |editor, _window, cx| {
3418                let expected_hints = vec![
3419                    "move".to_string(),
3420                    "(".to_string(),
3421                    "&x".to_string(),
3422                    ") ".to_string(),
3423                    ") ".to_string(),
3424                ];
3425                assert_eq!(
3426                    expected_hints,
3427                    cached_hint_labels(editor),
3428                    "Editor inlay hints should repeat server's order when placed at the same spot"
3429                );
3430                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3431            })
3432            .unwrap();
3433    }
3434
3435    pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
3436        cx.update(|cx| {
3437            let settings_store = SettingsStore::test(cx);
3438            cx.set_global(settings_store);
3439            theme::init(theme::LoadThemes::JustBase, cx);
3440            release_channel::init(SemanticVersion::default(), cx);
3441            client::init_settings(cx);
3442            language::init(cx);
3443            Project::init_settings(cx);
3444            workspace::init_settings(cx);
3445            crate::init(cx);
3446        });
3447
3448        update_test_language_settings(cx, f);
3449    }
3450
3451    async fn prepare_test_objects(
3452        cx: &mut TestAppContext,
3453        initialize: impl 'static + Send + Fn(&mut FakeLanguageServer, &'static str) + Send + Sync,
3454    ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
3455        let fs = FakeFs::new(cx.background_executor.clone());
3456        fs.insert_tree(
3457            path!("/a"),
3458            json!({
3459                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
3460                "other.rs": "// Test file",
3461            }),
3462        )
3463        .await;
3464
3465        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3466        let file_path = path!("/a/main.rs");
3467
3468        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3469        language_registry.add(rust_lang());
3470        let mut fake_servers = language_registry.register_fake_lsp(
3471            "Rust",
3472            FakeLspAdapter {
3473                capabilities: lsp::ServerCapabilities {
3474                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3475                    ..Default::default()
3476                },
3477                initializer: Some(Box::new(move |server| initialize(server, file_path))),
3478                ..Default::default()
3479            },
3480        );
3481
3482        let buffer = project
3483            .update(cx, |project, cx| {
3484                project.open_local_buffer(path!("/a/main.rs"), cx)
3485            })
3486            .await
3487            .unwrap();
3488        let editor =
3489            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3490
3491        editor
3492            .update(cx, |editor, _, cx| {
3493                assert!(cached_hint_labels(editor).is_empty());
3494                assert!(visible_hint_labels(editor, cx).is_empty());
3495            })
3496            .unwrap();
3497
3498        cx.executor().run_until_parked();
3499        let fake_server = fake_servers.next().await.unwrap();
3500        (file_path, editor, fake_server)
3501    }
3502
3503    // 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.
3504    // Ensure a stable order for testing.
3505    fn sorted_cached_hint_labels(editor: &Editor) -> Vec<String> {
3506        let mut labels = cached_hint_labels(editor);
3507        labels.sort();
3508        labels
3509    }
3510
3511    pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
3512        let mut labels = Vec::new();
3513        for excerpt_hints in editor.inlay_hint_cache().hints.values() {
3514            let excerpt_hints = excerpt_hints.read();
3515            for id in &excerpt_hints.ordered_hints {
3516                let hint = &excerpt_hints.hints_by_id[id];
3517                let mut label = hint.text();
3518                if hint.padding_left {
3519                    label.insert(0, ' ');
3520                }
3521                if hint.padding_right {
3522                    label.push_str(" ");
3523                }
3524                labels.push(label);
3525            }
3526        }
3527
3528        labels
3529    }
3530
3531    pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
3532        editor
3533            .visible_inlay_hints(cx)
3534            .into_iter()
3535            .map(|hint| hint.text.to_string())
3536            .collect()
3537    }
3538}