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