Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:40:01.511Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

inlay_hints.rs

5070 lines · 215.7 KB · rust
1use std::{
2    collections::hash_map,
3    ops::{ControlFlow, Range},
4    time::Duration,
5};
6
7use clock::Global;
8use collections::{HashMap, HashSet};
9use futures::future::join_all;
10use gpui::{App, Entity, Pixels, Task};
11use itertools::Itertools;
12use language::{
13    BufferRow,
14    language_settings::{InlayHintKind, InlayHintSettings},
15};
16use lsp::LanguageServerId;
17use multi_buffer::{Anchor, MultiBufferSnapshot};
18use project::{
19    HoverBlock, HoverBlockKind, InlayHintLabel, InlayHintLabelPartTooltip, InlayHintTooltip,
20    InvalidationStrategy, ResolveState,
21    lsp_store::{CacheInlayHints, ResolvedHint},
22};
23use text::{Bias, BufferId};
24use ui::{Context, Window};
25use util::debug_panic;
26
27use super::{Inlay, InlayId};
28use crate::{
29    Editor, EditorSnapshot, PointForPosition, ToggleInlayHints, ToggleInlineValues, debounce_value,
30    display_map::{DisplayMap, InlayOffset},
31    hover_links::{InlayHighlight, TriggerPoint, show_link_definition},
32    hover_popover::{self, InlayHover},
33    inlays::InlaySplice,
34};
35
36pub fn inlay_hint_settings(
37    location: Anchor,
38    snapshot: &MultiBufferSnapshot,
39    cx: &mut Context<Editor>,
40) -> InlayHintSettings {
41    snapshot.language_settings_at(location, cx).inlay_hints
42}
43
44#[derive(Debug)]
45pub struct LspInlayHintData {
46    enabled: bool,
47    modifiers_override: bool,
48    enabled_in_settings: bool,
49    allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
50    invalidate_debounce: Option<Duration>,
51    append_debounce: Option<Duration>,
52    hint_refresh_tasks: HashMap<BufferId, Vec<Task<()>>>,
53    hint_chunk_fetching: HashMap<BufferId, (Global, HashSet<Range<BufferRow>>)>,
54    invalidate_hints_for_buffers: HashSet<BufferId>,
55    pub added_hints: HashMap<InlayId, Option<InlayHintKind>>,
56}
57
58impl LspInlayHintData {
59    pub fn new(settings: InlayHintSettings) -> Self {
60        Self {
61            modifiers_override: false,
62            enabled: settings.enabled,
63            enabled_in_settings: settings.enabled,
64            hint_refresh_tasks: HashMap::default(),
65            added_hints: HashMap::default(),
66            hint_chunk_fetching: HashMap::default(),
67            invalidate_hints_for_buffers: HashSet::default(),
68            invalidate_debounce: debounce_value(settings.edit_debounce_ms),
69            append_debounce: debounce_value(settings.scroll_debounce_ms),
70            allowed_hint_kinds: settings.enabled_inlay_hint_kinds(),
71        }
72    }
73
74    pub fn modifiers_override(&mut self, new_override: bool) -> Option<bool> {
75        if self.modifiers_override == new_override {
76            return None;
77        }
78        self.modifiers_override = new_override;
79        if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
80        {
81            self.clear();
82            Some(false)
83        } else {
84            Some(true)
85        }
86    }
87
88    pub fn toggle(&mut self, enabled: bool) -> bool {
89        if self.enabled == enabled {
90            return false;
91        }
92        self.enabled = enabled;
93        self.modifiers_override = false;
94        if !enabled {
95            self.clear();
96        }
97        true
98    }
99
100    pub fn clear(&mut self) {
101        self.hint_refresh_tasks.clear();
102        self.hint_chunk_fetching.clear();
103        self.added_hints.clear();
104    }
105
106    /// Like `clear`, but only wipes tracking state for the given buffer IDs.
107    /// Hints belonging to other buffers are left intact so they are neither
108    /// re-fetched nor duplicated on the next `NewLinesShown`.
109    pub fn clear_for_buffers(
110        &mut self,
111        buffer_ids: &HashSet<BufferId>,
112        current_hints: impl IntoIterator<Item = Inlay>,
113        snapshot: &MultiBufferSnapshot,
114    ) {
115        for buffer_id in buffer_ids {
116            self.hint_refresh_tasks.remove(buffer_id);
117            self.hint_chunk_fetching.remove(buffer_id);
118        }
119        for hint in current_hints {
120            if let Some((text_anchor, _)) = snapshot.anchor_to_buffer_anchor(hint.position) {
121                if buffer_ids.contains(&text_anchor.buffer_id) {
122                    self.added_hints.remove(&hint.id);
123                }
124            }
125        }
126    }
127
128    /// Checks inlay hint settings for enabled hint kinds and general enabled state.
129    /// Generates corresponding inlay_map splice updates on settings changes.
130    /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries.
131    fn update_settings(
132        &mut self,
133        new_hint_settings: InlayHintSettings,
134        visible_hints: impl IntoIterator<Item = Inlay>,
135    ) -> ControlFlow<Option<InlaySplice>, Option<InlaySplice>> {
136        let old_enabled = self.enabled;
137        // If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay
138        // hint visibility changes when other settings change (such as theme).
139        //
140        // Another option might be to store whether the user has manually toggled inlay hint
141        // visibility, and prefer this. This could lead to confusion as it means inlay hint
142        // visibility would not change when updating the setting if they were ever toggled.
143        if new_hint_settings.enabled != self.enabled_in_settings {
144            self.enabled = new_hint_settings.enabled;
145            self.enabled_in_settings = new_hint_settings.enabled;
146            self.modifiers_override = false;
147        };
148        self.invalidate_debounce = debounce_value(new_hint_settings.edit_debounce_ms);
149        self.append_debounce = debounce_value(new_hint_settings.scroll_debounce_ms);
150        let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
151        match (old_enabled, self.enabled) {
152            (false, false) => {
153                self.allowed_hint_kinds = new_allowed_hint_kinds;
154                ControlFlow::Break(None)
155            }
156            (true, true) => {
157                if new_allowed_hint_kinds == self.allowed_hint_kinds {
158                    ControlFlow::Break(None)
159                } else {
160                    self.allowed_hint_kinds = new_allowed_hint_kinds;
161                    ControlFlow::Continue(
162                        Some(InlaySplice {
163                            to_remove: visible_hints
164                                .into_iter()
165                                .filter_map(|inlay| {
166                                    let inlay_kind = self.added_hints.get(&inlay.id).copied()?;
167                                    if !self.allowed_hint_kinds.contains(&inlay_kind) {
168                                        Some(inlay.id)
169                                    } else {
170                                        None
171                                    }
172                                })
173                                .collect(),
174                            to_insert: Vec::new(),
175                        })
176                        .filter(|splice| !splice.is_empty()),
177                    )
178                }
179            }
180            (true, false) => {
181                self.modifiers_override = false;
182                self.allowed_hint_kinds = new_allowed_hint_kinds;
183                let mut visible_hints = visible_hints.into_iter().peekable();
184                if visible_hints.peek().is_none() {
185                    ControlFlow::Break(None)
186                } else {
187                    self.clear();
188                    ControlFlow::Break(Some(InlaySplice {
189                        to_remove: visible_hints.map(|inlay| inlay.id).collect(),
190                        to_insert: Vec::new(),
191                    }))
192                }
193            }
194            (false, true) => {
195                self.modifiers_override = false;
196                self.allowed_hint_kinds = new_allowed_hint_kinds;
197                ControlFlow::Continue(
198                    Some(InlaySplice {
199                        to_remove: visible_hints
200                            .into_iter()
201                            .filter_map(|inlay| {
202                                let inlay_kind = self.added_hints.get(&inlay.id).copied()?;
203                                if !self.allowed_hint_kinds.contains(&inlay_kind) {
204                                    Some(inlay.id)
205                                } else {
206                                    None
207                                }
208                            })
209                            .collect(),
210                        to_insert: Vec::new(),
211                    })
212                    .filter(|splice| !splice.is_empty()),
213                )
214            }
215        }
216    }
217
218    pub(crate) fn remove_inlay_chunk_data<'a>(
219        &'a mut self,
220        removed_buffer_ids: impl IntoIterator<Item = &'a BufferId> + 'a,
221    ) {
222        for buffer_id in removed_buffer_ids {
223            self.hint_refresh_tasks.remove(buffer_id);
224            self.hint_chunk_fetching.remove(buffer_id);
225        }
226    }
227}
228
229#[derive(Debug, Clone)]
230pub enum InlayHintRefreshReason {
231    ModifiersChanged(bool),
232    Toggle(bool),
233    SettingsChange(InlayHintSettings),
234    NewLinesShown,
235    BufferEdited(BufferId),
236    ServerRemoved,
237    LanguageServerRegistered,
238    RefreshRequested { server_id: LanguageServerId },
239    BuffersRemoved(Vec<BufferId>),
240}
241
242impl Editor {
243    pub fn supports_inlay_hints(&self, cx: &mut App) -> bool {
244        let Some(provider) = self.semantics_provider.as_ref() else {
245            return false;
246        };
247
248        let mut supports = false;
249        self.buffer().update(cx, |this, cx| {
250            this.for_each_buffer(&mut |buffer| {
251                supports |= provider.supports_inlay_hints(buffer, cx);
252            });
253        });
254
255        supports
256    }
257
258    pub fn toggle_inline_values(
259        &mut self,
260        _: &ToggleInlineValues,
261        _: &mut Window,
262        cx: &mut Context<Self>,
263    ) {
264        self.inline_value_cache.enabled = !self.inline_value_cache.enabled;
265
266        self.refresh_inline_values(cx);
267    }
268
269    pub fn toggle_inlay_hints(
270        &mut self,
271        _: &ToggleInlayHints,
272        _: &mut Window,
273        cx: &mut Context<Self>,
274    ) {
275        self.refresh_inlay_hints(
276            InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()),
277            cx,
278        );
279    }
280
281    pub fn inlay_hints_enabled(&self) -> bool {
282        self.inlay_hints.as_ref().is_some_and(|cache| cache.enabled)
283    }
284
285    /// Updates inlay hints for the visible ranges of the singleton buffer(s).
286    /// Based on its parameters, either invalidates the previous data, or appends to it.
287    pub(crate) fn refresh_inlay_hints(
288        &mut self,
289        reason: InlayHintRefreshReason,
290        cx: &mut Context<Self>,
291    ) {
292        if !self.lsp_data_enabled() || self.inlay_hints.is_none() {
293            return;
294        }
295        let Some(semantics_provider) = self.semantics_provider() else {
296            return;
297        };
298        let Some(invalidate_cache) = self.refresh_editor_data(&reason, cx) else {
299            return;
300        };
301
302        let debounce = match &reason {
303            InlayHintRefreshReason::SettingsChange(_)
304            | InlayHintRefreshReason::Toggle(_)
305            | InlayHintRefreshReason::BuffersRemoved(_)
306            | InlayHintRefreshReason::ModifiersChanged(_) => None,
307            _may_need_lsp_call => self.inlay_hints.as_ref().and_then(|inlay_hints| {
308                if invalidate_cache.should_invalidate() {
309                    inlay_hints.invalidate_debounce
310                } else {
311                    inlay_hints.append_debounce
312                }
313            }),
314        };
315
316        let mut visible_excerpts = self.visible_buffer_ranges(cx);
317        visible_excerpts.retain(|(snapshot, _, _)| self.is_lsp_relevant(snapshot.file(), cx));
318
319        let mut invalidate_hints_for_buffers = HashSet::default();
320        let ignore_previous_fetches = match reason {
321            InlayHintRefreshReason::ModifiersChanged(_)
322            | InlayHintRefreshReason::Toggle(_)
323            | InlayHintRefreshReason::SettingsChange(_)
324            | InlayHintRefreshReason::ServerRemoved
325            | InlayHintRefreshReason::LanguageServerRegistered
326            | InlayHintRefreshReason::RefreshRequested { .. } => true,
327            InlayHintRefreshReason::NewLinesShown | InlayHintRefreshReason::BuffersRemoved(_) => {
328                false
329            }
330            InlayHintRefreshReason::BufferEdited(buffer_id) => {
331                let Some(affected_language) = self
332                    .buffer()
333                    .read(cx)
334                    .buffer(buffer_id)
335                    .and_then(|buffer| buffer.read(cx).language().cloned())
336                else {
337                    return;
338                };
339
340                invalidate_hints_for_buffers.extend(
341                    self.buffer()
342                        .read(cx)
343                        .all_buffers()
344                        .into_iter()
345                        .filter_map(|buffer| {
346                            let buffer = buffer.read(cx);
347                            if buffer.language() == Some(&affected_language) {
348                                Some(buffer.remote_id())
349                            } else {
350                                None
351                            }
352                        }),
353                );
354
355                semantics_provider.invalidate_inlay_hints(&invalidate_hints_for_buffers, cx);
356                visible_excerpts.retain(|(buffer_snapshot, _, _)| {
357                    buffer_snapshot.language() == Some(&affected_language)
358                });
359                false
360            }
361        };
362
363        let multi_buffer = self.buffer().clone();
364
365        let Some(inlay_hints) = self.inlay_hints.as_mut() else {
366            return;
367        };
368
369        if invalidate_cache.should_invalidate()
370            && !matches!(reason, InlayHintRefreshReason::RefreshRequested { .. })
371        {
372            if invalidate_hints_for_buffers.is_empty() {
373                inlay_hints.clear();
374            } else {
375                inlay_hints.clear_for_buffers(
376                    &invalidate_hints_for_buffers,
377                    Self::visible_inlay_hints(self.display_map.read(cx)),
378                    &multi_buffer.read(cx).snapshot(cx),
379                );
380            }
381        }
382        inlay_hints
383            .invalidate_hints_for_buffers
384            .extend(invalidate_hints_for_buffers);
385
386        let mut buffers_to_query = HashMap::default();
387        for (buffer_snapshot, visible_range, _) in visible_excerpts {
388            let buffer_id = buffer_snapshot.remote_id();
389
390            if !self.registered_buffers.contains_key(&buffer_id) {
391                continue;
392            }
393
394            let Some(buffer) = multi_buffer.read(cx).buffer(buffer_id) else {
395                continue;
396            };
397
398            let buffer_version = buffer_snapshot.version().clone();
399            let buffer_anchor_range = buffer_snapshot.anchor_before(visible_range.start)
400                ..buffer_snapshot.anchor_after(visible_range.end);
401
402            let visible_excerpts =
403                buffers_to_query
404                    .entry(buffer_id)
405                    .or_insert_with(|| VisibleExcerpts {
406                        ranges: Vec::new(),
407                        buffer_version: buffer_version.clone(),
408                        buffer: buffer.clone(),
409                    });
410            visible_excerpts.buffer_version = buffer_version;
411            visible_excerpts.ranges.push(buffer_anchor_range);
412        }
413
414        for (buffer_id, visible_excerpts) in buffers_to_query {
415            let Some(buffer) = multi_buffer.read(cx).buffer(buffer_id) else {
416                continue;
417            };
418
419            let (fetched_for_version, fetched_chunks) = inlay_hints
420                .hint_chunk_fetching
421                .entry(buffer_id)
422                .or_default();
423            if visible_excerpts
424                .buffer_version
425                .changed_since(fetched_for_version)
426            {
427                *fetched_for_version = visible_excerpts.buffer_version.clone();
428                fetched_chunks.clear();
429                inlay_hints.hint_refresh_tasks.remove(&buffer_id);
430            }
431
432            let known_chunks = if ignore_previous_fetches {
433                None
434            } else {
435                Some((fetched_for_version.clone(), fetched_chunks.clone()))
436            };
437
438            let mut applicable_chunks =
439                semantics_provider.applicable_inlay_chunks(&buffer, &visible_excerpts.ranges, cx);
440            applicable_chunks.retain(|chunk| fetched_chunks.insert(chunk.clone()));
441            if applicable_chunks.is_empty() && !ignore_previous_fetches {
442                continue;
443            }
444            inlay_hints
445                .hint_refresh_tasks
446                .entry(buffer_id)
447                .or_default()
448                .push(spawn_editor_hints_refresh(
449                    buffer_id,
450                    invalidate_cache,
451                    debounce,
452                    visible_excerpts,
453                    known_chunks,
454                    applicable_chunks,
455                    cx,
456                ));
457        }
458    }
459
460    pub fn clear_inlay_hints(&mut self, cx: &mut Context<Self>) {
461        let to_remove = Self::visible_inlay_hints(self.display_map.read(cx))
462            .map(|inlay| inlay.id)
463            .collect::<Vec<_>>();
464        self.splice_inlays(&to_remove, Vec::new(), cx);
465    }
466
467    fn refresh_editor_data(
468        &mut self,
469        reason: &InlayHintRefreshReason,
470        cx: &mut Context<'_, Editor>,
471    ) -> Option<InvalidationStrategy> {
472        let Some(inlay_hints) = self.inlay_hints.as_mut() else {
473            return None;
474        };
475
476        let invalidate_cache = match reason {
477            InlayHintRefreshReason::ModifiersChanged(enabled) => {
478                match inlay_hints.modifiers_override(*enabled) {
479                    Some(enabled) => {
480                        if enabled {
481                            InvalidationStrategy::None
482                        } else {
483                            self.clear_inlay_hints(cx);
484                            return None;
485                        }
486                    }
487                    None => return None,
488                }
489            }
490            InlayHintRefreshReason::Toggle(enabled) => {
491                if inlay_hints.toggle(*enabled) {
492                    if *enabled {
493                        InvalidationStrategy::None
494                    } else {
495                        self.clear_inlay_hints(cx);
496                        return None;
497                    }
498                } else {
499                    return None;
500                }
501            }
502            InlayHintRefreshReason::SettingsChange(new_settings) => {
503                let visible_inlay_hints =
504                    Self::visible_inlay_hints(self.display_map.read(cx)).collect::<Vec<_>>();
505                match inlay_hints.update_settings(*new_settings, visible_inlay_hints) {
506                    ControlFlow::Break(Some(InlaySplice {
507                        to_remove,
508                        to_insert,
509                    })) => {
510                        self.splice_inlays(&to_remove, to_insert, cx);
511                        return None;
512                    }
513                    ControlFlow::Break(None) => return None,
514                    ControlFlow::Continue(splice) => {
515                        if let Some(InlaySplice {
516                            to_remove,
517                            to_insert,
518                        }) = splice
519                        {
520                            self.splice_inlays(&to_remove, to_insert, cx);
521                        }
522                        InvalidationStrategy::None
523                    }
524                }
525            }
526            InlayHintRefreshReason::BuffersRemoved(buffers_removed) => {
527                let to_remove = self
528                    .display_map
529                    .read(cx)
530                    .current_inlays()
531                    .filter_map(|inlay| {
532                        let anchor = inlay.position.raw_text_anchor()?;
533                        if buffers_removed.contains(&anchor.buffer_id) {
534                            Some(inlay.id)
535                        } else {
536                            None
537                        }
538                    })
539                    .collect::<Vec<_>>();
540                self.splice_inlays(&to_remove, Vec::new(), cx);
541                return None;
542            }
543            InlayHintRefreshReason::ServerRemoved => InvalidationStrategy::BufferEdited,
544            InlayHintRefreshReason::NewLinesShown
545            | InlayHintRefreshReason::LanguageServerRegistered => InvalidationStrategy::None,
546            InlayHintRefreshReason::BufferEdited(_) => InvalidationStrategy::BufferEdited,
547            InlayHintRefreshReason::RefreshRequested { server_id } => {
548                InvalidationStrategy::RefreshRequested {
549                    server_id: *server_id,
550                }
551            }
552        };
553
554        match &mut self.inlay_hints {
555            Some(inlay_hints) => {
556                if !inlay_hints.enabled
557                    && !matches!(reason, InlayHintRefreshReason::ModifiersChanged(_))
558                {
559                    return None;
560                }
561            }
562            None => return None,
563        }
564
565        Some(invalidate_cache)
566    }
567
568    fn visible_inlay_hints(display_map: &DisplayMap) -> impl Iterator<Item = Inlay> + use<'_> {
569        display_map
570            .current_inlays()
571            .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
572            .cloned()
573    }
574
575    pub fn update_inlay_link_and_hover_points(
576        &mut self,
577        snapshot: &EditorSnapshot,
578        point_for_position: PointForPosition,
579        mouse_position: Option<gpui::Point<Pixels>>,
580        secondary_held: bool,
581        shift_held: bool,
582        window: &mut Window,
583        cx: &mut Context<Self>,
584    ) {
585        let Some(lsp_store) = self.project().map(|project| project.read(cx).lsp_store()) else {
586            return;
587        };
588        let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
589            Some(
590                snapshot
591                    .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left),
592            )
593        } else {
594            None
595        };
596        let mut go_to_definition_updated = false;
597        let mut hover_updated = false;
598        if let Some(hovered_offset) = hovered_offset {
599            let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
600            let previous_valid_anchor = buffer_snapshot.anchor_at(
601                point_for_position.previous_valid.to_point(snapshot),
602                Bias::Left,
603            );
604            let next_valid_anchor = buffer_snapshot.anchor_at(
605                point_for_position.next_valid.to_point(snapshot),
606                Bias::Right,
607            );
608            if let Some(hovered_hint) = Self::visible_inlay_hints(self.display_map.read(cx))
609                .filter(|hint| snapshot.can_resolve(&hint.position))
610                .skip_while(|hint| {
611                    hint.position
612                        .cmp(&previous_valid_anchor, &buffer_snapshot)
613                        .is_lt()
614                })
615                .take_while(|hint| {
616                    hint.position
617                        .cmp(&next_valid_anchor, &buffer_snapshot)
618                        .is_le()
619                })
620                .max_by_key(|hint| hint.id)
621            {
622                if let Some(ResolvedHint::Resolved(cached_hint)) = buffer_snapshot
623                    .anchor_to_buffer_anchor(hovered_hint.position)
624                    .and_then(|(anchor, _)| {
625                        lsp_store.update(cx, |lsp_store, cx| {
626                            lsp_store.resolved_hint(anchor.buffer_id, hovered_hint.id, cx)
627                        })
628                    })
629                {
630                    match cached_hint.resolve_state {
631                        ResolveState::Resolved => {
632                            let original_text = cached_hint.text();
633                            let actual_left_padding =
634                                if cached_hint.padding_left && !original_text.starts_with(" ") {
635                                    1
636                                } else {
637                                    0
638                                };
639                            let actual_right_padding =
640                                if cached_hint.padding_right && !original_text.ends_with(" ") {
641                                    1
642                                } else {
643                                    0
644                                };
645                            match cached_hint.label {
646                                InlayHintLabel::String(_) => {
647                                    if let Some(tooltip) = cached_hint.tooltip {
648                                        hover_popover::hover_at_inlay(
649                                            self,
650                                            InlayHover {
651                                                tooltip: match tooltip {
652                                                    InlayHintTooltip::String(text) => HoverBlock {
653                                                        text,
654                                                        kind: HoverBlockKind::PlainText,
655                                                    },
656                                                    InlayHintTooltip::MarkupContent(content) => {
657                                                        HoverBlock {
658                                                            text: content.value,
659                                                            kind: content.kind,
660                                                        }
661                                                    }
662                                                },
663                                                range: InlayHighlight {
664                                                    inlay: hovered_hint.id,
665                                                    inlay_position: hovered_hint.position,
666                                                    range: actual_left_padding
667                                                        ..hovered_hint.text().len()
668                                                            - actual_right_padding,
669                                                },
670                                            },
671                                            window,
672                                            cx,
673                                        );
674                                        hover_updated = true;
675                                    }
676                                }
677                                InlayHintLabel::LabelParts(label_parts) => {
678                                    let hint_start =
679                                        snapshot.anchor_to_inlay_offset(hovered_hint.position);
680                                    let content_start =
681                                        InlayOffset(hint_start.0 + actual_left_padding);
682                                    if let Some((hovered_hint_part, part_range)) =
683                                        hover_popover::find_hovered_hint_part(
684                                            label_parts,
685                                            content_start,
686                                            hovered_offset,
687                                        )
688                                    {
689                                        let highlight_start = part_range.start - hint_start;
690                                        let highlight_end = part_range.end - hint_start;
691                                        let highlight = InlayHighlight {
692                                            inlay: hovered_hint.id,
693                                            inlay_position: hovered_hint.position,
694                                            range: highlight_start..highlight_end,
695                                        };
696                                        if let Some(tooltip) = hovered_hint_part.tooltip {
697                                            hover_popover::hover_at_inlay(
698                                                self,
699                                                InlayHover {
700                                                    tooltip: match tooltip {
701                                                        InlayHintLabelPartTooltip::String(text) => {
702                                                            HoverBlock {
703                                                                text,
704                                                                kind: HoverBlockKind::PlainText,
705                                                            }
706                                                        }
707                                                        InlayHintLabelPartTooltip::MarkupContent(
708                                                            content,
709                                                        ) => HoverBlock {
710                                                            text: content.value,
711                                                            kind: content.kind,
712                                                        },
713                                                    },
714                                                    range: highlight.clone(),
715                                                },
716                                                window,
717                                                cx,
718                                            );
719                                            hover_updated = true;
720                                        }
721                                        if let Some((language_server_id, location)) =
722                                            hovered_hint_part.location
723                                            && secondary_held
724                                            && !self.has_pending_nonempty_selection()
725                                        {
726                                            go_to_definition_updated = true;
727                                            show_link_definition(
728                                                shift_held,
729                                                self,
730                                                TriggerPoint::InlayHint(
731                                                    highlight,
732                                                    location,
733                                                    language_server_id,
734                                                ),
735                                                snapshot,
736                                                window,
737                                                cx,
738                                            );
739                                        }
740                                    }
741                                }
742                            };
743                        }
744                        ResolveState::CanResolve(_, _) => debug_panic!(
745                            "Expected resolved_hint retrieval to return a resolved hint"
746                        ),
747                        ResolveState::Resolving => {}
748                    }
749                }
750            }
751        }
752
753        if !go_to_definition_updated {
754            self.hide_hovered_link(cx)
755        }
756        if !hover_updated {
757            hover_popover::hover_at(self, None, mouse_position, window, cx);
758        }
759    }
760
761    fn inlay_hints_for_buffer(
762        &mut self,
763        invalidate_cache: InvalidationStrategy,
764        buffer_excerpts: VisibleExcerpts,
765        known_chunks: Option<(Global, HashSet<Range<BufferRow>>)>,
766        cx: &mut Context<Self>,
767    ) -> Option<Vec<Task<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>>> {
768        let semantics_provider = self.semantics_provider()?;
769
770        let new_hint_tasks = semantics_provider
771            .inlay_hints(
772                invalidate_cache,
773                buffer_excerpts.buffer,
774                buffer_excerpts.ranges,
775                known_chunks,
776                cx,
777            )
778            .unwrap_or_default();
779
780        let mut hint_tasks = None;
781        for (row_range, new_hints_task) in new_hint_tasks {
782            hint_tasks
783                .get_or_insert_with(Vec::new)
784                .push(cx.spawn(async move |_, _| (row_range, new_hints_task.await)));
785        }
786        hint_tasks
787    }
788
789    fn apply_fetched_hints(
790        &mut self,
791        buffer_id: BufferId,
792        query_version: Global,
793        invalidate_cache: InvalidationStrategy,
794        new_hints: Vec<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>,
795        cx: &mut Context<Self>,
796    ) {
797        let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
798        let visible_inlay_hint_ids = Self::visible_inlay_hints(self.display_map.read(cx))
799            .filter(|inlay| {
800                multi_buffer_snapshot
801                    .anchor_to_buffer_anchor(inlay.position)
802                    .map(|(anchor, _)| anchor.buffer_id)
803                    == Some(buffer_id)
804            })
805            .map(|inlay| inlay.id)
806            .collect::<Vec<_>>();
807        let Some(inlay_hints) = &mut self.inlay_hints else {
808            return;
809        };
810        let Some(buffer_snapshot) = self
811            .buffer
812            .read(cx)
813            .buffer(buffer_id)
814            .map(|buffer| buffer.read(cx).snapshot())
815        else {
816            return;
817        };
818
819        let mut hints_to_remove = Vec::new();
820
821        // If we've received hints from the cache, it means `invalidate_cache` had invalidated whatever possible there,
822        // and most probably there are no more hints with IDs from `visible_inlay_hint_ids` in the cache.
823        // So, if we hover such hints, no resolve will happen.
824        //
825        // Another issue is in the fact that changing one buffer may lead to other buffers' hints changing, so more cache entries may be removed.
826        // Hence, clear all excerpts' hints in the multi buffer: later, the invalidated ones will re-trigger the LSP query, the rest will be restored
827        // from the cache.
828        if invalidate_cache.should_invalidate() {
829            for hint_id in &visible_inlay_hint_ids {
830                inlay_hints.added_hints.remove(hint_id);
831            }
832            hints_to_remove.extend(visible_inlay_hint_ids);
833
834            // When invalidating, this task removes ALL visible hints for the buffer
835            // but only adds back hints for its own chunk ranges. Chunks fetched by
836            // other concurrent tasks (e.g., a scroll task that completed before this
837            // edit task) would have their hints removed but remain marked as "already
838            // fetched" in hint_chunk_fetching, preventing re-fetch on the next
839            // NewLinesShown. Fix: retain only chunks that this task has results for.
840            let task_chunk_ranges: HashSet<&Range<BufferRow>> =
841                new_hints.iter().map(|(range, _)| range).collect();
842            if let Some((_, fetched_chunks)) = inlay_hints.hint_chunk_fetching.get_mut(&buffer_id) {
843                fetched_chunks.retain(|chunk| task_chunk_ranges.contains(chunk));
844            }
845        }
846
847        let mut inserted_hint_text = HashMap::default();
848        let new_hints = new_hints
849            .into_iter()
850            .filter_map(|(chunk_range, hints_result)| {
851                let chunks_fetched = inlay_hints.hint_chunk_fetching.get_mut(&buffer_id);
852                match hints_result {
853                    Ok(new_hints) => {
854                        if new_hints.is_empty() {
855                            if let Some((_, chunks_fetched)) = chunks_fetched {
856                                chunks_fetched.remove(&chunk_range);
857                            }
858                        }
859                        Some(new_hints)
860                    }
861                    Err(e) => {
862                        log::error!(
863                            "Failed to query inlays for buffer row range {chunk_range:?}, {e:#}"
864                        );
865                        if let Some((for_version, chunks_fetched)) = chunks_fetched {
866                            if for_version == &query_version {
867                                chunks_fetched.remove(&chunk_range);
868                            }
869                        }
870                        None
871                    }
872                }
873            })
874            .flat_map(|new_hints| {
875                let mut hints_deduplicated = Vec::new();
876
877                if new_hints.len() > 1 {
878                    for (server_id, new_hints) in new_hints {
879                        for (new_id, new_hint) in new_hints {
880                            let hints_text_for_position = inserted_hint_text
881                                .entry(new_hint.position)
882                                .or_insert_with(HashMap::default);
883                            let insert =
884                                match hints_text_for_position.entry(new_hint.text().to_string()) {
885                                    hash_map::Entry::Occupied(o) => o.get() == &server_id,
886                                    hash_map::Entry::Vacant(v) => {
887                                        v.insert(server_id);
888                                        true
889                                    }
890                                };
891
892                            if insert {
893                                hints_deduplicated.push((new_id, new_hint));
894                            }
895                        }
896                    }
897                } else {
898                    hints_deduplicated.extend(new_hints.into_values().flatten());
899                }
900
901                hints_deduplicated
902            })
903            .filter(|(hint_id, lsp_hint)| {
904                inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
905                    && inlay_hints
906                        .added_hints
907                        .insert(*hint_id, lsp_hint.kind)
908                        .is_none()
909            })
910            .sorted_by(|(_, a), (_, b)| a.position.cmp(&b.position, &buffer_snapshot))
911            .collect::<Vec<_>>();
912
913        let hints_to_insert = multi_buffer_snapshot
914            .text_anchors_to_visible_anchors(
915                new_hints.iter().map(|(_, lsp_hint)| lsp_hint.position),
916            )
917            .into_iter()
918            .zip(&new_hints)
919            .filter_map(|(position, (hint_id, hint))| Some(Inlay::hint(*hint_id, position?, &hint)))
920            .collect();
921        let invalidate_hints_for_buffers =
922            std::mem::take(&mut inlay_hints.invalidate_hints_for_buffers);
923        if !invalidate_hints_for_buffers.is_empty() {
924            hints_to_remove.extend(
925                Self::visible_inlay_hints(self.display_map.read(cx))
926                    .filter(|inlay| {
927                        multi_buffer_snapshot
928                            .anchor_to_buffer_anchor(inlay.position)
929                            .is_none_or(|(anchor, _)| {
930                                invalidate_hints_for_buffers.contains(&anchor.buffer_id)
931                            })
932                    })
933                    .map(|inlay| inlay.id),
934            );
935        }
936
937        self.splice_inlays(&hints_to_remove, hints_to_insert, cx);
938    }
939}
940
941#[derive(Debug)]
942struct VisibleExcerpts {
943    ranges: Vec<Range<text::Anchor>>,
944    buffer_version: Global,
945    buffer: Entity<language::Buffer>,
946}
947
948fn spawn_editor_hints_refresh(
949    buffer_id: BufferId,
950    invalidate_cache: InvalidationStrategy,
951    debounce: Option<Duration>,
952    buffer_excerpts: VisibleExcerpts,
953    known_chunks: Option<(Global, HashSet<Range<BufferRow>>)>,
954    applicable_chunks: Vec<Range<BufferRow>>,
955    cx: &mut Context<'_, Editor>,
956) -> Task<()> {
957    cx.spawn(async move |editor, cx| {
958        if let Some(debounce) = debounce {
959            cx.background_executor().timer(debounce).await;
960        }
961
962        let query_version = buffer_excerpts.buffer_version.clone();
963        let Some(hint_tasks) = editor
964            .update(cx, |editor, cx| {
965                editor.inlay_hints_for_buffer(invalidate_cache, buffer_excerpts, known_chunks, cx)
966            })
967            .ok()
968        else {
969            return;
970        };
971        let hint_tasks = hint_tasks.unwrap_or_default();
972        if hint_tasks.is_empty() {
973            editor
974                .update(cx, |editor, _| {
975                    if let Some((_, hint_chunk_fetching)) = editor
976                        .inlay_hints
977                        .as_mut()
978                        .and_then(|inlay_hints| inlay_hints.hint_chunk_fetching.get_mut(&buffer_id))
979                    {
980                        for applicable_chunks in &applicable_chunks {
981                            hint_chunk_fetching.remove(applicable_chunks);
982                        }
983                    }
984                })
985                .ok();
986            return;
987        }
988        let new_hints = join_all(hint_tasks).await;
989        editor
990            .update(cx, |editor, cx| {
991                editor.apply_fetched_hints(
992                    buffer_id,
993                    query_version,
994                    invalidate_cache,
995                    new_hints,
996                    cx,
997                );
998            })
999            .ok();
1000    })
1001}
1002
1003#[cfg(test)]
1004pub mod tests {
1005    use crate::editor_tests::update_test_language_settings;
1006    use crate::inlays::inlay_hints::InlayHintRefreshReason;
1007    use crate::scroll::Autoscroll;
1008    use crate::scroll::ScrollAmount;
1009    use crate::{Editor, SelectionEffects};
1010    use collections::HashSet;
1011    use futures::channel::oneshot;
1012    use futures::{StreamExt, future};
1013    use gpui::{AppContext as _, Context, TestAppContext, WindowHandle};
1014    use itertools::Itertools as _;
1015    use language::language_settings::InlayHintKind;
1016    use language::{Capability, FakeLspAdapter};
1017    use language::{Language, LanguageConfig, LanguageMatcher};
1018    use languages::rust_lang;
1019    use lsp::{DEFAULT_LSP_REQUEST_TIMEOUT, FakeLanguageServer};
1020    use multi_buffer::{MultiBuffer, MultiBufferOffset, PathKey};
1021    use parking_lot::Mutex;
1022    use pretty_assertions::assert_eq;
1023    use project::{FakeFs, Project};
1024    use serde_json::json;
1025    use settings::{AllLanguageSettingsContent, InlayHintSettingsContent, SettingsStore};
1026    use std::ops::Range;
1027    use std::sync::Arc;
1028    use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
1029    use std::time::Duration;
1030    use text::{OffsetRangeExt, Point};
1031    use ui::App;
1032    use util::path;
1033    use util::paths::natural_sort;
1034
1035    #[gpui::test]
1036    async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
1037        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1038        init_test(cx, &|settings| {
1039            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1040                show_value_hints: Some(true),
1041                enabled: Some(true),
1042                edit_debounce_ms: Some(0),
1043                scroll_debounce_ms: Some(0),
1044                show_type_hints: Some(allowed_hint_kinds.contains(&Some(InlayHintKind::Type))),
1045                show_parameter_hints: Some(
1046                    allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1047                ),
1048                show_other_hints: Some(allowed_hint_kinds.contains(&None)),
1049                show_background: Some(false),
1050                toggle_on_modifiers_press: None,
1051            })
1052        });
1053        let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1054            let lsp_request_count = Arc::new(AtomicU32::new(0));
1055            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1056                move |params, _| {
1057                    let task_lsp_request_count = Arc::clone(&lsp_request_count);
1058                    async move {
1059                        let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1;
1060                        assert_eq!(
1061                            params.text_document.uri,
1062                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
1063                        );
1064                        Ok(Some(vec![lsp::InlayHint {
1065                            position: lsp::Position::new(0, i),
1066                            label: lsp::InlayHintLabel::String(i.to_string()),
1067                            kind: None,
1068                            text_edits: None,
1069                            tooltip: None,
1070                            padding_left: None,
1071                            padding_right: None,
1072                            data: None,
1073                        }]))
1074                    }
1075                },
1076            );
1077        })
1078        .await;
1079        cx.executor().run_until_parked();
1080
1081        editor
1082            .update(cx, |editor, _window, cx| {
1083                let expected_hints = vec!["1".to_string()];
1084                assert_eq!(
1085                    expected_hints,
1086                    cached_hint_labels(editor, cx),
1087                    "Should get its first hints when opening the editor"
1088                );
1089                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1090                assert_eq!(
1091                    allowed_hint_kinds_for_editor(editor),
1092                    allowed_hint_kinds,
1093                    "Cache should use editor settings to get the allowed hint kinds"
1094                );
1095            })
1096            .unwrap();
1097
1098        editor
1099            .update(cx, |editor, window, cx| {
1100                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1101                    s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1102                });
1103                editor.handle_input("some change", window, cx);
1104            })
1105            .unwrap();
1106        cx.executor().run_until_parked();
1107        editor
1108            .update(cx, |editor, _window, cx| {
1109                let expected_hints = vec!["2".to_string()];
1110                assert_eq!(
1111                    expected_hints,
1112                    cached_hint_labels(editor, cx),
1113                    "Should get new hints after an edit"
1114                );
1115                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1116                assert_eq!(
1117                    allowed_hint_kinds_for_editor(editor),
1118                    allowed_hint_kinds,
1119                    "Cache should use editor settings to get the allowed hint kinds"
1120                );
1121            })
1122            .unwrap();
1123
1124        fake_server
1125            .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1126            .await
1127            .into_response()
1128            .expect("inlay refresh request failed");
1129        cx.executor().run_until_parked();
1130        editor
1131            .update(cx, |editor, _window, cx| {
1132                let expected_hints = vec!["3".to_string()];
1133                assert_eq!(
1134                    expected_hints,
1135                    cached_hint_labels(editor, cx),
1136                    "Should get new hints after hint refresh/ request"
1137                );
1138                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1139                assert_eq!(
1140                    allowed_hint_kinds_for_editor(editor),
1141                    allowed_hint_kinds,
1142                    "Cache should use editor settings to get the allowed hint kinds"
1143                );
1144            })
1145            .unwrap();
1146    }
1147
1148    #[gpui::test]
1149    async fn test_racy_cache_updates(cx: &mut gpui::TestAppContext) {
1150        init_test(cx, &|settings| {
1151            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1152                enabled: Some(true),
1153                ..InlayHintSettingsContent::default()
1154            })
1155        });
1156        let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1157            let lsp_request_count = Arc::new(AtomicU32::new(0));
1158            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1159                move |params, _| {
1160                    let task_lsp_request_count = Arc::clone(&lsp_request_count);
1161                    async move {
1162                        let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1;
1163                        assert_eq!(
1164                            params.text_document.uri,
1165                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
1166                        );
1167                        Ok(Some(vec![lsp::InlayHint {
1168                            position: lsp::Position::new(0, i),
1169                            label: lsp::InlayHintLabel::String(i.to_string()),
1170                            kind: Some(lsp::InlayHintKind::TYPE),
1171                            text_edits: None,
1172                            tooltip: None,
1173                            padding_left: None,
1174                            padding_right: None,
1175                            data: None,
1176                        }]))
1177                    }
1178                },
1179            );
1180        })
1181        .await;
1182        cx.executor().advance_clock(Duration::from_secs(1));
1183        cx.executor().run_until_parked();
1184
1185        editor
1186            .update(cx, |editor, _window, cx| {
1187                let expected_hints = vec!["1".to_string()];
1188                assert_eq!(
1189                    expected_hints,
1190                    cached_hint_labels(editor, cx),
1191                    "Should get its first hints when opening the editor"
1192                );
1193                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1194            })
1195            .unwrap();
1196
1197        // Emulate simultaneous events: both editing, refresh and, slightly after, scroll updates are triggered.
1198        editor
1199            .update(cx, |editor, window, cx| {
1200                editor.handle_input("foo", window, cx);
1201            })
1202            .unwrap();
1203        cx.executor().advance_clock(Duration::from_millis(5));
1204        let refresh_request = fake_server
1205            .request::<lsp::request::InlayHintRefreshRequest>((), lsp::DEFAULT_LSP_REQUEST_TIMEOUT);
1206        cx.executor().advance_clock(Duration::from_millis(5));
1207        editor
1208            .update(cx, |editor, _window, cx| {
1209                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
1210            })
1211            .unwrap();
1212        cx.executor().advance_clock(Duration::from_secs(1));
1213        cx.executor().run_until_parked();
1214        refresh_request.await.into_response().unwrap();
1215        editor
1216            .update(cx, |editor, _window, cx| {
1217                let expected_hints = vec!["2".to_string()];
1218                assert_eq!(expected_hints, cached_hint_labels(editor, cx), "Despite multiple simultaneous refreshes, only one inlay hint query should be issued");
1219                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1220            })
1221            .unwrap();
1222    }
1223
1224    #[gpui::test]
1225    async fn test_no_hint_duplication_when_refresh_races_with_fetch(cx: &mut gpui::TestAppContext) {
1226        init_test(cx, &|settings| {
1227            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1228                enabled: Some(true),
1229                ..InlayHintSettingsContent::default()
1230            })
1231        });
1232        let (first_request_unblock, first_request_gate) = oneshot::channel::<()>();
1233        let first_request_gate = Arc::new(Mutex::new(Some(first_request_gate)));
1234        let lsp_request_count = Arc::new(AtomicU32::new(0));
1235        let (_, editor, fake_server) = prepare_test_objects(cx, {
1236            let first_request_gate = first_request_gate.clone();
1237            let lsp_request_count = lsp_request_count.clone();
1238            move |fake_server, file_with_hints| {
1239                let lsp_request_count = lsp_request_count.clone();
1240                let first_request_gate = first_request_gate.clone();
1241                fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1242                    move |params, _| {
1243                        let first_request_gate = first_request_gate.lock().take();
1244                        let i = lsp_request_count.fetch_add(1, Ordering::Release) + 1;
1245                        async move {
1246                            if let Some(first_request_gate) = first_request_gate {
1247                                first_request_gate.await.ok();
1248                            }
1249                            assert_eq!(
1250                                params.text_document.uri,
1251                                lsp::Uri::from_file_path(file_with_hints).unwrap(),
1252                            );
1253                            Ok(Some(vec![lsp::InlayHint {
1254                                position: lsp::Position::new(0, 1),
1255                                label: lsp::InlayHintLabel::String(i.to_string()),
1256                                kind: Some(lsp::InlayHintKind::TYPE),
1257                                text_edits: None,
1258                                tooltip: None,
1259                                padding_left: None,
1260                                padding_right: None,
1261                                data: None,
1262                            }]))
1263                        }
1264                    },
1265                );
1266            }
1267        })
1268        .await;
1269        cx.executor().advance_clock(Duration::from_secs(1));
1270        cx.executor().run_until_parked();
1271
1272        editor
1273            .update(cx, |editor, _window, cx| {
1274                assert!(
1275                    cached_hint_labels(editor, cx).is_empty(),
1276                    "The initial hint fetch is blocked and should not have populated the cache yet"
1277                );
1278            })
1279            .unwrap();
1280
1281        // Emulate a server refresh request arriving while the initial fetch is still running.
1282        fake_server
1283            .request::<lsp::request::InlayHintRefreshRequest>((), lsp::DEFAULT_LSP_REQUEST_TIMEOUT)
1284            .await
1285            .into_response()
1286            .unwrap();
1287        cx.executor().advance_clock(Duration::from_secs(1));
1288        cx.executor().run_until_parked();
1289
1290        first_request_unblock.send(()).unwrap();
1291        cx.executor().advance_clock(Duration::from_secs(1));
1292        cx.executor().run_until_parked();
1293        assert_eq!(
1294            2,
1295            lsp_request_count.load(Ordering::Acquire),
1296            "The refresh should have re-queried the server"
1297        );
1298
1299        editor
1300            .update(cx, |editor, _window, cx| {
1301                let expected_hints = vec!["2".to_string()];
1302                assert_eq!(
1303                    expected_hints,
1304                    cached_hint_labels(editor, cx),
1305                    "A refresh racing with an in-flight fetch should replace its hints, not duplicate them"
1306                );
1307                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1308            })
1309            .unwrap();
1310    }
1311
1312    #[gpui::test]
1313    async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
1314        init_test(cx, &|settings| {
1315            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1316                show_value_hints: Some(true),
1317                enabled: Some(true),
1318                edit_debounce_ms: Some(0),
1319                scroll_debounce_ms: Some(0),
1320                show_type_hints: Some(true),
1321                show_parameter_hints: Some(true),
1322                show_other_hints: Some(true),
1323                show_background: Some(false),
1324                toggle_on_modifiers_press: None,
1325            })
1326        });
1327
1328        let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1329            let lsp_request_count = Arc::new(AtomicU32::new(0));
1330            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1331                move |params, _| {
1332                    let task_lsp_request_count = Arc::clone(&lsp_request_count);
1333                    async move {
1334                        assert_eq!(
1335                            params.text_document.uri,
1336                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
1337                        );
1338                        let current_call_id =
1339                            Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1340                        Ok(Some(vec![lsp::InlayHint {
1341                            position: lsp::Position::new(0, current_call_id),
1342                            label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1343                            kind: None,
1344                            text_edits: None,
1345                            tooltip: None,
1346                            padding_left: None,
1347                            padding_right: None,
1348                            data: None,
1349                        }]))
1350                    }
1351                },
1352            );
1353        })
1354        .await;
1355        cx.executor().run_until_parked();
1356
1357        editor
1358            .update(cx, |editor, _, cx| {
1359                let expected_hints = vec!["0".to_string()];
1360                assert_eq!(
1361                    expected_hints,
1362                    cached_hint_labels(editor, cx),
1363                    "Should get its first hints when opening the editor"
1364                );
1365                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1366            })
1367            .unwrap();
1368
1369        let progress_token = 42;
1370        fake_server
1371            .request::<lsp::request::WorkDoneProgressCreate>(
1372                lsp::WorkDoneProgressCreateParams {
1373                    token: lsp::ProgressToken::Number(progress_token),
1374                },
1375                DEFAULT_LSP_REQUEST_TIMEOUT,
1376            )
1377            .await
1378            .into_response()
1379            .expect("work done progress create request failed");
1380        cx.executor().run_until_parked();
1381        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1382            token: lsp::ProgressToken::Number(progress_token),
1383            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1384                lsp::WorkDoneProgressBegin::default(),
1385            )),
1386        });
1387        cx.executor().run_until_parked();
1388
1389        editor
1390            .update(cx, |editor, _, cx| {
1391                let expected_hints = vec!["0".to_string()];
1392                assert_eq!(
1393                    expected_hints,
1394                    cached_hint_labels(editor, cx),
1395                    "Should not update hints while the work task is running"
1396                );
1397                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1398            })
1399            .unwrap();
1400
1401        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1402            token: lsp::ProgressToken::Number(progress_token),
1403            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1404                lsp::WorkDoneProgressEnd::default(),
1405            )),
1406        });
1407        cx.executor().run_until_parked();
1408
1409        editor
1410            .update(cx, |editor, _, cx| {
1411                let expected_hints = vec!["1".to_string()];
1412                assert_eq!(
1413                    expected_hints,
1414                    cached_hint_labels(editor, cx),
1415                    "New hints should be queried after the work task is done"
1416                );
1417                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1418            })
1419            .unwrap();
1420
1421        run_work_cycle(&fake_server, progress_token + 1, cx).await;
1422
1423        editor
1424            .update(cx, |editor, _, cx| {
1425                let expected_hints = vec!["1".to_string()];
1426                assert_eq!(
1427                    expected_hints,
1428                    cached_hint_labels(editor, cx),
1429                    "Repeated work cycles without buffer changes should not invalidate hints again"
1430                );
1431                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1432            })
1433            .unwrap();
1434
1435        editor
1436            .update(cx, |editor, window, cx| {
1437                editor.handle_input("~", window, cx);
1438            })
1439            .unwrap();
1440        cx.executor().run_until_parked();
1441        editor
1442            .update(cx, |editor, _, cx| {
1443                let expected_hints = vec!["2".to_string()];
1444                assert_eq!(
1445                    expected_hints,
1446                    cached_hint_labels(editor, cx),
1447                    "A buffer edit should invalidate and re-query the hints"
1448                );
1449                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1450            })
1451            .unwrap();
1452
1453        run_work_cycle(&fake_server, progress_token + 2, cx).await;
1454
1455        editor
1456            .update(cx, |editor, _, cx| {
1457                let expected_hints = vec!["3".to_string()];
1458                assert_eq!(
1459                    expected_hints,
1460                    cached_hint_labels(editor, cx),
1461                    "A buffer edit should re-allow the work-end hint refresh"
1462                );
1463                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1464            })
1465            .unwrap();
1466    }
1467
1468    #[gpui::test]
1469    async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1470        init_test(cx, &|settings| {
1471            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1472                show_value_hints: Some(true),
1473                enabled: Some(true),
1474                edit_debounce_ms: Some(0),
1475                scroll_debounce_ms: Some(0),
1476                show_type_hints: Some(true),
1477                show_parameter_hints: Some(true),
1478                show_other_hints: Some(true),
1479                show_background: Some(false),
1480                toggle_on_modifiers_press: None,
1481            })
1482        });
1483
1484        let fs = FakeFs::new(cx.background_executor.clone());
1485        fs.insert_tree(
1486            path!("/a"),
1487            json!({
1488                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1489                "other.md": "Test md file with some text",
1490            }),
1491        )
1492        .await;
1493
1494        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1495
1496        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1497        let mut rs_fake_servers = None;
1498        let mut md_fake_servers = None;
1499        for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1500            language_registry.add(Arc::new(Language::new(
1501                LanguageConfig {
1502                    name: name.into(),
1503                    matcher: (LanguageMatcher {
1504                        path_suffixes: vec![path_suffix.to_string()],
1505                        ..Default::default()
1506                    })
1507                    .into(),
1508                    ..Default::default()
1509                },
1510                Some(tree_sitter_rust::LANGUAGE.into()),
1511            )));
1512            let fake_servers = language_registry.register_fake_lsp(
1513                name,
1514                FakeLspAdapter {
1515                    name,
1516                    capabilities: lsp::ServerCapabilities {
1517                        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1518                        ..Default::default()
1519                    },
1520                    initializer: Some(Box::new({
1521                        move |fake_server| {
1522                            let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1523                            let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1524                            fake_server
1525                                .set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1526                                    move |params, _| {
1527                                        let i = match name {
1528                                            "Rust" => {
1529                                                assert_eq!(
1530                                                    params.text_document.uri,
1531                                                    lsp::Uri::from_file_path(path!("/a/main.rs"))
1532                                                        .unwrap(),
1533                                                );
1534                                                rs_lsp_request_count.fetch_add(1, Ordering::Release)
1535                                                    + 1
1536                                            }
1537                                            "Markdown" => {
1538                                                assert_eq!(
1539                                                    params.text_document.uri,
1540                                                    lsp::Uri::from_file_path(path!("/a/other.md"))
1541                                                        .unwrap(),
1542                                                );
1543                                                md_lsp_request_count.fetch_add(1, Ordering::Release)
1544                                                    + 1
1545                                            }
1546                                            unexpected => {
1547                                                panic!("Unexpected language: {unexpected}")
1548                                            }
1549                                        };
1550
1551                                        async move {
1552                                            let query_start = params.range.start;
1553                                            Ok(Some(vec![lsp::InlayHint {
1554                                                position: query_start,
1555                                                label: lsp::InlayHintLabel::String(i.to_string()),
1556                                                kind: None,
1557                                                text_edits: None,
1558                                                tooltip: None,
1559                                                padding_left: None,
1560                                                padding_right: None,
1561                                                data: None,
1562                                            }]))
1563                                        }
1564                                    },
1565                                );
1566                        }
1567                    })),
1568                    ..Default::default()
1569                },
1570            );
1571            match name {
1572                "Rust" => rs_fake_servers = Some(fake_servers),
1573                "Markdown" => md_fake_servers = Some(fake_servers),
1574                _ => unreachable!(),
1575            }
1576        }
1577
1578        let rs_buffer = project
1579            .update(cx, |project, cx| {
1580                project.open_local_buffer(path!("/a/main.rs"), cx)
1581            })
1582            .await
1583            .unwrap();
1584        let rs_editor = cx.add_window(|window, cx| {
1585            Editor::for_buffer(rs_buffer, Some(project.clone()), window, cx)
1586        });
1587        cx.executor().run_until_parked();
1588
1589        let _rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1590        cx.executor().run_until_parked();
1591
1592        // Establish a viewport so the editor considers itself visible and the hint refresh
1593        // pipeline runs. Then explicitly trigger a refresh.
1594        rs_editor
1595            .update(cx, |editor, window, cx| {
1596                editor.set_visible_line_count(50.0, window, cx);
1597                editor.set_visible_column_count(120.0);
1598                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
1599            })
1600            .unwrap();
1601        cx.executor().run_until_parked();
1602        rs_editor
1603            .update(cx, |editor, _window, cx| {
1604                let expected_hints = vec!["1".to_string()];
1605                assert_eq!(
1606                    expected_hints,
1607                    cached_hint_labels(editor, cx),
1608                    "Should get its first hints when opening the editor"
1609                );
1610                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1611            })
1612            .unwrap();
1613
1614        cx.executor().run_until_parked();
1615        let md_buffer = project
1616            .update(cx, |project, cx| {
1617                project.open_local_buffer(path!("/a/other.md"), cx)
1618            })
1619            .await
1620            .unwrap();
1621        let md_editor =
1622            cx.add_window(|window, cx| Editor::for_buffer(md_buffer, Some(project), window, cx));
1623        cx.executor().run_until_parked();
1624
1625        let _md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1626        cx.executor().run_until_parked();
1627
1628        // Establish a viewport so the editor considers itself visible and the hint refresh
1629        // pipeline runs. Then explicitly trigger a refresh.
1630        md_editor
1631            .update(cx, |editor, window, cx| {
1632                editor.set_visible_line_count(50.0, window, cx);
1633                editor.set_visible_column_count(120.0);
1634                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
1635            })
1636            .unwrap();
1637        cx.executor().run_until_parked();
1638        md_editor
1639            .update(cx, |editor, _window, cx| {
1640                let expected_hints = vec!["1".to_string()];
1641                assert_eq!(
1642                    expected_hints,
1643                    cached_hint_labels(editor, cx),
1644                    "Markdown editor should have a separate version, repeating Rust editor rules"
1645                );
1646                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1647            })
1648            .unwrap();
1649
1650        rs_editor
1651            .update(cx, |editor, window, cx| {
1652                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1653                    s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1654                });
1655                editor.handle_input("some rs change", window, cx);
1656            })
1657            .unwrap();
1658        cx.executor().run_until_parked();
1659        rs_editor
1660            .update(cx, |editor, _window, cx| {
1661                let expected_hints = vec!["2".to_string()];
1662                assert_eq!(
1663                    expected_hints,
1664                    cached_hint_labels(editor, cx),
1665                    "Rust inlay cache should change after the edit"
1666                );
1667                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1668            })
1669            .unwrap();
1670        md_editor
1671            .update(cx, |editor, _window, cx| {
1672                let expected_hints = vec!["1".to_string()];
1673                assert_eq!(
1674                    expected_hints,
1675                    cached_hint_labels(editor, cx),
1676                    "Markdown editor should not be affected by Rust editor changes"
1677                );
1678                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1679            })
1680            .unwrap();
1681
1682        md_editor
1683            .update(cx, |editor, window, cx| {
1684                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1685                    s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1686                });
1687                editor.handle_input("some md change", window, cx);
1688            })
1689            .unwrap();
1690        cx.executor().run_until_parked();
1691        md_editor
1692            .update(cx, |editor, _window, cx| {
1693                let expected_hints = vec!["2".to_string()];
1694                assert_eq!(
1695                    expected_hints,
1696                    cached_hint_labels(editor, cx),
1697                    "Rust editor should not be affected by Markdown editor changes"
1698                );
1699                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1700            })
1701            .unwrap();
1702        rs_editor
1703            .update(cx, |editor, _window, cx| {
1704                let expected_hints = vec!["2".to_string()];
1705                assert_eq!(
1706                    expected_hints,
1707                    cached_hint_labels(editor, cx),
1708                    "Markdown editor should also change independently"
1709                );
1710                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1711            })
1712            .unwrap();
1713    }
1714
1715    #[gpui::test]
1716    async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1717        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1718        init_test(cx, &|settings| {
1719            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1720                show_value_hints: Some(true),
1721                enabled: Some(true),
1722                edit_debounce_ms: Some(0),
1723                scroll_debounce_ms: Some(0),
1724                show_type_hints: Some(allowed_hint_kinds.contains(&Some(InlayHintKind::Type))),
1725                show_parameter_hints: Some(
1726                    allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1727                ),
1728                show_other_hints: Some(allowed_hint_kinds.contains(&None)),
1729                show_background: Some(false),
1730                toggle_on_modifiers_press: None,
1731            })
1732        });
1733
1734        let lsp_request_count = Arc::new(AtomicUsize::new(0));
1735        let (_, editor, fake_server) = prepare_test_objects(cx, {
1736            let lsp_request_count = lsp_request_count.clone();
1737            move |fake_server, file_with_hints| {
1738                let lsp_request_count = lsp_request_count.clone();
1739                fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1740                    move |params, _| {
1741                        lsp_request_count.fetch_add(1, Ordering::Release);
1742                        async move {
1743                            assert_eq!(
1744                                params.text_document.uri,
1745                                lsp::Uri::from_file_path(file_with_hints).unwrap(),
1746                            );
1747                            Ok(Some(vec![
1748                                lsp::InlayHint {
1749                                    position: lsp::Position::new(0, 1),
1750                                    label: lsp::InlayHintLabel::String("type hint".to_string()),
1751                                    kind: Some(lsp::InlayHintKind::TYPE),
1752                                    text_edits: None,
1753                                    tooltip: None,
1754                                    padding_left: None,
1755                                    padding_right: None,
1756                                    data: None,
1757                                },
1758                                lsp::InlayHint {
1759                                    position: lsp::Position::new(0, 2),
1760                                    label: lsp::InlayHintLabel::String(
1761                                        "parameter hint".to_string(),
1762                                    ),
1763                                    kind: Some(lsp::InlayHintKind::PARAMETER),
1764                                    text_edits: None,
1765                                    tooltip: None,
1766                                    padding_left: None,
1767                                    padding_right: None,
1768                                    data: None,
1769                                },
1770                                lsp::InlayHint {
1771                                    position: lsp::Position::new(0, 3),
1772                                    label: lsp::InlayHintLabel::String("other hint".to_string()),
1773                                    kind: None,
1774                                    text_edits: None,
1775                                    tooltip: None,
1776                                    padding_left: None,
1777                                    padding_right: None,
1778                                    data: None,
1779                                },
1780                            ]))
1781                        }
1782                    },
1783                );
1784            }
1785        })
1786        .await;
1787        cx.executor().run_until_parked();
1788
1789        editor
1790            .update(cx, |editor, _, cx| {
1791                assert_eq!(
1792                    lsp_request_count.load(Ordering::Relaxed),
1793                    1,
1794                    "Should query new hints once"
1795                );
1796                assert_eq!(
1797                    vec![
1798                        "type hint".to_string(),
1799                        "parameter hint".to_string(),
1800                        "other hint".to_string(),
1801                    ],
1802                    cached_hint_labels(editor, cx),
1803                    "Should get its first hints when opening the editor"
1804                );
1805                assert_eq!(
1806                    vec!["type hint".to_string(), "other hint".to_string()],
1807                    visible_hint_labels(editor, cx)
1808                );
1809                assert_eq!(
1810                    allowed_hint_kinds_for_editor(editor),
1811                    allowed_hint_kinds,
1812                    "Cache should use editor settings to get the allowed hint kinds"
1813                );
1814            })
1815            .unwrap();
1816
1817        fake_server
1818            .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1819            .await
1820            .into_response()
1821            .expect("inlay refresh request failed");
1822        cx.executor().run_until_parked();
1823        editor
1824            .update(cx, |editor, _, cx| {
1825                assert_eq!(
1826                    lsp_request_count.load(Ordering::Relaxed),
1827                    2,
1828                    "Should load new hints twice"
1829                );
1830                assert_eq!(
1831                    vec![
1832                        "type hint".to_string(),
1833                        "parameter hint".to_string(),
1834                        "other hint".to_string(),
1835                    ],
1836                    cached_hint_labels(editor, cx),
1837                    "Cached hints should not change due to allowed hint kinds settings update"
1838                );
1839                assert_eq!(
1840                    vec!["type hint".to_string(), "other hint".to_string()],
1841                    visible_hint_labels(editor, cx)
1842                );
1843            })
1844            .unwrap();
1845
1846        for (new_allowed_hint_kinds, expected_visible_hints) in [
1847            (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1848            (
1849                HashSet::from_iter([Some(InlayHintKind::Type)]),
1850                vec!["type hint".to_string()],
1851            ),
1852            (
1853                HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1854                vec!["parameter hint".to_string()],
1855            ),
1856            (
1857                HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1858                vec!["type hint".to_string(), "other hint".to_string()],
1859            ),
1860            (
1861                HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1862                vec!["parameter hint".to_string(), "other hint".to_string()],
1863            ),
1864            (
1865                HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1866                vec!["type hint".to_string(), "parameter hint".to_string()],
1867            ),
1868            (
1869                HashSet::from_iter([
1870                    None,
1871                    Some(InlayHintKind::Type),
1872                    Some(InlayHintKind::Parameter),
1873                ]),
1874                vec![
1875                    "type hint".to_string(),
1876                    "parameter hint".to_string(),
1877                    "other hint".to_string(),
1878                ],
1879            ),
1880        ] {
1881            update_test_language_settings(cx, &|settings| {
1882                settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1883                    show_value_hints: Some(true),
1884                    enabled: Some(true),
1885                    edit_debounce_ms: Some(0),
1886                    scroll_debounce_ms: Some(0),
1887                    show_type_hints: Some(
1888                        new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1889                    ),
1890                    show_parameter_hints: Some(
1891                        new_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1892                    ),
1893                    show_other_hints: Some(new_allowed_hint_kinds.contains(&None)),
1894                    show_background: Some(false),
1895                    toggle_on_modifiers_press: None,
1896                })
1897            });
1898            cx.executor().run_until_parked();
1899            editor.update(cx, |editor, _, cx| {
1900                assert_eq!(
1901                    lsp_request_count.load(Ordering::Relaxed),
1902                    2,
1903                    "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1904                );
1905                assert_eq!(
1906                    vec![
1907                        "type hint".to_string(),
1908                        "parameter hint".to_string(),
1909                        "other hint".to_string(),
1910                    ],
1911                    cached_hint_labels(editor, cx),
1912                    "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1913                );
1914                assert_eq!(
1915                    expected_visible_hints,
1916                    visible_hint_labels(editor, cx),
1917                    "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1918                );
1919                assert_eq!(
1920                    allowed_hint_kinds_for_editor(editor),
1921                    new_allowed_hint_kinds,
1922                    "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1923                );
1924            }).unwrap();
1925        }
1926
1927        let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1928        update_test_language_settings(cx, &|settings| {
1929            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1930                show_value_hints: Some(true),
1931                enabled: Some(false),
1932                edit_debounce_ms: Some(0),
1933                scroll_debounce_ms: Some(0),
1934                show_type_hints: Some(
1935                    another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1936                ),
1937                show_parameter_hints: Some(
1938                    another_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1939                ),
1940                show_other_hints: Some(another_allowed_hint_kinds.contains(&None)),
1941                show_background: Some(false),
1942                toggle_on_modifiers_press: None,
1943            })
1944        });
1945        cx.executor().run_until_parked();
1946        editor
1947            .update(cx, |editor, _, cx| {
1948                assert_eq!(
1949                    lsp_request_count.load(Ordering::Relaxed),
1950                    2,
1951                    "Should not load new hints when hints got disabled"
1952                );
1953                assert_eq!(
1954                    vec![
1955                        "type hint".to_string(),
1956                        "parameter hint".to_string(),
1957                        "other hint".to_string(),
1958                    ],
1959                    cached_hint_labels(editor, cx),
1960                    "Should not clear the cache when hints got disabled"
1961                );
1962                assert_eq!(
1963                    Vec::<String>::new(),
1964                    visible_hint_labels(editor, cx),
1965                    "Should clear visible hints when hints got disabled"
1966                );
1967                assert_eq!(
1968                    allowed_hint_kinds_for_editor(editor),
1969                    another_allowed_hint_kinds,
1970                    "Should update its allowed hint kinds even when hints got disabled"
1971                );
1972            })
1973            .unwrap();
1974
1975        fake_server
1976            .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1977            .await
1978            .into_response()
1979            .expect("inlay refresh request failed");
1980        cx.executor().run_until_parked();
1981        editor
1982            .update(cx, |editor, _window, cx| {
1983                assert_eq!(
1984                    lsp_request_count.load(Ordering::Relaxed),
1985                    2,
1986                    "Should not load new hints when they got disabled"
1987                );
1988                assert_eq!(
1989                    vec![
1990                        "type hint".to_string(),
1991                        "parameter hint".to_string(),
1992                        "other hint".to_string(),
1993                    ],
1994                    cached_hint_labels(editor, cx)
1995                );
1996                assert_eq!(Vec::<String>::new(), visible_hint_labels(editor, cx));
1997            })
1998            .unwrap();
1999
2000        let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
2001        update_test_language_settings(cx, &|settings| {
2002            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2003                show_value_hints: Some(true),
2004                enabled: Some(true),
2005                edit_debounce_ms: Some(0),
2006                scroll_debounce_ms: Some(0),
2007                show_type_hints: Some(
2008                    final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
2009                ),
2010                show_parameter_hints: Some(
2011                    final_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
2012                ),
2013                show_other_hints: Some(final_allowed_hint_kinds.contains(&None)),
2014                show_background: Some(false),
2015                toggle_on_modifiers_press: None,
2016            })
2017        });
2018        cx.executor().run_until_parked();
2019        editor
2020            .update(cx, |editor, _, cx| {
2021                assert_eq!(
2022                    lsp_request_count.load(Ordering::Relaxed),
2023                    2,
2024                    "Should not query for new hints when they got re-enabled, as the file version did not change"
2025                );
2026                assert_eq!(
2027                    vec![
2028                        "type hint".to_string(),
2029                        "parameter hint".to_string(),
2030                        "other hint".to_string(),
2031                    ],
2032                    cached_hint_labels(editor, cx),
2033                    "Should get its cached hints fully repopulated after the hints got re-enabled"
2034                );
2035                assert_eq!(
2036                    vec!["parameter hint".to_string()],
2037                    visible_hint_labels(editor, cx),
2038                    "Should get its visible hints repopulated and filtered after the h"
2039                );
2040                assert_eq!(
2041                    allowed_hint_kinds_for_editor(editor),
2042                    final_allowed_hint_kinds,
2043                    "Cache should update editor settings when hints got re-enabled"
2044                );
2045            })
2046            .unwrap();
2047
2048        fake_server
2049            .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
2050            .await
2051            .into_response()
2052            .expect("inlay refresh request failed");
2053        cx.executor().run_until_parked();
2054        editor
2055            .update(cx, |editor, _, cx| {
2056                assert_eq!(
2057                    lsp_request_count.load(Ordering::Relaxed),
2058                    3,
2059                    "Should query for new hints again"
2060                );
2061                assert_eq!(
2062                    vec![
2063                        "type hint".to_string(),
2064                        "parameter hint".to_string(),
2065                        "other hint".to_string(),
2066                    ],
2067                    cached_hint_labels(editor, cx),
2068                );
2069                assert_eq!(
2070                    vec!["parameter hint".to_string()],
2071                    visible_hint_labels(editor, cx),
2072                );
2073            })
2074            .unwrap();
2075    }
2076
2077    #[gpui::test]
2078    async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
2079        init_test(cx, &|settings| {
2080            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2081                show_value_hints: Some(true),
2082                enabled: Some(true),
2083                edit_debounce_ms: Some(0),
2084                scroll_debounce_ms: Some(0),
2085                show_type_hints: Some(true),
2086                show_parameter_hints: Some(true),
2087                show_other_hints: Some(true),
2088                show_background: Some(false),
2089                toggle_on_modifiers_press: None,
2090            })
2091        });
2092
2093        let lsp_request_count = Arc::new(AtomicU32::new(0));
2094        let (_, editor, _) = prepare_test_objects(cx, {
2095            let lsp_request_count = lsp_request_count.clone();
2096            move |fake_server, file_with_hints| {
2097                let lsp_request_count = lsp_request_count.clone();
2098                fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2099                    move |params, _| {
2100                        let lsp_request_count = lsp_request_count.clone();
2101                        async move {
2102                            let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
2103                            assert_eq!(
2104                                params.text_document.uri,
2105                                lsp::Uri::from_file_path(file_with_hints).unwrap(),
2106                            );
2107                            Ok(Some(vec![lsp::InlayHint {
2108                                position: lsp::Position::new(0, i),
2109                                label: lsp::InlayHintLabel::String(i.to_string()),
2110                                kind: None,
2111                                text_edits: None,
2112                                tooltip: None,
2113                                padding_left: None,
2114                                padding_right: None,
2115                                data: None,
2116                            }]))
2117                        }
2118                    },
2119                );
2120            }
2121        })
2122        .await;
2123
2124        let mut expected_changes = Vec::new();
2125        for change_after_opening in [
2126            "initial change #1",
2127            "initial change #2",
2128            "initial change #3",
2129        ] {
2130            editor
2131                .update(cx, |editor, window, cx| {
2132                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2133                        s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
2134                    });
2135                    editor.handle_input(change_after_opening, window, cx);
2136                })
2137                .unwrap();
2138            expected_changes.push(change_after_opening);
2139        }
2140
2141        cx.executor().run_until_parked();
2142
2143        editor
2144            .update(cx, |editor, _window, cx| {
2145                let current_text = editor.text(cx);
2146                for change in &expected_changes {
2147                    assert!(
2148                        current_text.contains(change),
2149                        "Should apply all changes made"
2150                    );
2151                }
2152                assert_eq!(
2153                    lsp_request_count.load(Ordering::Relaxed),
2154                    2,
2155                    "Should query new hints twice: for editor init and for the last edit that interrupted all others"
2156                );
2157                let expected_hints = vec!["2".to_string()];
2158                assert_eq!(
2159                    expected_hints,
2160                    cached_hint_labels(editor, cx),
2161                    "Should get hints from the last edit landed only"
2162                );
2163                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2164            })
2165            .unwrap();
2166
2167        let mut edits = Vec::new();
2168        for async_later_change in [
2169            "another change #1",
2170            "another change #2",
2171            "another change #3",
2172        ] {
2173            expected_changes.push(async_later_change);
2174            let task_editor = editor;
2175            edits.push(cx.spawn(|mut cx| async move {
2176                task_editor
2177                    .update(&mut cx, |editor, window, cx| {
2178                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2179                            s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
2180                        });
2181                        editor.handle_input(async_later_change, window, cx);
2182                    })
2183                    .unwrap();
2184            }));
2185        }
2186        let _ = future::join_all(edits).await;
2187        cx.executor().run_until_parked();
2188
2189        editor
2190            .update(cx, |editor, _, cx| {
2191                let current_text = editor.text(cx);
2192                for change in &expected_changes {
2193                    assert!(
2194                        current_text.contains(change),
2195                        "Should apply all changes made"
2196                    );
2197                }
2198                assert_eq!(
2199                    lsp_request_count.load(Ordering::SeqCst),
2200                    3,
2201                    "Should query new hints one more time, for the last edit only"
2202                );
2203                let expected_hints = vec!["3".to_string()];
2204                assert_eq!(
2205                    expected_hints,
2206                    cached_hint_labels(editor, cx),
2207                    "Should get hints from the last edit landed only"
2208                );
2209                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2210            })
2211            .unwrap();
2212    }
2213
2214    #[gpui::test(iterations = 4)]
2215    async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
2216        init_test(cx, &|settings| {
2217            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2218                enabled: Some(true),
2219                ..InlayHintSettingsContent::default()
2220            })
2221        });
2222
2223        let fs = FakeFs::new(cx.background_executor.clone());
2224        fs.insert_tree(
2225            path!("/a"),
2226            json!({
2227                "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
2228                "other.rs": "// Test file",
2229            }),
2230        )
2231        .await;
2232
2233        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2234
2235        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2236        language_registry.add(rust_lang());
2237
2238        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2239        let lsp_request_count = Arc::new(AtomicUsize::new(0));
2240        let mut fake_servers = language_registry.register_fake_lsp(
2241            "Rust",
2242            FakeLspAdapter {
2243                capabilities: lsp::ServerCapabilities {
2244                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2245                    ..lsp::ServerCapabilities::default()
2246                },
2247                initializer: Some(Box::new({
2248                    let lsp_request_ranges = lsp_request_ranges.clone();
2249                    let lsp_request_count = lsp_request_count.clone();
2250                    move |fake_server| {
2251                        let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
2252                        let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2253                        fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2254                            move |params, _| {
2255                                let task_lsp_request_ranges =
2256                                    Arc::clone(&closure_lsp_request_ranges);
2257                                let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2258                                async move {
2259                                    assert_eq!(
2260                                        params.text_document.uri,
2261                                        lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2262                                    );
2263
2264                                    task_lsp_request_ranges.lock().push(params.range);
2265                                    task_lsp_request_count.fetch_add(1, Ordering::Release);
2266                                    Ok(Some(vec![lsp::InlayHint {
2267                                        position: params.range.start,
2268                                        label: lsp::InlayHintLabel::String(
2269                                            params.range.end.line.to_string(),
2270                                        ),
2271                                        kind: None,
2272                                        text_edits: None,
2273                                        tooltip: None,
2274                                        padding_left: None,
2275                                        padding_right: None,
2276                                        data: None,
2277                                    }]))
2278                                }
2279                            },
2280                        );
2281                    }
2282                })),
2283                ..FakeLspAdapter::default()
2284            },
2285        );
2286
2287        let buffer = project
2288            .update(cx, |project, cx| {
2289                project.open_local_buffer(path!("/a/main.rs"), cx)
2290            })
2291            .await
2292            .unwrap();
2293        let editor =
2294            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
2295        cx.executor().run_until_parked();
2296        let _fake_server = fake_servers.next().await.unwrap();
2297        cx.executor().advance_clock(Duration::from_millis(100));
2298        cx.executor().run_until_parked();
2299
2300        let ranges = lsp_request_ranges
2301            .lock()
2302            .drain(..)
2303            .sorted_by_key(|r| r.start)
2304            .collect::<Vec<_>>();
2305        assert_eq!(
2306            ranges.len(),
2307            1,
2308            "Should query 1 range initially, but got: {ranges:?}"
2309        );
2310
2311        editor
2312            .update(cx, |editor, window, cx| {
2313                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2314            })
2315            .unwrap();
2316        // Wait for the first hints request to fire off
2317        cx.executor().advance_clock(Duration::from_millis(100));
2318        cx.executor().run_until_parked();
2319        editor
2320            .update(cx, |editor, window, cx| {
2321                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2322            })
2323            .unwrap();
2324        cx.executor().advance_clock(Duration::from_millis(100));
2325        cx.executor().run_until_parked();
2326        let visible_range_after_scrolls = editor_visible_range(&editor, cx);
2327        let visible_line_count = editor
2328            .update(cx, |editor, _window, _| {
2329                editor.visible_line_count().unwrap()
2330            })
2331            .unwrap();
2332        let selection_in_cached_range = editor
2333            .update(cx, |editor, _window, cx| {
2334                let ranges = lsp_request_ranges
2335                    .lock()
2336                    .drain(..)
2337                    .sorted_by_key(|r| r.start)
2338                    .collect::<Vec<_>>();
2339                assert_eq!(
2340                    ranges.len(),
2341                    2,
2342                    "Should query 2 ranges after both scrolls, but got: {ranges:?}"
2343                );
2344                let first_scroll = &ranges[0];
2345                let second_scroll = &ranges[1];
2346                assert_eq!(
2347                    first_scroll.end.line, second_scroll.start.line,
2348                    "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
2349                );
2350
2351                let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2352                assert_eq!(
2353                    lsp_requests, 3,
2354                    "Should query hints initially, and after each scroll (2 times)"
2355                );
2356                assert_eq!(
2357                    vec!["50".to_string(), "100".to_string(), "150".to_string()],
2358                    cached_hint_labels(editor, cx),
2359                    "Chunks of 50 line width should have been queried each time"
2360                );
2361                assert_eq!(
2362                    vec!["50".to_string(), "100".to_string(), "150".to_string()],
2363                    visible_hint_labels(editor, cx),
2364                    "Editor should show only hints that it's scrolled to"
2365                );
2366
2367                let mut selection_in_cached_range = visible_range_after_scrolls.end;
2368                selection_in_cached_range.row -= visible_line_count.ceil() as u32;
2369                selection_in_cached_range
2370            })
2371            .unwrap();
2372
2373        editor
2374            .update(cx, |editor, window, cx| {
2375                editor.change_selections(
2376                    SelectionEffects::scroll(Autoscroll::center()),
2377                    window,
2378                    cx,
2379                    |s| s.select_ranges([selection_in_cached_range..selection_in_cached_range]),
2380                );
2381            })
2382            .unwrap();
2383        cx.executor().advance_clock(Duration::from_millis(100));
2384        cx.executor().run_until_parked();
2385        editor.update(cx, |_, _, _| {
2386            let ranges = lsp_request_ranges
2387                .lock()
2388                .drain(..)
2389                .sorted_by_key(|r| r.start)
2390                .collect::<Vec<_>>();
2391            assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
2392            assert_eq!(lsp_request_count.load(Ordering::Acquire), 3, "No new requests should be made when selecting within cached chunks");
2393        }).unwrap();
2394
2395        editor
2396            .update(cx, |editor, window, cx| {
2397                editor.handle_input("++++more text++++", window, cx);
2398            })
2399            .unwrap();
2400        cx.executor().advance_clock(Duration::from_secs(1));
2401        cx.executor().run_until_parked();
2402        editor.update(cx, |editor, _window, cx| {
2403            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2404            ranges.sort_by_key(|r| r.start);
2405
2406            assert_eq!(ranges.len(), 2,
2407                "On edit, should scroll to selection and query a range around it: that range should split into 2 50 rows wide chunks. Instead, got query ranges {ranges:?}");
2408            let first_chunk = &ranges[0];
2409            let second_chunk = &ranges[1];
2410            assert!(first_chunk.end.line == second_chunk.start.line,
2411                "First chunk {first_chunk:?} should be before second chunk {second_chunk:?}");
2412            assert!(first_chunk.start.line < selection_in_cached_range.row,
2413                "Hints should be queried with the selected range after the query range start");
2414
2415            let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2416            assert_eq!(lsp_requests, 5, "Two chunks should be re-queried");
2417            assert_eq!(vec!["100".to_string(), "150".to_string()], cached_hint_labels(editor, cx),
2418                "Should have (less) hints from the new LSP response after the edit");
2419            assert_eq!(vec!["100".to_string(), "150".to_string()], visible_hint_labels(editor, cx), "Should show only visible hints (in the center) from the new cached set");
2420        }).unwrap();
2421    }
2422
2423    fn editor_visible_range(
2424        editor: &WindowHandle<Editor>,
2425        cx: &mut gpui::TestAppContext,
2426    ) -> Range<Point> {
2427        let ranges = editor
2428            .update(cx, |editor, _window, cx| editor.visible_buffer_ranges(cx))
2429            .unwrap();
2430        assert_eq!(
2431            ranges.len(),
2432            1,
2433            "Single buffer should produce a single excerpt with visible range"
2434        );
2435        let (buffer_snapshot, visible_range, _) = ranges.into_iter().next().unwrap();
2436        visible_range.to_point(&buffer_snapshot)
2437    }
2438
2439    #[gpui::test]
2440    async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
2441        init_test(cx, &|settings| {
2442            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2443                show_value_hints: Some(true),
2444                enabled: Some(true),
2445                edit_debounce_ms: Some(0),
2446                scroll_debounce_ms: Some(0),
2447                show_type_hints: Some(true),
2448                show_parameter_hints: Some(true),
2449                show_other_hints: Some(true),
2450                show_background: Some(false),
2451                toggle_on_modifiers_press: None,
2452            })
2453        });
2454
2455        let fs = FakeFs::new(cx.background_executor.clone());
2456        fs.insert_tree(
2457                path!("/a"),
2458                json!({
2459                    "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<String>()),
2460                    "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<String>()),
2461                }),
2462            )
2463            .await;
2464
2465        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2466
2467        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2468        let language = rust_lang();
2469        language_registry.add(language);
2470        let mut fake_servers = language_registry.register_fake_lsp(
2471            "Rust",
2472            FakeLspAdapter {
2473                capabilities: lsp::ServerCapabilities {
2474                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2475                    ..lsp::ServerCapabilities::default()
2476                },
2477                ..FakeLspAdapter::default()
2478            },
2479        );
2480
2481        let (buffer_1, _handle1) = project
2482            .update(cx, |project, cx| {
2483                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2484            })
2485            .await
2486            .unwrap();
2487        let (buffer_2, _handle2) = project
2488            .update(cx, |project, cx| {
2489                project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2490            })
2491            .await
2492            .unwrap();
2493        let multibuffer = cx.new(|cx| {
2494            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2495            multibuffer.set_excerpts_for_path(
2496                PathKey::sorted(0),
2497                buffer_1.clone(),
2498                [
2499                    Point::new(0, 0)..Point::new(2, 0),
2500                    Point::new(4, 0)..Point::new(11, 0),
2501                    Point::new(22, 0)..Point::new(33, 0),
2502                    Point::new(44, 0)..Point::new(55, 0),
2503                    Point::new(56, 0)..Point::new(66, 0),
2504                    Point::new(67, 0)..Point::new(77, 0),
2505                ],
2506                0,
2507                cx,
2508            );
2509            multibuffer.set_excerpts_for_path(
2510                PathKey::sorted(1),
2511                buffer_2.clone(),
2512                [
2513                    Point::new(0, 1)..Point::new(2, 1),
2514                    Point::new(4, 1)..Point::new(11, 1),
2515                    Point::new(22, 1)..Point::new(33, 1),
2516                    Point::new(44, 1)..Point::new(55, 1),
2517                    Point::new(56, 1)..Point::new(66, 1),
2518                    Point::new(67, 1)..Point::new(77, 1),
2519                ],
2520                0,
2521                cx,
2522            );
2523            multibuffer
2524        });
2525
2526        cx.executor().run_until_parked();
2527        let editor = cx.add_window(|window, cx| {
2528            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2529        });
2530
2531        let editor_edited = Arc::new(AtomicBool::new(false));
2532        let fake_server = fake_servers.next().await.unwrap();
2533        let closure_editor_edited = Arc::clone(&editor_edited);
2534        fake_server
2535            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2536                let task_editor_edited = Arc::clone(&closure_editor_edited);
2537                async move {
2538                    let hint_text = if params.text_document.uri
2539                        == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
2540                    {
2541                        "main hint"
2542                    } else if params.text_document.uri
2543                        == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
2544                    {
2545                        "other hint"
2546                    } else {
2547                        panic!("unexpected uri: {:?}", params.text_document.uri);
2548                    };
2549
2550                    // one hint per excerpt
2551                    let positions = [
2552                        lsp::Position::new(0, 2),
2553                        lsp::Position::new(4, 2),
2554                        lsp::Position::new(22, 2),
2555                        lsp::Position::new(44, 2),
2556                        lsp::Position::new(56, 2),
2557                        lsp::Position::new(67, 2),
2558                    ];
2559                    let out_of_range_hint = lsp::InlayHint {
2560                        position: lsp::Position::new(
2561                            params.range.start.line + 99,
2562                            params.range.start.character + 99,
2563                        ),
2564                        label: lsp::InlayHintLabel::String(
2565                            "out of excerpt range, should be ignored".to_string(),
2566                        ),
2567                        kind: None,
2568                        text_edits: None,
2569                        tooltip: None,
2570                        padding_left: None,
2571                        padding_right: None,
2572                        data: None,
2573                    };
2574
2575                    let edited = task_editor_edited.load(Ordering::Acquire);
2576                    Ok(Some(
2577                        std::iter::once(out_of_range_hint)
2578                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
2579                                lsp::InlayHint {
2580                                    position,
2581                                    label: lsp::InlayHintLabel::String(format!(
2582                                        "{hint_text}{E} #{i}",
2583                                        E = if edited { "(edited)" } else { "" },
2584                                    )),
2585                                    kind: None,
2586                                    text_edits: None,
2587                                    tooltip: None,
2588                                    padding_left: None,
2589                                    padding_right: None,
2590                                    data: None,
2591                                }
2592                            }))
2593                            .collect(),
2594                    ))
2595                }
2596            })
2597            .next()
2598            .await;
2599        cx.executor().run_until_parked();
2600
2601        editor
2602            .update(cx, |editor, _window, cx| {
2603                let expected_hints = vec![
2604                    "main hint #0".to_string(),
2605                    "main hint #1".to_string(),
2606                    "main hint #2".to_string(),
2607                    "main hint #3".to_string(),
2608                    "main hint #4".to_string(),
2609                    "main hint #5".to_string(),
2610                ];
2611                assert_eq!(
2612                    expected_hints,
2613                    sorted_cached_hint_labels(editor, cx),
2614                    "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2615                );
2616                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2617            })
2618            .unwrap();
2619
2620        editor
2621            .update(cx, |editor, window, cx| {
2622                editor.change_selections(
2623                    SelectionEffects::scroll(Autoscroll::Next),
2624                    window,
2625                    cx,
2626                    |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
2627                );
2628                editor.change_selections(
2629                    SelectionEffects::scroll(Autoscroll::Next),
2630                    window,
2631                    cx,
2632                    |s| s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]),
2633                );
2634                editor.change_selections(
2635                    SelectionEffects::scroll(Autoscroll::Next),
2636                    window,
2637                    cx,
2638                    |s| s.select_ranges([Point::new(57, 0)..Point::new(57, 0)]),
2639                );
2640            })
2641            .unwrap();
2642        cx.executor().run_until_parked();
2643        editor
2644            .update(cx, |editor, _window, cx| {
2645                let expected_hints = vec![
2646                    "main hint #0".to_string(),
2647                    "main hint #1".to_string(),
2648                    "main hint #2".to_string(),
2649                    "main hint #3".to_string(),
2650                    "main hint #4".to_string(),
2651                    "main hint #5".to_string(),
2652                ];
2653                assert_eq!(expected_hints, sorted_cached_hint_labels(editor, cx),
2654                    "New hints are not shown right after scrolling, we need to wait for the buffer to be registered");
2655                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2656            })
2657            .unwrap();
2658        cx.executor().advance_clock(Duration::from_millis(100));
2659        cx.executor().run_until_parked();
2660        editor
2661            .update(cx, |editor, _window, cx| {
2662                let expected_hints = vec![
2663                    "main hint #0".to_string(),
2664                    "main hint #1".to_string(),
2665                    "main hint #2".to_string(),
2666                    "main hint #3".to_string(),
2667                    "main hint #4".to_string(),
2668                    "main hint #5".to_string(),
2669                    "other hint #0".to_string(),
2670                    "other hint #1".to_string(),
2671                    "other hint #2".to_string(),
2672                    "other hint #3".to_string(),
2673                ];
2674                assert_eq!(
2675                    expected_hints,
2676                    sorted_cached_hint_labels(editor, cx),
2677                    "After scrolling to the new buffer and waiting for it to be registered, new hints should appear");
2678                assert_eq!(
2679                    expected_hints,
2680                    visible_hint_labels(editor, cx),
2681                    "Editor should show only visible hints",
2682                );
2683            })
2684            .unwrap();
2685
2686        editor
2687            .update(cx, |editor, window, cx| {
2688                editor.change_selections(
2689                    SelectionEffects::scroll(Autoscroll::Next),
2690                    window,
2691                    cx,
2692                    |s| s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]),
2693                );
2694            })
2695            .unwrap();
2696        cx.executor().advance_clock(Duration::from_millis(100));
2697        cx.executor().run_until_parked();
2698        editor
2699            .update(cx, |editor, _window, cx| {
2700                let expected_hints = vec![
2701                    "main hint #0".to_string(),
2702                    "main hint #1".to_string(),
2703                    "main hint #2".to_string(),
2704                    "main hint #3".to_string(),
2705                    "main hint #4".to_string(),
2706                    "main hint #5".to_string(),
2707                    "other hint #0".to_string(),
2708                    "other hint #1".to_string(),
2709                    "other hint #2".to_string(),
2710                    "other hint #3".to_string(),
2711                    "other hint #4".to_string(),
2712                    "other hint #5".to_string(),
2713                ];
2714                assert_eq!(
2715                    expected_hints,
2716                    sorted_cached_hint_labels(editor, cx),
2717                    "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"
2718                );
2719                assert_eq!(
2720                    expected_hints,
2721                    visible_hint_labels(editor, cx),
2722                    "Editor shows only hints for excerpts that were visible when scrolling"
2723                );
2724            })
2725            .unwrap();
2726
2727        editor
2728            .update(cx, |editor, window, cx| {
2729                editor.change_selections(
2730                    SelectionEffects::scroll(Autoscroll::Next),
2731                    window,
2732                    cx,
2733                    |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
2734                );
2735            })
2736            .unwrap();
2737        cx.executor().run_until_parked();
2738        editor
2739            .update(cx, |editor, _window, cx| {
2740                let expected_hints = vec![
2741                    "main hint #0".to_string(),
2742                    "main hint #1".to_string(),
2743                    "main hint #2".to_string(),
2744                    "main hint #3".to_string(),
2745                    "main hint #4".to_string(),
2746                    "main hint #5".to_string(),
2747                    "other hint #0".to_string(),
2748                    "other hint #1".to_string(),
2749                    "other hint #2".to_string(),
2750                    "other hint #3".to_string(),
2751                    "other hint #4".to_string(),
2752                    "other hint #5".to_string(),
2753                ];
2754                assert_eq!(
2755                    expected_hints,
2756                    sorted_cached_hint_labels(editor, cx),
2757                    "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"
2758                );
2759                assert_eq!(
2760                    expected_hints,
2761                    visible_hint_labels(editor, cx),
2762                );
2763            })
2764            .unwrap();
2765
2766        // We prepare to change the scrolling on edit, but do not scroll yet
2767        editor
2768            .update(cx, |editor, window, cx| {
2769                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2770                    s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
2771                });
2772            })
2773            .unwrap();
2774        cx.executor().run_until_parked();
2775        // Edit triggers the scrolling too
2776        editor_edited.store(true, Ordering::Release);
2777        editor
2778            .update(cx, |editor, window, cx| {
2779                editor.handle_input("++++more text++++", window, cx);
2780            })
2781            .unwrap();
2782        cx.executor().run_until_parked();
2783        // Wait again to trigger the inlay hints fetch on scroll
2784        cx.executor().advance_clock(Duration::from_millis(100));
2785        cx.executor().run_until_parked();
2786        editor
2787            .update(cx, |editor, _window, cx| {
2788                let expected_hints = vec![
2789                    "main hint(edited) #0".to_string(),
2790                    "main hint(edited) #1".to_string(),
2791                    "main hint(edited) #2".to_string(),
2792                    "main hint(edited) #3".to_string(),
2793                    "main hint(edited) #4".to_string(),
2794                    "main hint(edited) #5".to_string(),
2795                    "other hint(edited) #0".to_string(),
2796                    "other hint(edited) #1".to_string(),
2797                    "other hint(edited) #2".to_string(),
2798                    "other hint(edited) #3".to_string(),
2799                ];
2800                assert_eq!(
2801                    expected_hints,
2802                    sorted_cached_hint_labels(editor, cx),
2803                    "After multibuffer edit, editor gets scrolled back to the last selection; \
2804                all hints should be invalidated and required for all of its visible excerpts"
2805                );
2806                assert_eq!(
2807                    expected_hints,
2808                    visible_hint_labels(editor, cx),
2809                    "All excerpts should get their hints"
2810                );
2811            })
2812            .unwrap();
2813    }
2814
2815    #[gpui::test]
2816    async fn test_editing_in_multi_buffer(cx: &mut gpui::TestAppContext) {
2817        init_test(cx, &|settings| {
2818            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2819                enabled: Some(true),
2820                ..InlayHintSettingsContent::default()
2821            })
2822        });
2823
2824        let fs = FakeFs::new(cx.background_executor.clone());
2825        fs.insert_tree(
2826            path!("/a"),
2827            json!({
2828                "main.rs": format!("fn main() {{\n{}\n}}", (0..200).map(|i| format!("let i = {i};\n")).collect::<String>()),
2829                "lib.rs": r#"let a = 1;
2830let b = 2;
2831let c = 3;"#
2832            }),
2833        )
2834        .await;
2835
2836        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2837
2838        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2839        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2840        let language = rust_lang();
2841        language_registry.add(language);
2842
2843        let closure_ranges_fetched = lsp_request_ranges.clone();
2844        let mut fake_servers = language_registry.register_fake_lsp(
2845            "Rust",
2846            FakeLspAdapter {
2847                capabilities: lsp::ServerCapabilities {
2848                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2849                    ..lsp::ServerCapabilities::default()
2850                },
2851                initializer: Some(Box::new(move |fake_server| {
2852                    let closure_ranges_fetched = closure_ranges_fetched.clone();
2853                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2854                        move |params, _| {
2855                            let closure_ranges_fetched = closure_ranges_fetched.clone();
2856                            async move {
2857                                let prefix = if params.text_document.uri
2858                                    == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
2859                                {
2860                                    closure_ranges_fetched
2861                                        .lock()
2862                                        .push(("main.rs", params.range));
2863                                    "main.rs"
2864                                } else if params.text_document.uri
2865                                    == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
2866                                {
2867                                    closure_ranges_fetched.lock().push(("lib.rs", params.range));
2868                                    "lib.rs"
2869                                } else {
2870                                    panic!("Unexpected file path {:?}", params.text_document.uri);
2871                                };
2872                                Ok(Some(
2873                                    (params.range.start.line..params.range.end.line)
2874                                        .map(|row| lsp::InlayHint {
2875                                            position: lsp::Position::new(row, 0),
2876                                            label: lsp::InlayHintLabel::String(format!(
2877                                                "{prefix} Inlay hint #{row}"
2878                                            )),
2879                                            kind: Some(lsp::InlayHintKind::TYPE),
2880                                            text_edits: None,
2881                                            tooltip: None,
2882                                            padding_left: None,
2883                                            padding_right: None,
2884                                            data: None,
2885                                        })
2886                                        .collect(),
2887                                ))
2888                            }
2889                        },
2890                    );
2891                })),
2892                ..FakeLspAdapter::default()
2893            },
2894        );
2895
2896        let (buffer_1, _handle_1) = project
2897            .update(cx, |project, cx| {
2898                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2899            })
2900            .await
2901            .unwrap();
2902        let (buffer_2, _handle_2) = project
2903            .update(cx, |project, cx| {
2904                project.open_local_buffer_with_lsp(path!("/a/lib.rs"), cx)
2905            })
2906            .await
2907            .unwrap();
2908        let multi_buffer = cx.new(|cx| {
2909            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2910            multibuffer.set_excerpts_for_path(
2911                PathKey::sorted(0),
2912                buffer_1.clone(),
2913                [
2914                    Point::new(49, 0)..Point::new(53, 0),
2915                    Point::new(70, 0)..Point::new(73, 0),
2916                ],
2917                0,
2918                cx,
2919            );
2920            multibuffer.set_excerpts_for_path(
2921                PathKey::sorted(1),
2922                buffer_2.clone(),
2923                [Point::new(0, 0)..Point::new(4, 0)],
2924                0,
2925                cx,
2926            );
2927            multibuffer
2928        });
2929
2930        let editor = cx.add_window(|window, cx| {
2931            let mut editor =
2932                Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx);
2933            editor.change_selections(SelectionEffects::default(), window, cx, |s| {
2934                s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
2935            });
2936            editor
2937        });
2938
2939        let _fake_server = fake_servers.next().await.unwrap();
2940        cx.executor().advance_clock(Duration::from_millis(100));
2941        cx.executor().run_until_parked();
2942
2943        assert_eq!(
2944            vec![
2945                (
2946                    "lib.rs",
2947                    lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(2, 10))
2948                ),
2949                (
2950                    "main.rs",
2951                    lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(50, 0))
2952                ),
2953                (
2954                    "main.rs",
2955                    lsp::Range::new(lsp::Position::new(50, 0), lsp::Position::new(100, 0))
2956                ),
2957            ],
2958            lsp_request_ranges
2959                .lock()
2960                .drain(..)
2961                .sorted_by_key(|(prefix, r)| (prefix.to_owned(), r.start))
2962                .collect::<Vec<_>>(),
2963            "For large buffers, should query chunks that cover both visible excerpt"
2964        );
2965        editor
2966            .update(cx, |editor, _window, cx| {
2967                assert_eq!(
2968                    (0..2)
2969                        .map(|i| format!("lib.rs Inlay hint #{i}"))
2970                        .chain((0..100).map(|i| format!("main.rs Inlay hint #{i}")))
2971                        .collect::<Vec<_>>(),
2972                    sorted_cached_hint_labels(editor, cx),
2973                    "Both chunks should provide their inlay hints"
2974                );
2975                assert_eq!(
2976                    vec![
2977                        "main.rs Inlay hint #49".to_owned(),
2978                        "main.rs Inlay hint #50".to_owned(),
2979                        "main.rs Inlay hint #51".to_owned(),
2980                        "main.rs Inlay hint #52".to_owned(),
2981                        "main.rs Inlay hint #53".to_owned(),
2982                        "main.rs Inlay hint #70".to_owned(),
2983                        "main.rs Inlay hint #71".to_owned(),
2984                        "main.rs Inlay hint #72".to_owned(),
2985                        "main.rs Inlay hint #73".to_owned(),
2986                        "lib.rs Inlay hint #0".to_owned(),
2987                        "lib.rs Inlay hint #1".to_owned(),
2988                    ],
2989                    visible_hint_labels(editor, cx),
2990                    "Only hints from visible excerpt should be added into the editor"
2991                );
2992            })
2993            .unwrap();
2994
2995        editor
2996            .update(cx, |editor, window, cx| {
2997                editor.handle_input("a", window, cx);
2998            })
2999            .unwrap();
3000        cx.executor().advance_clock(Duration::from_millis(1000));
3001        cx.executor().run_until_parked();
3002        assert_eq!(
3003            vec![
3004                (
3005                    "lib.rs",
3006                    lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(2, 10))
3007                ),
3008                (
3009                    "main.rs",
3010                    lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(50, 0))
3011                ),
3012                (
3013                    "main.rs",
3014                    lsp::Range::new(lsp::Position::new(50, 0), lsp::Position::new(100, 0))
3015                ),
3016            ],
3017            lsp_request_ranges
3018                .lock()
3019                .drain(..)
3020                .sorted_by_key(|(prefix, r)| (prefix.to_owned(), r.start))
3021                .collect::<Vec<_>>(),
3022            "Same chunks should be re-queried on edit"
3023        );
3024        editor
3025            .update(cx, |editor, _window, cx| {
3026                assert_eq!(
3027                    (0..2)
3028                        .map(|i| format!("lib.rs Inlay hint #{i}"))
3029                        .chain((0..100).map(|i| format!("main.rs Inlay hint #{i}")))
3030                        .collect::<Vec<_>>(),
3031                    sorted_cached_hint_labels(editor, cx),
3032                    "Same hints should be re-inserted after the edit"
3033                );
3034                assert_eq!(
3035                    vec![
3036                        "main.rs Inlay hint #49".to_owned(),
3037                        "main.rs Inlay hint #50".to_owned(),
3038                        "main.rs Inlay hint #51".to_owned(),
3039                        "main.rs Inlay hint #52".to_owned(),
3040                        "main.rs Inlay hint #53".to_owned(),
3041                        "main.rs Inlay hint #70".to_owned(),
3042                        "main.rs Inlay hint #71".to_owned(),
3043                        "main.rs Inlay hint #72".to_owned(),
3044                        "main.rs Inlay hint #73".to_owned(),
3045                        "lib.rs Inlay hint #0".to_owned(),
3046                        "lib.rs Inlay hint #1".to_owned(),
3047                    ],
3048                    visible_hint_labels(editor, cx),
3049                    "Same hints should be re-inserted into the editor after the edit"
3050                );
3051            })
3052            .unwrap();
3053    }
3054
3055    #[gpui::test]
3056    async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
3057        init_test(cx, &|settings| {
3058            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3059                show_value_hints: Some(true),
3060                enabled: Some(true),
3061                edit_debounce_ms: Some(0),
3062                scroll_debounce_ms: Some(0),
3063                show_type_hints: Some(false),
3064                show_parameter_hints: Some(false),
3065                show_other_hints: Some(false),
3066                show_background: Some(false),
3067                toggle_on_modifiers_press: None,
3068            })
3069        });
3070
3071        let fs = FakeFs::new(cx.background_executor.clone());
3072        fs.insert_tree(
3073            path!("/a"),
3074            json!({
3075                "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<String>()),
3076                "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<String>()),
3077            }),
3078        )
3079        .await;
3080
3081        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3082
3083        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3084        language_registry.add(rust_lang());
3085        let mut fake_servers = language_registry.register_fake_lsp(
3086            "Rust",
3087            FakeLspAdapter {
3088                capabilities: lsp::ServerCapabilities {
3089                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3090                    ..lsp::ServerCapabilities::default()
3091                },
3092                ..FakeLspAdapter::default()
3093            },
3094        );
3095
3096        let (buffer_1, _handle) = project
3097            .update(cx, |project, cx| {
3098                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
3099            })
3100            .await
3101            .unwrap();
3102        let (buffer_2, _handle2) = project
3103            .update(cx, |project, cx| {
3104                project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
3105            })
3106            .await
3107            .unwrap();
3108        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
3109        multibuffer.update(cx, |multibuffer, cx| {
3110            multibuffer.set_excerpts_for_path(
3111                PathKey::sorted(0),
3112                buffer_1.clone(),
3113                [Point::new(0, 0)..Point::new(2, 0)],
3114                0,
3115                cx,
3116            );
3117            multibuffer.set_excerpts_for_path(
3118                PathKey::sorted(1),
3119                buffer_2.clone(),
3120                [Point::new(0, 1)..Point::new(2, 1)],
3121                0,
3122                cx,
3123            );
3124        });
3125
3126        cx.executor().run_until_parked();
3127        let editor = cx.add_window(|window, cx| {
3128            Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
3129        });
3130        let editor_edited = Arc::new(AtomicBool::new(false));
3131        let fake_server = fake_servers.next().await.unwrap();
3132        let closure_editor_edited = Arc::clone(&editor_edited);
3133        fake_server
3134            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
3135                let task_editor_edited = Arc::clone(&closure_editor_edited);
3136                async move {
3137                    let hint_text = if params.text_document.uri
3138                        == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
3139                    {
3140                        "main hint"
3141                    } else if params.text_document.uri
3142                        == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
3143                    {
3144                        "other hint"
3145                    } else {
3146                        panic!("unexpected uri: {:?}", params.text_document.uri);
3147                    };
3148
3149                    let positions = [
3150                        lsp::Position::new(0, 2),
3151                        lsp::Position::new(4, 2),
3152                        lsp::Position::new(22, 2),
3153                        lsp::Position::new(44, 2),
3154                        lsp::Position::new(56, 2),
3155                        lsp::Position::new(67, 2),
3156                    ];
3157                    let out_of_range_hint = lsp::InlayHint {
3158                        position: lsp::Position::new(
3159                            params.range.start.line + 99,
3160                            params.range.start.character + 99,
3161                        ),
3162                        label: lsp::InlayHintLabel::String(
3163                            "out of excerpt range, should be ignored".to_string(),
3164                        ),
3165                        kind: None,
3166                        text_edits: None,
3167                        tooltip: None,
3168                        padding_left: None,
3169                        padding_right: None,
3170                        data: None,
3171                    };
3172
3173                    let edited = task_editor_edited.load(Ordering::Acquire);
3174                    Ok(Some(
3175                        std::iter::once(out_of_range_hint)
3176                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
3177                                lsp::InlayHint {
3178                                    position,
3179                                    label: lsp::InlayHintLabel::String(format!(
3180                                        "{hint_text}{} #{i}",
3181                                        if edited { "(edited)" } else { "" },
3182                                    )),
3183                                    kind: None,
3184                                    text_edits: None,
3185                                    tooltip: None,
3186                                    padding_left: None,
3187                                    padding_right: None,
3188                                    data: None,
3189                                }
3190                            }))
3191                            .collect(),
3192                    ))
3193                }
3194            })
3195            .next()
3196            .await;
3197        cx.executor().advance_clock(Duration::from_millis(100));
3198        cx.executor().run_until_parked();
3199        editor
3200            .update(cx, |editor, _, cx| {
3201                assert_eq!(
3202                    vec![
3203                        "main hint #0".to_string(),
3204                        "main hint #1".to_string(),
3205                        "main hint #2".to_string(),
3206                        "main hint #3".to_string(),
3207                        "other hint #0".to_string(),
3208                        "other hint #1".to_string(),
3209                        "other hint #2".to_string(),
3210                        "other hint #3".to_string(),
3211                    ],
3212                    sorted_cached_hint_labels(editor, cx),
3213                    "Cache should update for both excerpts despite hints display was disabled; after selecting 2nd buffer, it's now registered with the langserever and should get its hints"
3214                );
3215                assert_eq!(
3216                    Vec::<String>::new(),
3217                    visible_hint_labels(editor, cx),
3218                    "All hints are disabled and should not be shown despite being present in the cache"
3219                );
3220            })
3221            .unwrap();
3222
3223        editor
3224            .update(cx, |editor, _, cx| {
3225                editor.buffer().update(cx, |multibuffer, cx| {
3226                    multibuffer.remove_excerpts(PathKey::sorted(1), cx);
3227                })
3228            })
3229            .unwrap();
3230        cx.executor().run_until_parked();
3231        editor
3232            .update(cx, |editor, _, cx| {
3233                assert_eq!(
3234                    vec![
3235                        "main hint #0".to_string(),
3236                        "main hint #1".to_string(),
3237                        "main hint #2".to_string(),
3238                        "main hint #3".to_string(),
3239                    ],
3240                    cached_hint_labels(editor, cx),
3241                    "For the removed excerpt, should clean corresponding cached hints as its buffer was dropped"
3242                );
3243                assert!(
3244                visible_hint_labels(editor, cx).is_empty(),
3245                "All hints are disabled and should not be shown despite being present in the cache"
3246            );
3247            })
3248            .unwrap();
3249
3250        update_test_language_settings(cx, &|settings| {
3251            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3252                show_value_hints: Some(true),
3253                enabled: Some(true),
3254                edit_debounce_ms: Some(0),
3255                scroll_debounce_ms: Some(0),
3256                show_type_hints: Some(true),
3257                show_parameter_hints: Some(true),
3258                show_other_hints: Some(true),
3259                show_background: Some(false),
3260                toggle_on_modifiers_press: None,
3261            })
3262        });
3263        cx.executor().run_until_parked();
3264        editor
3265            .update(cx, |editor, _, cx| {
3266                assert_eq!(
3267                    vec![
3268                        "main hint #0".to_string(),
3269                        "main hint #1".to_string(),
3270                        "main hint #2".to_string(),
3271                        "main hint #3".to_string(),
3272                    ],
3273                    cached_hint_labels(editor, cx),
3274                    "Hint display settings change should not change the cache"
3275                );
3276                assert_eq!(
3277                    vec![
3278                        "main hint #0".to_string(),
3279                    ],
3280                    visible_hint_labels(editor, cx),
3281                    "Settings change should make cached hints visible, but only the visible ones, from the remaining excerpt"
3282                );
3283            })
3284            .unwrap();
3285    }
3286
3287    #[gpui::test]
3288    async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
3289        init_test(cx, &|settings| {
3290            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3291                show_value_hints: Some(true),
3292                enabled: Some(true),
3293                edit_debounce_ms: Some(0),
3294                scroll_debounce_ms: Some(0),
3295                show_type_hints: Some(true),
3296                show_parameter_hints: Some(true),
3297                show_other_hints: Some(true),
3298                show_background: Some(false),
3299                toggle_on_modifiers_press: None,
3300            })
3301        });
3302
3303        let fs = FakeFs::new(cx.background_executor.clone());
3304        fs.insert_tree(
3305            path!("/a"),
3306            json!({
3307                "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)),
3308                "other.rs": "// Test file",
3309            }),
3310        )
3311        .await;
3312
3313        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3314
3315        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3316        language_registry.add(rust_lang());
3317        language_registry.register_fake_lsp(
3318            "Rust",
3319            FakeLspAdapter {
3320                capabilities: lsp::ServerCapabilities {
3321                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3322                    ..lsp::ServerCapabilities::default()
3323                },
3324                initializer: Some(Box::new(move |fake_server| {
3325                    let lsp_request_count = Arc::new(AtomicU32::new(0));
3326                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3327                        move |params, _| {
3328                            let i = lsp_request_count.fetch_add(1, Ordering::Release) + 1;
3329                            async move {
3330                                assert_eq!(
3331                                    params.text_document.uri,
3332                                    lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3333                                );
3334                                let query_start = params.range.start;
3335                                Ok(Some(vec![lsp::InlayHint {
3336                                    position: query_start,
3337                                    label: lsp::InlayHintLabel::String(i.to_string()),
3338                                    kind: None,
3339                                    text_edits: None,
3340                                    tooltip: None,
3341                                    padding_left: None,
3342                                    padding_right: None,
3343                                    data: None,
3344                                }]))
3345                            }
3346                        },
3347                    );
3348                })),
3349                ..FakeLspAdapter::default()
3350            },
3351        );
3352
3353        let buffer = project
3354            .update(cx, |project, cx| {
3355                project.open_local_buffer(path!("/a/main.rs"), cx)
3356            })
3357            .await
3358            .unwrap();
3359        let editor =
3360            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3361
3362        // Allow LSP to initialize
3363        cx.executor().run_until_parked();
3364
3365        // Establish a viewport and explicitly trigger hint refresh.
3366        // This ensures we control exactly when hints are requested.
3367        editor
3368            .update(cx, |editor, window, cx| {
3369                editor.set_visible_line_count(50.0, window, cx);
3370                editor.set_visible_column_count(120.0);
3371                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
3372            })
3373            .unwrap();
3374
3375        // Allow LSP initialization and hint request/response to complete.
3376        // Use multiple advance_clock + run_until_parked cycles to ensure all async work completes.
3377        for _ in 0..5 {
3378            cx.executor().advance_clock(Duration::from_millis(100));
3379            cx.executor().run_until_parked();
3380        }
3381
3382        // At this point we should have exactly one hint from our explicit refresh.
3383        // The test verifies that hints at character boundaries are handled correctly.
3384        editor
3385            .update(cx, |editor, _, cx| {
3386                assert!(
3387                    !cached_hint_labels(editor, cx).is_empty(),
3388                    "Should have at least one hint after refresh"
3389                );
3390                assert!(
3391                    !visible_hint_labels(editor, cx).is_empty(),
3392                    "Should have at least one visible hint"
3393                );
3394            })
3395            .unwrap();
3396    }
3397
3398    #[gpui::test]
3399    async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
3400        init_test(cx, &|settings| {
3401            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3402                show_value_hints: Some(true),
3403                enabled: Some(false),
3404                edit_debounce_ms: Some(0),
3405                scroll_debounce_ms: Some(0),
3406                show_type_hints: Some(true),
3407                show_parameter_hints: Some(true),
3408                show_other_hints: Some(true),
3409                show_background: Some(false),
3410                toggle_on_modifiers_press: None,
3411            })
3412        });
3413
3414        let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3415            let lsp_request_count = Arc::new(AtomicU32::new(0));
3416            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3417                move |params, _| {
3418                    let lsp_request_count = lsp_request_count.clone();
3419                    async move {
3420                        assert_eq!(
3421                            params.text_document.uri,
3422                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
3423                        );
3424
3425                        let i = lsp_request_count.fetch_add(1, Ordering::AcqRel) + 1;
3426                        Ok(Some(vec![lsp::InlayHint {
3427                            position: lsp::Position::new(0, i),
3428                            label: lsp::InlayHintLabel::String(i.to_string()),
3429                            kind: None,
3430                            text_edits: None,
3431                            tooltip: None,
3432                            padding_left: None,
3433                            padding_right: None,
3434                            data: None,
3435                        }]))
3436                    }
3437                },
3438            );
3439        })
3440        .await;
3441
3442        editor
3443            .update(cx, |editor, window, cx| {
3444                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3445            })
3446            .unwrap();
3447
3448        cx.executor().run_until_parked();
3449        editor
3450            .update(cx, |editor, _, cx| {
3451                let expected_hints = vec!["1".to_string()];
3452                assert_eq!(
3453                    expected_hints,
3454                    cached_hint_labels(editor, cx),
3455                    "Should display inlays after toggle despite them disabled in settings"
3456                );
3457                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3458            })
3459            .unwrap();
3460
3461        editor
3462            .update(cx, |editor, window, cx| {
3463                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3464            })
3465            .unwrap();
3466        cx.executor().run_until_parked();
3467        editor
3468            .update(cx, |editor, _, cx| {
3469                assert_eq!(
3470                    vec!["1".to_string()],
3471                    cached_hint_labels(editor, cx),
3472                    "Cache does not change because of toggles in the editor"
3473                );
3474                assert_eq!(
3475                    Vec::<String>::new(),
3476                    visible_hint_labels(editor, cx),
3477                    "Should clear hints after 2nd toggle"
3478                );
3479            })
3480            .unwrap();
3481
3482        update_test_language_settings(cx, &|settings| {
3483            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3484                show_value_hints: Some(true),
3485                enabled: Some(true),
3486                edit_debounce_ms: Some(0),
3487                scroll_debounce_ms: Some(0),
3488                show_type_hints: Some(true),
3489                show_parameter_hints: Some(true),
3490                show_other_hints: Some(true),
3491                show_background: Some(false),
3492                toggle_on_modifiers_press: None,
3493            })
3494        });
3495        cx.executor().run_until_parked();
3496        editor
3497            .update(cx, |editor, _, cx| {
3498                let expected_hints = vec!["1".to_string()];
3499                assert_eq!(
3500                    expected_hints,
3501                    cached_hint_labels(editor, cx),
3502                    "Should not query LSP hints after enabling hints in settings, as file version is the same"
3503                );
3504                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3505            })
3506            .unwrap();
3507
3508        editor
3509            .update(cx, |editor, window, cx| {
3510                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3511            })
3512            .unwrap();
3513        cx.executor().run_until_parked();
3514        editor
3515            .update(cx, |editor, _, cx| {
3516                assert_eq!(
3517                    vec!["1".to_string()],
3518                    cached_hint_labels(editor, cx),
3519                    "Cache does not change because of toggles in the editor"
3520                );
3521                assert_eq!(
3522                    Vec::<String>::new(),
3523                    visible_hint_labels(editor, cx),
3524                    "Should clear hints after enabling in settings and a 3rd toggle"
3525                );
3526            })
3527            .unwrap();
3528
3529        editor
3530            .update(cx, |editor, window, cx| {
3531                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3532            })
3533            .unwrap();
3534        cx.executor().run_until_parked();
3535        editor.update(cx, |editor, _, cx| {
3536            let expected_hints = vec!["1".to_string()];
3537            assert_eq!(
3538                expected_hints,
3539                cached_hint_labels(editor,cx),
3540                "Should not query LSP hints after enabling hints in settings and toggling them back on"
3541            );
3542            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3543        }).unwrap();
3544    }
3545
3546    #[gpui::test]
3547    async fn test_modifiers_change(cx: &mut gpui::TestAppContext) {
3548        init_test(cx, &|settings| {
3549            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3550                show_value_hints: Some(true),
3551                enabled: Some(true),
3552                edit_debounce_ms: Some(0),
3553                scroll_debounce_ms: Some(0),
3554                show_type_hints: Some(true),
3555                show_parameter_hints: Some(true),
3556                show_other_hints: Some(true),
3557                show_background: Some(false),
3558                toggle_on_modifiers_press: None,
3559            })
3560        });
3561
3562        let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3563            let lsp_request_count = Arc::new(AtomicU32::new(0));
3564            fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3565                move |params, _| {
3566                    let lsp_request_count = lsp_request_count.clone();
3567                    async move {
3568                        assert_eq!(
3569                            params.text_document.uri,
3570                            lsp::Uri::from_file_path(file_with_hints).unwrap(),
3571                        );
3572
3573                        let i = lsp_request_count.fetch_add(1, Ordering::AcqRel) + 1;
3574                        Ok(Some(vec![lsp::InlayHint {
3575                            position: lsp::Position::new(0, i),
3576                            label: lsp::InlayHintLabel::String(i.to_string()),
3577                            kind: None,
3578                            text_edits: None,
3579                            tooltip: None,
3580                            padding_left: None,
3581                            padding_right: None,
3582                            data: None,
3583                        }]))
3584                    }
3585                },
3586            );
3587        })
3588        .await;
3589
3590        cx.executor().run_until_parked();
3591        editor
3592            .update(cx, |editor, _, cx| {
3593                assert_eq!(
3594                    vec!["1".to_string()],
3595                    cached_hint_labels(editor, cx),
3596                    "Should display inlays after toggle despite them disabled in settings"
3597                );
3598                assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx));
3599            })
3600            .unwrap();
3601
3602        editor
3603            .update(cx, |editor, _, cx| {
3604                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3605            })
3606            .unwrap();
3607        cx.executor().run_until_parked();
3608        editor
3609            .update(cx, |editor, _, cx| {
3610                assert_eq!(
3611                    vec!["1".to_string()],
3612                    cached_hint_labels(editor, cx),
3613                    "Nothing happens with the cache on modifiers change"
3614                );
3615                assert_eq!(
3616                    Vec::<String>::new(),
3617                    visible_hint_labels(editor, cx),
3618                    "On modifiers change and hints toggled on, should hide editor inlays"
3619                );
3620            })
3621            .unwrap();
3622        editor
3623            .update(cx, |editor, _, cx| {
3624                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3625            })
3626            .unwrap();
3627        cx.executor().run_until_parked();
3628        editor
3629            .update(cx, |editor, _, cx| {
3630                assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3631                assert_eq!(
3632                    Vec::<String>::new(),
3633                    visible_hint_labels(editor, cx),
3634                    "Nothing changes on consequent modifiers change of the same kind"
3635                );
3636            })
3637            .unwrap();
3638
3639        editor
3640            .update(cx, |editor, _, cx| {
3641                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3642            })
3643            .unwrap();
3644        cx.executor().run_until_parked();
3645        editor
3646            .update(cx, |editor, _, cx| {
3647                assert_eq!(
3648                    vec!["1".to_string()],
3649                    cached_hint_labels(editor, cx),
3650                    "When modifiers change is off, no extra requests are sent"
3651                );
3652                assert_eq!(
3653                    vec!["1".to_string()],
3654                    visible_hint_labels(editor, cx),
3655                    "When modifiers change is off, hints are back into the editor"
3656                );
3657            })
3658            .unwrap();
3659        editor
3660            .update(cx, |editor, _, cx| {
3661                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3662            })
3663            .unwrap();
3664        cx.executor().run_until_parked();
3665        editor
3666            .update(cx, |editor, _, cx| {
3667                assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3668                assert_eq!(
3669                    vec!["1".to_string()],
3670                    visible_hint_labels(editor, cx),
3671                    "Nothing changes on consequent modifiers change of the same kind (2)"
3672                );
3673            })
3674            .unwrap();
3675
3676        editor
3677            .update(cx, |editor, window, cx| {
3678                editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3679            })
3680            .unwrap();
3681        cx.executor().run_until_parked();
3682        editor
3683            .update(cx, |editor, _, cx| {
3684                assert_eq!(
3685                    vec!["1".to_string()],
3686                    cached_hint_labels(editor, cx),
3687                    "Nothing happens with the cache on modifiers change"
3688                );
3689                assert_eq!(
3690                    Vec::<String>::new(),
3691                    visible_hint_labels(editor, cx),
3692                    "When toggled off, should hide editor inlays"
3693                );
3694            })
3695            .unwrap();
3696
3697        editor
3698            .update(cx, |editor, _, cx| {
3699                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3700            })
3701            .unwrap();
3702        cx.executor().run_until_parked();
3703        editor
3704            .update(cx, |editor, _, cx| {
3705                assert_eq!(
3706                    vec!["1".to_string()],
3707                    cached_hint_labels(editor, cx),
3708                    "Nothing happens with the cache on modifiers change"
3709                );
3710                assert_eq!(
3711                    vec!["1".to_string()],
3712                    visible_hint_labels(editor, cx),
3713                    "On modifiers change & hints toggled off, should show editor inlays"
3714                );
3715            })
3716            .unwrap();
3717        editor
3718            .update(cx, |editor, _, cx| {
3719                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3720            })
3721            .unwrap();
3722        cx.executor().run_until_parked();
3723        editor
3724            .update(cx, |editor, _, cx| {
3725                assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3726                assert_eq!(
3727                    vec!["1".to_string()],
3728                    visible_hint_labels(editor, cx),
3729                    "Nothing changes on consequent modifiers change of the same kind"
3730                );
3731            })
3732            .unwrap();
3733
3734        editor
3735            .update(cx, |editor, _, cx| {
3736                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3737            })
3738            .unwrap();
3739        cx.executor().run_until_parked();
3740        editor
3741            .update(cx, |editor, _, cx| {
3742                assert_eq!(
3743                    vec!["1".to_string()],
3744                    cached_hint_labels(editor, cx),
3745                    "When modifiers change is off, no extra requests are sent"
3746                );
3747                assert_eq!(
3748                    Vec::<String>::new(),
3749                    visible_hint_labels(editor, cx),
3750                    "When modifiers change is off, editor hints are back into their toggled off state"
3751                );
3752            })
3753            .unwrap();
3754        editor
3755            .update(cx, |editor, _, cx| {
3756                editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3757            })
3758            .unwrap();
3759        cx.executor().run_until_parked();
3760        editor
3761            .update(cx, |editor, _, cx| {
3762                assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3763                assert_eq!(
3764                    Vec::<String>::new(),
3765                    visible_hint_labels(editor, cx),
3766                    "Nothing changes on consequent modifiers change of the same kind (3)"
3767                );
3768            })
3769            .unwrap();
3770    }
3771
3772    #[gpui::test]
3773    async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) {
3774        init_test(cx, &|settings| {
3775            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3776                show_value_hints: Some(true),
3777                enabled: Some(true),
3778                edit_debounce_ms: Some(0),
3779                scroll_debounce_ms: Some(0),
3780                show_type_hints: Some(true),
3781                show_parameter_hints: Some(true),
3782                show_other_hints: Some(true),
3783                show_background: Some(false),
3784                toggle_on_modifiers_press: None,
3785            })
3786        });
3787
3788        let fs = FakeFs::new(cx.background_executor.clone());
3789        fs.insert_tree(
3790            path!("/a"),
3791            json!({
3792                "main.rs": "fn main() {
3793                    let x = 42;
3794                    std::thread::scope(|s| {
3795                        s.spawn(|| {
3796                            let _x = x;
3797                        });
3798                    });
3799                }",
3800                "other.rs": "// Test file",
3801            }),
3802        )
3803        .await;
3804
3805        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3806
3807        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3808        language_registry.add(rust_lang());
3809        language_registry.register_fake_lsp(
3810            "Rust",
3811            FakeLspAdapter {
3812                capabilities: lsp::ServerCapabilities {
3813                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3814                    ..Default::default()
3815                },
3816                initializer: Some(Box::new(move |fake_server| {
3817                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3818                        move |params, _| async move {
3819                            assert_eq!(
3820                                params.text_document.uri,
3821                                lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3822                            );
3823                            Ok(Some(
3824                                serde_json::from_value(json!([
3825                                    {
3826                                        "position": {
3827                                            "line": 3,
3828                                            "character": 16
3829                                        },
3830                                        "label": "move",
3831                                        "paddingLeft": false,
3832                                        "paddingRight": false
3833                                    },
3834                                    {
3835                                        "position": {
3836                                            "line": 3,
3837                                            "character": 16
3838                                        },
3839                                        "label": "(",
3840                                        "paddingLeft": false,
3841                                        "paddingRight": false
3842                                    },
3843                                    {
3844                                        "position": {
3845                                            "line": 3,
3846                                            "character": 16
3847                                        },
3848                                        "label": [
3849                                            {
3850                                                "value": "&x"
3851                                            }
3852                                        ],
3853                                        "paddingLeft": false,
3854                                        "paddingRight": false,
3855                                        "data": {
3856                                            "file_id": 0
3857                                        }
3858                                    },
3859                                    {
3860                                        "position": {
3861                                            "line": 3,
3862                                            "character": 16
3863                                        },
3864                                        "label": ")",
3865                                        "paddingLeft": false,
3866                                        "paddingRight": true
3867                                    },
3868                                    // not a correct syntax, but checks that same symbols at the same place
3869                                    // are not deduplicated
3870                                    {
3871                                        "position": {
3872                                            "line": 3,
3873                                            "character": 16
3874                                        },
3875                                        "label": ")",
3876                                        "paddingLeft": false,
3877                                        "paddingRight": true
3878                                    },
3879                                ]))
3880                                .unwrap(),
3881                            ))
3882                        },
3883                    );
3884                })),
3885                ..FakeLspAdapter::default()
3886            },
3887        );
3888
3889        let buffer = project
3890            .update(cx, |project, cx| {
3891                project.open_local_buffer(path!("/a/main.rs"), cx)
3892            })
3893            .await
3894            .unwrap();
3895
3896        // Use a VisualTestContext and explicitly establish a viewport on the editor (the production
3897        // trigger for `NewLinesShown` / inlay hint refresh) by setting visible line/column counts.
3898        let (editor_entity, cx) =
3899            cx.add_window_view(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3900
3901        editor_entity.update_in(cx, |editor, window, cx| {
3902            // Establish a viewport. The exact values are not important for this test; we just need
3903            // the editor to consider itself visible so the refresh pipeline runs.
3904            editor.set_visible_line_count(50.0, window, cx);
3905            editor.set_visible_column_count(120.0);
3906
3907            // Explicitly trigger a refresh now that the viewport exists.
3908            editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
3909        });
3910        cx.executor().run_until_parked();
3911
3912        editor_entity.update_in(cx, |editor, window, cx| {
3913            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3914                s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3915            });
3916        });
3917        cx.executor().run_until_parked();
3918
3919        // Allow any async inlay hint request/response work to complete.
3920        cx.executor().advance_clock(Duration::from_millis(100));
3921        cx.executor().run_until_parked();
3922
3923        editor_entity.update(cx, |editor, cx| {
3924            let expected_hints = vec![
3925                "move".to_string(),
3926                "(".to_string(),
3927                "&x".to_string(),
3928                ") ".to_string(),
3929                ") ".to_string(),
3930            ];
3931            assert_eq!(
3932                expected_hints,
3933                cached_hint_labels(editor, cx),
3934                "Editor inlay hints should repeat server's order when placed at the same spot"
3935            );
3936            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3937        });
3938    }
3939
3940    #[gpui::test]
3941    async fn test_invalidation_and_addition_race(cx: &mut gpui::TestAppContext) {
3942        init_test(cx, &|settings| {
3943            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3944                enabled: Some(true),
3945                ..InlayHintSettingsContent::default()
3946            })
3947        });
3948
3949        let fs = FakeFs::new(cx.background_executor.clone());
3950        fs.insert_tree(
3951            path!("/a"),
3952            json!({
3953                "main.rs": r#"fn main() {
3954                    let x = 1;
3955                    ////
3956                    ////
3957                    ////
3958                    ////
3959                    ////
3960                    ////
3961                    ////
3962                    ////
3963                    ////
3964                    ////
3965                    ////
3966                    ////
3967                    ////
3968                    ////
3969                    ////
3970                    ////
3971                    ////
3972                    let x = "2";
3973                }
3974"#,
3975                "lib.rs": r#"fn aaa() {
3976                    let aa = 22;
3977                }
3978                //
3979                //
3980                //
3981                //
3982                //
3983                //
3984                //
3985                //
3986                //
3987                //
3988                //
3989                //
3990                //
3991                //
3992                //
3993                //
3994                //
3995                //
3996                //
3997                //
3998                //
3999                //
4000                //
4001                //
4002
4003                fn bb() {
4004                    let bb = 33;
4005                }
4006"#
4007            }),
4008        )
4009        .await;
4010
4011        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
4012        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4013        let language = rust_lang();
4014        language_registry.add(language);
4015
4016        let requests_count = Arc::new(AtomicUsize::new(0));
4017        let closure_requests_count = requests_count.clone();
4018        let mut fake_servers = language_registry.register_fake_lsp(
4019            "Rust",
4020            FakeLspAdapter {
4021                name: "rust-analyzer",
4022                capabilities: lsp::ServerCapabilities {
4023                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4024                    ..lsp::ServerCapabilities::default()
4025                },
4026                initializer: Some(Box::new(move |fake_server| {
4027                    let requests_count = closure_requests_count.clone();
4028                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
4029                        move |params, _| {
4030                            let requests_count = requests_count.clone();
4031                            async move {
4032                                requests_count.fetch_add(1, Ordering::Release);
4033                                if params.text_document.uri
4034                                    == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
4035                                {
4036                                    Ok(Some(vec![
4037                                        lsp::InlayHint {
4038                                            position: lsp::Position::new(1, 9),
4039                                            label: lsp::InlayHintLabel::String(": i32".to_owned()),
4040                                            kind: Some(lsp::InlayHintKind::TYPE),
4041                                            text_edits: None,
4042                                            tooltip: None,
4043                                            padding_left: None,
4044                                            padding_right: None,
4045                                            data: None,
4046                                        },
4047                                        lsp::InlayHint {
4048                                            position: lsp::Position::new(19, 9),
4049                                            label: lsp::InlayHintLabel::String(": i33".to_owned()),
4050                                            kind: Some(lsp::InlayHintKind::TYPE),
4051                                            text_edits: None,
4052                                            tooltip: None,
4053                                            padding_left: None,
4054                                            padding_right: None,
4055                                            data: None,
4056                                        },
4057                                    ]))
4058                                } else if params.text_document.uri
4059                                    == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
4060                                {
4061                                    Ok(Some(vec![
4062                                        lsp::InlayHint {
4063                                            position: lsp::Position::new(1, 10),
4064                                            label: lsp::InlayHintLabel::String(": i34".to_owned()),
4065                                            kind: Some(lsp::InlayHintKind::TYPE),
4066                                            text_edits: None,
4067                                            tooltip: None,
4068                                            padding_left: None,
4069                                            padding_right: None,
4070                                            data: None,
4071                                        },
4072                                        lsp::InlayHint {
4073                                            position: lsp::Position::new(29, 10),
4074                                            label: lsp::InlayHintLabel::String(": i35".to_owned()),
4075                                            kind: Some(lsp::InlayHintKind::TYPE),
4076                                            text_edits: None,
4077                                            tooltip: None,
4078                                            padding_left: None,
4079                                            padding_right: None,
4080                                            data: None,
4081                                        },
4082                                    ]))
4083                                } else {
4084                                    panic!("Unexpected file path {:?}", params.text_document.uri);
4085                                }
4086                            }
4087                        },
4088                    );
4089                })),
4090                ..FakeLspAdapter::default()
4091            },
4092        );
4093
4094        // Add another server that does send the same, duplicate hints back
4095        let mut fake_servers_2 = language_registry.register_fake_lsp(
4096            "Rust",
4097            FakeLspAdapter {
4098                name: "CrabLang-ls",
4099                capabilities: lsp::ServerCapabilities {
4100                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4101                    ..lsp::ServerCapabilities::default()
4102                },
4103                initializer: Some(Box::new(move |fake_server| {
4104                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
4105                        move |params, _| async move {
4106                            if params.text_document.uri
4107                                == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
4108                            {
4109                                Ok(Some(vec![
4110                                    lsp::InlayHint {
4111                                        position: lsp::Position::new(1, 9),
4112                                        label: lsp::InlayHintLabel::String(": i32".to_owned()),
4113                                        kind: Some(lsp::InlayHintKind::TYPE),
4114                                        text_edits: None,
4115                                        tooltip: None,
4116                                        padding_left: None,
4117                                        padding_right: None,
4118                                        data: None,
4119                                    },
4120                                    lsp::InlayHint {
4121                                        position: lsp::Position::new(19, 9),
4122                                        label: lsp::InlayHintLabel::String(": i33".to_owned()),
4123                                        kind: Some(lsp::InlayHintKind::TYPE),
4124                                        text_edits: None,
4125                                        tooltip: None,
4126                                        padding_left: None,
4127                                        padding_right: None,
4128                                        data: None,
4129                                    },
4130                                ]))
4131                            } else if params.text_document.uri
4132                                == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
4133                            {
4134                                Ok(Some(vec![
4135                                    lsp::InlayHint {
4136                                        position: lsp::Position::new(1, 10),
4137                                        label: lsp::InlayHintLabel::String(": i34".to_owned()),
4138                                        kind: Some(lsp::InlayHintKind::TYPE),
4139                                        text_edits: None,
4140                                        tooltip: None,
4141                                        padding_left: None,
4142                                        padding_right: None,
4143                                        data: None,
4144                                    },
4145                                    lsp::InlayHint {
4146                                        position: lsp::Position::new(29, 10),
4147                                        label: lsp::InlayHintLabel::String(": i35".to_owned()),
4148                                        kind: Some(lsp::InlayHintKind::TYPE),
4149                                        text_edits: None,
4150                                        tooltip: None,
4151                                        padding_left: None,
4152                                        padding_right: None,
4153                                        data: None,
4154                                    },
4155                                ]))
4156                            } else {
4157                                panic!("Unexpected file path {:?}", params.text_document.uri);
4158                            }
4159                        },
4160                    );
4161                })),
4162                ..FakeLspAdapter::default()
4163            },
4164        );
4165
4166        let (buffer_1, _handle_1) = project
4167            .update(cx, |project, cx| {
4168                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
4169            })
4170            .await
4171            .unwrap();
4172        let (buffer_2, _handle_2) = project
4173            .update(cx, |project, cx| {
4174                project.open_local_buffer_with_lsp(path!("/a/lib.rs"), cx)
4175            })
4176            .await
4177            .unwrap();
4178        let multi_buffer = cx.new(|cx| {
4179            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
4180            multibuffer.set_excerpts_for_path(
4181                PathKey::sorted(0),
4182                buffer_2.clone(),
4183                [
4184                    Point::new(0, 0)..Point::new(10, 0),
4185                    Point::new(23, 0)..Point::new(34, 0),
4186                ],
4187                0,
4188                cx,
4189            );
4190            multibuffer.set_excerpts_for_path(
4191                PathKey::sorted(1),
4192                buffer_1.clone(),
4193                [
4194                    Point::new(0, 0)..Point::new(10, 0),
4195                    Point::new(13, 0)..Point::new(23, 0),
4196                ],
4197                0,
4198                cx,
4199            );
4200            multibuffer
4201        });
4202
4203        let editor = cx.add_window(|window, cx| {
4204            let mut editor =
4205                Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx);
4206            editor.change_selections(SelectionEffects::default(), window, cx, |s| {
4207                s.select_ranges([Point::new(3, 3)..Point::new(3, 3)])
4208            });
4209            editor
4210        });
4211
4212        let fake_server = fake_servers.next().await.unwrap();
4213        let _fake_server_2 = fake_servers_2.next().await.unwrap();
4214        cx.executor().advance_clock(Duration::from_millis(100));
4215        cx.executor().run_until_parked();
4216
4217        editor
4218            .update(cx, |editor, _window, cx| {
4219                assert_eq!(
4220                    vec![
4221                        ": i32".to_string(),
4222                        ": i32".to_string(),
4223                        ": i33".to_string(),
4224                        ": i33".to_string(),
4225                        ": i34".to_string(),
4226                        ": i34".to_string(),
4227                        ": i35".to_string(),
4228                        ": i35".to_string(),
4229                    ],
4230                    sorted_cached_hint_labels(editor, cx),
4231                    "We receive duplicate hints from 2 servers and cache them all"
4232                );
4233                assert_eq!(
4234                    vec![
4235                        ": i34".to_string(),
4236                        ": i35".to_string(),
4237                        ": i32".to_string(),
4238                        ": i33".to_string(),
4239                    ],
4240                    visible_hint_labels(editor, cx),
4241                    "lib.rs is added before main.rs , so its excerpts should be visible first; hints should be deduplicated per label"
4242                );
4243            })
4244            .unwrap();
4245        assert_eq!(
4246            requests_count.load(Ordering::Acquire),
4247            2,
4248            "Should have queried hints once per each file"
4249        );
4250
4251        // Scroll all the way down so the 1st buffer is out of sight.
4252        // The selection is on the 1st buffer still.
4253        editor
4254            .update(cx, |editor, window, cx| {
4255                editor.scroll_screen(&ScrollAmount::Line(88.0), window, cx);
4256            })
4257            .unwrap();
4258        // Emulate a language server refresh request, coming in the background..
4259        let refresh_request = fake_server
4260            .request::<lsp::request::InlayHintRefreshRequest>((), lsp::DEFAULT_LSP_REQUEST_TIMEOUT);
4261        // Edit the 1st buffer while scrolled down and not seeing that.
4262        // The edit will auto scroll to the edit (1st buffer).
4263        editor
4264            .update(cx, |editor, window, cx| {
4265                editor.handle_input("a", window, cx);
4266            })
4267            .unwrap();
4268        // Add more racy additive hint tasks.
4269        editor
4270            .update(cx, |editor, window, cx| {
4271                editor.scroll_screen(&ScrollAmount::Line(0.2), window, cx);
4272            })
4273            .unwrap();
4274
4275        cx.executor().advance_clock(Duration::from_millis(1000));
4276        cx.executor().run_until_parked();
4277        refresh_request.await.into_response().unwrap();
4278        editor
4279            .update(cx, |editor, _window, cx| {
4280                assert_eq!(
4281                    vec![
4282                        ": i32".to_string(),
4283                        ": i32".to_string(),
4284                        ": i33".to_string(),
4285                        ": i33".to_string(),
4286                        ": i34".to_string(),
4287                        ": i34".to_string(),
4288                        ": i35".to_string(),
4289                        ": i35".to_string(),
4290                    ],
4291                    sorted_cached_hint_labels(editor, cx),
4292                    "No hint changes/duplicates should occur in the cache",
4293                );
4294                assert_eq!(
4295                    vec![
4296                        ": i34".to_string(),
4297                        ": i35".to_string(),
4298                        ": i32".to_string(),
4299                        ": i33".to_string(),
4300                    ],
4301                    visible_hint_labels(editor, cx),
4302                    "No hint changes/duplicates should occur in the editor excerpts",
4303                );
4304            })
4305            .unwrap();
4306        assert_eq!(
4307            requests_count.load(Ordering::Acquire),
4308            4,
4309            "Should have queried hints once more per each file, after editing the file once"
4310        );
4311    }
4312
4313    #[gpui::test]
4314    async fn test_edit_then_scroll_race(cx: &mut gpui::TestAppContext) {
4315        // Bug 1: An edit fires with a long debounce, and a scroll brings new lines
4316        // before that debounce elapses. The edit task's apply_fetched_hints removes
4317        // ALL visible hints (including the scroll-added ones) but only adds back
4318        // hints for its own chunks. The scroll chunk remains in hint_chunk_fetching,
4319        // so it is never re-queried, leaving it permanently empty.
4320        init_test(cx, &|settings| {
4321            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
4322                enabled: Some(true),
4323                edit_debounce_ms: Some(700),
4324                scroll_debounce_ms: Some(50),
4325                show_type_hints: Some(true),
4326                show_parameter_hints: Some(true),
4327                show_other_hints: Some(true),
4328                ..InlayHintSettingsContent::default()
4329            })
4330        });
4331
4332        let fs = FakeFs::new(cx.background_executor.clone());
4333        let mut file_content = String::from("fn main() {\n");
4334        for i in 0..150 {
4335            file_content.push_str(&format!("    let v{i} = {i};\n"));
4336        }
4337        file_content.push_str("}\n");
4338        fs.insert_tree(
4339            path!("/a"),
4340            json!({
4341                "main.rs": file_content,
4342                "other.rs": "// Test file",
4343            }),
4344        )
4345        .await;
4346
4347        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
4348        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4349        language_registry.add(rust_lang());
4350
4351        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
4352        let mut fake_servers = language_registry.register_fake_lsp(
4353            "Rust",
4354            FakeLspAdapter {
4355                capabilities: lsp::ServerCapabilities {
4356                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4357                    ..lsp::ServerCapabilities::default()
4358                },
4359                initializer: Some(Box::new({
4360                    let lsp_request_ranges = lsp_request_ranges.clone();
4361                    move |fake_server| {
4362                        let lsp_request_ranges = lsp_request_ranges.clone();
4363                        fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
4364                            move |params, _| {
4365                                let lsp_request_ranges = lsp_request_ranges.clone();
4366                                async move {
4367                                    lsp_request_ranges.lock().push(params.range);
4368                                    let start_line = params.range.start.line;
4369                                    Ok(Some(vec![lsp::InlayHint {
4370                                        position: lsp::Position::new(start_line + 1, 9),
4371                                        label: lsp::InlayHintLabel::String(format!(
4372                                            "chunk_{start_line}"
4373                                        )),
4374                                        kind: Some(lsp::InlayHintKind::TYPE),
4375                                        text_edits: None,
4376                                        tooltip: None,
4377                                        padding_left: None,
4378                                        padding_right: None,
4379                                        data: None,
4380                                    }]))
4381                                }
4382                            },
4383                        );
4384                    }
4385                })),
4386                ..FakeLspAdapter::default()
4387            },
4388        );
4389
4390        let buffer = project
4391            .update(cx, |project, cx| {
4392                project.open_local_buffer(path!("/a/main.rs"), cx)
4393            })
4394            .await
4395            .unwrap();
4396        let editor =
4397            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
4398        cx.executor().run_until_parked();
4399        let _fake_server = fake_servers.next().await.unwrap();
4400
4401        editor
4402            .update(cx, |editor, window, cx| {
4403                editor.set_visible_line_count(50.0, window, cx);
4404                editor.set_visible_column_count(120.0);
4405                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4406            })
4407            .unwrap();
4408        cx.executor().advance_clock(Duration::from_millis(100));
4409        cx.executor().run_until_parked();
4410
4411        editor
4412            .update(cx, |editor, _window, cx| {
4413                let visible = visible_hint_labels(editor, cx);
4414                assert!(
4415                    visible.iter().any(|h| h.starts_with("chunk_0")),
4416                    "Should have chunk_0 hints initially, got: {visible:?}"
4417                );
4418            })
4419            .unwrap();
4420
4421        lsp_request_ranges.lock().clear();
4422
4423        // Step 1: Make an edit → triggers BufferEdited with 700ms debounce.
4424        editor
4425            .update(cx, |editor, window, cx| {
4426                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4427                    s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
4428                });
4429                editor.handle_input("x", window, cx);
4430            })
4431            .unwrap();
4432        // Let the BufferEdited event propagate and the edit task get spawned.
4433        cx.executor().run_until_parked();
4434
4435        // Step 2: Scroll down to reveal a new chunk, then trigger NewLinesShown.
4436        // This spawns a scroll task with the shorter 50ms debounce.
4437        editor
4438            .update(cx, |editor, window, cx| {
4439                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
4440            })
4441            .unwrap();
4442        // Explicitly trigger NewLinesShown for the new visible range.
4443        editor
4444            .update(cx, |editor, _window, cx| {
4445                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4446            })
4447            .unwrap();
4448
4449        // Step 3: Advance clock past scroll debounce (50ms) but NOT past edit
4450        // debounce (700ms). The scroll task completes and adds hints for the
4451        // new chunk.
4452        cx.executor().advance_clock(Duration::from_millis(100));
4453        cx.executor().run_until_parked();
4454
4455        // The scroll task's apply_fetched_hints also processes
4456        // invalidate_hints_for_buffers (set by the earlier BufferEdited), which
4457        // removes the old chunk_0 hint. Only the scroll chunk's hint remains.
4458        editor
4459            .update(cx, |editor, _window, cx| {
4460                let visible = visible_hint_labels(editor, cx);
4461                assert!(
4462                    visible.iter().any(|h| h.starts_with("chunk_50")),
4463                    "After scroll task completes, the scroll chunk's hints should be \
4464                     present, got: {visible:?}"
4465                );
4466            })
4467            .unwrap();
4468
4469        // Step 4: Advance clock past the edit debounce (700ms). The edit task
4470        // completes, calling apply_fetched_hints with should_invalidate()=true,
4471        // which removes ALL visible hints (including the scroll chunk's) but only
4472        // adds back hints for its own chunks (chunk_0).
4473        cx.executor().advance_clock(Duration::from_millis(700));
4474        cx.executor().run_until_parked();
4475
4476        // At this point the edit task has:
4477        //   - removed chunk_50's hint (via should_invalidate removing all visible)
4478        //   - added chunk_0's hint (from its own fetch)
4479        //   - (with fix) cleared chunk_50 from hint_chunk_fetching
4480        // Without the fix, chunk_50 is stuck in hint_chunk_fetching and will
4481        // never be re-queried by NewLinesShown.
4482
4483        // Step 5: Trigger NewLinesShown to give the system a chance to re-fetch
4484        // any chunks whose hints were lost.
4485        editor
4486            .update(cx, |editor, _window, cx| {
4487                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4488            })
4489            .unwrap();
4490        cx.executor().advance_clock(Duration::from_millis(100));
4491        cx.executor().run_until_parked();
4492
4493        editor
4494            .update(cx, |editor, _window, cx| {
4495                let visible = visible_hint_labels(editor, cx);
4496                assert!(
4497                    visible.iter().any(|h| h.starts_with("chunk_0")),
4498                    "chunk_0 hints (from edit task) should be present. Got: {visible:?}"
4499                );
4500                assert!(
4501                    visible.iter().any(|h| h.starts_with("chunk_50")),
4502                    "chunk_50 hints should have been re-fetched after NewLinesShown. \
4503                     Bug 1: the scroll chunk's hints were removed by the edit task \
4504                     and the chunk was stuck in hint_chunk_fetching, preventing \
4505                     re-fetch. Got: {visible:?}"
4506                );
4507            })
4508            .unwrap();
4509    }
4510
4511    #[gpui::test]
4512    async fn test_refresh_requested_multi_server(cx: &mut gpui::TestAppContext) {
4513        // Bug 2: When one LSP server sends workspace/inlayHint/refresh, the editor
4514        // wipes all tracking state via clear(), then spawns tasks that call
4515        // LspStore::inlay_hints with for_server=Some(requesting_server). The LspStore
4516        // filters out other servers' cached hints via the for_server guard, so only
4517        // the requesting server's hints are returned. apply_fetched_hints removes ALL
4518        // visible hints (should_invalidate()=true) but only adds back the requesting
4519        // server's hints. Other servers' hints disappear permanently.
4520        init_test(cx, &|settings| {
4521            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
4522                enabled: Some(true),
4523                edit_debounce_ms: Some(0),
4524                scroll_debounce_ms: Some(0),
4525                show_type_hints: Some(true),
4526                show_parameter_hints: Some(true),
4527                show_other_hints: Some(true),
4528                ..InlayHintSettingsContent::default()
4529            })
4530        });
4531
4532        let fs = FakeFs::new(cx.background_executor.clone());
4533        fs.insert_tree(
4534            path!("/a"),
4535            json!({
4536                "main.rs": "fn main() { let x = 1; } // padding to keep hints from being trimmed",
4537                "other.rs": "// Test file",
4538            }),
4539        )
4540        .await;
4541
4542        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
4543        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4544        language_registry.add(rust_lang());
4545
4546        // Server A returns a hint labeled "server_a".
4547        let server_a_request_count = Arc::new(AtomicU32::new(0));
4548        let mut fake_servers_a = language_registry.register_fake_lsp(
4549            "Rust",
4550            FakeLspAdapter {
4551                name: "rust-analyzer",
4552                capabilities: lsp::ServerCapabilities {
4553                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4554                    ..lsp::ServerCapabilities::default()
4555                },
4556                initializer: Some(Box::new({
4557                    let server_a_request_count = server_a_request_count.clone();
4558                    move |fake_server| {
4559                        let server_a_request_count = server_a_request_count.clone();
4560                        fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
4561                            move |_params, _| {
4562                                let count =
4563                                    server_a_request_count.fetch_add(1, Ordering::Release) + 1;
4564                                async move {
4565                                    Ok(Some(vec![lsp::InlayHint {
4566                                        position: lsp::Position::new(0, 9),
4567                                        label: lsp::InlayHintLabel::String(format!(
4568                                            "server_a_{count}"
4569                                        )),
4570                                        kind: Some(lsp::InlayHintKind::TYPE),
4571                                        text_edits: None,
4572                                        tooltip: None,
4573                                        padding_left: None,
4574                                        padding_right: None,
4575                                        data: None,
4576                                    }]))
4577                                }
4578                            },
4579                        );
4580                    }
4581                })),
4582                ..FakeLspAdapter::default()
4583            },
4584        );
4585
4586        // Server B returns a hint labeled "server_b" at a different position.
4587        let server_b_request_count = Arc::new(AtomicU32::new(0));
4588        let mut fake_servers_b = language_registry.register_fake_lsp(
4589            "Rust",
4590            FakeLspAdapter {
4591                name: "secondary-ls",
4592                capabilities: lsp::ServerCapabilities {
4593                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4594                    ..lsp::ServerCapabilities::default()
4595                },
4596                initializer: Some(Box::new({
4597                    let server_b_request_count = server_b_request_count.clone();
4598                    move |fake_server| {
4599                        let server_b_request_count = server_b_request_count.clone();
4600                        fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
4601                            move |_params, _| {
4602                                let count =
4603                                    server_b_request_count.fetch_add(1, Ordering::Release) + 1;
4604                                async move {
4605                                    Ok(Some(vec![lsp::InlayHint {
4606                                        position: lsp::Position::new(0, 22),
4607                                        label: lsp::InlayHintLabel::String(format!(
4608                                            "server_b_{count}"
4609                                        )),
4610                                        kind: Some(lsp::InlayHintKind::TYPE),
4611                                        text_edits: None,
4612                                        tooltip: None,
4613                                        padding_left: None,
4614                                        padding_right: None,
4615                                        data: None,
4616                                    }]))
4617                                }
4618                            },
4619                        );
4620                    }
4621                })),
4622                ..FakeLspAdapter::default()
4623            },
4624        );
4625
4626        let (buffer, _buffer_handle) = project
4627            .update(cx, |project, cx| {
4628                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
4629            })
4630            .await
4631            .unwrap();
4632        let editor =
4633            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
4634        cx.executor().run_until_parked();
4635
4636        let fake_server_a = fake_servers_a.next().await.unwrap();
4637        let _fake_server_b = fake_servers_b.next().await.unwrap();
4638
4639        editor
4640            .update(cx, |editor, window, cx| {
4641                editor.set_visible_line_count(50.0, window, cx);
4642                editor.set_visible_column_count(120.0);
4643                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4644            })
4645            .unwrap();
4646        cx.executor().advance_clock(Duration::from_millis(100));
4647        cx.executor().run_until_parked();
4648
4649        // Verify both servers' hints are present initially.
4650        editor
4651            .update(cx, |editor, _window, cx| {
4652                let visible = visible_hint_labels(editor, cx);
4653                let has_a = visible.iter().any(|h| h.starts_with("server_a"));
4654                let has_b = visible.iter().any(|h| h.starts_with("server_b"));
4655                assert!(
4656                    has_a && has_b,
4657                    "Both servers should have hints initially. Got: {visible:?}"
4658                );
4659            })
4660            .unwrap();
4661
4662        // Trigger RefreshRequested from server A. This should re-fetch server A's
4663        // hints while keeping server B's hints intact.
4664        fake_server_a
4665            .request::<lsp::request::InlayHintRefreshRequest>((), lsp::DEFAULT_LSP_REQUEST_TIMEOUT)
4666            .await
4667            .into_response()
4668            .unwrap();
4669        cx.executor().advance_clock(Duration::from_millis(100));
4670        cx.executor().run_until_parked();
4671
4672        // Also trigger NewLinesShown to give the system a chance to recover
4673        // any chunks that might have been cleared.
4674        editor
4675            .update(cx, |editor, _window, cx| {
4676                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4677            })
4678            .unwrap();
4679        cx.executor().advance_clock(Duration::from_millis(100));
4680        cx.executor().run_until_parked();
4681
4682        editor
4683            .update(cx, |editor, _window, cx| {
4684                let visible = visible_hint_labels(editor, cx);
4685                let has_a = visible.iter().any(|h| h.starts_with("server_a"));
4686                let has_b = visible.iter().any(|h| h.starts_with("server_b"));
4687                assert!(
4688                    has_a,
4689                    "Server A hints should be present after its own refresh. Got: {visible:?}"
4690                );
4691                assert!(
4692                    has_b,
4693                    "Server B hints should NOT be lost when server A triggers \
4694                     RefreshRequested. Bug 2: clear() wipes all tracking, then \
4695                     LspStore filters out server B's cached hints via the for_server \
4696                     guard, and apply_fetched_hints removes all visible hints but only \
4697                     adds back server A's. Got: {visible:?}"
4698                );
4699            })
4700            .unwrap();
4701    }
4702
4703    #[gpui::test]
4704    async fn test_multi_language_multibuffer_no_duplicate_hints(cx: &mut gpui::TestAppContext) {
4705        init_test(cx, &|settings| {
4706            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
4707                show_value_hints: Some(true),
4708                enabled: Some(true),
4709                edit_debounce_ms: Some(0),
4710                scroll_debounce_ms: Some(0),
4711                show_type_hints: Some(true),
4712                show_parameter_hints: Some(true),
4713                show_other_hints: Some(true),
4714                show_background: Some(false),
4715                toggle_on_modifiers_press: None,
4716            })
4717        });
4718
4719        let fs = FakeFs::new(cx.background_executor.clone());
4720        fs.insert_tree(
4721            path!("/a"),
4722            json!({
4723                "main.rs": "fn main() { let x = 1; } // padding to keep hints from being trimmed",
4724                "index.ts": "const y = 2; // padding to keep hints from being trimmed in typescript",
4725            }),
4726        )
4727        .await;
4728
4729        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
4730        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4731
4732        let mut rs_fake_servers = None;
4733        let mut ts_fake_servers = None;
4734        for (name, path_suffix) in [("Rust", "rs"), ("TypeScript", "ts")] {
4735            language_registry.add(Arc::new(Language::new(
4736                LanguageConfig {
4737                    name: name.into(),
4738                    matcher: (LanguageMatcher {
4739                        path_suffixes: vec![path_suffix.to_string()],
4740                        ..Default::default()
4741                    })
4742                    .into(),
4743                    ..Default::default()
4744                },
4745                Some(tree_sitter_rust::LANGUAGE.into()),
4746            )));
4747            let fake_servers = language_registry.register_fake_lsp(
4748                name,
4749                FakeLspAdapter {
4750                    name,
4751                    capabilities: lsp::ServerCapabilities {
4752                        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4753                        ..Default::default()
4754                    },
4755                    initializer: Some(Box::new({
4756                        move |fake_server| {
4757                            let request_count = Arc::new(AtomicU32::new(0));
4758                            fake_server
4759                                .set_request_handler::<lsp::request::InlayHintRequest, _, _>(
4760                                    move |params, _| {
4761                                        let count =
4762                                            request_count.fetch_add(1, Ordering::Release) + 1;
4763                                        let prefix = match name {
4764                                            "Rust" => "rs_hint",
4765                                            "TypeScript" => "ts_hint",
4766                                            other => panic!("Unexpected language: {other}"),
4767                                        };
4768                                        async move {
4769                                            Ok(Some(vec![lsp::InlayHint {
4770                                                position: params.range.start,
4771                                                label: lsp::InlayHintLabel::String(format!(
4772                                                    "{prefix}_{count}"
4773                                                )),
4774                                                kind: None,
4775                                                text_edits: None,
4776                                                tooltip: None,
4777                                                padding_left: None,
4778                                                padding_right: None,
4779                                                data: None,
4780                                            }]))
4781                                        }
4782                                    },
4783                                );
4784                        }
4785                    })),
4786                    ..Default::default()
4787                },
4788            );
4789            match name {
4790                "Rust" => rs_fake_servers = Some(fake_servers),
4791                "TypeScript" => ts_fake_servers = Some(fake_servers),
4792                _ => unreachable!(),
4793            }
4794        }
4795
4796        let (rs_buffer, _rs_handle) = project
4797            .update(cx, |project, cx| {
4798                project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
4799            })
4800            .await
4801            .unwrap();
4802        let (ts_buffer, _ts_handle) = project
4803            .update(cx, |project, cx| {
4804                project.open_local_buffer_with_lsp(path!("/a/index.ts"), cx)
4805            })
4806            .await
4807            .unwrap();
4808
4809        let multi_buffer = cx.new(|cx| {
4810            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
4811            multibuffer.set_excerpts_for_path(
4812                PathKey::sorted(0),
4813                rs_buffer.clone(),
4814                [Point::new(0, 0)..Point::new(1, 0)],
4815                0,
4816                cx,
4817            );
4818            multibuffer.set_excerpts_for_path(
4819                PathKey::sorted(1),
4820                ts_buffer.clone(),
4821                [Point::new(0, 0)..Point::new(1, 0)],
4822                0,
4823                cx,
4824            );
4825            multibuffer
4826        });
4827
4828        cx.executor().run_until_parked();
4829        let editor = cx.add_window(|window, cx| {
4830            Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
4831        });
4832
4833        let _rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
4834        let _ts_fake_server = ts_fake_servers.unwrap().next().await.unwrap();
4835        cx.executor().advance_clock(Duration::from_millis(100));
4836        cx.executor().run_until_parked();
4837
4838        // Verify initial state: both languages have exactly one hint each
4839        editor
4840            .update(cx, |editor, _window, cx| {
4841                let visible = visible_hint_labels(editor, cx);
4842                let rs_hints: Vec<_> = visible
4843                    .iter()
4844                    .filter(|h| h.starts_with("rs_hint"))
4845                    .collect();
4846                let ts_hints: Vec<_> = visible
4847                    .iter()
4848                    .filter(|h| h.starts_with("ts_hint"))
4849                    .collect();
4850                assert_eq!(
4851                    rs_hints.len(),
4852                    1,
4853                    "Should have exactly 1 Rust hint initially, got: {rs_hints:?}"
4854                );
4855                assert_eq!(
4856                    ts_hints.len(),
4857                    1,
4858                    "Should have exactly 1 TypeScript hint initially, got: {ts_hints:?}"
4859                );
4860            })
4861            .unwrap();
4862
4863        // Edit the Rust buffer — triggers BufferEdited(rust_buffer_id).
4864        // The language filter in refresh_inlay_hints excludes TypeScript excerpts
4865        // from processing, but the global clear() wipes added_hints for ALL buffers.
4866        editor
4867            .update(cx, |editor, window, cx| {
4868                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4869                    s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
4870                });
4871                editor.handle_input("x", window, cx);
4872            })
4873            .unwrap();
4874        cx.executor().run_until_parked();
4875
4876        // Trigger NewLinesShown — this causes TypeScript chunks to be re-fetched
4877        // because hint_chunk_fetching was wiped by clear(). The cached hints pass
4878        // the added_hints.insert(...).is_none() filter (also wiped) and get inserted
4879        // alongside the still-displayed copies, causing duplicates.
4880        editor
4881            .update(cx, |editor, _window, cx| {
4882                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4883            })
4884            .unwrap();
4885        cx.executor().run_until_parked();
4886
4887        // Assert: TypeScript hints must NOT be duplicated
4888        editor
4889            .update(cx, |editor, _window, cx| {
4890                let visible = visible_hint_labels(editor, cx);
4891                let ts_hints: Vec<_> = visible
4892                    .iter()
4893                    .filter(|h| h.starts_with("ts_hint"))
4894                    .collect();
4895                assert_eq!(
4896                    ts_hints.len(),
4897                    1,
4898                    "TypeScript hints should NOT be duplicated after editing Rust buffer \
4899                     and triggering NewLinesShown. Got: {ts_hints:?}"
4900                );
4901
4902                let rs_hints: Vec<_> = visible
4903                    .iter()
4904                    .filter(|h| h.starts_with("rs_hint"))
4905                    .collect();
4906                assert_eq!(
4907                    rs_hints.len(),
4908                    1,
4909                    "Rust hints should still be present after editing. Got: {rs_hints:?}"
4910                );
4911            })
4912            .unwrap();
4913    }
4914
4915    pub(crate) fn init_test(cx: &mut TestAppContext, f: &dyn Fn(&mut AllLanguageSettingsContent)) {
4916        cx.update(|cx| {
4917            let settings_store = SettingsStore::test(cx);
4918            cx.set_global(settings_store);
4919            theme_settings::init(theme::LoadThemes::JustBase, cx);
4920            release_channel::init(semver::Version::new(0, 0, 0), cx);
4921            crate::init(cx);
4922        });
4923
4924        update_test_language_settings(cx, f);
4925    }
4926
4927    async fn prepare_test_objects(
4928        cx: &mut TestAppContext,
4929        initialize: impl 'static + Send + Fn(&mut FakeLanguageServer, &'static str) + Send + Sync,
4930    ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
4931        let fs = FakeFs::new(cx.background_executor.clone());
4932        fs.insert_tree(
4933            path!("/a"),
4934            json!({
4935                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
4936                "other.rs": "// Test file",
4937            }),
4938        )
4939        .await;
4940
4941        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
4942        let file_path = path!("/a/main.rs");
4943
4944        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4945        language_registry.add(rust_lang());
4946        let mut fake_servers = language_registry.register_fake_lsp(
4947            "Rust",
4948            FakeLspAdapter {
4949                capabilities: lsp::ServerCapabilities {
4950                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4951                    ..lsp::ServerCapabilities::default()
4952                },
4953                initializer: Some(Box::new(move |server| initialize(server, file_path))),
4954                ..FakeLspAdapter::default()
4955            },
4956        );
4957
4958        let buffer = project
4959            .update(cx, |project, cx| {
4960                project.open_local_buffer(path!("/a/main.rs"), cx)
4961            })
4962            .await
4963            .unwrap();
4964        let editor =
4965            cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
4966
4967        editor
4968            .update(cx, |editor, _, cx| {
4969                assert!(cached_hint_labels(editor, cx).is_empty());
4970                assert!(visible_hint_labels(editor, cx).is_empty());
4971            })
4972            .unwrap();
4973
4974        cx.executor().run_until_parked();
4975        let fake_server = fake_servers.next().await.unwrap();
4976
4977        // Establish a viewport so the editor considers itself visible and the hint refresh
4978        // pipeline runs. Then explicitly trigger a refresh.
4979        editor
4980            .update(cx, |editor, window, cx| {
4981                editor.set_visible_line_count(50.0, window, cx);
4982                editor.set_visible_column_count(120.0);
4983                editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4984            })
4985            .unwrap();
4986        cx.executor().run_until_parked();
4987        (file_path, editor, fake_server)
4988    }
4989
4990    // 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.
4991    // Ensure a stable order for testing.
4992    fn sorted_cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4993        let mut labels = cached_hint_labels(editor, cx);
4994        labels.sort_by(|a, b| natural_sort(a, b));
4995        labels
4996    }
4997
4998    pub fn cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4999        let lsp_store = editor.project().unwrap().read(cx).lsp_store();
5000
5001        let mut all_cached_labels = Vec::new();
5002        let mut all_fetched_hints = Vec::new();
5003        for buffer in editor.buffer.read(cx).all_buffers() {
5004            lsp_store.update(cx, |lsp_store, cx| {
5005                let hints = lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
5006                all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
5007                    let mut label = hint.text().to_string();
5008                    if hint.padding_left {
5009                        label.insert(0, ' ');
5010                    }
5011                    if hint.padding_right {
5012                        label.push_str(" ");
5013                    }
5014                    label
5015                }));
5016                all_fetched_hints.extend(hints.all_fetched_hints());
5017            });
5018        }
5019
5020        all_cached_labels
5021    }
5022
5023    pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
5024        Editor::visible_inlay_hints(editor.display_map.read(cx))
5025            .map(|hint| hint.text().to_string())
5026            .collect()
5027    }
5028
5029    fn allowed_hint_kinds_for_editor(editor: &Editor) -> HashSet<Option<InlayHintKind>> {
5030        editor
5031            .inlay_hints
5032            .as_ref()
5033            .unwrap()
5034            .allowed_hint_kinds
5035            .clone()
5036    }
5037
5038    async fn run_work_cycle(
5039        fake_server: &FakeLanguageServer,
5040        progress_token: i32,
5041        cx: &mut gpui::TestAppContext,
5042    ) {
5043        fake_server
5044            .request::<lsp::request::WorkDoneProgressCreate>(
5045                lsp::WorkDoneProgressCreateParams {
5046                    token: lsp::ProgressToken::Number(progress_token),
5047                },
5048                DEFAULT_LSP_REQUEST_TIMEOUT,
5049            )
5050            .await
5051            .into_response()
5052            .expect("work done progress create request failed");
5053        cx.executor().run_until_parked();
5054        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5055            token: lsp::ProgressToken::Number(progress_token),
5056            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
5057                lsp::WorkDoneProgressBegin::default(),
5058            )),
5059        });
5060        cx.executor().run_until_parked();
5061        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5062            token: lsp::ProgressToken::Number(progress_token),
5063            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
5064                lsp::WorkDoneProgressEnd::default(),
5065            )),
5066        });
5067        cx.executor().run_until_parked();
5068    }
5069}
5070
Served at tenant.openagents/omega Member data and write actions are omitted.