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