inlay_hint_cache.rs

  1use std::{cmp, ops::Range};
  2
  3use crate::{
  4    display_map::Inlay, editor_settings, Anchor, Editor, ExcerptId, InlayId, MultiBuffer,
  5    MultiBufferSnapshot,
  6};
  7use anyhow::Context;
  8use gpui::{ModelHandle, Task, ViewContext};
  9use language::BufferSnapshot;
 10use log::error;
 11use project::{InlayHint, InlayHintKind};
 12
 13use collections::{hash_map, HashMap, HashSet};
 14use util::post_inc;
 15
 16pub struct InlayHintCache {
 17    snapshot: Box<CacheSnapshot>,
 18    update_tasks: HashMap<ExcerptId, InlayHintUpdateTask>,
 19}
 20
 21struct InlayHintUpdateTask {
 22    version: usize,
 23    _task: Task<()>,
 24}
 25
 26#[derive(Clone)]
 27struct CacheSnapshot {
 28    hints: HashMap<ExcerptId, ExcerptCachedHints>,
 29    allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
 30    version: usize,
 31}
 32
 33#[derive(Clone)]
 34struct ExcerptCachedHints {
 35    version: usize,
 36    hints: Vec<(InlayId, InlayHint)>,
 37}
 38
 39#[derive(Clone)]
 40pub struct HintsUpdateState {
 41    visible_inlays: Vec<Inlay>,
 42    cache: Box<CacheSnapshot>,
 43}
 44
 45#[derive(Debug, Clone)]
 46struct ExcerptQuery {
 47    buffer_id: u64,
 48    excerpt_id: ExcerptId,
 49    excerpt_range: Range<language::Anchor>,
 50    cache_version: usize,
 51    invalidate_cache: bool,
 52}
 53
 54#[derive(Debug, Default)]
 55pub struct InlaySplice {
 56    pub to_remove: Vec<InlayId>,
 57    pub to_insert: Vec<(Anchor, InlayId, InlayHint)>,
 58}
 59
 60#[derive(Debug)]
 61struct ExcerptHintsUpdate {
 62    excerpt_id: ExcerptId,
 63    cache_version: usize,
 64    remove_from_visible: Vec<InlayId>,
 65    remove_from_cache: HashSet<InlayId>,
 66    add_to_cache: Vec<InlayHint>,
 67}
 68
 69impl InlayHintCache {
 70    pub fn new(inlay_hint_settings: editor_settings::InlayHints) -> Self {
 71        Self {
 72            snapshot: Box::new(CacheSnapshot {
 73                allowed_hint_kinds: allowed_hint_types(inlay_hint_settings),
 74                hints: HashMap::default(),
 75                version: 0,
 76            }),
 77            update_tasks: HashMap::default(),
 78        }
 79    }
 80
 81    pub fn update_settings(
 82        &mut self,
 83        multi_buffer: &ModelHandle<MultiBuffer>,
 84        inlay_hint_settings: editor_settings::InlayHints,
 85        update_state: HintsUpdateState,
 86        cx: &mut ViewContext<Editor>,
 87    ) -> Option<InlaySplice> {
 88        let new_allowed_hint_kinds = allowed_hint_types(inlay_hint_settings);
 89        if !inlay_hint_settings.enabled {
 90            if self.snapshot.hints.is_empty() {
 91                self.snapshot.allowed_hint_kinds = new_allowed_hint_kinds;
 92            } else {
 93                self.clear();
 94                self.snapshot.allowed_hint_kinds = new_allowed_hint_kinds;
 95                return Some(InlaySplice {
 96                    to_remove: update_state
 97                        .visible_inlays
 98                        .iter()
 99                        .map(|inlay| inlay.id)
100                        .collect(),
101                    to_insert: Vec::new(),
102                });
103            }
104
105            return None;
106        }
107
108        if new_allowed_hint_kinds == self.snapshot.allowed_hint_kinds {
109            return None;
110        }
111
112        let new_splice =
113            new_allowed_hint_kinds_splice(multi_buffer, update_state, &new_allowed_hint_kinds, cx);
114        if new_splice.is_some() {
115            self.snapshot.version += 1;
116            self.update_tasks.clear();
117            self.snapshot.allowed_hint_kinds = new_allowed_hint_kinds;
118        }
119        new_splice
120    }
121
122    pub fn spawn_hints_update(
123        &mut self,
124        mut excerpts_to_query: HashMap<ExcerptId, u64>,
125        invalidate_cache: bool,
126        cx: &mut ViewContext<Editor>,
127    ) {
128        let update_tasks = &mut self.update_tasks;
129        if invalidate_cache {
130            update_tasks
131                .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
132        }
133        excerpts_to_query.retain(|visible_excerpt_id, _| {
134            match update_tasks.entry(*visible_excerpt_id) {
135                hash_map::Entry::Occupied(o) => match o.get().version.cmp(&self.snapshot.version) {
136                    cmp::Ordering::Less => true,
137                    cmp::Ordering::Equal => invalidate_cache,
138                    cmp::Ordering::Greater => false,
139                },
140                hash_map::Entry::Vacant(_) => true,
141            }
142        });
143
144        cx.spawn(|editor, mut cx| async move {
145            editor
146                .update(&mut cx, |editor, cx| {
147                    let mut excerpts_to_query = editor
148                        .excerpt_visible_offsets(cx)
149                        .into_iter()
150                        .map(|(buffer, _, excerpt_id)| (excerpt_id, buffer))
151                        .collect::<HashMap<_, _>>();
152
153                    let update_state = get_update_state(editor, cx);
154                    let update_tasks = &mut editor.inlay_hint_cache.update_tasks;
155                    if invalidate_cache {
156                        update_tasks.retain(|task_excerpt_id, _| {
157                            excerpts_to_query.contains_key(task_excerpt_id)
158                        });
159                    }
160
161                    let cache_version = editor.inlay_hint_cache.snapshot.version;
162                    excerpts_to_query.retain(|visible_excerpt_id, _| {
163                        match update_tasks.entry(*visible_excerpt_id) {
164                            hash_map::Entry::Occupied(o) => {
165                                match o.get().version.cmp(&cache_version) {
166                                    cmp::Ordering::Less => true,
167                                    cmp::Ordering::Equal => invalidate_cache,
168                                    cmp::Ordering::Greater => false,
169                                }
170                            }
171                            hash_map::Entry::Vacant(_) => true,
172                        }
173                    });
174
175                    for (excerpt_id, buffer_handle) in excerpts_to_query {
176                        let (multi_buffer_snapshot, excerpt_range) =
177                            editor.buffer.update(cx, |multi_buffer, cx| {
178                                let multi_buffer_snapshot = multi_buffer.snapshot(cx);
179                                (
180                                    multi_buffer_snapshot,
181                                    multi_buffer
182                                        .excerpts_for_buffer(&buffer_handle, cx)
183                                        .into_iter()
184                                        .find(|(id, _)| id == &excerpt_id)
185                                        .map(|(_, range)| range.context),
186                                )
187                            });
188
189                        if let Some(excerpt_range) = excerpt_range {
190                            let buffer = buffer_handle.read(cx);
191                            let buffer_snapshot = buffer.snapshot();
192                            let query = ExcerptQuery {
193                                buffer_id: buffer.remote_id(),
194                                excerpt_id,
195                                excerpt_range,
196                                cache_version,
197                                invalidate_cache,
198                            };
199                            update_tasks.insert(
200                                excerpt_id,
201                                new_update_task(
202                                    query,
203                                    update_state.clone(),
204                                    multi_buffer_snapshot,
205                                    buffer_snapshot,
206                                    cx,
207                                ),
208                            );
209                        }
210                    }
211                })
212                .ok();
213        })
214        .detach();
215    }
216
217    fn snapshot(&self) -> Box<CacheSnapshot> {
218        self.snapshot.clone()
219    }
220
221    fn clear(&mut self) {
222        self.snapshot.version += 1;
223        self.update_tasks.clear();
224        self.snapshot.hints.clear();
225        self.snapshot.allowed_hint_kinds.clear();
226    }
227}
228
229fn new_update_task(
230    query: ExcerptQuery,
231    state: HintsUpdateState,
232    multi_buffer_snapshot: MultiBufferSnapshot,
233    buffer_snapshot: BufferSnapshot,
234    cx: &mut ViewContext<'_, '_, Editor>,
235) -> InlayHintUpdateTask {
236    let hints_fetch_task = hints_fetch_task(query.clone(), cx);
237    InlayHintUpdateTask {
238        version: query.cache_version,
239        _task: cx.spawn(|editor, mut cx| async move {
240            match hints_fetch_task.await {
241                Ok(Some(new_hints)) => {
242                    let task_buffer_snapshot = buffer_snapshot.clone();
243                    if let Some(new_update) = cx
244                        .background()
245                        .spawn(async move {
246                            new_excerpt_hints_update_result(
247                                state,
248                                query.excerpt_id,
249                                new_hints,
250                                query.invalidate_cache,
251                                &task_buffer_snapshot,
252                                query.excerpt_range,
253                            )
254                        })
255                        .await
256                    {
257                        editor
258                            .update(&mut cx, |editor, cx| {
259                                let cached_excerpt_hints = editor
260                                    .inlay_hint_cache
261                                    .snapshot
262                                    .hints
263                                    .entry(new_update.excerpt_id)
264                                    .or_insert_with(|| ExcerptCachedHints {
265                                        version: new_update.cache_version,
266                                        hints: Vec::new(),
267                                    });
268                                match new_update.cache_version.cmp(&cached_excerpt_hints.version) {
269                                    cmp::Ordering::Less => return,
270                                    cmp::Ordering::Greater | cmp::Ordering::Equal => {
271                                        cached_excerpt_hints.version = new_update.cache_version;
272                                    }
273                                }
274
275                                editor.inlay_hint_cache.snapshot.version += 1;
276
277                                let mut splice = InlaySplice {
278                                    to_remove: new_update.remove_from_visible,
279                                    to_insert: Vec::new(),
280                                };
281
282                                for new_hint in new_update.add_to_cache {
283                                    let new_hint_position = multi_buffer_snapshot
284                                        .anchor_in_excerpt(query.excerpt_id, new_hint.position);
285                                    let new_inlay_id = InlayId(post_inc(&mut editor.next_inlay_id));
286                                    if editor
287                                        .inlay_hint_cache
288                                        .snapshot
289                                        .allowed_hint_kinds
290                                        .contains(&new_hint.kind)
291                                    {
292                                        splice.to_insert.push((
293                                            new_hint_position,
294                                            new_inlay_id,
295                                            new_hint.clone(),
296                                        ));
297                                    }
298
299                                    cached_excerpt_hints.hints.push((new_inlay_id, new_hint));
300                                }
301
302                                cached_excerpt_hints
303                                    .hints
304                                    .sort_by(|(_, hint_a), (_, hint_b)| {
305                                        hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
306                                    });
307                                editor.inlay_hint_cache.snapshot.hints.retain(
308                                    |_, excerpt_hints| {
309                                        excerpt_hints.hints.retain(|(hint_id, _)| {
310                                            !new_update.remove_from_cache.contains(hint_id)
311                                        });
312                                        !excerpt_hints.hints.is_empty()
313                                    },
314                                );
315
316                                let InlaySplice {
317                                    to_remove,
318                                    to_insert,
319                                } = splice;
320                                if !to_remove.is_empty() || !to_insert.is_empty() {
321                                    editor.splice_inlay_hints(to_remove, to_insert, cx)
322                                }
323                            })
324                            .ok();
325                    }
326                }
327                Ok(None) => {}
328                Err(e) => error!(
329                    "Failed to fecth hints for excerpt {:?} in buffer {} : {}",
330                    query.excerpt_id, query.buffer_id, e
331                ),
332            }
333        }),
334    }
335}
336
337pub fn get_update_state(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> HintsUpdateState {
338    HintsUpdateState {
339        visible_inlays: visible_inlay_hints(editor, cx).cloned().collect(),
340        cache: editor.inlay_hint_cache.snapshot(),
341    }
342}
343
344fn new_allowed_hint_kinds_splice(
345    multi_buffer: &ModelHandle<MultiBuffer>,
346    state: HintsUpdateState,
347    new_kinds: &HashSet<Option<InlayHintKind>>,
348    cx: &mut ViewContext<Editor>,
349) -> Option<InlaySplice> {
350    let old_kinds = &state.cache.allowed_hint_kinds;
351    if new_kinds == old_kinds {
352        return None;
353    }
354
355    let mut to_remove = Vec::new();
356    let mut to_insert = Vec::new();
357    let mut shown_hints_to_remove = state.visible_inlays.iter().fold(
358        HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
359        |mut current_hints, inlay| {
360            current_hints
361                .entry(inlay.position.excerpt_id)
362                .or_default()
363                .push((inlay.position, inlay.id));
364            current_hints
365        },
366    );
367
368    let multi_buffer = multi_buffer.read(cx);
369    let multi_buffer_snapshot = multi_buffer.snapshot(cx);
370
371    for (excerpt_id, excerpt_cached_hints) in &state.cache.hints {
372        let shown_excerpt_hints_to_remove = shown_hints_to_remove.entry(*excerpt_id).or_default();
373        let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable();
374        shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
375            let Some(buffer) = shown_anchor
376                .buffer_id
377                .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false };
378            let buffer_snapshot = buffer.read(cx).snapshot();
379            loop {
380                match excerpt_cache.peek() {
381                    Some((cached_hint_id, cached_hint)) => {
382                        if cached_hint_id == shown_hint_id {
383                            excerpt_cache.next();
384                            return !new_kinds.contains(&cached_hint.kind);
385                        }
386
387                        match cached_hint
388                            .position
389                            .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
390                        {
391                            cmp::Ordering::Less | cmp::Ordering::Equal => {
392                                if !old_kinds.contains(&cached_hint.kind)
393                                    && new_kinds.contains(&cached_hint.kind)
394                                {
395                                    to_insert.push((
396                                        multi_buffer_snapshot
397                                            .anchor_in_excerpt(*excerpt_id, cached_hint.position),
398                                        *cached_hint_id,
399                                        cached_hint.clone(),
400                                    ));
401                                }
402                                excerpt_cache.next();
403                            }
404                            cmp::Ordering::Greater => return true,
405                        }
406                    }
407                    None => return true,
408                }
409            }
410        });
411
412        for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
413            let cached_hint_kind = maybe_missed_cached_hint.kind;
414            if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
415                to_insert.push((
416                    multi_buffer_snapshot
417                        .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
418                    *cached_hint_id,
419                    maybe_missed_cached_hint.clone(),
420                ));
421            }
422        }
423    }
424
425    to_remove.extend(
426        shown_hints_to_remove
427            .into_values()
428            .flatten()
429            .map(|(_, hint_id)| hint_id),
430    );
431    if to_remove.is_empty() && to_insert.is_empty() {
432        None
433    } else {
434        Some(InlaySplice {
435            to_remove,
436            to_insert,
437        })
438    }
439}
440
441fn new_excerpt_hints_update_result(
442    state: HintsUpdateState,
443    excerpt_id: ExcerptId,
444    new_excerpt_hints: Vec<InlayHint>,
445    invalidate_cache: bool,
446    buffer_snapshot: &BufferSnapshot,
447    excerpt_range: Range<language::Anchor>,
448) -> Option<ExcerptHintsUpdate> {
449    let mut add_to_cache: Vec<InlayHint> = Vec::new();
450    let cached_excerpt_hints = state.cache.hints.get(&excerpt_id);
451
452    let mut excerpt_hints_to_persist = HashMap::default();
453    for new_hint in new_excerpt_hints {
454        let missing_from_cache = match cached_excerpt_hints {
455            Some(cached_excerpt_hints) => {
456                match cached_excerpt_hints.hints.binary_search_by(|probe| {
457                    probe.1.position.cmp(&new_hint.position, buffer_snapshot)
458                }) {
459                    Ok(ix) => {
460                        let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
461                        if cached_hint == &new_hint {
462                            excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
463                            false
464                        } else {
465                            true
466                        }
467                    }
468                    Err(_) => true,
469                }
470            }
471            None => true,
472        };
473        if missing_from_cache {
474            add_to_cache.push(new_hint);
475        }
476    }
477
478    let mut remove_from_visible = Vec::new();
479    let mut remove_from_cache = HashSet::default();
480    if invalidate_cache {
481        remove_from_visible.extend(
482            state
483                .visible_inlays
484                .iter()
485                .filter(|hint| hint.position.excerpt_id == excerpt_id)
486                .filter(|hint| {
487                    excerpt_range
488                        .start
489                        .cmp(&hint.position.text_anchor, buffer_snapshot)
490                        .is_le()
491                })
492                .filter(|hint| {
493                    excerpt_range
494                        .end
495                        .cmp(&hint.position.text_anchor, buffer_snapshot)
496                        .is_ge()
497                })
498                .map(|inlay_hint| inlay_hint.id)
499                .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
500        );
501        remove_from_cache.extend(
502            state
503                .cache
504                .hints
505                .values()
506                .flat_map(|excerpt_hints| excerpt_hints.hints.iter().map(|(id, _)| id))
507                .filter(|cached_inlay_id| !excerpt_hints_to_persist.contains_key(cached_inlay_id)),
508        );
509    }
510
511    if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
512        None
513    } else {
514        Some(ExcerptHintsUpdate {
515            cache_version: state.cache.version,
516            excerpt_id,
517            remove_from_visible,
518            remove_from_cache,
519            add_to_cache,
520        })
521    }
522}
523
524fn allowed_hint_types(
525    inlay_hint_settings: editor_settings::InlayHints,
526) -> HashSet<Option<InlayHintKind>> {
527    let mut new_allowed_hint_types = HashSet::default();
528    if inlay_hint_settings.show_type_hints {
529        new_allowed_hint_types.insert(Some(InlayHintKind::Type));
530    }
531    if inlay_hint_settings.show_parameter_hints {
532        new_allowed_hint_types.insert(Some(InlayHintKind::Parameter));
533    }
534    if inlay_hint_settings.show_other_hints {
535        new_allowed_hint_types.insert(None);
536    }
537    new_allowed_hint_types
538}
539
540fn hints_fetch_task(
541    query: ExcerptQuery,
542    cx: &mut ViewContext<'_, '_, Editor>,
543) -> Task<anyhow::Result<Option<Vec<InlayHint>>>> {
544    cx.spawn(|editor, mut cx| async move {
545        let 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, query.excerpt_range, cx)
555                        }))
556                    })
557            })
558            .ok()
559            .flatten();
560        Ok(match task {
561            Some(task) => Some(task.await.context("inlays for buffer task")?),
562            None => None,
563        })
564    })
565}
566
567fn visible_inlay_hints<'a, 'b: 'a, 'c, 'd: 'a>(
568    editor: &'a Editor,
569    cx: &'b ViewContext<'c, 'd, Editor>,
570) -> impl Iterator<Item = &'b Inlay> + 'a {
571    editor
572        .display_map
573        .read(cx)
574        .current_inlays()
575        .filter(|inlay| Some(inlay.id) != editor.copilot_state.suggestion.as_ref().map(|h| h.id))
576}