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