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