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