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