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