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,
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_language_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_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
979 init_test(cx, |settings| {
980 settings.defaults.inlay_hints = Some(InlayHintSettings {
981 enabled: true,
982 show_type_hints: true,
983 show_parameter_hints: true,
984 show_other_hints: true,
985 })
986 });
987
988 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
989 let lsp_request_count = Arc::new(AtomicU32::new(0));
990 fake_server
991 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
992 let task_lsp_request_count = Arc::clone(&lsp_request_count);
993 async move {
994 assert_eq!(
995 params.text_document.uri,
996 lsp::Url::from_file_path(file_with_hints).unwrap(),
997 );
998 let current_call_id =
999 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1000 Ok(Some(vec![lsp::InlayHint {
1001 position: lsp::Position::new(0, current_call_id),
1002 label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1003 kind: None,
1004 text_edits: None,
1005 tooltip: None,
1006 padding_left: None,
1007 padding_right: None,
1008 data: None,
1009 }]))
1010 }
1011 })
1012 .next()
1013 .await;
1014 cx.foreground().run_until_parked();
1015
1016 let mut edits_made = 1;
1017 editor.update(cx, |editor, cx| {
1018 let expected_layers = vec!["0".to_string()];
1019 assert_eq!(
1020 expected_layers,
1021 cached_hint_labels(editor),
1022 "Should get its first hints when opening the editor"
1023 );
1024 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1025 let inlay_cache = editor.inlay_hint_cache();
1026 assert_eq!(
1027 inlay_cache.version, edits_made,
1028 "The editor update the cache version after every cache/view change"
1029 );
1030 });
1031
1032 let progress_token = "test_progress_token";
1033 fake_server
1034 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
1035 token: lsp::ProgressToken::String(progress_token.to_string()),
1036 })
1037 .await
1038 .expect("work done progress create request failed");
1039 cx.foreground().run_until_parked();
1040 fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1041 token: lsp::ProgressToken::String(progress_token.to_string()),
1042 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1043 lsp::WorkDoneProgressBegin::default(),
1044 )),
1045 });
1046 cx.foreground().run_until_parked();
1047
1048 editor.update(cx, |editor, cx| {
1049 let expected_layers = vec!["0".to_string()];
1050 assert_eq!(
1051 expected_layers,
1052 cached_hint_labels(editor),
1053 "Should not update hints while the work task is running"
1054 );
1055 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1056 let inlay_cache = editor.inlay_hint_cache();
1057 assert_eq!(
1058 inlay_cache.version, edits_made,
1059 "Should not update the cache while the work task is running"
1060 );
1061 });
1062
1063 fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1064 token: lsp::ProgressToken::String(progress_token.to_string()),
1065 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1066 lsp::WorkDoneProgressEnd::default(),
1067 )),
1068 });
1069 cx.foreground().run_until_parked();
1070
1071 edits_made += 1;
1072 editor.update(cx, |editor, cx| {
1073 let expected_layers = vec!["1".to_string()];
1074 assert_eq!(
1075 expected_layers,
1076 cached_hint_labels(editor),
1077 "New hints should be queried after the work task is done"
1078 );
1079 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1080 let inlay_cache = editor.inlay_hint_cache();
1081 assert_eq!(
1082 inlay_cache.version, edits_made,
1083 "Cache version should udpate once after the work task is done"
1084 );
1085 });
1086 }
1087
1088 #[gpui::test]
1089 async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1090 init_test(cx, |settings| {
1091 settings.defaults.inlay_hints = Some(InlayHintSettings {
1092 enabled: true,
1093 show_type_hints: true,
1094 show_parameter_hints: true,
1095 show_other_hints: true,
1096 })
1097 });
1098
1099 let fs = FakeFs::new(cx.background());
1100 fs.insert_tree(
1101 "/a",
1102 json!({
1103 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1104 "other.md": "Test md file with some text",
1105 }),
1106 )
1107 .await;
1108 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1109 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1110 let worktree_id = workspace.update(cx, |workspace, cx| {
1111 workspace.project().read_with(cx, |project, cx| {
1112 project.worktrees(cx).next().unwrap().read(cx).id()
1113 })
1114 });
1115
1116 let mut rs_fake_servers = None;
1117 let mut md_fake_servers = None;
1118 for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1119 let mut language = Language::new(
1120 LanguageConfig {
1121 name: name.into(),
1122 path_suffixes: vec![path_suffix.to_string()],
1123 ..Default::default()
1124 },
1125 Some(tree_sitter_rust::language()),
1126 );
1127 let fake_servers = language
1128 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1129 name,
1130 capabilities: lsp::ServerCapabilities {
1131 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1132 ..Default::default()
1133 },
1134 ..Default::default()
1135 }))
1136 .await;
1137 match name {
1138 "Rust" => rs_fake_servers = Some(fake_servers),
1139 "Markdown" => md_fake_servers = Some(fake_servers),
1140 _ => unreachable!(),
1141 }
1142 project.update(cx, |project, _| {
1143 project.languages().add(Arc::new(language));
1144 });
1145 }
1146
1147 let _rs_buffer = project
1148 .update(cx, |project, cx| {
1149 project.open_local_buffer("/a/main.rs", cx)
1150 })
1151 .await
1152 .unwrap();
1153 cx.foreground().run_until_parked();
1154 cx.foreground().start_waiting();
1155 let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1156 let rs_editor = workspace
1157 .update(cx, |workspace, cx| {
1158 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1159 })
1160 .await
1161 .unwrap()
1162 .downcast::<Editor>()
1163 .unwrap();
1164 let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1165 rs_fake_server
1166 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1167 let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
1168 async move {
1169 assert_eq!(
1170 params.text_document.uri,
1171 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1172 );
1173 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1174 Ok(Some(vec![lsp::InlayHint {
1175 position: lsp::Position::new(0, i),
1176 label: lsp::InlayHintLabel::String(i.to_string()),
1177 kind: None,
1178 text_edits: None,
1179 tooltip: None,
1180 padding_left: None,
1181 padding_right: None,
1182 data: None,
1183 }]))
1184 }
1185 })
1186 .next()
1187 .await;
1188 cx.foreground().run_until_parked();
1189 rs_editor.update(cx, |editor, cx| {
1190 let expected_layers = vec!["0".to_string()];
1191 assert_eq!(
1192 expected_layers,
1193 cached_hint_labels(editor),
1194 "Should get its first hints when opening the editor"
1195 );
1196 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1197 let inlay_cache = editor.inlay_hint_cache();
1198 assert_eq!(
1199 inlay_cache.version, 1,
1200 "Rust editor update the cache version after every cache/view change"
1201 );
1202 });
1203
1204 cx.foreground().run_until_parked();
1205 let _md_buffer = project
1206 .update(cx, |project, cx| {
1207 project.open_local_buffer("/a/other.md", cx)
1208 })
1209 .await
1210 .unwrap();
1211 cx.foreground().run_until_parked();
1212 cx.foreground().start_waiting();
1213 let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1214 let md_editor = workspace
1215 .update(cx, |workspace, cx| {
1216 workspace.open_path((worktree_id, "other.md"), None, true, cx)
1217 })
1218 .await
1219 .unwrap()
1220 .downcast::<Editor>()
1221 .unwrap();
1222 let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1223 md_fake_server
1224 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1225 let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
1226 async move {
1227 assert_eq!(
1228 params.text_document.uri,
1229 lsp::Url::from_file_path("/a/other.md").unwrap(),
1230 );
1231 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1232 Ok(Some(vec![lsp::InlayHint {
1233 position: lsp::Position::new(0, i),
1234 label: lsp::InlayHintLabel::String(i.to_string()),
1235 kind: None,
1236 text_edits: None,
1237 tooltip: None,
1238 padding_left: None,
1239 padding_right: None,
1240 data: None,
1241 }]))
1242 }
1243 })
1244 .next()
1245 .await;
1246 cx.foreground().run_until_parked();
1247 md_editor.update(cx, |editor, cx| {
1248 let expected_layers = vec!["0".to_string()];
1249 assert_eq!(
1250 expected_layers,
1251 cached_hint_labels(editor),
1252 "Markdown editor should have a separate verison, repeating Rust editor rules"
1253 );
1254 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1255 let inlay_cache = editor.inlay_hint_cache();
1256 assert_eq!(inlay_cache.version, 1);
1257 });
1258
1259 rs_editor.update(cx, |editor, cx| {
1260 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1261 editor.handle_input("some rs change", cx);
1262 });
1263 cx.foreground().run_until_parked();
1264 rs_editor.update(cx, |editor, cx| {
1265 let expected_layers = vec!["1".to_string()];
1266 assert_eq!(
1267 expected_layers,
1268 cached_hint_labels(editor),
1269 "Rust inlay cache should change after the edit"
1270 );
1271 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1272 let inlay_cache = editor.inlay_hint_cache();
1273 assert_eq!(
1274 inlay_cache.version, 2,
1275 "Every time hint cache changes, cache version should be incremented"
1276 );
1277 });
1278 md_editor.update(cx, |editor, cx| {
1279 let expected_layers = vec!["0".to_string()];
1280 assert_eq!(
1281 expected_layers,
1282 cached_hint_labels(editor),
1283 "Markdown editor should not be affected by Rust editor changes"
1284 );
1285 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1286 let inlay_cache = editor.inlay_hint_cache();
1287 assert_eq!(inlay_cache.version, 1);
1288 });
1289
1290 md_editor.update(cx, |editor, cx| {
1291 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1292 editor.handle_input("some md change", cx);
1293 });
1294 cx.foreground().run_until_parked();
1295 md_editor.update(cx, |editor, cx| {
1296 let expected_layers = vec!["1".to_string()];
1297 assert_eq!(
1298 expected_layers,
1299 cached_hint_labels(editor),
1300 "Rust editor should not be affected by Markdown editor changes"
1301 );
1302 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1303 let inlay_cache = editor.inlay_hint_cache();
1304 assert_eq!(inlay_cache.version, 2);
1305 });
1306 rs_editor.update(cx, |editor, cx| {
1307 let expected_layers = vec!["1".to_string()];
1308 assert_eq!(
1309 expected_layers,
1310 cached_hint_labels(editor),
1311 "Markdown editor should also change independently"
1312 );
1313 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1314 let inlay_cache = editor.inlay_hint_cache();
1315 assert_eq!(inlay_cache.version, 2);
1316 });
1317 }
1318
1319 #[gpui::test]
1320 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1321 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1322 init_test(cx, |settings| {
1323 settings.defaults.inlay_hints = Some(InlayHintSettings {
1324 enabled: true,
1325 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1326 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1327 show_other_hints: allowed_hint_kinds.contains(&None),
1328 })
1329 });
1330
1331 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1332 let lsp_request_count = Arc::new(AtomicU32::new(0));
1333 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1334 fake_server
1335 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1336 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1337 async move {
1338 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1339 assert_eq!(
1340 params.text_document.uri,
1341 lsp::Url::from_file_path(file_with_hints).unwrap(),
1342 );
1343 Ok(Some(vec![
1344 lsp::InlayHint {
1345 position: lsp::Position::new(0, 1),
1346 label: lsp::InlayHintLabel::String("type hint".to_string()),
1347 kind: Some(lsp::InlayHintKind::TYPE),
1348 text_edits: None,
1349 tooltip: None,
1350 padding_left: None,
1351 padding_right: None,
1352 data: None,
1353 },
1354 lsp::InlayHint {
1355 position: lsp::Position::new(0, 2),
1356 label: lsp::InlayHintLabel::String("parameter hint".to_string()),
1357 kind: Some(lsp::InlayHintKind::PARAMETER),
1358 text_edits: None,
1359 tooltip: None,
1360 padding_left: None,
1361 padding_right: None,
1362 data: None,
1363 },
1364 lsp::InlayHint {
1365 position: lsp::Position::new(0, 3),
1366 label: lsp::InlayHintLabel::String("other hint".to_string()),
1367 kind: None,
1368 text_edits: None,
1369 tooltip: None,
1370 padding_left: None,
1371 padding_right: None,
1372 data: None,
1373 },
1374 ]))
1375 }
1376 })
1377 .next()
1378 .await;
1379 cx.foreground().run_until_parked();
1380
1381 let mut edits_made = 1;
1382 editor.update(cx, |editor, cx| {
1383 assert_eq!(
1384 lsp_request_count.load(Ordering::Relaxed),
1385 1,
1386 "Should query new hints once"
1387 );
1388 assert_eq!(
1389 vec![
1390 "other hint".to_string(),
1391 "parameter hint".to_string(),
1392 "type hint".to_string(),
1393 ],
1394 cached_hint_labels(editor),
1395 "Should get its first hints when opening the editor"
1396 );
1397 assert_eq!(
1398 vec!["other hint".to_string(), "type hint".to_string()],
1399 visible_hint_labels(editor, cx)
1400 );
1401 let inlay_cache = editor.inlay_hint_cache();
1402 assert_eq!(
1403 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1404 "Cache should use editor settings to get the allowed hint kinds"
1405 );
1406 assert_eq!(
1407 inlay_cache.version, edits_made,
1408 "The editor update the cache version after every cache/view change"
1409 );
1410 });
1411
1412 fake_server
1413 .request::<lsp::request::InlayHintRefreshRequest>(())
1414 .await
1415 .expect("inlay refresh request failed");
1416 cx.foreground().run_until_parked();
1417 editor.update(cx, |editor, cx| {
1418 assert_eq!(
1419 lsp_request_count.load(Ordering::Relaxed),
1420 2,
1421 "Should load new hints twice"
1422 );
1423 assert_eq!(
1424 vec![
1425 "other hint".to_string(),
1426 "parameter hint".to_string(),
1427 "type hint".to_string(),
1428 ],
1429 cached_hint_labels(editor),
1430 "Cached hints should not change due to allowed hint kinds settings update"
1431 );
1432 assert_eq!(
1433 vec!["other hint".to_string(), "type hint".to_string()],
1434 visible_hint_labels(editor, cx)
1435 );
1436 let inlay_cache = editor.inlay_hint_cache();
1437 assert_eq!(
1438 inlay_cache.version, edits_made,
1439 "Should not update cache version due to new loaded hints being the same"
1440 );
1441 });
1442
1443 for (new_allowed_hint_kinds, expected_visible_hints) in [
1444 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1445 (
1446 HashSet::from_iter([Some(InlayHintKind::Type)]),
1447 vec!["type hint".to_string()],
1448 ),
1449 (
1450 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1451 vec!["parameter hint".to_string()],
1452 ),
1453 (
1454 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1455 vec!["other hint".to_string(), "type hint".to_string()],
1456 ),
1457 (
1458 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1459 vec!["other hint".to_string(), "parameter hint".to_string()],
1460 ),
1461 (
1462 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1463 vec!["parameter hint".to_string(), "type hint".to_string()],
1464 ),
1465 (
1466 HashSet::from_iter([
1467 None,
1468 Some(InlayHintKind::Type),
1469 Some(InlayHintKind::Parameter),
1470 ]),
1471 vec![
1472 "other hint".to_string(),
1473 "parameter hint".to_string(),
1474 "type hint".to_string(),
1475 ],
1476 ),
1477 ] {
1478 edits_made += 1;
1479 update_test_language_settings(cx, |settings| {
1480 settings.defaults.inlay_hints = Some(InlayHintSettings {
1481 enabled: true,
1482 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1483 show_parameter_hints: new_allowed_hint_kinds
1484 .contains(&Some(InlayHintKind::Parameter)),
1485 show_other_hints: new_allowed_hint_kinds.contains(&None),
1486 })
1487 });
1488 cx.foreground().run_until_parked();
1489 editor.update(cx, |editor, cx| {
1490 assert_eq!(
1491 lsp_request_count.load(Ordering::Relaxed),
1492 2,
1493 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1494 );
1495 assert_eq!(
1496 vec![
1497 "other hint".to_string(),
1498 "parameter hint".to_string(),
1499 "type hint".to_string(),
1500 ],
1501 cached_hint_labels(editor),
1502 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1503 );
1504 assert_eq!(
1505 expected_visible_hints,
1506 visible_hint_labels(editor, cx),
1507 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1508 );
1509 let inlay_cache = editor.inlay_hint_cache();
1510 assert_eq!(
1511 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1512 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1513 );
1514 assert_eq!(
1515 inlay_cache.version, edits_made,
1516 "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
1517 );
1518 });
1519 }
1520
1521 edits_made += 1;
1522 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1523 update_test_language_settings(cx, |settings| {
1524 settings.defaults.inlay_hints = Some(InlayHintSettings {
1525 enabled: false,
1526 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1527 show_parameter_hints: another_allowed_hint_kinds
1528 .contains(&Some(InlayHintKind::Parameter)),
1529 show_other_hints: another_allowed_hint_kinds.contains(&None),
1530 })
1531 });
1532 cx.foreground().run_until_parked();
1533 editor.update(cx, |editor, cx| {
1534 assert_eq!(
1535 lsp_request_count.load(Ordering::Relaxed),
1536 2,
1537 "Should not load new hints when hints got disabled"
1538 );
1539 assert!(
1540 cached_hint_labels(editor).is_empty(),
1541 "Should clear the cache when hints got disabled"
1542 );
1543 assert!(
1544 visible_hint_labels(editor, cx).is_empty(),
1545 "Should clear visible hints when hints got disabled"
1546 );
1547 let inlay_cache = editor.inlay_hint_cache();
1548 assert_eq!(
1549 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1550 "Should update its allowed hint kinds even when hints got disabled"
1551 );
1552 assert_eq!(
1553 inlay_cache.version, edits_made,
1554 "The editor should update the cache version after hints got disabled"
1555 );
1556 });
1557
1558 fake_server
1559 .request::<lsp::request::InlayHintRefreshRequest>(())
1560 .await
1561 .expect("inlay refresh request failed");
1562 cx.foreground().run_until_parked();
1563 editor.update(cx, |editor, cx| {
1564 assert_eq!(
1565 lsp_request_count.load(Ordering::Relaxed),
1566 2,
1567 "Should not load new hints when they got disabled"
1568 );
1569 assert!(cached_hint_labels(editor).is_empty());
1570 assert!(visible_hint_labels(editor, cx).is_empty());
1571 let inlay_cache = editor.inlay_hint_cache();
1572 assert_eq!(
1573 inlay_cache.version, edits_made,
1574 "The editor should not update the cache version after /refresh query without updates"
1575 );
1576 });
1577
1578 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1579 edits_made += 1;
1580 update_test_language_settings(cx, |settings| {
1581 settings.defaults.inlay_hints = Some(InlayHintSettings {
1582 enabled: true,
1583 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1584 show_parameter_hints: final_allowed_hint_kinds
1585 .contains(&Some(InlayHintKind::Parameter)),
1586 show_other_hints: final_allowed_hint_kinds.contains(&None),
1587 })
1588 });
1589 cx.foreground().run_until_parked();
1590 editor.update(cx, |editor, cx| {
1591 assert_eq!(
1592 lsp_request_count.load(Ordering::Relaxed),
1593 3,
1594 "Should query for new hints when they got reenabled"
1595 );
1596 assert_eq!(
1597 vec![
1598 "other hint".to_string(),
1599 "parameter hint".to_string(),
1600 "type hint".to_string(),
1601 ],
1602 cached_hint_labels(editor),
1603 "Should get its cached hints fully repopulated after the hints got reenabled"
1604 );
1605 assert_eq!(
1606 vec!["parameter hint".to_string()],
1607 visible_hint_labels(editor, cx),
1608 "Should get its visible hints repopulated and filtered after the h"
1609 );
1610 let inlay_cache = editor.inlay_hint_cache();
1611 assert_eq!(
1612 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
1613 "Cache should update editor settings when hints got reenabled"
1614 );
1615 assert_eq!(
1616 inlay_cache.version, edits_made,
1617 "Cache should update its version after hints got reenabled"
1618 );
1619 });
1620
1621 fake_server
1622 .request::<lsp::request::InlayHintRefreshRequest>(())
1623 .await
1624 .expect("inlay refresh request failed");
1625 cx.foreground().run_until_parked();
1626 editor.update(cx, |editor, cx| {
1627 assert_eq!(
1628 lsp_request_count.load(Ordering::Relaxed),
1629 4,
1630 "Should query for new hints again"
1631 );
1632 assert_eq!(
1633 vec![
1634 "other hint".to_string(),
1635 "parameter hint".to_string(),
1636 "type hint".to_string(),
1637 ],
1638 cached_hint_labels(editor),
1639 );
1640 assert_eq!(
1641 vec!["parameter hint".to_string()],
1642 visible_hint_labels(editor, cx),
1643 );
1644 let inlay_cache = editor.inlay_hint_cache();
1645 assert_eq!(inlay_cache.version, edits_made);
1646 });
1647 }
1648
1649 #[gpui::test]
1650 async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
1651 init_test(cx, |settings| {
1652 settings.defaults.inlay_hints = Some(InlayHintSettings {
1653 enabled: true,
1654 show_type_hints: true,
1655 show_parameter_hints: true,
1656 show_other_hints: true,
1657 })
1658 });
1659
1660 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1661 let fake_server = Arc::new(fake_server);
1662 let lsp_request_count = Arc::new(AtomicU32::new(0));
1663 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1664 fake_server
1665 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1666 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1667 async move {
1668 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1669 assert_eq!(
1670 params.text_document.uri,
1671 lsp::Url::from_file_path(file_with_hints).unwrap(),
1672 );
1673 Ok(Some(vec![lsp::InlayHint {
1674 position: lsp::Position::new(0, i),
1675 label: lsp::InlayHintLabel::String(i.to_string()),
1676 kind: None,
1677 text_edits: None,
1678 tooltip: None,
1679 padding_left: None,
1680 padding_right: None,
1681 data: None,
1682 }]))
1683 }
1684 })
1685 .next()
1686 .await;
1687
1688 let mut expected_changes = Vec::new();
1689 for change_after_opening in [
1690 "initial change #1",
1691 "initial change #2",
1692 "initial change #3",
1693 ] {
1694 editor.update(cx, |editor, cx| {
1695 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1696 editor.handle_input(change_after_opening, cx);
1697 });
1698 expected_changes.push(change_after_opening);
1699 }
1700
1701 cx.foreground().run_until_parked();
1702
1703 editor.update(cx, |editor, cx| {
1704 let current_text = editor.text(cx);
1705 for change in &expected_changes {
1706 assert!(
1707 current_text.contains(change),
1708 "Should apply all changes made"
1709 );
1710 }
1711 assert_eq!(
1712 lsp_request_count.load(Ordering::Relaxed),
1713 2,
1714 "Should query new hints twice: for editor init and for the last edit that interrupted all others"
1715 );
1716 let expected_hints = vec!["2".to_string()];
1717 assert_eq!(
1718 expected_hints,
1719 cached_hint_labels(editor),
1720 "Should get hints from the last edit landed only"
1721 );
1722 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1723 let inlay_cache = editor.inlay_hint_cache();
1724 assert_eq!(
1725 inlay_cache.version, 1,
1726 "Only one update should be registered in the cache after all cancellations"
1727 );
1728 });
1729
1730 let mut edits = Vec::new();
1731 for async_later_change in [
1732 "another change #1",
1733 "another change #2",
1734 "another change #3",
1735 ] {
1736 expected_changes.push(async_later_change);
1737 let task_editor = editor.clone();
1738 let mut task_cx = cx.clone();
1739 edits.push(cx.foreground().spawn(async move {
1740 task_editor.update(&mut task_cx, |editor, cx| {
1741 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1742 editor.handle_input(async_later_change, cx);
1743 });
1744 }));
1745 }
1746 let _ = futures::future::join_all(edits).await;
1747 cx.foreground().run_until_parked();
1748
1749 editor.update(cx, |editor, cx| {
1750 let current_text = editor.text(cx);
1751 for change in &expected_changes {
1752 assert!(
1753 current_text.contains(change),
1754 "Should apply all changes made"
1755 );
1756 }
1757 assert_eq!(
1758 lsp_request_count.load(Ordering::SeqCst),
1759 3,
1760 "Should query new hints one more time, for the last edit only"
1761 );
1762 let expected_hints = vec!["3".to_string()];
1763 assert_eq!(
1764 expected_hints,
1765 cached_hint_labels(editor),
1766 "Should get hints from the last edit landed only"
1767 );
1768 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1769 let inlay_cache = editor.inlay_hint_cache();
1770 assert_eq!(
1771 inlay_cache.version, 2,
1772 "Should update the cache version once more, for the new change"
1773 );
1774 });
1775 }
1776
1777 #[gpui::test]
1778 async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
1779 init_test(cx, |settings| {
1780 settings.defaults.inlay_hints = Some(InlayHintSettings {
1781 enabled: true,
1782 show_type_hints: true,
1783 show_parameter_hints: true,
1784 show_other_hints: true,
1785 })
1786 });
1787
1788 let mut language = Language::new(
1789 LanguageConfig {
1790 name: "Rust".into(),
1791 path_suffixes: vec!["rs".to_string()],
1792 ..Default::default()
1793 },
1794 Some(tree_sitter_rust::language()),
1795 );
1796 let mut fake_servers = language
1797 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1798 capabilities: lsp::ServerCapabilities {
1799 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1800 ..Default::default()
1801 },
1802 ..Default::default()
1803 }))
1804 .await;
1805 let fs = FakeFs::new(cx.background());
1806 fs.insert_tree(
1807 "/a",
1808 json!({
1809 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
1810 "other.rs": "// Test file",
1811 }),
1812 )
1813 .await;
1814 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1815 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
1816 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1817 let worktree_id = workspace.update(cx, |workspace, cx| {
1818 workspace.project().read_with(cx, |project, cx| {
1819 project.worktrees(cx).next().unwrap().read(cx).id()
1820 })
1821 });
1822
1823 let _buffer = project
1824 .update(cx, |project, cx| {
1825 project.open_local_buffer("/a/main.rs", cx)
1826 })
1827 .await
1828 .unwrap();
1829 cx.foreground().run_until_parked();
1830 cx.foreground().start_waiting();
1831 let fake_server = fake_servers.next().await.unwrap();
1832 let editor = workspace
1833 .update(cx, |workspace, cx| {
1834 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1835 })
1836 .await
1837 .unwrap()
1838 .downcast::<Editor>()
1839 .unwrap();
1840 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
1841 let lsp_request_count = Arc::new(AtomicU32::new(0));
1842 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
1843 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
1844 fake_server
1845 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1846 let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
1847 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
1848 async move {
1849 assert_eq!(
1850 params.text_document.uri,
1851 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1852 );
1853
1854 task_lsp_request_ranges.lock().push(params.range);
1855 let query_start = params.range.start;
1856 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1857 Ok(Some(vec![lsp::InlayHint {
1858 position: query_start,
1859 label: lsp::InlayHintLabel::String(i.to_string()),
1860 kind: None,
1861 text_edits: None,
1862 tooltip: None,
1863 padding_left: None,
1864 padding_right: None,
1865 data: None,
1866 }]))
1867 }
1868 })
1869 .next()
1870 .await;
1871 cx.foreground().run_until_parked();
1872 editor.update(cx, |editor, cx| {
1873 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1874 ranges.sort_by_key(|range| range.start);
1875 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");
1876 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1877 assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
1878 assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
1879
1880 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
1881 "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
1882 let expected_layers = vec!["1".to_string(), "2".to_string()];
1883 assert_eq!(
1884 expected_layers,
1885 cached_hint_labels(editor),
1886 "Should have hints from both LSP requests made for a big file"
1887 );
1888 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1889 let inlay_cache = editor.inlay_hint_cache();
1890 assert_eq!(
1891 inlay_cache.version, 2,
1892 "Both LSP queries should've bumped the cache version"
1893 );
1894 });
1895
1896 editor.update(cx, |editor, cx| {
1897 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1898 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1899 editor.change_selections(None, cx, |s| s.select_ranges([600..600]));
1900 editor.handle_input("++++more text++++", cx);
1901 });
1902
1903 cx.foreground().run_until_parked();
1904 editor.update(cx, |editor, cx| {
1905 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1906 ranges.sort_by_key(|range| range.start);
1907 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");
1908 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1909 assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end");
1910 assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning");
1911 assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning");
1912 assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line");
1913 assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent");
1914
1915 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5,
1916 "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints");
1917 let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()];
1918 assert_eq!(expected_layers, cached_hint_labels(editor),
1919 "Should have hints from the new LSP response after edit");
1920 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1921 let inlay_cache = editor.inlay_hint_cache();
1922 assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
1923 });
1924 }
1925
1926 #[gpui::test]
1927 async fn test_multiple_excerpts_large_multibuffer(
1928 deterministic: Arc<Deterministic>,
1929 cx: &mut gpui::TestAppContext,
1930 ) {
1931 init_test(cx, |settings| {
1932 settings.defaults.inlay_hints = Some(InlayHintSettings {
1933 enabled: true,
1934 show_type_hints: true,
1935 show_parameter_hints: true,
1936 show_other_hints: true,
1937 })
1938 });
1939
1940 let mut language = Language::new(
1941 LanguageConfig {
1942 name: "Rust".into(),
1943 path_suffixes: vec!["rs".to_string()],
1944 ..Default::default()
1945 },
1946 Some(tree_sitter_rust::language()),
1947 );
1948 let mut fake_servers = language
1949 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1950 capabilities: lsp::ServerCapabilities {
1951 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1952 ..Default::default()
1953 },
1954 ..Default::default()
1955 }))
1956 .await;
1957 let language = Arc::new(language);
1958 let fs = FakeFs::new(cx.background());
1959 fs.insert_tree(
1960 "/a",
1961 json!({
1962 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
1963 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
1964 }),
1965 )
1966 .await;
1967 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1968 project.update(cx, |project, _| {
1969 project.languages().add(Arc::clone(&language))
1970 });
1971 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1972 let worktree_id = workspace.update(cx, |workspace, cx| {
1973 workspace.project().read_with(cx, |project, cx| {
1974 project.worktrees(cx).next().unwrap().read(cx).id()
1975 })
1976 });
1977
1978 let buffer_1 = project
1979 .update(cx, |project, cx| {
1980 project.open_buffer((worktree_id, "main.rs"), cx)
1981 })
1982 .await
1983 .unwrap();
1984 let buffer_2 = project
1985 .update(cx, |project, cx| {
1986 project.open_buffer((worktree_id, "other.rs"), cx)
1987 })
1988 .await
1989 .unwrap();
1990 let multibuffer = cx.add_model(|cx| {
1991 let mut multibuffer = MultiBuffer::new(0);
1992 multibuffer.push_excerpts(
1993 buffer_1.clone(),
1994 [
1995 ExcerptRange {
1996 context: Point::new(0, 0)..Point::new(2, 0),
1997 primary: None,
1998 },
1999 ExcerptRange {
2000 context: Point::new(4, 0)..Point::new(11, 0),
2001 primary: None,
2002 },
2003 ExcerptRange {
2004 context: Point::new(22, 0)..Point::new(33, 0),
2005 primary: None,
2006 },
2007 ExcerptRange {
2008 context: Point::new(44, 0)..Point::new(55, 0),
2009 primary: None,
2010 },
2011 ExcerptRange {
2012 context: Point::new(56, 0)..Point::new(66, 0),
2013 primary: None,
2014 },
2015 ExcerptRange {
2016 context: Point::new(67, 0)..Point::new(77, 0),
2017 primary: None,
2018 },
2019 ],
2020 cx,
2021 );
2022 multibuffer.push_excerpts(
2023 buffer_2.clone(),
2024 [
2025 ExcerptRange {
2026 context: Point::new(0, 1)..Point::new(2, 1),
2027 primary: None,
2028 },
2029 ExcerptRange {
2030 context: Point::new(4, 1)..Point::new(11, 1),
2031 primary: None,
2032 },
2033 ExcerptRange {
2034 context: Point::new(22, 1)..Point::new(33, 1),
2035 primary: None,
2036 },
2037 ExcerptRange {
2038 context: Point::new(44, 1)..Point::new(55, 1),
2039 primary: None,
2040 },
2041 ExcerptRange {
2042 context: Point::new(56, 1)..Point::new(66, 1),
2043 primary: None,
2044 },
2045 ExcerptRange {
2046 context: Point::new(67, 1)..Point::new(77, 1),
2047 primary: None,
2048 },
2049 ],
2050 cx,
2051 );
2052 multibuffer
2053 });
2054
2055 deterministic.run_until_parked();
2056 cx.foreground().run_until_parked();
2057 let (_, editor) =
2058 cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
2059 let editor_edited = Arc::new(AtomicBool::new(false));
2060 let fake_server = fake_servers.next().await.unwrap();
2061 let closure_editor_edited = Arc::clone(&editor_edited);
2062 fake_server
2063 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2064 let task_editor_edited = Arc::clone(&closure_editor_edited);
2065 async move {
2066 let hint_text = if params.text_document.uri
2067 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2068 {
2069 "main hint"
2070 } else if params.text_document.uri
2071 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2072 {
2073 "other hint"
2074 } else {
2075 panic!("unexpected uri: {:?}", params.text_document.uri);
2076 };
2077
2078 let positions = [
2079 lsp::Position::new(0, 2),
2080 lsp::Position::new(4, 2),
2081 lsp::Position::new(22, 2),
2082 lsp::Position::new(44, 2),
2083 lsp::Position::new(56, 2),
2084 lsp::Position::new(67, 2),
2085 ];
2086 let out_of_range_hint = lsp::InlayHint {
2087 position: lsp::Position::new(
2088 params.range.start.line + 99,
2089 params.range.start.character + 99,
2090 ),
2091 label: lsp::InlayHintLabel::String(
2092 "out of excerpt range, should be ignored".to_string(),
2093 ),
2094 kind: None,
2095 text_edits: None,
2096 tooltip: None,
2097 padding_left: None,
2098 padding_right: None,
2099 data: None,
2100 };
2101
2102 let edited = task_editor_edited.load(Ordering::Acquire);
2103 Ok(Some(
2104 std::iter::once(out_of_range_hint)
2105 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2106 lsp::InlayHint {
2107 position,
2108 label: lsp::InlayHintLabel::String(format!(
2109 "{hint_text}{} #{i}",
2110 if edited { "(edited)" } else { "" },
2111 )),
2112 kind: None,
2113 text_edits: None,
2114 tooltip: None,
2115 padding_left: None,
2116 padding_right: None,
2117 data: None,
2118 }
2119 }))
2120 .collect(),
2121 ))
2122 }
2123 })
2124 .next()
2125 .await;
2126 cx.foreground().run_until_parked();
2127
2128 editor.update(cx, |editor, cx| {
2129 let expected_layers = vec![
2130 "main hint #0".to_string(),
2131 "main hint #1".to_string(),
2132 "main hint #2".to_string(),
2133 "main hint #3".to_string(),
2134 ];
2135 assert_eq!(
2136 expected_layers,
2137 cached_hint_labels(editor),
2138 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2139 );
2140 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2141 let inlay_cache = editor.inlay_hint_cache();
2142 assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
2143 });
2144
2145 editor.update(cx, |editor, cx| {
2146 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2147 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2148 });
2149 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2150 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2151 });
2152 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2153 s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
2154 });
2155 });
2156 cx.foreground().run_until_parked();
2157 editor.update(cx, |editor, cx| {
2158 let expected_layers = vec![
2159 "main hint #0".to_string(),
2160 "main hint #1".to_string(),
2161 "main hint #2".to_string(),
2162 "main hint #3".to_string(),
2163 "main hint #4".to_string(),
2164 "main hint #5".to_string(),
2165 "other hint #0".to_string(),
2166 "other hint #1".to_string(),
2167 "other hint #2".to_string(),
2168 ];
2169 assert_eq!(expected_layers, cached_hint_labels(editor),
2170 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2171 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2172 let inlay_cache = editor.inlay_hint_cache();
2173 assert_eq!(inlay_cache.version, 9);
2174 });
2175
2176 editor.update(cx, |editor, cx| {
2177 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2178 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2179 });
2180 });
2181 cx.foreground().run_until_parked();
2182 editor.update(cx, |editor, cx| {
2183 let expected_layers = vec![
2184 "main hint #0".to_string(),
2185 "main hint #1".to_string(),
2186 "main hint #2".to_string(),
2187 "main hint #3".to_string(),
2188 "main hint #4".to_string(),
2189 "main hint #5".to_string(),
2190 "other hint #0".to_string(),
2191 "other hint #1".to_string(),
2192 "other hint #2".to_string(),
2193 "other hint #3".to_string(),
2194 "other hint #4".to_string(),
2195 "other hint #5".to_string(),
2196 ];
2197 assert_eq!(expected_layers, cached_hint_labels(editor),
2198 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2199 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2200 let inlay_cache = editor.inlay_hint_cache();
2201 assert_eq!(inlay_cache.version, 12);
2202 });
2203
2204 editor.update(cx, |editor, cx| {
2205 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2206 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2207 });
2208 });
2209 cx.foreground().run_until_parked();
2210 editor.update(cx, |editor, cx| {
2211 let expected_layers = vec![
2212 "main hint #0".to_string(),
2213 "main hint #1".to_string(),
2214 "main hint #2".to_string(),
2215 "main hint #3".to_string(),
2216 "main hint #4".to_string(),
2217 "main hint #5".to_string(),
2218 "other hint #0".to_string(),
2219 "other hint #1".to_string(),
2220 "other hint #2".to_string(),
2221 "other hint #3".to_string(),
2222 "other hint #4".to_string(),
2223 "other hint #5".to_string(),
2224 ];
2225 assert_eq!(expected_layers, cached_hint_labels(editor),
2226 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2227 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2228 let inlay_cache = editor.inlay_hint_cache();
2229 assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
2230 });
2231
2232 editor_edited.store(true, Ordering::Release);
2233 editor.update(cx, |editor, cx| {
2234 editor.handle_input("++++more text++++", cx);
2235 });
2236 cx.foreground().run_until_parked();
2237 editor.update(cx, |editor, cx| {
2238 let expected_layers = vec![
2239 "main hint(edited) #0".to_string(),
2240 "main hint(edited) #1".to_string(),
2241 "main hint(edited) #2".to_string(),
2242 "main hint(edited) #3".to_string(),
2243 "other hint #0".to_string(),
2244 "other hint #1".to_string(),
2245 "other hint #2".to_string(),
2246 "other hint #3".to_string(),
2247 "other hint #4".to_string(),
2248 "other hint #5".to_string(),
2249 ];
2250 assert_eq!(expected_layers, cached_hint_labels(editor),
2251 "After multibuffer was edited, hints for the edited buffer (1st) should be invalidated and requeried for all of its visible excerpts, \
2252unedited (2nd) buffer should have the same hint");
2253 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2254 let inlay_cache = editor.inlay_hint_cache();
2255 assert_eq!(inlay_cache.version, 16);
2256 });
2257 }
2258
2259 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
2260 cx.foreground().forbid_parking();
2261
2262 cx.update(|cx| {
2263 cx.set_global(SettingsStore::test(cx));
2264 theme::init((), cx);
2265 client::init_settings(cx);
2266 language::init(cx);
2267 Project::init_settings(cx);
2268 workspace::init_settings(cx);
2269 crate::init(cx);
2270 });
2271
2272 update_test_language_settings(cx, f);
2273 }
2274
2275 async fn prepare_test_objects(
2276 cx: &mut TestAppContext,
2277 ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
2278 let mut language = Language::new(
2279 LanguageConfig {
2280 name: "Rust".into(),
2281 path_suffixes: vec!["rs".to_string()],
2282 ..Default::default()
2283 },
2284 Some(tree_sitter_rust::language()),
2285 );
2286 let mut fake_servers = language
2287 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2288 capabilities: lsp::ServerCapabilities {
2289 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2290 ..Default::default()
2291 },
2292 ..Default::default()
2293 }))
2294 .await;
2295
2296 let fs = FakeFs::new(cx.background());
2297 fs.insert_tree(
2298 "/a",
2299 json!({
2300 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
2301 "other.rs": "// Test file",
2302 }),
2303 )
2304 .await;
2305
2306 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2307 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
2308 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2309 let worktree_id = workspace.update(cx, |workspace, cx| {
2310 workspace.project().read_with(cx, |project, cx| {
2311 project.worktrees(cx).next().unwrap().read(cx).id()
2312 })
2313 });
2314
2315 let _buffer = project
2316 .update(cx, |project, cx| {
2317 project.open_local_buffer("/a/main.rs", cx)
2318 })
2319 .await
2320 .unwrap();
2321 cx.foreground().run_until_parked();
2322 cx.foreground().start_waiting();
2323 let fake_server = fake_servers.next().await.unwrap();
2324 let editor = workspace
2325 .update(cx, |workspace, cx| {
2326 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
2327 })
2328 .await
2329 .unwrap()
2330 .downcast::<Editor>()
2331 .unwrap();
2332
2333 ("/a/main.rs", editor, fake_server)
2334 }
2335
2336 fn cached_hint_labels(editor: &Editor) -> Vec<String> {
2337 let mut labels = Vec::new();
2338 for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
2339 let excerpt_hints = excerpt_hints.read();
2340 for (_, inlay) in excerpt_hints.hints.iter() {
2341 match &inlay.label {
2342 project::InlayHintLabel::String(s) => labels.push(s.to_string()),
2343 _ => unreachable!(),
2344 }
2345 }
2346 }
2347
2348 labels.sort();
2349 labels
2350 }
2351
2352 fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
2353 let mut hints = editor
2354 .visible_inlay_hints(cx)
2355 .into_iter()
2356 .map(|hint| hint.text.to_string())
2357 .collect::<Vec<_>>();
2358 hints.sort();
2359 hints
2360 }
2361}