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