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