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