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