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