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