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