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