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