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