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 dbg!(new_hint_settings);
164 let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
165 match (self.enabled, new_hint_settings.enabled) {
166 (false, false) => {
167 self.allowed_hint_kinds = new_allowed_hint_kinds;
168 ControlFlow::Break(None)
169 }
170 (true, true) => {
171 if new_allowed_hint_kinds == self.allowed_hint_kinds {
172 ControlFlow::Break(None)
173 } else {
174 let new_splice = self.new_allowed_hint_kinds_splice(
175 multi_buffer,
176 &visible_hints,
177 &new_allowed_hint_kinds,
178 cx,
179 );
180 if new_splice.is_some() {
181 self.version += 1;
182 self.update_tasks.clear();
183 self.allowed_hint_kinds = new_allowed_hint_kinds;
184 }
185 ControlFlow::Break(new_splice)
186 }
187 }
188 (true, false) => {
189 self.enabled = new_hint_settings.enabled;
190 self.allowed_hint_kinds = new_allowed_hint_kinds;
191 if self.hints.is_empty() {
192 ControlFlow::Break(None)
193 } else {
194 self.clear();
195 ControlFlow::Break(Some(InlaySplice {
196 to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
197 to_insert: Vec::new(),
198 }))
199 }
200 }
201 (false, true) => {
202 self.enabled = new_hint_settings.enabled;
203 self.allowed_hint_kinds = new_allowed_hint_kinds;
204 ControlFlow::Continue(())
205 }
206 }
207 }
208
209 pub fn refresh_inlay_hints(
210 &mut self,
211 mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
212 invalidate: InvalidationStrategy,
213 cx: &mut ViewContext<Editor>,
214 ) {
215 if !self.enabled {
216 return;
217 }
218 let update_tasks = &mut self.update_tasks;
219 let invalidate_cache = matches!(
220 invalidate,
221 InvalidationStrategy::Forced | InvalidationStrategy::OnConflict
222 );
223 if invalidate_cache {
224 update_tasks
225 .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
226 }
227 let cache_version = self.version;
228 excerpts_to_query.retain(|visible_excerpt_id, _| {
229 match update_tasks.entry(*visible_excerpt_id) {
230 hash_map::Entry::Occupied(o) => match o.get().cache_version().cmp(&cache_version) {
231 cmp::Ordering::Less => true,
232 cmp::Ordering::Equal => invalidate_cache,
233 cmp::Ordering::Greater => false,
234 },
235 hash_map::Entry::Vacant(_) => true,
236 }
237 });
238
239 cx.spawn(|editor, mut cx| async move {
240 editor
241 .update(&mut cx, |editor, cx| {
242 spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx)
243 })
244 .ok();
245 })
246 .detach();
247 }
248
249 fn new_allowed_hint_kinds_splice(
250 &self,
251 multi_buffer: &ModelHandle<MultiBuffer>,
252 visible_hints: &[Inlay],
253 new_kinds: &HashSet<Option<InlayHintKind>>,
254 cx: &mut ViewContext<Editor>,
255 ) -> Option<InlaySplice> {
256 let old_kinds = &self.allowed_hint_kinds;
257 if new_kinds == old_kinds {
258 return None;
259 }
260
261 let mut to_remove = Vec::new();
262 let mut to_insert = Vec::new();
263 let mut shown_hints_to_remove = visible_hints.iter().fold(
264 HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
265 |mut current_hints, inlay| {
266 current_hints
267 .entry(inlay.position.excerpt_id)
268 .or_default()
269 .push((inlay.position, inlay.id));
270 current_hints
271 },
272 );
273
274 let multi_buffer = multi_buffer.read(cx);
275 let multi_buffer_snapshot = multi_buffer.snapshot(cx);
276
277 for (excerpt_id, excerpt_cached_hints) in &self.hints {
278 let shown_excerpt_hints_to_remove =
279 shown_hints_to_remove.entry(*excerpt_id).or_default();
280 let excerpt_cached_hints = excerpt_cached_hints.read();
281 let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable();
282 shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
283 let Some(buffer) = shown_anchor
284 .buffer_id
285 .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false };
286 let buffer_snapshot = buffer.read(cx).snapshot();
287 loop {
288 match excerpt_cache.peek() {
289 Some((cached_hint_id, cached_hint)) => {
290 if cached_hint_id == shown_hint_id {
291 excerpt_cache.next();
292 return !new_kinds.contains(&cached_hint.kind);
293 }
294
295 match cached_hint
296 .position
297 .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
298 {
299 cmp::Ordering::Less | cmp::Ordering::Equal => {
300 if !old_kinds.contains(&cached_hint.kind)
301 && new_kinds.contains(&cached_hint.kind)
302 {
303 to_insert.push((
304 multi_buffer_snapshot.anchor_in_excerpt(
305 *excerpt_id,
306 cached_hint.position,
307 ),
308 *cached_hint_id,
309 cached_hint.clone(),
310 ));
311 }
312 excerpt_cache.next();
313 }
314 cmp::Ordering::Greater => return true,
315 }
316 }
317 None => return true,
318 }
319 }
320 });
321
322 for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
323 let cached_hint_kind = maybe_missed_cached_hint.kind;
324 if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
325 to_insert.push((
326 multi_buffer_snapshot
327 .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
328 *cached_hint_id,
329 maybe_missed_cached_hint.clone(),
330 ));
331 }
332 }
333 }
334
335 to_remove.extend(
336 shown_hints_to_remove
337 .into_values()
338 .flatten()
339 .map(|(_, hint_id)| hint_id),
340 );
341 if to_remove.is_empty() && to_insert.is_empty() {
342 None
343 } else {
344 Some(InlaySplice {
345 to_remove,
346 to_insert,
347 })
348 }
349 }
350
351 fn clear(&mut self) {
352 self.version += 1;
353 self.update_tasks.clear();
354 self.hints.clear();
355 self.allowed_hint_kinds.clear();
356 }
357}
358
359fn spawn_new_update_tasks(
360 editor: &mut Editor,
361 excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
362 invalidation_strategy: InvalidationStrategy,
363 update_cache_version: usize,
364 cx: &mut ViewContext<'_, '_, Editor>,
365) {
366 let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
367 for (excerpt_id, (buffer_handle, excerpt_visible_range)) in excerpts_to_query {
368 if !excerpt_visible_range.is_empty() {
369 let buffer = buffer_handle.read(cx);
370 let buffer_snapshot = buffer.snapshot();
371 let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
372 let cache_is_empty = match &cached_excerpt_hints {
373 Some(cached_excerpt_hints) => {
374 let new_task_buffer_version = buffer_snapshot.version();
375 let cached_excerpt_hints = cached_excerpt_hints.read();
376 let cached_buffer_version = &cached_excerpt_hints.buffer_version;
377 if cached_excerpt_hints.version > update_cache_version
378 || cached_buffer_version.changed_since(new_task_buffer_version)
379 {
380 return;
381 }
382 if !new_task_buffer_version.changed_since(&cached_buffer_version)
383 && !matches!(invalidation_strategy, InvalidationStrategy::Forced)
384 {
385 return;
386 }
387
388 cached_excerpt_hints.hints.is_empty()
389 }
390 None => true,
391 };
392
393 let buffer_id = buffer.remote_id();
394 let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
395 let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
396
397 let (multi_buffer_snapshot, full_excerpt_range) =
398 editor.buffer.update(cx, |multi_buffer, cx| {
399 let multi_buffer_snapshot = multi_buffer.snapshot(cx);
400 (
401 multi_buffer_snapshot,
402 multi_buffer
403 .excerpts_for_buffer(&buffer_handle, cx)
404 .into_iter()
405 .find(|(id, _)| id == &excerpt_id)
406 .map(|(_, range)| range.context),
407 )
408 });
409
410 if let Some(full_excerpt_range) = full_excerpt_range {
411 let query = ExcerptQuery {
412 buffer_id,
413 excerpt_id,
414 dimensions: ExcerptDimensions {
415 excerpt_range_start: full_excerpt_range.start,
416 excerpt_range_end: full_excerpt_range.end,
417 excerpt_visible_range_start,
418 excerpt_visible_range_end,
419 },
420 cache_version: update_cache_version,
421 invalidate: invalidation_strategy,
422 };
423
424 let new_update_task = |previous_task| {
425 new_update_task(
426 query,
427 multi_buffer_snapshot,
428 buffer_snapshot,
429 Arc::clone(&visible_hints),
430 cached_excerpt_hints,
431 previous_task,
432 cx,
433 )
434 };
435 match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
436 hash_map::Entry::Occupied(mut o) => {
437 let update_task = o.get_mut();
438 if update_task.is_running() {
439 match (update_task.invalidation_strategy(), invalidation_strategy) {
440 (InvalidationStrategy::Forced, _)
441 | (_, InvalidationStrategy::OnConflict) => {
442 o.insert(UpdateTask::new(
443 invalidation_strategy,
444 new_update_task(None),
445 ));
446 }
447 (_, InvalidationStrategy::Forced) => {
448 if cache_is_empty {
449 o.insert(UpdateTask::new(
450 invalidation_strategy,
451 new_update_task(None),
452 ));
453 } else if update_task.pending_refresh.is_none() {
454 update_task.pending_refresh = Some(new_update_task(Some(
455 update_task.current.1.is_running_rx.clone(),
456 )));
457 }
458 }
459 _ => {}
460 }
461 } else {
462 o.insert(UpdateTask::new(
463 invalidation_strategy,
464 new_update_task(None),
465 ));
466 }
467 }
468 hash_map::Entry::Vacant(v) => {
469 v.insert(UpdateTask::new(
470 invalidation_strategy,
471 new_update_task(None),
472 ));
473 }
474 }
475 }
476 }
477 }
478}
479
480fn new_update_task(
481 query: ExcerptQuery,
482 multi_buffer_snapshot: MultiBufferSnapshot,
483 buffer_snapshot: BufferSnapshot,
484 visible_hints: Arc<Vec<Inlay>>,
485 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
486 task_before_refresh: Option<smol::channel::Receiver<()>>,
487 cx: &mut ViewContext<'_, '_, Editor>,
488) -> SpawnedTask {
489 let hints_fetch_tasks = query.hints_fetch_ranges(&buffer_snapshot);
490 let (is_running_tx, is_running_rx) = smol::channel::bounded(1);
491 let is_refresh_task = task_before_refresh.is_some();
492 let _task = cx.spawn(|editor, cx| async move {
493 let _is_running_tx = is_running_tx;
494 if let Some(task_before_refresh) = task_before_refresh {
495 task_before_refresh.recv().await.ok();
496 }
497 let create_update_task = |range| {
498 fetch_and_update_hints(
499 editor.clone(),
500 multi_buffer_snapshot.clone(),
501 buffer_snapshot.clone(),
502 Arc::clone(&visible_hints),
503 cached_excerpt_hints.as_ref().map(Arc::clone),
504 query,
505 range,
506 cx.clone(),
507 )
508 };
509
510 if is_refresh_task {
511 let visible_range_has_updates =
512 match create_update_task(hints_fetch_tasks.visible_range).await {
513 Ok(updated) => updated,
514 Err(e) => {
515 error!("inlay hint visible range update task failed: {e:#}");
516 return;
517 }
518 };
519
520 if visible_range_has_updates {
521 let other_update_results = futures::future::join_all(
522 hints_fetch_tasks
523 .other_ranges
524 .into_iter()
525 .map(create_update_task),
526 )
527 .await;
528
529 for result in other_update_results {
530 if let Err(e) = result {
531 error!("inlay hint update task failed: {e:#}");
532 return;
533 }
534 }
535 }
536 } else {
537 let task_update_results = futures::future::join_all(
538 std::iter::once(hints_fetch_tasks.visible_range)
539 .chain(hints_fetch_tasks.other_ranges.into_iter())
540 .map(create_update_task),
541 )
542 .await;
543
544 for result in task_update_results {
545 if let Err(e) = result {
546 error!("inlay hint update task failed: {e:#}");
547 }
548 }
549 }
550 });
551
552 SpawnedTask {
553 version: query.cache_version,
554 _task,
555 is_running_rx,
556 }
557}
558
559async fn fetch_and_update_hints(
560 editor: gpui::WeakViewHandle<Editor>,
561 multi_buffer_snapshot: MultiBufferSnapshot,
562 buffer_snapshot: BufferSnapshot,
563 visible_hints: Arc<Vec<Inlay>>,
564 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
565 query: ExcerptQuery,
566 fetch_range: Range<language::Anchor>,
567 mut cx: gpui::AsyncAppContext,
568) -> anyhow::Result<bool> {
569 let inlay_hints_fetch_task = editor
570 .update(&mut cx, |editor, cx| {
571 editor
572 .buffer()
573 .read(cx)
574 .buffer(query.buffer_id)
575 .and_then(|buffer| {
576 let project = editor.project.as_ref()?;
577 Some(project.update(cx, |project, cx| {
578 project.inlay_hints(buffer, fetch_range.clone(), cx)
579 }))
580 })
581 })
582 .ok()
583 .flatten();
584 let mut update_happened = false;
585 let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) };
586
587 let new_hints = inlay_hints_fetch_task
588 .await
589 .context("inlay hint fetch task")?;
590 let background_task_buffer_snapshot = buffer_snapshot.clone();
591 let backround_fetch_range = fetch_range.clone();
592 if let Some(new_update) = cx
593 .background()
594 .spawn(async move {
595 calculate_hint_updates(
596 query,
597 backround_fetch_range,
598 new_hints,
599 &background_task_buffer_snapshot,
600 cached_excerpt_hints,
601 &visible_hints,
602 )
603 })
604 .await
605 {
606 update_happened = !new_update.add_to_cache.is_empty()
607 || !new_update.remove_from_cache.is_empty()
608 || !new_update.remove_from_visible.is_empty();
609 editor
610 .update(&mut cx, |editor, cx| {
611 let cached_excerpt_hints = editor
612 .inlay_hint_cache
613 .hints
614 .entry(new_update.excerpt_id)
615 .or_insert_with(|| {
616 Arc::new(RwLock::new(CachedExcerptHints {
617 version: new_update.cache_version,
618 buffer_version: buffer_snapshot.version().clone(),
619 hints: Vec::new(),
620 }))
621 });
622 let mut cached_excerpt_hints = cached_excerpt_hints.write();
623 match new_update.cache_version.cmp(&cached_excerpt_hints.version) {
624 cmp::Ordering::Less => return,
625 cmp::Ordering::Greater | cmp::Ordering::Equal => {
626 cached_excerpt_hints.version = new_update.cache_version;
627 }
628 }
629 cached_excerpt_hints
630 .hints
631 .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
632 cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
633 editor.inlay_hint_cache.version += 1;
634
635 let mut splice = InlaySplice {
636 to_remove: new_update.remove_from_visible,
637 to_insert: Vec::new(),
638 };
639
640 for new_hint in new_update.add_to_cache {
641 let new_hint_position = multi_buffer_snapshot
642 .anchor_in_excerpt(query.excerpt_id, new_hint.position);
643 let new_inlay_id = InlayId::Hint(post_inc(&mut editor.next_inlay_id));
644 if editor
645 .inlay_hint_cache
646 .allowed_hint_kinds
647 .contains(&new_hint.kind)
648 {
649 splice
650 .to_insert
651 .push((new_hint_position, new_inlay_id, new_hint.clone()));
652 }
653
654 cached_excerpt_hints.hints.push((new_inlay_id, new_hint));
655 }
656
657 cached_excerpt_hints
658 .hints
659 .sort_by(|(_, hint_a), (_, hint_b)| {
660 hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
661 });
662 drop(cached_excerpt_hints);
663
664 let InlaySplice {
665 to_remove,
666 to_insert,
667 } = splice;
668 if !to_remove.is_empty() || !to_insert.is_empty() {
669 editor.splice_inlay_hints(to_remove, to_insert, cx)
670 }
671 })
672 .ok();
673 }
674
675 Ok(update_happened)
676}
677
678fn calculate_hint_updates(
679 query: ExcerptQuery,
680 fetch_range: Range<language::Anchor>,
681 new_excerpt_hints: Vec<InlayHint>,
682 buffer_snapshot: &BufferSnapshot,
683 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
684 visible_hints: &[Inlay],
685) -> Option<ExcerptHintsUpdate> {
686 let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
687 let mut excerpt_hints_to_persist = HashMap::default();
688 for new_hint in new_excerpt_hints {
689 if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
690 continue;
691 }
692 let missing_from_cache = match &cached_excerpt_hints {
693 Some(cached_excerpt_hints) => {
694 let cached_excerpt_hints = cached_excerpt_hints.read();
695 match cached_excerpt_hints.hints.binary_search_by(|probe| {
696 probe.1.position.cmp(&new_hint.position, buffer_snapshot)
697 }) {
698 Ok(ix) => {
699 let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
700 if cached_hint == &new_hint {
701 excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
702 false
703 } else {
704 true
705 }
706 }
707 Err(_) => true,
708 }
709 }
710 None => true,
711 };
712 if missing_from_cache {
713 add_to_cache.insert(new_hint);
714 }
715 }
716
717 let mut remove_from_visible = Vec::new();
718 let mut remove_from_cache = HashSet::default();
719 if matches!(
720 query.invalidate,
721 InvalidationStrategy::Forced | InvalidationStrategy::OnConflict
722 ) {
723 remove_from_visible.extend(
724 visible_hints
725 .iter()
726 .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
727 .filter(|hint| {
728 contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
729 })
730 .filter(|hint| {
731 fetch_range
732 .start
733 .cmp(&hint.position.text_anchor, buffer_snapshot)
734 .is_le()
735 && fetch_range
736 .end
737 .cmp(&hint.position.text_anchor, buffer_snapshot)
738 .is_ge()
739 })
740 .map(|inlay_hint| inlay_hint.id)
741 .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
742 );
743
744 if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
745 let cached_excerpt_hints = cached_excerpt_hints.read();
746 remove_from_cache.extend(
747 cached_excerpt_hints
748 .hints
749 .iter()
750 .filter(|(cached_inlay_id, _)| {
751 !excerpt_hints_to_persist.contains_key(cached_inlay_id)
752 })
753 .filter(|(_, cached_hint)| {
754 fetch_range
755 .start
756 .cmp(&cached_hint.position, buffer_snapshot)
757 .is_le()
758 && fetch_range
759 .end
760 .cmp(&cached_hint.position, buffer_snapshot)
761 .is_ge()
762 })
763 .map(|(cached_inlay_id, _)| *cached_inlay_id),
764 );
765 }
766 }
767
768 if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
769 None
770 } else {
771 Some(ExcerptHintsUpdate {
772 cache_version: query.cache_version,
773 excerpt_id: query.excerpt_id,
774 remove_from_visible,
775 remove_from_cache,
776 add_to_cache,
777 })
778 }
779}
780
781struct HintFetchRanges {
782 visible_range: Range<language::Anchor>,
783 other_ranges: Vec<Range<language::Anchor>>,
784}
785
786fn contains_position(
787 range: &Range<language::Anchor>,
788 position: language::Anchor,
789 buffer_snapshot: &BufferSnapshot,
790) -> bool {
791 range.start.cmp(&position, buffer_snapshot).is_le()
792 && range.end.cmp(&position, buffer_snapshot).is_ge()
793}
794
795#[cfg(test)]
796mod tests {
797 use std::sync::atomic::{AtomicU32, Ordering};
798
799 use crate::{serde_json::json, InlayHintSettings};
800 use futures::StreamExt;
801 use gpui::{TestAppContext, ViewHandle};
802 use language::{
803 language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
804 FakeLspAdapter, Language, LanguageConfig,
805 };
806 use lsp::FakeLanguageServer;
807 use project::{FakeFs, Project};
808 use settings::SettingsStore;
809 use workspace::Workspace;
810
811 use crate::editor_tests::update_test_settings;
812
813 use super::*;
814
815 #[gpui::test]
816 async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
817 init_test(cx, |_| {});
818 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
819 let (file_with_hints, editor, fake_server) =
820 prepare_test_objects(cx, &allowed_hint_kinds).await;
821
822 let lsp_request_count = Arc::new(AtomicU32::new(0));
823 fake_server
824 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
825 let task_lsp_request_count = Arc::clone(&lsp_request_count);
826 async move {
827 assert_eq!(
828 params.text_document.uri,
829 lsp::Url::from_file_path(file_with_hints).unwrap(),
830 );
831 let current_call_id =
832 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
833 let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
834 for _ in 0..2 {
835 let mut i = current_call_id;
836 loop {
837 new_hints.push(lsp::InlayHint {
838 position: lsp::Position::new(0, i),
839 label: lsp::InlayHintLabel::String(i.to_string()),
840 kind: None,
841 text_edits: None,
842 tooltip: None,
843 padding_left: None,
844 padding_right: None,
845 data: None,
846 });
847 if i == 0 {
848 break;
849 }
850 i -= 1;
851 }
852 }
853
854 Ok(Some(new_hints))
855 }
856 })
857 .next()
858 .await;
859 cx.foreground().finish_waiting();
860 cx.foreground().run_until_parked();
861 let mut edits_made = 1;
862 editor.update(cx, |editor, cx| {
863 let expected_layers = vec!["0".to_string()];
864 assert_eq!(
865 expected_layers,
866 cached_hint_labels(editor),
867 "Should get its first hints when opening the editor"
868 );
869 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
870 let inlay_cache = editor.inlay_hint_cache();
871 assert_eq!(
872 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
873 "Cache should use editor settings to get the allowed hint kinds"
874 );
875 assert_eq!(
876 inlay_cache.version, edits_made,
877 "The editor update the cache version after every cache/view change"
878 );
879 });
880
881 editor.update(cx, |editor, cx| {
882 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
883 editor.handle_input("some change", cx);
884 edits_made += 1;
885 });
886 cx.foreground().run_until_parked();
887 editor.update(cx, |editor, cx| {
888 let expected_layers = vec!["0".to_string(), "1".to_string()];
889 assert_eq!(
890 expected_layers,
891 cached_hint_labels(editor),
892 "Should get new hints after an edit"
893 );
894 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
895 let inlay_cache = editor.inlay_hint_cache();
896 assert_eq!(
897 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
898 "Cache should use editor settings to get the allowed hint kinds"
899 );
900 assert_eq!(
901 inlay_cache.version, edits_made,
902 "The editor update the cache version after every cache/view change"
903 );
904 });
905
906 fake_server
907 .request::<lsp::request::InlayHintRefreshRequest>(())
908 .await
909 .expect("inlay refresh request failed");
910 edits_made += 1;
911 cx.foreground().run_until_parked();
912 editor.update(cx, |editor, cx| {
913 let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
914 assert_eq!(
915 expected_layers,
916 cached_hint_labels(editor),
917 "Should get new hints after hint refresh/ request"
918 );
919 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
920 let inlay_cache = editor.inlay_hint_cache();
921 assert_eq!(
922 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
923 "Cache should use editor settings to get the allowed hint kinds"
924 );
925 assert_eq!(
926 inlay_cache.version, edits_made,
927 "The editor update the cache version after every cache/view change"
928 );
929 });
930 }
931
932 async fn prepare_test_objects(
933 cx: &mut TestAppContext,
934 allowed_hint_kinds: &HashSet<Option<InlayHintKind>>,
935 ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
936 cx.update(|cx| {
937 cx.update_global(|store: &mut SettingsStore, cx| {
938 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
939 settings.defaults.inlay_hints = Some(InlayHintSettings {
940 enabled: true,
941 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
942 show_parameter_hints: allowed_hint_kinds
943 .contains(&Some(InlayHintKind::Parameter)),
944 show_other_hints: allowed_hint_kinds.contains(&None),
945 })
946 });
947 });
948 });
949
950 let mut language = Language::new(
951 LanguageConfig {
952 name: "Rust".into(),
953 path_suffixes: vec!["rs".to_string()],
954 ..Default::default()
955 },
956 Some(tree_sitter_rust::language()),
957 );
958 let mut fake_servers = language
959 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
960 capabilities: lsp::ServerCapabilities {
961 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
962 ..Default::default()
963 },
964 ..Default::default()
965 }))
966 .await;
967
968 let fs = FakeFs::new(cx.background());
969 fs.insert_tree(
970 "/a",
971 json!({
972 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
973 "other.rs": "// Test file",
974 }),
975 )
976 .await;
977
978 let project = Project::test(fs, ["/a".as_ref()], cx).await;
979 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
980 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
981 let worktree_id = workspace.update(cx, |workspace, cx| {
982 workspace.project().read_with(cx, |project, cx| {
983 project.worktrees(cx).next().unwrap().read(cx).id()
984 })
985 });
986
987 cx.foreground().start_waiting();
988 let editor = workspace
989 .update(cx, |workspace, cx| {
990 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
991 })
992 .await
993 .unwrap()
994 .downcast::<Editor>()
995 .unwrap();
996
997 let fake_server = fake_servers.next().await.unwrap();
998
999 ("/a/main.rs", editor, fake_server)
1000 }
1001
1002 #[gpui::test]
1003 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1004 init_test(cx, |_| {});
1005 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1006 let (file_with_hints, editor, fake_server) =
1007 prepare_test_objects(cx, &allowed_hint_kinds).await;
1008
1009 fake_server
1010 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| async move {
1011 assert_eq!(
1012 params.text_document.uri,
1013 lsp::Url::from_file_path(file_with_hints).unwrap(),
1014 );
1015 Ok(Some(vec![
1016 lsp::InlayHint {
1017 position: lsp::Position::new(0, 1),
1018 label: lsp::InlayHintLabel::String("type hint".to_string()),
1019 kind: Some(lsp::InlayHintKind::TYPE),
1020 text_edits: None,
1021 tooltip: None,
1022 padding_left: None,
1023 padding_right: None,
1024 data: None,
1025 },
1026 lsp::InlayHint {
1027 position: lsp::Position::new(0, 2),
1028 label: lsp::InlayHintLabel::String("parameter hint".to_string()),
1029 kind: Some(lsp::InlayHintKind::PARAMETER),
1030 text_edits: None,
1031 tooltip: None,
1032 padding_left: None,
1033 padding_right: None,
1034 data: None,
1035 },
1036 lsp::InlayHint {
1037 position: lsp::Position::new(0, 3),
1038 label: lsp::InlayHintLabel::String("other hint".to_string()),
1039 kind: None,
1040 text_edits: None,
1041 tooltip: None,
1042 padding_left: None,
1043 padding_right: None,
1044 data: None,
1045 },
1046 ]))
1047 })
1048 .next()
1049 .await;
1050 cx.foreground().finish_waiting();
1051 cx.foreground().run_until_parked();
1052
1053 let edits_made = 1;
1054 editor.update(cx, |editor, cx| {
1055 assert_eq!(
1056 vec![
1057 "type hint".to_string(),
1058 "parameter hint".to_string(),
1059 "other hint".to_string()
1060 ],
1061 cached_hint_labels(editor),
1062 "Should get its first hints when opening the editor"
1063 );
1064 assert_eq!(
1065 vec!["type hint".to_string(), "other hint".to_string()],
1066 visible_hint_labels(editor, cx)
1067 );
1068 let inlay_cache = editor.inlay_hint_cache();
1069 assert_eq!(
1070 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1071 "Cache should use editor settings to get the allowed hint kinds"
1072 );
1073 assert_eq!(
1074 inlay_cache.version, edits_made,
1075 "The editor update the cache version after every cache/view change"
1076 );
1077 });
1078
1079 //
1080 }
1081
1082 pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1083 cx.foreground().forbid_parking();
1084
1085 cx.update(|cx| {
1086 cx.set_global(SettingsStore::test(cx));
1087 theme::init((), cx);
1088 client::init_settings(cx);
1089 language::init(cx);
1090 Project::init_settings(cx);
1091 workspace::init_settings(cx);
1092 crate::init(cx);
1093 });
1094
1095 update_test_settings(cx, f);
1096 }
1097
1098 fn cached_hint_labels(editor: &Editor) -> Vec<String> {
1099 let mut labels = Vec::new();
1100 for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
1101 let excerpt_hints = excerpt_hints.read();
1102 for (_, inlay) in excerpt_hints.hints.iter() {
1103 match &inlay.label {
1104 project::InlayHintLabel::String(s) => labels.push(s.to_string()),
1105 _ => unreachable!(),
1106 }
1107 }
1108 }
1109 labels
1110 }
1111
1112 fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
1113 editor
1114 .visible_inlay_hints(cx)
1115 .into_iter()
1116 .map(|hint| hint.text.to_string())
1117 .collect()
1118 }
1119}