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::Autoscroll;
964 use crate::scroll::ScrollAmount;
965 use crate::{Editor, SelectionEffects};
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, PathKey};
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.set_excerpts_for_path(
2326 PathKey::sorted(0),
2327 buffer_1.clone(),
2328 [
2329 Point::new(0, 0)..Point::new(2, 0),
2330 Point::new(4, 0)..Point::new(11, 0),
2331 Point::new(22, 0)..Point::new(33, 0),
2332 Point::new(44, 0)..Point::new(55, 0),
2333 Point::new(56, 0)..Point::new(66, 0),
2334 Point::new(67, 0)..Point::new(77, 0),
2335 ],
2336 0,
2337 cx,
2338 );
2339 multibuffer.set_excerpts_for_path(
2340 PathKey::sorted(1),
2341 buffer_2.clone(),
2342 [
2343 Point::new(0, 1)..Point::new(2, 1),
2344 Point::new(4, 1)..Point::new(11, 1),
2345 Point::new(22, 1)..Point::new(33, 1),
2346 Point::new(44, 1)..Point::new(55, 1),
2347 Point::new(56, 1)..Point::new(66, 1),
2348 Point::new(67, 1)..Point::new(77, 1),
2349 ],
2350 0,
2351 cx,
2352 );
2353 multibuffer
2354 });
2355
2356 cx.executor().run_until_parked();
2357 let editor = cx.add_window(|window, cx| {
2358 Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2359 });
2360
2361 let editor_edited = Arc::new(AtomicBool::new(false));
2362 let fake_server = fake_servers.next().await.unwrap();
2363 let closure_editor_edited = Arc::clone(&editor_edited);
2364 fake_server
2365 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2366 let task_editor_edited = Arc::clone(&closure_editor_edited);
2367 async move {
2368 let hint_text = if params.text_document.uri
2369 == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
2370 {
2371 "main hint"
2372 } else if params.text_document.uri
2373 == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
2374 {
2375 "other hint"
2376 } else {
2377 panic!("unexpected uri: {:?}", params.text_document.uri);
2378 };
2379
2380 // one hint per excerpt
2381 let positions = [
2382 lsp::Position::new(0, 2),
2383 lsp::Position::new(4, 2),
2384 lsp::Position::new(22, 2),
2385 lsp::Position::new(44, 2),
2386 lsp::Position::new(56, 2),
2387 lsp::Position::new(67, 2),
2388 ];
2389 let out_of_range_hint = lsp::InlayHint {
2390 position: lsp::Position::new(
2391 params.range.start.line + 99,
2392 params.range.start.character + 99,
2393 ),
2394 label: lsp::InlayHintLabel::String(
2395 "out of excerpt range, should be ignored".to_string(),
2396 ),
2397 kind: None,
2398 text_edits: None,
2399 tooltip: None,
2400 padding_left: None,
2401 padding_right: None,
2402 data: None,
2403 };
2404
2405 let edited = task_editor_edited.load(Ordering::Acquire);
2406 Ok(Some(
2407 std::iter::once(out_of_range_hint)
2408 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2409 lsp::InlayHint {
2410 position,
2411 label: lsp::InlayHintLabel::String(format!(
2412 "{hint_text}{E} #{i}",
2413 E = if edited { "(edited)" } else { "" },
2414 )),
2415 kind: None,
2416 text_edits: None,
2417 tooltip: None,
2418 padding_left: None,
2419 padding_right: None,
2420 data: None,
2421 }
2422 }))
2423 .collect(),
2424 ))
2425 }
2426 })
2427 .next()
2428 .await;
2429 cx.executor().run_until_parked();
2430
2431 editor
2432 .update(cx, |editor, _window, cx| {
2433 let expected_hints = vec![
2434 "main hint #0".to_string(),
2435 "main hint #1".to_string(),
2436 "main hint #2".to_string(),
2437 "main hint #3".to_string(),
2438 "main hint #4".to_string(),
2439 "main hint #5".to_string(),
2440 ];
2441 assert_eq!(
2442 expected_hints,
2443 sorted_cached_hint_labels(editor, cx),
2444 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2445 );
2446 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2447 })
2448 .unwrap();
2449
2450 editor
2451 .update(cx, |editor, window, cx| {
2452 editor.change_selections(
2453 SelectionEffects::scroll(Autoscroll::Next),
2454 window,
2455 cx,
2456 |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
2457 );
2458 editor.change_selections(
2459 SelectionEffects::scroll(Autoscroll::Next),
2460 window,
2461 cx,
2462 |s| s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]),
2463 );
2464 editor.change_selections(
2465 SelectionEffects::scroll(Autoscroll::Next),
2466 window,
2467 cx,
2468 |s| s.select_ranges([Point::new(57, 0)..Point::new(57, 0)]),
2469 );
2470 })
2471 .unwrap();
2472 cx.executor().run_until_parked();
2473 editor
2474 .update(cx, |editor, _window, cx| {
2475 let expected_hints = vec![
2476 "main hint #0".to_string(),
2477 "main hint #1".to_string(),
2478 "main hint #2".to_string(),
2479 "main hint #3".to_string(),
2480 "main hint #4".to_string(),
2481 "main hint #5".to_string(),
2482 ];
2483 assert_eq!(expected_hints, sorted_cached_hint_labels(editor, cx),
2484 "New hints are not shown right after scrolling, we need to wait for the buffer to be registered");
2485 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2486 })
2487 .unwrap();
2488 cx.executor().advance_clock(Duration::from_millis(100));
2489 cx.executor().run_until_parked();
2490 editor
2491 .update(cx, |editor, _window, cx| {
2492 let expected_hints = vec![
2493 "main hint #0".to_string(),
2494 "main hint #1".to_string(),
2495 "main hint #2".to_string(),
2496 "main hint #3".to_string(),
2497 "main hint #4".to_string(),
2498 "main hint #5".to_string(),
2499 "other hint #0".to_string(),
2500 "other hint #1".to_string(),
2501 "other hint #2".to_string(),
2502 "other hint #3".to_string(),
2503 ];
2504 assert_eq!(
2505 expected_hints,
2506 sorted_cached_hint_labels(editor, cx),
2507 "After scrolling to the new buffer and waiting for it to be registered, new hints should appear");
2508 assert_eq!(
2509 expected_hints,
2510 visible_hint_labels(editor, cx),
2511 "Editor should show only visible hints",
2512 );
2513 })
2514 .unwrap();
2515
2516 editor
2517 .update(cx, |editor, window, cx| {
2518 editor.change_selections(
2519 SelectionEffects::scroll(Autoscroll::Next),
2520 window,
2521 cx,
2522 |s| s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]),
2523 );
2524 })
2525 .unwrap();
2526 cx.executor().advance_clock(Duration::from_millis(100));
2527 cx.executor().run_until_parked();
2528 editor
2529 .update(cx, |editor, _window, cx| {
2530 let expected_hints = vec![
2531 "main hint #0".to_string(),
2532 "main hint #1".to_string(),
2533 "main hint #2".to_string(),
2534 "main hint #3".to_string(),
2535 "main hint #4".to_string(),
2536 "main hint #5".to_string(),
2537 "other hint #0".to_string(),
2538 "other hint #1".to_string(),
2539 "other hint #2".to_string(),
2540 "other hint #3".to_string(),
2541 "other hint #4".to_string(),
2542 "other hint #5".to_string(),
2543 ];
2544 assert_eq!(
2545 expected_hints,
2546 sorted_cached_hint_labels(editor, cx),
2547 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"
2548 );
2549 assert_eq!(
2550 expected_hints,
2551 visible_hint_labels(editor, cx),
2552 "Editor shows only hints for excerpts that were visible when scrolling"
2553 );
2554 })
2555 .unwrap();
2556
2557 editor
2558 .update(cx, |editor, window, cx| {
2559 editor.change_selections(
2560 SelectionEffects::scroll(Autoscroll::Next),
2561 window,
2562 cx,
2563 |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
2564 );
2565 })
2566 .unwrap();
2567 cx.executor().run_until_parked();
2568 editor
2569 .update(cx, |editor, _window, cx| {
2570 let expected_hints = vec![
2571 "main hint #0".to_string(),
2572 "main hint #1".to_string(),
2573 "main hint #2".to_string(),
2574 "main hint #3".to_string(),
2575 "main hint #4".to_string(),
2576 "main hint #5".to_string(),
2577 "other hint #0".to_string(),
2578 "other hint #1".to_string(),
2579 "other hint #2".to_string(),
2580 "other hint #3".to_string(),
2581 "other hint #4".to_string(),
2582 "other hint #5".to_string(),
2583 ];
2584 assert_eq!(
2585 expected_hints,
2586 sorted_cached_hint_labels(editor, cx),
2587 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"
2588 );
2589 assert_eq!(
2590 expected_hints,
2591 visible_hint_labels(editor, cx),
2592 );
2593 })
2594 .unwrap();
2595
2596 // We prepare to change the scrolling on edit, but do not scroll yet
2597 editor
2598 .update(cx, |editor, window, cx| {
2599 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2600 s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
2601 });
2602 })
2603 .unwrap();
2604 cx.executor().run_until_parked();
2605 // Edit triggers the scrolling too
2606 editor_edited.store(true, Ordering::Release);
2607 editor
2608 .update(cx, |editor, window, cx| {
2609 editor.handle_input("++++more text++++", window, cx);
2610 })
2611 .unwrap();
2612 cx.executor().run_until_parked();
2613 // Wait again to trigger the inlay hints fetch on scroll
2614 cx.executor().advance_clock(Duration::from_millis(100));
2615 cx.executor().run_until_parked();
2616 editor
2617 .update(cx, |editor, _window, cx| {
2618 let expected_hints = vec![
2619 "main hint(edited) #0".to_string(),
2620 "main hint(edited) #1".to_string(),
2621 "main hint(edited) #2".to_string(),
2622 "main hint(edited) #3".to_string(),
2623 "main hint(edited) #4".to_string(),
2624 "main hint(edited) #5".to_string(),
2625 "other hint(edited) #0".to_string(),
2626 "other hint(edited) #1".to_string(),
2627 "other hint(edited) #2".to_string(),
2628 "other hint(edited) #3".to_string(),
2629 ];
2630 assert_eq!(
2631 expected_hints,
2632 sorted_cached_hint_labels(editor, cx),
2633 "After multibuffer edit, editor gets scrolled back to the last selection; \
2634 all hints should be invalidated and required for all of its visible excerpts"
2635 );
2636 assert_eq!(
2637 expected_hints,
2638 visible_hint_labels(editor, cx),
2639 "All excerpts should get their hints"
2640 );
2641 })
2642 .unwrap();
2643 }
2644
2645 #[gpui::test]
2646 async fn test_editing_in_multi_buffer(cx: &mut gpui::TestAppContext) {
2647 init_test(cx, &|settings| {
2648 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2649 enabled: Some(true),
2650 ..InlayHintSettingsContent::default()
2651 })
2652 });
2653
2654 let fs = FakeFs::new(cx.background_executor.clone());
2655 fs.insert_tree(
2656 path!("/a"),
2657 json!({
2658 "main.rs": format!("fn main() {{\n{}\n}}", (0..200).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2659 "lib.rs": r#"let a = 1;
2660let b = 2;
2661let c = 3;"#
2662 }),
2663 )
2664 .await;
2665
2666 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2667
2668 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2669 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2670 let language = rust_lang();
2671 language_registry.add(language);
2672
2673 let closure_ranges_fetched = lsp_request_ranges.clone();
2674 let mut fake_servers = language_registry.register_fake_lsp(
2675 "Rust",
2676 FakeLspAdapter {
2677 capabilities: lsp::ServerCapabilities {
2678 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2679 ..lsp::ServerCapabilities::default()
2680 },
2681 initializer: Some(Box::new(move |fake_server| {
2682 let closure_ranges_fetched = closure_ranges_fetched.clone();
2683 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2684 move |params, _| {
2685 let closure_ranges_fetched = closure_ranges_fetched.clone();
2686 async move {
2687 let prefix = if params.text_document.uri
2688 == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
2689 {
2690 closure_ranges_fetched
2691 .lock()
2692 .push(("main.rs", params.range));
2693 "main.rs"
2694 } else if params.text_document.uri
2695 == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
2696 {
2697 closure_ranges_fetched.lock().push(("lib.rs", params.range));
2698 "lib.rs"
2699 } else {
2700 panic!("Unexpected file path {:?}", params.text_document.uri);
2701 };
2702 Ok(Some(
2703 (params.range.start.line..params.range.end.line)
2704 .map(|row| lsp::InlayHint {
2705 position: lsp::Position::new(row, 0),
2706 label: lsp::InlayHintLabel::String(format!(
2707 "{prefix} Inlay hint #{row}"
2708 )),
2709 kind: Some(lsp::InlayHintKind::TYPE),
2710 text_edits: None,
2711 tooltip: None,
2712 padding_left: None,
2713 padding_right: None,
2714 data: None,
2715 })
2716 .collect(),
2717 ))
2718 }
2719 },
2720 );
2721 })),
2722 ..FakeLspAdapter::default()
2723 },
2724 );
2725
2726 let (buffer_1, _handle_1) = project
2727 .update(cx, |project, cx| {
2728 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2729 })
2730 .await
2731 .unwrap();
2732 let (buffer_2, _handle_2) = project
2733 .update(cx, |project, cx| {
2734 project.open_local_buffer_with_lsp(path!("/a/lib.rs"), cx)
2735 })
2736 .await
2737 .unwrap();
2738 let multi_buffer = cx.new(|cx| {
2739 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2740 multibuffer.set_excerpts_for_path(
2741 PathKey::sorted(0),
2742 buffer_1.clone(),
2743 [
2744 Point::new(49, 0)..Point::new(53, 0),
2745 Point::new(70, 0)..Point::new(73, 0),
2746 ],
2747 0,
2748 cx,
2749 );
2750 multibuffer.set_excerpts_for_path(
2751 PathKey::sorted(1),
2752 buffer_2.clone(),
2753 [Point::new(0, 0)..Point::new(4, 0)],
2754 0,
2755 cx,
2756 );
2757 multibuffer
2758 });
2759
2760 let editor = cx.add_window(|window, cx| {
2761 let mut editor =
2762 Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx);
2763 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
2764 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
2765 });
2766 editor
2767 });
2768
2769 let _fake_server = fake_servers.next().await.unwrap();
2770 cx.executor().advance_clock(Duration::from_millis(100));
2771 cx.executor().run_until_parked();
2772
2773 assert_eq!(
2774 vec![
2775 (
2776 "lib.rs",
2777 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(2, 10))
2778 ),
2779 (
2780 "main.rs",
2781 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(50, 0))
2782 ),
2783 (
2784 "main.rs",
2785 lsp::Range::new(lsp::Position::new(50, 0), lsp::Position::new(100, 0))
2786 ),
2787 ],
2788 lsp_request_ranges
2789 .lock()
2790 .drain(..)
2791 .sorted_by_key(|(prefix, r)| (prefix.to_owned(), r.start))
2792 .collect::<Vec<_>>(),
2793 "For large buffers, should query chunks that cover both visible excerpt"
2794 );
2795 editor
2796 .update(cx, |editor, _window, cx| {
2797 assert_eq!(
2798 (0..2)
2799 .map(|i| format!("lib.rs Inlay hint #{i}"))
2800 .chain((0..100).map(|i| format!("main.rs Inlay hint #{i}")))
2801 .collect::<Vec<_>>(),
2802 sorted_cached_hint_labels(editor, cx),
2803 "Both chunks should provide their inlay hints"
2804 );
2805 assert_eq!(
2806 vec![
2807 "main.rs Inlay hint #49".to_owned(),
2808 "main.rs Inlay hint #50".to_owned(),
2809 "main.rs Inlay hint #51".to_owned(),
2810 "main.rs Inlay hint #52".to_owned(),
2811 "main.rs Inlay hint #53".to_owned(),
2812 "main.rs Inlay hint #70".to_owned(),
2813 "main.rs Inlay hint #71".to_owned(),
2814 "main.rs Inlay hint #72".to_owned(),
2815 "main.rs Inlay hint #73".to_owned(),
2816 "lib.rs Inlay hint #0".to_owned(),
2817 "lib.rs Inlay hint #1".to_owned(),
2818 ],
2819 visible_hint_labels(editor, cx),
2820 "Only hints from visible excerpt should be added into the editor"
2821 );
2822 })
2823 .unwrap();
2824
2825 editor
2826 .update(cx, |editor, window, cx| {
2827 editor.handle_input("a", window, cx);
2828 })
2829 .unwrap();
2830 cx.executor().advance_clock(Duration::from_millis(1000));
2831 cx.executor().run_until_parked();
2832 assert_eq!(
2833 vec![
2834 (
2835 "lib.rs",
2836 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(2, 10))
2837 ),
2838 (
2839 "main.rs",
2840 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(50, 0))
2841 ),
2842 (
2843 "main.rs",
2844 lsp::Range::new(lsp::Position::new(50, 0), lsp::Position::new(100, 0))
2845 ),
2846 ],
2847 lsp_request_ranges
2848 .lock()
2849 .drain(..)
2850 .sorted_by_key(|(prefix, r)| (prefix.to_owned(), r.start))
2851 .collect::<Vec<_>>(),
2852 "Same chunks should be re-queried on edit"
2853 );
2854 editor
2855 .update(cx, |editor, _window, cx| {
2856 assert_eq!(
2857 (0..2)
2858 .map(|i| format!("lib.rs Inlay hint #{i}"))
2859 .chain((0..100).map(|i| format!("main.rs Inlay hint #{i}")))
2860 .collect::<Vec<_>>(),
2861 sorted_cached_hint_labels(editor, cx),
2862 "Same hints should be re-inserted after the edit"
2863 );
2864 assert_eq!(
2865 vec![
2866 "main.rs Inlay hint #49".to_owned(),
2867 "main.rs Inlay hint #50".to_owned(),
2868 "main.rs Inlay hint #51".to_owned(),
2869 "main.rs Inlay hint #52".to_owned(),
2870 "main.rs Inlay hint #53".to_owned(),
2871 "main.rs Inlay hint #70".to_owned(),
2872 "main.rs Inlay hint #71".to_owned(),
2873 "main.rs Inlay hint #72".to_owned(),
2874 "main.rs Inlay hint #73".to_owned(),
2875 "lib.rs Inlay hint #0".to_owned(),
2876 "lib.rs Inlay hint #1".to_owned(),
2877 ],
2878 visible_hint_labels(editor, cx),
2879 "Same hints should be re-inserted into the editor after the edit"
2880 );
2881 })
2882 .unwrap();
2883 }
2884
2885 #[gpui::test]
2886 async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
2887 init_test(cx, &|settings| {
2888 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
2889 show_value_hints: Some(true),
2890 enabled: Some(true),
2891 edit_debounce_ms: Some(0),
2892 scroll_debounce_ms: Some(0),
2893 show_type_hints: Some(false),
2894 show_parameter_hints: Some(false),
2895 show_other_hints: Some(false),
2896 show_background: Some(false),
2897 toggle_on_modifiers_press: None,
2898 })
2899 });
2900
2901 let fs = FakeFs::new(cx.background_executor.clone());
2902 fs.insert_tree(
2903 path!("/a"),
2904 json!({
2905 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2906 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2907 }),
2908 )
2909 .await;
2910
2911 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2912
2913 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2914 language_registry.add(rust_lang());
2915 let mut fake_servers = language_registry.register_fake_lsp(
2916 "Rust",
2917 FakeLspAdapter {
2918 capabilities: lsp::ServerCapabilities {
2919 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2920 ..lsp::ServerCapabilities::default()
2921 },
2922 ..FakeLspAdapter::default()
2923 },
2924 );
2925
2926 let (buffer_1, _handle) = project
2927 .update(cx, |project, cx| {
2928 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2929 })
2930 .await
2931 .unwrap();
2932 let (buffer_2, _handle2) = project
2933 .update(cx, |project, cx| {
2934 project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2935 })
2936 .await
2937 .unwrap();
2938 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2939 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2940 multibuffer.set_excerpts_for_path(
2941 PathKey::sorted(0),
2942 buffer_1.clone(),
2943 [Point::new(0, 0)..Point::new(2, 0)],
2944 0,
2945 cx,
2946 );
2947 multibuffer.set_excerpts_for_path(
2948 PathKey::sorted(1),
2949 buffer_2.clone(),
2950 [Point::new(0, 1)..Point::new(2, 1)],
2951 0,
2952 cx,
2953 );
2954 let excerpt_ids = multibuffer.excerpt_ids();
2955 let buffer_1_excerpts = vec![excerpt_ids[0]];
2956 let buffer_2_excerpts = vec![excerpt_ids[1]];
2957 (buffer_1_excerpts, buffer_2_excerpts)
2958 });
2959
2960 assert!(!buffer_1_excerpts.is_empty());
2961 assert!(!buffer_2_excerpts.is_empty());
2962
2963 cx.executor().run_until_parked();
2964 let editor = cx.add_window(|window, cx| {
2965 Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2966 });
2967 let editor_edited = Arc::new(AtomicBool::new(false));
2968 let fake_server = fake_servers.next().await.unwrap();
2969 let closure_editor_edited = Arc::clone(&editor_edited);
2970 fake_server
2971 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2972 let task_editor_edited = Arc::clone(&closure_editor_edited);
2973 async move {
2974 let hint_text = if params.text_document.uri
2975 == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
2976 {
2977 "main hint"
2978 } else if params.text_document.uri
2979 == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
2980 {
2981 "other hint"
2982 } else {
2983 panic!("unexpected uri: {:?}", params.text_document.uri);
2984 };
2985
2986 let positions = [
2987 lsp::Position::new(0, 2),
2988 lsp::Position::new(4, 2),
2989 lsp::Position::new(22, 2),
2990 lsp::Position::new(44, 2),
2991 lsp::Position::new(56, 2),
2992 lsp::Position::new(67, 2),
2993 ];
2994 let out_of_range_hint = lsp::InlayHint {
2995 position: lsp::Position::new(
2996 params.range.start.line + 99,
2997 params.range.start.character + 99,
2998 ),
2999 label: lsp::InlayHintLabel::String(
3000 "out of excerpt range, should be ignored".to_string(),
3001 ),
3002 kind: None,
3003 text_edits: None,
3004 tooltip: None,
3005 padding_left: None,
3006 padding_right: None,
3007 data: None,
3008 };
3009
3010 let edited = task_editor_edited.load(Ordering::Acquire);
3011 Ok(Some(
3012 std::iter::once(out_of_range_hint)
3013 .chain(positions.into_iter().enumerate().map(|(i, position)| {
3014 lsp::InlayHint {
3015 position,
3016 label: lsp::InlayHintLabel::String(format!(
3017 "{hint_text}{} #{i}",
3018 if edited { "(edited)" } else { "" },
3019 )),
3020 kind: None,
3021 text_edits: None,
3022 tooltip: None,
3023 padding_left: None,
3024 padding_right: None,
3025 data: None,
3026 }
3027 }))
3028 .collect(),
3029 ))
3030 }
3031 })
3032 .next()
3033 .await;
3034 cx.executor().advance_clock(Duration::from_millis(100));
3035 cx.executor().run_until_parked();
3036 editor
3037 .update(cx, |editor, _, cx| {
3038 assert_eq!(
3039 vec![
3040 "main hint #0".to_string(),
3041 "main hint #1".to_string(),
3042 "main hint #2".to_string(),
3043 "main hint #3".to_string(),
3044 "other hint #0".to_string(),
3045 "other hint #1".to_string(),
3046 "other hint #2".to_string(),
3047 "other hint #3".to_string(),
3048 ],
3049 sorted_cached_hint_labels(editor, cx),
3050 "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"
3051 );
3052 assert_eq!(
3053 Vec::<String>::new(),
3054 visible_hint_labels(editor, cx),
3055 "All hints are disabled and should not be shown despite being present in the cache"
3056 );
3057 })
3058 .unwrap();
3059
3060 editor
3061 .update(cx, |editor, _, cx| {
3062 editor.buffer().update(cx, |multibuffer, cx| {
3063 multibuffer.remove_excerpts_for_path(PathKey::sorted(1), cx);
3064 })
3065 })
3066 .unwrap();
3067 cx.executor().run_until_parked();
3068 editor
3069 .update(cx, |editor, _, cx| {
3070 assert_eq!(
3071 vec![
3072 "main hint #0".to_string(),
3073 "main hint #1".to_string(),
3074 "main hint #2".to_string(),
3075 "main hint #3".to_string(),
3076 ],
3077 cached_hint_labels(editor, cx),
3078 "For the removed excerpt, should clean corresponding cached hints as its buffer was dropped"
3079 );
3080 assert!(
3081 visible_hint_labels(editor, cx).is_empty(),
3082 "All hints are disabled and should not be shown despite being present in the cache"
3083 );
3084 })
3085 .unwrap();
3086
3087 update_test_language_settings(cx, &|settings| {
3088 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3089 show_value_hints: Some(true),
3090 enabled: Some(true),
3091 edit_debounce_ms: Some(0),
3092 scroll_debounce_ms: Some(0),
3093 show_type_hints: Some(true),
3094 show_parameter_hints: Some(true),
3095 show_other_hints: Some(true),
3096 show_background: Some(false),
3097 toggle_on_modifiers_press: None,
3098 })
3099 });
3100 cx.executor().run_until_parked();
3101 editor
3102 .update(cx, |editor, _, cx| {
3103 assert_eq!(
3104 vec![
3105 "main hint #0".to_string(),
3106 "main hint #1".to_string(),
3107 "main hint #2".to_string(),
3108 "main hint #3".to_string(),
3109 ],
3110 cached_hint_labels(editor, cx),
3111 "Hint display settings change should not change the cache"
3112 );
3113 assert_eq!(
3114 vec![
3115 "main hint #0".to_string(),
3116 ],
3117 visible_hint_labels(editor, cx),
3118 "Settings change should make cached hints visible, but only the visible ones, from the remaining excerpt"
3119 );
3120 })
3121 .unwrap();
3122 }
3123
3124 #[gpui::test]
3125 async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
3126 init_test(cx, &|settings| {
3127 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3128 show_value_hints: Some(true),
3129 enabled: Some(true),
3130 edit_debounce_ms: Some(0),
3131 scroll_debounce_ms: Some(0),
3132 show_type_hints: Some(true),
3133 show_parameter_hints: Some(true),
3134 show_other_hints: Some(true),
3135 show_background: Some(false),
3136 toggle_on_modifiers_press: None,
3137 })
3138 });
3139
3140 let fs = FakeFs::new(cx.background_executor.clone());
3141 fs.insert_tree(
3142 path!("/a"),
3143 json!({
3144 "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)),
3145 "other.rs": "// Test file",
3146 }),
3147 )
3148 .await;
3149
3150 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3151
3152 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3153 language_registry.add(rust_lang());
3154 language_registry.register_fake_lsp(
3155 "Rust",
3156 FakeLspAdapter {
3157 capabilities: lsp::ServerCapabilities {
3158 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3159 ..lsp::ServerCapabilities::default()
3160 },
3161 initializer: Some(Box::new(move |fake_server| {
3162 let lsp_request_count = Arc::new(AtomicU32::new(0));
3163 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3164 move |params, _| {
3165 let i = lsp_request_count.fetch_add(1, Ordering::Release) + 1;
3166 async move {
3167 assert_eq!(
3168 params.text_document.uri,
3169 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3170 );
3171 let query_start = params.range.start;
3172 Ok(Some(vec![lsp::InlayHint {
3173 position: query_start,
3174 label: lsp::InlayHintLabel::String(i.to_string()),
3175 kind: None,
3176 text_edits: None,
3177 tooltip: None,
3178 padding_left: None,
3179 padding_right: None,
3180 data: None,
3181 }]))
3182 }
3183 },
3184 );
3185 })),
3186 ..FakeLspAdapter::default()
3187 },
3188 );
3189
3190 let buffer = project
3191 .update(cx, |project, cx| {
3192 project.open_local_buffer(path!("/a/main.rs"), cx)
3193 })
3194 .await
3195 .unwrap();
3196 let editor =
3197 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3198
3199 // Allow LSP to initialize
3200 cx.executor().run_until_parked();
3201
3202 // Establish a viewport and explicitly trigger hint refresh.
3203 // This ensures we control exactly when hints are requested.
3204 editor
3205 .update(cx, |editor, window, cx| {
3206 editor.set_visible_line_count(50.0, window, cx);
3207 editor.set_visible_column_count(120.0);
3208 editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
3209 })
3210 .unwrap();
3211
3212 // Allow LSP initialization and hint request/response to complete.
3213 // Use multiple advance_clock + run_until_parked cycles to ensure all async work completes.
3214 for _ in 0..5 {
3215 cx.executor().advance_clock(Duration::from_millis(100));
3216 cx.executor().run_until_parked();
3217 }
3218
3219 // At this point we should have exactly one hint from our explicit refresh.
3220 // The test verifies that hints at character boundaries are handled correctly.
3221 editor
3222 .update(cx, |editor, _, cx| {
3223 assert!(
3224 !cached_hint_labels(editor, cx).is_empty(),
3225 "Should have at least one hint after refresh"
3226 );
3227 assert!(
3228 !visible_hint_labels(editor, cx).is_empty(),
3229 "Should have at least one visible hint"
3230 );
3231 })
3232 .unwrap();
3233 }
3234
3235 #[gpui::test]
3236 async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
3237 init_test(cx, &|settings| {
3238 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3239 show_value_hints: Some(true),
3240 enabled: Some(false),
3241 edit_debounce_ms: Some(0),
3242 scroll_debounce_ms: Some(0),
3243 show_type_hints: Some(true),
3244 show_parameter_hints: Some(true),
3245 show_other_hints: Some(true),
3246 show_background: Some(false),
3247 toggle_on_modifiers_press: None,
3248 })
3249 });
3250
3251 let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3252 let lsp_request_count = Arc::new(AtomicU32::new(0));
3253 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3254 move |params, _| {
3255 let lsp_request_count = lsp_request_count.clone();
3256 async move {
3257 assert_eq!(
3258 params.text_document.uri,
3259 lsp::Uri::from_file_path(file_with_hints).unwrap(),
3260 );
3261
3262 let i = lsp_request_count.fetch_add(1, Ordering::AcqRel) + 1;
3263 Ok(Some(vec![lsp::InlayHint {
3264 position: lsp::Position::new(0, i),
3265 label: lsp::InlayHintLabel::String(i.to_string()),
3266 kind: None,
3267 text_edits: None,
3268 tooltip: None,
3269 padding_left: None,
3270 padding_right: None,
3271 data: None,
3272 }]))
3273 }
3274 },
3275 );
3276 })
3277 .await;
3278
3279 editor
3280 .update(cx, |editor, window, cx| {
3281 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3282 })
3283 .unwrap();
3284
3285 cx.executor().run_until_parked();
3286 editor
3287 .update(cx, |editor, _, cx| {
3288 let expected_hints = vec!["1".to_string()];
3289 assert_eq!(
3290 expected_hints,
3291 cached_hint_labels(editor, cx),
3292 "Should display inlays after toggle despite them disabled in settings"
3293 );
3294 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3295 })
3296 .unwrap();
3297
3298 editor
3299 .update(cx, |editor, window, cx| {
3300 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3301 })
3302 .unwrap();
3303 cx.executor().run_until_parked();
3304 editor
3305 .update(cx, |editor, _, cx| {
3306 assert_eq!(
3307 vec!["1".to_string()],
3308 cached_hint_labels(editor, cx),
3309 "Cache does not change because of toggles in the editor"
3310 );
3311 assert_eq!(
3312 Vec::<String>::new(),
3313 visible_hint_labels(editor, cx),
3314 "Should clear hints after 2nd toggle"
3315 );
3316 })
3317 .unwrap();
3318
3319 update_test_language_settings(cx, &|settings| {
3320 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3321 show_value_hints: Some(true),
3322 enabled: Some(true),
3323 edit_debounce_ms: Some(0),
3324 scroll_debounce_ms: Some(0),
3325 show_type_hints: Some(true),
3326 show_parameter_hints: Some(true),
3327 show_other_hints: Some(true),
3328 show_background: Some(false),
3329 toggle_on_modifiers_press: None,
3330 })
3331 });
3332 cx.executor().run_until_parked();
3333 editor
3334 .update(cx, |editor, _, cx| {
3335 let expected_hints = vec!["1".to_string()];
3336 assert_eq!(
3337 expected_hints,
3338 cached_hint_labels(editor, cx),
3339 "Should not query LSP hints after enabling hints in settings, as file version is the same"
3340 );
3341 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3342 })
3343 .unwrap();
3344
3345 editor
3346 .update(cx, |editor, window, cx| {
3347 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3348 })
3349 .unwrap();
3350 cx.executor().run_until_parked();
3351 editor
3352 .update(cx, |editor, _, cx| {
3353 assert_eq!(
3354 vec!["1".to_string()],
3355 cached_hint_labels(editor, cx),
3356 "Cache does not change because of toggles in the editor"
3357 );
3358 assert_eq!(
3359 Vec::<String>::new(),
3360 visible_hint_labels(editor, cx),
3361 "Should clear hints after enabling in settings and a 3rd toggle"
3362 );
3363 })
3364 .unwrap();
3365
3366 editor
3367 .update(cx, |editor, window, cx| {
3368 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3369 })
3370 .unwrap();
3371 cx.executor().run_until_parked();
3372 editor.update(cx, |editor, _, cx| {
3373 let expected_hints = vec!["1".to_string()];
3374 assert_eq!(
3375 expected_hints,
3376 cached_hint_labels(editor,cx),
3377 "Should not query LSP hints after enabling hints in settings and toggling them back on"
3378 );
3379 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3380 }).unwrap();
3381 }
3382
3383 #[gpui::test]
3384 async fn test_modifiers_change(cx: &mut gpui::TestAppContext) {
3385 init_test(cx, &|settings| {
3386 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3387 show_value_hints: Some(true),
3388 enabled: Some(true),
3389 edit_debounce_ms: Some(0),
3390 scroll_debounce_ms: Some(0),
3391 show_type_hints: Some(true),
3392 show_parameter_hints: Some(true),
3393 show_other_hints: Some(true),
3394 show_background: Some(false),
3395 toggle_on_modifiers_press: None,
3396 })
3397 });
3398
3399 let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3400 let lsp_request_count = Arc::new(AtomicU32::new(0));
3401 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3402 move |params, _| {
3403 let lsp_request_count = lsp_request_count.clone();
3404 async move {
3405 assert_eq!(
3406 params.text_document.uri,
3407 lsp::Uri::from_file_path(file_with_hints).unwrap(),
3408 );
3409
3410 let i = lsp_request_count.fetch_add(1, Ordering::AcqRel) + 1;
3411 Ok(Some(vec![lsp::InlayHint {
3412 position: lsp::Position::new(0, i),
3413 label: lsp::InlayHintLabel::String(i.to_string()),
3414 kind: None,
3415 text_edits: None,
3416 tooltip: None,
3417 padding_left: None,
3418 padding_right: None,
3419 data: None,
3420 }]))
3421 }
3422 },
3423 );
3424 })
3425 .await;
3426
3427 cx.executor().run_until_parked();
3428 editor
3429 .update(cx, |editor, _, cx| {
3430 assert_eq!(
3431 vec!["1".to_string()],
3432 cached_hint_labels(editor, cx),
3433 "Should display inlays after toggle despite them disabled in settings"
3434 );
3435 assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx));
3436 })
3437 .unwrap();
3438
3439 editor
3440 .update(cx, |editor, _, cx| {
3441 editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3442 })
3443 .unwrap();
3444 cx.executor().run_until_parked();
3445 editor
3446 .update(cx, |editor, _, cx| {
3447 assert_eq!(
3448 vec!["1".to_string()],
3449 cached_hint_labels(editor, cx),
3450 "Nothing happens with the cache on modifiers change"
3451 );
3452 assert_eq!(
3453 Vec::<String>::new(),
3454 visible_hint_labels(editor, cx),
3455 "On modifiers change and hints toggled on, should hide editor inlays"
3456 );
3457 })
3458 .unwrap();
3459 editor
3460 .update(cx, |editor, _, cx| {
3461 editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3462 })
3463 .unwrap();
3464 cx.executor().run_until_parked();
3465 editor
3466 .update(cx, |editor, _, cx| {
3467 assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3468 assert_eq!(
3469 Vec::<String>::new(),
3470 visible_hint_labels(editor, cx),
3471 "Nothing changes on consequent modifiers change of the same kind"
3472 );
3473 })
3474 .unwrap();
3475
3476 editor
3477 .update(cx, |editor, _, cx| {
3478 editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3479 })
3480 .unwrap();
3481 cx.executor().run_until_parked();
3482 editor
3483 .update(cx, |editor, _, cx| {
3484 assert_eq!(
3485 vec!["1".to_string()],
3486 cached_hint_labels(editor, cx),
3487 "When modifiers change is off, no extra requests are sent"
3488 );
3489 assert_eq!(
3490 vec!["1".to_string()],
3491 visible_hint_labels(editor, cx),
3492 "When modifiers change is off, hints are back into the editor"
3493 );
3494 })
3495 .unwrap();
3496 editor
3497 .update(cx, |editor, _, cx| {
3498 editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3499 })
3500 .unwrap();
3501 cx.executor().run_until_parked();
3502 editor
3503 .update(cx, |editor, _, cx| {
3504 assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3505 assert_eq!(
3506 vec!["1".to_string()],
3507 visible_hint_labels(editor, cx),
3508 "Nothing changes on consequent modifiers change of the same kind (2)"
3509 );
3510 })
3511 .unwrap();
3512
3513 editor
3514 .update(cx, |editor, window, cx| {
3515 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3516 })
3517 .unwrap();
3518 cx.executor().run_until_parked();
3519 editor
3520 .update(cx, |editor, _, cx| {
3521 assert_eq!(
3522 vec!["1".to_string()],
3523 cached_hint_labels(editor, cx),
3524 "Nothing happens with the cache on modifiers change"
3525 );
3526 assert_eq!(
3527 Vec::<String>::new(),
3528 visible_hint_labels(editor, cx),
3529 "When toggled off, should hide editor inlays"
3530 );
3531 })
3532 .unwrap();
3533
3534 editor
3535 .update(cx, |editor, _, cx| {
3536 editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3537 })
3538 .unwrap();
3539 cx.executor().run_until_parked();
3540 editor
3541 .update(cx, |editor, _, cx| {
3542 assert_eq!(
3543 vec!["1".to_string()],
3544 cached_hint_labels(editor, cx),
3545 "Nothing happens with the cache on modifiers change"
3546 );
3547 assert_eq!(
3548 vec!["1".to_string()],
3549 visible_hint_labels(editor, cx),
3550 "On modifiers change & hints toggled off, should show editor inlays"
3551 );
3552 })
3553 .unwrap();
3554 editor
3555 .update(cx, |editor, _, cx| {
3556 editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx);
3557 })
3558 .unwrap();
3559 cx.executor().run_until_parked();
3560 editor
3561 .update(cx, |editor, _, cx| {
3562 assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3563 assert_eq!(
3564 vec!["1".to_string()],
3565 visible_hint_labels(editor, cx),
3566 "Nothing changes on consequent modifiers change of the same kind"
3567 );
3568 })
3569 .unwrap();
3570
3571 editor
3572 .update(cx, |editor, _, cx| {
3573 editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3574 })
3575 .unwrap();
3576 cx.executor().run_until_parked();
3577 editor
3578 .update(cx, |editor, _, cx| {
3579 assert_eq!(
3580 vec!["1".to_string()],
3581 cached_hint_labels(editor, cx),
3582 "When modifiers change is off, no extra requests are sent"
3583 );
3584 assert_eq!(
3585 Vec::<String>::new(),
3586 visible_hint_labels(editor, cx),
3587 "When modifiers change is off, editor hints are back into their toggled off state"
3588 );
3589 })
3590 .unwrap();
3591 editor
3592 .update(cx, |editor, _, cx| {
3593 editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
3594 })
3595 .unwrap();
3596 cx.executor().run_until_parked();
3597 editor
3598 .update(cx, |editor, _, cx| {
3599 assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx));
3600 assert_eq!(
3601 Vec::<String>::new(),
3602 visible_hint_labels(editor, cx),
3603 "Nothing changes on consequent modifiers change of the same kind (3)"
3604 );
3605 })
3606 .unwrap();
3607 }
3608
3609 #[gpui::test]
3610 async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) {
3611 init_test(cx, &|settings| {
3612 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3613 show_value_hints: Some(true),
3614 enabled: Some(true),
3615 edit_debounce_ms: Some(0),
3616 scroll_debounce_ms: Some(0),
3617 show_type_hints: Some(true),
3618 show_parameter_hints: Some(true),
3619 show_other_hints: Some(true),
3620 show_background: Some(false),
3621 toggle_on_modifiers_press: None,
3622 })
3623 });
3624
3625 let fs = FakeFs::new(cx.background_executor.clone());
3626 fs.insert_tree(
3627 path!("/a"),
3628 json!({
3629 "main.rs": "fn main() {
3630 let x = 42;
3631 std::thread::scope(|s| {
3632 s.spawn(|| {
3633 let _x = x;
3634 });
3635 });
3636 }",
3637 "other.rs": "// Test file",
3638 }),
3639 )
3640 .await;
3641
3642 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3643
3644 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3645 language_registry.add(rust_lang());
3646 language_registry.register_fake_lsp(
3647 "Rust",
3648 FakeLspAdapter {
3649 capabilities: lsp::ServerCapabilities {
3650 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3651 ..Default::default()
3652 },
3653 initializer: Some(Box::new(move |fake_server| {
3654 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3655 move |params, _| async move {
3656 assert_eq!(
3657 params.text_document.uri,
3658 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3659 );
3660 Ok(Some(
3661 serde_json::from_value(json!([
3662 {
3663 "position": {
3664 "line": 3,
3665 "character": 16
3666 },
3667 "label": "move",
3668 "paddingLeft": false,
3669 "paddingRight": false
3670 },
3671 {
3672 "position": {
3673 "line": 3,
3674 "character": 16
3675 },
3676 "label": "(",
3677 "paddingLeft": false,
3678 "paddingRight": false
3679 },
3680 {
3681 "position": {
3682 "line": 3,
3683 "character": 16
3684 },
3685 "label": [
3686 {
3687 "value": "&x"
3688 }
3689 ],
3690 "paddingLeft": false,
3691 "paddingRight": false,
3692 "data": {
3693 "file_id": 0
3694 }
3695 },
3696 {
3697 "position": {
3698 "line": 3,
3699 "character": 16
3700 },
3701 "label": ")",
3702 "paddingLeft": false,
3703 "paddingRight": true
3704 },
3705 // not a correct syntax, but checks that same symbols at the same place
3706 // are not deduplicated
3707 {
3708 "position": {
3709 "line": 3,
3710 "character": 16
3711 },
3712 "label": ")",
3713 "paddingLeft": false,
3714 "paddingRight": true
3715 },
3716 ]))
3717 .unwrap(),
3718 ))
3719 },
3720 );
3721 })),
3722 ..FakeLspAdapter::default()
3723 },
3724 );
3725
3726 let buffer = project
3727 .update(cx, |project, cx| {
3728 project.open_local_buffer(path!("/a/main.rs"), cx)
3729 })
3730 .await
3731 .unwrap();
3732
3733 // Use a VisualTestContext and explicitly establish a viewport on the editor (the production
3734 // trigger for `NewLinesShown` / inlay hint refresh) by setting visible line/column counts.
3735 let (editor_entity, cx) =
3736 cx.add_window_view(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3737
3738 editor_entity.update_in(cx, |editor, window, cx| {
3739 // Establish a viewport. The exact values are not important for this test; we just need
3740 // the editor to consider itself visible so the refresh pipeline runs.
3741 editor.set_visible_line_count(50.0, window, cx);
3742 editor.set_visible_column_count(120.0);
3743
3744 // Explicitly trigger a refresh now that the viewport exists.
3745 editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
3746 });
3747 cx.executor().run_until_parked();
3748
3749 editor_entity.update_in(cx, |editor, window, cx| {
3750 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3751 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3752 });
3753 });
3754 cx.executor().run_until_parked();
3755
3756 // Allow any async inlay hint request/response work to complete.
3757 cx.executor().advance_clock(Duration::from_millis(100));
3758 cx.executor().run_until_parked();
3759
3760 editor_entity.update(cx, |editor, cx| {
3761 let expected_hints = vec![
3762 "move".to_string(),
3763 "(".to_string(),
3764 "&x".to_string(),
3765 ") ".to_string(),
3766 ") ".to_string(),
3767 ];
3768 assert_eq!(
3769 expected_hints,
3770 cached_hint_labels(editor, cx),
3771 "Editor inlay hints should repeat server's order when placed at the same spot"
3772 );
3773 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3774 });
3775 }
3776
3777 #[gpui::test]
3778 async fn test_invalidation_and_addition_race(cx: &mut gpui::TestAppContext) {
3779 init_test(cx, &|settings| {
3780 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
3781 enabled: Some(true),
3782 ..InlayHintSettingsContent::default()
3783 })
3784 });
3785
3786 let fs = FakeFs::new(cx.background_executor.clone());
3787 fs.insert_tree(
3788 path!("/a"),
3789 json!({
3790 "main.rs": r#"fn main() {
3791 let x = 1;
3792 ////
3793 ////
3794 ////
3795 ////
3796 ////
3797 ////
3798 ////
3799 ////
3800 ////
3801 ////
3802 ////
3803 ////
3804 ////
3805 ////
3806 ////
3807 ////
3808 ////
3809 let x = "2";
3810 }
3811"#,
3812 "lib.rs": r#"fn aaa() {
3813 let aa = 22;
3814 }
3815 //
3816 //
3817 //
3818 //
3819 //
3820 //
3821 //
3822 //
3823 //
3824 //
3825 //
3826 //
3827 //
3828 //
3829 //
3830 //
3831 //
3832 //
3833 //
3834 //
3835 //
3836 //
3837 //
3838 //
3839
3840 fn bb() {
3841 let bb = 33;
3842 }
3843"#
3844 }),
3845 )
3846 .await;
3847
3848 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3849 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3850 let language = rust_lang();
3851 language_registry.add(language);
3852
3853 let requests_count = Arc::new(AtomicUsize::new(0));
3854 let closure_requests_count = requests_count.clone();
3855 let mut fake_servers = language_registry.register_fake_lsp(
3856 "Rust",
3857 FakeLspAdapter {
3858 name: "rust-analyzer",
3859 capabilities: lsp::ServerCapabilities {
3860 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3861 ..lsp::ServerCapabilities::default()
3862 },
3863 initializer: Some(Box::new(move |fake_server| {
3864 let requests_count = closure_requests_count.clone();
3865 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3866 move |params, _| {
3867 let requests_count = requests_count.clone();
3868 async move {
3869 requests_count.fetch_add(1, Ordering::Release);
3870 if params.text_document.uri
3871 == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
3872 {
3873 Ok(Some(vec![
3874 lsp::InlayHint {
3875 position: lsp::Position::new(1, 9),
3876 label: lsp::InlayHintLabel::String(": i32".to_owned()),
3877 kind: Some(lsp::InlayHintKind::TYPE),
3878 text_edits: None,
3879 tooltip: None,
3880 padding_left: None,
3881 padding_right: None,
3882 data: None,
3883 },
3884 lsp::InlayHint {
3885 position: lsp::Position::new(19, 9),
3886 label: lsp::InlayHintLabel::String(": i33".to_owned()),
3887 kind: Some(lsp::InlayHintKind::TYPE),
3888 text_edits: None,
3889 tooltip: None,
3890 padding_left: None,
3891 padding_right: None,
3892 data: None,
3893 },
3894 ]))
3895 } else if params.text_document.uri
3896 == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
3897 {
3898 Ok(Some(vec![
3899 lsp::InlayHint {
3900 position: lsp::Position::new(1, 10),
3901 label: lsp::InlayHintLabel::String(": i34".to_owned()),
3902 kind: Some(lsp::InlayHintKind::TYPE),
3903 text_edits: None,
3904 tooltip: None,
3905 padding_left: None,
3906 padding_right: None,
3907 data: None,
3908 },
3909 lsp::InlayHint {
3910 position: lsp::Position::new(29, 10),
3911 label: lsp::InlayHintLabel::String(": i35".to_owned()),
3912 kind: Some(lsp::InlayHintKind::TYPE),
3913 text_edits: None,
3914 tooltip: None,
3915 padding_left: None,
3916 padding_right: None,
3917 data: None,
3918 },
3919 ]))
3920 } else {
3921 panic!("Unexpected file path {:?}", params.text_document.uri);
3922 }
3923 }
3924 },
3925 );
3926 })),
3927 ..FakeLspAdapter::default()
3928 },
3929 );
3930
3931 // Add another server that does send the same, duplicate hints back
3932 let mut fake_servers_2 = language_registry.register_fake_lsp(
3933 "Rust",
3934 FakeLspAdapter {
3935 name: "CrabLang-ls",
3936 capabilities: lsp::ServerCapabilities {
3937 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3938 ..lsp::ServerCapabilities::default()
3939 },
3940 initializer: Some(Box::new(move |fake_server| {
3941 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3942 move |params, _| async move {
3943 if params.text_document.uri
3944 == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
3945 {
3946 Ok(Some(vec![
3947 lsp::InlayHint {
3948 position: lsp::Position::new(1, 9),
3949 label: lsp::InlayHintLabel::String(": i32".to_owned()),
3950 kind: Some(lsp::InlayHintKind::TYPE),
3951 text_edits: None,
3952 tooltip: None,
3953 padding_left: None,
3954 padding_right: None,
3955 data: None,
3956 },
3957 lsp::InlayHint {
3958 position: lsp::Position::new(19, 9),
3959 label: lsp::InlayHintLabel::String(": i33".to_owned()),
3960 kind: Some(lsp::InlayHintKind::TYPE),
3961 text_edits: None,
3962 tooltip: None,
3963 padding_left: None,
3964 padding_right: None,
3965 data: None,
3966 },
3967 ]))
3968 } else if params.text_document.uri
3969 == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
3970 {
3971 Ok(Some(vec![
3972 lsp::InlayHint {
3973 position: lsp::Position::new(1, 10),
3974 label: lsp::InlayHintLabel::String(": i34".to_owned()),
3975 kind: Some(lsp::InlayHintKind::TYPE),
3976 text_edits: None,
3977 tooltip: None,
3978 padding_left: None,
3979 padding_right: None,
3980 data: None,
3981 },
3982 lsp::InlayHint {
3983 position: lsp::Position::new(29, 10),
3984 label: lsp::InlayHintLabel::String(": i35".to_owned()),
3985 kind: Some(lsp::InlayHintKind::TYPE),
3986 text_edits: None,
3987 tooltip: None,
3988 padding_left: None,
3989 padding_right: None,
3990 data: None,
3991 },
3992 ]))
3993 } else {
3994 panic!("Unexpected file path {:?}", params.text_document.uri);
3995 }
3996 },
3997 );
3998 })),
3999 ..FakeLspAdapter::default()
4000 },
4001 );
4002
4003 let (buffer_1, _handle_1) = project
4004 .update(cx, |project, cx| {
4005 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
4006 })
4007 .await
4008 .unwrap();
4009 let (buffer_2, _handle_2) = project
4010 .update(cx, |project, cx| {
4011 project.open_local_buffer_with_lsp(path!("/a/lib.rs"), cx)
4012 })
4013 .await
4014 .unwrap();
4015 let multi_buffer = cx.new(|cx| {
4016 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
4017 multibuffer.set_excerpts_for_path(
4018 PathKey::sorted(0),
4019 buffer_2.clone(),
4020 [
4021 Point::new(0, 0)..Point::new(10, 0),
4022 Point::new(23, 0)..Point::new(34, 0),
4023 ],
4024 0,
4025 cx,
4026 );
4027 multibuffer.set_excerpts_for_path(
4028 PathKey::sorted(1),
4029 buffer_1.clone(),
4030 [
4031 Point::new(0, 0)..Point::new(10, 0),
4032 Point::new(13, 0)..Point::new(23, 0),
4033 ],
4034 0,
4035 cx,
4036 );
4037 multibuffer
4038 });
4039
4040 let editor = cx.add_window(|window, cx| {
4041 let mut editor =
4042 Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx);
4043 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
4044 s.select_ranges([Point::new(3, 3)..Point::new(3, 3)])
4045 });
4046 editor
4047 });
4048
4049 let fake_server = fake_servers.next().await.unwrap();
4050 let _fake_server_2 = fake_servers_2.next().await.unwrap();
4051 cx.executor().advance_clock(Duration::from_millis(100));
4052 cx.executor().run_until_parked();
4053
4054 editor
4055 .update(cx, |editor, _window, cx| {
4056 assert_eq!(
4057 vec![
4058 ": i32".to_string(),
4059 ": i32".to_string(),
4060 ": i33".to_string(),
4061 ": i33".to_string(),
4062 ": i34".to_string(),
4063 ": i34".to_string(),
4064 ": i35".to_string(),
4065 ": i35".to_string(),
4066 ],
4067 sorted_cached_hint_labels(editor, cx),
4068 "We receive duplicate hints from 2 servers and cache them all"
4069 );
4070 assert_eq!(
4071 vec![
4072 ": i34".to_string(),
4073 ": i35".to_string(),
4074 ": i32".to_string(),
4075 ": i33".to_string(),
4076 ],
4077 visible_hint_labels(editor, cx),
4078 "lib.rs is added before main.rs , so its excerpts should be visible first; hints should be deduplicated per label"
4079 );
4080 })
4081 .unwrap();
4082 assert_eq!(
4083 requests_count.load(Ordering::Acquire),
4084 2,
4085 "Should have queried hints once per each file"
4086 );
4087
4088 // Scroll all the way down so the 1st buffer is out of sight.
4089 // The selection is on the 1st buffer still.
4090 editor
4091 .update(cx, |editor, window, cx| {
4092 editor.scroll_screen(&ScrollAmount::Line(88.0), window, cx);
4093 })
4094 .unwrap();
4095 // Emulate a language server refresh request, coming in the background..
4096 editor
4097 .update(cx, |editor, _, cx| {
4098 editor.refresh_inlay_hints(
4099 InlayHintRefreshReason::RefreshRequested {
4100 server_id: fake_server.server.server_id(),
4101 request_id: Some(1),
4102 },
4103 cx,
4104 );
4105 })
4106 .unwrap();
4107 // Edit the 1st buffer while scrolled down and not seeing that.
4108 // The edit will auto scroll to the edit (1st buffer).
4109 editor
4110 .update(cx, |editor, window, cx| {
4111 editor.handle_input("a", window, cx);
4112 })
4113 .unwrap();
4114 // Add more racy additive hint tasks.
4115 editor
4116 .update(cx, |editor, window, cx| {
4117 editor.scroll_screen(&ScrollAmount::Line(0.2), window, cx);
4118 })
4119 .unwrap();
4120
4121 cx.executor().advance_clock(Duration::from_millis(1000));
4122 cx.executor().run_until_parked();
4123 editor
4124 .update(cx, |editor, _window, cx| {
4125 assert_eq!(
4126 vec![
4127 ": i32".to_string(),
4128 ": i32".to_string(),
4129 ": i33".to_string(),
4130 ": i33".to_string(),
4131 ": i34".to_string(),
4132 ": i34".to_string(),
4133 ": i35".to_string(),
4134 ": i35".to_string(),
4135 ],
4136 sorted_cached_hint_labels(editor, cx),
4137 "No hint changes/duplicates should occur in the cache",
4138 );
4139 assert_eq!(
4140 vec![
4141 ": i34".to_string(),
4142 ": i35".to_string(),
4143 ": i32".to_string(),
4144 ": i33".to_string(),
4145 ],
4146 visible_hint_labels(editor, cx),
4147 "No hint changes/duplicates should occur in the editor excerpts",
4148 );
4149 })
4150 .unwrap();
4151 assert_eq!(
4152 requests_count.load(Ordering::Acquire),
4153 4,
4154 "Should have queried hints once more per each file, after editing the file once"
4155 );
4156 }
4157
4158 pub(crate) fn init_test(cx: &mut TestAppContext, f: &dyn Fn(&mut AllLanguageSettingsContent)) {
4159 cx.update(|cx| {
4160 let settings_store = SettingsStore::test(cx);
4161 cx.set_global(settings_store);
4162 theme::init(theme::LoadThemes::JustBase, cx);
4163 release_channel::init(semver::Version::new(0, 0, 0), cx);
4164 crate::init(cx);
4165 });
4166
4167 update_test_language_settings(cx, f);
4168 }
4169
4170 async fn prepare_test_objects(
4171 cx: &mut TestAppContext,
4172 initialize: impl 'static + Send + Fn(&mut FakeLanguageServer, &'static str) + Send + Sync,
4173 ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
4174 let fs = FakeFs::new(cx.background_executor.clone());
4175 fs.insert_tree(
4176 path!("/a"),
4177 json!({
4178 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
4179 "other.rs": "// Test file",
4180 }),
4181 )
4182 .await;
4183
4184 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
4185 let file_path = path!("/a/main.rs");
4186
4187 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4188 language_registry.add(rust_lang());
4189 let mut fake_servers = language_registry.register_fake_lsp(
4190 "Rust",
4191 FakeLspAdapter {
4192 capabilities: lsp::ServerCapabilities {
4193 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
4194 ..lsp::ServerCapabilities::default()
4195 },
4196 initializer: Some(Box::new(move |server| initialize(server, file_path))),
4197 ..FakeLspAdapter::default()
4198 },
4199 );
4200
4201 let buffer = project
4202 .update(cx, |project, cx| {
4203 project.open_local_buffer(path!("/a/main.rs"), cx)
4204 })
4205 .await
4206 .unwrap();
4207 let editor =
4208 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
4209
4210 editor
4211 .update(cx, |editor, _, cx| {
4212 assert!(cached_hint_labels(editor, cx).is_empty());
4213 assert!(visible_hint_labels(editor, cx).is_empty());
4214 })
4215 .unwrap();
4216
4217 cx.executor().run_until_parked();
4218 let fake_server = fake_servers.next().await.unwrap();
4219
4220 // Establish a viewport so the editor considers itself visible and the hint refresh
4221 // pipeline runs. Then explicitly trigger a refresh.
4222 editor
4223 .update(cx, |editor, window, cx| {
4224 editor.set_visible_line_count(50.0, window, cx);
4225 editor.set_visible_column_count(120.0);
4226 editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
4227 })
4228 .unwrap();
4229 cx.executor().run_until_parked();
4230 (file_path, editor, fake_server)
4231 }
4232
4233 // 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.
4234 // Ensure a stable order for testing.
4235 fn sorted_cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4236 let mut labels = cached_hint_labels(editor, cx);
4237 labels.sort_by(|a, b| natural_sort(a, b));
4238 labels
4239 }
4240
4241 pub fn cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4242 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
4243
4244 let mut all_cached_labels = Vec::new();
4245 let mut all_fetched_hints = Vec::new();
4246 for buffer in editor.buffer.read(cx).all_buffers() {
4247 lsp_store.update(cx, |lsp_store, cx| {
4248 let hints = lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
4249 all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
4250 let mut label = hint.text().to_string();
4251 if hint.padding_left {
4252 label.insert(0, ' ');
4253 }
4254 if hint.padding_right {
4255 label.push_str(" ");
4256 }
4257 label
4258 }));
4259 all_fetched_hints.extend(hints.all_fetched_hints());
4260 });
4261 }
4262
4263 all_cached_labels
4264 }
4265
4266 pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
4267 editor
4268 .visible_inlay_hints(cx)
4269 .into_iter()
4270 .map(|hint| hint.text().to_string())
4271 .collect()
4272 }
4273
4274 fn allowed_hint_kinds_for_editor(editor: &Editor) -> HashSet<Option<InlayHintKind>> {
4275 editor
4276 .inlay_hints
4277 .as_ref()
4278 .unwrap()
4279 .allowed_hint_kinds
4280 .clone()
4281 }
4282}