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