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