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