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}