1use std::{
2 collections::hash_map,
3 ops::{ControlFlow, Range},
4 sync::Arc,
5 time::Duration,
6};
7
8use clock::Global;
9use collections::{HashMap, HashSet};
10use futures::future::join_all;
11use gpui::{App, Entity, Task};
12use language::{
13 BufferRow,
14 language_settings::{InlayHintKind, InlayHintSettings, language_settings},
15};
16use lsp::LanguageServerId;
17use multi_buffer::{Anchor, ExcerptId, MultiBufferSnapshot};
18use parking_lot::Mutex;
19use project::{
20 HoverBlock, HoverBlockKind, InlayHintLabel, InlayHintLabelPartTooltip, InlayHintTooltip,
21 InvalidationStrategy, ResolveState,
22 lsp_store::{CacheInlayHints, ResolvedHint},
23};
24use text::{Bias, BufferId};
25use ui::{Context, Window};
26use util::debug_panic;
27
28use super::{Inlay, InlayId};
29use crate::{
30 Editor, EditorSnapshot, PointForPosition, ToggleInlayHints, ToggleInlineValues, debounce_value,
31 hover_links::{InlayHighlight, TriggerPoint, show_link_definition},
32 hover_popover::{self, InlayHover},
33 inlays::InlaySplice,
34};
35
36pub fn inlay_hint_settings(
37 location: Anchor,
38 snapshot: &MultiBufferSnapshot,
39 cx: &mut Context<Editor>,
40) -> InlayHintSettings {
41 let file = snapshot.file_at(location);
42 let language = snapshot.language_at(location).map(|l| l.name());
43 language_settings(language, file, cx).inlay_hints
44}
45
46#[derive(Debug)]
47pub struct LspInlayHintData {
48 enabled: bool,
49 modifiers_override: bool,
50 enabled_in_settings: bool,
51 allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
52 invalidate_debounce: Option<Duration>,
53 append_debounce: Option<Duration>,
54 hint_refresh_tasks: HashMap<BufferId, HashMap<Vec<Range<BufferRow>>, Vec<Task<()>>>>,
55 hint_chunk_fetched: HashMap<BufferId, (Global, HashSet<Range<BufferRow>>)>,
56 pub added_hints: HashMap<InlayId, Option<InlayHintKind>>,
57}
58
59impl LspInlayHintData {
60 pub fn new(settings: InlayHintSettings) -> Self {
61 Self {
62 modifiers_override: false,
63 enabled: settings.enabled,
64 enabled_in_settings: settings.enabled,
65 hint_refresh_tasks: HashMap::default(),
66 added_hints: HashMap::default(),
67 hint_chunk_fetched: HashMap::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 }
105
106 /// Checks inlay hint settings for enabled hint kinds and general enabled state.
107 /// Generates corresponding inlay_map splice updates on settings changes.
108 /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries.
109 fn update_settings(
110 &mut self,
111 new_hint_settings: InlayHintSettings,
112 visible_hints: Vec<Inlay>,
113 ) -> ControlFlow<Option<InlaySplice>, Option<InlaySplice>> {
114 let old_enabled = self.enabled;
115 // If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay
116 // hint visibility changes when other settings change (such as theme).
117 //
118 // Another option might be to store whether the user has manually toggled inlay hint
119 // visibility, and prefer this. This could lead to confusion as it means inlay hint
120 // visibility would not change when updating the setting if they were ever toggled.
121 if new_hint_settings.enabled != self.enabled_in_settings {
122 self.enabled = new_hint_settings.enabled;
123 self.enabled_in_settings = new_hint_settings.enabled;
124 self.modifiers_override = false;
125 };
126 self.invalidate_debounce = debounce_value(new_hint_settings.edit_debounce_ms);
127 self.append_debounce = debounce_value(new_hint_settings.scroll_debounce_ms);
128 let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
129 match (old_enabled, self.enabled) {
130 (false, false) => {
131 self.allowed_hint_kinds = new_allowed_hint_kinds;
132 ControlFlow::Break(None)
133 }
134 (true, true) => {
135 if new_allowed_hint_kinds == self.allowed_hint_kinds {
136 ControlFlow::Break(None)
137 } else {
138 self.allowed_hint_kinds = new_allowed_hint_kinds;
139 ControlFlow::Continue(
140 Some(InlaySplice {
141 to_remove: visible_hints
142 .iter()
143 .filter_map(|inlay| {
144 let inlay_kind = self.added_hints.get(&inlay.id).copied()?;
145 if !self.allowed_hint_kinds.contains(&inlay_kind) {
146 Some(inlay.id)
147 } else {
148 None
149 }
150 })
151 .collect(),
152 to_insert: Vec::new(),
153 })
154 .filter(|splice| !splice.is_empty()),
155 )
156 }
157 }
158 (true, false) => {
159 self.modifiers_override = false;
160 self.allowed_hint_kinds = new_allowed_hint_kinds;
161 if visible_hints.is_empty() {
162 ControlFlow::Break(None)
163 } else {
164 self.clear();
165 ControlFlow::Break(Some(InlaySplice {
166 to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
167 to_insert: Vec::new(),
168 }))
169 }
170 }
171 (false, true) => {
172 self.modifiers_override = false;
173 self.allowed_hint_kinds = new_allowed_hint_kinds;
174 ControlFlow::Continue(
175 Some(InlaySplice {
176 to_remove: visible_hints
177 .iter()
178 .filter_map(|inlay| {
179 let inlay_kind = self.added_hints.get(&inlay.id).copied()?;
180 if !self.allowed_hint_kinds.contains(&inlay_kind) {
181 Some(inlay.id)
182 } else {
183 None
184 }
185 })
186 .collect(),
187 to_insert: Vec::new(),
188 })
189 .filter(|splice| !splice.is_empty()),
190 )
191 }
192 }
193 }
194
195 pub(crate) fn remove_inlay_chunk_data<'a>(
196 &'a mut self,
197 removed_buffer_ids: impl IntoIterator<Item = &'a BufferId> + 'a,
198 ) {
199 for buffer_id in removed_buffer_ids {
200 self.hint_refresh_tasks.remove(buffer_id);
201 self.hint_chunk_fetched.remove(buffer_id);
202 }
203 }
204}
205
206#[derive(Debug, Clone)]
207pub enum InlayHintRefreshReason {
208 ModifiersChanged(bool),
209 Toggle(bool),
210 SettingsChange(InlayHintSettings),
211 NewLinesShown,
212 BufferEdited(BufferId),
213 RefreshRequested(LanguageServerId),
214 ExcerptsRemoved(Vec<ExcerptId>),
215}
216
217impl Editor {
218 pub fn supports_inlay_hints(&self, cx: &mut App) -> bool {
219 let Some(provider) = self.semantics_provider.as_ref() else {
220 return false;
221 };
222
223 let mut supports = false;
224 self.buffer().update(cx, |this, cx| {
225 this.for_each_buffer(|buffer| {
226 supports |= provider.supports_inlay_hints(buffer, cx);
227 });
228 });
229
230 supports
231 }
232
233 pub fn toggle_inline_values(
234 &mut self,
235 _: &ToggleInlineValues,
236 _: &mut Window,
237 cx: &mut Context<Self>,
238 ) {
239 self.inline_value_cache.enabled = !self.inline_value_cache.enabled;
240
241 self.refresh_inline_values(cx);
242 }
243
244 pub fn toggle_inlay_hints(
245 &mut self,
246 _: &ToggleInlayHints,
247 _: &mut Window,
248 cx: &mut Context<Self>,
249 ) {
250 self.refresh_inlay_hints(
251 InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()),
252 cx,
253 );
254 }
255
256 pub fn inlay_hints_enabled(&self) -> bool {
257 self.inlay_hints.as_ref().is_some_and(|cache| cache.enabled)
258 }
259
260 /// Updates inlay hints for the visible ranges of the singleton buffer(s).
261 /// Based on its parameters, either invalidates the previous data, or appends to it.
262 pub(crate) fn refresh_inlay_hints(
263 &mut self,
264 reason: InlayHintRefreshReason,
265 cx: &mut Context<Self>,
266 ) {
267 if !self.mode.is_full() || self.inlay_hints.is_none() {
268 return;
269 }
270 let Some(semantics_provider) = self.semantics_provider() else {
271 return;
272 };
273 let Some(invalidate_cache) = self.refresh_editor_data(&reason, cx) else {
274 return;
275 };
276
277 let debounce = match &reason {
278 InlayHintRefreshReason::SettingsChange(_)
279 | InlayHintRefreshReason::Toggle(_)
280 | InlayHintRefreshReason::ExcerptsRemoved(_)
281 | InlayHintRefreshReason::ModifiersChanged(_) => None,
282 _may_need_lsp_call => self.inlay_hints.as_ref().and_then(|inlay_hints| {
283 if invalidate_cache.should_invalidate() {
284 inlay_hints.invalidate_debounce
285 } else {
286 inlay_hints.append_debounce
287 }
288 }),
289 };
290
291 let mut visible_excerpts = self.visible_excerpts(cx);
292 let mut all_affected_buffers = HashSet::default();
293 let ignore_previous_fetches = match reason {
294 InlayHintRefreshReason::ModifiersChanged(_)
295 | InlayHintRefreshReason::Toggle(_)
296 | InlayHintRefreshReason::SettingsChange(_) => true,
297 InlayHintRefreshReason::NewLinesShown
298 | InlayHintRefreshReason::RefreshRequested(_)
299 | InlayHintRefreshReason::ExcerptsRemoved(_) => false,
300 InlayHintRefreshReason::BufferEdited(buffer_id) => {
301 let Some(affected_language) = self
302 .buffer()
303 .read(cx)
304 .buffer(buffer_id)
305 .and_then(|buffer| buffer.read(cx).language().cloned())
306 else {
307 return;
308 };
309
310 all_affected_buffers.extend(
311 self.buffer()
312 .read(cx)
313 .all_buffers()
314 .into_iter()
315 .filter_map(|buffer| {
316 let buffer = buffer.read(cx);
317 if buffer.language() == Some(&affected_language) {
318 Some(buffer.remote_id())
319 } else {
320 None
321 }
322 }),
323 );
324
325 semantics_provider.invalidate_inlay_hints(&all_affected_buffers, cx);
326 visible_excerpts.retain(|_, (visible_buffer, _, _)| {
327 visible_buffer.read(cx).language() == Some(&affected_language)
328 });
329 false
330 }
331 };
332
333 let Some(inlay_hints) = self.inlay_hints.as_mut() else {
334 return;
335 };
336
337 if invalidate_cache.should_invalidate() {
338 inlay_hints.clear();
339 }
340
341 let mut buffers_to_query = HashMap::default();
342 for (excerpt_id, (buffer, buffer_version, visible_range)) in visible_excerpts {
343 let buffer_id = buffer.read(cx).remote_id();
344 if !self.registered_buffers.contains_key(&buffer_id) {
345 continue;
346 }
347
348 let buffer_snapshot = buffer.read(cx).snapshot();
349 let buffer_anchor_range = buffer_snapshot.anchor_before(visible_range.start)
350 ..buffer_snapshot.anchor_after(visible_range.end);
351
352 let visible_excerpts =
353 buffers_to_query
354 .entry(buffer_id)
355 .or_insert_with(|| VisibleExcerpts {
356 excerpts: Vec::new(),
357 ranges: Vec::new(),
358 buffer_version: buffer_version.clone(),
359 buffer: buffer.clone(),
360 });
361 visible_excerpts.buffer_version = buffer_version;
362 visible_excerpts.excerpts.push(excerpt_id);
363 visible_excerpts.ranges.push(buffer_anchor_range);
364 }
365
366 let all_affected_buffers = Arc::new(Mutex::new(all_affected_buffers));
367 for (buffer_id, visible_excerpts) in buffers_to_query {
368 let fetched_tasks = inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default();
369 if visible_excerpts
370 .buffer_version
371 .changed_since(&fetched_tasks.0)
372 {
373 fetched_tasks.1.clear();
374 fetched_tasks.0 = visible_excerpts.buffer_version.clone();
375 inlay_hints.hint_refresh_tasks.remove(&buffer_id);
376 }
377
378 let applicable_chunks =
379 semantics_provider.applicable_inlay_chunks(buffer_id, &visible_excerpts.ranges, cx);
380
381 match inlay_hints
382 .hint_refresh_tasks
383 .entry(buffer_id)
384 .or_default()
385 .entry(applicable_chunks)
386 {
387 hash_map::Entry::Occupied(mut o) => {
388 if invalidate_cache.should_invalidate() || ignore_previous_fetches {
389 o.get_mut().push(spawn_editor_hints_refresh(
390 buffer_id,
391 invalidate_cache,
392 ignore_previous_fetches,
393 debounce,
394 visible_excerpts,
395 all_affected_buffers.clone(),
396 cx,
397 ));
398 }
399 }
400 hash_map::Entry::Vacant(v) => {
401 v.insert(Vec::new()).push(spawn_editor_hints_refresh(
402 buffer_id,
403 invalidate_cache,
404 ignore_previous_fetches,
405 debounce,
406 visible_excerpts,
407 all_affected_buffers.clone(),
408 cx,
409 ));
410 }
411 }
412 }
413 }
414
415 pub fn clear_inlay_hints(&mut self, cx: &mut Context<Self>) {
416 let to_remove = self
417 .visible_inlay_hints(cx)
418 .into_iter()
419 .map(|inlay| {
420 let inlay_id = inlay.id;
421 if let Some(inlay_hints) = &mut self.inlay_hints {
422 inlay_hints.added_hints.remove(&inlay_id);
423 }
424 inlay_id
425 })
426 .collect::<Vec<_>>();
427 self.splice_inlays(&to_remove, Vec::new(), cx);
428 }
429
430 fn refresh_editor_data(
431 &mut self,
432 reason: &InlayHintRefreshReason,
433 cx: &mut Context<'_, Editor>,
434 ) -> Option<InvalidationStrategy> {
435 let visible_inlay_hints = self.visible_inlay_hints(cx);
436 let Some(inlay_hints) = self.inlay_hints.as_mut() else {
437 return None;
438 };
439
440 let invalidate_cache = match reason {
441 InlayHintRefreshReason::ModifiersChanged(enabled) => {
442 match inlay_hints.modifiers_override(*enabled) {
443 Some(enabled) => {
444 if enabled {
445 InvalidationStrategy::None
446 } else {
447 self.clear_inlay_hints(cx);
448 return None;
449 }
450 }
451 None => return None,
452 }
453 }
454 InlayHintRefreshReason::Toggle(enabled) => {
455 if inlay_hints.toggle(*enabled) {
456 if *enabled {
457 InvalidationStrategy::None
458 } else {
459 self.clear_inlay_hints(cx);
460 return None;
461 }
462 } else {
463 return None;
464 }
465 }
466 InlayHintRefreshReason::SettingsChange(new_settings) => {
467 match inlay_hints.update_settings(*new_settings, visible_inlay_hints) {
468 ControlFlow::Break(Some(InlaySplice {
469 to_remove,
470 to_insert,
471 })) => {
472 self.splice_inlays(&to_remove, to_insert, cx);
473 return None;
474 }
475 ControlFlow::Break(None) => return None,
476 ControlFlow::Continue(splice) => {
477 if let Some(InlaySplice {
478 to_remove,
479 to_insert,
480 }) = splice
481 {
482 self.splice_inlays(&to_remove, to_insert, cx);
483 }
484 InvalidationStrategy::None
485 }
486 }
487 }
488 InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => {
489 let to_remove = self
490 .display_map
491 .read(cx)
492 .current_inlays()
493 .filter_map(|inlay| {
494 if excerpts_removed.contains(&inlay.position.excerpt_id) {
495 Some(inlay.id)
496 } else {
497 None
498 }
499 })
500 .collect::<Vec<_>>();
501 self.splice_inlays(&to_remove, Vec::new(), cx);
502 return None;
503 }
504 InlayHintRefreshReason::NewLinesShown => InvalidationStrategy::None,
505 InlayHintRefreshReason::BufferEdited(_) => InvalidationStrategy::BufferEdited,
506 InlayHintRefreshReason::RefreshRequested(server_id) => {
507 InvalidationStrategy::RefreshRequested(*server_id)
508 }
509 };
510
511 match &mut self.inlay_hints {
512 Some(inlay_hints) => {
513 if !inlay_hints.enabled
514 && !matches!(reason, InlayHintRefreshReason::ModifiersChanged(_))
515 {
516 return None;
517 }
518 }
519 None => return None,
520 }
521
522 Some(invalidate_cache)
523 }
524
525 pub(crate) fn visible_inlay_hints(&self, cx: &Context<Editor>) -> Vec<Inlay> {
526 self.display_map
527 .read(cx)
528 .current_inlays()
529 .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
530 .cloned()
531 .collect()
532 }
533
534 pub fn update_inlay_link_and_hover_points(
535 &mut self,
536 snapshot: &EditorSnapshot,
537 point_for_position: PointForPosition,
538 secondary_held: bool,
539 shift_held: bool,
540 window: &mut Window,
541 cx: &mut Context<Self>,
542 ) {
543 let Some(lsp_store) = self.project().map(|project| project.read(cx).lsp_store()) else {
544 return;
545 };
546 let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
547 Some(
548 snapshot
549 .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left),
550 )
551 } else {
552 None
553 };
554 let mut go_to_definition_updated = false;
555 let mut hover_updated = false;
556 if let Some(hovered_offset) = hovered_offset {
557 let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
558 let previous_valid_anchor = buffer_snapshot.anchor_at(
559 point_for_position.previous_valid.to_point(snapshot),
560 Bias::Left,
561 );
562 let next_valid_anchor = buffer_snapshot.anchor_at(
563 point_for_position.next_valid.to_point(snapshot),
564 Bias::Right,
565 );
566 if let Some(hovered_hint) = self
567 .visible_inlay_hints(cx)
568 .into_iter()
569 .skip_while(|hint| {
570 hint.position
571 .cmp(&previous_valid_anchor, &buffer_snapshot)
572 .is_lt()
573 })
574 .take_while(|hint| {
575 hint.position
576 .cmp(&next_valid_anchor, &buffer_snapshot)
577 .is_le()
578 })
579 .max_by_key(|hint| hint.id)
580 {
581 if let Some(ResolvedHint::Resolved(cached_hint)) =
582 hovered_hint.position.buffer_id.and_then(|buffer_id| {
583 lsp_store.update(cx, |lsp_store, cx| {
584 lsp_store.resolved_hint(buffer_id, hovered_hint.id, cx)
585 })
586 })
587 {
588 match cached_hint.resolve_state {
589 ResolveState::Resolved => {
590 let mut extra_shift_left = 0;
591 let mut extra_shift_right = 0;
592 if cached_hint.padding_left {
593 extra_shift_left += 1;
594 extra_shift_right += 1;
595 }
596 if cached_hint.padding_right {
597 extra_shift_right += 1;
598 }
599 match cached_hint.label {
600 InlayHintLabel::String(_) => {
601 if let Some(tooltip) = cached_hint.tooltip {
602 hover_popover::hover_at_inlay(
603 self,
604 InlayHover {
605 tooltip: match tooltip {
606 InlayHintTooltip::String(text) => HoverBlock {
607 text,
608 kind: HoverBlockKind::PlainText,
609 },
610 InlayHintTooltip::MarkupContent(content) => {
611 HoverBlock {
612 text: content.value,
613 kind: content.kind,
614 }
615 }
616 },
617 range: InlayHighlight {
618 inlay: hovered_hint.id,
619 inlay_position: hovered_hint.position,
620 range: extra_shift_left
621 ..hovered_hint.text().len()
622 + extra_shift_right,
623 },
624 },
625 window,
626 cx,
627 );
628 hover_updated = true;
629 }
630 }
631 InlayHintLabel::LabelParts(label_parts) => {
632 let hint_start =
633 snapshot.anchor_to_inlay_offset(hovered_hint.position);
634 if let Some((hovered_hint_part, part_range)) =
635 hover_popover::find_hovered_hint_part(
636 label_parts,
637 hint_start,
638 hovered_offset,
639 )
640 {
641 let highlight_start =
642 (part_range.start - hint_start).0 + extra_shift_left;
643 let highlight_end =
644 (part_range.end - hint_start).0 + extra_shift_right;
645 let highlight = InlayHighlight {
646 inlay: hovered_hint.id,
647 inlay_position: hovered_hint.position,
648 range: highlight_start..highlight_end,
649 };
650 if let Some(tooltip) = hovered_hint_part.tooltip {
651 hover_popover::hover_at_inlay(
652 self,
653 InlayHover {
654 tooltip: match tooltip {
655 InlayHintLabelPartTooltip::String(text) => {
656 HoverBlock {
657 text,
658 kind: HoverBlockKind::PlainText,
659 }
660 }
661 InlayHintLabelPartTooltip::MarkupContent(
662 content,
663 ) => HoverBlock {
664 text: content.value,
665 kind: content.kind,
666 },
667 },
668 range: highlight.clone(),
669 },
670 window,
671 cx,
672 );
673 hover_updated = true;
674 }
675 if let Some((language_server_id, location)) =
676 hovered_hint_part.location
677 && secondary_held
678 && !self.has_pending_nonempty_selection()
679 {
680 go_to_definition_updated = true;
681 show_link_definition(
682 shift_held,
683 self,
684 TriggerPoint::InlayHint(
685 highlight,
686 location,
687 language_server_id,
688 ),
689 snapshot,
690 window,
691 cx,
692 );
693 }
694 }
695 }
696 };
697 }
698 ResolveState::CanResolve(_, _) => debug_panic!(
699 "Expected resolved_hint retrieval to return a resolved hint"
700 ),
701 ResolveState::Resolving => {}
702 }
703 }
704 }
705 }
706
707 if !go_to_definition_updated {
708 self.hide_hovered_link(cx)
709 }
710 if !hover_updated {
711 hover_popover::hover_at(self, None, window, cx);
712 }
713 }
714
715 fn inlay_hints_for_buffer(
716 &mut self,
717 invalidate_cache: InvalidationStrategy,
718 ignore_previous_fetches: bool,
719 buffer_excerpts: VisibleExcerpts,
720 cx: &mut Context<Self>,
721 ) -> Option<Vec<Task<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>>> {
722 let semantics_provider = self.semantics_provider()?;
723 let inlay_hints = self.inlay_hints.as_mut()?;
724 let buffer_id = buffer_excerpts.buffer.read(cx).remote_id();
725
726 let new_hint_tasks = semantics_provider
727 .inlay_hints(
728 invalidate_cache,
729 buffer_excerpts.buffer,
730 buffer_excerpts.ranges,
731 inlay_hints
732 .hint_chunk_fetched
733 .get(&buffer_id)
734 .filter(|_| !ignore_previous_fetches && !invalidate_cache.should_invalidate())
735 .cloned(),
736 cx,
737 )
738 .unwrap_or_default();
739
740 let (known_version, known_chunks) =
741 inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default();
742 if buffer_excerpts.buffer_version.changed_since(known_version) {
743 known_chunks.clear();
744 *known_version = buffer_excerpts.buffer_version;
745 }
746
747 let mut hint_tasks = Vec::new();
748 for (row_range, new_hints_task) in new_hint_tasks {
749 let inserted = known_chunks.insert(row_range.clone());
750 if inserted || ignore_previous_fetches || invalidate_cache.should_invalidate() {
751 hint_tasks.push(cx.spawn(async move |_, _| (row_range, new_hints_task.await)));
752 }
753 }
754
755 Some(hint_tasks)
756 }
757
758 fn apply_fetched_hints(
759 &mut self,
760 buffer_id: BufferId,
761 query_version: Global,
762 invalidate_cache: InvalidationStrategy,
763 new_hints: Vec<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>,
764 all_affected_buffers: Arc<Mutex<HashSet<BufferId>>>,
765 cx: &mut Context<Self>,
766 ) {
767 let visible_inlay_hint_ids = self
768 .visible_inlay_hints(cx)
769 .iter()
770 .filter(|inlay| inlay.position.buffer_id == Some(buffer_id))
771 .map(|inlay| inlay.id)
772 .collect::<Vec<_>>();
773 let Some(inlay_hints) = &mut self.inlay_hints else {
774 return;
775 };
776
777 let mut hints_to_remove = Vec::new();
778 let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
779
780 // If we've received hints from the cache, it means `invalidate_cache` had invalidated whatever possible there,
781 // and most probably there are no more hints with IDs from `visible_inlay_hint_ids` in the cache.
782 // So, if we hover such hints, no resolve will happen.
783 //
784 // Another issue is in the fact that changing one buffer may lead to other buffers' hints changing, so more cache entries may be removed.
785 // Hence, clear all excerpts' hints in the multi buffer: later, the invalidated ones will re-trigger the LSP query, the rest will be restored
786 // from the cache.
787 if invalidate_cache.should_invalidate() {
788 hints_to_remove.extend(visible_inlay_hint_ids);
789 }
790
791 let excerpts = self.buffer.read(cx).excerpt_ids();
792 let hints_to_insert = new_hints
793 .into_iter()
794 .filter_map(|(chunk_range, hints_result)| match hints_result {
795 Ok(new_hints) => Some(new_hints),
796 Err(e) => {
797 log::error!(
798 "Failed to query inlays for buffer row range {chunk_range:?}, {e:#}"
799 );
800 if let Some((for_version, chunks_fetched)) =
801 inlay_hints.hint_chunk_fetched.get_mut(&buffer_id)
802 {
803 if for_version == &query_version {
804 chunks_fetched.remove(&chunk_range);
805 }
806 }
807 None
808 }
809 })
810 .flat_map(|hints| hints.into_values())
811 .flatten()
812 .filter_map(|(hint_id, lsp_hint)| {
813 if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
814 && inlay_hints
815 .added_hints
816 .insert(hint_id, lsp_hint.kind)
817 .is_none()
818 {
819 let position = excerpts.iter().find_map(|excerpt_id| {
820 multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, lsp_hint.position)
821 })?;
822 return Some(Inlay::hint(hint_id, position, &lsp_hint));
823 }
824 None
825 })
826 .collect::<Vec<_>>();
827
828 // We need to invalidate excerpts all buffers with the same language, do that once only, after first new data chunk is inserted.
829 let all_other_affected_buffers = all_affected_buffers
830 .lock()
831 .drain()
832 .filter(|id| buffer_id != *id)
833 .collect::<HashSet<_>>();
834 if !all_other_affected_buffers.is_empty() {
835 hints_to_remove.extend(
836 self.visible_inlay_hints(cx)
837 .iter()
838 .filter(|inlay| {
839 inlay
840 .position
841 .buffer_id
842 .is_none_or(|buffer_id| all_other_affected_buffers.contains(&buffer_id))
843 })
844 .map(|inlay| inlay.id),
845 );
846 }
847
848 self.splice_inlays(&hints_to_remove, hints_to_insert, cx);
849 }
850}
851
852#[derive(Debug)]
853struct VisibleExcerpts {
854 excerpts: Vec<ExcerptId>,
855 ranges: Vec<Range<text::Anchor>>,
856 buffer_version: Global,
857 buffer: Entity<language::Buffer>,
858}
859
860fn spawn_editor_hints_refresh(
861 buffer_id: BufferId,
862 invalidate_cache: InvalidationStrategy,
863 ignore_previous_fetches: bool,
864 debounce: Option<Duration>,
865 buffer_excerpts: VisibleExcerpts,
866 all_affected_buffers: Arc<Mutex<HashSet<BufferId>>>,
867 cx: &mut Context<'_, Editor>,
868) -> Task<()> {
869 cx.spawn(async move |editor, cx| {
870 if let Some(debounce) = debounce {
871 cx.background_executor().timer(debounce).await;
872 }
873
874 let query_version = buffer_excerpts.buffer_version.clone();
875 let Some(hint_tasks) = editor
876 .update(cx, |editor, cx| {
877 editor.inlay_hints_for_buffer(
878 invalidate_cache,
879 ignore_previous_fetches,
880 buffer_excerpts,
881 cx,
882 )
883 })
884 .ok()
885 else {
886 return;
887 };
888 let hint_tasks = hint_tasks.unwrap_or_default();
889 if hint_tasks.is_empty() {
890 return;
891 }
892 let new_hints = join_all(hint_tasks).await;
893 editor
894 .update(cx, |editor, cx| {
895 editor.apply_fetched_hints(
896 buffer_id,
897 query_version,
898 invalidate_cache,
899 new_hints,
900 all_affected_buffers,
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 = "test_progress_token";
1191 fake_server
1192 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
1193 token: lsp::ProgressToken::String(progress_token.to_string()),
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::String(progress_token.to_string()),
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::String(progress_token.to_string()),
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.end,
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, 11))
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, 11))
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 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
3658 cx.update(|cx| {
3659 let settings_store = SettingsStore::test(cx);
3660 cx.set_global(settings_store);
3661 theme::init(theme::LoadThemes::JustBase, cx);
3662 release_channel::init(SemanticVersion::default(), cx);
3663 client::init_settings(cx);
3664 language::init(cx);
3665 Project::init_settings(cx);
3666 workspace::init_settings(cx);
3667 crate::init(cx);
3668 });
3669
3670 update_test_language_settings(cx, f);
3671 }
3672
3673 async fn prepare_test_objects(
3674 cx: &mut TestAppContext,
3675 initialize: impl 'static + Send + Fn(&mut FakeLanguageServer, &'static str) + Send + Sync,
3676 ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
3677 let fs = FakeFs::new(cx.background_executor.clone());
3678 fs.insert_tree(
3679 path!("/a"),
3680 json!({
3681 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
3682 "other.rs": "// Test file",
3683 }),
3684 )
3685 .await;
3686
3687 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3688 let file_path = path!("/a/main.rs");
3689
3690 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3691 language_registry.add(rust_lang());
3692 let mut fake_servers = language_registry.register_fake_lsp(
3693 "Rust",
3694 FakeLspAdapter {
3695 capabilities: lsp::ServerCapabilities {
3696 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3697 ..lsp::ServerCapabilities::default()
3698 },
3699 initializer: Some(Box::new(move |server| initialize(server, file_path))),
3700 ..FakeLspAdapter::default()
3701 },
3702 );
3703
3704 let buffer = project
3705 .update(cx, |project, cx| {
3706 project.open_local_buffer(path!("/a/main.rs"), cx)
3707 })
3708 .await
3709 .unwrap();
3710 let editor =
3711 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3712
3713 editor
3714 .update(cx, |editor, _, cx| {
3715 assert!(cached_hint_labels(editor, cx).is_empty());
3716 assert!(visible_hint_labels(editor, cx).is_empty());
3717 })
3718 .unwrap();
3719
3720 cx.executor().run_until_parked();
3721 let fake_server = fake_servers.next().await.unwrap();
3722 (file_path, editor, fake_server)
3723 }
3724
3725 // 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.
3726 // Ensure a stable order for testing.
3727 fn sorted_cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
3728 let mut labels = cached_hint_labels(editor, cx);
3729 labels.sort_by(|a, b| natural_sort(a, b));
3730 labels
3731 }
3732
3733 pub fn cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
3734 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
3735
3736 let mut all_cached_labels = Vec::new();
3737 let mut all_fetched_hints = Vec::new();
3738 for buffer in editor.buffer.read(cx).all_buffers() {
3739 lsp_store.update(cx, |lsp_store, cx| {
3740 let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
3741 all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
3742 let mut label = hint.text().to_string();
3743 if hint.padding_left {
3744 label.insert(0, ' ');
3745 }
3746 if hint.padding_right {
3747 label.push_str(" ");
3748 }
3749 label
3750 }));
3751 all_fetched_hints.extend(hints.all_fetched_hints());
3752 });
3753 }
3754
3755 all_cached_labels
3756 }
3757
3758 pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
3759 editor
3760 .visible_inlay_hints(cx)
3761 .into_iter()
3762 .map(|hint| hint.text().to_string())
3763 .collect()
3764 }
3765
3766 fn allowed_hint_kinds_for_editor(editor: &Editor) -> HashSet<Option<InlayHintKind>> {
3767 editor
3768 .inlay_hints
3769 .as_ref()
3770 .unwrap()
3771 .allowed_hint_kinds
3772 .clone()
3773 }
3774}