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