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<(Anchor, InlayId, InlayHint)>,
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((
289 multi_buffer_snapshot.anchor_in_excerpt(
290 *excerpt_id,
291 cached_hint.position,
292 ),
293 *cached_hint_id,
294 cached_hint.clone(),
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((
311 multi_buffer_snapshot
312 .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
313 *cached_hint_id,
314 maybe_missed_cached_hint.clone(),
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 = InlayId::Hint(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
667 .to_insert
668 .push((new_hint_position, new_inlay_id, new_hint.clone()));
669 }
670
671 cached_excerpt_hints.hints.push((new_inlay_id, new_hint));
672 }
673
674 cached_excerpt_hints
675 .hints
676 .sort_by(|(_, hint_a), (_, hint_b)| {
677 hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
678 });
679 drop(cached_excerpt_hints);
680
681 if query.invalidate.should_invalidate() {
682 let mut outdated_excerpt_caches = HashSet::default();
683 for (excerpt_id, excerpt_hints) in editor.inlay_hint_cache().hints.iter() {
684 let excerpt_hints = excerpt_hints.read();
685 if excerpt_hints.buffer_id == query.buffer_id
686 && excerpt_id != &query.excerpt_id
687 && buffer_snapshot
688 .version()
689 .changed_since(&excerpt_hints.buffer_version)
690 {
691 outdated_excerpt_caches.insert(*excerpt_id);
692 splice
693 .to_remove
694 .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
695 }
696 }
697 editor
698 .inlay_hint_cache
699 .hints
700 .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
701 }
702
703 let InlaySplice {
704 to_remove,
705 to_insert,
706 } = splice;
707 if !to_remove.is_empty() || !to_insert.is_empty() {
708 editor.splice_inlay_hints(to_remove, to_insert, cx)
709 }
710 }
711 })
712 .ok();
713
714 Ok(update_happened)
715}
716
717fn calculate_hint_updates(
718 query: ExcerptQuery,
719 fetch_range: Range<language::Anchor>,
720 new_excerpt_hints: Vec<InlayHint>,
721 buffer_snapshot: &BufferSnapshot,
722 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
723 visible_hints: &[Inlay],
724) -> Option<ExcerptHintsUpdate> {
725 let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
726 let mut excerpt_hints_to_persist = HashMap::default();
727 for new_hint in new_excerpt_hints {
728 if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
729 continue;
730 }
731 let missing_from_cache = match &cached_excerpt_hints {
732 Some(cached_excerpt_hints) => {
733 let cached_excerpt_hints = cached_excerpt_hints.read();
734 match cached_excerpt_hints.hints.binary_search_by(|probe| {
735 probe.1.position.cmp(&new_hint.position, buffer_snapshot)
736 }) {
737 Ok(ix) => {
738 let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
739 if cached_hint == &new_hint {
740 excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
741 false
742 } else {
743 true
744 }
745 }
746 Err(_) => true,
747 }
748 }
749 None => true,
750 };
751 if missing_from_cache {
752 add_to_cache.insert(new_hint);
753 }
754 }
755
756 let mut remove_from_visible = Vec::new();
757 let mut remove_from_cache = HashSet::default();
758 if query.invalidate.should_invalidate() {
759 remove_from_visible.extend(
760 visible_hints
761 .iter()
762 .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
763 .filter(|hint| {
764 contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
765 })
766 .filter(|hint| {
767 fetch_range
768 .start
769 .cmp(&hint.position.text_anchor, buffer_snapshot)
770 .is_le()
771 && fetch_range
772 .end
773 .cmp(&hint.position.text_anchor, buffer_snapshot)
774 .is_ge()
775 })
776 .map(|inlay_hint| inlay_hint.id)
777 .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
778 );
779
780 if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
781 let cached_excerpt_hints = cached_excerpt_hints.read();
782 remove_from_cache.extend(
783 cached_excerpt_hints
784 .hints
785 .iter()
786 .filter(|(cached_inlay_id, _)| {
787 !excerpt_hints_to_persist.contains_key(cached_inlay_id)
788 })
789 .filter(|(_, cached_hint)| {
790 fetch_range
791 .start
792 .cmp(&cached_hint.position, buffer_snapshot)
793 .is_le()
794 && fetch_range
795 .end
796 .cmp(&cached_hint.position, buffer_snapshot)
797 .is_ge()
798 })
799 .map(|(cached_inlay_id, _)| *cached_inlay_id),
800 );
801 }
802 }
803
804 if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
805 None
806 } else {
807 Some(ExcerptHintsUpdate {
808 excerpt_id: query.excerpt_id,
809 remove_from_visible,
810 remove_from_cache,
811 add_to_cache,
812 })
813 }
814}
815
816fn contains_position(
817 range: &Range<language::Anchor>,
818 position: language::Anchor,
819 buffer_snapshot: &BufferSnapshot,
820) -> bool {
821 range.start.cmp(&position, buffer_snapshot).is_le()
822 && range.end.cmp(&position, buffer_snapshot).is_ge()
823}
824
825#[cfg(test)]
826mod tests {
827 use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
828
829 use crate::{
830 scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
831 serde_json::json,
832 ExcerptRange, InlayHintSettings,
833 };
834 use futures::StreamExt;
835 use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
836 use language::{
837 language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
838 };
839 use lsp::FakeLanguageServer;
840 use parking_lot::Mutex;
841 use project::{FakeFs, Project};
842 use settings::SettingsStore;
843 use text::Point;
844 use workspace::Workspace;
845
846 use crate::editor_tests::update_test_settings;
847
848 use super::*;
849
850 #[gpui::test]
851 async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
852 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
853 init_test(cx, |settings| {
854 settings.defaults.inlay_hints = Some(InlayHintSettings {
855 enabled: true,
856 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
857 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
858 show_other_hints: allowed_hint_kinds.contains(&None),
859 })
860 });
861
862 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
863 let lsp_request_count = Arc::new(AtomicU32::new(0));
864 fake_server
865 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
866 let task_lsp_request_count = Arc::clone(&lsp_request_count);
867 async move {
868 assert_eq!(
869 params.text_document.uri,
870 lsp::Url::from_file_path(file_with_hints).unwrap(),
871 );
872 let current_call_id =
873 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
874 let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
875 for _ in 0..2 {
876 let mut i = current_call_id;
877 loop {
878 new_hints.push(lsp::InlayHint {
879 position: lsp::Position::new(0, i),
880 label: lsp::InlayHintLabel::String(i.to_string()),
881 kind: None,
882 text_edits: None,
883 tooltip: None,
884 padding_left: None,
885 padding_right: None,
886 data: None,
887 });
888 if i == 0 {
889 break;
890 }
891 i -= 1;
892 }
893 }
894
895 Ok(Some(new_hints))
896 }
897 })
898 .next()
899 .await;
900 cx.foreground().run_until_parked();
901
902 let mut edits_made = 1;
903 editor.update(cx, |editor, cx| {
904 let expected_layers = vec!["0".to_string()];
905 assert_eq!(
906 expected_layers,
907 cached_hint_labels(editor),
908 "Should get its first hints when opening the editor"
909 );
910 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
911 let inlay_cache = editor.inlay_hint_cache();
912 assert_eq!(
913 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
914 "Cache should use editor settings to get the allowed hint kinds"
915 );
916 assert_eq!(
917 inlay_cache.version, edits_made,
918 "The editor update the cache version after every cache/view change"
919 );
920 });
921
922 editor.update(cx, |editor, cx| {
923 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
924 editor.handle_input("some change", cx);
925 edits_made += 1;
926 });
927 cx.foreground().run_until_parked();
928 editor.update(cx, |editor, cx| {
929 let expected_layers = vec!["0".to_string(), "1".to_string()];
930 assert_eq!(
931 expected_layers,
932 cached_hint_labels(editor),
933 "Should get new hints after an edit"
934 );
935 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
936 let inlay_cache = editor.inlay_hint_cache();
937 assert_eq!(
938 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
939 "Cache should use editor settings to get the allowed hint kinds"
940 );
941 assert_eq!(
942 inlay_cache.version, edits_made,
943 "The editor update the cache version after every cache/view change"
944 );
945 });
946
947 fake_server
948 .request::<lsp::request::InlayHintRefreshRequest>(())
949 .await
950 .expect("inlay refresh request failed");
951 edits_made += 1;
952 cx.foreground().run_until_parked();
953 editor.update(cx, |editor, cx| {
954 let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
955 assert_eq!(
956 expected_layers,
957 cached_hint_labels(editor),
958 "Should get new hints after hint refresh/ request"
959 );
960 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
961 let inlay_cache = editor.inlay_hint_cache();
962 assert_eq!(
963 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
964 "Cache should use editor settings to get the allowed hint kinds"
965 );
966 assert_eq!(
967 inlay_cache.version, edits_made,
968 "The editor update the cache version after every cache/view change"
969 );
970 });
971 }
972
973 #[gpui::test]
974 async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
975 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
976 init_test(cx, |settings| {
977 settings.defaults.inlay_hints = Some(InlayHintSettings {
978 enabled: true,
979 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
980 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
981 show_other_hints: allowed_hint_kinds.contains(&None),
982 })
983 });
984
985 let fs = FakeFs::new(cx.background());
986 fs.insert_tree(
987 "/a",
988 json!({
989 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
990 "other.md": "Test md file with some text",
991 }),
992 )
993 .await;
994 let project = Project::test(fs, ["/a".as_ref()], cx).await;
995 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
996 let worktree_id = workspace.update(cx, |workspace, cx| {
997 workspace.project().read_with(cx, |project, cx| {
998 project.worktrees(cx).next().unwrap().read(cx).id()
999 })
1000 });
1001
1002 let mut rs_fake_servers = None;
1003 let mut md_fake_servers = None;
1004 for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1005 let mut language = Language::new(
1006 LanguageConfig {
1007 name: name.into(),
1008 path_suffixes: vec![path_suffix.to_string()],
1009 ..Default::default()
1010 },
1011 Some(tree_sitter_rust::language()),
1012 );
1013 let fake_servers = language
1014 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1015 name,
1016 capabilities: lsp::ServerCapabilities {
1017 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1018 ..Default::default()
1019 },
1020 ..Default::default()
1021 }))
1022 .await;
1023 match name {
1024 "Rust" => rs_fake_servers = Some(fake_servers),
1025 "Markdown" => md_fake_servers = Some(fake_servers),
1026 _ => unreachable!(),
1027 }
1028 project.update(cx, |project, _| {
1029 project.languages().add(Arc::new(language));
1030 });
1031 }
1032
1033 let _rs_buffer = project
1034 .update(cx, |project, cx| {
1035 project.open_local_buffer("/a/main.rs", cx)
1036 })
1037 .await
1038 .unwrap();
1039 cx.foreground().run_until_parked();
1040 cx.foreground().start_waiting();
1041 let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1042 let rs_editor = workspace
1043 .update(cx, |workspace, cx| {
1044 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1045 })
1046 .await
1047 .unwrap()
1048 .downcast::<Editor>()
1049 .unwrap();
1050 let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1051 rs_fake_server
1052 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1053 let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
1054 async move {
1055 assert_eq!(
1056 params.text_document.uri,
1057 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1058 );
1059 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1060 Ok(Some(vec![lsp::InlayHint {
1061 position: lsp::Position::new(0, i),
1062 label: lsp::InlayHintLabel::String(i.to_string()),
1063 kind: None,
1064 text_edits: None,
1065 tooltip: None,
1066 padding_left: None,
1067 padding_right: None,
1068 data: None,
1069 }]))
1070 }
1071 })
1072 .next()
1073 .await;
1074 cx.foreground().run_until_parked();
1075 rs_editor.update(cx, |editor, cx| {
1076 let expected_layers = vec!["0".to_string()];
1077 assert_eq!(
1078 expected_layers,
1079 cached_hint_labels(editor),
1080 "Should get its first hints when opening the editor"
1081 );
1082 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1083 let inlay_cache = editor.inlay_hint_cache();
1084 assert_eq!(
1085 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1086 "Cache should use editor settings to get the allowed hint kinds"
1087 );
1088 assert_eq!(
1089 inlay_cache.version, 1,
1090 "Rust editor update the cache version after every cache/view change"
1091 );
1092 });
1093
1094 cx.foreground().run_until_parked();
1095 let _md_buffer = project
1096 .update(cx, |project, cx| {
1097 project.open_local_buffer("/a/other.md", cx)
1098 })
1099 .await
1100 .unwrap();
1101 cx.foreground().run_until_parked();
1102 cx.foreground().start_waiting();
1103 let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1104 let md_editor = workspace
1105 .update(cx, |workspace, cx| {
1106 workspace.open_path((worktree_id, "other.md"), None, true, cx)
1107 })
1108 .await
1109 .unwrap()
1110 .downcast::<Editor>()
1111 .unwrap();
1112 let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1113 md_fake_server
1114 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1115 let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
1116 async move {
1117 assert_eq!(
1118 params.text_document.uri,
1119 lsp::Url::from_file_path("/a/other.md").unwrap(),
1120 );
1121 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1122 Ok(Some(vec![lsp::InlayHint {
1123 position: lsp::Position::new(0, i),
1124 label: lsp::InlayHintLabel::String(i.to_string()),
1125 kind: None,
1126 text_edits: None,
1127 tooltip: None,
1128 padding_left: None,
1129 padding_right: None,
1130 data: None,
1131 }]))
1132 }
1133 })
1134 .next()
1135 .await;
1136 cx.foreground().run_until_parked();
1137 md_editor.update(cx, |editor, cx| {
1138 let expected_layers = vec!["0".to_string()];
1139 assert_eq!(
1140 expected_layers,
1141 cached_hint_labels(editor),
1142 "Markdown editor should have a separate verison, repeating Rust editor rules"
1143 );
1144 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1145 let inlay_cache = editor.inlay_hint_cache();
1146 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
1147 assert_eq!(inlay_cache.version, 1);
1148 });
1149
1150 rs_editor.update(cx, |editor, cx| {
1151 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1152 editor.handle_input("some rs change", cx);
1153 });
1154 cx.foreground().run_until_parked();
1155 rs_editor.update(cx, |editor, cx| {
1156 let expected_layers = vec!["1".to_string()];
1157 assert_eq!(
1158 expected_layers,
1159 cached_hint_labels(editor),
1160 "Rust inlay cache should change after the edit"
1161 );
1162 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1163 let inlay_cache = editor.inlay_hint_cache();
1164 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
1165 assert_eq!(
1166 inlay_cache.version, 2,
1167 "Every time hint cache changes, cache version should be incremented"
1168 );
1169 });
1170 md_editor.update(cx, |editor, cx| {
1171 let expected_layers = vec!["0".to_string()];
1172 assert_eq!(
1173 expected_layers,
1174 cached_hint_labels(editor),
1175 "Markdown editor should not be affected by Rust editor changes"
1176 );
1177 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1178 let inlay_cache = editor.inlay_hint_cache();
1179 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
1180 assert_eq!(inlay_cache.version, 1);
1181 });
1182
1183 md_editor.update(cx, |editor, cx| {
1184 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1185 editor.handle_input("some md change", cx);
1186 });
1187 cx.foreground().run_until_parked();
1188 md_editor.update(cx, |editor, cx| {
1189 let expected_layers = vec!["1".to_string()];
1190 assert_eq!(
1191 expected_layers,
1192 cached_hint_labels(editor),
1193 "Rust editor should not be affected by Markdown editor changes"
1194 );
1195 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1196 let inlay_cache = editor.inlay_hint_cache();
1197 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
1198 assert_eq!(inlay_cache.version, 2);
1199 });
1200 rs_editor.update(cx, |editor, cx| {
1201 let expected_layers = vec!["1".to_string()];
1202 assert_eq!(
1203 expected_layers,
1204 cached_hint_labels(editor),
1205 "Markdown editor should also change independently"
1206 );
1207 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1208 let inlay_cache = editor.inlay_hint_cache();
1209 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
1210 assert_eq!(inlay_cache.version, 2);
1211 });
1212 }
1213
1214 #[gpui::test]
1215 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1216 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1217 init_test(cx, |settings| {
1218 settings.defaults.inlay_hints = Some(InlayHintSettings {
1219 enabled: true,
1220 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1221 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1222 show_other_hints: allowed_hint_kinds.contains(&None),
1223 })
1224 });
1225
1226 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1227 let lsp_request_count = Arc::new(AtomicU32::new(0));
1228 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1229 fake_server
1230 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1231 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1232 async move {
1233 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1234 assert_eq!(
1235 params.text_document.uri,
1236 lsp::Url::from_file_path(file_with_hints).unwrap(),
1237 );
1238 Ok(Some(vec![
1239 lsp::InlayHint {
1240 position: lsp::Position::new(0, 1),
1241 label: lsp::InlayHintLabel::String("type hint".to_string()),
1242 kind: Some(lsp::InlayHintKind::TYPE),
1243 text_edits: None,
1244 tooltip: None,
1245 padding_left: None,
1246 padding_right: None,
1247 data: None,
1248 },
1249 lsp::InlayHint {
1250 position: lsp::Position::new(0, 2),
1251 label: lsp::InlayHintLabel::String("parameter hint".to_string()),
1252 kind: Some(lsp::InlayHintKind::PARAMETER),
1253 text_edits: None,
1254 tooltip: None,
1255 padding_left: None,
1256 padding_right: None,
1257 data: None,
1258 },
1259 lsp::InlayHint {
1260 position: lsp::Position::new(0, 3),
1261 label: lsp::InlayHintLabel::String("other hint".to_string()),
1262 kind: None,
1263 text_edits: None,
1264 tooltip: None,
1265 padding_left: None,
1266 padding_right: None,
1267 data: None,
1268 },
1269 ]))
1270 }
1271 })
1272 .next()
1273 .await;
1274 cx.foreground().run_until_parked();
1275
1276 let mut edits_made = 1;
1277 editor.update(cx, |editor, cx| {
1278 assert_eq!(
1279 lsp_request_count.load(Ordering::Relaxed),
1280 1,
1281 "Should query new hints once"
1282 );
1283 assert_eq!(
1284 vec![
1285 "other hint".to_string(),
1286 "parameter hint".to_string(),
1287 "type hint".to_string(),
1288 ],
1289 cached_hint_labels(editor),
1290 "Should get its first hints when opening the editor"
1291 );
1292 assert_eq!(
1293 vec!["other hint".to_string(), "type hint".to_string()],
1294 visible_hint_labels(editor, cx)
1295 );
1296 let inlay_cache = editor.inlay_hint_cache();
1297 assert_eq!(
1298 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1299 "Cache should use editor settings to get the allowed hint kinds"
1300 );
1301 assert_eq!(
1302 inlay_cache.version, edits_made,
1303 "The editor update the cache version after every cache/view change"
1304 );
1305 });
1306
1307 fake_server
1308 .request::<lsp::request::InlayHintRefreshRequest>(())
1309 .await
1310 .expect("inlay refresh request failed");
1311 cx.foreground().run_until_parked();
1312 editor.update(cx, |editor, cx| {
1313 assert_eq!(
1314 lsp_request_count.load(Ordering::Relaxed),
1315 2,
1316 "Should load new hints twice"
1317 );
1318 assert_eq!(
1319 vec![
1320 "other hint".to_string(),
1321 "parameter hint".to_string(),
1322 "type hint".to_string(),
1323 ],
1324 cached_hint_labels(editor),
1325 "Cached hints should not change due to allowed hint kinds settings update"
1326 );
1327 assert_eq!(
1328 vec!["other hint".to_string(), "type hint".to_string()],
1329 visible_hint_labels(editor, cx)
1330 );
1331 let inlay_cache = editor.inlay_hint_cache();
1332 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
1333 assert_eq!(
1334 inlay_cache.version, edits_made,
1335 "Should not update cache version due to new loaded hints being the same"
1336 );
1337 });
1338
1339 for (new_allowed_hint_kinds, expected_visible_hints) in [
1340 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1341 (
1342 HashSet::from_iter([Some(InlayHintKind::Type)]),
1343 vec!["type hint".to_string()],
1344 ),
1345 (
1346 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1347 vec!["parameter hint".to_string()],
1348 ),
1349 (
1350 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1351 vec!["other hint".to_string(), "type hint".to_string()],
1352 ),
1353 (
1354 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1355 vec!["other hint".to_string(), "parameter hint".to_string()],
1356 ),
1357 (
1358 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1359 vec!["parameter hint".to_string(), "type hint".to_string()],
1360 ),
1361 (
1362 HashSet::from_iter([
1363 None,
1364 Some(InlayHintKind::Type),
1365 Some(InlayHintKind::Parameter),
1366 ]),
1367 vec![
1368 "other hint".to_string(),
1369 "parameter hint".to_string(),
1370 "type hint".to_string(),
1371 ],
1372 ),
1373 ] {
1374 edits_made += 1;
1375 update_test_settings(cx, |settings| {
1376 settings.defaults.inlay_hints = Some(InlayHintSettings {
1377 enabled: true,
1378 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1379 show_parameter_hints: new_allowed_hint_kinds
1380 .contains(&Some(InlayHintKind::Parameter)),
1381 show_other_hints: new_allowed_hint_kinds.contains(&None),
1382 })
1383 });
1384 cx.foreground().run_until_parked();
1385 editor.update(cx, |editor, cx| {
1386 assert_eq!(
1387 lsp_request_count.load(Ordering::Relaxed),
1388 2,
1389 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1390 );
1391 assert_eq!(
1392 vec![
1393 "other hint".to_string(),
1394 "parameter hint".to_string(),
1395 "type hint".to_string(),
1396 ],
1397 cached_hint_labels(editor),
1398 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1399 );
1400 assert_eq!(
1401 expected_visible_hints,
1402 visible_hint_labels(editor, cx),
1403 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1404 );
1405 let inlay_cache = editor.inlay_hint_cache();
1406 assert_eq!(
1407 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1408 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1409 );
1410 assert_eq!(
1411 inlay_cache.version, edits_made,
1412 "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
1413 );
1414 });
1415 }
1416
1417 edits_made += 1;
1418 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1419 update_test_settings(cx, |settings| {
1420 settings.defaults.inlay_hints = Some(InlayHintSettings {
1421 enabled: false,
1422 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1423 show_parameter_hints: another_allowed_hint_kinds
1424 .contains(&Some(InlayHintKind::Parameter)),
1425 show_other_hints: another_allowed_hint_kinds.contains(&None),
1426 })
1427 });
1428 cx.foreground().run_until_parked();
1429 editor.update(cx, |editor, cx| {
1430 assert_eq!(
1431 lsp_request_count.load(Ordering::Relaxed),
1432 2,
1433 "Should not load new hints when hints got disabled"
1434 );
1435 assert!(
1436 cached_hint_labels(editor).is_empty(),
1437 "Should clear the cache when hints got disabled"
1438 );
1439 assert!(
1440 visible_hint_labels(editor, cx).is_empty(),
1441 "Should clear visible hints when hints got disabled"
1442 );
1443 let inlay_cache = editor.inlay_hint_cache();
1444 assert_eq!(
1445 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1446 "Should update its allowed hint kinds even when hints got disabled"
1447 );
1448 assert_eq!(
1449 inlay_cache.version, edits_made,
1450 "The editor should update the cache version after hints got disabled"
1451 );
1452 });
1453
1454 fake_server
1455 .request::<lsp::request::InlayHintRefreshRequest>(())
1456 .await
1457 .expect("inlay refresh request failed");
1458 cx.foreground().run_until_parked();
1459 editor.update(cx, |editor, cx| {
1460 assert_eq!(
1461 lsp_request_count.load(Ordering::Relaxed),
1462 2,
1463 "Should not load new hints when they got disabled"
1464 );
1465 assert!(cached_hint_labels(editor).is_empty());
1466 assert!(visible_hint_labels(editor, cx).is_empty());
1467 let inlay_cache = editor.inlay_hint_cache();
1468 assert_eq!(inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds);
1469 assert_eq!(
1470 inlay_cache.version, edits_made,
1471 "The editor should not update the cache version after /refresh query without updates"
1472 );
1473 });
1474
1475 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1476 edits_made += 1;
1477 update_test_settings(cx, |settings| {
1478 settings.defaults.inlay_hints = Some(InlayHintSettings {
1479 enabled: true,
1480 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1481 show_parameter_hints: final_allowed_hint_kinds
1482 .contains(&Some(InlayHintKind::Parameter)),
1483 show_other_hints: final_allowed_hint_kinds.contains(&None),
1484 })
1485 });
1486 cx.foreground().run_until_parked();
1487 editor.update(cx, |editor, cx| {
1488 assert_eq!(
1489 lsp_request_count.load(Ordering::Relaxed),
1490 3,
1491 "Should query for new hints when they got reenabled"
1492 );
1493 assert_eq!(
1494 vec![
1495 "other hint".to_string(),
1496 "parameter hint".to_string(),
1497 "type hint".to_string(),
1498 ],
1499 cached_hint_labels(editor),
1500 "Should get its cached hints fully repopulated after the hints got reenabled"
1501 );
1502 assert_eq!(
1503 vec!["parameter hint".to_string()],
1504 visible_hint_labels(editor, cx),
1505 "Should get its visible hints repopulated and filtered after the h"
1506 );
1507 let inlay_cache = editor.inlay_hint_cache();
1508 assert_eq!(
1509 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
1510 "Cache should update editor settings when hints got reenabled"
1511 );
1512 assert_eq!(
1513 inlay_cache.version, edits_made,
1514 "Cache should update its version after hints got reenabled"
1515 );
1516 });
1517
1518 fake_server
1519 .request::<lsp::request::InlayHintRefreshRequest>(())
1520 .await
1521 .expect("inlay refresh request failed");
1522 cx.foreground().run_until_parked();
1523 editor.update(cx, |editor, cx| {
1524 assert_eq!(
1525 lsp_request_count.load(Ordering::Relaxed),
1526 4,
1527 "Should query for new hints again"
1528 );
1529 assert_eq!(
1530 vec![
1531 "other hint".to_string(),
1532 "parameter hint".to_string(),
1533 "type hint".to_string(),
1534 ],
1535 cached_hint_labels(editor),
1536 );
1537 assert_eq!(
1538 vec!["parameter hint".to_string()],
1539 visible_hint_labels(editor, cx),
1540 );
1541 let inlay_cache = editor.inlay_hint_cache();
1542 assert_eq!(inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds);
1543 assert_eq!(inlay_cache.version, edits_made);
1544 });
1545 }
1546
1547 #[gpui::test]
1548 async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
1549 let allowed_hint_kinds = HashSet::from_iter([None]);
1550 init_test(cx, |settings| {
1551 settings.defaults.inlay_hints = Some(InlayHintSettings {
1552 enabled: true,
1553 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1554 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1555 show_other_hints: allowed_hint_kinds.contains(&None),
1556 })
1557 });
1558
1559 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1560 let fake_server = Arc::new(fake_server);
1561 let lsp_request_count = Arc::new(AtomicU32::new(0));
1562 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1563 fake_server
1564 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1565 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1566 async move {
1567 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1568 assert_eq!(
1569 params.text_document.uri,
1570 lsp::Url::from_file_path(file_with_hints).unwrap(),
1571 );
1572 Ok(Some(vec![lsp::InlayHint {
1573 position: lsp::Position::new(0, i),
1574 label: lsp::InlayHintLabel::String(i.to_string()),
1575 kind: None,
1576 text_edits: None,
1577 tooltip: None,
1578 padding_left: None,
1579 padding_right: None,
1580 data: None,
1581 }]))
1582 }
1583 })
1584 .next()
1585 .await;
1586
1587 let mut expected_changes = Vec::new();
1588 for change_after_opening in [
1589 "initial change #1",
1590 "initial change #2",
1591 "initial change #3",
1592 ] {
1593 editor.update(cx, |editor, cx| {
1594 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1595 editor.handle_input(change_after_opening, cx);
1596 });
1597 expected_changes.push(change_after_opening);
1598 }
1599
1600 cx.foreground().run_until_parked();
1601
1602 editor.update(cx, |editor, cx| {
1603 let current_text = editor.text(cx);
1604 for change in &expected_changes {
1605 assert!(
1606 current_text.contains(change),
1607 "Should apply all changes made"
1608 );
1609 }
1610 assert_eq!(
1611 lsp_request_count.load(Ordering::Relaxed),
1612 2,
1613 "Should query new hints twice: for editor init and for the last edit that interrupted all others"
1614 );
1615 let expected_hints = vec!["2".to_string()];
1616 assert_eq!(
1617 expected_hints,
1618 cached_hint_labels(editor),
1619 "Should get hints from the last edit landed only"
1620 );
1621 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1622 let inlay_cache = editor.inlay_hint_cache();
1623 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
1624 assert_eq!(
1625 inlay_cache.version, 1,
1626 "Only one update should be registered in the cache after all cancellations"
1627 );
1628 });
1629
1630 let mut edits = Vec::new();
1631 for async_later_change in [
1632 "another change #1",
1633 "another change #2",
1634 "another change #3",
1635 ] {
1636 expected_changes.push(async_later_change);
1637 let task_editor = editor.clone();
1638 let mut task_cx = cx.clone();
1639 edits.push(cx.foreground().spawn(async move {
1640 task_editor.update(&mut task_cx, |editor, cx| {
1641 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1642 editor.handle_input(async_later_change, cx);
1643 });
1644 }));
1645 }
1646 let _ = futures::future::join_all(edits).await;
1647 cx.foreground().run_until_parked();
1648
1649 editor.update(cx, |editor, cx| {
1650 let current_text = editor.text(cx);
1651 for change in &expected_changes {
1652 assert!(
1653 current_text.contains(change),
1654 "Should apply all changes made"
1655 );
1656 }
1657 assert_eq!(
1658 lsp_request_count.load(Ordering::SeqCst),
1659 3,
1660 "Should query new hints one more time, for the last edit only"
1661 );
1662 let expected_hints = vec!["3".to_string()];
1663 assert_eq!(
1664 expected_hints,
1665 cached_hint_labels(editor),
1666 "Should get hints from the last edit landed only"
1667 );
1668 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1669 let inlay_cache = editor.inlay_hint_cache();
1670 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
1671 assert_eq!(
1672 inlay_cache.version, 2,
1673 "Should update the cache version once more, for the new change"
1674 );
1675 });
1676 }
1677
1678 #[gpui::test]
1679 async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
1680 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1681 init_test(cx, |settings| {
1682 settings.defaults.inlay_hints = Some(InlayHintSettings {
1683 enabled: true,
1684 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1685 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1686 show_other_hints: allowed_hint_kinds.contains(&None),
1687 })
1688 });
1689
1690 let mut language = Language::new(
1691 LanguageConfig {
1692 name: "Rust".into(),
1693 path_suffixes: vec!["rs".to_string()],
1694 ..Default::default()
1695 },
1696 Some(tree_sitter_rust::language()),
1697 );
1698 let mut fake_servers = language
1699 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1700 capabilities: lsp::ServerCapabilities {
1701 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1702 ..Default::default()
1703 },
1704 ..Default::default()
1705 }))
1706 .await;
1707 let fs = FakeFs::new(cx.background());
1708 fs.insert_tree(
1709 "/a",
1710 json!({
1711 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
1712 "other.rs": "// Test file",
1713 }),
1714 )
1715 .await;
1716 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1717 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
1718 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1719 let worktree_id = workspace.update(cx, |workspace, cx| {
1720 workspace.project().read_with(cx, |project, cx| {
1721 project.worktrees(cx).next().unwrap().read(cx).id()
1722 })
1723 });
1724
1725 let _buffer = project
1726 .update(cx, |project, cx| {
1727 project.open_local_buffer("/a/main.rs", cx)
1728 })
1729 .await
1730 .unwrap();
1731 cx.foreground().run_until_parked();
1732 cx.foreground().start_waiting();
1733 let fake_server = fake_servers.next().await.unwrap();
1734 let editor = workspace
1735 .update(cx, |workspace, cx| {
1736 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1737 })
1738 .await
1739 .unwrap()
1740 .downcast::<Editor>()
1741 .unwrap();
1742 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
1743 let lsp_request_count = Arc::new(AtomicU32::new(0));
1744 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
1745 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
1746 fake_server
1747 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1748 let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
1749 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
1750 async move {
1751 assert_eq!(
1752 params.text_document.uri,
1753 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1754 );
1755
1756 task_lsp_request_ranges.lock().push(params.range);
1757 let query_start = params.range.start;
1758 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1759 Ok(Some(vec![lsp::InlayHint {
1760 position: query_start,
1761 label: lsp::InlayHintLabel::String(i.to_string()),
1762 kind: None,
1763 text_edits: None,
1764 tooltip: None,
1765 padding_left: None,
1766 padding_right: None,
1767 data: None,
1768 }]))
1769 }
1770 })
1771 .next()
1772 .await;
1773 cx.foreground().run_until_parked();
1774 editor.update(cx, |editor, cx| {
1775 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1776 ranges.sort_by_key(|range| range.start);
1777 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");
1778 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1779 assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
1780 assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
1781
1782 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
1783 "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
1784 let expected_layers = vec!["1".to_string(), "2".to_string()];
1785 assert_eq!(
1786 expected_layers,
1787 cached_hint_labels(editor),
1788 "Should have hints from both LSP requests made for a big file"
1789 );
1790 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1791 let inlay_cache = editor.inlay_hint_cache();
1792 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
1793 assert_eq!(
1794 inlay_cache.version, 2,
1795 "Both LSP queries should've bumped the cache version"
1796 );
1797 });
1798
1799 editor.update(cx, |editor, cx| {
1800 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1801 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1802 editor.change_selections(None, cx, |s| s.select_ranges([600..600]));
1803 editor.handle_input("++++more text++++", cx);
1804 });
1805
1806 cx.foreground().run_until_parked();
1807 editor.update(cx, |editor, cx| {
1808 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1809 ranges.sort_by_key(|range| range.start);
1810 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");
1811 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1812 assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end");
1813 assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning");
1814 assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning");
1815 assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line");
1816 assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent");
1817
1818 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5,
1819 "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints");
1820 let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()];
1821 assert_eq!(expected_layers, cached_hint_labels(editor),
1822 "Should have hints from the new LSP response after edit");
1823 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1824 let inlay_cache = editor.inlay_hint_cache();
1825 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
1826 assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
1827 });
1828 }
1829
1830 #[gpui::test]
1831 async fn test_multiple_excerpts_large_multibuffer(
1832 deterministic: Arc<Deterministic>,
1833 cx: &mut gpui::TestAppContext,
1834 ) {
1835 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1836 init_test(cx, |settings| {
1837 settings.defaults.inlay_hints = Some(InlayHintSettings {
1838 enabled: true,
1839 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1840 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1841 show_other_hints: allowed_hint_kinds.contains(&None),
1842 })
1843 });
1844
1845 let mut language = Language::new(
1846 LanguageConfig {
1847 name: "Rust".into(),
1848 path_suffixes: vec!["rs".to_string()],
1849 ..Default::default()
1850 },
1851 Some(tree_sitter_rust::language()),
1852 );
1853 let mut fake_servers = language
1854 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1855 capabilities: lsp::ServerCapabilities {
1856 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1857 ..Default::default()
1858 },
1859 ..Default::default()
1860 }))
1861 .await;
1862 let language = Arc::new(language);
1863 let fs = FakeFs::new(cx.background());
1864 fs.insert_tree(
1865 "/a",
1866 json!({
1867 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
1868 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
1869 }),
1870 )
1871 .await;
1872 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1873 project.update(cx, |project, _| {
1874 project.languages().add(Arc::clone(&language))
1875 });
1876 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1877 let worktree_id = workspace.update(cx, |workspace, cx| {
1878 workspace.project().read_with(cx, |project, cx| {
1879 project.worktrees(cx).next().unwrap().read(cx).id()
1880 })
1881 });
1882
1883 let buffer_1 = project
1884 .update(cx, |project, cx| {
1885 project.open_buffer((worktree_id, "main.rs"), cx)
1886 })
1887 .await
1888 .unwrap();
1889 let buffer_2 = project
1890 .update(cx, |project, cx| {
1891 project.open_buffer((worktree_id, "other.rs"), cx)
1892 })
1893 .await
1894 .unwrap();
1895 let multibuffer = cx.add_model(|cx| {
1896 let mut multibuffer = MultiBuffer::new(0);
1897 multibuffer.push_excerpts(
1898 buffer_1.clone(),
1899 [
1900 ExcerptRange {
1901 context: Point::new(0, 0)..Point::new(2, 0),
1902 primary: None,
1903 },
1904 ExcerptRange {
1905 context: Point::new(4, 0)..Point::new(11, 0),
1906 primary: None,
1907 },
1908 ExcerptRange {
1909 context: Point::new(22, 0)..Point::new(33, 0),
1910 primary: None,
1911 },
1912 ExcerptRange {
1913 context: Point::new(44, 0)..Point::new(55, 0),
1914 primary: None,
1915 },
1916 ExcerptRange {
1917 context: Point::new(56, 0)..Point::new(66, 0),
1918 primary: None,
1919 },
1920 ExcerptRange {
1921 context: Point::new(67, 0)..Point::new(77, 0),
1922 primary: None,
1923 },
1924 ],
1925 cx,
1926 );
1927 multibuffer.push_excerpts(
1928 buffer_2.clone(),
1929 [
1930 ExcerptRange {
1931 context: Point::new(0, 1)..Point::new(2, 1),
1932 primary: None,
1933 },
1934 ExcerptRange {
1935 context: Point::new(4, 1)..Point::new(11, 1),
1936 primary: None,
1937 },
1938 ExcerptRange {
1939 context: Point::new(22, 1)..Point::new(33, 1),
1940 primary: None,
1941 },
1942 ExcerptRange {
1943 context: Point::new(44, 1)..Point::new(55, 1),
1944 primary: None,
1945 },
1946 ExcerptRange {
1947 context: Point::new(56, 1)..Point::new(66, 1),
1948 primary: None,
1949 },
1950 ExcerptRange {
1951 context: Point::new(67, 1)..Point::new(77, 1),
1952 primary: None,
1953 },
1954 ],
1955 cx,
1956 );
1957 multibuffer
1958 });
1959
1960 deterministic.run_until_parked();
1961 cx.foreground().run_until_parked();
1962 let (_, editor) =
1963 cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
1964 let editor_edited = Arc::new(AtomicBool::new(false));
1965 let fake_server = fake_servers.next().await.unwrap();
1966 let closure_editor_edited = Arc::clone(&editor_edited);
1967 fake_server
1968 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1969 let task_editor_edited = Arc::clone(&closure_editor_edited);
1970 async move {
1971 let hint_text = if params.text_document.uri
1972 == lsp::Url::from_file_path("/a/main.rs").unwrap()
1973 {
1974 "main hint"
1975 } else if params.text_document.uri
1976 == lsp::Url::from_file_path("/a/other.rs").unwrap()
1977 {
1978 "other hint"
1979 } else {
1980 panic!("unexpected uri: {:?}", params.text_document.uri);
1981 };
1982
1983 let positions = [
1984 lsp::Position::new(0, 2),
1985 lsp::Position::new(4, 2),
1986 lsp::Position::new(22, 2),
1987 lsp::Position::new(44, 2),
1988 lsp::Position::new(56, 2),
1989 lsp::Position::new(67, 2),
1990 ];
1991 let out_of_range_hint = lsp::InlayHint {
1992 position: lsp::Position::new(
1993 params.range.start.line + 99,
1994 params.range.start.character + 99,
1995 ),
1996 label: lsp::InlayHintLabel::String(
1997 "out of excerpt range, should be ignored".to_string(),
1998 ),
1999 kind: None,
2000 text_edits: None,
2001 tooltip: None,
2002 padding_left: None,
2003 padding_right: None,
2004 data: None,
2005 };
2006
2007 let edited = task_editor_edited.load(Ordering::Acquire);
2008 Ok(Some(
2009 std::iter::once(out_of_range_hint)
2010 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2011 lsp::InlayHint {
2012 position,
2013 label: lsp::InlayHintLabel::String(format!(
2014 "{hint_text}{} #{i}",
2015 if edited { "(edited)" } else { "" },
2016 )),
2017 kind: None,
2018 text_edits: None,
2019 tooltip: None,
2020 padding_left: None,
2021 padding_right: None,
2022 data: None,
2023 }
2024 }))
2025 .collect(),
2026 ))
2027 }
2028 })
2029 .next()
2030 .await;
2031 cx.foreground().run_until_parked();
2032
2033 editor.update(cx, |editor, cx| {
2034 let expected_layers = vec![
2035 "main hint #0".to_string(),
2036 "main hint #1".to_string(),
2037 "main hint #2".to_string(),
2038 "main hint #3".to_string(),
2039 ];
2040 assert_eq!(
2041 expected_layers,
2042 cached_hint_labels(editor),
2043 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2044 );
2045 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2046 let inlay_cache = editor.inlay_hint_cache();
2047 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
2048 assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
2049 });
2050
2051 editor.update(cx, |editor, cx| {
2052 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2053 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2054 });
2055 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2056 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2057 });
2058 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2059 s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
2060 });
2061 });
2062 cx.foreground().run_until_parked();
2063 editor.update(cx, |editor, cx| {
2064 let expected_layers = vec![
2065 "main hint #0".to_string(),
2066 "main hint #1".to_string(),
2067 "main hint #2".to_string(),
2068 "main hint #3".to_string(),
2069 "main hint #4".to_string(),
2070 "main hint #5".to_string(),
2071 "other hint #0".to_string(),
2072 "other hint #1".to_string(),
2073 "other hint #2".to_string(),
2074 ];
2075 assert_eq!(expected_layers, cached_hint_labels(editor),
2076 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2077 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2078 let inlay_cache = editor.inlay_hint_cache();
2079 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
2080 assert_eq!(inlay_cache.version, 9);
2081 });
2082
2083 editor.update(cx, |editor, cx| {
2084 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2085 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2086 });
2087 });
2088 cx.foreground().run_until_parked();
2089 editor.update(cx, |editor, cx| {
2090 let expected_layers = vec![
2091 "main hint #0".to_string(),
2092 "main hint #1".to_string(),
2093 "main hint #2".to_string(),
2094 "main hint #3".to_string(),
2095 "main hint #4".to_string(),
2096 "main hint #5".to_string(),
2097 "other hint #0".to_string(),
2098 "other hint #1".to_string(),
2099 "other hint #2".to_string(),
2100 "other hint #3".to_string(),
2101 "other hint #4".to_string(),
2102 "other hint #5".to_string(),
2103 ];
2104 assert_eq!(expected_layers, cached_hint_labels(editor),
2105 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2106 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2107 let inlay_cache = editor.inlay_hint_cache();
2108 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
2109 assert_eq!(inlay_cache.version, 12);
2110 });
2111
2112 editor.update(cx, |editor, cx| {
2113 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2114 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2115 });
2116 });
2117 cx.foreground().run_until_parked();
2118 editor.update(cx, |editor, cx| {
2119 let expected_layers = vec![
2120 "main hint #0".to_string(),
2121 "main hint #1".to_string(),
2122 "main hint #2".to_string(),
2123 "main hint #3".to_string(),
2124 "main hint #4".to_string(),
2125 "main hint #5".to_string(),
2126 "other hint #0".to_string(),
2127 "other hint #1".to_string(),
2128 "other hint #2".to_string(),
2129 "other hint #3".to_string(),
2130 "other hint #4".to_string(),
2131 "other hint #5".to_string(),
2132 ];
2133 assert_eq!(expected_layers, cached_hint_labels(editor),
2134 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2135 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2136 let inlay_cache = editor.inlay_hint_cache();
2137 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
2138 assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
2139 });
2140
2141 editor_edited.store(true, Ordering::Release);
2142 editor.update(cx, |editor, cx| {
2143 editor.handle_input("++++more text++++", cx);
2144 });
2145 cx.foreground().run_until_parked();
2146 editor.update(cx, |editor, cx| {
2147 let expected_layers = vec![
2148 "main hint(edited) #0".to_string(),
2149 "main hint(edited) #1".to_string(),
2150 "main hint(edited) #2".to_string(),
2151 "main hint(edited) #3".to_string(),
2152 "other hint #0".to_string(),
2153 "other hint #1".to_string(),
2154 "other hint #2".to_string(),
2155 "other hint #3".to_string(),
2156 "other hint #4".to_string(),
2157 "other hint #5".to_string(),
2158 ];
2159 assert_eq!(expected_layers, cached_hint_labels(editor),
2160 "After multibuffer was edited, hints for the edited buffer (1st) should be invalidated and requeried for all of its visible excerpts, \
2161unedited (2nd) buffer should have the same hint");
2162 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2163 let inlay_cache = editor.inlay_hint_cache();
2164 assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
2165 assert_eq!(inlay_cache.version, 16);
2166 });
2167 }
2168
2169 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
2170 cx.foreground().forbid_parking();
2171
2172 cx.update(|cx| {
2173 cx.set_global(SettingsStore::test(cx));
2174 theme::init((), cx);
2175 client::init_settings(cx);
2176 language::init(cx);
2177 Project::init_settings(cx);
2178 workspace::init_settings(cx);
2179 crate::init(cx);
2180 });
2181
2182 update_test_settings(cx, f);
2183 }
2184
2185 async fn prepare_test_objects(
2186 cx: &mut TestAppContext,
2187 ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
2188 let mut language = Language::new(
2189 LanguageConfig {
2190 name: "Rust".into(),
2191 path_suffixes: vec!["rs".to_string()],
2192 ..Default::default()
2193 },
2194 Some(tree_sitter_rust::language()),
2195 );
2196 let mut fake_servers = language
2197 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2198 capabilities: lsp::ServerCapabilities {
2199 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2200 ..Default::default()
2201 },
2202 ..Default::default()
2203 }))
2204 .await;
2205
2206 let fs = FakeFs::new(cx.background());
2207 fs.insert_tree(
2208 "/a",
2209 json!({
2210 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
2211 "other.rs": "// Test file",
2212 }),
2213 )
2214 .await;
2215
2216 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2217 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
2218 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2219 let worktree_id = workspace.update(cx, |workspace, cx| {
2220 workspace.project().read_with(cx, |project, cx| {
2221 project.worktrees(cx).next().unwrap().read(cx).id()
2222 })
2223 });
2224
2225 let _buffer = project
2226 .update(cx, |project, cx| {
2227 project.open_local_buffer("/a/main.rs", cx)
2228 })
2229 .await
2230 .unwrap();
2231 cx.foreground().run_until_parked();
2232 cx.foreground().start_waiting();
2233 let fake_server = fake_servers.next().await.unwrap();
2234 let editor = workspace
2235 .update(cx, |workspace, cx| {
2236 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
2237 })
2238 .await
2239 .unwrap()
2240 .downcast::<Editor>()
2241 .unwrap();
2242
2243 ("/a/main.rs", editor, fake_server)
2244 }
2245
2246 fn cached_hint_labels(editor: &Editor) -> Vec<String> {
2247 let mut labels = Vec::new();
2248 for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
2249 let excerpt_hints = excerpt_hints.read();
2250 for (_, inlay) in excerpt_hints.hints.iter() {
2251 match &inlay.label {
2252 project::InlayHintLabel::String(s) => labels.push(s.to_string()),
2253 _ => unreachable!(),
2254 }
2255 }
2256 }
2257
2258 labels.sort();
2259 labels
2260 }
2261
2262 fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
2263 let mut hints = editor
2264 .visible_inlay_hints(cx)
2265 .into_iter()
2266 .map(|hint| hint.text.to_string())
2267 .collect::<Vec<_>>();
2268 hints.sort();
2269 hints
2270 }
2271}