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 drop(refresh_multi_buffer);
575 editor.inlay_hint_cache.update_tasks.insert(
576 pending_refresh_query.excerpt_id,
577 UpdateTask {
578 invalidate: InvalidationStrategy::RefreshRequested,
579 cache_version: editor.inlay_hint_cache.version,
580 task: new_update_task(
581 pending_refresh_query,
582 refresh_multi_buffer_snapshot,
583 buffer.read(cx).snapshot(),
584 refresh_visible_hints,
585 refresh_cached_excerpt_hints,
586 true,
587 cx,
588 ),
589 pending_refresh: None,
590 },
591 );
592 }
593 }
594 })
595 .ok();
596 });
597
598 RunningTask {
599 _task,
600 is_running_rx,
601 }
602}
603
604async fn fetch_and_update_hints(
605 editor: gpui::WeakViewHandle<Editor>,
606 multi_buffer_snapshot: MultiBufferSnapshot,
607 buffer_snapshot: BufferSnapshot,
608 visible_hints: Arc<Vec<Inlay>>,
609 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
610 query: ExcerptQuery,
611 fetch_range: Range<language::Anchor>,
612 mut cx: gpui::AsyncAppContext,
613) -> anyhow::Result<bool> {
614 let inlay_hints_fetch_task = editor
615 .update(&mut cx, |editor, cx| {
616 editor
617 .buffer()
618 .read(cx)
619 .buffer(query.buffer_id)
620 .and_then(|buffer| {
621 let project = editor.project.as_ref()?;
622 Some(project.update(cx, |project, cx| {
623 project.inlay_hints(buffer, fetch_range.clone(), cx)
624 }))
625 })
626 })
627 .ok()
628 .flatten();
629 let mut update_happened = false;
630 let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) };
631 let new_hints = inlay_hints_fetch_task
632 .await
633 .context("inlay hint fetch task")?;
634 let background_task_buffer_snapshot = buffer_snapshot.clone();
635 let backround_fetch_range = fetch_range.clone();
636 let new_update = cx
637 .background()
638 .spawn(async move {
639 calculate_hint_updates(
640 query,
641 backround_fetch_range,
642 new_hints,
643 &background_task_buffer_snapshot,
644 cached_excerpt_hints,
645 &visible_hints,
646 )
647 })
648 .await;
649
650 editor
651 .update(&mut cx, |editor, cx| {
652 if let Some(new_update) = new_update {
653 update_happened = !new_update.add_to_cache.is_empty()
654 || !new_update.remove_from_cache.is_empty()
655 || !new_update.remove_from_visible.is_empty();
656
657 let cached_excerpt_hints = editor
658 .inlay_hint_cache
659 .hints
660 .entry(new_update.excerpt_id)
661 .or_insert_with(|| {
662 Arc::new(RwLock::new(CachedExcerptHints {
663 version: query.cache_version,
664 buffer_version: buffer_snapshot.version().clone(),
665 buffer_id: query.buffer_id,
666 hints: Vec::new(),
667 }))
668 });
669 let mut cached_excerpt_hints = cached_excerpt_hints.write();
670 match query.cache_version.cmp(&cached_excerpt_hints.version) {
671 cmp::Ordering::Less => return,
672 cmp::Ordering::Greater | cmp::Ordering::Equal => {
673 cached_excerpt_hints.version = query.cache_version;
674 }
675 }
676 cached_excerpt_hints
677 .hints
678 .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
679 cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
680 editor.inlay_hint_cache.version += 1;
681
682 let mut splice = InlaySplice {
683 to_remove: new_update.remove_from_visible,
684 to_insert: Vec::new(),
685 };
686
687 for new_hint in new_update.add_to_cache {
688 let new_hint_position = multi_buffer_snapshot
689 .anchor_in_excerpt(query.excerpt_id, new_hint.position);
690 let new_inlay_id = post_inc(&mut editor.next_inlay_id);
691 if editor
692 .inlay_hint_cache
693 .allowed_hint_kinds
694 .contains(&new_hint.kind)
695 {
696 splice.to_insert.push(Inlay::hint(
697 new_inlay_id,
698 new_hint_position,
699 &new_hint,
700 ));
701 }
702
703 cached_excerpt_hints
704 .hints
705 .push((InlayId::Hint(new_inlay_id), new_hint));
706 }
707
708 cached_excerpt_hints
709 .hints
710 .sort_by(|(_, hint_a), (_, hint_b)| {
711 hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
712 });
713 drop(cached_excerpt_hints);
714
715 if query.invalidate.should_invalidate() {
716 let mut outdated_excerpt_caches = HashSet::default();
717 for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
718 let excerpt_hints = excerpt_hints.read();
719 if excerpt_hints.buffer_id == query.buffer_id
720 && excerpt_id != &query.excerpt_id
721 && buffer_snapshot
722 .version()
723 .changed_since(&excerpt_hints.buffer_version)
724 {
725 outdated_excerpt_caches.insert(*excerpt_id);
726 splice
727 .to_remove
728 .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
729 }
730 }
731 editor
732 .inlay_hint_cache
733 .hints
734 .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
735 }
736
737 let InlaySplice {
738 to_remove,
739 to_insert,
740 } = splice;
741 if !to_remove.is_empty() || !to_insert.is_empty() {
742 editor.splice_inlay_hints(to_remove, to_insert, cx)
743 }
744 }
745 })
746 .ok();
747
748 Ok(update_happened)
749}
750
751fn calculate_hint_updates(
752 query: ExcerptQuery,
753 fetch_range: Range<language::Anchor>,
754 new_excerpt_hints: Vec<InlayHint>,
755 buffer_snapshot: &BufferSnapshot,
756 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
757 visible_hints: &[Inlay],
758) -> Option<ExcerptHintsUpdate> {
759 let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
760 let mut excerpt_hints_to_persist = HashMap::default();
761 for new_hint in new_excerpt_hints {
762 if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
763 continue;
764 }
765 let missing_from_cache = match &cached_excerpt_hints {
766 Some(cached_excerpt_hints) => {
767 let cached_excerpt_hints = cached_excerpt_hints.read();
768 match cached_excerpt_hints.hints.binary_search_by(|probe| {
769 probe.1.position.cmp(&new_hint.position, buffer_snapshot)
770 }) {
771 Ok(ix) => {
772 let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
773 if cached_hint == &new_hint {
774 excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
775 false
776 } else {
777 true
778 }
779 }
780 Err(_) => true,
781 }
782 }
783 None => true,
784 };
785 if missing_from_cache {
786 add_to_cache.insert(new_hint);
787 }
788 }
789
790 let mut remove_from_visible = Vec::new();
791 let mut remove_from_cache = HashSet::default();
792 if query.invalidate.should_invalidate() {
793 remove_from_visible.extend(
794 visible_hints
795 .iter()
796 .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
797 .filter(|hint| {
798 contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
799 })
800 .filter(|hint| {
801 fetch_range
802 .start
803 .cmp(&hint.position.text_anchor, buffer_snapshot)
804 .is_le()
805 && fetch_range
806 .end
807 .cmp(&hint.position.text_anchor, buffer_snapshot)
808 .is_ge()
809 })
810 .map(|inlay_hint| inlay_hint.id)
811 .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
812 );
813
814 if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
815 let cached_excerpt_hints = cached_excerpt_hints.read();
816 remove_from_cache.extend(
817 cached_excerpt_hints
818 .hints
819 .iter()
820 .filter(|(cached_inlay_id, _)| {
821 !excerpt_hints_to_persist.contains_key(cached_inlay_id)
822 })
823 .filter(|(_, cached_hint)| {
824 fetch_range
825 .start
826 .cmp(&cached_hint.position, buffer_snapshot)
827 .is_le()
828 && fetch_range
829 .end
830 .cmp(&cached_hint.position, buffer_snapshot)
831 .is_ge()
832 })
833 .map(|(cached_inlay_id, _)| *cached_inlay_id),
834 );
835 }
836 }
837
838 if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
839 None
840 } else {
841 Some(ExcerptHintsUpdate {
842 excerpt_id: query.excerpt_id,
843 remove_from_visible,
844 remove_from_cache,
845 add_to_cache,
846 })
847 }
848}
849
850fn contains_position(
851 range: &Range<language::Anchor>,
852 position: language::Anchor,
853 buffer_snapshot: &BufferSnapshot,
854) -> bool {
855 range.start.cmp(&position, buffer_snapshot).is_le()
856 && range.end.cmp(&position, buffer_snapshot).is_ge()
857}
858
859#[cfg(test)]
860mod tests {
861 use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
862
863 use crate::{
864 scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
865 serde_json::json,
866 ExcerptRange,
867 };
868 use futures::StreamExt;
869 use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
870 use language::{
871 language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
872 };
873 use lsp::FakeLanguageServer;
874 use parking_lot::Mutex;
875 use project::{FakeFs, Project};
876 use settings::SettingsStore;
877 use text::Point;
878 use workspace::Workspace;
879
880 use crate::editor_tests::update_test_language_settings;
881
882 use super::*;
883
884 #[gpui::test]
885 async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
886 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
887 init_test(cx, |settings| {
888 settings.defaults.inlay_hints = Some(InlayHintSettings {
889 enabled: true,
890 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
891 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
892 show_other_hints: allowed_hint_kinds.contains(&None),
893 })
894 });
895
896 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
897 let lsp_request_count = Arc::new(AtomicU32::new(0));
898 fake_server
899 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
900 let task_lsp_request_count = Arc::clone(&lsp_request_count);
901 async move {
902 assert_eq!(
903 params.text_document.uri,
904 lsp::Url::from_file_path(file_with_hints).unwrap(),
905 );
906 let current_call_id =
907 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
908 let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
909 for _ in 0..2 {
910 let mut i = current_call_id;
911 loop {
912 new_hints.push(lsp::InlayHint {
913 position: lsp::Position::new(0, i),
914 label: lsp::InlayHintLabel::String(i.to_string()),
915 kind: None,
916 text_edits: None,
917 tooltip: None,
918 padding_left: None,
919 padding_right: None,
920 data: None,
921 });
922 if i == 0 {
923 break;
924 }
925 i -= 1;
926 }
927 }
928
929 Ok(Some(new_hints))
930 }
931 })
932 .next()
933 .await;
934 cx.foreground().run_until_parked();
935
936 let mut edits_made = 1;
937 editor.update(cx, |editor, cx| {
938 let expected_layers = vec!["0".to_string()];
939 assert_eq!(
940 expected_layers,
941 cached_hint_labels(editor),
942 "Should get its first hints when opening the editor"
943 );
944 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
945 let inlay_cache = editor.inlay_hint_cache();
946 assert_eq!(
947 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
948 "Cache should use editor settings to get the allowed hint kinds"
949 );
950 assert_eq!(
951 inlay_cache.version, edits_made,
952 "The editor update the cache version after every cache/view change"
953 );
954 });
955
956 editor.update(cx, |editor, cx| {
957 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
958 editor.handle_input("some change", cx);
959 edits_made += 1;
960 });
961 cx.foreground().run_until_parked();
962 editor.update(cx, |editor, cx| {
963 let expected_layers = vec!["0".to_string(), "1".to_string()];
964 assert_eq!(
965 expected_layers,
966 cached_hint_labels(editor),
967 "Should get new hints after an edit"
968 );
969 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
970 let inlay_cache = editor.inlay_hint_cache();
971 assert_eq!(
972 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
973 "Cache should use editor settings to get the allowed hint kinds"
974 );
975 assert_eq!(
976 inlay_cache.version, edits_made,
977 "The editor update the cache version after every cache/view change"
978 );
979 });
980
981 fake_server
982 .request::<lsp::request::InlayHintRefreshRequest>(())
983 .await
984 .expect("inlay refresh request failed");
985 edits_made += 1;
986 cx.foreground().run_until_parked();
987 editor.update(cx, |editor, cx| {
988 let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
989 assert_eq!(
990 expected_layers,
991 cached_hint_labels(editor),
992 "Should get new hints after hint refresh/ request"
993 );
994 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
995 let inlay_cache = editor.inlay_hint_cache();
996 assert_eq!(
997 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
998 "Cache should use editor settings to get the allowed hint kinds"
999 );
1000 assert_eq!(
1001 inlay_cache.version, edits_made,
1002 "The editor update the cache version after every cache/view change"
1003 );
1004 });
1005 }
1006
1007 #[gpui::test]
1008 async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
1009 init_test(cx, |settings| {
1010 settings.defaults.inlay_hints = Some(InlayHintSettings {
1011 enabled: true,
1012 show_type_hints: true,
1013 show_parameter_hints: true,
1014 show_other_hints: true,
1015 })
1016 });
1017
1018 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1019 let lsp_request_count = Arc::new(AtomicU32::new(0));
1020 fake_server
1021 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1022 let task_lsp_request_count = Arc::clone(&lsp_request_count);
1023 async move {
1024 assert_eq!(
1025 params.text_document.uri,
1026 lsp::Url::from_file_path(file_with_hints).unwrap(),
1027 );
1028 let current_call_id =
1029 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1030 Ok(Some(vec![lsp::InlayHint {
1031 position: lsp::Position::new(0, current_call_id),
1032 label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1033 kind: None,
1034 text_edits: None,
1035 tooltip: None,
1036 padding_left: None,
1037 padding_right: None,
1038 data: None,
1039 }]))
1040 }
1041 })
1042 .next()
1043 .await;
1044 cx.foreground().run_until_parked();
1045
1046 let mut edits_made = 1;
1047 editor.update(cx, |editor, cx| {
1048 let expected_layers = vec!["0".to_string()];
1049 assert_eq!(
1050 expected_layers,
1051 cached_hint_labels(editor),
1052 "Should get its first hints when opening the editor"
1053 );
1054 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1055 assert_eq!(
1056 editor.inlay_hint_cache().version,
1057 edits_made,
1058 "The editor update the cache version after every cache/view change"
1059 );
1060 });
1061
1062 let progress_token = "test_progress_token";
1063 fake_server
1064 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
1065 token: lsp::ProgressToken::String(progress_token.to_string()),
1066 })
1067 .await
1068 .expect("work done progress create request failed");
1069 cx.foreground().run_until_parked();
1070 fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1071 token: lsp::ProgressToken::String(progress_token.to_string()),
1072 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1073 lsp::WorkDoneProgressBegin::default(),
1074 )),
1075 });
1076 cx.foreground().run_until_parked();
1077
1078 editor.update(cx, |editor, cx| {
1079 let expected_layers = vec!["0".to_string()];
1080 assert_eq!(
1081 expected_layers,
1082 cached_hint_labels(editor),
1083 "Should not update hints while the work task is running"
1084 );
1085 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1086 assert_eq!(
1087 editor.inlay_hint_cache().version,
1088 edits_made,
1089 "Should not update the cache while the work task is running"
1090 );
1091 });
1092
1093 fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1094 token: lsp::ProgressToken::String(progress_token.to_string()),
1095 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1096 lsp::WorkDoneProgressEnd::default(),
1097 )),
1098 });
1099 cx.foreground().run_until_parked();
1100
1101 edits_made += 1;
1102 editor.update(cx, |editor, cx| {
1103 let expected_layers = vec!["1".to_string()];
1104 assert_eq!(
1105 expected_layers,
1106 cached_hint_labels(editor),
1107 "New hints should be queried after the work task is done"
1108 );
1109 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1110 assert_eq!(
1111 editor.inlay_hint_cache().version,
1112 edits_made,
1113 "Cache version should udpate once after the work task is done"
1114 );
1115 });
1116 }
1117
1118 #[gpui::test]
1119 async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1120 init_test(cx, |settings| {
1121 settings.defaults.inlay_hints = Some(InlayHintSettings {
1122 enabled: true,
1123 show_type_hints: true,
1124 show_parameter_hints: true,
1125 show_other_hints: true,
1126 })
1127 });
1128
1129 let fs = FakeFs::new(cx.background());
1130 fs.insert_tree(
1131 "/a",
1132 json!({
1133 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1134 "other.md": "Test md file with some text",
1135 }),
1136 )
1137 .await;
1138 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1139 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1140 let worktree_id = workspace.update(cx, |workspace, cx| {
1141 workspace.project().read_with(cx, |project, cx| {
1142 project.worktrees(cx).next().unwrap().read(cx).id()
1143 })
1144 });
1145
1146 let mut rs_fake_servers = None;
1147 let mut md_fake_servers = None;
1148 for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1149 let mut language = Language::new(
1150 LanguageConfig {
1151 name: name.into(),
1152 path_suffixes: vec![path_suffix.to_string()],
1153 ..Default::default()
1154 },
1155 Some(tree_sitter_rust::language()),
1156 );
1157 let fake_servers = language
1158 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1159 name,
1160 capabilities: lsp::ServerCapabilities {
1161 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1162 ..Default::default()
1163 },
1164 ..Default::default()
1165 }))
1166 .await;
1167 match name {
1168 "Rust" => rs_fake_servers = Some(fake_servers),
1169 "Markdown" => md_fake_servers = Some(fake_servers),
1170 _ => unreachable!(),
1171 }
1172 project.update(cx, |project, _| {
1173 project.languages().add(Arc::new(language));
1174 });
1175 }
1176
1177 let _rs_buffer = project
1178 .update(cx, |project, cx| {
1179 project.open_local_buffer("/a/main.rs", cx)
1180 })
1181 .await
1182 .unwrap();
1183 cx.foreground().run_until_parked();
1184 cx.foreground().start_waiting();
1185 let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1186 let rs_editor = workspace
1187 .update(cx, |workspace, cx| {
1188 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1189 })
1190 .await
1191 .unwrap()
1192 .downcast::<Editor>()
1193 .unwrap();
1194 let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1195 rs_fake_server
1196 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1197 let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
1198 async move {
1199 assert_eq!(
1200 params.text_document.uri,
1201 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1202 );
1203 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1204 Ok(Some(vec![lsp::InlayHint {
1205 position: lsp::Position::new(0, i),
1206 label: lsp::InlayHintLabel::String(i.to_string()),
1207 kind: None,
1208 text_edits: None,
1209 tooltip: None,
1210 padding_left: None,
1211 padding_right: None,
1212 data: None,
1213 }]))
1214 }
1215 })
1216 .next()
1217 .await;
1218 cx.foreground().run_until_parked();
1219 rs_editor.update(cx, |editor, cx| {
1220 let expected_layers = vec!["0".to_string()];
1221 assert_eq!(
1222 expected_layers,
1223 cached_hint_labels(editor),
1224 "Should get its first hints when opening the editor"
1225 );
1226 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1227 assert_eq!(
1228 editor.inlay_hint_cache().version,
1229 1,
1230 "Rust editor update the cache version after every cache/view change"
1231 );
1232 });
1233
1234 cx.foreground().run_until_parked();
1235 let _md_buffer = project
1236 .update(cx, |project, cx| {
1237 project.open_local_buffer("/a/other.md", cx)
1238 })
1239 .await
1240 .unwrap();
1241 cx.foreground().run_until_parked();
1242 cx.foreground().start_waiting();
1243 let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1244 let md_editor = workspace
1245 .update(cx, |workspace, cx| {
1246 workspace.open_path((worktree_id, "other.md"), None, true, cx)
1247 })
1248 .await
1249 .unwrap()
1250 .downcast::<Editor>()
1251 .unwrap();
1252 let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1253 md_fake_server
1254 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1255 let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
1256 async move {
1257 assert_eq!(
1258 params.text_document.uri,
1259 lsp::Url::from_file_path("/a/other.md").unwrap(),
1260 );
1261 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1262 Ok(Some(vec![lsp::InlayHint {
1263 position: lsp::Position::new(0, i),
1264 label: lsp::InlayHintLabel::String(i.to_string()),
1265 kind: None,
1266 text_edits: None,
1267 tooltip: None,
1268 padding_left: None,
1269 padding_right: None,
1270 data: None,
1271 }]))
1272 }
1273 })
1274 .next()
1275 .await;
1276 cx.foreground().run_until_parked();
1277 md_editor.update(cx, |editor, cx| {
1278 let expected_layers = vec!["0".to_string()];
1279 assert_eq!(
1280 expected_layers,
1281 cached_hint_labels(editor),
1282 "Markdown editor should have a separate verison, repeating Rust editor rules"
1283 );
1284 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1285 assert_eq!(editor.inlay_hint_cache().version, 1);
1286 });
1287
1288 rs_editor.update(cx, |editor, cx| {
1289 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1290 editor.handle_input("some rs change", cx);
1291 });
1292 cx.foreground().run_until_parked();
1293 rs_editor.update(cx, |editor, cx| {
1294 let expected_layers = vec!["1".to_string()];
1295 assert_eq!(
1296 expected_layers,
1297 cached_hint_labels(editor),
1298 "Rust inlay cache should change after the edit"
1299 );
1300 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1301 assert_eq!(
1302 editor.inlay_hint_cache().version,
1303 2,
1304 "Every time hint cache changes, cache version should be incremented"
1305 );
1306 });
1307 md_editor.update(cx, |editor, cx| {
1308 let expected_layers = vec!["0".to_string()];
1309 assert_eq!(
1310 expected_layers,
1311 cached_hint_labels(editor),
1312 "Markdown editor should not be affected by Rust editor changes"
1313 );
1314 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1315 assert_eq!(editor.inlay_hint_cache().version, 1);
1316 });
1317
1318 md_editor.update(cx, |editor, cx| {
1319 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1320 editor.handle_input("some md change", cx);
1321 });
1322 cx.foreground().run_until_parked();
1323 md_editor.update(cx, |editor, cx| {
1324 let expected_layers = vec!["1".to_string()];
1325 assert_eq!(
1326 expected_layers,
1327 cached_hint_labels(editor),
1328 "Rust editor should not be affected by Markdown editor changes"
1329 );
1330 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1331 assert_eq!(editor.inlay_hint_cache().version, 2);
1332 });
1333 rs_editor.update(cx, |editor, cx| {
1334 let expected_layers = vec!["1".to_string()];
1335 assert_eq!(
1336 expected_layers,
1337 cached_hint_labels(editor),
1338 "Markdown editor should also change independently"
1339 );
1340 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1341 assert_eq!(editor.inlay_hint_cache().version, 2);
1342 });
1343 }
1344
1345 #[gpui::test]
1346 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1347 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1348 init_test(cx, |settings| {
1349 settings.defaults.inlay_hints = Some(InlayHintSettings {
1350 enabled: true,
1351 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1352 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1353 show_other_hints: allowed_hint_kinds.contains(&None),
1354 })
1355 });
1356
1357 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1358 let lsp_request_count = Arc::new(AtomicU32::new(0));
1359 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1360 fake_server
1361 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1362 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1363 async move {
1364 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1365 assert_eq!(
1366 params.text_document.uri,
1367 lsp::Url::from_file_path(file_with_hints).unwrap(),
1368 );
1369 Ok(Some(vec![
1370 lsp::InlayHint {
1371 position: lsp::Position::new(0, 1),
1372 label: lsp::InlayHintLabel::String("type hint".to_string()),
1373 kind: Some(lsp::InlayHintKind::TYPE),
1374 text_edits: None,
1375 tooltip: None,
1376 padding_left: None,
1377 padding_right: None,
1378 data: None,
1379 },
1380 lsp::InlayHint {
1381 position: lsp::Position::new(0, 2),
1382 label: lsp::InlayHintLabel::String("parameter hint".to_string()),
1383 kind: Some(lsp::InlayHintKind::PARAMETER),
1384 text_edits: None,
1385 tooltip: None,
1386 padding_left: None,
1387 padding_right: None,
1388 data: None,
1389 },
1390 lsp::InlayHint {
1391 position: lsp::Position::new(0, 3),
1392 label: lsp::InlayHintLabel::String("other hint".to_string()),
1393 kind: None,
1394 text_edits: None,
1395 tooltip: None,
1396 padding_left: None,
1397 padding_right: None,
1398 data: None,
1399 },
1400 ]))
1401 }
1402 })
1403 .next()
1404 .await;
1405 cx.foreground().run_until_parked();
1406
1407 let mut edits_made = 1;
1408 editor.update(cx, |editor, cx| {
1409 assert_eq!(
1410 lsp_request_count.load(Ordering::Relaxed),
1411 1,
1412 "Should query new hints once"
1413 );
1414 assert_eq!(
1415 vec![
1416 "other hint".to_string(),
1417 "parameter hint".to_string(),
1418 "type hint".to_string(),
1419 ],
1420 cached_hint_labels(editor),
1421 "Should get its first hints when opening the editor"
1422 );
1423 assert_eq!(
1424 vec!["other hint".to_string(), "type hint".to_string()],
1425 visible_hint_labels(editor, cx)
1426 );
1427 let inlay_cache = editor.inlay_hint_cache();
1428 assert_eq!(
1429 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1430 "Cache should use editor settings to get the allowed hint kinds"
1431 );
1432 assert_eq!(
1433 inlay_cache.version, edits_made,
1434 "The editor update the cache version after every cache/view change"
1435 );
1436 });
1437
1438 fake_server
1439 .request::<lsp::request::InlayHintRefreshRequest>(())
1440 .await
1441 .expect("inlay refresh request failed");
1442 cx.foreground().run_until_parked();
1443 editor.update(cx, |editor, cx| {
1444 assert_eq!(
1445 lsp_request_count.load(Ordering::Relaxed),
1446 2,
1447 "Should load new hints twice"
1448 );
1449 assert_eq!(
1450 vec![
1451 "other hint".to_string(),
1452 "parameter hint".to_string(),
1453 "type hint".to_string(),
1454 ],
1455 cached_hint_labels(editor),
1456 "Cached hints should not change due to allowed hint kinds settings update"
1457 );
1458 assert_eq!(
1459 vec!["other hint".to_string(), "type hint".to_string()],
1460 visible_hint_labels(editor, cx)
1461 );
1462 assert_eq!(
1463 editor.inlay_hint_cache().version,
1464 edits_made,
1465 "Should not update cache version due to new loaded hints being the same"
1466 );
1467 });
1468
1469 for (new_allowed_hint_kinds, expected_visible_hints) in [
1470 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1471 (
1472 HashSet::from_iter([Some(InlayHintKind::Type)]),
1473 vec!["type hint".to_string()],
1474 ),
1475 (
1476 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1477 vec!["parameter hint".to_string()],
1478 ),
1479 (
1480 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1481 vec!["other hint".to_string(), "type hint".to_string()],
1482 ),
1483 (
1484 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1485 vec!["other hint".to_string(), "parameter hint".to_string()],
1486 ),
1487 (
1488 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1489 vec!["parameter hint".to_string(), "type hint".to_string()],
1490 ),
1491 (
1492 HashSet::from_iter([
1493 None,
1494 Some(InlayHintKind::Type),
1495 Some(InlayHintKind::Parameter),
1496 ]),
1497 vec![
1498 "other hint".to_string(),
1499 "parameter hint".to_string(),
1500 "type hint".to_string(),
1501 ],
1502 ),
1503 ] {
1504 edits_made += 1;
1505 update_test_language_settings(cx, |settings| {
1506 settings.defaults.inlay_hints = Some(InlayHintSettings {
1507 enabled: true,
1508 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1509 show_parameter_hints: new_allowed_hint_kinds
1510 .contains(&Some(InlayHintKind::Parameter)),
1511 show_other_hints: new_allowed_hint_kinds.contains(&None),
1512 })
1513 });
1514 cx.foreground().run_until_parked();
1515 editor.update(cx, |editor, cx| {
1516 assert_eq!(
1517 lsp_request_count.load(Ordering::Relaxed),
1518 2,
1519 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1520 );
1521 assert_eq!(
1522 vec![
1523 "other hint".to_string(),
1524 "parameter hint".to_string(),
1525 "type hint".to_string(),
1526 ],
1527 cached_hint_labels(editor),
1528 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1529 );
1530 assert_eq!(
1531 expected_visible_hints,
1532 visible_hint_labels(editor, cx),
1533 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1534 );
1535 let inlay_cache = editor.inlay_hint_cache();
1536 assert_eq!(
1537 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1538 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1539 );
1540 assert_eq!(
1541 inlay_cache.version, edits_made,
1542 "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
1543 );
1544 });
1545 }
1546
1547 edits_made += 1;
1548 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1549 update_test_language_settings(cx, |settings| {
1550 settings.defaults.inlay_hints = Some(InlayHintSettings {
1551 enabled: false,
1552 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1553 show_parameter_hints: another_allowed_hint_kinds
1554 .contains(&Some(InlayHintKind::Parameter)),
1555 show_other_hints: another_allowed_hint_kinds.contains(&None),
1556 })
1557 });
1558 cx.foreground().run_until_parked();
1559 editor.update(cx, |editor, cx| {
1560 assert_eq!(
1561 lsp_request_count.load(Ordering::Relaxed),
1562 2,
1563 "Should not load new hints when hints got disabled"
1564 );
1565 assert!(
1566 cached_hint_labels(editor).is_empty(),
1567 "Should clear the cache when hints got disabled"
1568 );
1569 assert!(
1570 visible_hint_labels(editor, cx).is_empty(),
1571 "Should clear visible hints when hints got disabled"
1572 );
1573 let inlay_cache = editor.inlay_hint_cache();
1574 assert_eq!(
1575 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1576 "Should update its allowed hint kinds even when hints got disabled"
1577 );
1578 assert_eq!(
1579 inlay_cache.version, edits_made,
1580 "The editor should update the cache version after hints got disabled"
1581 );
1582 });
1583
1584 fake_server
1585 .request::<lsp::request::InlayHintRefreshRequest>(())
1586 .await
1587 .expect("inlay refresh request failed");
1588 cx.foreground().run_until_parked();
1589 editor.update(cx, |editor, cx| {
1590 assert_eq!(
1591 lsp_request_count.load(Ordering::Relaxed),
1592 2,
1593 "Should not load new hints when they got disabled"
1594 );
1595 assert!(cached_hint_labels(editor).is_empty());
1596 assert!(visible_hint_labels(editor, cx).is_empty());
1597 assert_eq!(
1598 editor.inlay_hint_cache().version, edits_made,
1599 "The editor should not update the cache version after /refresh query without updates"
1600 );
1601 });
1602
1603 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1604 edits_made += 1;
1605 update_test_language_settings(cx, |settings| {
1606 settings.defaults.inlay_hints = Some(InlayHintSettings {
1607 enabled: true,
1608 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1609 show_parameter_hints: final_allowed_hint_kinds
1610 .contains(&Some(InlayHintKind::Parameter)),
1611 show_other_hints: final_allowed_hint_kinds.contains(&None),
1612 })
1613 });
1614 cx.foreground().run_until_parked();
1615 editor.update(cx, |editor, cx| {
1616 assert_eq!(
1617 lsp_request_count.load(Ordering::Relaxed),
1618 3,
1619 "Should query for new hints when they got reenabled"
1620 );
1621 assert_eq!(
1622 vec![
1623 "other hint".to_string(),
1624 "parameter hint".to_string(),
1625 "type hint".to_string(),
1626 ],
1627 cached_hint_labels(editor),
1628 "Should get its cached hints fully repopulated after the hints got reenabled"
1629 );
1630 assert_eq!(
1631 vec!["parameter hint".to_string()],
1632 visible_hint_labels(editor, cx),
1633 "Should get its visible hints repopulated and filtered after the h"
1634 );
1635 let inlay_cache = editor.inlay_hint_cache();
1636 assert_eq!(
1637 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
1638 "Cache should update editor settings when hints got reenabled"
1639 );
1640 assert_eq!(
1641 inlay_cache.version, edits_made,
1642 "Cache should update its version after hints got reenabled"
1643 );
1644 });
1645
1646 fake_server
1647 .request::<lsp::request::InlayHintRefreshRequest>(())
1648 .await
1649 .expect("inlay refresh request failed");
1650 cx.foreground().run_until_parked();
1651 editor.update(cx, |editor, cx| {
1652 assert_eq!(
1653 lsp_request_count.load(Ordering::Relaxed),
1654 4,
1655 "Should query for new hints again"
1656 );
1657 assert_eq!(
1658 vec![
1659 "other hint".to_string(),
1660 "parameter hint".to_string(),
1661 "type hint".to_string(),
1662 ],
1663 cached_hint_labels(editor),
1664 );
1665 assert_eq!(
1666 vec!["parameter hint".to_string()],
1667 visible_hint_labels(editor, cx),
1668 );
1669 assert_eq!(editor.inlay_hint_cache().version, edits_made);
1670 });
1671 }
1672
1673 #[gpui::test]
1674 async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
1675 init_test(cx, |settings| {
1676 settings.defaults.inlay_hints = Some(InlayHintSettings {
1677 enabled: true,
1678 show_type_hints: true,
1679 show_parameter_hints: true,
1680 show_other_hints: true,
1681 })
1682 });
1683
1684 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1685 let fake_server = Arc::new(fake_server);
1686 let lsp_request_count = Arc::new(AtomicU32::new(0));
1687 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1688 fake_server
1689 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1690 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1691 async move {
1692 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1693 assert_eq!(
1694 params.text_document.uri,
1695 lsp::Url::from_file_path(file_with_hints).unwrap(),
1696 );
1697 Ok(Some(vec![lsp::InlayHint {
1698 position: lsp::Position::new(0, i),
1699 label: lsp::InlayHintLabel::String(i.to_string()),
1700 kind: None,
1701 text_edits: None,
1702 tooltip: None,
1703 padding_left: None,
1704 padding_right: None,
1705 data: None,
1706 }]))
1707 }
1708 })
1709 .next()
1710 .await;
1711
1712 let mut expected_changes = Vec::new();
1713 for change_after_opening in [
1714 "initial change #1",
1715 "initial change #2",
1716 "initial change #3",
1717 ] {
1718 editor.update(cx, |editor, cx| {
1719 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1720 editor.handle_input(change_after_opening, cx);
1721 });
1722 expected_changes.push(change_after_opening);
1723 }
1724
1725 cx.foreground().run_until_parked();
1726
1727 editor.update(cx, |editor, cx| {
1728 let current_text = editor.text(cx);
1729 for change in &expected_changes {
1730 assert!(
1731 current_text.contains(change),
1732 "Should apply all changes made"
1733 );
1734 }
1735 assert_eq!(
1736 lsp_request_count.load(Ordering::Relaxed),
1737 2,
1738 "Should query new hints twice: for editor init and for the last edit that interrupted all others"
1739 );
1740 let expected_hints = vec!["2".to_string()];
1741 assert_eq!(
1742 expected_hints,
1743 cached_hint_labels(editor),
1744 "Should get hints from the last edit landed only"
1745 );
1746 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1747 assert_eq!(
1748 editor.inlay_hint_cache().version, 1,
1749 "Only one update should be registered in the cache after all cancellations"
1750 );
1751 });
1752
1753 let mut edits = Vec::new();
1754 for async_later_change in [
1755 "another change #1",
1756 "another change #2",
1757 "another change #3",
1758 ] {
1759 expected_changes.push(async_later_change);
1760 let task_editor = editor.clone();
1761 let mut task_cx = cx.clone();
1762 edits.push(cx.foreground().spawn(async move {
1763 task_editor.update(&mut task_cx, |editor, cx| {
1764 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1765 editor.handle_input(async_later_change, cx);
1766 });
1767 }));
1768 }
1769 let _ = futures::future::join_all(edits).await;
1770 cx.foreground().run_until_parked();
1771
1772 editor.update(cx, |editor, cx| {
1773 let current_text = editor.text(cx);
1774 for change in &expected_changes {
1775 assert!(
1776 current_text.contains(change),
1777 "Should apply all changes made"
1778 );
1779 }
1780 assert_eq!(
1781 lsp_request_count.load(Ordering::SeqCst),
1782 3,
1783 "Should query new hints one more time, for the last edit only"
1784 );
1785 let expected_hints = vec!["3".to_string()];
1786 assert_eq!(
1787 expected_hints,
1788 cached_hint_labels(editor),
1789 "Should get hints from the last edit landed only"
1790 );
1791 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1792 assert_eq!(
1793 editor.inlay_hint_cache().version,
1794 2,
1795 "Should update the cache version once more, for the new change"
1796 );
1797 });
1798 }
1799
1800 #[gpui::test]
1801 async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
1802 init_test(cx, |settings| {
1803 settings.defaults.inlay_hints = Some(InlayHintSettings {
1804 enabled: true,
1805 show_type_hints: true,
1806 show_parameter_hints: true,
1807 show_other_hints: true,
1808 })
1809 });
1810
1811 let mut language = Language::new(
1812 LanguageConfig {
1813 name: "Rust".into(),
1814 path_suffixes: vec!["rs".to_string()],
1815 ..Default::default()
1816 },
1817 Some(tree_sitter_rust::language()),
1818 );
1819 let mut fake_servers = language
1820 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1821 capabilities: lsp::ServerCapabilities {
1822 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1823 ..Default::default()
1824 },
1825 ..Default::default()
1826 }))
1827 .await;
1828 let fs = FakeFs::new(cx.background());
1829 fs.insert_tree(
1830 "/a",
1831 json!({
1832 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
1833 "other.rs": "// Test file",
1834 }),
1835 )
1836 .await;
1837 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1838 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
1839 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1840 let worktree_id = workspace.update(cx, |workspace, cx| {
1841 workspace.project().read_with(cx, |project, cx| {
1842 project.worktrees(cx).next().unwrap().read(cx).id()
1843 })
1844 });
1845
1846 let _buffer = project
1847 .update(cx, |project, cx| {
1848 project.open_local_buffer("/a/main.rs", cx)
1849 })
1850 .await
1851 .unwrap();
1852 cx.foreground().run_until_parked();
1853 cx.foreground().start_waiting();
1854 let fake_server = fake_servers.next().await.unwrap();
1855 let editor = workspace
1856 .update(cx, |workspace, cx| {
1857 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1858 })
1859 .await
1860 .unwrap()
1861 .downcast::<Editor>()
1862 .unwrap();
1863 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
1864 let lsp_request_count = Arc::new(AtomicU32::new(0));
1865 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
1866 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
1867 fake_server
1868 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1869 let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
1870 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
1871 async move {
1872 assert_eq!(
1873 params.text_document.uri,
1874 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1875 );
1876
1877 task_lsp_request_ranges.lock().push(params.range);
1878 let query_start = params.range.start;
1879 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1880 Ok(Some(vec![lsp::InlayHint {
1881 position: query_start,
1882 label: lsp::InlayHintLabel::String(i.to_string()),
1883 kind: None,
1884 text_edits: None,
1885 tooltip: None,
1886 padding_left: None,
1887 padding_right: None,
1888 data: None,
1889 }]))
1890 }
1891 })
1892 .next()
1893 .await;
1894 cx.foreground().run_until_parked();
1895 editor.update(cx, |editor, cx| {
1896 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1897 ranges.sort_by_key(|range| range.start);
1898 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");
1899 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1900 assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
1901 assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
1902
1903 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
1904 "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
1905 let expected_layers = vec!["1".to_string(), "2".to_string()];
1906 assert_eq!(
1907 expected_layers,
1908 cached_hint_labels(editor),
1909 "Should have hints from both LSP requests made for a big file"
1910 );
1911 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1912 assert_eq!(
1913 editor.inlay_hint_cache().version, 2,
1914 "Both LSP queries should've bumped the cache version"
1915 );
1916 });
1917
1918 editor.update(cx, |editor, cx| {
1919 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1920 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1921 editor.change_selections(None, cx, |s| s.select_ranges([600..600]));
1922 editor.handle_input("++++more text++++", cx);
1923 });
1924
1925 cx.foreground().run_until_parked();
1926 editor.update(cx, |editor, cx| {
1927 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1928 ranges.sort_by_key(|range| range.start);
1929 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");
1930 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1931 assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end");
1932 assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning");
1933 assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning");
1934 assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line");
1935 assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent");
1936
1937 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5,
1938 "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints");
1939 let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()];
1940 assert_eq!(expected_layers, cached_hint_labels(editor),
1941 "Should have hints from the new LSP response after edit");
1942 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1943 assert_eq!(editor.inlay_hint_cache().version, 5, "Should update the cache for every LSP response with hints added");
1944 });
1945 }
1946
1947 #[gpui::test]
1948 async fn test_multiple_excerpts_large_multibuffer(
1949 deterministic: Arc<Deterministic>,
1950 cx: &mut gpui::TestAppContext,
1951 ) {
1952 init_test(cx, |settings| {
1953 settings.defaults.inlay_hints = Some(InlayHintSettings {
1954 enabled: true,
1955 show_type_hints: true,
1956 show_parameter_hints: true,
1957 show_other_hints: true,
1958 })
1959 });
1960
1961 let mut language = Language::new(
1962 LanguageConfig {
1963 name: "Rust".into(),
1964 path_suffixes: vec!["rs".to_string()],
1965 ..Default::default()
1966 },
1967 Some(tree_sitter_rust::language()),
1968 );
1969 let mut fake_servers = language
1970 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1971 capabilities: lsp::ServerCapabilities {
1972 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1973 ..Default::default()
1974 },
1975 ..Default::default()
1976 }))
1977 .await;
1978 let language = Arc::new(language);
1979 let fs = FakeFs::new(cx.background());
1980 fs.insert_tree(
1981 "/a",
1982 json!({
1983 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
1984 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
1985 }),
1986 )
1987 .await;
1988 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1989 project.update(cx, |project, _| {
1990 project.languages().add(Arc::clone(&language))
1991 });
1992 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1993 let worktree_id = workspace.update(cx, |workspace, cx| {
1994 workspace.project().read_with(cx, |project, cx| {
1995 project.worktrees(cx).next().unwrap().read(cx).id()
1996 })
1997 });
1998
1999 let buffer_1 = project
2000 .update(cx, |project, cx| {
2001 project.open_buffer((worktree_id, "main.rs"), cx)
2002 })
2003 .await
2004 .unwrap();
2005 let buffer_2 = project
2006 .update(cx, |project, cx| {
2007 project.open_buffer((worktree_id, "other.rs"), cx)
2008 })
2009 .await
2010 .unwrap();
2011 let multibuffer = cx.add_model(|cx| {
2012 let mut multibuffer = MultiBuffer::new(0);
2013 multibuffer.push_excerpts(
2014 buffer_1.clone(),
2015 [
2016 ExcerptRange {
2017 context: Point::new(0, 0)..Point::new(2, 0),
2018 primary: None,
2019 },
2020 ExcerptRange {
2021 context: Point::new(4, 0)..Point::new(11, 0),
2022 primary: None,
2023 },
2024 ExcerptRange {
2025 context: Point::new(22, 0)..Point::new(33, 0),
2026 primary: None,
2027 },
2028 ExcerptRange {
2029 context: Point::new(44, 0)..Point::new(55, 0),
2030 primary: None,
2031 },
2032 ExcerptRange {
2033 context: Point::new(56, 0)..Point::new(66, 0),
2034 primary: None,
2035 },
2036 ExcerptRange {
2037 context: Point::new(67, 0)..Point::new(77, 0),
2038 primary: None,
2039 },
2040 ],
2041 cx,
2042 );
2043 multibuffer.push_excerpts(
2044 buffer_2.clone(),
2045 [
2046 ExcerptRange {
2047 context: Point::new(0, 1)..Point::new(2, 1),
2048 primary: None,
2049 },
2050 ExcerptRange {
2051 context: Point::new(4, 1)..Point::new(11, 1),
2052 primary: None,
2053 },
2054 ExcerptRange {
2055 context: Point::new(22, 1)..Point::new(33, 1),
2056 primary: None,
2057 },
2058 ExcerptRange {
2059 context: Point::new(44, 1)..Point::new(55, 1),
2060 primary: None,
2061 },
2062 ExcerptRange {
2063 context: Point::new(56, 1)..Point::new(66, 1),
2064 primary: None,
2065 },
2066 ExcerptRange {
2067 context: Point::new(67, 1)..Point::new(77, 1),
2068 primary: None,
2069 },
2070 ],
2071 cx,
2072 );
2073 multibuffer
2074 });
2075
2076 deterministic.run_until_parked();
2077 cx.foreground().run_until_parked();
2078 let (_, editor) =
2079 cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
2080 let editor_edited = Arc::new(AtomicBool::new(false));
2081 let fake_server = fake_servers.next().await.unwrap();
2082 let closure_editor_edited = Arc::clone(&editor_edited);
2083 fake_server
2084 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2085 let task_editor_edited = Arc::clone(&closure_editor_edited);
2086 async move {
2087 let hint_text = if params.text_document.uri
2088 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2089 {
2090 "main hint"
2091 } else if params.text_document.uri
2092 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2093 {
2094 "other hint"
2095 } else {
2096 panic!("unexpected uri: {:?}", params.text_document.uri);
2097 };
2098
2099 // one hint per excerpt
2100 let positions = [
2101 lsp::Position::new(0, 2),
2102 lsp::Position::new(4, 2),
2103 lsp::Position::new(22, 2),
2104 lsp::Position::new(44, 2),
2105 lsp::Position::new(56, 2),
2106 lsp::Position::new(67, 2),
2107 ];
2108 let out_of_range_hint = lsp::InlayHint {
2109 position: lsp::Position::new(
2110 params.range.start.line + 99,
2111 params.range.start.character + 99,
2112 ),
2113 label: lsp::InlayHintLabel::String(
2114 "out of excerpt range, should be ignored".to_string(),
2115 ),
2116 kind: None,
2117 text_edits: None,
2118 tooltip: None,
2119 padding_left: None,
2120 padding_right: None,
2121 data: None,
2122 };
2123
2124 let edited = task_editor_edited.load(Ordering::Acquire);
2125 Ok(Some(
2126 std::iter::once(out_of_range_hint)
2127 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2128 lsp::InlayHint {
2129 position,
2130 label: lsp::InlayHintLabel::String(format!(
2131 "{hint_text}{} #{i}",
2132 if edited { "(edited)" } else { "" },
2133 )),
2134 kind: None,
2135 text_edits: None,
2136 tooltip: None,
2137 padding_left: None,
2138 padding_right: None,
2139 data: None,
2140 }
2141 }))
2142 .collect(),
2143 ))
2144 }
2145 })
2146 .next()
2147 .await;
2148 cx.foreground().run_until_parked();
2149
2150 editor.update(cx, |editor, cx| {
2151 let expected_layers = vec![
2152 "main hint #0".to_string(),
2153 "main hint #1".to_string(),
2154 "main hint #2".to_string(),
2155 "main hint #3".to_string(),
2156 ];
2157 assert_eq!(
2158 expected_layers,
2159 cached_hint_labels(editor),
2160 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2161 );
2162 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2163 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison");
2164 });
2165
2166 editor.update(cx, |editor, cx| {
2167 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2168 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2169 });
2170 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2171 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2172 });
2173 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2174 s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
2175 });
2176 });
2177 cx.foreground().run_until_parked();
2178 editor.update(cx, |editor, cx| {
2179 let expected_layers = vec![
2180 "main hint #0".to_string(),
2181 "main hint #1".to_string(),
2182 "main hint #2".to_string(),
2183 "main hint #3".to_string(),
2184 "main hint #4".to_string(),
2185 "main hint #5".to_string(),
2186 "other hint #0".to_string(),
2187 "other hint #1".to_string(),
2188 "other hint #2".to_string(),
2189 ];
2190 assert_eq!(expected_layers, cached_hint_labels(editor),
2191 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2192 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2193 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(),
2194 "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
2195 });
2196
2197 editor.update(cx, |editor, cx| {
2198 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2199 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2200 });
2201 });
2202 cx.foreground().run_until_parked();
2203 let last_scroll_update_version = editor.update(cx, |editor, cx| {
2204 let expected_layers = vec![
2205 "main hint #0".to_string(),
2206 "main hint #1".to_string(),
2207 "main hint #2".to_string(),
2208 "main hint #3".to_string(),
2209 "main hint #4".to_string(),
2210 "main hint #5".to_string(),
2211 "other hint #0".to_string(),
2212 "other hint #1".to_string(),
2213 "other hint #2".to_string(),
2214 "other hint #3".to_string(),
2215 "other hint #4".to_string(),
2216 "other hint #5".to_string(),
2217 ];
2218 assert_eq!(expected_layers, cached_hint_labels(editor),
2219 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2220 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2221 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len());
2222 expected_layers.len()
2223 });
2224
2225 editor.update(cx, |editor, cx| {
2226 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2227 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2228 });
2229 });
2230 cx.foreground().run_until_parked();
2231 editor.update(cx, |editor, cx| {
2232 let expected_layers = vec![
2233 "main hint #0".to_string(),
2234 "main hint #1".to_string(),
2235 "main hint #2".to_string(),
2236 "main hint #3".to_string(),
2237 "main hint #4".to_string(),
2238 "main hint #5".to_string(),
2239 "other hint #0".to_string(),
2240 "other hint #1".to_string(),
2241 "other hint #2".to_string(),
2242 "other hint #3".to_string(),
2243 "other hint #4".to_string(),
2244 "other hint #5".to_string(),
2245 ];
2246 assert_eq!(expected_layers, cached_hint_labels(editor),
2247 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2248 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2249 assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
2250 });
2251
2252 editor_edited.store(true, Ordering::Release);
2253 editor.update(cx, |editor, cx| {
2254 editor.change_selections(None, cx, |s| {
2255 s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
2256 });
2257 editor.handle_input("++++more text++++", cx);
2258 });
2259 cx.foreground().run_until_parked();
2260 editor.update(cx, |editor, cx| {
2261 let expected_layers = vec![
2262 "main hint(edited) #0".to_string(),
2263 "main hint(edited) #1".to_string(),
2264 "main hint(edited) #2".to_string(),
2265 "main hint(edited) #3".to_string(),
2266 "main hint(edited) #4".to_string(),
2267 "main hint(edited) #5".to_string(),
2268 "other hint(edited) #0".to_string(),
2269 "other hint(edited) #1".to_string(),
2270 ];
2271 assert_eq!(
2272 expected_layers,
2273 cached_hint_labels(editor),
2274 "After multibuffer edit, editor gets scolled back to the last selection; \
2275all hints should be invalidated and requeried for all of its visible excerpts"
2276 );
2277 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2278 assert_eq!(
2279 editor.inlay_hint_cache().version,
2280 last_scroll_update_version + expected_layers.len() + 1,
2281 "Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal"
2282 );
2283 });
2284 }
2285
2286 #[gpui::test]
2287 async fn test_excerpts_removed(
2288 deterministic: Arc<Deterministic>,
2289 cx: &mut gpui::TestAppContext,
2290 ) {
2291 init_test(cx, |settings| {
2292 settings.defaults.inlay_hints = Some(InlayHintSettings {
2293 enabled: true,
2294 show_type_hints: false,
2295 show_parameter_hints: false,
2296 show_other_hints: false,
2297 })
2298 });
2299
2300 let mut language = Language::new(
2301 LanguageConfig {
2302 name: "Rust".into(),
2303 path_suffixes: vec!["rs".to_string()],
2304 ..Default::default()
2305 },
2306 Some(tree_sitter_rust::language()),
2307 );
2308 let mut fake_servers = language
2309 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2310 capabilities: lsp::ServerCapabilities {
2311 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2312 ..Default::default()
2313 },
2314 ..Default::default()
2315 }))
2316 .await;
2317 let language = Arc::new(language);
2318 let fs = FakeFs::new(cx.background());
2319 fs.insert_tree(
2320 "/a",
2321 json!({
2322 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2323 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2324 }),
2325 )
2326 .await;
2327 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2328 project.update(cx, |project, _| {
2329 project.languages().add(Arc::clone(&language))
2330 });
2331 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2332 let worktree_id = workspace.update(cx, |workspace, cx| {
2333 workspace.project().read_with(cx, |project, cx| {
2334 project.worktrees(cx).next().unwrap().read(cx).id()
2335 })
2336 });
2337
2338 let buffer_1 = project
2339 .update(cx, |project, cx| {
2340 project.open_buffer((worktree_id, "main.rs"), cx)
2341 })
2342 .await
2343 .unwrap();
2344 let buffer_2 = project
2345 .update(cx, |project, cx| {
2346 project.open_buffer((worktree_id, "other.rs"), cx)
2347 })
2348 .await
2349 .unwrap();
2350 let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
2351 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2352 let buffer_1_excerpts = multibuffer.push_excerpts(
2353 buffer_1.clone(),
2354 [ExcerptRange {
2355 context: Point::new(0, 0)..Point::new(2, 0),
2356 primary: None,
2357 }],
2358 cx,
2359 );
2360 let buffer_2_excerpts = multibuffer.push_excerpts(
2361 buffer_2.clone(),
2362 [ExcerptRange {
2363 context: Point::new(0, 1)..Point::new(2, 1),
2364 primary: None,
2365 }],
2366 cx,
2367 );
2368 (buffer_1_excerpts, buffer_2_excerpts)
2369 });
2370
2371 assert!(!buffer_1_excerpts.is_empty());
2372 assert!(!buffer_2_excerpts.is_empty());
2373
2374 deterministic.run_until_parked();
2375 cx.foreground().run_until_parked();
2376 let (_, editor) =
2377 cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
2378 let editor_edited = Arc::new(AtomicBool::new(false));
2379 let fake_server = fake_servers.next().await.unwrap();
2380 let closure_editor_edited = Arc::clone(&editor_edited);
2381 fake_server
2382 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2383 let task_editor_edited = Arc::clone(&closure_editor_edited);
2384 async move {
2385 let hint_text = if params.text_document.uri
2386 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2387 {
2388 "main hint"
2389 } else if params.text_document.uri
2390 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2391 {
2392 "other hint"
2393 } else {
2394 panic!("unexpected uri: {:?}", params.text_document.uri);
2395 };
2396
2397 let positions = [
2398 lsp::Position::new(0, 2),
2399 lsp::Position::new(4, 2),
2400 lsp::Position::new(22, 2),
2401 lsp::Position::new(44, 2),
2402 lsp::Position::new(56, 2),
2403 lsp::Position::new(67, 2),
2404 ];
2405 let out_of_range_hint = lsp::InlayHint {
2406 position: lsp::Position::new(
2407 params.range.start.line + 99,
2408 params.range.start.character + 99,
2409 ),
2410 label: lsp::InlayHintLabel::String(
2411 "out of excerpt range, should be ignored".to_string(),
2412 ),
2413 kind: None,
2414 text_edits: None,
2415 tooltip: None,
2416 padding_left: None,
2417 padding_right: None,
2418 data: None,
2419 };
2420
2421 let edited = task_editor_edited.load(Ordering::Acquire);
2422 Ok(Some(
2423 std::iter::once(out_of_range_hint)
2424 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2425 lsp::InlayHint {
2426 position,
2427 label: lsp::InlayHintLabel::String(format!(
2428 "{hint_text}{} #{i}",
2429 if edited { "(edited)" } else { "" },
2430 )),
2431 kind: None,
2432 text_edits: None,
2433 tooltip: None,
2434 padding_left: None,
2435 padding_right: None,
2436 data: None,
2437 }
2438 }))
2439 .collect(),
2440 ))
2441 }
2442 })
2443 .next()
2444 .await;
2445 cx.foreground().run_until_parked();
2446
2447 editor.update(cx, |editor, cx| {
2448 assert_eq!(
2449 vec!["main hint #0".to_string(), "other hint #0".to_string()],
2450 cached_hint_labels(editor),
2451 "Cache should update for both excerpts despite hints display was disabled"
2452 );
2453 assert!(
2454 visible_hint_labels(editor, cx).is_empty(),
2455 "All hints are disabled and should not be shown despite being present in the cache"
2456 );
2457 assert_eq!(
2458 editor.inlay_hint_cache().version,
2459 2,
2460 "Cache should update once per excerpt query"
2461 );
2462 });
2463
2464 editor.update(cx, |editor, cx| {
2465 editor.buffer().update(cx, |multibuffer, cx| {
2466 multibuffer.remove_excerpts(buffer_2_excerpts, cx)
2467 })
2468 });
2469 cx.foreground().run_until_parked();
2470 editor.update(cx, |editor, cx| {
2471 assert_eq!(
2472 vec!["main hint #0".to_string()],
2473 cached_hint_labels(editor),
2474 "For the removed excerpt, should clean corresponding cached hints"
2475 );
2476 assert!(
2477 visible_hint_labels(editor, cx).is_empty(),
2478 "All hints are disabled and should not be shown despite being present in the cache"
2479 );
2480 assert_eq!(
2481 editor.inlay_hint_cache().version,
2482 3,
2483 "Excerpt removal should trigger cache update"
2484 );
2485 });
2486
2487 update_test_language_settings(cx, |settings| {
2488 settings.defaults.inlay_hints = Some(InlayHintSettings {
2489 enabled: true,
2490 show_type_hints: true,
2491 show_parameter_hints: true,
2492 show_other_hints: true,
2493 })
2494 });
2495 cx.foreground().run_until_parked();
2496 editor.update(cx, |editor, cx| {
2497 let expected_hints = vec!["main hint #0".to_string()];
2498 assert_eq!(
2499 expected_hints,
2500 cached_hint_labels(editor),
2501 "Hint display settings change should not change the cache"
2502 );
2503 assert_eq!(
2504 expected_hints,
2505 visible_hint_labels(editor, cx),
2506 "Settings change should make cached hints visible"
2507 );
2508 assert_eq!(
2509 editor.inlay_hint_cache().version,
2510 4,
2511 "Settings change should trigger cache update"
2512 );
2513 });
2514 }
2515
2516 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
2517 cx.foreground().forbid_parking();
2518
2519 cx.update(|cx| {
2520 cx.set_global(SettingsStore::test(cx));
2521 theme::init((), cx);
2522 client::init_settings(cx);
2523 language::init(cx);
2524 Project::init_settings(cx);
2525 workspace::init_settings(cx);
2526 crate::init(cx);
2527 });
2528
2529 update_test_language_settings(cx, f);
2530 }
2531
2532 async fn prepare_test_objects(
2533 cx: &mut TestAppContext,
2534 ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
2535 let mut language = Language::new(
2536 LanguageConfig {
2537 name: "Rust".into(),
2538 path_suffixes: vec!["rs".to_string()],
2539 ..Default::default()
2540 },
2541 Some(tree_sitter_rust::language()),
2542 );
2543 let mut fake_servers = language
2544 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2545 capabilities: lsp::ServerCapabilities {
2546 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2547 ..Default::default()
2548 },
2549 ..Default::default()
2550 }))
2551 .await;
2552
2553 let fs = FakeFs::new(cx.background());
2554 fs.insert_tree(
2555 "/a",
2556 json!({
2557 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
2558 "other.rs": "// Test file",
2559 }),
2560 )
2561 .await;
2562
2563 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2564 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
2565 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2566 let worktree_id = workspace.update(cx, |workspace, cx| {
2567 workspace.project().read_with(cx, |project, cx| {
2568 project.worktrees(cx).next().unwrap().read(cx).id()
2569 })
2570 });
2571
2572 let _buffer = project
2573 .update(cx, |project, cx| {
2574 project.open_local_buffer("/a/main.rs", cx)
2575 })
2576 .await
2577 .unwrap();
2578 cx.foreground().run_until_parked();
2579 cx.foreground().start_waiting();
2580 let fake_server = fake_servers.next().await.unwrap();
2581 let editor = workspace
2582 .update(cx, |workspace, cx| {
2583 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
2584 })
2585 .await
2586 .unwrap()
2587 .downcast::<Editor>()
2588 .unwrap();
2589
2590 ("/a/main.rs", editor, fake_server)
2591 }
2592
2593 fn cached_hint_labels(editor: &Editor) -> Vec<String> {
2594 let mut labels = Vec::new();
2595 for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
2596 let excerpt_hints = excerpt_hints.read();
2597 for (_, inlay) in excerpt_hints.hints.iter() {
2598 match &inlay.label {
2599 project::InlayHintLabel::String(s) => labels.push(s.to_string()),
2600 _ => unreachable!(),
2601 }
2602 }
2603 }
2604
2605 labels.sort();
2606 labels
2607 }
2608
2609 fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
2610 let mut hints = editor
2611 .visible_inlay_hints(cx)
2612 .into_iter()
2613 .map(|hint| hint.text.to_string())
2614 .collect::<Vec<_>>();
2615 hints.sort();
2616 hints
2617 }
2618}