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