inlay_hint_cache.rs

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