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