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