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