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