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