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