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