1use std::{
2 cmp,
3 ops::{ControlFlow, Range},
4 sync::Arc,
5};
6
7use crate::{
8 display_map::Inlay, Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot,
9};
10use anyhow::Context;
11use clock::Global;
12use gpui::{ModelHandle, Task, ViewContext};
13use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
14use log::error;
15use parking_lot::RwLock;
16use project::InlayHint;
17
18use collections::{hash_map, HashMap, HashSet};
19use language::language_settings::InlayHintSettings;
20use util::post_inc;
21
22pub struct InlayHintCache {
23 pub hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
24 pub allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
25 pub version: usize,
26 pub enabled: bool,
27 update_tasks: HashMap<ExcerptId, UpdateTask>,
28}
29
30struct UpdateTask {
31 current: (InvalidationStrategy, SpawnedTask),
32 pending_refresh: Option<SpawnedTask>,
33}
34
35struct SpawnedTask {
36 version: usize,
37 is_running_rx: smol::channel::Receiver<()>,
38 _task: Task<()>,
39}
40
41#[derive(Debug)]
42pub struct CachedExcerptHints {
43 version: usize,
44 buffer_version: Global,
45 pub hints: Vec<(InlayId, InlayHint)>,
46}
47
48#[derive(Debug, Clone, Copy)]
49struct ExcerptQuery {
50 buffer_id: u64,
51 excerpt_id: ExcerptId,
52 dimensions: ExcerptDimensions,
53 cache_version: usize,
54 invalidate: InvalidationStrategy,
55}
56
57#[derive(Debug, Clone, Copy)]
58struct ExcerptDimensions {
59 excerpt_range_start: language::Anchor,
60 excerpt_range_end: language::Anchor,
61 excerpt_visible_range_start: language::Anchor,
62 excerpt_visible_range_end: language::Anchor,
63}
64
65impl ExcerptQuery {
66 fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges {
67 let visible_range =
68 self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end;
69 let mut other_ranges = Vec::new();
70 if self
71 .dimensions
72 .excerpt_range_start
73 .cmp(&self.dimensions.excerpt_visible_range_start, buffer)
74 .is_lt()
75 {
76 let mut end = self.dimensions.excerpt_visible_range_start;
77 end.offset -= 1;
78 other_ranges.push(self.dimensions.excerpt_range_start..end);
79 }
80 if self
81 .dimensions
82 .excerpt_range_end
83 .cmp(&self.dimensions.excerpt_visible_range_end, buffer)
84 .is_gt()
85 {
86 let mut start = self.dimensions.excerpt_visible_range_end;
87 start.offset += 1;
88 other_ranges.push(start..self.dimensions.excerpt_range_end);
89 }
90
91 HintFetchRanges {
92 visible_range,
93 other_ranges: other_ranges.into_iter().map(|range| range).collect(),
94 }
95 }
96}
97
98impl UpdateTask {
99 fn new(invalidation_strategy: InvalidationStrategy, spawned_task: SpawnedTask) -> Self {
100 Self {
101 current: (invalidation_strategy, spawned_task),
102 pending_refresh: None,
103 }
104 }
105
106 fn is_running(&self) -> bool {
107 !self.current.1.is_running_rx.is_closed()
108 || self
109 .pending_refresh
110 .as_ref()
111 .map_or(false, |task| !task.is_running_rx.is_closed())
112 }
113
114 fn cache_version(&self) -> usize {
115 self.current.1.version
116 }
117
118 fn invalidation_strategy(&self) -> InvalidationStrategy {
119 self.current.0
120 }
121}
122
123#[derive(Debug, Clone, Copy)]
124pub enum InvalidationStrategy {
125 Forced,
126 OnConflict,
127 None,
128}
129
130#[derive(Debug, Default)]
131pub struct InlaySplice {
132 pub to_remove: Vec<InlayId>,
133 pub to_insert: Vec<(Anchor, InlayId, InlayHint)>,
134}
135
136#[derive(Debug)]
137struct ExcerptHintsUpdate {
138 excerpt_id: ExcerptId,
139 cache_version: usize,
140 remove_from_visible: Vec<InlayId>,
141 remove_from_cache: HashSet<InlayId>,
142 add_to_cache: HashSet<InlayHint>,
143}
144
145impl InlayHintCache {
146 pub fn new(inlay_hint_settings: InlayHintSettings) -> Self {
147 Self {
148 allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
149 enabled: inlay_hint_settings.enabled,
150 hints: HashMap::default(),
151 update_tasks: HashMap::default(),
152 version: 0,
153 }
154 }
155
156 pub fn update_settings(
157 &mut self,
158 multi_buffer: &ModelHandle<MultiBuffer>,
159 new_hint_settings: InlayHintSettings,
160 visible_hints: Vec<Inlay>,
161 cx: &mut ViewContext<Editor>,
162 ) -> ControlFlow<Option<InlaySplice>> {
163 let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
164 match (self.enabled, new_hint_settings.enabled) {
165 (false, false) => {
166 self.allowed_hint_kinds = new_allowed_hint_kinds;
167 ControlFlow::Break(None)
168 }
169 (true, true) => {
170 if new_allowed_hint_kinds == self.allowed_hint_kinds {
171 ControlFlow::Break(None)
172 } else {
173 let new_splice = self.new_allowed_hint_kinds_splice(
174 multi_buffer,
175 &visible_hints,
176 &new_allowed_hint_kinds,
177 cx,
178 );
179 if new_splice.is_some() {
180 self.version += 1;
181 self.update_tasks.clear();
182 self.allowed_hint_kinds = new_allowed_hint_kinds;
183 }
184 ControlFlow::Break(new_splice)
185 }
186 }
187 (true, false) => {
188 self.enabled = new_hint_settings.enabled;
189 self.allowed_hint_kinds = new_allowed_hint_kinds;
190 if self.hints.is_empty() {
191 ControlFlow::Break(None)
192 } else {
193 self.clear();
194 ControlFlow::Break(Some(InlaySplice {
195 to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
196 to_insert: Vec::new(),
197 }))
198 }
199 }
200 (false, true) => {
201 self.enabled = new_hint_settings.enabled;
202 self.allowed_hint_kinds = new_allowed_hint_kinds;
203 ControlFlow::Continue(())
204 }
205 }
206 }
207
208 pub fn refresh_inlay_hints(
209 &mut self,
210 mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
211 invalidate: InvalidationStrategy,
212 cx: &mut ViewContext<Editor>,
213 ) {
214 if !self.enabled {
215 return;
216 }
217 let update_tasks = &mut self.update_tasks;
218 let invalidate_cache = matches!(
219 invalidate,
220 InvalidationStrategy::Forced | InvalidationStrategy::OnConflict
221 );
222 if invalidate_cache {
223 update_tasks
224 .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
225 }
226 let cache_version = self.version;
227 excerpts_to_query.retain(|visible_excerpt_id, _| {
228 match update_tasks.entry(*visible_excerpt_id) {
229 hash_map::Entry::Occupied(o) => match o.get().cache_version().cmp(&cache_version) {
230 cmp::Ordering::Less => true,
231 cmp::Ordering::Equal => invalidate_cache,
232 cmp::Ordering::Greater => false,
233 },
234 hash_map::Entry::Vacant(_) => true,
235 }
236 });
237
238 cx.spawn(|editor, mut cx| async move {
239 editor
240 .update(&mut cx, |editor, cx| {
241 spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx)
242 })
243 .ok();
244 })
245 .detach();
246 }
247
248 fn new_allowed_hint_kinds_splice(
249 &self,
250 multi_buffer: &ModelHandle<MultiBuffer>,
251 visible_hints: &[Inlay],
252 new_kinds: &HashSet<Option<InlayHintKind>>,
253 cx: &mut ViewContext<Editor>,
254 ) -> Option<InlaySplice> {
255 let old_kinds = &self.allowed_hint_kinds;
256 if new_kinds == old_kinds {
257 return None;
258 }
259
260 let mut to_remove = Vec::new();
261 let mut to_insert = Vec::new();
262 let mut shown_hints_to_remove = visible_hints.iter().fold(
263 HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
264 |mut current_hints, inlay| {
265 current_hints
266 .entry(inlay.position.excerpt_id)
267 .or_default()
268 .push((inlay.position, inlay.id));
269 current_hints
270 },
271 );
272
273 let multi_buffer = multi_buffer.read(cx);
274 let multi_buffer_snapshot = multi_buffer.snapshot(cx);
275
276 for (excerpt_id, excerpt_cached_hints) in &self.hints {
277 let shown_excerpt_hints_to_remove =
278 shown_hints_to_remove.entry(*excerpt_id).or_default();
279 let excerpt_cached_hints = excerpt_cached_hints.read();
280 let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable();
281 shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
282 let Some(buffer) = shown_anchor
283 .buffer_id
284 .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false };
285 let buffer_snapshot = buffer.read(cx).snapshot();
286 loop {
287 match excerpt_cache.peek() {
288 Some((cached_hint_id, cached_hint)) => {
289 if cached_hint_id == shown_hint_id {
290 excerpt_cache.next();
291 return !new_kinds.contains(&cached_hint.kind);
292 }
293
294 match cached_hint
295 .position
296 .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
297 {
298 cmp::Ordering::Less | cmp::Ordering::Equal => {
299 if !old_kinds.contains(&cached_hint.kind)
300 && new_kinds.contains(&cached_hint.kind)
301 {
302 to_insert.push((
303 multi_buffer_snapshot.anchor_in_excerpt(
304 *excerpt_id,
305 cached_hint.position,
306 ),
307 *cached_hint_id,
308 cached_hint.clone(),
309 ));
310 }
311 excerpt_cache.next();
312 }
313 cmp::Ordering::Greater => return true,
314 }
315 }
316 None => return true,
317 }
318 }
319 });
320
321 for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
322 let cached_hint_kind = maybe_missed_cached_hint.kind;
323 if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
324 to_insert.push((
325 multi_buffer_snapshot
326 .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
327 *cached_hint_id,
328 maybe_missed_cached_hint.clone(),
329 ));
330 }
331 }
332 }
333
334 to_remove.extend(
335 shown_hints_to_remove
336 .into_values()
337 .flatten()
338 .map(|(_, hint_id)| hint_id),
339 );
340 if to_remove.is_empty() && to_insert.is_empty() {
341 None
342 } else {
343 Some(InlaySplice {
344 to_remove,
345 to_insert,
346 })
347 }
348 }
349
350 fn clear(&mut self) {
351 self.version += 1;
352 self.update_tasks.clear();
353 self.hints.clear();
354 }
355}
356
357fn spawn_new_update_tasks(
358 editor: &mut Editor,
359 excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
360 invalidation_strategy: InvalidationStrategy,
361 update_cache_version: usize,
362 cx: &mut ViewContext<'_, '_, Editor>,
363) {
364 let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
365 for (excerpt_id, (buffer_handle, excerpt_visible_range)) in excerpts_to_query {
366 if !excerpt_visible_range.is_empty() {
367 let buffer = buffer_handle.read(cx);
368 let buffer_snapshot = buffer.snapshot();
369 let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
370 let cache_is_empty = match &cached_excerpt_hints {
371 Some(cached_excerpt_hints) => {
372 let new_task_buffer_version = buffer_snapshot.version();
373 let cached_excerpt_hints = cached_excerpt_hints.read();
374 let cached_buffer_version = &cached_excerpt_hints.buffer_version;
375 if cached_excerpt_hints.version > update_cache_version
376 || cached_buffer_version.changed_since(new_task_buffer_version)
377 {
378 return;
379 }
380 if !new_task_buffer_version.changed_since(&cached_buffer_version)
381 && !matches!(invalidation_strategy, InvalidationStrategy::Forced)
382 {
383 return;
384 }
385
386 cached_excerpt_hints.hints.is_empty()
387 }
388 None => true,
389 };
390
391 let buffer_id = buffer.remote_id();
392 let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
393 let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
394
395 let (multi_buffer_snapshot, full_excerpt_range) =
396 editor.buffer.update(cx, |multi_buffer, cx| {
397 let multi_buffer_snapshot = multi_buffer.snapshot(cx);
398 (
399 multi_buffer_snapshot,
400 multi_buffer
401 .excerpts_for_buffer(&buffer_handle, cx)
402 .into_iter()
403 .find(|(id, _)| id == &excerpt_id)
404 .map(|(_, range)| range.context),
405 )
406 });
407
408 if let Some(full_excerpt_range) = full_excerpt_range {
409 let query = ExcerptQuery {
410 buffer_id,
411 excerpt_id,
412 dimensions: ExcerptDimensions {
413 excerpt_range_start: full_excerpt_range.start,
414 excerpt_range_end: full_excerpt_range.end,
415 excerpt_visible_range_start,
416 excerpt_visible_range_end,
417 },
418 cache_version: update_cache_version,
419 invalidate: invalidation_strategy,
420 };
421
422 let new_update_task = |previous_task| {
423 new_update_task(
424 query,
425 multi_buffer_snapshot,
426 buffer_snapshot,
427 Arc::clone(&visible_hints),
428 cached_excerpt_hints,
429 previous_task,
430 cx,
431 )
432 };
433 match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
434 hash_map::Entry::Occupied(mut o) => {
435 let update_task = o.get_mut();
436 if update_task.is_running() {
437 match (update_task.invalidation_strategy(), invalidation_strategy) {
438 (InvalidationStrategy::Forced, _)
439 | (_, InvalidationStrategy::OnConflict) => {
440 o.insert(UpdateTask::new(
441 invalidation_strategy,
442 new_update_task(None),
443 ));
444 }
445 (_, InvalidationStrategy::Forced) => {
446 if cache_is_empty {
447 o.insert(UpdateTask::new(
448 invalidation_strategy,
449 new_update_task(None),
450 ));
451 } else if update_task.pending_refresh.is_none() {
452 update_task.pending_refresh = Some(new_update_task(Some(
453 update_task.current.1.is_running_rx.clone(),
454 )));
455 }
456 }
457 _ => {}
458 }
459 } else {
460 o.insert(UpdateTask::new(
461 invalidation_strategy,
462 new_update_task(None),
463 ));
464 }
465 }
466 hash_map::Entry::Vacant(v) => {
467 v.insert(UpdateTask::new(
468 invalidation_strategy,
469 new_update_task(None),
470 ));
471 }
472 }
473 }
474 }
475 }
476}
477
478fn new_update_task(
479 query: ExcerptQuery,
480 multi_buffer_snapshot: MultiBufferSnapshot,
481 buffer_snapshot: BufferSnapshot,
482 visible_hints: Arc<Vec<Inlay>>,
483 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
484 task_before_refresh: Option<smol::channel::Receiver<()>>,
485 cx: &mut ViewContext<'_, '_, Editor>,
486) -> SpawnedTask {
487 let hints_fetch_tasks = query.hints_fetch_ranges(&buffer_snapshot);
488 let (is_running_tx, is_running_rx) = smol::channel::bounded(1);
489 let is_refresh_task = task_before_refresh.is_some();
490 let _task = cx.spawn(|editor, cx| async move {
491 let _is_running_tx = is_running_tx;
492 if let Some(task_before_refresh) = task_before_refresh {
493 task_before_refresh.recv().await.ok();
494 }
495 let create_update_task = |range| {
496 fetch_and_update_hints(
497 editor.clone(),
498 multi_buffer_snapshot.clone(),
499 buffer_snapshot.clone(),
500 Arc::clone(&visible_hints),
501 cached_excerpt_hints.as_ref().map(Arc::clone),
502 query,
503 range,
504 cx.clone(),
505 )
506 };
507
508 if is_refresh_task {
509 let visible_range_has_updates =
510 match create_update_task(hints_fetch_tasks.visible_range).await {
511 Ok(updated) => updated,
512 Err(e) => {
513 error!("inlay hint visible range update task failed: {e:#}");
514 return;
515 }
516 };
517
518 if visible_range_has_updates {
519 let other_update_results = futures::future::join_all(
520 hints_fetch_tasks
521 .other_ranges
522 .into_iter()
523 .map(create_update_task),
524 )
525 .await;
526
527 for result in other_update_results {
528 if let Err(e) = result {
529 error!("inlay hint update task failed: {e:#}");
530 return;
531 }
532 }
533 }
534 } else {
535 let task_update_results = futures::future::join_all(
536 std::iter::once(hints_fetch_tasks.visible_range)
537 .chain(hints_fetch_tasks.other_ranges.into_iter())
538 .map(create_update_task),
539 )
540 .await;
541
542 for result in task_update_results {
543 if let Err(e) = result {
544 error!("inlay hint update task failed: {e:#}");
545 }
546 }
547 }
548 });
549
550 SpawnedTask {
551 version: query.cache_version,
552 _task,
553 is_running_rx,
554 }
555}
556
557async fn fetch_and_update_hints(
558 editor: gpui::WeakViewHandle<Editor>,
559 multi_buffer_snapshot: MultiBufferSnapshot,
560 buffer_snapshot: BufferSnapshot,
561 visible_hints: Arc<Vec<Inlay>>,
562 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
563 query: ExcerptQuery,
564 fetch_range: Range<language::Anchor>,
565 mut cx: gpui::AsyncAppContext,
566) -> anyhow::Result<bool> {
567 let inlay_hints_fetch_task = editor
568 .update(&mut cx, |editor, cx| {
569 editor
570 .buffer()
571 .read(cx)
572 .buffer(query.buffer_id)
573 .and_then(|buffer| {
574 let project = editor.project.as_ref()?;
575 Some(project.update(cx, |project, cx| {
576 project.inlay_hints(buffer, fetch_range.clone(), cx)
577 }))
578 })
579 })
580 .ok()
581 .flatten();
582 let mut update_happened = false;
583 let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) };
584
585 let new_hints = inlay_hints_fetch_task
586 .await
587 .context("inlay hint fetch task")?;
588 let background_task_buffer_snapshot = buffer_snapshot.clone();
589 let backround_fetch_range = fetch_range.clone();
590 if let Some(new_update) = cx
591 .background()
592 .spawn(async move {
593 calculate_hint_updates(
594 query,
595 backround_fetch_range,
596 new_hints,
597 &background_task_buffer_snapshot,
598 cached_excerpt_hints,
599 &visible_hints,
600 )
601 })
602 .await
603 {
604 update_happened = !new_update.add_to_cache.is_empty()
605 || !new_update.remove_from_cache.is_empty()
606 || !new_update.remove_from_visible.is_empty();
607 editor
608 .update(&mut cx, |editor, cx| {
609 let cached_excerpt_hints = editor
610 .inlay_hint_cache
611 .hints
612 .entry(new_update.excerpt_id)
613 .or_insert_with(|| {
614 Arc::new(RwLock::new(CachedExcerptHints {
615 version: new_update.cache_version,
616 buffer_version: buffer_snapshot.version().clone(),
617 hints: Vec::new(),
618 }))
619 });
620 let mut cached_excerpt_hints = cached_excerpt_hints.write();
621 match new_update.cache_version.cmp(&cached_excerpt_hints.version) {
622 cmp::Ordering::Less => return,
623 cmp::Ordering::Greater | cmp::Ordering::Equal => {
624 cached_excerpt_hints.version = new_update.cache_version;
625 }
626 }
627 cached_excerpt_hints
628 .hints
629 .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
630 cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
631 editor.inlay_hint_cache.version += 1;
632
633 let mut splice = InlaySplice {
634 to_remove: new_update.remove_from_visible,
635 to_insert: Vec::new(),
636 };
637
638 for new_hint in new_update.add_to_cache {
639 let new_hint_position = multi_buffer_snapshot
640 .anchor_in_excerpt(query.excerpt_id, new_hint.position);
641 let new_inlay_id = InlayId::Hint(post_inc(&mut editor.next_inlay_id));
642 if editor
643 .inlay_hint_cache
644 .allowed_hint_kinds
645 .contains(&new_hint.kind)
646 {
647 splice
648 .to_insert
649 .push((new_hint_position, new_inlay_id, new_hint.clone()));
650 }
651
652 cached_excerpt_hints.hints.push((new_inlay_id, new_hint));
653 }
654
655 cached_excerpt_hints
656 .hints
657 .sort_by(|(_, hint_a), (_, hint_b)| {
658 hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
659 });
660 drop(cached_excerpt_hints);
661
662 let InlaySplice {
663 to_remove,
664 to_insert,
665 } = splice;
666 if !to_remove.is_empty() || !to_insert.is_empty() {
667 editor.splice_inlay_hints(to_remove, to_insert, cx)
668 }
669 })
670 .ok();
671 }
672
673 Ok(update_happened)
674}
675
676fn calculate_hint_updates(
677 query: ExcerptQuery,
678 fetch_range: Range<language::Anchor>,
679 new_excerpt_hints: Vec<InlayHint>,
680 buffer_snapshot: &BufferSnapshot,
681 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
682 visible_hints: &[Inlay],
683) -> Option<ExcerptHintsUpdate> {
684 let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
685 let mut excerpt_hints_to_persist = HashMap::default();
686 for new_hint in new_excerpt_hints {
687 if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
688 continue;
689 }
690 let missing_from_cache = match &cached_excerpt_hints {
691 Some(cached_excerpt_hints) => {
692 let cached_excerpt_hints = cached_excerpt_hints.read();
693 match cached_excerpt_hints.hints.binary_search_by(|probe| {
694 probe.1.position.cmp(&new_hint.position, buffer_snapshot)
695 }) {
696 Ok(ix) => {
697 let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
698 if cached_hint == &new_hint {
699 excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
700 false
701 } else {
702 true
703 }
704 }
705 Err(_) => true,
706 }
707 }
708 None => true,
709 };
710 if missing_from_cache {
711 add_to_cache.insert(new_hint);
712 }
713 }
714
715 let mut remove_from_visible = Vec::new();
716 let mut remove_from_cache = HashSet::default();
717 if matches!(
718 query.invalidate,
719 InvalidationStrategy::Forced | InvalidationStrategy::OnConflict
720 ) {
721 remove_from_visible.extend(
722 visible_hints
723 .iter()
724 .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
725 .filter(|hint| {
726 contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
727 })
728 .filter(|hint| {
729 fetch_range
730 .start
731 .cmp(&hint.position.text_anchor, buffer_snapshot)
732 .is_le()
733 && fetch_range
734 .end
735 .cmp(&hint.position.text_anchor, buffer_snapshot)
736 .is_ge()
737 })
738 .map(|inlay_hint| inlay_hint.id)
739 .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
740 );
741
742 if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
743 let cached_excerpt_hints = cached_excerpt_hints.read();
744 remove_from_cache.extend(
745 cached_excerpt_hints
746 .hints
747 .iter()
748 .filter(|(cached_inlay_id, _)| {
749 !excerpt_hints_to_persist.contains_key(cached_inlay_id)
750 })
751 .filter(|(_, cached_hint)| {
752 fetch_range
753 .start
754 .cmp(&cached_hint.position, buffer_snapshot)
755 .is_le()
756 && fetch_range
757 .end
758 .cmp(&cached_hint.position, buffer_snapshot)
759 .is_ge()
760 })
761 .map(|(cached_inlay_id, _)| *cached_inlay_id),
762 );
763 }
764 }
765
766 if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
767 None
768 } else {
769 Some(ExcerptHintsUpdate {
770 cache_version: query.cache_version,
771 excerpt_id: query.excerpt_id,
772 remove_from_visible,
773 remove_from_cache,
774 add_to_cache,
775 })
776 }
777}
778
779struct HintFetchRanges {
780 visible_range: Range<language::Anchor>,
781 other_ranges: Vec<Range<language::Anchor>>,
782}
783
784fn contains_position(
785 range: &Range<language::Anchor>,
786 position: language::Anchor,
787 buffer_snapshot: &BufferSnapshot,
788) -> bool {
789 range.start.cmp(&position, buffer_snapshot).is_le()
790 && range.end.cmp(&position, buffer_snapshot).is_ge()
791}
792
793#[cfg(test)]
794mod tests {
795 use std::sync::atomic::{AtomicU32, Ordering};
796
797 use crate::{serde_json::json, InlayHintSettings};
798 use futures::StreamExt;
799 use gpui::{TestAppContext, ViewHandle};
800 use language::{
801 language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
802 };
803 use lsp::FakeLanguageServer;
804 use project::{FakeFs, Project};
805 use settings::SettingsStore;
806 use workspace::Workspace;
807
808 use crate::editor_tests::update_test_settings;
809
810 use super::*;
811
812 #[gpui::test]
813 async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
814 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
815 init_test(cx, |settings| {
816 settings.defaults.inlay_hints = Some(InlayHintSettings {
817 enabled: true,
818 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
819 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
820 show_other_hints: allowed_hint_kinds.contains(&None),
821 })
822 });
823 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
824 let lsp_request_count = Arc::new(AtomicU32::new(0));
825 fake_server
826 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
827 let task_lsp_request_count = Arc::clone(&lsp_request_count);
828 async move {
829 assert_eq!(
830 params.text_document.uri,
831 lsp::Url::from_file_path(file_with_hints).unwrap(),
832 );
833 let current_call_id =
834 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
835 let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
836 for _ in 0..2 {
837 let mut i = current_call_id;
838 loop {
839 new_hints.push(lsp::InlayHint {
840 position: lsp::Position::new(0, i),
841 label: lsp::InlayHintLabel::String(i.to_string()),
842 kind: None,
843 text_edits: None,
844 tooltip: None,
845 padding_left: None,
846 padding_right: None,
847 data: None,
848 });
849 if i == 0 {
850 break;
851 }
852 i -= 1;
853 }
854 }
855
856 Ok(Some(new_hints))
857 }
858 })
859 .next()
860 .await;
861 cx.foreground().finish_waiting();
862 cx.foreground().run_until_parked();
863 let mut edits_made = 1;
864 editor.update(cx, |editor, cx| {
865 let expected_layers = vec!["0".to_string()];
866 assert_eq!(
867 expected_layers,
868 cached_hint_labels(editor),
869 "Should get its first hints when opening the editor"
870 );
871 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
872 let inlay_cache = editor.inlay_hint_cache();
873 assert_eq!(
874 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
875 "Cache should use editor settings to get the allowed hint kinds"
876 );
877 assert_eq!(
878 inlay_cache.version, edits_made,
879 "The editor update the cache version after every cache/view change"
880 );
881 });
882
883 editor.update(cx, |editor, cx| {
884 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
885 editor.handle_input("some change", cx);
886 edits_made += 1;
887 });
888 cx.foreground().run_until_parked();
889 editor.update(cx, |editor, cx| {
890 let expected_layers = vec!["0".to_string(), "1".to_string()];
891 assert_eq!(
892 expected_layers,
893 cached_hint_labels(editor),
894 "Should get new hints after an edit"
895 );
896 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
897 let inlay_cache = editor.inlay_hint_cache();
898 assert_eq!(
899 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
900 "Cache should use editor settings to get the allowed hint kinds"
901 );
902 assert_eq!(
903 inlay_cache.version, edits_made,
904 "The editor update the cache version after every cache/view change"
905 );
906 });
907
908 fake_server
909 .request::<lsp::request::InlayHintRefreshRequest>(())
910 .await
911 .expect("inlay refresh request failed");
912 edits_made += 1;
913 cx.foreground().run_until_parked();
914 editor.update(cx, |editor, cx| {
915 let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
916 assert_eq!(
917 expected_layers,
918 cached_hint_labels(editor),
919 "Should get new hints after hint refresh/ request"
920 );
921 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
922 let inlay_cache = editor.inlay_hint_cache();
923 assert_eq!(
924 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
925 "Cache should use editor settings to get the allowed hint kinds"
926 );
927 assert_eq!(
928 inlay_cache.version, edits_made,
929 "The editor update the cache version after every cache/view change"
930 );
931 });
932 }
933
934 async fn prepare_test_objects(
935 cx: &mut TestAppContext,
936 ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
937 let mut language = Language::new(
938 LanguageConfig {
939 name: "Rust".into(),
940 path_suffixes: vec!["rs".to_string()],
941 ..Default::default()
942 },
943 Some(tree_sitter_rust::language()),
944 );
945 let mut fake_servers = language
946 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
947 capabilities: lsp::ServerCapabilities {
948 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
949 ..Default::default()
950 },
951 ..Default::default()
952 }))
953 .await;
954
955 let fs = FakeFs::new(cx.background());
956 fs.insert_tree(
957 "/a",
958 json!({
959 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
960 "other.rs": "// Test file",
961 }),
962 )
963 .await;
964
965 let project = Project::test(fs, ["/a".as_ref()], cx).await;
966 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
967 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
968 let worktree_id = workspace.update(cx, |workspace, cx| {
969 workspace.project().read_with(cx, |project, cx| {
970 project.worktrees(cx).next().unwrap().read(cx).id()
971 })
972 });
973
974 cx.foreground().start_waiting();
975 let editor = workspace
976 .update(cx, |workspace, cx| {
977 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
978 })
979 .await
980 .unwrap()
981 .downcast::<Editor>()
982 .unwrap();
983
984 let fake_server = fake_servers.next().await.unwrap();
985
986 ("/a/main.rs", editor, fake_server)
987 }
988
989 #[gpui::test]
990 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
991 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
992 init_test(cx, |settings| {
993 settings.defaults.inlay_hints = Some(InlayHintSettings {
994 enabled: true,
995 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
996 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
997 show_other_hints: allowed_hint_kinds.contains(&None),
998 })
999 });
1000 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1001 let lsp_request_count = Arc::new(AtomicU32::new(0));
1002 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1003 fake_server
1004 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1005 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1006 async move {
1007 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1008 assert_eq!(
1009 params.text_document.uri,
1010 lsp::Url::from_file_path(file_with_hints).unwrap(),
1011 );
1012 Ok(Some(vec![
1013 lsp::InlayHint {
1014 position: lsp::Position::new(0, 1),
1015 label: lsp::InlayHintLabel::String("type hint".to_string()),
1016 kind: Some(lsp::InlayHintKind::TYPE),
1017 text_edits: None,
1018 tooltip: None,
1019 padding_left: None,
1020 padding_right: None,
1021 data: None,
1022 },
1023 lsp::InlayHint {
1024 position: lsp::Position::new(0, 2),
1025 label: lsp::InlayHintLabel::String("parameter hint".to_string()),
1026 kind: Some(lsp::InlayHintKind::PARAMETER),
1027 text_edits: None,
1028 tooltip: None,
1029 padding_left: None,
1030 padding_right: None,
1031 data: None,
1032 },
1033 lsp::InlayHint {
1034 position: lsp::Position::new(0, 3),
1035 label: lsp::InlayHintLabel::String("other hint".to_string()),
1036 kind: None,
1037 text_edits: None,
1038 tooltip: None,
1039 padding_left: None,
1040 padding_right: None,
1041 data: None,
1042 },
1043 ]))
1044 }
1045 })
1046 .next()
1047 .await;
1048 cx.foreground().finish_waiting();
1049 cx.foreground().run_until_parked();
1050
1051 let mut edits_made = 1;
1052 editor.update(cx, |editor, cx| {
1053 assert_eq!(
1054 lsp_request_count.load(Ordering::Relaxed),
1055 1,
1056 "Should query new hints once"
1057 );
1058 assert_eq!(
1059 vec![
1060 "type hint".to_string(),
1061 "parameter hint".to_string(),
1062 "other hint".to_string()
1063 ],
1064 cached_hint_labels(editor),
1065 "Should get its first hints when opening the editor"
1066 );
1067 assert_eq!(
1068 vec!["type hint".to_string(), "other hint".to_string()],
1069 visible_hint_labels(editor, cx)
1070 );
1071 let inlay_cache = editor.inlay_hint_cache();
1072 assert_eq!(
1073 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1074 "Cache should use editor settings to get the allowed hint kinds"
1075 );
1076 assert_eq!(
1077 inlay_cache.version, edits_made,
1078 "The editor update the cache version after every cache/view change"
1079 );
1080 });
1081
1082 fake_server
1083 .request::<lsp::request::InlayHintRefreshRequest>(())
1084 .await
1085 .expect("inlay refresh request failed");
1086 cx.foreground().run_until_parked();
1087 editor.update(cx, |editor, cx| {
1088 assert_eq!(
1089 lsp_request_count.load(Ordering::Relaxed),
1090 2,
1091 "Should load new hints twice"
1092 );
1093 assert_eq!(
1094 vec![
1095 "type hint".to_string(),
1096 "parameter hint".to_string(),
1097 "other hint".to_string()
1098 ],
1099 cached_hint_labels(editor),
1100 "Cached hints should not change due to allowed hint kinds settings update"
1101 );
1102 assert_eq!(
1103 vec!["type hint".to_string(), "other hint".to_string()],
1104 visible_hint_labels(editor, cx)
1105 );
1106 let inlay_cache = editor.inlay_hint_cache();
1107 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
1108 assert_eq!(
1109 inlay_cache.version, edits_made,
1110 "Should not update cache version due to new loaded hints being the same"
1111 );
1112 });
1113
1114 for (new_allowed_hint_kinds, expected_visible_hints) in [
1115 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1116 (
1117 HashSet::from_iter([Some(InlayHintKind::Type)]),
1118 vec!["type hint".to_string()],
1119 ),
1120 (
1121 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1122 vec!["parameter hint".to_string()],
1123 ),
1124 (
1125 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1126 vec!["type hint".to_string(), "other hint".to_string()],
1127 ),
1128 (
1129 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1130 vec!["parameter hint".to_string(), "other hint".to_string()],
1131 ),
1132 (
1133 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1134 vec!["type hint".to_string(), "parameter hint".to_string()],
1135 ),
1136 (
1137 HashSet::from_iter([
1138 None,
1139 Some(InlayHintKind::Type),
1140 Some(InlayHintKind::Parameter),
1141 ]),
1142 vec![
1143 "type hint".to_string(),
1144 "parameter hint".to_string(),
1145 "other hint".to_string(),
1146 ],
1147 ),
1148 ] {
1149 edits_made += 1;
1150 update_test_settings(cx, |settings| {
1151 settings.defaults.inlay_hints = Some(InlayHintSettings {
1152 enabled: true,
1153 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1154 show_parameter_hints: new_allowed_hint_kinds
1155 .contains(&Some(InlayHintKind::Parameter)),
1156 show_other_hints: new_allowed_hint_kinds.contains(&None),
1157 })
1158 });
1159 cx.foreground().run_until_parked();
1160 editor.update(cx, |editor, cx| {
1161 assert_eq!(
1162 lsp_request_count.load(Ordering::Relaxed),
1163 2,
1164 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1165 );
1166 assert_eq!(
1167 vec![
1168 "type hint".to_string(),
1169 "parameter hint".to_string(),
1170 "other hint".to_string(),
1171 ],
1172 cached_hint_labels(editor),
1173 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1174 );
1175 assert_eq!(
1176 expected_visible_hints,
1177 visible_hint_labels(editor, cx),
1178 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1179 );
1180 let inlay_cache = editor.inlay_hint_cache();
1181 assert_eq!(
1182 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1183 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1184 );
1185 assert_eq!(
1186 inlay_cache.version, edits_made,
1187 "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
1188 );
1189 });
1190 }
1191
1192 edits_made += 1;
1193 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1194 update_test_settings(cx, |settings| {
1195 settings.defaults.inlay_hints = Some(InlayHintSettings {
1196 enabled: false,
1197 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1198 show_parameter_hints: another_allowed_hint_kinds
1199 .contains(&Some(InlayHintKind::Parameter)),
1200 show_other_hints: another_allowed_hint_kinds.contains(&None),
1201 })
1202 });
1203 cx.foreground().run_until_parked();
1204 editor.update(cx, |editor, cx| {
1205 assert_eq!(
1206 lsp_request_count.load(Ordering::Relaxed),
1207 2,
1208 "Should not load new hints when hints got disabled"
1209 );
1210 assert!(
1211 cached_hint_labels(editor).is_empty(),
1212 "Should clear the cache when hints got disabled"
1213 );
1214 assert!(
1215 visible_hint_labels(editor, cx).is_empty(),
1216 "Should clear visible hints when hints got disabled"
1217 );
1218 let inlay_cache = editor.inlay_hint_cache();
1219 assert_eq!(
1220 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1221 "Should update its allowed hint kinds even when hints got disabled"
1222 );
1223 assert_eq!(
1224 inlay_cache.version, edits_made,
1225 "The editor should update the cache version after hints got disabled"
1226 );
1227 });
1228
1229 fake_server
1230 .request::<lsp::request::InlayHintRefreshRequest>(())
1231 .await
1232 .expect("inlay refresh request failed");
1233 cx.foreground().run_until_parked();
1234 editor.update(cx, |editor, cx| {
1235 assert_eq!(
1236 lsp_request_count.load(Ordering::Relaxed),
1237 2,
1238 "Should not load new hints when they got disabled"
1239 );
1240 assert!(cached_hint_labels(editor).is_empty());
1241 assert!(visible_hint_labels(editor, cx).is_empty());
1242 let inlay_cache = editor.inlay_hint_cache();
1243 assert_eq!(inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds);
1244 assert_eq!(
1245 inlay_cache.version, edits_made,
1246 "The editor should not update the cache version after /refresh query without updates"
1247 );
1248 });
1249
1250 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1251 edits_made += 1;
1252 update_test_settings(cx, |settings| {
1253 settings.defaults.inlay_hints = Some(InlayHintSettings {
1254 enabled: true,
1255 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1256 show_parameter_hints: final_allowed_hint_kinds
1257 .contains(&Some(InlayHintKind::Parameter)),
1258 show_other_hints: final_allowed_hint_kinds.contains(&None),
1259 })
1260 });
1261 cx.foreground().run_until_parked();
1262 editor.update(cx, |editor, cx| {
1263 assert_eq!(
1264 lsp_request_count.load(Ordering::Relaxed),
1265 3,
1266 "Should query for new hints when they got reenabled"
1267 );
1268 assert_eq!(
1269 vec![
1270 "type hint".to_string(),
1271 "parameter hint".to_string(),
1272 "other hint".to_string(),
1273 ],
1274 cached_hint_labels(editor),
1275 "Should get its cached hints fully repopulated after the hints got reenabled"
1276 );
1277 assert_eq!(
1278 vec!["parameter hint".to_string()],
1279 visible_hint_labels(editor, cx),
1280 "Should get its visible hints repopulated and filtered after the h"
1281 );
1282 let inlay_cache = editor.inlay_hint_cache();
1283 assert_eq!(
1284 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
1285 "Cache should update editor settings when hints got reenabled"
1286 );
1287 assert_eq!(
1288 inlay_cache.version, edits_made,
1289 "Cache should update its version after hints got reenabled"
1290 );
1291 });
1292
1293 fake_server
1294 .request::<lsp::request::InlayHintRefreshRequest>(())
1295 .await
1296 .expect("inlay refresh request failed");
1297 cx.foreground().run_until_parked();
1298 editor.update(cx, |editor, cx| {
1299 assert_eq!(
1300 lsp_request_count.load(Ordering::Relaxed),
1301 4,
1302 "Should query for new hints again"
1303 );
1304 assert_eq!(
1305 vec![
1306 "type hint".to_string(),
1307 "parameter hint".to_string(),
1308 "other hint".to_string(),
1309 ],
1310 cached_hint_labels(editor),
1311 );
1312 assert_eq!(
1313 vec!["parameter hint".to_string()],
1314 visible_hint_labels(editor, cx),
1315 );
1316 let inlay_cache = editor.inlay_hint_cache();
1317 assert_eq!(inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,);
1318 assert_eq!(inlay_cache.version, edits_made);
1319 });
1320 }
1321
1322 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
1323 cx.foreground().forbid_parking();
1324
1325 cx.update(|cx| {
1326 cx.set_global(SettingsStore::test(cx));
1327 theme::init((), cx);
1328 client::init_settings(cx);
1329 language::init(cx);
1330 Project::init_settings(cx);
1331 workspace::init_settings(cx);
1332 crate::init(cx);
1333 });
1334
1335 update_test_settings(cx, f);
1336 }
1337
1338 fn cached_hint_labels(editor: &Editor) -> Vec<String> {
1339 let mut labels = Vec::new();
1340 for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
1341 let excerpt_hints = excerpt_hints.read();
1342 for (_, inlay) in excerpt_hints.hints.iter() {
1343 match &inlay.label {
1344 project::InlayHintLabel::String(s) => labels.push(s.to_string()),
1345 _ => unreachable!(),
1346 }
1347 }
1348 }
1349 labels
1350 }
1351
1352 fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
1353 editor
1354 .visible_inlay_hints(cx)
1355 .into_iter()
1356 .map(|hint| hint.text.to_string())
1357 .collect()
1358 }
1359}