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