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