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