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