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 buffer = excerpt_buffer.read(cx);
747 let full_excerpt_range = multi_buffer
748 .excerpts_for_buffer(buffer.remote_id(), cx)
749 .into_iter()
750 .find(|(id, _)| id == &excerpt_id)
751 .map(|(_, range)| range.context)?;
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 use util::path;
1278
1279 use super::*;
1280
1281 #[gpui::test]
1282 async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
1283 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1284 init_test(cx, |settings| {
1285 settings.defaults.inlay_hints = Some(InlayHintSettings {
1286 enabled: true,
1287 edit_debounce_ms: 0,
1288 scroll_debounce_ms: 0,
1289 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1290 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1291 show_other_hints: allowed_hint_kinds.contains(&None),
1292 show_background: false,
1293 })
1294 });
1295 let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1296 let lsp_request_count = Arc::new(AtomicU32::new(0));
1297 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1298 let task_lsp_request_count = Arc::clone(&lsp_request_count);
1299 async move {
1300 let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1;
1301 assert_eq!(
1302 params.text_document.uri,
1303 lsp::Url::from_file_path(file_with_hints).unwrap(),
1304 );
1305 Ok(Some(vec![lsp::InlayHint {
1306 position: lsp::Position::new(0, i),
1307 label: lsp::InlayHintLabel::String(i.to_string()),
1308 kind: None,
1309 text_edits: None,
1310 tooltip: None,
1311 padding_left: None,
1312 padding_right: None,
1313 data: None,
1314 }]))
1315 }
1316 });
1317 })
1318 .await;
1319 cx.executor().run_until_parked();
1320
1321 editor
1322 .update(cx, |editor, _window, cx| {
1323 let expected_hints = vec!["1".to_string()];
1324 assert_eq!(
1325 expected_hints,
1326 cached_hint_labels(editor),
1327 "Should get its first hints when opening the editor"
1328 );
1329 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1330 let inlay_cache = editor.inlay_hint_cache();
1331 assert_eq!(
1332 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1333 "Cache should use editor settings to get the allowed hint kinds"
1334 );
1335 })
1336 .unwrap();
1337
1338 editor
1339 .update(cx, |editor, window, cx| {
1340 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1341 editor.handle_input("some change", window, cx);
1342 })
1343 .unwrap();
1344 cx.executor().run_until_parked();
1345 editor
1346 .update(cx, |editor, _window, cx| {
1347 let expected_hints = vec!["2".to_string()];
1348 assert_eq!(
1349 expected_hints,
1350 cached_hint_labels(editor),
1351 "Should get new hints after an edit"
1352 );
1353 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1354 let inlay_cache = editor.inlay_hint_cache();
1355 assert_eq!(
1356 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1357 "Cache should use editor settings to get the allowed hint kinds"
1358 );
1359 })
1360 .unwrap();
1361
1362 fake_server
1363 .request::<lsp::request::InlayHintRefreshRequest>(())
1364 .await
1365 .expect("inlay refresh request failed");
1366 cx.executor().run_until_parked();
1367 editor
1368 .update(cx, |editor, _window, cx| {
1369 let expected_hints = vec!["3".to_string()];
1370 assert_eq!(
1371 expected_hints,
1372 cached_hint_labels(editor),
1373 "Should get new hints after hint refresh/ request"
1374 );
1375 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1376 let inlay_cache = editor.inlay_hint_cache();
1377 assert_eq!(
1378 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1379 "Cache should use editor settings to get the allowed hint kinds"
1380 );
1381 })
1382 .unwrap();
1383 }
1384
1385 #[gpui::test]
1386 async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
1387 init_test(cx, |settings| {
1388 settings.defaults.inlay_hints = Some(InlayHintSettings {
1389 enabled: true,
1390 edit_debounce_ms: 0,
1391 scroll_debounce_ms: 0,
1392 show_type_hints: true,
1393 show_parameter_hints: true,
1394 show_other_hints: true,
1395 show_background: false,
1396 })
1397 });
1398
1399 let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1400 let lsp_request_count = Arc::new(AtomicU32::new(0));
1401 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1402 let task_lsp_request_count = Arc::clone(&lsp_request_count);
1403 async move {
1404 assert_eq!(
1405 params.text_document.uri,
1406 lsp::Url::from_file_path(file_with_hints).unwrap(),
1407 );
1408 let current_call_id =
1409 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1410 Ok(Some(vec![lsp::InlayHint {
1411 position: lsp::Position::new(0, current_call_id),
1412 label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1413 kind: None,
1414 text_edits: None,
1415 tooltip: None,
1416 padding_left: None,
1417 padding_right: None,
1418 data: None,
1419 }]))
1420 }
1421 });
1422 })
1423 .await;
1424 cx.executor().run_until_parked();
1425
1426 editor
1427 .update(cx, |editor, _, cx| {
1428 let expected_hints = vec!["0".to_string()];
1429 assert_eq!(
1430 expected_hints,
1431 cached_hint_labels(editor),
1432 "Should get its first hints when opening the editor"
1433 );
1434 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1435 })
1436 .unwrap();
1437
1438 let progress_token = "test_progress_token";
1439 fake_server
1440 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
1441 token: lsp::ProgressToken::String(progress_token.to_string()),
1442 })
1443 .await
1444 .expect("work done progress create request failed");
1445 cx.executor().run_until_parked();
1446 fake_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1447 token: lsp::ProgressToken::String(progress_token.to_string()),
1448 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1449 lsp::WorkDoneProgressBegin::default(),
1450 )),
1451 });
1452 cx.executor().run_until_parked();
1453
1454 editor
1455 .update(cx, |editor, _, cx| {
1456 let expected_hints = vec!["0".to_string()];
1457 assert_eq!(
1458 expected_hints,
1459 cached_hint_labels(editor),
1460 "Should not update hints while the work task is running"
1461 );
1462 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1463 })
1464 .unwrap();
1465
1466 fake_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1467 token: lsp::ProgressToken::String(progress_token.to_string()),
1468 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1469 lsp::WorkDoneProgressEnd::default(),
1470 )),
1471 });
1472 cx.executor().run_until_parked();
1473
1474 editor
1475 .update(cx, |editor, _, cx| {
1476 let expected_hints = vec!["1".to_string()];
1477 assert_eq!(
1478 expected_hints,
1479 cached_hint_labels(editor),
1480 "New hints should be queried after the work task is done"
1481 );
1482 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1483 })
1484 .unwrap();
1485 }
1486
1487 #[gpui::test]
1488 async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1489 init_test(cx, |settings| {
1490 settings.defaults.inlay_hints = Some(InlayHintSettings {
1491 enabled: true,
1492 edit_debounce_ms: 0,
1493 scroll_debounce_ms: 0,
1494 show_type_hints: true,
1495 show_parameter_hints: true,
1496 show_other_hints: true,
1497 show_background: false,
1498 })
1499 });
1500
1501 let fs = FakeFs::new(cx.background_executor.clone());
1502 fs.insert_tree(
1503 path!("/a"),
1504 json!({
1505 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1506 "other.md": "Test md file with some text",
1507 }),
1508 )
1509 .await;
1510
1511 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1512
1513 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1514 let mut rs_fake_servers = None;
1515 let mut md_fake_servers = None;
1516 for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1517 language_registry.add(Arc::new(Language::new(
1518 LanguageConfig {
1519 name: name.into(),
1520 matcher: LanguageMatcher {
1521 path_suffixes: vec![path_suffix.to_string()],
1522 ..Default::default()
1523 },
1524 ..Default::default()
1525 },
1526 Some(tree_sitter_rust::LANGUAGE.into()),
1527 )));
1528 let fake_servers = language_registry.register_fake_lsp(
1529 name,
1530 FakeLspAdapter {
1531 name,
1532 capabilities: lsp::ServerCapabilities {
1533 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1534 ..Default::default()
1535 },
1536 initializer: Some(Box::new({
1537 move |fake_server| {
1538 let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1539 let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1540 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
1541 move |params, _| {
1542 let i = match name {
1543 "Rust" => {
1544 assert_eq!(
1545 params.text_document.uri,
1546 lsp::Url::from_file_path(path!("/a/main.rs"))
1547 .unwrap(),
1548 );
1549 rs_lsp_request_count.fetch_add(1, Ordering::Release) + 1
1550 }
1551 "Markdown" => {
1552 assert_eq!(
1553 params.text_document.uri,
1554 lsp::Url::from_file_path(path!("/a/other.md"))
1555 .unwrap(),
1556 );
1557 md_lsp_request_count.fetch_add(1, Ordering::Release) + 1
1558 }
1559 unexpected => panic!("Unexpected language: {unexpected}"),
1560 };
1561
1562 async move {
1563 let query_start = params.range.start;
1564 Ok(Some(vec![lsp::InlayHint {
1565 position: query_start,
1566 label: lsp::InlayHintLabel::String(i.to_string()),
1567 kind: None,
1568 text_edits: None,
1569 tooltip: None,
1570 padding_left: None,
1571 padding_right: None,
1572 data: None,
1573 }]))
1574 }
1575 },
1576 );
1577 }
1578 })),
1579 ..Default::default()
1580 },
1581 );
1582 match name {
1583 "Rust" => rs_fake_servers = Some(fake_servers),
1584 "Markdown" => md_fake_servers = Some(fake_servers),
1585 _ => unreachable!(),
1586 }
1587 }
1588
1589 let rs_buffer = project
1590 .update(cx, |project, cx| {
1591 project.open_local_buffer(path!("/a/main.rs"), cx)
1592 })
1593 .await
1594 .unwrap();
1595 let rs_editor = cx.add_window(|window, cx| {
1596 Editor::for_buffer(rs_buffer, Some(project.clone()), window, cx)
1597 });
1598 cx.executor().run_until_parked();
1599
1600 let _rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1601 cx.executor().run_until_parked();
1602 rs_editor
1603 .update(cx, |editor, _window, cx| {
1604 let expected_hints = vec!["1".to_string()];
1605 assert_eq!(
1606 expected_hints,
1607 cached_hint_labels(editor),
1608 "Should get its first hints when opening the editor"
1609 );
1610 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1611 })
1612 .unwrap();
1613
1614 cx.executor().run_until_parked();
1615 let md_buffer = project
1616 .update(cx, |project, cx| {
1617 project.open_local_buffer(path!("/a/other.md"), cx)
1618 })
1619 .await
1620 .unwrap();
1621 let md_editor =
1622 cx.add_window(|window, cx| Editor::for_buffer(md_buffer, Some(project), window, cx));
1623 cx.executor().run_until_parked();
1624
1625 let _md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1626 cx.executor().run_until_parked();
1627 md_editor
1628 .update(cx, |editor, _window, cx| {
1629 let expected_hints = vec!["1".to_string()];
1630 assert_eq!(
1631 expected_hints,
1632 cached_hint_labels(editor),
1633 "Markdown editor should have a separate version, repeating Rust editor rules"
1634 );
1635 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1636 })
1637 .unwrap();
1638
1639 rs_editor
1640 .update(cx, |editor, window, cx| {
1641 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1642 editor.handle_input("some rs change", window, cx);
1643 })
1644 .unwrap();
1645 cx.executor().run_until_parked();
1646 rs_editor
1647 .update(cx, |editor, _window, cx| {
1648 // TODO: Here, we do not get "2", because inserting another language server will trigger `RefreshInlayHints` event from the `LspStore`
1649 // A project is listened in every editor, so each of them will react to this event.
1650 //
1651 // We do not have language server IDs for remote projects, so cannot easily say on the editor level,
1652 // whether we should ignore a particular `RefreshInlayHints` event.
1653 let expected_hints = vec!["3".to_string()];
1654 assert_eq!(
1655 expected_hints,
1656 cached_hint_labels(editor),
1657 "Rust inlay cache should change after the edit"
1658 );
1659 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1660 })
1661 .unwrap();
1662 md_editor
1663 .update(cx, |editor, _window, cx| {
1664 let expected_hints = vec!["1".to_string()];
1665 assert_eq!(
1666 expected_hints,
1667 cached_hint_labels(editor),
1668 "Markdown editor should not be affected by Rust editor changes"
1669 );
1670 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1671 })
1672 .unwrap();
1673
1674 md_editor
1675 .update(cx, |editor, window, cx| {
1676 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1677 editor.handle_input("some md change", window, cx);
1678 })
1679 .unwrap();
1680 cx.executor().run_until_parked();
1681 md_editor
1682 .update(cx, |editor, _window, cx| {
1683 let expected_hints = vec!["2".to_string()];
1684 assert_eq!(
1685 expected_hints,
1686 cached_hint_labels(editor),
1687 "Rust editor should not be affected by Markdown editor changes"
1688 );
1689 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1690 })
1691 .unwrap();
1692 rs_editor
1693 .update(cx, |editor, _window, cx| {
1694 let expected_hints = vec!["3".to_string()];
1695 assert_eq!(
1696 expected_hints,
1697 cached_hint_labels(editor),
1698 "Markdown editor should also change independently"
1699 );
1700 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1701 })
1702 .unwrap();
1703 }
1704
1705 #[gpui::test]
1706 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1707 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1708 init_test(cx, |settings| {
1709 settings.defaults.inlay_hints = Some(InlayHintSettings {
1710 enabled: true,
1711 edit_debounce_ms: 0,
1712 scroll_debounce_ms: 0,
1713 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1714 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1715 show_other_hints: allowed_hint_kinds.contains(&None),
1716 show_background: false,
1717 })
1718 });
1719
1720 let lsp_request_count = Arc::new(AtomicUsize::new(0));
1721 let (_, editor, fake_server) = prepare_test_objects(cx, {
1722 let lsp_request_count = lsp_request_count.clone();
1723 move |fake_server, file_with_hints| {
1724 let lsp_request_count = lsp_request_count.clone();
1725 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
1726 move |params, _| {
1727 lsp_request_count.fetch_add(1, Ordering::Release);
1728 async move {
1729 assert_eq!(
1730 params.text_document.uri,
1731 lsp::Url::from_file_path(file_with_hints).unwrap(),
1732 );
1733 Ok(Some(vec![
1734 lsp::InlayHint {
1735 position: lsp::Position::new(0, 1),
1736 label: lsp::InlayHintLabel::String("type hint".to_string()),
1737 kind: Some(lsp::InlayHintKind::TYPE),
1738 text_edits: None,
1739 tooltip: None,
1740 padding_left: None,
1741 padding_right: None,
1742 data: None,
1743 },
1744 lsp::InlayHint {
1745 position: lsp::Position::new(0, 2),
1746 label: lsp::InlayHintLabel::String(
1747 "parameter hint".to_string(),
1748 ),
1749 kind: Some(lsp::InlayHintKind::PARAMETER),
1750 text_edits: None,
1751 tooltip: None,
1752 padding_left: None,
1753 padding_right: None,
1754 data: None,
1755 },
1756 lsp::InlayHint {
1757 position: lsp::Position::new(0, 3),
1758 label: lsp::InlayHintLabel::String("other hint".to_string()),
1759 kind: None,
1760 text_edits: None,
1761 tooltip: None,
1762 padding_left: None,
1763 padding_right: None,
1764 data: None,
1765 },
1766 ]))
1767 }
1768 },
1769 );
1770 }
1771 })
1772 .await;
1773 cx.executor().run_until_parked();
1774
1775 editor
1776 .update(cx, |editor, _, cx| {
1777 assert_eq!(
1778 lsp_request_count.load(Ordering::Relaxed),
1779 1,
1780 "Should query new hints once"
1781 );
1782 assert_eq!(
1783 vec![
1784 "type hint".to_string(),
1785 "parameter hint".to_string(),
1786 "other hint".to_string(),
1787 ],
1788 cached_hint_labels(editor),
1789 "Should get its first hints when opening the editor"
1790 );
1791 assert_eq!(
1792 vec!["type hint".to_string(), "other hint".to_string()],
1793 visible_hint_labels(editor, cx)
1794 );
1795 let inlay_cache = editor.inlay_hint_cache();
1796 assert_eq!(
1797 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1798 "Cache should use editor settings to get the allowed hint kinds"
1799 );
1800 })
1801 .unwrap();
1802
1803 fake_server
1804 .request::<lsp::request::InlayHintRefreshRequest>(())
1805 .await
1806 .expect("inlay refresh request failed");
1807 cx.executor().run_until_parked();
1808 editor
1809 .update(cx, |editor, _, cx| {
1810 assert_eq!(
1811 lsp_request_count.load(Ordering::Relaxed),
1812 2,
1813 "Should load new hints twice"
1814 );
1815 assert_eq!(
1816 vec![
1817 "type hint".to_string(),
1818 "parameter hint".to_string(),
1819 "other hint".to_string(),
1820 ],
1821 cached_hint_labels(editor),
1822 "Cached hints should not change due to allowed hint kinds settings update"
1823 );
1824 assert_eq!(
1825 vec!["type hint".to_string(), "other hint".to_string()],
1826 visible_hint_labels(editor, cx)
1827 );
1828 })
1829 .unwrap();
1830
1831 for (new_allowed_hint_kinds, expected_visible_hints) in [
1832 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1833 (
1834 HashSet::from_iter([Some(InlayHintKind::Type)]),
1835 vec!["type hint".to_string()],
1836 ),
1837 (
1838 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1839 vec!["parameter hint".to_string()],
1840 ),
1841 (
1842 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1843 vec!["type hint".to_string(), "other hint".to_string()],
1844 ),
1845 (
1846 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1847 vec!["parameter hint".to_string(), "other hint".to_string()],
1848 ),
1849 (
1850 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1851 vec!["type hint".to_string(), "parameter hint".to_string()],
1852 ),
1853 (
1854 HashSet::from_iter([
1855 None,
1856 Some(InlayHintKind::Type),
1857 Some(InlayHintKind::Parameter),
1858 ]),
1859 vec![
1860 "type hint".to_string(),
1861 "parameter hint".to_string(),
1862 "other hint".to_string(),
1863 ],
1864 ),
1865 ] {
1866 update_test_language_settings(cx, |settings| {
1867 settings.defaults.inlay_hints = Some(InlayHintSettings {
1868 enabled: true,
1869 edit_debounce_ms: 0,
1870 scroll_debounce_ms: 0,
1871 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1872 show_parameter_hints: new_allowed_hint_kinds
1873 .contains(&Some(InlayHintKind::Parameter)),
1874 show_other_hints: new_allowed_hint_kinds.contains(&None),
1875 show_background: false,
1876 })
1877 });
1878 cx.executor().run_until_parked();
1879 editor.update(cx, |editor, _, cx| {
1880 assert_eq!(
1881 lsp_request_count.load(Ordering::Relaxed),
1882 2,
1883 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1884 );
1885 assert_eq!(
1886 vec![
1887 "type hint".to_string(),
1888 "parameter hint".to_string(),
1889 "other hint".to_string(),
1890 ],
1891 cached_hint_labels(editor),
1892 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1893 );
1894 assert_eq!(
1895 expected_visible_hints,
1896 visible_hint_labels(editor, cx),
1897 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1898 );
1899 let inlay_cache = editor.inlay_hint_cache();
1900 assert_eq!(
1901 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1902 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1903 );
1904 }).unwrap();
1905 }
1906
1907 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1908 update_test_language_settings(cx, |settings| {
1909 settings.defaults.inlay_hints = Some(InlayHintSettings {
1910 enabled: false,
1911 edit_debounce_ms: 0,
1912 scroll_debounce_ms: 0,
1913 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1914 show_parameter_hints: another_allowed_hint_kinds
1915 .contains(&Some(InlayHintKind::Parameter)),
1916 show_other_hints: another_allowed_hint_kinds.contains(&None),
1917 show_background: false,
1918 })
1919 });
1920 cx.executor().run_until_parked();
1921 editor
1922 .update(cx, |editor, _, cx| {
1923 assert_eq!(
1924 lsp_request_count.load(Ordering::Relaxed),
1925 2,
1926 "Should not load new hints when hints got disabled"
1927 );
1928 assert!(
1929 cached_hint_labels(editor).is_empty(),
1930 "Should clear the cache when hints got disabled"
1931 );
1932 assert!(
1933 visible_hint_labels(editor, cx).is_empty(),
1934 "Should clear visible hints when hints got disabled"
1935 );
1936 let inlay_cache = editor.inlay_hint_cache();
1937 assert_eq!(
1938 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1939 "Should update its allowed hint kinds even when hints got disabled"
1940 );
1941 })
1942 .unwrap();
1943
1944 fake_server
1945 .request::<lsp::request::InlayHintRefreshRequest>(())
1946 .await
1947 .expect("inlay refresh request failed");
1948 cx.executor().run_until_parked();
1949 editor
1950 .update(cx, |editor, _window, cx| {
1951 assert_eq!(
1952 lsp_request_count.load(Ordering::Relaxed),
1953 2,
1954 "Should not load new hints when they got disabled"
1955 );
1956 assert!(cached_hint_labels(editor).is_empty());
1957 assert!(visible_hint_labels(editor, cx).is_empty());
1958 })
1959 .unwrap();
1960
1961 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1962 update_test_language_settings(cx, |settings| {
1963 settings.defaults.inlay_hints = Some(InlayHintSettings {
1964 enabled: true,
1965 edit_debounce_ms: 0,
1966 scroll_debounce_ms: 0,
1967 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1968 show_parameter_hints: final_allowed_hint_kinds
1969 .contains(&Some(InlayHintKind::Parameter)),
1970 show_other_hints: final_allowed_hint_kinds.contains(&None),
1971 show_background: false,
1972 })
1973 });
1974 cx.executor().run_until_parked();
1975 editor
1976 .update(cx, |editor, _, cx| {
1977 assert_eq!(
1978 lsp_request_count.load(Ordering::Relaxed),
1979 3,
1980 "Should query for new hints when they got re-enabled"
1981 );
1982 assert_eq!(
1983 vec![
1984 "type hint".to_string(),
1985 "parameter hint".to_string(),
1986 "other hint".to_string(),
1987 ],
1988 cached_hint_labels(editor),
1989 "Should get its cached hints fully repopulated after the hints got re-enabled"
1990 );
1991 assert_eq!(
1992 vec!["parameter hint".to_string()],
1993 visible_hint_labels(editor, cx),
1994 "Should get its visible hints repopulated and filtered after the h"
1995 );
1996 let inlay_cache = editor.inlay_hint_cache();
1997 assert_eq!(
1998 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
1999 "Cache should update editor settings when hints got re-enabled"
2000 );
2001 })
2002 .unwrap();
2003
2004 fake_server
2005 .request::<lsp::request::InlayHintRefreshRequest>(())
2006 .await
2007 .expect("inlay refresh request failed");
2008 cx.executor().run_until_parked();
2009 editor
2010 .update(cx, |editor, _, cx| {
2011 assert_eq!(
2012 lsp_request_count.load(Ordering::Relaxed),
2013 4,
2014 "Should query for new hints again"
2015 );
2016 assert_eq!(
2017 vec![
2018 "type hint".to_string(),
2019 "parameter hint".to_string(),
2020 "other hint".to_string(),
2021 ],
2022 cached_hint_labels(editor),
2023 );
2024 assert_eq!(
2025 vec!["parameter hint".to_string()],
2026 visible_hint_labels(editor, cx),
2027 );
2028 })
2029 .unwrap();
2030 }
2031
2032 #[gpui::test]
2033 async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
2034 init_test(cx, |settings| {
2035 settings.defaults.inlay_hints = Some(InlayHintSettings {
2036 enabled: true,
2037 edit_debounce_ms: 0,
2038 scroll_debounce_ms: 0,
2039 show_type_hints: true,
2040 show_parameter_hints: true,
2041 show_other_hints: true,
2042 show_background: false,
2043 })
2044 });
2045
2046 let lsp_request_count = Arc::new(AtomicU32::new(0));
2047 let (_, editor, _) = prepare_test_objects(cx, {
2048 let lsp_request_count = lsp_request_count.clone();
2049 move |fake_server, file_with_hints| {
2050 let lsp_request_count = lsp_request_count.clone();
2051 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
2052 move |params, _| {
2053 let lsp_request_count = lsp_request_count.clone();
2054 async move {
2055 let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
2056 assert_eq!(
2057 params.text_document.uri,
2058 lsp::Url::from_file_path(file_with_hints).unwrap(),
2059 );
2060 Ok(Some(vec![lsp::InlayHint {
2061 position: lsp::Position::new(0, i),
2062 label: lsp::InlayHintLabel::String(i.to_string()),
2063 kind: None,
2064 text_edits: None,
2065 tooltip: None,
2066 padding_left: None,
2067 padding_right: None,
2068 data: None,
2069 }]))
2070 }
2071 },
2072 );
2073 }
2074 })
2075 .await;
2076
2077 let mut expected_changes = Vec::new();
2078 for change_after_opening in [
2079 "initial change #1",
2080 "initial change #2",
2081 "initial change #3",
2082 ] {
2083 editor
2084 .update(cx, |editor, window, cx| {
2085 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
2086 editor.handle_input(change_after_opening, window, cx);
2087 })
2088 .unwrap();
2089 expected_changes.push(change_after_opening);
2090 }
2091
2092 cx.executor().run_until_parked();
2093
2094 editor
2095 .update(cx, |editor, _window, cx| {
2096 let current_text = editor.text(cx);
2097 for change in &expected_changes {
2098 assert!(
2099 current_text.contains(change),
2100 "Should apply all changes made"
2101 );
2102 }
2103 assert_eq!(
2104 lsp_request_count.load(Ordering::Relaxed),
2105 2,
2106 "Should query new hints twice: for editor init and for the last edit that interrupted all others"
2107 );
2108 let expected_hints = vec!["2".to_string()];
2109 assert_eq!(
2110 expected_hints,
2111 cached_hint_labels(editor),
2112 "Should get hints from the last edit landed only"
2113 );
2114 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2115 })
2116 .unwrap();
2117
2118 let mut edits = Vec::new();
2119 for async_later_change in [
2120 "another change #1",
2121 "another change #2",
2122 "another change #3",
2123 ] {
2124 expected_changes.push(async_later_change);
2125 let task_editor = editor;
2126 edits.push(cx.spawn(|mut cx| async move {
2127 task_editor
2128 .update(&mut cx, |editor, window, cx| {
2129 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
2130 editor.handle_input(async_later_change, window, cx);
2131 })
2132 .unwrap();
2133 }));
2134 }
2135 let _ = future::join_all(edits).await;
2136 cx.executor().run_until_parked();
2137
2138 editor
2139 .update(cx, |editor, _, cx| {
2140 let current_text = editor.text(cx);
2141 for change in &expected_changes {
2142 assert!(
2143 current_text.contains(change),
2144 "Should apply all changes made"
2145 );
2146 }
2147 assert_eq!(
2148 lsp_request_count.load(Ordering::SeqCst),
2149 3,
2150 "Should query new hints one more time, for the last edit only"
2151 );
2152 let expected_hints = vec!["3".to_string()];
2153 assert_eq!(
2154 expected_hints,
2155 cached_hint_labels(editor),
2156 "Should get hints from the last edit landed only"
2157 );
2158 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2159 })
2160 .unwrap();
2161 }
2162
2163 #[gpui::test(iterations = 10)]
2164 async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
2165 init_test(cx, |settings| {
2166 settings.defaults.inlay_hints = Some(InlayHintSettings {
2167 enabled: true,
2168 edit_debounce_ms: 0,
2169 scroll_debounce_ms: 0,
2170 show_type_hints: true,
2171 show_parameter_hints: true,
2172 show_other_hints: true,
2173 show_background: false,
2174 })
2175 });
2176
2177 let fs = FakeFs::new(cx.background_executor.clone());
2178 fs.insert_tree(
2179 path!("/a"),
2180 json!({
2181 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
2182 "other.rs": "// Test file",
2183 }),
2184 )
2185 .await;
2186
2187 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2188
2189 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2190 language_registry.add(rust_lang());
2191
2192 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2193 let lsp_request_count = Arc::new(AtomicUsize::new(0));
2194 let mut fake_servers = language_registry.register_fake_lsp(
2195 "Rust",
2196 FakeLspAdapter {
2197 capabilities: lsp::ServerCapabilities {
2198 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2199 ..Default::default()
2200 },
2201 initializer: Some(Box::new({
2202 let lsp_request_ranges = lsp_request_ranges.clone();
2203 let lsp_request_count = lsp_request_count.clone();
2204 move |fake_server| {
2205 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
2206 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2207 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
2208 move |params, _| {
2209 let task_lsp_request_ranges =
2210 Arc::clone(&closure_lsp_request_ranges);
2211 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2212 async move {
2213 assert_eq!(
2214 params.text_document.uri,
2215 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
2216 );
2217
2218 task_lsp_request_ranges.lock().push(params.range);
2219 task_lsp_request_count.fetch_add(1, Ordering::Release);
2220 Ok(Some(vec![lsp::InlayHint {
2221 position: params.range.end,
2222 label: lsp::InlayHintLabel::String(
2223 params.range.end.line.to_string(),
2224 ),
2225 kind: None,
2226 text_edits: None,
2227 tooltip: None,
2228 padding_left: None,
2229 padding_right: None,
2230 data: None,
2231 }]))
2232 }
2233 },
2234 );
2235 }
2236 })),
2237 ..Default::default()
2238 },
2239 );
2240
2241 let buffer = project
2242 .update(cx, |project, cx| {
2243 project.open_local_buffer(path!("/a/main.rs"), cx)
2244 })
2245 .await
2246 .unwrap();
2247 let editor =
2248 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
2249
2250 cx.executor().run_until_parked();
2251
2252 let _fake_server = fake_servers.next().await.unwrap();
2253
2254 // in large buffers, requests are made for more than visible range of a buffer.
2255 // invisible parts are queried later, to avoid excessive requests on quick typing.
2256 // wait the timeout needed to get all requests.
2257 cx.executor().advance_clock(Duration::from_millis(
2258 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2259 ));
2260 cx.executor().run_until_parked();
2261 let initial_visible_range = editor_visible_range(&editor, cx);
2262 let lsp_initial_visible_range = lsp::Range::new(
2263 lsp::Position::new(
2264 initial_visible_range.start.row,
2265 initial_visible_range.start.column,
2266 ),
2267 lsp::Position::new(
2268 initial_visible_range.end.row,
2269 initial_visible_range.end.column,
2270 ),
2271 );
2272 let expected_initial_query_range_end =
2273 lsp::Position::new(initial_visible_range.end.row * 2, 2);
2274 let mut expected_invisible_query_start = lsp_initial_visible_range.end;
2275 expected_invisible_query_start.character += 1;
2276 editor.update(cx, |editor, _window, cx| {
2277 let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2278 assert_eq!(ranges.len(), 2,
2279 "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:?}");
2280 let visible_query_range = &ranges[0];
2281 assert_eq!(visible_query_range.start, lsp_initial_visible_range.start);
2282 assert_eq!(visible_query_range.end, lsp_initial_visible_range.end);
2283 let invisible_query_range = &ranges[1];
2284
2285 assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document");
2286 assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document");
2287
2288 let requests_count = lsp_request_count.load(Ordering::Acquire);
2289 assert_eq!(requests_count, 2, "Visible + invisible request");
2290 let expected_hints = vec!["47".to_string(), "94".to_string()];
2291 assert_eq!(
2292 expected_hints,
2293 cached_hint_labels(editor),
2294 "Should have hints from both LSP requests made for a big file"
2295 );
2296 assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
2297 }).unwrap();
2298
2299 editor
2300 .update(cx, |editor, window, cx| {
2301 editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2302 })
2303 .unwrap();
2304 cx.executor().run_until_parked();
2305 editor
2306 .update(cx, |editor, window, cx| {
2307 editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2308 })
2309 .unwrap();
2310 cx.executor().advance_clock(Duration::from_millis(
2311 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2312 ));
2313 cx.executor().run_until_parked();
2314 let visible_range_after_scrolls = editor_visible_range(&editor, cx);
2315 let visible_line_count = editor
2316 .update(cx, |editor, _window, _| {
2317 editor.visible_line_count().unwrap()
2318 })
2319 .unwrap();
2320 let selection_in_cached_range = editor
2321 .update(cx, |editor, _window, cx| {
2322 let ranges = lsp_request_ranges
2323 .lock()
2324 .drain(..)
2325 .sorted_by_key(|r| r.start)
2326 .collect::<Vec<_>>();
2327 assert_eq!(
2328 ranges.len(),
2329 2,
2330 "Should query 2 ranges after both scrolls, but got: {ranges:?}"
2331 );
2332 let first_scroll = &ranges[0];
2333 let second_scroll = &ranges[1];
2334 assert_eq!(
2335 first_scroll.end, second_scroll.start,
2336 "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
2337 );
2338 assert_eq!(
2339 first_scroll.start, expected_initial_query_range_end,
2340 "First scroll should start the query right after the end of the original scroll",
2341 );
2342 assert_eq!(
2343 second_scroll.end,
2344 lsp::Position::new(
2345 visible_range_after_scrolls.end.row
2346 + visible_line_count.ceil() as u32,
2347 1,
2348 ),
2349 "Second scroll should query one more screen down after the end of the visible range"
2350 );
2351
2352 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2353 assert_eq!(lsp_requests, 4, "Should query for hints after every scroll");
2354 let expected_hints = vec![
2355 "47".to_string(),
2356 "94".to_string(),
2357 "139".to_string(),
2358 "184".to_string(),
2359 ];
2360 assert_eq!(
2361 expected_hints,
2362 cached_hint_labels(editor),
2363 "Should have hints from the new LSP response after the edit"
2364 );
2365 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2366
2367 let mut selection_in_cached_range = visible_range_after_scrolls.end;
2368 selection_in_cached_range.row -= visible_line_count.ceil() as u32;
2369 selection_in_cached_range
2370 })
2371 .unwrap();
2372
2373 editor
2374 .update(cx, |editor, window, cx| {
2375 editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
2376 s.select_ranges([selection_in_cached_range..selection_in_cached_range])
2377 });
2378 })
2379 .unwrap();
2380 cx.executor().advance_clock(Duration::from_millis(
2381 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2382 ));
2383 cx.executor().run_until_parked();
2384 editor.update(cx, |_, _, _| {
2385 let ranges = lsp_request_ranges
2386 .lock()
2387 .drain(..)
2388 .sorted_by_key(|r| r.start)
2389 .collect::<Vec<_>>();
2390 assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
2391 assert_eq!(lsp_request_count.load(Ordering::Acquire), 4);
2392 }).unwrap();
2393
2394 editor
2395 .update(cx, |editor, window, cx| {
2396 editor.handle_input("++++more text++++", window, cx);
2397 })
2398 .unwrap();
2399 cx.executor().advance_clock(Duration::from_millis(
2400 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2401 ));
2402 cx.executor().run_until_parked();
2403 editor.update(cx, |editor, _window, cx| {
2404 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2405 ranges.sort_by_key(|r| r.start);
2406
2407 assert_eq!(ranges.len(), 3,
2408 "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
2409 let above_query_range = &ranges[0];
2410 let visible_query_range = &ranges[1];
2411 let below_query_range = &ranges[2];
2412 assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
2413 "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");
2414 assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line,
2415 "Visible range {visible_query_range:?} should be before below range {below_query_range:?}");
2416 assert!(above_query_range.start.line < selection_in_cached_range.row,
2417 "Hints should be queried with the selected range after the query range start");
2418 assert!(below_query_range.end.line > selection_in_cached_range.row,
2419 "Hints should be queried with the selected range before the query range end");
2420 assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32,
2421 "Hints query range should contain one more screen before");
2422 assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32,
2423 "Hints query range should contain one more screen after");
2424
2425 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2426 assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried");
2427 let expected_hints = vec!["67".to_string(), "115".to_string(), "163".to_string()];
2428 assert_eq!(expected_hints, cached_hint_labels(editor),
2429 "Should have hints from the new LSP response after the edit");
2430 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2431 }).unwrap();
2432 }
2433
2434 fn editor_visible_range(
2435 editor: &WindowHandle<Editor>,
2436 cx: &mut gpui::TestAppContext,
2437 ) -> Range<Point> {
2438 let ranges = editor
2439 .update(cx, |editor, _window, cx| {
2440 editor.excerpts_for_inlay_hints_query(None, cx)
2441 })
2442 .unwrap();
2443 assert_eq!(
2444 ranges.len(),
2445 1,
2446 "Single buffer should produce a single excerpt with visible range"
2447 );
2448 let (_, (excerpt_buffer, _, excerpt_visible_range)) = ranges.into_iter().next().unwrap();
2449 excerpt_buffer.update(cx, |buffer, _| {
2450 let snapshot = buffer.snapshot();
2451 let start = buffer
2452 .anchor_before(excerpt_visible_range.start)
2453 .to_point(&snapshot);
2454 let end = buffer
2455 .anchor_after(excerpt_visible_range.end)
2456 .to_point(&snapshot);
2457 start..end
2458 })
2459 }
2460
2461 #[gpui::test]
2462 async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
2463 init_test(cx, |settings| {
2464 settings.defaults.inlay_hints = Some(InlayHintSettings {
2465 enabled: true,
2466 edit_debounce_ms: 0,
2467 scroll_debounce_ms: 0,
2468 show_type_hints: true,
2469 show_parameter_hints: true,
2470 show_other_hints: true,
2471 show_background: false,
2472 })
2473 });
2474
2475 let fs = FakeFs::new(cx.background_executor.clone());
2476 fs.insert_tree(
2477 path!("/a"),
2478 json!({
2479 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2480 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2481 }),
2482 )
2483 .await;
2484
2485 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2486
2487 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2488 let language = rust_lang();
2489 language_registry.add(language);
2490 let mut fake_servers = language_registry.register_fake_lsp(
2491 "Rust",
2492 FakeLspAdapter {
2493 capabilities: lsp::ServerCapabilities {
2494 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2495 ..Default::default()
2496 },
2497 ..Default::default()
2498 },
2499 );
2500
2501 let (buffer_1, _handle1) = project
2502 .update(cx, |project, cx| {
2503 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2504 })
2505 .await
2506 .unwrap();
2507 let (buffer_2, _handle2) = project
2508 .update(cx, |project, cx| {
2509 project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2510 })
2511 .await
2512 .unwrap();
2513 let multibuffer = cx.new(|cx| {
2514 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2515 multibuffer.push_excerpts(
2516 buffer_1.clone(),
2517 [
2518 ExcerptRange {
2519 context: Point::new(0, 0)..Point::new(2, 0),
2520 primary: None,
2521 },
2522 ExcerptRange {
2523 context: Point::new(4, 0)..Point::new(11, 0),
2524 primary: None,
2525 },
2526 ExcerptRange {
2527 context: Point::new(22, 0)..Point::new(33, 0),
2528 primary: None,
2529 },
2530 ExcerptRange {
2531 context: Point::new(44, 0)..Point::new(55, 0),
2532 primary: None,
2533 },
2534 ExcerptRange {
2535 context: Point::new(56, 0)..Point::new(66, 0),
2536 primary: None,
2537 },
2538 ExcerptRange {
2539 context: Point::new(67, 0)..Point::new(77, 0),
2540 primary: None,
2541 },
2542 ],
2543 cx,
2544 );
2545 multibuffer.push_excerpts(
2546 buffer_2.clone(),
2547 [
2548 ExcerptRange {
2549 context: Point::new(0, 1)..Point::new(2, 1),
2550 primary: None,
2551 },
2552 ExcerptRange {
2553 context: Point::new(4, 1)..Point::new(11, 1),
2554 primary: None,
2555 },
2556 ExcerptRange {
2557 context: Point::new(22, 1)..Point::new(33, 1),
2558 primary: None,
2559 },
2560 ExcerptRange {
2561 context: Point::new(44, 1)..Point::new(55, 1),
2562 primary: None,
2563 },
2564 ExcerptRange {
2565 context: Point::new(56, 1)..Point::new(66, 1),
2566 primary: None,
2567 },
2568 ExcerptRange {
2569 context: Point::new(67, 1)..Point::new(77, 1),
2570 primary: None,
2571 },
2572 ],
2573 cx,
2574 );
2575 multibuffer
2576 });
2577
2578 cx.executor().run_until_parked();
2579 let editor = cx.add_window(|window, cx| {
2580 Editor::for_multibuffer(multibuffer, Some(project.clone()), true, window, cx)
2581 });
2582
2583 let editor_edited = Arc::new(AtomicBool::new(false));
2584 let fake_server = fake_servers.next().await.unwrap();
2585 let closure_editor_edited = Arc::clone(&editor_edited);
2586 fake_server
2587 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2588 let task_editor_edited = Arc::clone(&closure_editor_edited);
2589 async move {
2590 let hint_text = if params.text_document.uri
2591 == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
2592 {
2593 "main hint"
2594 } else if params.text_document.uri
2595 == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
2596 {
2597 "other hint"
2598 } else {
2599 panic!("unexpected uri: {:?}", params.text_document.uri);
2600 };
2601
2602 // one hint per excerpt
2603 let positions = [
2604 lsp::Position::new(0, 2),
2605 lsp::Position::new(4, 2),
2606 lsp::Position::new(22, 2),
2607 lsp::Position::new(44, 2),
2608 lsp::Position::new(56, 2),
2609 lsp::Position::new(67, 2),
2610 ];
2611 let out_of_range_hint = lsp::InlayHint {
2612 position: lsp::Position::new(
2613 params.range.start.line + 99,
2614 params.range.start.character + 99,
2615 ),
2616 label: lsp::InlayHintLabel::String(
2617 "out of excerpt range, should be ignored".to_string(),
2618 ),
2619 kind: None,
2620 text_edits: None,
2621 tooltip: None,
2622 padding_left: None,
2623 padding_right: None,
2624 data: None,
2625 };
2626
2627 let edited = task_editor_edited.load(Ordering::Acquire);
2628 Ok(Some(
2629 std::iter::once(out_of_range_hint)
2630 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2631 lsp::InlayHint {
2632 position,
2633 label: lsp::InlayHintLabel::String(format!(
2634 "{hint_text}{E} #{i}",
2635 E = if edited { "(edited)" } else { "" },
2636 )),
2637 kind: None,
2638 text_edits: None,
2639 tooltip: None,
2640 padding_left: None,
2641 padding_right: None,
2642 data: None,
2643 }
2644 }))
2645 .collect(),
2646 ))
2647 }
2648 })
2649 .next()
2650 .await;
2651 cx.executor().run_until_parked();
2652
2653 editor
2654 .update(cx, |editor, _window, cx| {
2655 let expected_hints = vec![
2656 "main hint #0".to_string(),
2657 "main hint #1".to_string(),
2658 "main hint #2".to_string(),
2659 "main hint #3".to_string(),
2660 "main hint #4".to_string(),
2661 "main hint #5".to_string(),
2662 ];
2663 assert_eq!(
2664 expected_hints,
2665 sorted_cached_hint_labels(editor),
2666 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2667 );
2668 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2669 })
2670 .unwrap();
2671
2672 editor
2673 .update(cx, |editor, window, cx| {
2674 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2675 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2676 });
2677 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2678 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2679 });
2680 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2681 s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
2682 });
2683 })
2684 .unwrap();
2685 cx.executor().run_until_parked();
2686 editor
2687 .update(cx, |editor, _window, cx| {
2688 let expected_hints = vec![
2689 "main hint #0".to_string(),
2690 "main hint #1".to_string(),
2691 "main hint #2".to_string(),
2692 "main hint #3".to_string(),
2693 "main hint #4".to_string(),
2694 "main hint #5".to_string(),
2695 "other hint #0".to_string(),
2696 "other hint #1".to_string(),
2697 "other hint #2".to_string(),
2698 ];
2699 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2700 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2701 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2702 })
2703 .unwrap();
2704
2705 editor
2706 .update(cx, |editor, window, cx| {
2707 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2708 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2709 });
2710 })
2711 .unwrap();
2712 cx.executor().advance_clock(Duration::from_millis(
2713 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2714 ));
2715 cx.executor().run_until_parked();
2716 editor
2717 .update(cx, |editor, _window, cx| {
2718 let expected_hints = vec![
2719 "main hint #0".to_string(),
2720 "main hint #1".to_string(),
2721 "main hint #2".to_string(),
2722 "main hint #3".to_string(),
2723 "main hint #4".to_string(),
2724 "main hint #5".to_string(),
2725 "other hint #0".to_string(),
2726 "other hint #1".to_string(),
2727 "other hint #2".to_string(),
2728 "other hint #3".to_string(),
2729 "other hint #4".to_string(),
2730 "other hint #5".to_string(),
2731 ];
2732 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2733 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2734 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2735 })
2736 .unwrap();
2737
2738 editor
2739 .update(cx, |editor, window, cx| {
2740 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2741 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2742 });
2743 })
2744 .unwrap();
2745 cx.executor().advance_clock(Duration::from_millis(
2746 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2747 ));
2748 cx.executor().run_until_parked();
2749 editor
2750 .update(cx, |editor, _window, cx| {
2751 let expected_hints = vec![
2752 "main hint #0".to_string(),
2753 "main hint #1".to_string(),
2754 "main hint #2".to_string(),
2755 "main hint #3".to_string(),
2756 "main hint #4".to_string(),
2757 "main hint #5".to_string(),
2758 "other hint #0".to_string(),
2759 "other hint #1".to_string(),
2760 "other hint #2".to_string(),
2761 "other hint #3".to_string(),
2762 "other hint #4".to_string(),
2763 "other hint #5".to_string(),
2764 ];
2765 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2766 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2767 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2768 })
2769 .unwrap();
2770
2771 editor_edited.store(true, Ordering::Release);
2772 editor
2773 .update(cx, |editor, window, cx| {
2774 editor.change_selections(None, window, cx, |s| {
2775 s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
2776 });
2777 editor.handle_input("++++more text++++", window, cx);
2778 })
2779 .unwrap();
2780 cx.executor().run_until_parked();
2781 editor
2782 .update(cx, |editor, _window, cx| {
2783 let expected_hints = vec![
2784 "main hint #0".to_string(),
2785 "main hint #1".to_string(),
2786 "main hint #2".to_string(),
2787 "main hint #3".to_string(),
2788 "main hint #4".to_string(),
2789 "main hint #5".to_string(),
2790 "other hint(edited) #0".to_string(),
2791 "other hint(edited) #1".to_string(),
2792 "other hint(edited) #2".to_string(),
2793 ];
2794 assert_eq!(
2795 expected_hints,
2796 sorted_cached_hint_labels(editor),
2797 "After multibuffer edit, editor gets scrolled back to the last selection; \
2798 all hints should be invalidated and required for all of its visible excerpts"
2799 );
2800 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2801 })
2802 .unwrap();
2803 }
2804
2805 #[gpui::test]
2806 async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
2807 init_test(cx, |settings| {
2808 settings.defaults.inlay_hints = Some(InlayHintSettings {
2809 enabled: true,
2810 edit_debounce_ms: 0,
2811 scroll_debounce_ms: 0,
2812 show_type_hints: false,
2813 show_parameter_hints: false,
2814 show_other_hints: false,
2815 show_background: false,
2816 })
2817 });
2818
2819 let fs = FakeFs::new(cx.background_executor.clone());
2820 fs.insert_tree(
2821 path!("/a"),
2822 json!({
2823 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2824 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2825 }),
2826 )
2827 .await;
2828
2829 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2830
2831 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2832 language_registry.add(rust_lang());
2833 let mut fake_servers = language_registry.register_fake_lsp(
2834 "Rust",
2835 FakeLspAdapter {
2836 capabilities: lsp::ServerCapabilities {
2837 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2838 ..Default::default()
2839 },
2840 ..Default::default()
2841 },
2842 );
2843
2844 let (buffer_1, _handle) = project
2845 .update(cx, |project, cx| {
2846 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2847 })
2848 .await
2849 .unwrap();
2850 let (buffer_2, _handle2) = project
2851 .update(cx, |project, cx| {
2852 project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2853 })
2854 .await
2855 .unwrap();
2856 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2857 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2858 let buffer_1_excerpts = multibuffer.push_excerpts(
2859 buffer_1.clone(),
2860 [ExcerptRange {
2861 context: Point::new(0, 0)..Point::new(2, 0),
2862 primary: None,
2863 }],
2864 cx,
2865 );
2866 let buffer_2_excerpts = multibuffer.push_excerpts(
2867 buffer_2.clone(),
2868 [ExcerptRange {
2869 context: Point::new(0, 1)..Point::new(2, 1),
2870 primary: None,
2871 }],
2872 cx,
2873 );
2874 (buffer_1_excerpts, buffer_2_excerpts)
2875 });
2876
2877 assert!(!buffer_1_excerpts.is_empty());
2878 assert!(!buffer_2_excerpts.is_empty());
2879
2880 cx.executor().run_until_parked();
2881 let editor = cx.add_window(|window, cx| {
2882 Editor::for_multibuffer(multibuffer, Some(project.clone()), true, window, cx)
2883 });
2884 let editor_edited = Arc::new(AtomicBool::new(false));
2885 let fake_server = fake_servers.next().await.unwrap();
2886 let closure_editor_edited = Arc::clone(&editor_edited);
2887 fake_server
2888 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2889 let task_editor_edited = Arc::clone(&closure_editor_edited);
2890 async move {
2891 let hint_text = if params.text_document.uri
2892 == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
2893 {
2894 "main hint"
2895 } else if params.text_document.uri
2896 == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
2897 {
2898 "other hint"
2899 } else {
2900 panic!("unexpected uri: {:?}", params.text_document.uri);
2901 };
2902
2903 let positions = [
2904 lsp::Position::new(0, 2),
2905 lsp::Position::new(4, 2),
2906 lsp::Position::new(22, 2),
2907 lsp::Position::new(44, 2),
2908 lsp::Position::new(56, 2),
2909 lsp::Position::new(67, 2),
2910 ];
2911 let out_of_range_hint = lsp::InlayHint {
2912 position: lsp::Position::new(
2913 params.range.start.line + 99,
2914 params.range.start.character + 99,
2915 ),
2916 label: lsp::InlayHintLabel::String(
2917 "out of excerpt range, should be ignored".to_string(),
2918 ),
2919 kind: None,
2920 text_edits: None,
2921 tooltip: None,
2922 padding_left: None,
2923 padding_right: None,
2924 data: None,
2925 };
2926
2927 let edited = task_editor_edited.load(Ordering::Acquire);
2928 Ok(Some(
2929 std::iter::once(out_of_range_hint)
2930 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2931 lsp::InlayHint {
2932 position,
2933 label: lsp::InlayHintLabel::String(format!(
2934 "{hint_text}{} #{i}",
2935 if edited { "(edited)" } else { "" },
2936 )),
2937 kind: None,
2938 text_edits: None,
2939 tooltip: None,
2940 padding_left: None,
2941 padding_right: None,
2942 data: None,
2943 }
2944 }))
2945 .collect(),
2946 ))
2947 }
2948 })
2949 .next()
2950 .await;
2951 cx.executor().run_until_parked();
2952 editor
2953 .update(cx, |editor, _, cx| {
2954 assert_eq!(
2955 vec!["main hint #0".to_string(), "other hint #0".to_string()],
2956 sorted_cached_hint_labels(editor),
2957 "Cache should update for both excerpts despite hints display was disabled"
2958 );
2959 assert!(
2960 visible_hint_labels(editor, cx).is_empty(),
2961 "All hints are disabled and should not be shown despite being present in the cache"
2962 );
2963 })
2964 .unwrap();
2965
2966 editor
2967 .update(cx, |editor, _, cx| {
2968 editor.buffer().update(cx, |multibuffer, cx| {
2969 multibuffer.remove_excerpts(buffer_2_excerpts, cx)
2970 })
2971 })
2972 .unwrap();
2973 cx.executor().run_until_parked();
2974 editor
2975 .update(cx, |editor, _, cx| {
2976 assert_eq!(
2977 vec!["main hint #0".to_string()],
2978 cached_hint_labels(editor),
2979 "For the removed excerpt, should clean corresponding cached hints"
2980 );
2981 assert!(
2982 visible_hint_labels(editor, cx).is_empty(),
2983 "All hints are disabled and should not be shown despite being present in the cache"
2984 );
2985 })
2986 .unwrap();
2987
2988 update_test_language_settings(cx, |settings| {
2989 settings.defaults.inlay_hints = Some(InlayHintSettings {
2990 enabled: true,
2991 edit_debounce_ms: 0,
2992 scroll_debounce_ms: 0,
2993 show_type_hints: true,
2994 show_parameter_hints: true,
2995 show_other_hints: true,
2996 show_background: false,
2997 })
2998 });
2999 cx.executor().run_until_parked();
3000 editor
3001 .update(cx, |editor, _, cx| {
3002 let expected_hints = vec!["main hint #0".to_string()];
3003 assert_eq!(
3004 expected_hints,
3005 cached_hint_labels(editor),
3006 "Hint display settings change should not change the cache"
3007 );
3008 assert_eq!(
3009 expected_hints,
3010 visible_hint_labels(editor, cx),
3011 "Settings change should make cached hints visible"
3012 );
3013 })
3014 .unwrap();
3015 }
3016
3017 #[gpui::test]
3018 async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
3019 init_test(cx, |settings| {
3020 settings.defaults.inlay_hints = Some(InlayHintSettings {
3021 enabled: true,
3022 edit_debounce_ms: 0,
3023 scroll_debounce_ms: 0,
3024 show_type_hints: true,
3025 show_parameter_hints: true,
3026 show_other_hints: true,
3027 show_background: false,
3028 })
3029 });
3030
3031 let fs = FakeFs::new(cx.background_executor.clone());
3032 fs.insert_tree(
3033 path!("/a"),
3034 json!({
3035 "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)),
3036 "other.rs": "// Test file",
3037 }),
3038 )
3039 .await;
3040
3041 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3042
3043 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3044 language_registry.add(rust_lang());
3045 language_registry.register_fake_lsp(
3046 "Rust",
3047 FakeLspAdapter {
3048 capabilities: lsp::ServerCapabilities {
3049 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3050 ..Default::default()
3051 },
3052 initializer: Some(Box::new(move |fake_server| {
3053 let lsp_request_count = Arc::new(AtomicU32::new(0));
3054 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
3055 move |params, _| {
3056 let i = lsp_request_count.fetch_add(1, Ordering::Release) + 1;
3057 async move {
3058 assert_eq!(
3059 params.text_document.uri,
3060 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3061 );
3062 let query_start = params.range.start;
3063 Ok(Some(vec![lsp::InlayHint {
3064 position: query_start,
3065 label: lsp::InlayHintLabel::String(i.to_string()),
3066 kind: None,
3067 text_edits: None,
3068 tooltip: None,
3069 padding_left: None,
3070 padding_right: None,
3071 data: None,
3072 }]))
3073 }
3074 },
3075 );
3076 })),
3077 ..Default::default()
3078 },
3079 );
3080
3081 let buffer = project
3082 .update(cx, |project, cx| {
3083 project.open_local_buffer(path!("/a/main.rs"), cx)
3084 })
3085 .await
3086 .unwrap();
3087 let editor =
3088 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3089
3090 cx.executor().run_until_parked();
3091 editor
3092 .update(cx, |editor, window, cx| {
3093 editor.change_selections(None, window, cx, |s| {
3094 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3095 })
3096 })
3097 .unwrap();
3098 cx.executor().run_until_parked();
3099 editor
3100 .update(cx, |editor, _, cx| {
3101 let expected_hints = vec!["1".to_string()];
3102 assert_eq!(expected_hints, cached_hint_labels(editor));
3103 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3104 })
3105 .unwrap();
3106 }
3107
3108 #[gpui::test]
3109 async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
3110 init_test(cx, |settings| {
3111 settings.defaults.inlay_hints = Some(InlayHintSettings {
3112 enabled: false,
3113 edit_debounce_ms: 0,
3114 scroll_debounce_ms: 0,
3115 show_type_hints: true,
3116 show_parameter_hints: true,
3117 show_other_hints: true,
3118 show_background: false,
3119 })
3120 });
3121
3122 let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3123 let lsp_request_count = Arc::new(AtomicU32::new(0));
3124 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
3125 let lsp_request_count = lsp_request_count.clone();
3126 async move {
3127 assert_eq!(
3128 params.text_document.uri,
3129 lsp::Url::from_file_path(file_with_hints).unwrap(),
3130 );
3131
3132 let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
3133 Ok(Some(vec![lsp::InlayHint {
3134 position: lsp::Position::new(0, i),
3135 label: lsp::InlayHintLabel::String(i.to_string()),
3136 kind: None,
3137 text_edits: None,
3138 tooltip: None,
3139 padding_left: None,
3140 padding_right: None,
3141 data: None,
3142 }]))
3143 }
3144 });
3145 })
3146 .await;
3147
3148 editor
3149 .update(cx, |editor, window, cx| {
3150 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3151 })
3152 .unwrap();
3153
3154 cx.executor().run_until_parked();
3155 editor
3156 .update(cx, |editor, _, cx| {
3157 let expected_hints = vec!["1".to_string()];
3158 assert_eq!(
3159 expected_hints,
3160 cached_hint_labels(editor),
3161 "Should display inlays after toggle despite them disabled in settings"
3162 );
3163 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3164 })
3165 .unwrap();
3166
3167 editor
3168 .update(cx, |editor, window, cx| {
3169 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3170 })
3171 .unwrap();
3172 cx.executor().run_until_parked();
3173 editor
3174 .update(cx, |editor, _, cx| {
3175 assert!(
3176 cached_hint_labels(editor).is_empty(),
3177 "Should clear hints after 2nd toggle"
3178 );
3179 assert!(visible_hint_labels(editor, cx).is_empty());
3180 })
3181 .unwrap();
3182
3183 update_test_language_settings(cx, |settings| {
3184 settings.defaults.inlay_hints = Some(InlayHintSettings {
3185 enabled: true,
3186 edit_debounce_ms: 0,
3187 scroll_debounce_ms: 0,
3188 show_type_hints: true,
3189 show_parameter_hints: true,
3190 show_other_hints: true,
3191 show_background: false,
3192 })
3193 });
3194 cx.executor().run_until_parked();
3195 editor
3196 .update(cx, |editor, _, cx| {
3197 let expected_hints = vec!["2".to_string()];
3198 assert_eq!(
3199 expected_hints,
3200 cached_hint_labels(editor),
3201 "Should query LSP hints for the 2nd time after enabling hints in settings"
3202 );
3203 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3204 })
3205 .unwrap();
3206
3207 editor
3208 .update(cx, |editor, window, cx| {
3209 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3210 })
3211 .unwrap();
3212 cx.executor().run_until_parked();
3213 editor
3214 .update(cx, |editor, _, cx| {
3215 assert!(
3216 cached_hint_labels(editor).is_empty(),
3217 "Should clear hints after enabling in settings and a 3rd toggle"
3218 );
3219 assert!(visible_hint_labels(editor, cx).is_empty());
3220 })
3221 .unwrap();
3222
3223 editor
3224 .update(cx, |editor, window, cx| {
3225 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3226 })
3227 .unwrap();
3228 cx.executor().run_until_parked();
3229 editor.update(cx, |editor, _, cx| {
3230 let expected_hints = vec!["3".to_string()];
3231 assert_eq!(
3232 expected_hints,
3233 cached_hint_labels(editor),
3234 "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
3235 );
3236 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3237 }).unwrap();
3238 }
3239
3240 #[gpui::test]
3241 async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) {
3242 init_test(cx, |settings| {
3243 settings.defaults.inlay_hints = Some(InlayHintSettings {
3244 enabled: true,
3245 edit_debounce_ms: 0,
3246 scroll_debounce_ms: 0,
3247 show_type_hints: true,
3248 show_parameter_hints: true,
3249 show_other_hints: true,
3250 show_background: false,
3251 })
3252 });
3253
3254 let fs = FakeFs::new(cx.background_executor.clone());
3255 fs.insert_tree(
3256 path!("/a"),
3257 json!({
3258 "main.rs": "fn main() {
3259 let x = 42;
3260 std::thread::scope(|s| {
3261 s.spawn(|| {
3262 let _x = x;
3263 });
3264 });
3265 }",
3266 "other.rs": "// Test file",
3267 }),
3268 )
3269 .await;
3270
3271 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3272
3273 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3274 language_registry.add(rust_lang());
3275 language_registry.register_fake_lsp(
3276 "Rust",
3277 FakeLspAdapter {
3278 capabilities: lsp::ServerCapabilities {
3279 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3280 ..Default::default()
3281 },
3282 initializer: Some(Box::new(move |fake_server| {
3283 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
3284 move |params, _| async move {
3285 assert_eq!(
3286 params.text_document.uri,
3287 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3288 );
3289 Ok(Some(
3290 serde_json::from_value(json!([
3291 {
3292 "position": {
3293 "line": 3,
3294 "character": 16
3295 },
3296 "label": "move",
3297 "paddingLeft": false,
3298 "paddingRight": false
3299 },
3300 {
3301 "position": {
3302 "line": 3,
3303 "character": 16
3304 },
3305 "label": "(",
3306 "paddingLeft": false,
3307 "paddingRight": false
3308 },
3309 {
3310 "position": {
3311 "line": 3,
3312 "character": 16
3313 },
3314 "label": [
3315 {
3316 "value": "&x"
3317 }
3318 ],
3319 "paddingLeft": false,
3320 "paddingRight": false,
3321 "data": {
3322 "file_id": 0
3323 }
3324 },
3325 {
3326 "position": {
3327 "line": 3,
3328 "character": 16
3329 },
3330 "label": ")",
3331 "paddingLeft": false,
3332 "paddingRight": true
3333 },
3334 // not a correct syntax, but checks that same symbols at the same place
3335 // are not deduplicated
3336 {
3337 "position": {
3338 "line": 3,
3339 "character": 16
3340 },
3341 "label": ")",
3342 "paddingLeft": false,
3343 "paddingRight": true
3344 },
3345 ]))
3346 .unwrap(),
3347 ))
3348 },
3349 );
3350 })),
3351 ..FakeLspAdapter::default()
3352 },
3353 );
3354
3355 let buffer = project
3356 .update(cx, |project, cx| {
3357 project.open_local_buffer(path!("/a/main.rs"), cx)
3358 })
3359 .await
3360 .unwrap();
3361 let editor =
3362 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3363
3364 cx.executor().run_until_parked();
3365 editor
3366 .update(cx, |editor, window, cx| {
3367 editor.change_selections(None, window, cx, |s| {
3368 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3369 })
3370 })
3371 .unwrap();
3372 cx.executor().run_until_parked();
3373 editor
3374 .update(cx, |editor, _window, cx| {
3375 let expected_hints = vec![
3376 "move".to_string(),
3377 "(".to_string(),
3378 "&x".to_string(),
3379 ") ".to_string(),
3380 ") ".to_string(),
3381 ];
3382 assert_eq!(
3383 expected_hints,
3384 cached_hint_labels(editor),
3385 "Editor inlay hints should repeat server's order when placed at the same spot"
3386 );
3387 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3388 })
3389 .unwrap();
3390 }
3391
3392 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
3393 cx.update(|cx| {
3394 let settings_store = SettingsStore::test(cx);
3395 cx.set_global(settings_store);
3396 theme::init(theme::LoadThemes::JustBase, cx);
3397 release_channel::init(SemanticVersion::default(), cx);
3398 client::init_settings(cx);
3399 language::init(cx);
3400 Project::init_settings(cx);
3401 workspace::init_settings(cx);
3402 crate::init(cx);
3403 });
3404
3405 update_test_language_settings(cx, f);
3406 }
3407
3408 async fn prepare_test_objects(
3409 cx: &mut TestAppContext,
3410 initialize: impl 'static + Send + Fn(&mut FakeLanguageServer, &'static str) + Send + Sync,
3411 ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
3412 let fs = FakeFs::new(cx.background_executor.clone());
3413 fs.insert_tree(
3414 path!("/a"),
3415 json!({
3416 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
3417 "other.rs": "// Test file",
3418 }),
3419 )
3420 .await;
3421
3422 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3423 let file_path = path!("/a/main.rs");
3424
3425 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3426 language_registry.add(rust_lang());
3427 let mut fake_servers = language_registry.register_fake_lsp(
3428 "Rust",
3429 FakeLspAdapter {
3430 capabilities: lsp::ServerCapabilities {
3431 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3432 ..Default::default()
3433 },
3434 initializer: Some(Box::new(move |server| initialize(server, file_path))),
3435 ..Default::default()
3436 },
3437 );
3438
3439 let buffer = project
3440 .update(cx, |project, cx| {
3441 project.open_local_buffer(path!("/a/main.rs"), cx)
3442 })
3443 .await
3444 .unwrap();
3445 let editor =
3446 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3447
3448 editor
3449 .update(cx, |editor, _, cx| {
3450 assert!(cached_hint_labels(editor).is_empty());
3451 assert!(visible_hint_labels(editor, cx).is_empty());
3452 })
3453 .unwrap();
3454
3455 cx.executor().run_until_parked();
3456 let fake_server = fake_servers.next().await.unwrap();
3457 (file_path, editor, fake_server)
3458 }
3459
3460 // 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.
3461 // Ensure a stable order for testing.
3462 fn sorted_cached_hint_labels(editor: &Editor) -> Vec<String> {
3463 let mut labels = cached_hint_labels(editor);
3464 labels.sort();
3465 labels
3466 }
3467
3468 pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
3469 let mut labels = Vec::new();
3470 for excerpt_hints in editor.inlay_hint_cache().hints.values() {
3471 let excerpt_hints = excerpt_hints.read();
3472 for id in &excerpt_hints.ordered_hints {
3473 let hint = &excerpt_hints.hints_by_id[id];
3474 let mut label = hint.text();
3475 if hint.padding_left {
3476 label.insert(0, ' ');
3477 }
3478 if hint.padding_right {
3479 label.push_str(" ");
3480 }
3481 labels.push(label);
3482 }
3483 }
3484
3485 labels
3486 }
3487
3488 pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
3489 editor
3490 .visible_inlay_hints(cx)
3491 .into_iter()
3492 .map(|hint| hint.text.to_string())
3493 .collect()
3494 }
3495}