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