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