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 hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
24 allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
25 version: usize,
26 enabled: bool,
27 // TODO kb track them by excerpt range
28 update_tasks: HashMap<ExcerptId, UpdateTask>,
29}
30
31#[derive(Debug)]
32pub struct CachedExcerptHints {
33 version: usize,
34 buffer_version: Global,
35 buffer_id: u64,
36 hints: Vec<(InlayId, InlayHint)>,
37}
38
39#[derive(Debug, Clone, Copy)]
40pub enum InvalidationStrategy {
41 RefreshRequested,
42 BufferEdited,
43 None,
44}
45
46#[derive(Debug, Default)]
47pub struct InlaySplice {
48 pub to_remove: Vec<InlayId>,
49 pub to_insert: Vec<Inlay>,
50}
51
52struct UpdateTask {
53 invalidate: InvalidationStrategy,
54 cache_version: usize,
55 task: RunningTask,
56 pending_refresh: Option<ExcerptQuery>,
57}
58
59struct RunningTask {
60 _task: Task<()>,
61 is_running_rx: smol::channel::Receiver<()>,
62}
63
64#[derive(Debug)]
65struct ExcerptHintsUpdate {
66 excerpt_id: ExcerptId,
67 remove_from_visible: Vec<InlayId>,
68 remove_from_cache: HashSet<InlayId>,
69 add_to_cache: HashSet<InlayHint>,
70}
71
72#[derive(Debug, Clone, Copy)]
73struct ExcerptQuery {
74 buffer_id: u64,
75 excerpt_id: ExcerptId,
76 dimensions: ExcerptDimensions,
77 cache_version: usize,
78 invalidate: InvalidationStrategy,
79}
80
81#[derive(Debug, Clone, Copy)]
82struct ExcerptDimensions {
83 excerpt_range_start: language::Anchor,
84 excerpt_range_end: language::Anchor,
85 excerpt_visible_range_start: language::Anchor,
86 excerpt_visible_range_end: language::Anchor,
87}
88
89struct HintFetchRanges {
90 visible_range: Range<language::Anchor>,
91 other_ranges: Vec<Range<language::Anchor>>,
92}
93
94impl InvalidationStrategy {
95 fn should_invalidate(&self) -> bool {
96 matches!(
97 self,
98 InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
99 )
100 }
101}
102
103impl ExcerptQuery {
104 // TODO kb query only visible + one visible below and above
105 fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges {
106 let visible_range =
107 self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end;
108 let mut other_ranges = Vec::new();
109 if self
110 .dimensions
111 .excerpt_range_start
112 .cmp(&visible_range.start, buffer)
113 .is_lt()
114 {
115 let mut end = visible_range.start;
116 end.offset -= 1;
117 other_ranges.push(self.dimensions.excerpt_range_start..end);
118 }
119 if self
120 .dimensions
121 .excerpt_range_end
122 .cmp(&visible_range.end, buffer)
123 .is_gt()
124 {
125 let mut start = visible_range.end;
126 start.offset += 1;
127 other_ranges.push(start..self.dimensions.excerpt_range_end);
128 }
129
130 HintFetchRanges {
131 visible_range,
132 other_ranges: other_ranges.into_iter().map(|range| range).collect(),
133 }
134 }
135}
136
137impl InlayHintCache {
138 pub fn new(inlay_hint_settings: InlayHintSettings) -> Self {
139 Self {
140 allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
141 enabled: inlay_hint_settings.enabled,
142 hints: HashMap::default(),
143 update_tasks: HashMap::default(),
144 version: 0,
145 }
146 }
147
148 pub fn update_settings(
149 &mut self,
150 multi_buffer: &ModelHandle<MultiBuffer>,
151 new_hint_settings: InlayHintSettings,
152 visible_hints: Vec<Inlay>,
153 cx: &mut ViewContext<Editor>,
154 ) -> ControlFlow<Option<InlaySplice>> {
155 let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
156 match (self.enabled, new_hint_settings.enabled) {
157 (false, false) => {
158 self.allowed_hint_kinds = new_allowed_hint_kinds;
159 ControlFlow::Break(None)
160 }
161 (true, true) => {
162 if new_allowed_hint_kinds == self.allowed_hint_kinds {
163 ControlFlow::Break(None)
164 } else {
165 let new_splice = self.new_allowed_hint_kinds_splice(
166 multi_buffer,
167 &visible_hints,
168 &new_allowed_hint_kinds,
169 cx,
170 );
171 if new_splice.is_some() {
172 self.version += 1;
173 self.allowed_hint_kinds = new_allowed_hint_kinds;
174 }
175 ControlFlow::Break(new_splice)
176 }
177 }
178 (true, false) => {
179 self.enabled = new_hint_settings.enabled;
180 self.allowed_hint_kinds = new_allowed_hint_kinds;
181 if self.hints.is_empty() {
182 ControlFlow::Break(None)
183 } else {
184 self.clear();
185 ControlFlow::Break(Some(InlaySplice {
186 to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
187 to_insert: Vec::new(),
188 }))
189 }
190 }
191 (false, true) => {
192 self.enabled = new_hint_settings.enabled;
193 self.allowed_hint_kinds = new_allowed_hint_kinds;
194 ControlFlow::Continue(())
195 }
196 }
197 }
198
199 pub fn spawn_hint_refresh(
200 &mut self,
201 mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
202 invalidate: InvalidationStrategy,
203 cx: &mut ViewContext<Editor>,
204 ) -> Option<InlaySplice> {
205 if !self.enabled {
206 return None;
207 }
208
209 let update_tasks = &mut self.update_tasks;
210 let mut invalidated_hints = Vec::new();
211 if invalidate.should_invalidate() {
212 let mut changed = false;
213 update_tasks.retain(|task_excerpt_id, _| {
214 let retain = excerpts_to_query.contains_key(task_excerpt_id);
215 changed |= !retain;
216 retain
217 });
218 self.hints.retain(|cached_excerpt, cached_hints| {
219 let retain = excerpts_to_query.contains_key(cached_excerpt);
220 changed |= !retain;
221 if !retain {
222 invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id));
223 }
224 retain
225 });
226 if changed {
227 self.version += 1;
228 }
229 }
230 if excerpts_to_query.is_empty() && invalidated_hints.is_empty() {
231 return None;
232 }
233
234 let cache_version = self.version;
235 excerpts_to_query.retain(|visible_excerpt_id, _| {
236 match update_tasks.entry(*visible_excerpt_id) {
237 hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) {
238 cmp::Ordering::Less => true,
239 cmp::Ordering::Equal => invalidate.should_invalidate(),
240 cmp::Ordering::Greater => false,
241 },
242 hash_map::Entry::Vacant(_) => true,
243 }
244 });
245
246 cx.spawn(|editor, mut cx| async move {
247 editor
248 .update(&mut cx, |editor, cx| {
249 spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx)
250 })
251 .ok();
252 })
253 .detach();
254
255 if invalidated_hints.is_empty() {
256 None
257 } else {
258 Some(InlaySplice {
259 to_remove: invalidated_hints,
260 to_insert: Vec::new(),
261 })
262 }
263 }
264
265 fn new_allowed_hint_kinds_splice(
266 &self,
267 multi_buffer: &ModelHandle<MultiBuffer>,
268 visible_hints: &[Inlay],
269 new_kinds: &HashSet<Option<InlayHintKind>>,
270 cx: &mut ViewContext<Editor>,
271 ) -> Option<InlaySplice> {
272 let old_kinds = &self.allowed_hint_kinds;
273 if new_kinds == old_kinds {
274 return None;
275 }
276
277 let mut to_remove = Vec::new();
278 let mut to_insert = Vec::new();
279 let mut shown_hints_to_remove = visible_hints.iter().fold(
280 HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
281 |mut current_hints, inlay| {
282 current_hints
283 .entry(inlay.position.excerpt_id)
284 .or_default()
285 .push((inlay.position, inlay.id));
286 current_hints
287 },
288 );
289
290 let multi_buffer = multi_buffer.read(cx);
291 let multi_buffer_snapshot = multi_buffer.snapshot(cx);
292
293 for (excerpt_id, excerpt_cached_hints) in &self.hints {
294 let shown_excerpt_hints_to_remove =
295 shown_hints_to_remove.entry(*excerpt_id).or_default();
296 let excerpt_cached_hints = excerpt_cached_hints.read();
297 let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable();
298 shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
299 let Some(buffer) = shown_anchor
300 .buffer_id
301 .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false };
302 let buffer_snapshot = buffer.read(cx).snapshot();
303 loop {
304 match excerpt_cache.peek() {
305 Some((cached_hint_id, cached_hint)) => {
306 if cached_hint_id == shown_hint_id {
307 excerpt_cache.next();
308 return !new_kinds.contains(&cached_hint.kind);
309 }
310
311 match cached_hint
312 .position
313 .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
314 {
315 cmp::Ordering::Less | cmp::Ordering::Equal => {
316 if !old_kinds.contains(&cached_hint.kind)
317 && new_kinds.contains(&cached_hint.kind)
318 {
319 to_insert.push(Inlay::hint(
320 cached_hint_id.id(),
321 multi_buffer_snapshot.anchor_in_excerpt(
322 *excerpt_id,
323 cached_hint.position,
324 ),
325 &cached_hint,
326 ));
327 }
328 excerpt_cache.next();
329 }
330 cmp::Ordering::Greater => return true,
331 }
332 }
333 None => return true,
334 }
335 }
336 });
337
338 for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
339 let cached_hint_kind = maybe_missed_cached_hint.kind;
340 if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
341 to_insert.push(Inlay::hint(
342 cached_hint_id.id(),
343 multi_buffer_snapshot
344 .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
345 &maybe_missed_cached_hint,
346 ));
347 }
348 }
349 }
350
351 to_remove.extend(
352 shown_hints_to_remove
353 .into_values()
354 .flatten()
355 .map(|(_, hint_id)| hint_id),
356 );
357 if to_remove.is_empty() && to_insert.is_empty() {
358 None
359 } else {
360 Some(InlaySplice {
361 to_remove,
362 to_insert,
363 })
364 }
365 }
366
367 fn clear(&mut self) {
368 self.version += 1;
369 self.update_tasks.clear();
370 self.hints.clear();
371 }
372
373 pub fn hints(&self) -> Vec<InlayHint> {
374 let mut hints = Vec::new();
375 for excerpt_hints in self.hints.values() {
376 let excerpt_hints = excerpt_hints.read();
377 hints.extend(excerpt_hints.hints.iter().map(|(_, hint)| hint).cloned());
378 }
379 hints
380 }
381
382 pub fn version(&self) -> usize {
383 self.version
384 }
385}
386
387fn spawn_new_update_tasks(
388 editor: &mut Editor,
389 excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
390 invalidate: InvalidationStrategy,
391 update_cache_version: usize,
392 cx: &mut ViewContext<'_, '_, Editor>,
393) {
394 let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
395 for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in
396 excerpts_to_query
397 {
398 if excerpt_visible_range.is_empty() {
399 continue;
400 }
401 let buffer = buffer_handle.read(cx);
402 let buffer_snapshot = buffer.snapshot();
403 if buffer_snapshot
404 .version()
405 .changed_since(&new_task_buffer_version)
406 {
407 continue;
408 }
409
410 let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
411 if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
412 let cached_excerpt_hints = cached_excerpt_hints.read();
413 let cached_buffer_version = &cached_excerpt_hints.buffer_version;
414 if cached_excerpt_hints.version > update_cache_version
415 || cached_buffer_version.changed_since(&new_task_buffer_version)
416 {
417 continue;
418 }
419 if !new_task_buffer_version.changed_since(&cached_buffer_version)
420 && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
421 {
422 continue;
423 }
424 };
425
426 let buffer_id = buffer.remote_id();
427 let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
428 let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
429
430 let (multi_buffer_snapshot, full_excerpt_range) =
431 editor.buffer.update(cx, |multi_buffer, cx| {
432 let multi_buffer_snapshot = multi_buffer.snapshot(cx);
433 (
434 multi_buffer_snapshot,
435 multi_buffer
436 .excerpts_for_buffer(&buffer_handle, cx)
437 .into_iter()
438 .find(|(id, _)| id == &excerpt_id)
439 .map(|(_, range)| range.context),
440 )
441 });
442
443 if let Some(full_excerpt_range) = full_excerpt_range {
444 let query = ExcerptQuery {
445 buffer_id,
446 excerpt_id,
447 dimensions: ExcerptDimensions {
448 excerpt_range_start: full_excerpt_range.start,
449 excerpt_range_end: full_excerpt_range.end,
450 excerpt_visible_range_start,
451 excerpt_visible_range_end,
452 },
453 cache_version: update_cache_version,
454 invalidate,
455 };
456
457 let new_update_task = |is_refresh_after_regular_task| {
458 new_update_task(
459 query,
460 multi_buffer_snapshot,
461 buffer_snapshot,
462 Arc::clone(&visible_hints),
463 cached_excerpt_hints,
464 is_refresh_after_regular_task,
465 cx,
466 )
467 };
468 // TODO kb need to add to update tasks + ensure RefreshRequested cleans other ranges
469 match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
470 hash_map::Entry::Occupied(mut o) => {
471 let update_task = o.get_mut();
472 match (update_task.invalidate, invalidate) {
473 (_, InvalidationStrategy::None) => {}
474 (
475 InvalidationStrategy::BufferEdited,
476 InvalidationStrategy::RefreshRequested,
477 ) if !update_task.task.is_running_rx.is_closed() => {
478 update_task.pending_refresh = Some(query);
479 }
480 _ => {
481 o.insert(UpdateTask {
482 invalidate,
483 cache_version: query.cache_version,
484 task: new_update_task(false),
485 pending_refresh: None,
486 });
487 }
488 }
489 }
490 hash_map::Entry::Vacant(v) => {
491 v.insert(UpdateTask {
492 invalidate,
493 cache_version: query.cache_version,
494 task: new_update_task(false),
495 pending_refresh: None,
496 });
497 }
498 }
499 }
500 }
501}
502
503fn new_update_task(
504 query: ExcerptQuery,
505 multi_buffer_snapshot: MultiBufferSnapshot,
506 buffer_snapshot: BufferSnapshot,
507 visible_hints: Arc<Vec<Inlay>>,
508 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
509 is_refresh_after_regular_task: bool,
510 cx: &mut ViewContext<'_, '_, Editor>,
511) -> RunningTask {
512 let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot);
513 let (is_running_tx, is_running_rx) = smol::channel::bounded(1);
514 let _task = cx.spawn(|editor, mut cx| async move {
515 let _is_running_tx = is_running_tx;
516 let create_update_task = |range| {
517 fetch_and_update_hints(
518 editor.clone(),
519 multi_buffer_snapshot.clone(),
520 buffer_snapshot.clone(),
521 Arc::clone(&visible_hints),
522 cached_excerpt_hints.as_ref().map(Arc::clone),
523 query,
524 range,
525 cx.clone(),
526 )
527 };
528
529 if is_refresh_after_regular_task {
530 let visible_range_has_updates =
531 match create_update_task(hints_fetch_ranges.visible_range).await {
532 Ok(updated) => updated,
533 Err(e) => {
534 error!("inlay hint visible range update task failed: {e:#}");
535 return;
536 }
537 };
538
539 if visible_range_has_updates {
540 let other_update_results = futures::future::join_all(
541 hints_fetch_ranges
542 .other_ranges
543 .into_iter()
544 .map(create_update_task),
545 )
546 .await;
547
548 for result in other_update_results {
549 if let Err(e) = result {
550 error!("inlay hint update task failed: {e:#}");
551 }
552 }
553 }
554 } else {
555 let task_update_results = futures::future::join_all(
556 std::iter::once(hints_fetch_ranges.visible_range)
557 .chain(hints_fetch_ranges.other_ranges.into_iter())
558 .map(create_update_task),
559 )
560 .await;
561
562 for result in task_update_results {
563 if let Err(e) = result {
564 error!("inlay hint update task failed: {e:#}");
565 }
566 }
567 }
568
569 editor
570 .update(&mut cx, |editor, cx| {
571 let pending_refresh_query = editor
572 .inlay_hint_cache
573 .update_tasks
574 .get_mut(&query.excerpt_id)
575 .and_then(|task| task.pending_refresh.take());
576
577 if let Some(pending_refresh_query) = pending_refresh_query {
578 let refresh_multi_buffer = editor.buffer().read(cx);
579 let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx);
580 let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx));
581 let refresh_cached_excerpt_hints = editor
582 .inlay_hint_cache
583 .hints
584 .get(&pending_refresh_query.excerpt_id)
585 .map(Arc::clone);
586 if let Some(buffer) =
587 refresh_multi_buffer.buffer(pending_refresh_query.buffer_id)
588 {
589 editor.inlay_hint_cache.update_tasks.insert(
590 pending_refresh_query.excerpt_id,
591 UpdateTask {
592 invalidate: InvalidationStrategy::RefreshRequested,
593 cache_version: editor.inlay_hint_cache.version,
594 task: new_update_task(
595 pending_refresh_query,
596 refresh_multi_buffer_snapshot,
597 buffer.read(cx).snapshot(),
598 refresh_visible_hints,
599 refresh_cached_excerpt_hints,
600 true,
601 cx,
602 ),
603 pending_refresh: None,
604 },
605 );
606 }
607 }
608 })
609 .ok();
610 });
611
612 RunningTask {
613 _task,
614 is_running_rx,
615 }
616}
617
618async fn fetch_and_update_hints(
619 editor: gpui::WeakViewHandle<Editor>,
620 multi_buffer_snapshot: MultiBufferSnapshot,
621 buffer_snapshot: BufferSnapshot,
622 visible_hints: Arc<Vec<Inlay>>,
623 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
624 query: ExcerptQuery,
625 fetch_range: Range<language::Anchor>,
626 mut cx: gpui::AsyncAppContext,
627) -> anyhow::Result<bool> {
628 let inlay_hints_fetch_task = editor
629 .update(&mut cx, |editor, cx| {
630 editor
631 .buffer()
632 .read(cx)
633 .buffer(query.buffer_id)
634 .and_then(|buffer| {
635 let project = editor.project.as_ref()?;
636 Some(project.update(cx, |project, cx| {
637 project.inlay_hints(buffer, fetch_range.clone(), cx)
638 }))
639 })
640 })
641 .ok()
642 .flatten();
643 let mut update_happened = false;
644 let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) };
645 let new_hints = inlay_hints_fetch_task
646 .await
647 .context("inlay hint fetch task")?;
648 let background_task_buffer_snapshot = buffer_snapshot.clone();
649 let backround_fetch_range = fetch_range.clone();
650 let new_update = cx
651 .background()
652 .spawn(async move {
653 calculate_hint_updates(
654 query,
655 backround_fetch_range,
656 new_hints,
657 &background_task_buffer_snapshot,
658 cached_excerpt_hints,
659 &visible_hints,
660 )
661 })
662 .await;
663
664 editor
665 .update(&mut cx, |editor, cx| {
666 if let Some(new_update) = new_update {
667 update_happened = !new_update.add_to_cache.is_empty()
668 || !new_update.remove_from_cache.is_empty()
669 || !new_update.remove_from_visible.is_empty();
670
671 let cached_excerpt_hints = editor
672 .inlay_hint_cache
673 .hints
674 .entry(new_update.excerpt_id)
675 .or_insert_with(|| {
676 Arc::new(RwLock::new(CachedExcerptHints {
677 version: query.cache_version,
678 buffer_version: buffer_snapshot.version().clone(),
679 buffer_id: query.buffer_id,
680 hints: Vec::new(),
681 }))
682 });
683 let mut cached_excerpt_hints = cached_excerpt_hints.write();
684 match query.cache_version.cmp(&cached_excerpt_hints.version) {
685 cmp::Ordering::Less => return,
686 cmp::Ordering::Greater | cmp::Ordering::Equal => {
687 cached_excerpt_hints.version = query.cache_version;
688 }
689 }
690 cached_excerpt_hints
691 .hints
692 .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
693 cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
694 editor.inlay_hint_cache.version += 1;
695
696 let mut splice = InlaySplice {
697 to_remove: new_update.remove_from_visible,
698 to_insert: Vec::new(),
699 };
700
701 for new_hint in new_update.add_to_cache {
702 let new_hint_position = multi_buffer_snapshot
703 .anchor_in_excerpt(query.excerpt_id, new_hint.position);
704 let new_inlay_id = post_inc(&mut editor.next_inlay_id);
705 if editor
706 .inlay_hint_cache
707 .allowed_hint_kinds
708 .contains(&new_hint.kind)
709 {
710 splice.to_insert.push(Inlay::hint(
711 new_inlay_id,
712 new_hint_position,
713 &new_hint,
714 ));
715 }
716
717 cached_excerpt_hints
718 .hints
719 .push((InlayId::Hint(new_inlay_id), new_hint));
720 }
721
722 cached_excerpt_hints
723 .hints
724 .sort_by(|(_, hint_a), (_, hint_b)| {
725 hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
726 });
727 drop(cached_excerpt_hints);
728
729 if query.invalidate.should_invalidate() {
730 let mut outdated_excerpt_caches = HashSet::default();
731 for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
732 let excerpt_hints = excerpt_hints.read();
733 if excerpt_hints.buffer_id == query.buffer_id
734 && excerpt_id != &query.excerpt_id
735 && buffer_snapshot
736 .version()
737 .changed_since(&excerpt_hints.buffer_version)
738 {
739 outdated_excerpt_caches.insert(*excerpt_id);
740 splice
741 .to_remove
742 .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
743 }
744 }
745 editor
746 .inlay_hint_cache
747 .hints
748 .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
749 }
750
751 let InlaySplice {
752 to_remove,
753 to_insert,
754 } = splice;
755 if !to_remove.is_empty() || !to_insert.is_empty() {
756 editor.splice_inlay_hints(to_remove, to_insert, cx)
757 }
758 }
759 })
760 .ok();
761
762 Ok(update_happened)
763}
764
765fn calculate_hint_updates(
766 query: ExcerptQuery,
767 fetch_range: Range<language::Anchor>,
768 new_excerpt_hints: Vec<InlayHint>,
769 buffer_snapshot: &BufferSnapshot,
770 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
771 visible_hints: &[Inlay],
772) -> Option<ExcerptHintsUpdate> {
773 let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
774 let mut excerpt_hints_to_persist = HashMap::default();
775 for new_hint in new_excerpt_hints {
776 if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
777 continue;
778 }
779 let missing_from_cache = match &cached_excerpt_hints {
780 Some(cached_excerpt_hints) => {
781 let cached_excerpt_hints = cached_excerpt_hints.read();
782 match cached_excerpt_hints.hints.binary_search_by(|probe| {
783 probe.1.position.cmp(&new_hint.position, buffer_snapshot)
784 }) {
785 Ok(ix) => {
786 let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
787 if cached_hint == &new_hint {
788 excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
789 false
790 } else {
791 true
792 }
793 }
794 Err(_) => true,
795 }
796 }
797 None => true,
798 };
799 if missing_from_cache {
800 add_to_cache.insert(new_hint);
801 }
802 }
803
804 let mut remove_from_visible = Vec::new();
805 let mut remove_from_cache = HashSet::default();
806 if query.invalidate.should_invalidate() {
807 remove_from_visible.extend(
808 visible_hints
809 .iter()
810 .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
811 .filter(|hint| {
812 contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
813 })
814 .filter(|hint| {
815 fetch_range
816 .start
817 .cmp(&hint.position.text_anchor, buffer_snapshot)
818 .is_le()
819 && fetch_range
820 .end
821 .cmp(&hint.position.text_anchor, buffer_snapshot)
822 .is_ge()
823 })
824 .map(|inlay_hint| inlay_hint.id)
825 .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
826 );
827
828 if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
829 let cached_excerpt_hints = cached_excerpt_hints.read();
830 remove_from_cache.extend(
831 cached_excerpt_hints
832 .hints
833 .iter()
834 .filter(|(cached_inlay_id, _)| {
835 !excerpt_hints_to_persist.contains_key(cached_inlay_id)
836 })
837 .filter(|(_, cached_hint)| {
838 fetch_range
839 .start
840 .cmp(&cached_hint.position, buffer_snapshot)
841 .is_le()
842 && fetch_range
843 .end
844 .cmp(&cached_hint.position, buffer_snapshot)
845 .is_ge()
846 })
847 .map(|(cached_inlay_id, _)| *cached_inlay_id),
848 );
849 }
850 }
851
852 if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
853 None
854 } else {
855 Some(ExcerptHintsUpdate {
856 excerpt_id: query.excerpt_id,
857 remove_from_visible,
858 remove_from_cache,
859 add_to_cache,
860 })
861 }
862}
863
864fn contains_position(
865 range: &Range<language::Anchor>,
866 position: language::Anchor,
867 buffer_snapshot: &BufferSnapshot,
868) -> bool {
869 range.start.cmp(&position, buffer_snapshot).is_le()
870 && range.end.cmp(&position, buffer_snapshot).is_ge()
871}
872
873#[cfg(test)]
874mod tests {
875 use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
876
877 use crate::{
878 scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
879 serde_json::json,
880 ExcerptRange,
881 };
882 use futures::StreamExt;
883 use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
884 use language::{
885 language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
886 };
887 use lsp::FakeLanguageServer;
888 use parking_lot::Mutex;
889 use project::{FakeFs, Project};
890 use settings::SettingsStore;
891 use text::Point;
892 use workspace::Workspace;
893
894 use crate::editor_tests::update_test_language_settings;
895
896 use super::*;
897
898 #[gpui::test]
899 async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
900 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
901 init_test(cx, |settings| {
902 settings.defaults.inlay_hints = Some(InlayHintSettings {
903 enabled: true,
904 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
905 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
906 show_other_hints: allowed_hint_kinds.contains(&None),
907 })
908 });
909
910 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
911 let lsp_request_count = Arc::new(AtomicU32::new(0));
912 fake_server
913 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
914 let task_lsp_request_count = Arc::clone(&lsp_request_count);
915 async move {
916 assert_eq!(
917 params.text_document.uri,
918 lsp::Url::from_file_path(file_with_hints).unwrap(),
919 );
920 let current_call_id =
921 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
922 let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
923 for _ in 0..2 {
924 let mut i = current_call_id;
925 loop {
926 new_hints.push(lsp::InlayHint {
927 position: lsp::Position::new(0, i),
928 label: lsp::InlayHintLabel::String(i.to_string()),
929 kind: None,
930 text_edits: None,
931 tooltip: None,
932 padding_left: None,
933 padding_right: None,
934 data: None,
935 });
936 if i == 0 {
937 break;
938 }
939 i -= 1;
940 }
941 }
942
943 Ok(Some(new_hints))
944 }
945 })
946 .next()
947 .await;
948 cx.foreground().run_until_parked();
949
950 let mut edits_made = 1;
951 editor.update(cx, |editor, cx| {
952 let expected_layers = vec!["0".to_string()];
953 assert_eq!(
954 expected_layers,
955 cached_hint_labels(editor),
956 "Should get its first hints when opening the editor"
957 );
958 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
959 let inlay_cache = editor.inlay_hint_cache();
960 assert_eq!(
961 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
962 "Cache should use editor settings to get the allowed hint kinds"
963 );
964 assert_eq!(
965 inlay_cache.version, edits_made,
966 "The editor update the cache version after every cache/view change"
967 );
968 });
969
970 editor.update(cx, |editor, cx| {
971 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
972 editor.handle_input("some change", cx);
973 edits_made += 1;
974 });
975 cx.foreground().run_until_parked();
976 editor.update(cx, |editor, cx| {
977 let expected_layers = vec!["0".to_string(), "1".to_string()];
978 assert_eq!(
979 expected_layers,
980 cached_hint_labels(editor),
981 "Should get new hints after an edit"
982 );
983 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
984 let inlay_cache = editor.inlay_hint_cache();
985 assert_eq!(
986 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
987 "Cache should use editor settings to get the allowed hint kinds"
988 );
989 assert_eq!(
990 inlay_cache.version, edits_made,
991 "The editor update the cache version after every cache/view change"
992 );
993 });
994
995 fake_server
996 .request::<lsp::request::InlayHintRefreshRequest>(())
997 .await
998 .expect("inlay refresh request failed");
999 edits_made += 1;
1000 cx.foreground().run_until_parked();
1001 editor.update(cx, |editor, cx| {
1002 let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
1003 assert_eq!(
1004 expected_layers,
1005 cached_hint_labels(editor),
1006 "Should get new hints after hint refresh/ request"
1007 );
1008 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1009 let inlay_cache = editor.inlay_hint_cache();
1010 assert_eq!(
1011 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1012 "Cache should use editor settings to get the allowed hint kinds"
1013 );
1014 assert_eq!(
1015 inlay_cache.version, edits_made,
1016 "The editor update the cache version after every cache/view change"
1017 );
1018 });
1019 }
1020
1021 #[gpui::test]
1022 async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
1023 init_test(cx, |settings| {
1024 settings.defaults.inlay_hints = Some(InlayHintSettings {
1025 enabled: true,
1026 show_type_hints: true,
1027 show_parameter_hints: true,
1028 show_other_hints: true,
1029 })
1030 });
1031
1032 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1033 let lsp_request_count = Arc::new(AtomicU32::new(0));
1034 fake_server
1035 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1036 let task_lsp_request_count = Arc::clone(&lsp_request_count);
1037 async move {
1038 assert_eq!(
1039 params.text_document.uri,
1040 lsp::Url::from_file_path(file_with_hints).unwrap(),
1041 );
1042 let current_call_id =
1043 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1044 Ok(Some(vec![lsp::InlayHint {
1045 position: lsp::Position::new(0, current_call_id),
1046 label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1047 kind: None,
1048 text_edits: None,
1049 tooltip: None,
1050 padding_left: None,
1051 padding_right: None,
1052 data: None,
1053 }]))
1054 }
1055 })
1056 .next()
1057 .await;
1058 cx.foreground().run_until_parked();
1059
1060 let mut edits_made = 1;
1061 editor.update(cx, |editor, cx| {
1062 let expected_layers = vec!["0".to_string()];
1063 assert_eq!(
1064 expected_layers,
1065 cached_hint_labels(editor),
1066 "Should get its first hints when opening the editor"
1067 );
1068 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1069 assert_eq!(
1070 editor.inlay_hint_cache().version,
1071 edits_made,
1072 "The editor update the cache version after every cache/view change"
1073 );
1074 });
1075
1076 let progress_token = "test_progress_token";
1077 fake_server
1078 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
1079 token: lsp::ProgressToken::String(progress_token.to_string()),
1080 })
1081 .await
1082 .expect("work done progress create request failed");
1083 cx.foreground().run_until_parked();
1084 fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1085 token: lsp::ProgressToken::String(progress_token.to_string()),
1086 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1087 lsp::WorkDoneProgressBegin::default(),
1088 )),
1089 });
1090 cx.foreground().run_until_parked();
1091
1092 editor.update(cx, |editor, cx| {
1093 let expected_layers = vec!["0".to_string()];
1094 assert_eq!(
1095 expected_layers,
1096 cached_hint_labels(editor),
1097 "Should not update hints while the work task is running"
1098 );
1099 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1100 assert_eq!(
1101 editor.inlay_hint_cache().version,
1102 edits_made,
1103 "Should not update the cache while the work task is running"
1104 );
1105 });
1106
1107 fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1108 token: lsp::ProgressToken::String(progress_token.to_string()),
1109 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1110 lsp::WorkDoneProgressEnd::default(),
1111 )),
1112 });
1113 cx.foreground().run_until_parked();
1114
1115 edits_made += 1;
1116 editor.update(cx, |editor, cx| {
1117 let expected_layers = vec!["1".to_string()];
1118 assert_eq!(
1119 expected_layers,
1120 cached_hint_labels(editor),
1121 "New hints should be queried after the work task is done"
1122 );
1123 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1124 assert_eq!(
1125 editor.inlay_hint_cache().version,
1126 edits_made,
1127 "Cache version should udpate once after the work task is done"
1128 );
1129 });
1130 }
1131
1132 #[gpui::test]
1133 async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1134 init_test(cx, |settings| {
1135 settings.defaults.inlay_hints = Some(InlayHintSettings {
1136 enabled: true,
1137 show_type_hints: true,
1138 show_parameter_hints: true,
1139 show_other_hints: true,
1140 })
1141 });
1142
1143 let fs = FakeFs::new(cx.background());
1144 fs.insert_tree(
1145 "/a",
1146 json!({
1147 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1148 "other.md": "Test md file with some text",
1149 }),
1150 )
1151 .await;
1152 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1153 let workspace = cx
1154 .add_window(|cx| Workspace::test_new(project.clone(), cx))
1155 .root(cx);
1156 let worktree_id = workspace.update(cx, |workspace, cx| {
1157 workspace.project().read_with(cx, |project, cx| {
1158 project.worktrees(cx).next().unwrap().read(cx).id()
1159 })
1160 });
1161
1162 let mut rs_fake_servers = None;
1163 let mut md_fake_servers = None;
1164 for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1165 let mut language = Language::new(
1166 LanguageConfig {
1167 name: name.into(),
1168 path_suffixes: vec![path_suffix.to_string()],
1169 ..Default::default()
1170 },
1171 Some(tree_sitter_rust::language()),
1172 );
1173 let fake_servers = language
1174 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1175 name,
1176 capabilities: lsp::ServerCapabilities {
1177 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1178 ..Default::default()
1179 },
1180 ..Default::default()
1181 }))
1182 .await;
1183 match name {
1184 "Rust" => rs_fake_servers = Some(fake_servers),
1185 "Markdown" => md_fake_servers = Some(fake_servers),
1186 _ => unreachable!(),
1187 }
1188 project.update(cx, |project, _| {
1189 project.languages().add(Arc::new(language));
1190 });
1191 }
1192
1193 let _rs_buffer = project
1194 .update(cx, |project, cx| {
1195 project.open_local_buffer("/a/main.rs", cx)
1196 })
1197 .await
1198 .unwrap();
1199 cx.foreground().run_until_parked();
1200 cx.foreground().start_waiting();
1201 let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1202 let rs_editor = workspace
1203 .update(cx, |workspace, cx| {
1204 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1205 })
1206 .await
1207 .unwrap()
1208 .downcast::<Editor>()
1209 .unwrap();
1210 let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1211 rs_fake_server
1212 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1213 let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
1214 async move {
1215 assert_eq!(
1216 params.text_document.uri,
1217 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1218 );
1219 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1220 Ok(Some(vec![lsp::InlayHint {
1221 position: lsp::Position::new(0, i),
1222 label: lsp::InlayHintLabel::String(i.to_string()),
1223 kind: None,
1224 text_edits: None,
1225 tooltip: None,
1226 padding_left: None,
1227 padding_right: None,
1228 data: None,
1229 }]))
1230 }
1231 })
1232 .next()
1233 .await;
1234 cx.foreground().run_until_parked();
1235 rs_editor.update(cx, |editor, cx| {
1236 let expected_layers = vec!["0".to_string()];
1237 assert_eq!(
1238 expected_layers,
1239 cached_hint_labels(editor),
1240 "Should get its first hints when opening the editor"
1241 );
1242 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1243 assert_eq!(
1244 editor.inlay_hint_cache().version,
1245 1,
1246 "Rust editor update the cache version after every cache/view change"
1247 );
1248 });
1249
1250 cx.foreground().run_until_parked();
1251 let _md_buffer = project
1252 .update(cx, |project, cx| {
1253 project.open_local_buffer("/a/other.md", cx)
1254 })
1255 .await
1256 .unwrap();
1257 cx.foreground().run_until_parked();
1258 cx.foreground().start_waiting();
1259 let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1260 let md_editor = workspace
1261 .update(cx, |workspace, cx| {
1262 workspace.open_path((worktree_id, "other.md"), None, true, cx)
1263 })
1264 .await
1265 .unwrap()
1266 .downcast::<Editor>()
1267 .unwrap();
1268 let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1269 md_fake_server
1270 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1271 let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
1272 async move {
1273 assert_eq!(
1274 params.text_document.uri,
1275 lsp::Url::from_file_path("/a/other.md").unwrap(),
1276 );
1277 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1278 Ok(Some(vec![lsp::InlayHint {
1279 position: lsp::Position::new(0, i),
1280 label: lsp::InlayHintLabel::String(i.to_string()),
1281 kind: None,
1282 text_edits: None,
1283 tooltip: None,
1284 padding_left: None,
1285 padding_right: None,
1286 data: None,
1287 }]))
1288 }
1289 })
1290 .next()
1291 .await;
1292 cx.foreground().run_until_parked();
1293 md_editor.update(cx, |editor, cx| {
1294 let expected_layers = vec!["0".to_string()];
1295 assert_eq!(
1296 expected_layers,
1297 cached_hint_labels(editor),
1298 "Markdown editor should have a separate verison, repeating Rust editor rules"
1299 );
1300 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1301 assert_eq!(editor.inlay_hint_cache().version, 1);
1302 });
1303
1304 rs_editor.update(cx, |editor, cx| {
1305 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1306 editor.handle_input("some rs change", cx);
1307 });
1308 cx.foreground().run_until_parked();
1309 rs_editor.update(cx, |editor, cx| {
1310 let expected_layers = vec!["1".to_string()];
1311 assert_eq!(
1312 expected_layers,
1313 cached_hint_labels(editor),
1314 "Rust inlay cache should change after the edit"
1315 );
1316 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1317 assert_eq!(
1318 editor.inlay_hint_cache().version,
1319 2,
1320 "Every time hint cache changes, cache version should be incremented"
1321 );
1322 });
1323 md_editor.update(cx, |editor, cx| {
1324 let expected_layers = vec!["0".to_string()];
1325 assert_eq!(
1326 expected_layers,
1327 cached_hint_labels(editor),
1328 "Markdown editor should not be affected by Rust editor changes"
1329 );
1330 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1331 assert_eq!(editor.inlay_hint_cache().version, 1);
1332 });
1333
1334 md_editor.update(cx, |editor, cx| {
1335 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1336 editor.handle_input("some md change", cx);
1337 });
1338 cx.foreground().run_until_parked();
1339 md_editor.update(cx, |editor, cx| {
1340 let expected_layers = vec!["1".to_string()];
1341 assert_eq!(
1342 expected_layers,
1343 cached_hint_labels(editor),
1344 "Rust editor should not be affected by Markdown editor changes"
1345 );
1346 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1347 assert_eq!(editor.inlay_hint_cache().version, 2);
1348 });
1349 rs_editor.update(cx, |editor, cx| {
1350 let expected_layers = vec!["1".to_string()];
1351 assert_eq!(
1352 expected_layers,
1353 cached_hint_labels(editor),
1354 "Markdown editor should also change independently"
1355 );
1356 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1357 assert_eq!(editor.inlay_hint_cache().version, 2);
1358 });
1359 }
1360
1361 #[gpui::test]
1362 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1363 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1364 init_test(cx, |settings| {
1365 settings.defaults.inlay_hints = Some(InlayHintSettings {
1366 enabled: true,
1367 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1368 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1369 show_other_hints: allowed_hint_kinds.contains(&None),
1370 })
1371 });
1372
1373 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1374 let lsp_request_count = Arc::new(AtomicU32::new(0));
1375 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1376 fake_server
1377 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1378 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1379 async move {
1380 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1381 assert_eq!(
1382 params.text_document.uri,
1383 lsp::Url::from_file_path(file_with_hints).unwrap(),
1384 );
1385 Ok(Some(vec![
1386 lsp::InlayHint {
1387 position: lsp::Position::new(0, 1),
1388 label: lsp::InlayHintLabel::String("type hint".to_string()),
1389 kind: Some(lsp::InlayHintKind::TYPE),
1390 text_edits: None,
1391 tooltip: None,
1392 padding_left: None,
1393 padding_right: None,
1394 data: None,
1395 },
1396 lsp::InlayHint {
1397 position: lsp::Position::new(0, 2),
1398 label: lsp::InlayHintLabel::String("parameter hint".to_string()),
1399 kind: Some(lsp::InlayHintKind::PARAMETER),
1400 text_edits: None,
1401 tooltip: None,
1402 padding_left: None,
1403 padding_right: None,
1404 data: None,
1405 },
1406 lsp::InlayHint {
1407 position: lsp::Position::new(0, 3),
1408 label: lsp::InlayHintLabel::String("other hint".to_string()),
1409 kind: None,
1410 text_edits: None,
1411 tooltip: None,
1412 padding_left: None,
1413 padding_right: None,
1414 data: None,
1415 },
1416 ]))
1417 }
1418 })
1419 .next()
1420 .await;
1421 cx.foreground().run_until_parked();
1422
1423 let mut edits_made = 1;
1424 editor.update(cx, |editor, cx| {
1425 assert_eq!(
1426 lsp_request_count.load(Ordering::Relaxed),
1427 1,
1428 "Should query new hints once"
1429 );
1430 assert_eq!(
1431 vec![
1432 "other hint".to_string(),
1433 "parameter hint".to_string(),
1434 "type hint".to_string(),
1435 ],
1436 cached_hint_labels(editor),
1437 "Should get its first hints when opening the editor"
1438 );
1439 assert_eq!(
1440 vec!["other hint".to_string(), "type hint".to_string()],
1441 visible_hint_labels(editor, cx)
1442 );
1443 let inlay_cache = editor.inlay_hint_cache();
1444 assert_eq!(
1445 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1446 "Cache should use editor settings to get the allowed hint kinds"
1447 );
1448 assert_eq!(
1449 inlay_cache.version, edits_made,
1450 "The editor update the cache version after every cache/view change"
1451 );
1452 });
1453
1454 fake_server
1455 .request::<lsp::request::InlayHintRefreshRequest>(())
1456 .await
1457 .expect("inlay refresh request failed");
1458 cx.foreground().run_until_parked();
1459 editor.update(cx, |editor, cx| {
1460 assert_eq!(
1461 lsp_request_count.load(Ordering::Relaxed),
1462 2,
1463 "Should load new hints twice"
1464 );
1465 assert_eq!(
1466 vec![
1467 "other hint".to_string(),
1468 "parameter hint".to_string(),
1469 "type hint".to_string(),
1470 ],
1471 cached_hint_labels(editor),
1472 "Cached hints should not change due to allowed hint kinds settings update"
1473 );
1474 assert_eq!(
1475 vec!["other hint".to_string(), "type hint".to_string()],
1476 visible_hint_labels(editor, cx)
1477 );
1478 assert_eq!(
1479 editor.inlay_hint_cache().version,
1480 edits_made,
1481 "Should not update cache version due to new loaded hints being the same"
1482 );
1483 });
1484
1485 for (new_allowed_hint_kinds, expected_visible_hints) in [
1486 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1487 (
1488 HashSet::from_iter([Some(InlayHintKind::Type)]),
1489 vec!["type hint".to_string()],
1490 ),
1491 (
1492 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1493 vec!["parameter hint".to_string()],
1494 ),
1495 (
1496 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1497 vec!["other hint".to_string(), "type hint".to_string()],
1498 ),
1499 (
1500 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1501 vec!["other hint".to_string(), "parameter hint".to_string()],
1502 ),
1503 (
1504 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1505 vec!["parameter hint".to_string(), "type hint".to_string()],
1506 ),
1507 (
1508 HashSet::from_iter([
1509 None,
1510 Some(InlayHintKind::Type),
1511 Some(InlayHintKind::Parameter),
1512 ]),
1513 vec![
1514 "other hint".to_string(),
1515 "parameter hint".to_string(),
1516 "type hint".to_string(),
1517 ],
1518 ),
1519 ] {
1520 edits_made += 1;
1521 update_test_language_settings(cx, |settings| {
1522 settings.defaults.inlay_hints = Some(InlayHintSettings {
1523 enabled: true,
1524 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1525 show_parameter_hints: new_allowed_hint_kinds
1526 .contains(&Some(InlayHintKind::Parameter)),
1527 show_other_hints: new_allowed_hint_kinds.contains(&None),
1528 })
1529 });
1530 cx.foreground().run_until_parked();
1531 editor.update(cx, |editor, cx| {
1532 assert_eq!(
1533 lsp_request_count.load(Ordering::Relaxed),
1534 2,
1535 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1536 );
1537 assert_eq!(
1538 vec![
1539 "other hint".to_string(),
1540 "parameter hint".to_string(),
1541 "type hint".to_string(),
1542 ],
1543 cached_hint_labels(editor),
1544 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1545 );
1546 assert_eq!(
1547 expected_visible_hints,
1548 visible_hint_labels(editor, cx),
1549 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1550 );
1551 let inlay_cache = editor.inlay_hint_cache();
1552 assert_eq!(
1553 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1554 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1555 );
1556 assert_eq!(
1557 inlay_cache.version, edits_made,
1558 "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
1559 );
1560 });
1561 }
1562
1563 edits_made += 1;
1564 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1565 update_test_language_settings(cx, |settings| {
1566 settings.defaults.inlay_hints = Some(InlayHintSettings {
1567 enabled: false,
1568 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1569 show_parameter_hints: another_allowed_hint_kinds
1570 .contains(&Some(InlayHintKind::Parameter)),
1571 show_other_hints: another_allowed_hint_kinds.contains(&None),
1572 })
1573 });
1574 cx.foreground().run_until_parked();
1575 editor.update(cx, |editor, cx| {
1576 assert_eq!(
1577 lsp_request_count.load(Ordering::Relaxed),
1578 2,
1579 "Should not load new hints when hints got disabled"
1580 );
1581 assert!(
1582 cached_hint_labels(editor).is_empty(),
1583 "Should clear the cache when hints got disabled"
1584 );
1585 assert!(
1586 visible_hint_labels(editor, cx).is_empty(),
1587 "Should clear visible hints when hints got disabled"
1588 );
1589 let inlay_cache = editor.inlay_hint_cache();
1590 assert_eq!(
1591 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1592 "Should update its allowed hint kinds even when hints got disabled"
1593 );
1594 assert_eq!(
1595 inlay_cache.version, edits_made,
1596 "The editor should update the cache version after hints got disabled"
1597 );
1598 });
1599
1600 fake_server
1601 .request::<lsp::request::InlayHintRefreshRequest>(())
1602 .await
1603 .expect("inlay refresh request failed");
1604 cx.foreground().run_until_parked();
1605 editor.update(cx, |editor, cx| {
1606 assert_eq!(
1607 lsp_request_count.load(Ordering::Relaxed),
1608 2,
1609 "Should not load new hints when they got disabled"
1610 );
1611 assert!(cached_hint_labels(editor).is_empty());
1612 assert!(visible_hint_labels(editor, cx).is_empty());
1613 assert_eq!(
1614 editor.inlay_hint_cache().version, edits_made,
1615 "The editor should not update the cache version after /refresh query without updates"
1616 );
1617 });
1618
1619 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1620 edits_made += 1;
1621 update_test_language_settings(cx, |settings| {
1622 settings.defaults.inlay_hints = Some(InlayHintSettings {
1623 enabled: true,
1624 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1625 show_parameter_hints: final_allowed_hint_kinds
1626 .contains(&Some(InlayHintKind::Parameter)),
1627 show_other_hints: final_allowed_hint_kinds.contains(&None),
1628 })
1629 });
1630 cx.foreground().run_until_parked();
1631 editor.update(cx, |editor, cx| {
1632 assert_eq!(
1633 lsp_request_count.load(Ordering::Relaxed),
1634 3,
1635 "Should query for new hints when they got reenabled"
1636 );
1637 assert_eq!(
1638 vec![
1639 "other hint".to_string(),
1640 "parameter hint".to_string(),
1641 "type hint".to_string(),
1642 ],
1643 cached_hint_labels(editor),
1644 "Should get its cached hints fully repopulated after the hints got reenabled"
1645 );
1646 assert_eq!(
1647 vec!["parameter hint".to_string()],
1648 visible_hint_labels(editor, cx),
1649 "Should get its visible hints repopulated and filtered after the h"
1650 );
1651 let inlay_cache = editor.inlay_hint_cache();
1652 assert_eq!(
1653 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
1654 "Cache should update editor settings when hints got reenabled"
1655 );
1656 assert_eq!(
1657 inlay_cache.version, edits_made,
1658 "Cache should update its version after hints got reenabled"
1659 );
1660 });
1661
1662 fake_server
1663 .request::<lsp::request::InlayHintRefreshRequest>(())
1664 .await
1665 .expect("inlay refresh request failed");
1666 cx.foreground().run_until_parked();
1667 editor.update(cx, |editor, cx| {
1668 assert_eq!(
1669 lsp_request_count.load(Ordering::Relaxed),
1670 4,
1671 "Should query for new hints again"
1672 );
1673 assert_eq!(
1674 vec![
1675 "other hint".to_string(),
1676 "parameter hint".to_string(),
1677 "type hint".to_string(),
1678 ],
1679 cached_hint_labels(editor),
1680 );
1681 assert_eq!(
1682 vec!["parameter hint".to_string()],
1683 visible_hint_labels(editor, cx),
1684 );
1685 assert_eq!(editor.inlay_hint_cache().version, edits_made);
1686 });
1687 }
1688
1689 #[gpui::test]
1690 async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
1691 init_test(cx, |settings| {
1692 settings.defaults.inlay_hints = Some(InlayHintSettings {
1693 enabled: true,
1694 show_type_hints: true,
1695 show_parameter_hints: true,
1696 show_other_hints: true,
1697 })
1698 });
1699
1700 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1701 let fake_server = Arc::new(fake_server);
1702 let lsp_request_count = Arc::new(AtomicU32::new(0));
1703 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1704 fake_server
1705 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1706 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1707 async move {
1708 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1709 assert_eq!(
1710 params.text_document.uri,
1711 lsp::Url::from_file_path(file_with_hints).unwrap(),
1712 );
1713 Ok(Some(vec![lsp::InlayHint {
1714 position: lsp::Position::new(0, i),
1715 label: lsp::InlayHintLabel::String(i.to_string()),
1716 kind: None,
1717 text_edits: None,
1718 tooltip: None,
1719 padding_left: None,
1720 padding_right: None,
1721 data: None,
1722 }]))
1723 }
1724 })
1725 .next()
1726 .await;
1727
1728 let mut expected_changes = Vec::new();
1729 for change_after_opening in [
1730 "initial change #1",
1731 "initial change #2",
1732 "initial change #3",
1733 ] {
1734 editor.update(cx, |editor, cx| {
1735 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1736 editor.handle_input(change_after_opening, cx);
1737 });
1738 expected_changes.push(change_after_opening);
1739 }
1740
1741 cx.foreground().run_until_parked();
1742
1743 editor.update(cx, |editor, cx| {
1744 let current_text = editor.text(cx);
1745 for change in &expected_changes {
1746 assert!(
1747 current_text.contains(change),
1748 "Should apply all changes made"
1749 );
1750 }
1751 assert_eq!(
1752 lsp_request_count.load(Ordering::Relaxed),
1753 2,
1754 "Should query new hints twice: for editor init and for the last edit that interrupted all others"
1755 );
1756 let expected_hints = vec!["2".to_string()];
1757 assert_eq!(
1758 expected_hints,
1759 cached_hint_labels(editor),
1760 "Should get hints from the last edit landed only"
1761 );
1762 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1763 assert_eq!(
1764 editor.inlay_hint_cache().version, 1,
1765 "Only one update should be registered in the cache after all cancellations"
1766 );
1767 });
1768
1769 let mut edits = Vec::new();
1770 for async_later_change in [
1771 "another change #1",
1772 "another change #2",
1773 "another change #3",
1774 ] {
1775 expected_changes.push(async_later_change);
1776 let task_editor = editor.clone();
1777 let mut task_cx = cx.clone();
1778 edits.push(cx.foreground().spawn(async move {
1779 task_editor.update(&mut task_cx, |editor, cx| {
1780 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1781 editor.handle_input(async_later_change, cx);
1782 });
1783 }));
1784 }
1785 let _ = futures::future::join_all(edits).await;
1786 cx.foreground().run_until_parked();
1787
1788 editor.update(cx, |editor, cx| {
1789 let current_text = editor.text(cx);
1790 for change in &expected_changes {
1791 assert!(
1792 current_text.contains(change),
1793 "Should apply all changes made"
1794 );
1795 }
1796 assert_eq!(
1797 lsp_request_count.load(Ordering::SeqCst),
1798 3,
1799 "Should query new hints one more time, for the last edit only"
1800 );
1801 let expected_hints = vec!["3".to_string()];
1802 assert_eq!(
1803 expected_hints,
1804 cached_hint_labels(editor),
1805 "Should get hints from the last edit landed only"
1806 );
1807 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1808 assert_eq!(
1809 editor.inlay_hint_cache().version,
1810 2,
1811 "Should update the cache version once more, for the new change"
1812 );
1813 });
1814 }
1815
1816 #[gpui::test]
1817 async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
1818 init_test(cx, |settings| {
1819 settings.defaults.inlay_hints = Some(InlayHintSettings {
1820 enabled: true,
1821 show_type_hints: true,
1822 show_parameter_hints: true,
1823 show_other_hints: true,
1824 })
1825 });
1826
1827 let mut language = Language::new(
1828 LanguageConfig {
1829 name: "Rust".into(),
1830 path_suffixes: vec!["rs".to_string()],
1831 ..Default::default()
1832 },
1833 Some(tree_sitter_rust::language()),
1834 );
1835 let mut fake_servers = language
1836 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1837 capabilities: lsp::ServerCapabilities {
1838 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1839 ..Default::default()
1840 },
1841 ..Default::default()
1842 }))
1843 .await;
1844 let fs = FakeFs::new(cx.background());
1845 fs.insert_tree(
1846 "/a",
1847 json!({
1848 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
1849 "other.rs": "// Test file",
1850 }),
1851 )
1852 .await;
1853 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1854 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
1855 let workspace = cx
1856 .add_window(|cx| Workspace::test_new(project.clone(), cx))
1857 .root(cx);
1858 let worktree_id = workspace.update(cx, |workspace, cx| {
1859 workspace.project().read_with(cx, |project, cx| {
1860 project.worktrees(cx).next().unwrap().read(cx).id()
1861 })
1862 });
1863
1864 let _buffer = project
1865 .update(cx, |project, cx| {
1866 project.open_local_buffer("/a/main.rs", cx)
1867 })
1868 .await
1869 .unwrap();
1870 cx.foreground().run_until_parked();
1871 cx.foreground().start_waiting();
1872 let fake_server = fake_servers.next().await.unwrap();
1873 let editor = workspace
1874 .update(cx, |workspace, cx| {
1875 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1876 })
1877 .await
1878 .unwrap()
1879 .downcast::<Editor>()
1880 .unwrap();
1881 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
1882 let lsp_request_count = Arc::new(AtomicU32::new(0));
1883 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
1884 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
1885 fake_server
1886 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1887 let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
1888 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
1889 async move {
1890 assert_eq!(
1891 params.text_document.uri,
1892 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1893 );
1894
1895 task_lsp_request_ranges.lock().push(params.range);
1896 let query_start = params.range.start;
1897 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1898 Ok(Some(vec![lsp::InlayHint {
1899 position: query_start,
1900 label: lsp::InlayHintLabel::String(i.to_string()),
1901 kind: None,
1902 text_edits: None,
1903 tooltip: None,
1904 padding_left: None,
1905 padding_right: None,
1906 data: None,
1907 }]))
1908 }
1909 })
1910 .next()
1911 .await;
1912 cx.foreground().run_until_parked();
1913 editor.update(cx, |editor, cx| {
1914 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1915 ranges.sort_by_key(|range| range.start);
1916 assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
1917 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1918 assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
1919 assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
1920
1921 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
1922 "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
1923 let expected_layers = vec!["1".to_string(), "2".to_string()];
1924 assert_eq!(
1925 expected_layers,
1926 cached_hint_labels(editor),
1927 "Should have hints from both LSP requests made for a big file"
1928 );
1929 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1930 assert_eq!(
1931 editor.inlay_hint_cache().version, 2,
1932 "Both LSP queries should've bumped the cache version"
1933 );
1934 });
1935
1936 editor.update(cx, |editor, cx| {
1937 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1938 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1939 editor.change_selections(None, cx, |s| s.select_ranges([600..600]));
1940 editor.handle_input("++++more text++++", cx);
1941 });
1942
1943 cx.foreground().run_until_parked();
1944 editor.update(cx, |editor, cx| {
1945 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1946 ranges.sort_by_key(|range| range.start);
1947 assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints");
1948 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1949 assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end");
1950 assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning");
1951 assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning");
1952 assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line");
1953 assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent");
1954
1955 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5,
1956 "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints");
1957 let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()];
1958 assert_eq!(expected_layers, cached_hint_labels(editor),
1959 "Should have hints from the new LSP response after edit");
1960 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1961 assert_eq!(editor.inlay_hint_cache().version, 5, "Should update the cache for every LSP response with hints added");
1962 });
1963 }
1964
1965 #[gpui::test]
1966 async fn test_multiple_excerpts_large_multibuffer(
1967 deterministic: Arc<Deterministic>,
1968 cx: &mut gpui::TestAppContext,
1969 ) {
1970 init_test(cx, |settings| {
1971 settings.defaults.inlay_hints = Some(InlayHintSettings {
1972 enabled: true,
1973 show_type_hints: true,
1974 show_parameter_hints: true,
1975 show_other_hints: true,
1976 })
1977 });
1978
1979 let mut language = Language::new(
1980 LanguageConfig {
1981 name: "Rust".into(),
1982 path_suffixes: vec!["rs".to_string()],
1983 ..Default::default()
1984 },
1985 Some(tree_sitter_rust::language()),
1986 );
1987 let mut fake_servers = language
1988 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1989 capabilities: lsp::ServerCapabilities {
1990 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1991 ..Default::default()
1992 },
1993 ..Default::default()
1994 }))
1995 .await;
1996 let language = Arc::new(language);
1997 let fs = FakeFs::new(cx.background());
1998 fs.insert_tree(
1999 "/a",
2000 json!({
2001 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2002 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2003 }),
2004 )
2005 .await;
2006 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2007 project.update(cx, |project, _| {
2008 project.languages().add(Arc::clone(&language))
2009 });
2010 let workspace = cx
2011 .add_window(|cx| Workspace::test_new(project.clone(), cx))
2012 .root(cx);
2013 let worktree_id = workspace.update(cx, |workspace, cx| {
2014 workspace.project().read_with(cx, |project, cx| {
2015 project.worktrees(cx).next().unwrap().read(cx).id()
2016 })
2017 });
2018
2019 let buffer_1 = project
2020 .update(cx, |project, cx| {
2021 project.open_buffer((worktree_id, "main.rs"), cx)
2022 })
2023 .await
2024 .unwrap();
2025 let buffer_2 = project
2026 .update(cx, |project, cx| {
2027 project.open_buffer((worktree_id, "other.rs"), cx)
2028 })
2029 .await
2030 .unwrap();
2031 let multibuffer = cx.add_model(|cx| {
2032 let mut multibuffer = MultiBuffer::new(0);
2033 multibuffer.push_excerpts(
2034 buffer_1.clone(),
2035 [
2036 ExcerptRange {
2037 context: Point::new(0, 0)..Point::new(2, 0),
2038 primary: None,
2039 },
2040 ExcerptRange {
2041 context: Point::new(4, 0)..Point::new(11, 0),
2042 primary: None,
2043 },
2044 ExcerptRange {
2045 context: Point::new(22, 0)..Point::new(33, 0),
2046 primary: None,
2047 },
2048 ExcerptRange {
2049 context: Point::new(44, 0)..Point::new(55, 0),
2050 primary: None,
2051 },
2052 ExcerptRange {
2053 context: Point::new(56, 0)..Point::new(66, 0),
2054 primary: None,
2055 },
2056 ExcerptRange {
2057 context: Point::new(67, 0)..Point::new(77, 0),
2058 primary: None,
2059 },
2060 ],
2061 cx,
2062 );
2063 multibuffer.push_excerpts(
2064 buffer_2.clone(),
2065 [
2066 ExcerptRange {
2067 context: Point::new(0, 1)..Point::new(2, 1),
2068 primary: None,
2069 },
2070 ExcerptRange {
2071 context: Point::new(4, 1)..Point::new(11, 1),
2072 primary: None,
2073 },
2074 ExcerptRange {
2075 context: Point::new(22, 1)..Point::new(33, 1),
2076 primary: None,
2077 },
2078 ExcerptRange {
2079 context: Point::new(44, 1)..Point::new(55, 1),
2080 primary: None,
2081 },
2082 ExcerptRange {
2083 context: Point::new(56, 1)..Point::new(66, 1),
2084 primary: None,
2085 },
2086 ExcerptRange {
2087 context: Point::new(67, 1)..Point::new(77, 1),
2088 primary: None,
2089 },
2090 ],
2091 cx,
2092 );
2093 multibuffer
2094 });
2095
2096 deterministic.run_until_parked();
2097 cx.foreground().run_until_parked();
2098 let editor = cx
2099 .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
2100 .root(cx);
2101 let editor_edited = Arc::new(AtomicBool::new(false));
2102 let fake_server = fake_servers.next().await.unwrap();
2103 let closure_editor_edited = Arc::clone(&editor_edited);
2104 fake_server
2105 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2106 let task_editor_edited = Arc::clone(&closure_editor_edited);
2107 async move {
2108 let hint_text = if params.text_document.uri
2109 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2110 {
2111 "main hint"
2112 } else if params.text_document.uri
2113 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2114 {
2115 "other hint"
2116 } else {
2117 panic!("unexpected uri: {:?}", params.text_document.uri);
2118 };
2119
2120 // one hint per excerpt
2121 let positions = [
2122 lsp::Position::new(0, 2),
2123 lsp::Position::new(4, 2),
2124 lsp::Position::new(22, 2),
2125 lsp::Position::new(44, 2),
2126 lsp::Position::new(56, 2),
2127 lsp::Position::new(67, 2),
2128 ];
2129 let out_of_range_hint = lsp::InlayHint {
2130 position: lsp::Position::new(
2131 params.range.start.line + 99,
2132 params.range.start.character + 99,
2133 ),
2134 label: lsp::InlayHintLabel::String(
2135 "out of excerpt range, should be ignored".to_string(),
2136 ),
2137 kind: None,
2138 text_edits: None,
2139 tooltip: None,
2140 padding_left: None,
2141 padding_right: None,
2142 data: None,
2143 };
2144
2145 let edited = task_editor_edited.load(Ordering::Acquire);
2146 Ok(Some(
2147 std::iter::once(out_of_range_hint)
2148 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2149 lsp::InlayHint {
2150 position,
2151 label: lsp::InlayHintLabel::String(format!(
2152 "{hint_text}{} #{i}",
2153 if edited { "(edited)" } else { "" },
2154 )),
2155 kind: None,
2156 text_edits: None,
2157 tooltip: None,
2158 padding_left: None,
2159 padding_right: None,
2160 data: None,
2161 }
2162 }))
2163 .collect(),
2164 ))
2165 }
2166 })
2167 .next()
2168 .await;
2169 cx.foreground().run_until_parked();
2170
2171 editor.update(cx, |editor, cx| {
2172 let expected_layers = vec![
2173 "main hint #0".to_string(),
2174 "main hint #1".to_string(),
2175 "main hint #2".to_string(),
2176 "main hint #3".to_string(),
2177 ];
2178 assert_eq!(
2179 expected_layers,
2180 cached_hint_labels(editor),
2181 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2182 );
2183 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2184 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison");
2185 });
2186
2187 editor.update(cx, |editor, cx| {
2188 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2189 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2190 });
2191 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2192 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2193 });
2194 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2195 s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
2196 });
2197 });
2198 cx.foreground().run_until_parked();
2199 editor.update(cx, |editor, cx| {
2200 let expected_layers = vec![
2201 "main hint #0".to_string(),
2202 "main hint #1".to_string(),
2203 "main hint #2".to_string(),
2204 "main hint #3".to_string(),
2205 "main hint #4".to_string(),
2206 "main hint #5".to_string(),
2207 "other hint #0".to_string(),
2208 "other hint #1".to_string(),
2209 "other hint #2".to_string(),
2210 ];
2211 assert_eq!(expected_layers, cached_hint_labels(editor),
2212 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2213 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2214 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(),
2215 "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
2216 });
2217
2218 editor.update(cx, |editor, cx| {
2219 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2220 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2221 });
2222 });
2223 cx.foreground().run_until_parked();
2224 let last_scroll_update_version = editor.update(cx, |editor, cx| {
2225 let expected_layers = vec![
2226 "main hint #0".to_string(),
2227 "main hint #1".to_string(),
2228 "main hint #2".to_string(),
2229 "main hint #3".to_string(),
2230 "main hint #4".to_string(),
2231 "main hint #5".to_string(),
2232 "other hint #0".to_string(),
2233 "other hint #1".to_string(),
2234 "other hint #2".to_string(),
2235 "other hint #3".to_string(),
2236 "other hint #4".to_string(),
2237 "other hint #5".to_string(),
2238 ];
2239 assert_eq!(expected_layers, cached_hint_labels(editor),
2240 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2241 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2242 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len());
2243 expected_layers.len()
2244 });
2245
2246 editor.update(cx, |editor, cx| {
2247 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2248 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2249 });
2250 });
2251 cx.foreground().run_until_parked();
2252 editor.update(cx, |editor, cx| {
2253 let expected_layers = vec![
2254 "main hint #0".to_string(),
2255 "main hint #1".to_string(),
2256 "main hint #2".to_string(),
2257 "main hint #3".to_string(),
2258 "main hint #4".to_string(),
2259 "main hint #5".to_string(),
2260 "other hint #0".to_string(),
2261 "other hint #1".to_string(),
2262 "other hint #2".to_string(),
2263 "other hint #3".to_string(),
2264 "other hint #4".to_string(),
2265 "other hint #5".to_string(),
2266 ];
2267 assert_eq!(expected_layers, cached_hint_labels(editor),
2268 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2269 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2270 assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
2271 });
2272
2273 editor_edited.store(true, Ordering::Release);
2274 editor.update(cx, |editor, cx| {
2275 editor.change_selections(None, cx, |s| {
2276 s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
2277 });
2278 editor.handle_input("++++more text++++", cx);
2279 });
2280 cx.foreground().run_until_parked();
2281 editor.update(cx, |editor, cx| {
2282 let expected_layers = vec![
2283 "main hint(edited) #0".to_string(),
2284 "main hint(edited) #1".to_string(),
2285 "main hint(edited) #2".to_string(),
2286 "main hint(edited) #3".to_string(),
2287 "main hint(edited) #4".to_string(),
2288 "main hint(edited) #5".to_string(),
2289 "other hint(edited) #0".to_string(),
2290 "other hint(edited) #1".to_string(),
2291 ];
2292 assert_eq!(
2293 expected_layers,
2294 cached_hint_labels(editor),
2295 "After multibuffer edit, editor gets scolled back to the last selection; \
2296all hints should be invalidated and requeried for all of its visible excerpts"
2297 );
2298 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2299 assert_eq!(
2300 editor.inlay_hint_cache().version,
2301 last_scroll_update_version + expected_layers.len() + 1,
2302 "Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal"
2303 );
2304 });
2305 }
2306
2307 #[gpui::test]
2308 async fn test_excerpts_removed(
2309 deterministic: Arc<Deterministic>,
2310 cx: &mut gpui::TestAppContext,
2311 ) {
2312 init_test(cx, |settings| {
2313 settings.defaults.inlay_hints = Some(InlayHintSettings {
2314 enabled: true,
2315 show_type_hints: false,
2316 show_parameter_hints: false,
2317 show_other_hints: false,
2318 })
2319 });
2320
2321 let mut language = Language::new(
2322 LanguageConfig {
2323 name: "Rust".into(),
2324 path_suffixes: vec!["rs".to_string()],
2325 ..Default::default()
2326 },
2327 Some(tree_sitter_rust::language()),
2328 );
2329 let mut fake_servers = language
2330 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2331 capabilities: lsp::ServerCapabilities {
2332 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2333 ..Default::default()
2334 },
2335 ..Default::default()
2336 }))
2337 .await;
2338 let language = Arc::new(language);
2339 let fs = FakeFs::new(cx.background());
2340 fs.insert_tree(
2341 "/a",
2342 json!({
2343 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2344 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2345 }),
2346 )
2347 .await;
2348 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2349 project.update(cx, |project, _| {
2350 project.languages().add(Arc::clone(&language))
2351 });
2352 let workspace = cx
2353 .add_window(|cx| Workspace::test_new(project.clone(), cx))
2354 .root(cx);
2355 let worktree_id = workspace.update(cx, |workspace, cx| {
2356 workspace.project().read_with(cx, |project, cx| {
2357 project.worktrees(cx).next().unwrap().read(cx).id()
2358 })
2359 });
2360
2361 let buffer_1 = project
2362 .update(cx, |project, cx| {
2363 project.open_buffer((worktree_id, "main.rs"), cx)
2364 })
2365 .await
2366 .unwrap();
2367 let buffer_2 = project
2368 .update(cx, |project, cx| {
2369 project.open_buffer((worktree_id, "other.rs"), cx)
2370 })
2371 .await
2372 .unwrap();
2373 let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
2374 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2375 let buffer_1_excerpts = multibuffer.push_excerpts(
2376 buffer_1.clone(),
2377 [ExcerptRange {
2378 context: Point::new(0, 0)..Point::new(2, 0),
2379 primary: None,
2380 }],
2381 cx,
2382 );
2383 let buffer_2_excerpts = multibuffer.push_excerpts(
2384 buffer_2.clone(),
2385 [ExcerptRange {
2386 context: Point::new(0, 1)..Point::new(2, 1),
2387 primary: None,
2388 }],
2389 cx,
2390 );
2391 (buffer_1_excerpts, buffer_2_excerpts)
2392 });
2393
2394 assert!(!buffer_1_excerpts.is_empty());
2395 assert!(!buffer_2_excerpts.is_empty());
2396
2397 deterministic.run_until_parked();
2398 cx.foreground().run_until_parked();
2399 let editor = cx
2400 .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
2401 .root(cx);
2402 let editor_edited = Arc::new(AtomicBool::new(false));
2403 let fake_server = fake_servers.next().await.unwrap();
2404 let closure_editor_edited = Arc::clone(&editor_edited);
2405 fake_server
2406 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2407 let task_editor_edited = Arc::clone(&closure_editor_edited);
2408 async move {
2409 let hint_text = if params.text_document.uri
2410 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2411 {
2412 "main hint"
2413 } else if params.text_document.uri
2414 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2415 {
2416 "other hint"
2417 } else {
2418 panic!("unexpected uri: {:?}", params.text_document.uri);
2419 };
2420
2421 let positions = [
2422 lsp::Position::new(0, 2),
2423 lsp::Position::new(4, 2),
2424 lsp::Position::new(22, 2),
2425 lsp::Position::new(44, 2),
2426 lsp::Position::new(56, 2),
2427 lsp::Position::new(67, 2),
2428 ];
2429 let out_of_range_hint = lsp::InlayHint {
2430 position: lsp::Position::new(
2431 params.range.start.line + 99,
2432 params.range.start.character + 99,
2433 ),
2434 label: lsp::InlayHintLabel::String(
2435 "out of excerpt range, should be ignored".to_string(),
2436 ),
2437 kind: None,
2438 text_edits: None,
2439 tooltip: None,
2440 padding_left: None,
2441 padding_right: None,
2442 data: None,
2443 };
2444
2445 let edited = task_editor_edited.load(Ordering::Acquire);
2446 Ok(Some(
2447 std::iter::once(out_of_range_hint)
2448 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2449 lsp::InlayHint {
2450 position,
2451 label: lsp::InlayHintLabel::String(format!(
2452 "{hint_text}{} #{i}",
2453 if edited { "(edited)" } else { "" },
2454 )),
2455 kind: None,
2456 text_edits: None,
2457 tooltip: None,
2458 padding_left: None,
2459 padding_right: None,
2460 data: None,
2461 }
2462 }))
2463 .collect(),
2464 ))
2465 }
2466 })
2467 .next()
2468 .await;
2469 cx.foreground().run_until_parked();
2470
2471 editor.update(cx, |editor, cx| {
2472 assert_eq!(
2473 vec!["main hint #0".to_string(), "other hint #0".to_string()],
2474 cached_hint_labels(editor),
2475 "Cache should update for both excerpts despite hints display was disabled"
2476 );
2477 assert!(
2478 visible_hint_labels(editor, cx).is_empty(),
2479 "All hints are disabled and should not be shown despite being present in the cache"
2480 );
2481 assert_eq!(
2482 editor.inlay_hint_cache().version,
2483 2,
2484 "Cache should update once per excerpt query"
2485 );
2486 });
2487
2488 editor.update(cx, |editor, cx| {
2489 editor.buffer().update(cx, |multibuffer, cx| {
2490 multibuffer.remove_excerpts(buffer_2_excerpts, cx)
2491 })
2492 });
2493 cx.foreground().run_until_parked();
2494 editor.update(cx, |editor, cx| {
2495 assert_eq!(
2496 vec!["main hint #0".to_string()],
2497 cached_hint_labels(editor),
2498 "For the removed excerpt, should clean corresponding cached hints"
2499 );
2500 assert!(
2501 visible_hint_labels(editor, cx).is_empty(),
2502 "All hints are disabled and should not be shown despite being present in the cache"
2503 );
2504 assert_eq!(
2505 editor.inlay_hint_cache().version,
2506 3,
2507 "Excerpt removal should trigger cache update"
2508 );
2509 });
2510
2511 update_test_language_settings(cx, |settings| {
2512 settings.defaults.inlay_hints = Some(InlayHintSettings {
2513 enabled: true,
2514 show_type_hints: true,
2515 show_parameter_hints: true,
2516 show_other_hints: true,
2517 })
2518 });
2519 cx.foreground().run_until_parked();
2520 editor.update(cx, |editor, cx| {
2521 let expected_hints = vec!["main hint #0".to_string()];
2522 assert_eq!(
2523 expected_hints,
2524 cached_hint_labels(editor),
2525 "Hint display settings change should not change the cache"
2526 );
2527 assert_eq!(
2528 expected_hints,
2529 visible_hint_labels(editor, cx),
2530 "Settings change should make cached hints visible"
2531 );
2532 assert_eq!(
2533 editor.inlay_hint_cache().version,
2534 4,
2535 "Settings change should trigger cache update"
2536 );
2537 });
2538 }
2539
2540 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
2541 cx.foreground().forbid_parking();
2542
2543 cx.update(|cx| {
2544 cx.set_global(SettingsStore::test(cx));
2545 theme::init((), cx);
2546 client::init_settings(cx);
2547 language::init(cx);
2548 Project::init_settings(cx);
2549 workspace::init_settings(cx);
2550 crate::init(cx);
2551 });
2552
2553 update_test_language_settings(cx, f);
2554 }
2555
2556 async fn prepare_test_objects(
2557 cx: &mut TestAppContext,
2558 ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
2559 let mut language = Language::new(
2560 LanguageConfig {
2561 name: "Rust".into(),
2562 path_suffixes: vec!["rs".to_string()],
2563 ..Default::default()
2564 },
2565 Some(tree_sitter_rust::language()),
2566 );
2567 let mut fake_servers = language
2568 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2569 capabilities: lsp::ServerCapabilities {
2570 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2571 ..Default::default()
2572 },
2573 ..Default::default()
2574 }))
2575 .await;
2576
2577 let fs = FakeFs::new(cx.background());
2578 fs.insert_tree(
2579 "/a",
2580 json!({
2581 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
2582 "other.rs": "// Test file",
2583 }),
2584 )
2585 .await;
2586
2587 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2588 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
2589 let workspace = cx
2590 .add_window(|cx| Workspace::test_new(project.clone(), cx))
2591 .root(cx);
2592 let worktree_id = workspace.update(cx, |workspace, cx| {
2593 workspace.project().read_with(cx, |project, cx| {
2594 project.worktrees(cx).next().unwrap().read(cx).id()
2595 })
2596 });
2597
2598 let _buffer = project
2599 .update(cx, |project, cx| {
2600 project.open_local_buffer("/a/main.rs", cx)
2601 })
2602 .await
2603 .unwrap();
2604 cx.foreground().run_until_parked();
2605 cx.foreground().start_waiting();
2606 let fake_server = fake_servers.next().await.unwrap();
2607 let editor = workspace
2608 .update(cx, |workspace, cx| {
2609 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
2610 })
2611 .await
2612 .unwrap()
2613 .downcast::<Editor>()
2614 .unwrap();
2615
2616 ("/a/main.rs", editor, fake_server)
2617 }
2618
2619 fn cached_hint_labels(editor: &Editor) -> Vec<String> {
2620 let mut labels = Vec::new();
2621 for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
2622 let excerpt_hints = excerpt_hints.read();
2623 for (_, inlay) in excerpt_hints.hints.iter() {
2624 match &inlay.label {
2625 project::InlayHintLabel::String(s) => labels.push(s.to_string()),
2626 _ => unreachable!(),
2627 }
2628 }
2629 }
2630
2631 labels.sort();
2632 labels
2633 }
2634
2635 fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
2636 let mut hints = editor
2637 .visible_inlay_hints(cx)
2638 .into_iter()
2639 .map(|hint| hint.text.to_string())
2640 .collect::<Vec<_>>();
2641 hints.sort();
2642 hints
2643 }
2644}