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