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