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