1use std::{
2 cmp,
3 ops::{ControlFlow, Range},
4 sync::Arc,
5};
6
7use crate::{
8 display_map::Inlay, Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot,
9};
10use anyhow::Context;
11use clock::Global;
12use gpui::{ModelHandle, Task, ViewContext};
13use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
14use log::error;
15use parking_lot::RwLock;
16use project::InlayHint;
17
18use collections::{hash_map, HashMap, HashSet};
19use language::language_settings::InlayHintSettings;
20use util::post_inc;
21
22pub struct InlayHintCache {
23 pub hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
24 pub allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
25 pub version: usize,
26 pub enabled: bool,
27 update_tasks: HashMap<ExcerptId, UpdateTask>,
28}
29
30#[derive(Debug)]
31pub struct CachedExcerptHints {
32 version: usize,
33 buffer_version: Global,
34 buffer_id: u64,
35 pub hints: Vec<(InlayId, InlayHint)>,
36}
37
38#[derive(Debug, Clone, Copy)]
39pub enum InvalidationStrategy {
40 RefreshRequested,
41 BufferEdited,
42 None,
43}
44
45#[derive(Debug, Default)]
46pub struct InlaySplice {
47 pub to_remove: Vec<InlayId>,
48 pub to_insert: Vec<Inlay>,
49}
50
51struct UpdateTask {
52 invalidate: InvalidationStrategy,
53 cache_version: usize,
54 task: RunningTask,
55 pending_refresh: Option<ExcerptQuery>,
56}
57
58struct RunningTask {
59 _task: Task<()>,
60 is_running_rx: smol::channel::Receiver<()>,
61}
62
63#[derive(Debug)]
64struct ExcerptHintsUpdate {
65 excerpt_id: ExcerptId,
66 remove_from_visible: Vec<InlayId>,
67 remove_from_cache: HashSet<InlayId>,
68 add_to_cache: HashSet<InlayHint>,
69}
70
71#[derive(Debug, Clone, Copy)]
72struct ExcerptQuery {
73 buffer_id: u64,
74 excerpt_id: ExcerptId,
75 dimensions: ExcerptDimensions,
76 cache_version: usize,
77 invalidate: InvalidationStrategy,
78}
79
80#[derive(Debug, Clone, Copy)]
81struct ExcerptDimensions {
82 excerpt_range_start: language::Anchor,
83 excerpt_range_end: language::Anchor,
84 excerpt_visible_range_start: language::Anchor,
85 excerpt_visible_range_end: language::Anchor,
86}
87
88struct HintFetchRanges {
89 visible_range: Range<language::Anchor>,
90 other_ranges: Vec<Range<language::Anchor>>,
91}
92
93impl InvalidationStrategy {
94 fn should_invalidate(&self) -> bool {
95 matches!(
96 self,
97 InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
98 )
99 }
100}
101
102impl ExcerptQuery {
103 fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges {
104 let visible_range =
105 self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end;
106 let mut other_ranges = Vec::new();
107 if self
108 .dimensions
109 .excerpt_range_start
110 .cmp(&visible_range.start, buffer)
111 .is_lt()
112 {
113 let mut end = visible_range.start;
114 end.offset -= 1;
115 other_ranges.push(self.dimensions.excerpt_range_start..end);
116 }
117 if self
118 .dimensions
119 .excerpt_range_end
120 .cmp(&visible_range.end, buffer)
121 .is_gt()
122 {
123 let mut start = visible_range.end;
124 start.offset += 1;
125 other_ranges.push(start..self.dimensions.excerpt_range_end);
126 }
127
128 HintFetchRanges {
129 visible_range,
130 other_ranges: other_ranges.into_iter().map(|range| range).collect(),
131 }
132 }
133}
134
135impl InlayHintCache {
136 pub fn new(inlay_hint_settings: InlayHintSettings) -> Self {
137 Self {
138 allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
139 enabled: inlay_hint_settings.enabled,
140 hints: HashMap::default(),
141 update_tasks: HashMap::default(),
142 version: 0,
143 }
144 }
145
146 pub fn update_settings(
147 &mut self,
148 multi_buffer: &ModelHandle<MultiBuffer>,
149 new_hint_settings: InlayHintSettings,
150 visible_hints: Vec<Inlay>,
151 cx: &mut ViewContext<Editor>,
152 ) -> ControlFlow<Option<InlaySplice>> {
153 let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
154 match (self.enabled, new_hint_settings.enabled) {
155 (false, false) => {
156 self.allowed_hint_kinds = new_allowed_hint_kinds;
157 ControlFlow::Break(None)
158 }
159 (true, true) => {
160 if new_allowed_hint_kinds == self.allowed_hint_kinds {
161 ControlFlow::Break(None)
162 } else {
163 let new_splice = self.new_allowed_hint_kinds_splice(
164 multi_buffer,
165 &visible_hints,
166 &new_allowed_hint_kinds,
167 cx,
168 );
169 if new_splice.is_some() {
170 self.version += 1;
171 self.update_tasks.clear();
172 self.allowed_hint_kinds = new_allowed_hint_kinds;
173 }
174 ControlFlow::Break(new_splice)
175 }
176 }
177 (true, false) => {
178 self.enabled = new_hint_settings.enabled;
179 self.allowed_hint_kinds = new_allowed_hint_kinds;
180 if self.hints.is_empty() {
181 ControlFlow::Break(None)
182 } else {
183 self.clear();
184 ControlFlow::Break(Some(InlaySplice {
185 to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
186 to_insert: Vec::new(),
187 }))
188 }
189 }
190 (false, true) => {
191 self.enabled = new_hint_settings.enabled;
192 self.allowed_hint_kinds = new_allowed_hint_kinds;
193 ControlFlow::Continue(())
194 }
195 }
196 }
197
198 pub fn spawn_hint_refresh(
199 &mut self,
200 mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
201 invalidate: InvalidationStrategy,
202 cx: &mut ViewContext<Editor>,
203 ) -> Option<InlaySplice> {
204 if !self.enabled {
205 return None;
206 }
207
208 let update_tasks = &mut self.update_tasks;
209 let mut invalidated_hints = Vec::new();
210 if invalidate.should_invalidate() {
211 let mut changed = false;
212 update_tasks.retain(|task_excerpt_id, _| {
213 let retain = excerpts_to_query.contains_key(task_excerpt_id);
214 changed |= !retain;
215 retain
216 });
217 self.hints.retain(|cached_excerpt, cached_hints| {
218 let retain = excerpts_to_query.contains_key(cached_excerpt);
219 changed |= !retain;
220 if !retain {
221 invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id));
222 }
223 retain
224 });
225 if changed {
226 self.version += 1;
227 }
228 }
229 if excerpts_to_query.is_empty() && invalidated_hints.is_empty() {
230 return None;
231 }
232
233 let cache_version = self.version;
234 excerpts_to_query.retain(|visible_excerpt_id, _| {
235 match update_tasks.entry(*visible_excerpt_id) {
236 hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) {
237 cmp::Ordering::Less => true,
238 cmp::Ordering::Equal => invalidate.should_invalidate(),
239 cmp::Ordering::Greater => false,
240 },
241 hash_map::Entry::Vacant(_) => true,
242 }
243 });
244
245 cx.spawn(|editor, mut cx| async move {
246 editor
247 .update(&mut cx, |editor, cx| {
248 spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx)
249 })
250 .ok();
251 })
252 .detach();
253
254 if invalidated_hints.is_empty() {
255 None
256 } else {
257 Some(InlaySplice {
258 to_remove: invalidated_hints,
259 to_insert: Vec::new(),
260 })
261 }
262 }
263
264 fn new_allowed_hint_kinds_splice(
265 &self,
266 multi_buffer: &ModelHandle<MultiBuffer>,
267 visible_hints: &[Inlay],
268 new_kinds: &HashSet<Option<InlayHintKind>>,
269 cx: &mut ViewContext<Editor>,
270 ) -> Option<InlaySplice> {
271 let old_kinds = &self.allowed_hint_kinds;
272 if new_kinds == old_kinds {
273 return None;
274 }
275
276 let mut to_remove = Vec::new();
277 let mut to_insert = Vec::new();
278 let mut shown_hints_to_remove = visible_hints.iter().fold(
279 HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
280 |mut current_hints, inlay| {
281 current_hints
282 .entry(inlay.position.excerpt_id)
283 .or_default()
284 .push((inlay.position, inlay.id));
285 current_hints
286 },
287 );
288
289 let multi_buffer = multi_buffer.read(cx);
290 let multi_buffer_snapshot = multi_buffer.snapshot(cx);
291
292 for (excerpt_id, excerpt_cached_hints) in &self.hints {
293 let shown_excerpt_hints_to_remove =
294 shown_hints_to_remove.entry(*excerpt_id).or_default();
295 let excerpt_cached_hints = excerpt_cached_hints.read();
296 let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable();
297 shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
298 let Some(buffer) = shown_anchor
299 .buffer_id
300 .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false };
301 let buffer_snapshot = buffer.read(cx).snapshot();
302 loop {
303 match excerpt_cache.peek() {
304 Some((cached_hint_id, cached_hint)) => {
305 if cached_hint_id == shown_hint_id {
306 excerpt_cache.next();
307 return !new_kinds.contains(&cached_hint.kind);
308 }
309
310 match cached_hint
311 .position
312 .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
313 {
314 cmp::Ordering::Less | cmp::Ordering::Equal => {
315 if !old_kinds.contains(&cached_hint.kind)
316 && new_kinds.contains(&cached_hint.kind)
317 {
318 to_insert.push(Inlay::hint(
319 cached_hint_id.id(),
320 multi_buffer_snapshot.anchor_in_excerpt(
321 *excerpt_id,
322 cached_hint.position,
323 ),
324 &cached_hint,
325 ));
326 }
327 excerpt_cache.next();
328 }
329 cmp::Ordering::Greater => return true,
330 }
331 }
332 None => return true,
333 }
334 }
335 });
336
337 for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
338 let cached_hint_kind = maybe_missed_cached_hint.kind;
339 if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
340 to_insert.push(Inlay::hint(
341 cached_hint_id.id(),
342 multi_buffer_snapshot
343 .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
344 &maybe_missed_cached_hint,
345 ));
346 }
347 }
348 }
349
350 to_remove.extend(
351 shown_hints_to_remove
352 .into_values()
353 .flatten()
354 .map(|(_, hint_id)| hint_id),
355 );
356 if to_remove.is_empty() && to_insert.is_empty() {
357 None
358 } else {
359 Some(InlaySplice {
360 to_remove,
361 to_insert,
362 })
363 }
364 }
365
366 fn clear(&mut self) {
367 self.version += 1;
368 self.update_tasks.clear();
369 self.hints.clear();
370 }
371}
372
373fn spawn_new_update_tasks(
374 editor: &mut Editor,
375 excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
376 invalidate: InvalidationStrategy,
377 update_cache_version: usize,
378 cx: &mut ViewContext<'_, '_, Editor>,
379) {
380 let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
381 for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in
382 excerpts_to_query
383 {
384 if excerpt_visible_range.is_empty() {
385 continue;
386 }
387 let buffer = buffer_handle.read(cx);
388 let buffer_snapshot = buffer.snapshot();
389 if buffer_snapshot
390 .version()
391 .changed_since(&new_task_buffer_version)
392 {
393 continue;
394 }
395
396 let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
397 if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
398 let cached_excerpt_hints = cached_excerpt_hints.read();
399 let cached_buffer_version = &cached_excerpt_hints.buffer_version;
400 if cached_excerpt_hints.version > update_cache_version
401 || cached_buffer_version.changed_since(&new_task_buffer_version)
402 {
403 continue;
404 }
405 if !new_task_buffer_version.changed_since(&cached_buffer_version)
406 && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
407 {
408 continue;
409 }
410 };
411
412 let buffer_id = buffer.remote_id();
413 let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
414 let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
415
416 let (multi_buffer_snapshot, full_excerpt_range) =
417 editor.buffer.update(cx, |multi_buffer, cx| {
418 let multi_buffer_snapshot = multi_buffer.snapshot(cx);
419 (
420 multi_buffer_snapshot,
421 multi_buffer
422 .excerpts_for_buffer(&buffer_handle, cx)
423 .into_iter()
424 .find(|(id, _)| id == &excerpt_id)
425 .map(|(_, range)| range.context),
426 )
427 });
428
429 if let Some(full_excerpt_range) = full_excerpt_range {
430 let query = ExcerptQuery {
431 buffer_id,
432 excerpt_id,
433 dimensions: ExcerptDimensions {
434 excerpt_range_start: full_excerpt_range.start,
435 excerpt_range_end: full_excerpt_range.end,
436 excerpt_visible_range_start,
437 excerpt_visible_range_end,
438 },
439 cache_version: update_cache_version,
440 invalidate,
441 };
442
443 let new_update_task = |is_refresh_after_regular_task| {
444 new_update_task(
445 query,
446 multi_buffer_snapshot,
447 buffer_snapshot,
448 Arc::clone(&visible_hints),
449 cached_excerpt_hints,
450 is_refresh_after_regular_task,
451 cx,
452 )
453 };
454 match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
455 hash_map::Entry::Occupied(mut o) => {
456 let update_task = o.get_mut();
457 match (update_task.invalidate, invalidate) {
458 (_, InvalidationStrategy::None) => {}
459 (
460 InvalidationStrategy::BufferEdited,
461 InvalidationStrategy::RefreshRequested,
462 ) if !update_task.task.is_running_rx.is_closed() => {
463 update_task.pending_refresh = Some(query);
464 }
465 _ => {
466 o.insert(UpdateTask {
467 invalidate,
468 cache_version: query.cache_version,
469 task: new_update_task(false),
470 pending_refresh: None,
471 });
472 }
473 }
474 }
475 hash_map::Entry::Vacant(v) => {
476 v.insert(UpdateTask {
477 invalidate,
478 cache_version: query.cache_version,
479 task: new_update_task(false),
480 pending_refresh: None,
481 });
482 }
483 }
484 }
485 }
486}
487
488fn new_update_task(
489 query: ExcerptQuery,
490 multi_buffer_snapshot: MultiBufferSnapshot,
491 buffer_snapshot: BufferSnapshot,
492 visible_hints: Arc<Vec<Inlay>>,
493 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
494 is_refresh_after_regular_task: bool,
495 cx: &mut ViewContext<'_, '_, Editor>,
496) -> RunningTask {
497 let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot);
498 let (is_running_tx, is_running_rx) = smol::channel::bounded(1);
499 let _task = cx.spawn(|editor, mut cx| async move {
500 let _is_running_tx = is_running_tx;
501 let create_update_task = |range| {
502 fetch_and_update_hints(
503 editor.clone(),
504 multi_buffer_snapshot.clone(),
505 buffer_snapshot.clone(),
506 Arc::clone(&visible_hints),
507 cached_excerpt_hints.as_ref().map(Arc::clone),
508 query,
509 range,
510 cx.clone(),
511 )
512 };
513
514 if is_refresh_after_regular_task {
515 let visible_range_has_updates =
516 match create_update_task(hints_fetch_ranges.visible_range).await {
517 Ok(updated) => updated,
518 Err(e) => {
519 error!("inlay hint visible range update task failed: {e:#}");
520 return;
521 }
522 };
523
524 if visible_range_has_updates {
525 let other_update_results = futures::future::join_all(
526 hints_fetch_ranges
527 .other_ranges
528 .into_iter()
529 .map(create_update_task),
530 )
531 .await;
532
533 for result in other_update_results {
534 if let Err(e) = result {
535 error!("inlay hint update task failed: {e:#}");
536 }
537 }
538 }
539 } else {
540 let task_update_results = futures::future::join_all(
541 std::iter::once(hints_fetch_ranges.visible_range)
542 .chain(hints_fetch_ranges.other_ranges.into_iter())
543 .map(create_update_task),
544 )
545 .await;
546
547 for result in task_update_results {
548 if let Err(e) = result {
549 error!("inlay hint update task failed: {e:#}");
550 }
551 }
552 }
553
554 editor
555 .update(&mut cx, |editor, cx| {
556 let pending_refresh_query = editor
557 .inlay_hint_cache
558 .update_tasks
559 .get_mut(&query.excerpt_id)
560 .and_then(|task| task.pending_refresh.take());
561
562 if let Some(pending_refresh_query) = pending_refresh_query {
563 let refresh_multi_buffer = editor.buffer().read(cx);
564 let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx);
565 let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx));
566 let refresh_cached_excerpt_hints = editor
567 .inlay_hint_cache
568 .hints
569 .get(&pending_refresh_query.excerpt_id)
570 .map(Arc::clone);
571 if let Some(buffer) =
572 refresh_multi_buffer.buffer(pending_refresh_query.buffer_id)
573 {
574 editor.inlay_hint_cache.update_tasks.insert(
575 pending_refresh_query.excerpt_id,
576 UpdateTask {
577 invalidate: InvalidationStrategy::RefreshRequested,
578 cache_version: editor.inlay_hint_cache.version,
579 task: new_update_task(
580 pending_refresh_query,
581 refresh_multi_buffer_snapshot,
582 buffer.read(cx).snapshot(),
583 refresh_visible_hints,
584 refresh_cached_excerpt_hints,
585 true,
586 cx,
587 ),
588 pending_refresh: None,
589 },
590 );
591 }
592 }
593 })
594 .ok();
595 });
596
597 RunningTask {
598 _task,
599 is_running_rx,
600 }
601}
602
603async fn fetch_and_update_hints(
604 editor: gpui::WeakViewHandle<Editor>,
605 multi_buffer_snapshot: MultiBufferSnapshot,
606 buffer_snapshot: BufferSnapshot,
607 visible_hints: Arc<Vec<Inlay>>,
608 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
609 query: ExcerptQuery,
610 fetch_range: Range<language::Anchor>,
611 mut cx: gpui::AsyncAppContext,
612) -> anyhow::Result<bool> {
613 let inlay_hints_fetch_task = editor
614 .update(&mut cx, |editor, cx| {
615 editor
616 .buffer()
617 .read(cx)
618 .buffer(query.buffer_id)
619 .and_then(|buffer| {
620 let project = editor.project.as_ref()?;
621 Some(project.update(cx, |project, cx| {
622 project.inlay_hints(buffer, fetch_range.clone(), cx)
623 }))
624 })
625 })
626 .ok()
627 .flatten();
628 let mut update_happened = false;
629 let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) };
630 let new_hints = inlay_hints_fetch_task
631 .await
632 .context("inlay hint fetch task")?;
633 let background_task_buffer_snapshot = buffer_snapshot.clone();
634 let backround_fetch_range = fetch_range.clone();
635 let new_update = cx
636 .background()
637 .spawn(async move {
638 calculate_hint_updates(
639 query,
640 backround_fetch_range,
641 new_hints,
642 &background_task_buffer_snapshot,
643 cached_excerpt_hints,
644 &visible_hints,
645 )
646 })
647 .await;
648
649 editor
650 .update(&mut cx, |editor, cx| {
651 if let Some(new_update) = new_update {
652 update_happened = !new_update.add_to_cache.is_empty()
653 || !new_update.remove_from_cache.is_empty()
654 || !new_update.remove_from_visible.is_empty();
655
656 let cached_excerpt_hints = editor
657 .inlay_hint_cache
658 .hints
659 .entry(new_update.excerpt_id)
660 .or_insert_with(|| {
661 Arc::new(RwLock::new(CachedExcerptHints {
662 version: query.cache_version,
663 buffer_version: buffer_snapshot.version().clone(),
664 buffer_id: query.buffer_id,
665 hints: Vec::new(),
666 }))
667 });
668 let mut cached_excerpt_hints = cached_excerpt_hints.write();
669 match query.cache_version.cmp(&cached_excerpt_hints.version) {
670 cmp::Ordering::Less => return,
671 cmp::Ordering::Greater | cmp::Ordering::Equal => {
672 cached_excerpt_hints.version = query.cache_version;
673 }
674 }
675 cached_excerpt_hints
676 .hints
677 .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
678 cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
679 editor.inlay_hint_cache.version += 1;
680
681 let mut splice = InlaySplice {
682 to_remove: new_update.remove_from_visible,
683 to_insert: Vec::new(),
684 };
685
686 for new_hint in new_update.add_to_cache {
687 let new_hint_position = multi_buffer_snapshot
688 .anchor_in_excerpt(query.excerpt_id, new_hint.position);
689 let new_inlay_id = post_inc(&mut editor.next_inlay_id);
690 if editor
691 .inlay_hint_cache
692 .allowed_hint_kinds
693 .contains(&new_hint.kind)
694 {
695 splice.to_insert.push(Inlay::hint(
696 new_inlay_id,
697 new_hint_position,
698 &new_hint,
699 ));
700 }
701
702 cached_excerpt_hints
703 .hints
704 .push((InlayId::Hint(new_inlay_id), new_hint));
705 }
706
707 cached_excerpt_hints
708 .hints
709 .sort_by(|(_, hint_a), (_, hint_b)| {
710 hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
711 });
712 drop(cached_excerpt_hints);
713
714 if query.invalidate.should_invalidate() {
715 let mut outdated_excerpt_caches = HashSet::default();
716 for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
717 let excerpt_hints = excerpt_hints.read();
718 if excerpt_hints.buffer_id == query.buffer_id
719 && excerpt_id != &query.excerpt_id
720 && buffer_snapshot
721 .version()
722 .changed_since(&excerpt_hints.buffer_version)
723 {
724 outdated_excerpt_caches.insert(*excerpt_id);
725 splice
726 .to_remove
727 .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
728 }
729 }
730 editor
731 .inlay_hint_cache
732 .hints
733 .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
734 }
735
736 let InlaySplice {
737 to_remove,
738 to_insert,
739 } = splice;
740 if !to_remove.is_empty() || !to_insert.is_empty() {
741 editor.splice_inlay_hints(to_remove, to_insert, cx)
742 }
743 }
744 })
745 .ok();
746
747 Ok(update_happened)
748}
749
750fn calculate_hint_updates(
751 query: ExcerptQuery,
752 fetch_range: Range<language::Anchor>,
753 new_excerpt_hints: Vec<InlayHint>,
754 buffer_snapshot: &BufferSnapshot,
755 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
756 visible_hints: &[Inlay],
757) -> Option<ExcerptHintsUpdate> {
758 let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
759 let mut excerpt_hints_to_persist = HashMap::default();
760 for new_hint in new_excerpt_hints {
761 if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
762 continue;
763 }
764 let missing_from_cache = match &cached_excerpt_hints {
765 Some(cached_excerpt_hints) => {
766 let cached_excerpt_hints = cached_excerpt_hints.read();
767 match cached_excerpt_hints.hints.binary_search_by(|probe| {
768 probe.1.position.cmp(&new_hint.position, buffer_snapshot)
769 }) {
770 Ok(ix) => {
771 let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
772 if cached_hint == &new_hint {
773 excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
774 false
775 } else {
776 true
777 }
778 }
779 Err(_) => true,
780 }
781 }
782 None => true,
783 };
784 if missing_from_cache {
785 add_to_cache.insert(new_hint);
786 }
787 }
788
789 let mut remove_from_visible = Vec::new();
790 let mut remove_from_cache = HashSet::default();
791 if query.invalidate.should_invalidate() {
792 remove_from_visible.extend(
793 visible_hints
794 .iter()
795 .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
796 .filter(|hint| {
797 contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
798 })
799 .filter(|hint| {
800 fetch_range
801 .start
802 .cmp(&hint.position.text_anchor, buffer_snapshot)
803 .is_le()
804 && fetch_range
805 .end
806 .cmp(&hint.position.text_anchor, buffer_snapshot)
807 .is_ge()
808 })
809 .map(|inlay_hint| inlay_hint.id)
810 .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
811 );
812
813 if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
814 let cached_excerpt_hints = cached_excerpt_hints.read();
815 remove_from_cache.extend(
816 cached_excerpt_hints
817 .hints
818 .iter()
819 .filter(|(cached_inlay_id, _)| {
820 !excerpt_hints_to_persist.contains_key(cached_inlay_id)
821 })
822 .filter(|(_, cached_hint)| {
823 fetch_range
824 .start
825 .cmp(&cached_hint.position, buffer_snapshot)
826 .is_le()
827 && fetch_range
828 .end
829 .cmp(&cached_hint.position, buffer_snapshot)
830 .is_ge()
831 })
832 .map(|(cached_inlay_id, _)| *cached_inlay_id),
833 );
834 }
835 }
836
837 if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
838 None
839 } else {
840 Some(ExcerptHintsUpdate {
841 excerpt_id: query.excerpt_id,
842 remove_from_visible,
843 remove_from_cache,
844 add_to_cache,
845 })
846 }
847}
848
849fn contains_position(
850 range: &Range<language::Anchor>,
851 position: language::Anchor,
852 buffer_snapshot: &BufferSnapshot,
853) -> bool {
854 range.start.cmp(&position, buffer_snapshot).is_le()
855 && range.end.cmp(&position, buffer_snapshot).is_ge()
856}
857
858#[cfg(test)]
859mod tests {
860 use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
861
862 use crate::{
863 scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
864 serde_json::json,
865 ExcerptRange,
866 };
867 use futures::StreamExt;
868 use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
869 use language::{
870 language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
871 };
872 use lsp::FakeLanguageServer;
873 use parking_lot::Mutex;
874 use project::{FakeFs, Project};
875 use settings::SettingsStore;
876 use text::Point;
877 use workspace::Workspace;
878
879 use crate::editor_tests::update_test_language_settings;
880
881 use super::*;
882
883 #[gpui::test]
884 async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
885 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
886 init_test(cx, |settings| {
887 settings.defaults.inlay_hints = Some(InlayHintSettings {
888 enabled: true,
889 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
890 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
891 show_other_hints: allowed_hint_kinds.contains(&None),
892 })
893 });
894
895 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
896 let lsp_request_count = Arc::new(AtomicU32::new(0));
897 fake_server
898 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
899 let task_lsp_request_count = Arc::clone(&lsp_request_count);
900 async move {
901 assert_eq!(
902 params.text_document.uri,
903 lsp::Url::from_file_path(file_with_hints).unwrap(),
904 );
905 let current_call_id =
906 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
907 let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
908 for _ in 0..2 {
909 let mut i = current_call_id;
910 loop {
911 new_hints.push(lsp::InlayHint {
912 position: lsp::Position::new(0, i),
913 label: lsp::InlayHintLabel::String(i.to_string()),
914 kind: None,
915 text_edits: None,
916 tooltip: None,
917 padding_left: None,
918 padding_right: None,
919 data: None,
920 });
921 if i == 0 {
922 break;
923 }
924 i -= 1;
925 }
926 }
927
928 Ok(Some(new_hints))
929 }
930 })
931 .next()
932 .await;
933 cx.foreground().run_until_parked();
934
935 let mut edits_made = 1;
936 editor.update(cx, |editor, cx| {
937 let expected_layers = vec!["0".to_string()];
938 assert_eq!(
939 expected_layers,
940 cached_hint_labels(editor),
941 "Should get its first hints when opening the editor"
942 );
943 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
944 let inlay_cache = editor.inlay_hint_cache();
945 assert_eq!(
946 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
947 "Cache should use editor settings to get the allowed hint kinds"
948 );
949 assert_eq!(
950 inlay_cache.version, edits_made,
951 "The editor update the cache version after every cache/view change"
952 );
953 });
954
955 editor.update(cx, |editor, cx| {
956 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
957 editor.handle_input("some change", cx);
958 edits_made += 1;
959 });
960 cx.foreground().run_until_parked();
961 editor.update(cx, |editor, cx| {
962 let expected_layers = vec!["0".to_string(), "1".to_string()];
963 assert_eq!(
964 expected_layers,
965 cached_hint_labels(editor),
966 "Should get new hints after an edit"
967 );
968 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
969 let inlay_cache = editor.inlay_hint_cache();
970 assert_eq!(
971 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
972 "Cache should use editor settings to get the allowed hint kinds"
973 );
974 assert_eq!(
975 inlay_cache.version, edits_made,
976 "The editor update the cache version after every cache/view change"
977 );
978 });
979
980 fake_server
981 .request::<lsp::request::InlayHintRefreshRequest>(())
982 .await
983 .expect("inlay refresh request failed");
984 edits_made += 1;
985 cx.foreground().run_until_parked();
986 editor.update(cx, |editor, cx| {
987 let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
988 assert_eq!(
989 expected_layers,
990 cached_hint_labels(editor),
991 "Should get new hints after hint refresh/ request"
992 );
993 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
994 let inlay_cache = editor.inlay_hint_cache();
995 assert_eq!(
996 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
997 "Cache should use editor settings to get the allowed hint kinds"
998 );
999 assert_eq!(
1000 inlay_cache.version, edits_made,
1001 "The editor update the cache version after every cache/view change"
1002 );
1003 });
1004 }
1005
1006 #[gpui::test]
1007 async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
1008 init_test(cx, |settings| {
1009 settings.defaults.inlay_hints = Some(InlayHintSettings {
1010 enabled: true,
1011 show_type_hints: true,
1012 show_parameter_hints: true,
1013 show_other_hints: true,
1014 })
1015 });
1016
1017 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1018 let lsp_request_count = Arc::new(AtomicU32::new(0));
1019 fake_server
1020 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1021 let task_lsp_request_count = Arc::clone(&lsp_request_count);
1022 async move {
1023 assert_eq!(
1024 params.text_document.uri,
1025 lsp::Url::from_file_path(file_with_hints).unwrap(),
1026 );
1027 let current_call_id =
1028 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1029 Ok(Some(vec![lsp::InlayHint {
1030 position: lsp::Position::new(0, current_call_id),
1031 label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1032 kind: None,
1033 text_edits: None,
1034 tooltip: None,
1035 padding_left: None,
1036 padding_right: None,
1037 data: None,
1038 }]))
1039 }
1040 })
1041 .next()
1042 .await;
1043 cx.foreground().run_until_parked();
1044
1045 let mut edits_made = 1;
1046 editor.update(cx, |editor, cx| {
1047 let expected_layers = vec!["0".to_string()];
1048 assert_eq!(
1049 expected_layers,
1050 cached_hint_labels(editor),
1051 "Should get its first hints when opening the editor"
1052 );
1053 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1054 assert_eq!(
1055 editor.inlay_hint_cache().version,
1056 edits_made,
1057 "The editor update the cache version after every cache/view change"
1058 );
1059 });
1060
1061 let progress_token = "test_progress_token";
1062 fake_server
1063 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
1064 token: lsp::ProgressToken::String(progress_token.to_string()),
1065 })
1066 .await
1067 .expect("work done progress create request failed");
1068 cx.foreground().run_until_parked();
1069 fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1070 token: lsp::ProgressToken::String(progress_token.to_string()),
1071 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1072 lsp::WorkDoneProgressBegin::default(),
1073 )),
1074 });
1075 cx.foreground().run_until_parked();
1076
1077 editor.update(cx, |editor, cx| {
1078 let expected_layers = vec!["0".to_string()];
1079 assert_eq!(
1080 expected_layers,
1081 cached_hint_labels(editor),
1082 "Should not update hints while the work task is running"
1083 );
1084 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1085 assert_eq!(
1086 editor.inlay_hint_cache().version,
1087 edits_made,
1088 "Should not update the cache while the work task is running"
1089 );
1090 });
1091
1092 fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1093 token: lsp::ProgressToken::String(progress_token.to_string()),
1094 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1095 lsp::WorkDoneProgressEnd::default(),
1096 )),
1097 });
1098 cx.foreground().run_until_parked();
1099
1100 edits_made += 1;
1101 editor.update(cx, |editor, cx| {
1102 let expected_layers = vec!["1".to_string()];
1103 assert_eq!(
1104 expected_layers,
1105 cached_hint_labels(editor),
1106 "New hints should be queried after the work task is done"
1107 );
1108 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1109 assert_eq!(
1110 editor.inlay_hint_cache().version,
1111 edits_made,
1112 "Cache version should udpate once after the work task is done"
1113 );
1114 });
1115 }
1116
1117 #[gpui::test]
1118 async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1119 init_test(cx, |settings| {
1120 settings.defaults.inlay_hints = Some(InlayHintSettings {
1121 enabled: true,
1122 show_type_hints: true,
1123 show_parameter_hints: true,
1124 show_other_hints: true,
1125 })
1126 });
1127
1128 let fs = FakeFs::new(cx.background());
1129 fs.insert_tree(
1130 "/a",
1131 json!({
1132 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1133 "other.md": "Test md file with some text",
1134 }),
1135 )
1136 .await;
1137 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1138 let workspace = cx
1139 .add_window(|cx| Workspace::test_new(project.clone(), cx))
1140 .root(cx);
1141 let worktree_id = workspace.update(cx, |workspace, cx| {
1142 workspace.project().read_with(cx, |project, cx| {
1143 project.worktrees(cx).next().unwrap().read(cx).id()
1144 })
1145 });
1146
1147 let mut rs_fake_servers = None;
1148 let mut md_fake_servers = None;
1149 for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1150 let mut language = Language::new(
1151 LanguageConfig {
1152 name: name.into(),
1153 path_suffixes: vec![path_suffix.to_string()],
1154 ..Default::default()
1155 },
1156 Some(tree_sitter_rust::language()),
1157 );
1158 let fake_servers = language
1159 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1160 name,
1161 capabilities: lsp::ServerCapabilities {
1162 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1163 ..Default::default()
1164 },
1165 ..Default::default()
1166 }))
1167 .await;
1168 match name {
1169 "Rust" => rs_fake_servers = Some(fake_servers),
1170 "Markdown" => md_fake_servers = Some(fake_servers),
1171 _ => unreachable!(),
1172 }
1173 project.update(cx, |project, _| {
1174 project.languages().add(Arc::new(language));
1175 });
1176 }
1177
1178 let _rs_buffer = project
1179 .update(cx, |project, cx| {
1180 project.open_local_buffer("/a/main.rs", cx)
1181 })
1182 .await
1183 .unwrap();
1184 cx.foreground().run_until_parked();
1185 cx.foreground().start_waiting();
1186 let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1187 let rs_editor = workspace
1188 .update(cx, |workspace, cx| {
1189 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1190 })
1191 .await
1192 .unwrap()
1193 .downcast::<Editor>()
1194 .unwrap();
1195 let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1196 rs_fake_server
1197 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1198 let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
1199 async move {
1200 assert_eq!(
1201 params.text_document.uri,
1202 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1203 );
1204 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1205 Ok(Some(vec![lsp::InlayHint {
1206 position: lsp::Position::new(0, i),
1207 label: lsp::InlayHintLabel::String(i.to_string()),
1208 kind: None,
1209 text_edits: None,
1210 tooltip: None,
1211 padding_left: None,
1212 padding_right: None,
1213 data: None,
1214 }]))
1215 }
1216 })
1217 .next()
1218 .await;
1219 cx.foreground().run_until_parked();
1220 rs_editor.update(cx, |editor, cx| {
1221 let expected_layers = vec!["0".to_string()];
1222 assert_eq!(
1223 expected_layers,
1224 cached_hint_labels(editor),
1225 "Should get its first hints when opening the editor"
1226 );
1227 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1228 assert_eq!(
1229 editor.inlay_hint_cache().version,
1230 1,
1231 "Rust editor update the cache version after every cache/view change"
1232 );
1233 });
1234
1235 cx.foreground().run_until_parked();
1236 let _md_buffer = project
1237 .update(cx, |project, cx| {
1238 project.open_local_buffer("/a/other.md", cx)
1239 })
1240 .await
1241 .unwrap();
1242 cx.foreground().run_until_parked();
1243 cx.foreground().start_waiting();
1244 let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1245 let md_editor = workspace
1246 .update(cx, |workspace, cx| {
1247 workspace.open_path((worktree_id, "other.md"), None, true, cx)
1248 })
1249 .await
1250 .unwrap()
1251 .downcast::<Editor>()
1252 .unwrap();
1253 let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1254 md_fake_server
1255 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1256 let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
1257 async move {
1258 assert_eq!(
1259 params.text_document.uri,
1260 lsp::Url::from_file_path("/a/other.md").unwrap(),
1261 );
1262 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1263 Ok(Some(vec![lsp::InlayHint {
1264 position: lsp::Position::new(0, i),
1265 label: lsp::InlayHintLabel::String(i.to_string()),
1266 kind: None,
1267 text_edits: None,
1268 tooltip: None,
1269 padding_left: None,
1270 padding_right: None,
1271 data: None,
1272 }]))
1273 }
1274 })
1275 .next()
1276 .await;
1277 cx.foreground().run_until_parked();
1278 md_editor.update(cx, |editor, cx| {
1279 let expected_layers = vec!["0".to_string()];
1280 assert_eq!(
1281 expected_layers,
1282 cached_hint_labels(editor),
1283 "Markdown editor should have a separate verison, repeating Rust editor rules"
1284 );
1285 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1286 assert_eq!(editor.inlay_hint_cache().version, 1);
1287 });
1288
1289 rs_editor.update(cx, |editor, cx| {
1290 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1291 editor.handle_input("some rs change", cx);
1292 });
1293 cx.foreground().run_until_parked();
1294 rs_editor.update(cx, |editor, cx| {
1295 let expected_layers = vec!["1".to_string()];
1296 assert_eq!(
1297 expected_layers,
1298 cached_hint_labels(editor),
1299 "Rust inlay cache should change after the edit"
1300 );
1301 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1302 assert_eq!(
1303 editor.inlay_hint_cache().version,
1304 2,
1305 "Every time hint cache changes, cache version should be incremented"
1306 );
1307 });
1308 md_editor.update(cx, |editor, cx| {
1309 let expected_layers = vec!["0".to_string()];
1310 assert_eq!(
1311 expected_layers,
1312 cached_hint_labels(editor),
1313 "Markdown editor should not be affected by Rust editor changes"
1314 );
1315 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1316 assert_eq!(editor.inlay_hint_cache().version, 1);
1317 });
1318
1319 md_editor.update(cx, |editor, cx| {
1320 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1321 editor.handle_input("some md change", cx);
1322 });
1323 cx.foreground().run_until_parked();
1324 md_editor.update(cx, |editor, cx| {
1325 let expected_layers = vec!["1".to_string()];
1326 assert_eq!(
1327 expected_layers,
1328 cached_hint_labels(editor),
1329 "Rust editor should not be affected by Markdown editor changes"
1330 );
1331 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1332 assert_eq!(editor.inlay_hint_cache().version, 2);
1333 });
1334 rs_editor.update(cx, |editor, cx| {
1335 let expected_layers = vec!["1".to_string()];
1336 assert_eq!(
1337 expected_layers,
1338 cached_hint_labels(editor),
1339 "Markdown editor should also change independently"
1340 );
1341 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1342 assert_eq!(editor.inlay_hint_cache().version, 2);
1343 });
1344 }
1345
1346 #[gpui::test]
1347 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1348 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1349 init_test(cx, |settings| {
1350 settings.defaults.inlay_hints = Some(InlayHintSettings {
1351 enabled: true,
1352 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1353 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1354 show_other_hints: allowed_hint_kinds.contains(&None),
1355 })
1356 });
1357
1358 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1359 let lsp_request_count = Arc::new(AtomicU32::new(0));
1360 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1361 fake_server
1362 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1363 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1364 async move {
1365 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1366 assert_eq!(
1367 params.text_document.uri,
1368 lsp::Url::from_file_path(file_with_hints).unwrap(),
1369 );
1370 Ok(Some(vec![
1371 lsp::InlayHint {
1372 position: lsp::Position::new(0, 1),
1373 label: lsp::InlayHintLabel::String("type hint".to_string()),
1374 kind: Some(lsp::InlayHintKind::TYPE),
1375 text_edits: None,
1376 tooltip: None,
1377 padding_left: None,
1378 padding_right: None,
1379 data: None,
1380 },
1381 lsp::InlayHint {
1382 position: lsp::Position::new(0, 2),
1383 label: lsp::InlayHintLabel::String("parameter hint".to_string()),
1384 kind: Some(lsp::InlayHintKind::PARAMETER),
1385 text_edits: None,
1386 tooltip: None,
1387 padding_left: None,
1388 padding_right: None,
1389 data: None,
1390 },
1391 lsp::InlayHint {
1392 position: lsp::Position::new(0, 3),
1393 label: lsp::InlayHintLabel::String("other hint".to_string()),
1394 kind: None,
1395 text_edits: None,
1396 tooltip: None,
1397 padding_left: None,
1398 padding_right: None,
1399 data: None,
1400 },
1401 ]))
1402 }
1403 })
1404 .next()
1405 .await;
1406 cx.foreground().run_until_parked();
1407
1408 let mut edits_made = 1;
1409 editor.update(cx, |editor, cx| {
1410 assert_eq!(
1411 lsp_request_count.load(Ordering::Relaxed),
1412 1,
1413 "Should query new hints once"
1414 );
1415 assert_eq!(
1416 vec![
1417 "other hint".to_string(),
1418 "parameter hint".to_string(),
1419 "type hint".to_string(),
1420 ],
1421 cached_hint_labels(editor),
1422 "Should get its first hints when opening the editor"
1423 );
1424 assert_eq!(
1425 vec!["other hint".to_string(), "type hint".to_string()],
1426 visible_hint_labels(editor, cx)
1427 );
1428 let inlay_cache = editor.inlay_hint_cache();
1429 assert_eq!(
1430 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1431 "Cache should use editor settings to get the allowed hint kinds"
1432 );
1433 assert_eq!(
1434 inlay_cache.version, edits_made,
1435 "The editor update the cache version after every cache/view change"
1436 );
1437 });
1438
1439 fake_server
1440 .request::<lsp::request::InlayHintRefreshRequest>(())
1441 .await
1442 .expect("inlay refresh request failed");
1443 cx.foreground().run_until_parked();
1444 editor.update(cx, |editor, cx| {
1445 assert_eq!(
1446 lsp_request_count.load(Ordering::Relaxed),
1447 2,
1448 "Should load new hints twice"
1449 );
1450 assert_eq!(
1451 vec![
1452 "other hint".to_string(),
1453 "parameter hint".to_string(),
1454 "type hint".to_string(),
1455 ],
1456 cached_hint_labels(editor),
1457 "Cached hints should not change due to allowed hint kinds settings update"
1458 );
1459 assert_eq!(
1460 vec!["other hint".to_string(), "type hint".to_string()],
1461 visible_hint_labels(editor, cx)
1462 );
1463 assert_eq!(
1464 editor.inlay_hint_cache().version,
1465 edits_made,
1466 "Should not update cache version due to new loaded hints being the same"
1467 );
1468 });
1469
1470 for (new_allowed_hint_kinds, expected_visible_hints) in [
1471 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1472 (
1473 HashSet::from_iter([Some(InlayHintKind::Type)]),
1474 vec!["type hint".to_string()],
1475 ),
1476 (
1477 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1478 vec!["parameter hint".to_string()],
1479 ),
1480 (
1481 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1482 vec!["other hint".to_string(), "type hint".to_string()],
1483 ),
1484 (
1485 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1486 vec!["other hint".to_string(), "parameter hint".to_string()],
1487 ),
1488 (
1489 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1490 vec!["parameter hint".to_string(), "type hint".to_string()],
1491 ),
1492 (
1493 HashSet::from_iter([
1494 None,
1495 Some(InlayHintKind::Type),
1496 Some(InlayHintKind::Parameter),
1497 ]),
1498 vec![
1499 "other hint".to_string(),
1500 "parameter hint".to_string(),
1501 "type hint".to_string(),
1502 ],
1503 ),
1504 ] {
1505 edits_made += 1;
1506 update_test_language_settings(cx, |settings| {
1507 settings.defaults.inlay_hints = Some(InlayHintSettings {
1508 enabled: true,
1509 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1510 show_parameter_hints: new_allowed_hint_kinds
1511 .contains(&Some(InlayHintKind::Parameter)),
1512 show_other_hints: new_allowed_hint_kinds.contains(&None),
1513 })
1514 });
1515 cx.foreground().run_until_parked();
1516 editor.update(cx, |editor, cx| {
1517 assert_eq!(
1518 lsp_request_count.load(Ordering::Relaxed),
1519 2,
1520 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1521 );
1522 assert_eq!(
1523 vec![
1524 "other hint".to_string(),
1525 "parameter hint".to_string(),
1526 "type hint".to_string(),
1527 ],
1528 cached_hint_labels(editor),
1529 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1530 );
1531 assert_eq!(
1532 expected_visible_hints,
1533 visible_hint_labels(editor, cx),
1534 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1535 );
1536 let inlay_cache = editor.inlay_hint_cache();
1537 assert_eq!(
1538 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1539 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1540 );
1541 assert_eq!(
1542 inlay_cache.version, edits_made,
1543 "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
1544 );
1545 });
1546 }
1547
1548 edits_made += 1;
1549 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1550 update_test_language_settings(cx, |settings| {
1551 settings.defaults.inlay_hints = Some(InlayHintSettings {
1552 enabled: false,
1553 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1554 show_parameter_hints: another_allowed_hint_kinds
1555 .contains(&Some(InlayHintKind::Parameter)),
1556 show_other_hints: another_allowed_hint_kinds.contains(&None),
1557 })
1558 });
1559 cx.foreground().run_until_parked();
1560 editor.update(cx, |editor, cx| {
1561 assert_eq!(
1562 lsp_request_count.load(Ordering::Relaxed),
1563 2,
1564 "Should not load new hints when hints got disabled"
1565 );
1566 assert!(
1567 cached_hint_labels(editor).is_empty(),
1568 "Should clear the cache when hints got disabled"
1569 );
1570 assert!(
1571 visible_hint_labels(editor, cx).is_empty(),
1572 "Should clear visible hints when hints got disabled"
1573 );
1574 let inlay_cache = editor.inlay_hint_cache();
1575 assert_eq!(
1576 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1577 "Should update its allowed hint kinds even when hints got disabled"
1578 );
1579 assert_eq!(
1580 inlay_cache.version, edits_made,
1581 "The editor should update the cache version after hints got disabled"
1582 );
1583 });
1584
1585 fake_server
1586 .request::<lsp::request::InlayHintRefreshRequest>(())
1587 .await
1588 .expect("inlay refresh request failed");
1589 cx.foreground().run_until_parked();
1590 editor.update(cx, |editor, cx| {
1591 assert_eq!(
1592 lsp_request_count.load(Ordering::Relaxed),
1593 2,
1594 "Should not load new hints when they got disabled"
1595 );
1596 assert!(cached_hint_labels(editor).is_empty());
1597 assert!(visible_hint_labels(editor, cx).is_empty());
1598 assert_eq!(
1599 editor.inlay_hint_cache().version, edits_made,
1600 "The editor should not update the cache version after /refresh query without updates"
1601 );
1602 });
1603
1604 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1605 edits_made += 1;
1606 update_test_language_settings(cx, |settings| {
1607 settings.defaults.inlay_hints = Some(InlayHintSettings {
1608 enabled: true,
1609 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1610 show_parameter_hints: final_allowed_hint_kinds
1611 .contains(&Some(InlayHintKind::Parameter)),
1612 show_other_hints: final_allowed_hint_kinds.contains(&None),
1613 })
1614 });
1615 cx.foreground().run_until_parked();
1616 editor.update(cx, |editor, cx| {
1617 assert_eq!(
1618 lsp_request_count.load(Ordering::Relaxed),
1619 3,
1620 "Should query for new hints when they got reenabled"
1621 );
1622 assert_eq!(
1623 vec![
1624 "other hint".to_string(),
1625 "parameter hint".to_string(),
1626 "type hint".to_string(),
1627 ],
1628 cached_hint_labels(editor),
1629 "Should get its cached hints fully repopulated after the hints got reenabled"
1630 );
1631 assert_eq!(
1632 vec!["parameter hint".to_string()],
1633 visible_hint_labels(editor, cx),
1634 "Should get its visible hints repopulated and filtered after the h"
1635 );
1636 let inlay_cache = editor.inlay_hint_cache();
1637 assert_eq!(
1638 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
1639 "Cache should update editor settings when hints got reenabled"
1640 );
1641 assert_eq!(
1642 inlay_cache.version, edits_made,
1643 "Cache should update its version after hints got reenabled"
1644 );
1645 });
1646
1647 fake_server
1648 .request::<lsp::request::InlayHintRefreshRequest>(())
1649 .await
1650 .expect("inlay refresh request failed");
1651 cx.foreground().run_until_parked();
1652 editor.update(cx, |editor, cx| {
1653 assert_eq!(
1654 lsp_request_count.load(Ordering::Relaxed),
1655 4,
1656 "Should query for new hints again"
1657 );
1658 assert_eq!(
1659 vec![
1660 "other hint".to_string(),
1661 "parameter hint".to_string(),
1662 "type hint".to_string(),
1663 ],
1664 cached_hint_labels(editor),
1665 );
1666 assert_eq!(
1667 vec!["parameter hint".to_string()],
1668 visible_hint_labels(editor, cx),
1669 );
1670 assert_eq!(editor.inlay_hint_cache().version, edits_made);
1671 });
1672 }
1673
1674 #[gpui::test]
1675 async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
1676 init_test(cx, |settings| {
1677 settings.defaults.inlay_hints = Some(InlayHintSettings {
1678 enabled: true,
1679 show_type_hints: true,
1680 show_parameter_hints: true,
1681 show_other_hints: true,
1682 })
1683 });
1684
1685 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1686 let fake_server = Arc::new(fake_server);
1687 let lsp_request_count = Arc::new(AtomicU32::new(0));
1688 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1689 fake_server
1690 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1691 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1692 async move {
1693 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1694 assert_eq!(
1695 params.text_document.uri,
1696 lsp::Url::from_file_path(file_with_hints).unwrap(),
1697 );
1698 Ok(Some(vec![lsp::InlayHint {
1699 position: lsp::Position::new(0, i),
1700 label: lsp::InlayHintLabel::String(i.to_string()),
1701 kind: None,
1702 text_edits: None,
1703 tooltip: None,
1704 padding_left: None,
1705 padding_right: None,
1706 data: None,
1707 }]))
1708 }
1709 })
1710 .next()
1711 .await;
1712
1713 let mut expected_changes = Vec::new();
1714 for change_after_opening in [
1715 "initial change #1",
1716 "initial change #2",
1717 "initial change #3",
1718 ] {
1719 editor.update(cx, |editor, cx| {
1720 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1721 editor.handle_input(change_after_opening, cx);
1722 });
1723 expected_changes.push(change_after_opening);
1724 }
1725
1726 cx.foreground().run_until_parked();
1727
1728 editor.update(cx, |editor, cx| {
1729 let current_text = editor.text(cx);
1730 for change in &expected_changes {
1731 assert!(
1732 current_text.contains(change),
1733 "Should apply all changes made"
1734 );
1735 }
1736 assert_eq!(
1737 lsp_request_count.load(Ordering::Relaxed),
1738 2,
1739 "Should query new hints twice: for editor init and for the last edit that interrupted all others"
1740 );
1741 let expected_hints = vec!["2".to_string()];
1742 assert_eq!(
1743 expected_hints,
1744 cached_hint_labels(editor),
1745 "Should get hints from the last edit landed only"
1746 );
1747 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1748 assert_eq!(
1749 editor.inlay_hint_cache().version, 1,
1750 "Only one update should be registered in the cache after all cancellations"
1751 );
1752 });
1753
1754 let mut edits = Vec::new();
1755 for async_later_change in [
1756 "another change #1",
1757 "another change #2",
1758 "another change #3",
1759 ] {
1760 expected_changes.push(async_later_change);
1761 let task_editor = editor.clone();
1762 let mut task_cx = cx.clone();
1763 edits.push(cx.foreground().spawn(async move {
1764 task_editor.update(&mut task_cx, |editor, cx| {
1765 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1766 editor.handle_input(async_later_change, cx);
1767 });
1768 }));
1769 }
1770 let _ = futures::future::join_all(edits).await;
1771 cx.foreground().run_until_parked();
1772
1773 editor.update(cx, |editor, cx| {
1774 let current_text = editor.text(cx);
1775 for change in &expected_changes {
1776 assert!(
1777 current_text.contains(change),
1778 "Should apply all changes made"
1779 );
1780 }
1781 assert_eq!(
1782 lsp_request_count.load(Ordering::SeqCst),
1783 3,
1784 "Should query new hints one more time, for the last edit only"
1785 );
1786 let expected_hints = vec!["3".to_string()];
1787 assert_eq!(
1788 expected_hints,
1789 cached_hint_labels(editor),
1790 "Should get hints from the last edit landed only"
1791 );
1792 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1793 assert_eq!(
1794 editor.inlay_hint_cache().version,
1795 2,
1796 "Should update the cache version once more, for the new change"
1797 );
1798 });
1799 }
1800
1801 #[gpui::test]
1802 async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
1803 init_test(cx, |settings| {
1804 settings.defaults.inlay_hints = Some(InlayHintSettings {
1805 enabled: true,
1806 show_type_hints: true,
1807 show_parameter_hints: true,
1808 show_other_hints: true,
1809 })
1810 });
1811
1812 let mut language = Language::new(
1813 LanguageConfig {
1814 name: "Rust".into(),
1815 path_suffixes: vec!["rs".to_string()],
1816 ..Default::default()
1817 },
1818 Some(tree_sitter_rust::language()),
1819 );
1820 let mut fake_servers = language
1821 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1822 capabilities: lsp::ServerCapabilities {
1823 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1824 ..Default::default()
1825 },
1826 ..Default::default()
1827 }))
1828 .await;
1829 let fs = FakeFs::new(cx.background());
1830 fs.insert_tree(
1831 "/a",
1832 json!({
1833 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
1834 "other.rs": "// Test file",
1835 }),
1836 )
1837 .await;
1838 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1839 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
1840 let workspace = cx
1841 .add_window(|cx| Workspace::test_new(project.clone(), cx))
1842 .root(cx);
1843 let worktree_id = workspace.update(cx, |workspace, cx| {
1844 workspace.project().read_with(cx, |project, cx| {
1845 project.worktrees(cx).next().unwrap().read(cx).id()
1846 })
1847 });
1848
1849 let _buffer = project
1850 .update(cx, |project, cx| {
1851 project.open_local_buffer("/a/main.rs", cx)
1852 })
1853 .await
1854 .unwrap();
1855 cx.foreground().run_until_parked();
1856 cx.foreground().start_waiting();
1857 let fake_server = fake_servers.next().await.unwrap();
1858 let editor = workspace
1859 .update(cx, |workspace, cx| {
1860 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1861 })
1862 .await
1863 .unwrap()
1864 .downcast::<Editor>()
1865 .unwrap();
1866 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
1867 let lsp_request_count = Arc::new(AtomicU32::new(0));
1868 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
1869 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
1870 fake_server
1871 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1872 let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
1873 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
1874 async move {
1875 assert_eq!(
1876 params.text_document.uri,
1877 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1878 );
1879
1880 task_lsp_request_ranges.lock().push(params.range);
1881 let query_start = params.range.start;
1882 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1883 Ok(Some(vec![lsp::InlayHint {
1884 position: query_start,
1885 label: lsp::InlayHintLabel::String(i.to_string()),
1886 kind: None,
1887 text_edits: None,
1888 tooltip: None,
1889 padding_left: None,
1890 padding_right: None,
1891 data: None,
1892 }]))
1893 }
1894 })
1895 .next()
1896 .await;
1897 cx.foreground().run_until_parked();
1898 editor.update(cx, |editor, cx| {
1899 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1900 ranges.sort_by_key(|range| range.start);
1901 assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
1902 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1903 assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
1904 assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
1905
1906 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
1907 "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
1908 let expected_layers = vec!["1".to_string(), "2".to_string()];
1909 assert_eq!(
1910 expected_layers,
1911 cached_hint_labels(editor),
1912 "Should have hints from both LSP requests made for a big file"
1913 );
1914 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1915 assert_eq!(
1916 editor.inlay_hint_cache().version, 2,
1917 "Both LSP queries should've bumped the cache version"
1918 );
1919 });
1920
1921 editor.update(cx, |editor, cx| {
1922 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1923 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1924 editor.change_selections(None, cx, |s| s.select_ranges([600..600]));
1925 editor.handle_input("++++more text++++", cx);
1926 });
1927
1928 cx.foreground().run_until_parked();
1929 editor.update(cx, |editor, cx| {
1930 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1931 ranges.sort_by_key(|range| range.start);
1932 assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints");
1933 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1934 assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end");
1935 assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning");
1936 assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning");
1937 assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line");
1938 assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent");
1939
1940 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5,
1941 "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints");
1942 let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()];
1943 assert_eq!(expected_layers, cached_hint_labels(editor),
1944 "Should have hints from the new LSP response after edit");
1945 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1946 assert_eq!(editor.inlay_hint_cache().version, 5, "Should update the cache for every LSP response with hints added");
1947 });
1948 }
1949
1950 #[gpui::test]
1951 async fn test_multiple_excerpts_large_multibuffer(
1952 deterministic: Arc<Deterministic>,
1953 cx: &mut gpui::TestAppContext,
1954 ) {
1955 init_test(cx, |settings| {
1956 settings.defaults.inlay_hints = Some(InlayHintSettings {
1957 enabled: true,
1958 show_type_hints: true,
1959 show_parameter_hints: true,
1960 show_other_hints: true,
1961 })
1962 });
1963
1964 let mut language = Language::new(
1965 LanguageConfig {
1966 name: "Rust".into(),
1967 path_suffixes: vec!["rs".to_string()],
1968 ..Default::default()
1969 },
1970 Some(tree_sitter_rust::language()),
1971 );
1972 let mut fake_servers = language
1973 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1974 capabilities: lsp::ServerCapabilities {
1975 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1976 ..Default::default()
1977 },
1978 ..Default::default()
1979 }))
1980 .await;
1981 let language = Arc::new(language);
1982 let fs = FakeFs::new(cx.background());
1983 fs.insert_tree(
1984 "/a",
1985 json!({
1986 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
1987 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
1988 }),
1989 )
1990 .await;
1991 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1992 project.update(cx, |project, _| {
1993 project.languages().add(Arc::clone(&language))
1994 });
1995 let workspace = cx
1996 .add_window(|cx| Workspace::test_new(project.clone(), cx))
1997 .root(cx);
1998 let worktree_id = workspace.update(cx, |workspace, cx| {
1999 workspace.project().read_with(cx, |project, cx| {
2000 project.worktrees(cx).next().unwrap().read(cx).id()
2001 })
2002 });
2003
2004 let buffer_1 = project
2005 .update(cx, |project, cx| {
2006 project.open_buffer((worktree_id, "main.rs"), cx)
2007 })
2008 .await
2009 .unwrap();
2010 let buffer_2 = project
2011 .update(cx, |project, cx| {
2012 project.open_buffer((worktree_id, "other.rs"), cx)
2013 })
2014 .await
2015 .unwrap();
2016 let multibuffer = cx.add_model(|cx| {
2017 let mut multibuffer = MultiBuffer::new(0);
2018 multibuffer.push_excerpts(
2019 buffer_1.clone(),
2020 [
2021 ExcerptRange {
2022 context: Point::new(0, 0)..Point::new(2, 0),
2023 primary: None,
2024 },
2025 ExcerptRange {
2026 context: Point::new(4, 0)..Point::new(11, 0),
2027 primary: None,
2028 },
2029 ExcerptRange {
2030 context: Point::new(22, 0)..Point::new(33, 0),
2031 primary: None,
2032 },
2033 ExcerptRange {
2034 context: Point::new(44, 0)..Point::new(55, 0),
2035 primary: None,
2036 },
2037 ExcerptRange {
2038 context: Point::new(56, 0)..Point::new(66, 0),
2039 primary: None,
2040 },
2041 ExcerptRange {
2042 context: Point::new(67, 0)..Point::new(77, 0),
2043 primary: None,
2044 },
2045 ],
2046 cx,
2047 );
2048 multibuffer.push_excerpts(
2049 buffer_2.clone(),
2050 [
2051 ExcerptRange {
2052 context: Point::new(0, 1)..Point::new(2, 1),
2053 primary: None,
2054 },
2055 ExcerptRange {
2056 context: Point::new(4, 1)..Point::new(11, 1),
2057 primary: None,
2058 },
2059 ExcerptRange {
2060 context: Point::new(22, 1)..Point::new(33, 1),
2061 primary: None,
2062 },
2063 ExcerptRange {
2064 context: Point::new(44, 1)..Point::new(55, 1),
2065 primary: None,
2066 },
2067 ExcerptRange {
2068 context: Point::new(56, 1)..Point::new(66, 1),
2069 primary: None,
2070 },
2071 ExcerptRange {
2072 context: Point::new(67, 1)..Point::new(77, 1),
2073 primary: None,
2074 },
2075 ],
2076 cx,
2077 );
2078 multibuffer
2079 });
2080
2081 deterministic.run_until_parked();
2082 cx.foreground().run_until_parked();
2083 let editor = cx
2084 .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
2085 .root(cx);
2086 let editor_edited = Arc::new(AtomicBool::new(false));
2087 let fake_server = fake_servers.next().await.unwrap();
2088 let closure_editor_edited = Arc::clone(&editor_edited);
2089 fake_server
2090 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2091 let task_editor_edited = Arc::clone(&closure_editor_edited);
2092 async move {
2093 let hint_text = if params.text_document.uri
2094 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2095 {
2096 "main hint"
2097 } else if params.text_document.uri
2098 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2099 {
2100 "other hint"
2101 } else {
2102 panic!("unexpected uri: {:?}", params.text_document.uri);
2103 };
2104
2105 // one hint per excerpt
2106 let positions = [
2107 lsp::Position::new(0, 2),
2108 lsp::Position::new(4, 2),
2109 lsp::Position::new(22, 2),
2110 lsp::Position::new(44, 2),
2111 lsp::Position::new(56, 2),
2112 lsp::Position::new(67, 2),
2113 ];
2114 let out_of_range_hint = lsp::InlayHint {
2115 position: lsp::Position::new(
2116 params.range.start.line + 99,
2117 params.range.start.character + 99,
2118 ),
2119 label: lsp::InlayHintLabel::String(
2120 "out of excerpt range, should be ignored".to_string(),
2121 ),
2122 kind: None,
2123 text_edits: None,
2124 tooltip: None,
2125 padding_left: None,
2126 padding_right: None,
2127 data: None,
2128 };
2129
2130 let edited = task_editor_edited.load(Ordering::Acquire);
2131 Ok(Some(
2132 std::iter::once(out_of_range_hint)
2133 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2134 lsp::InlayHint {
2135 position,
2136 label: lsp::InlayHintLabel::String(format!(
2137 "{hint_text}{} #{i}",
2138 if edited { "(edited)" } else { "" },
2139 )),
2140 kind: None,
2141 text_edits: None,
2142 tooltip: None,
2143 padding_left: None,
2144 padding_right: None,
2145 data: None,
2146 }
2147 }))
2148 .collect(),
2149 ))
2150 }
2151 })
2152 .next()
2153 .await;
2154 cx.foreground().run_until_parked();
2155
2156 editor.update(cx, |editor, cx| {
2157 let expected_layers = vec![
2158 "main hint #0".to_string(),
2159 "main hint #1".to_string(),
2160 "main hint #2".to_string(),
2161 "main hint #3".to_string(),
2162 ];
2163 assert_eq!(
2164 expected_layers,
2165 cached_hint_labels(editor),
2166 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2167 );
2168 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2169 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison");
2170 });
2171
2172 editor.update(cx, |editor, cx| {
2173 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2174 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2175 });
2176 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2177 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2178 });
2179 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2180 s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
2181 });
2182 });
2183 cx.foreground().run_until_parked();
2184 editor.update(cx, |editor, cx| {
2185 let expected_layers = vec![
2186 "main hint #0".to_string(),
2187 "main hint #1".to_string(),
2188 "main hint #2".to_string(),
2189 "main hint #3".to_string(),
2190 "main hint #4".to_string(),
2191 "main hint #5".to_string(),
2192 "other hint #0".to_string(),
2193 "other hint #1".to_string(),
2194 "other hint #2".to_string(),
2195 ];
2196 assert_eq!(expected_layers, cached_hint_labels(editor),
2197 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2198 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2199 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(),
2200 "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
2201 });
2202
2203 editor.update(cx, |editor, cx| {
2204 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2205 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2206 });
2207 });
2208 cx.foreground().run_until_parked();
2209 let last_scroll_update_version = editor.update(cx, |editor, cx| {
2210 let expected_layers = vec![
2211 "main hint #0".to_string(),
2212 "main hint #1".to_string(),
2213 "main hint #2".to_string(),
2214 "main hint #3".to_string(),
2215 "main hint #4".to_string(),
2216 "main hint #5".to_string(),
2217 "other hint #0".to_string(),
2218 "other hint #1".to_string(),
2219 "other hint #2".to_string(),
2220 "other hint #3".to_string(),
2221 "other hint #4".to_string(),
2222 "other hint #5".to_string(),
2223 ];
2224 assert_eq!(expected_layers, cached_hint_labels(editor),
2225 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2226 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2227 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len());
2228 expected_layers.len()
2229 });
2230
2231 editor.update(cx, |editor, cx| {
2232 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2233 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2234 });
2235 });
2236 cx.foreground().run_until_parked();
2237 editor.update(cx, |editor, cx| {
2238 let expected_layers = vec![
2239 "main hint #0".to_string(),
2240 "main hint #1".to_string(),
2241 "main hint #2".to_string(),
2242 "main hint #3".to_string(),
2243 "main hint #4".to_string(),
2244 "main hint #5".to_string(),
2245 "other hint #0".to_string(),
2246 "other hint #1".to_string(),
2247 "other hint #2".to_string(),
2248 "other hint #3".to_string(),
2249 "other hint #4".to_string(),
2250 "other hint #5".to_string(),
2251 ];
2252 assert_eq!(expected_layers, cached_hint_labels(editor),
2253 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2254 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2255 assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
2256 });
2257
2258 editor_edited.store(true, Ordering::Release);
2259 editor.update(cx, |editor, cx| {
2260 editor.change_selections(None, cx, |s| {
2261 s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
2262 });
2263 editor.handle_input("++++more text++++", cx);
2264 });
2265 cx.foreground().run_until_parked();
2266 editor.update(cx, |editor, cx| {
2267 let expected_layers = vec![
2268 "main hint(edited) #0".to_string(),
2269 "main hint(edited) #1".to_string(),
2270 "main hint(edited) #2".to_string(),
2271 "main hint(edited) #3".to_string(),
2272 "main hint(edited) #4".to_string(),
2273 "main hint(edited) #5".to_string(),
2274 "other hint(edited) #0".to_string(),
2275 "other hint(edited) #1".to_string(),
2276 ];
2277 assert_eq!(
2278 expected_layers,
2279 cached_hint_labels(editor),
2280 "After multibuffer edit, editor gets scolled back to the last selection; \
2281all hints should be invalidated and requeried for all of its visible excerpts"
2282 );
2283 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2284 assert_eq!(
2285 editor.inlay_hint_cache().version,
2286 last_scroll_update_version + expected_layers.len() + 1,
2287 "Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal"
2288 );
2289 });
2290 }
2291
2292 #[gpui::test]
2293 async fn test_excerpts_removed(
2294 deterministic: Arc<Deterministic>,
2295 cx: &mut gpui::TestAppContext,
2296 ) {
2297 init_test(cx, |settings| {
2298 settings.defaults.inlay_hints = Some(InlayHintSettings {
2299 enabled: true,
2300 show_type_hints: false,
2301 show_parameter_hints: false,
2302 show_other_hints: false,
2303 })
2304 });
2305
2306 let mut language = Language::new(
2307 LanguageConfig {
2308 name: "Rust".into(),
2309 path_suffixes: vec!["rs".to_string()],
2310 ..Default::default()
2311 },
2312 Some(tree_sitter_rust::language()),
2313 );
2314 let mut fake_servers = language
2315 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2316 capabilities: lsp::ServerCapabilities {
2317 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2318 ..Default::default()
2319 },
2320 ..Default::default()
2321 }))
2322 .await;
2323 let language = Arc::new(language);
2324 let fs = FakeFs::new(cx.background());
2325 fs.insert_tree(
2326 "/a",
2327 json!({
2328 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2329 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2330 }),
2331 )
2332 .await;
2333 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2334 project.update(cx, |project, _| {
2335 project.languages().add(Arc::clone(&language))
2336 });
2337 let workspace = cx
2338 .add_window(|cx| Workspace::test_new(project.clone(), cx))
2339 .root(cx);
2340 let worktree_id = workspace.update(cx, |workspace, cx| {
2341 workspace.project().read_with(cx, |project, cx| {
2342 project.worktrees(cx).next().unwrap().read(cx).id()
2343 })
2344 });
2345
2346 let buffer_1 = project
2347 .update(cx, |project, cx| {
2348 project.open_buffer((worktree_id, "main.rs"), cx)
2349 })
2350 .await
2351 .unwrap();
2352 let buffer_2 = project
2353 .update(cx, |project, cx| {
2354 project.open_buffer((worktree_id, "other.rs"), cx)
2355 })
2356 .await
2357 .unwrap();
2358 let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
2359 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2360 let buffer_1_excerpts = multibuffer.push_excerpts(
2361 buffer_1.clone(),
2362 [ExcerptRange {
2363 context: Point::new(0, 0)..Point::new(2, 0),
2364 primary: None,
2365 }],
2366 cx,
2367 );
2368 let buffer_2_excerpts = multibuffer.push_excerpts(
2369 buffer_2.clone(),
2370 [ExcerptRange {
2371 context: Point::new(0, 1)..Point::new(2, 1),
2372 primary: None,
2373 }],
2374 cx,
2375 );
2376 (buffer_1_excerpts, buffer_2_excerpts)
2377 });
2378
2379 assert!(!buffer_1_excerpts.is_empty());
2380 assert!(!buffer_2_excerpts.is_empty());
2381
2382 deterministic.run_until_parked();
2383 cx.foreground().run_until_parked();
2384 let editor = cx
2385 .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
2386 .root(cx);
2387 let editor_edited = Arc::new(AtomicBool::new(false));
2388 let fake_server = fake_servers.next().await.unwrap();
2389 let closure_editor_edited = Arc::clone(&editor_edited);
2390 fake_server
2391 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2392 let task_editor_edited = Arc::clone(&closure_editor_edited);
2393 async move {
2394 let hint_text = if params.text_document.uri
2395 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2396 {
2397 "main hint"
2398 } else if params.text_document.uri
2399 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2400 {
2401 "other hint"
2402 } else {
2403 panic!("unexpected uri: {:?}", params.text_document.uri);
2404 };
2405
2406 let positions = [
2407 lsp::Position::new(0, 2),
2408 lsp::Position::new(4, 2),
2409 lsp::Position::new(22, 2),
2410 lsp::Position::new(44, 2),
2411 lsp::Position::new(56, 2),
2412 lsp::Position::new(67, 2),
2413 ];
2414 let out_of_range_hint = lsp::InlayHint {
2415 position: lsp::Position::new(
2416 params.range.start.line + 99,
2417 params.range.start.character + 99,
2418 ),
2419 label: lsp::InlayHintLabel::String(
2420 "out of excerpt range, should be ignored".to_string(),
2421 ),
2422 kind: None,
2423 text_edits: None,
2424 tooltip: None,
2425 padding_left: None,
2426 padding_right: None,
2427 data: None,
2428 };
2429
2430 let edited = task_editor_edited.load(Ordering::Acquire);
2431 Ok(Some(
2432 std::iter::once(out_of_range_hint)
2433 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2434 lsp::InlayHint {
2435 position,
2436 label: lsp::InlayHintLabel::String(format!(
2437 "{hint_text}{} #{i}",
2438 if edited { "(edited)" } else { "" },
2439 )),
2440 kind: None,
2441 text_edits: None,
2442 tooltip: None,
2443 padding_left: None,
2444 padding_right: None,
2445 data: None,
2446 }
2447 }))
2448 .collect(),
2449 ))
2450 }
2451 })
2452 .next()
2453 .await;
2454 cx.foreground().run_until_parked();
2455
2456 editor.update(cx, |editor, cx| {
2457 assert_eq!(
2458 vec!["main hint #0".to_string(), "other hint #0".to_string()],
2459 cached_hint_labels(editor),
2460 "Cache should update for both excerpts despite hints display was disabled"
2461 );
2462 assert!(
2463 visible_hint_labels(editor, cx).is_empty(),
2464 "All hints are disabled and should not be shown despite being present in the cache"
2465 );
2466 assert_eq!(
2467 editor.inlay_hint_cache().version,
2468 2,
2469 "Cache should update once per excerpt query"
2470 );
2471 });
2472
2473 editor.update(cx, |editor, cx| {
2474 editor.buffer().update(cx, |multibuffer, cx| {
2475 multibuffer.remove_excerpts(buffer_2_excerpts, cx)
2476 })
2477 });
2478 cx.foreground().run_until_parked();
2479 editor.update(cx, |editor, cx| {
2480 assert_eq!(
2481 vec!["main hint #0".to_string()],
2482 cached_hint_labels(editor),
2483 "For the removed excerpt, should clean corresponding cached hints"
2484 );
2485 assert!(
2486 visible_hint_labels(editor, cx).is_empty(),
2487 "All hints are disabled and should not be shown despite being present in the cache"
2488 );
2489 assert_eq!(
2490 editor.inlay_hint_cache().version,
2491 3,
2492 "Excerpt removal should trigger cache update"
2493 );
2494 });
2495
2496 update_test_language_settings(cx, |settings| {
2497 settings.defaults.inlay_hints = Some(InlayHintSettings {
2498 enabled: true,
2499 show_type_hints: true,
2500 show_parameter_hints: true,
2501 show_other_hints: true,
2502 })
2503 });
2504 cx.foreground().run_until_parked();
2505 editor.update(cx, |editor, cx| {
2506 let expected_hints = vec!["main hint #0".to_string()];
2507 assert_eq!(
2508 expected_hints,
2509 cached_hint_labels(editor),
2510 "Hint display settings change should not change the cache"
2511 );
2512 assert_eq!(
2513 expected_hints,
2514 visible_hint_labels(editor, cx),
2515 "Settings change should make cached hints visible"
2516 );
2517 assert_eq!(
2518 editor.inlay_hint_cache().version,
2519 4,
2520 "Settings change should trigger cache update"
2521 );
2522 });
2523 }
2524
2525 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
2526 cx.foreground().forbid_parking();
2527
2528 cx.update(|cx| {
2529 cx.set_global(SettingsStore::test(cx));
2530 theme::init((), cx);
2531 client::init_settings(cx);
2532 language::init(cx);
2533 Project::init_settings(cx);
2534 workspace::init_settings(cx);
2535 crate::init(cx);
2536 });
2537
2538 update_test_language_settings(cx, f);
2539 }
2540
2541 async fn prepare_test_objects(
2542 cx: &mut TestAppContext,
2543 ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
2544 let mut language = Language::new(
2545 LanguageConfig {
2546 name: "Rust".into(),
2547 path_suffixes: vec!["rs".to_string()],
2548 ..Default::default()
2549 },
2550 Some(tree_sitter_rust::language()),
2551 );
2552 let mut fake_servers = language
2553 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2554 capabilities: lsp::ServerCapabilities {
2555 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2556 ..Default::default()
2557 },
2558 ..Default::default()
2559 }))
2560 .await;
2561
2562 let fs = FakeFs::new(cx.background());
2563 fs.insert_tree(
2564 "/a",
2565 json!({
2566 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
2567 "other.rs": "// Test file",
2568 }),
2569 )
2570 .await;
2571
2572 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2573 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
2574 let workspace = cx
2575 .add_window(|cx| Workspace::test_new(project.clone(), cx))
2576 .root(cx);
2577 let worktree_id = workspace.update(cx, |workspace, cx| {
2578 workspace.project().read_with(cx, |project, cx| {
2579 project.worktrees(cx).next().unwrap().read(cx).id()
2580 })
2581 });
2582
2583 let _buffer = project
2584 .update(cx, |project, cx| {
2585 project.open_local_buffer("/a/main.rs", cx)
2586 })
2587 .await
2588 .unwrap();
2589 cx.foreground().run_until_parked();
2590 cx.foreground().start_waiting();
2591 let fake_server = fake_servers.next().await.unwrap();
2592 let editor = workspace
2593 .update(cx, |workspace, cx| {
2594 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
2595 })
2596 .await
2597 .unwrap()
2598 .downcast::<Editor>()
2599 .unwrap();
2600
2601 ("/a/main.rs", editor, fake_server)
2602 }
2603
2604 fn cached_hint_labels(editor: &Editor) -> Vec<String> {
2605 let mut labels = Vec::new();
2606 for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
2607 let excerpt_hints = excerpt_hints.read();
2608 for (_, inlay) in excerpt_hints.hints.iter() {
2609 match &inlay.label {
2610 project::InlayHintLabel::String(s) => labels.push(s.to_string()),
2611 _ => unreachable!(),
2612 }
2613 }
2614 }
2615
2616 labels.sort();
2617 labels
2618 }
2619
2620 fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
2621 let mut hints = editor
2622 .visible_inlay_hints(cx)
2623 .into_iter()
2624 .map(|hint| hint.text.to_string())
2625 .collect::<Vec<_>>();
2626 hints.sort();
2627 hints
2628 }
2629}