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
1140 .add_window(|cx| Workspace::test_new(project.clone(), cx))
1141 .detach(cx);
1142 let worktree_id = workspace.update(cx, |workspace, cx| {
1143 workspace.project().read_with(cx, |project, cx| {
1144 project.worktrees(cx).next().unwrap().read(cx).id()
1145 })
1146 });
1147
1148 let mut rs_fake_servers = None;
1149 let mut md_fake_servers = None;
1150 for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1151 let mut language = Language::new(
1152 LanguageConfig {
1153 name: name.into(),
1154 path_suffixes: vec![path_suffix.to_string()],
1155 ..Default::default()
1156 },
1157 Some(tree_sitter_rust::language()),
1158 );
1159 let fake_servers = language
1160 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1161 name,
1162 capabilities: lsp::ServerCapabilities {
1163 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1164 ..Default::default()
1165 },
1166 ..Default::default()
1167 }))
1168 .await;
1169 match name {
1170 "Rust" => rs_fake_servers = Some(fake_servers),
1171 "Markdown" => md_fake_servers = Some(fake_servers),
1172 _ => unreachable!(),
1173 }
1174 project.update(cx, |project, _| {
1175 project.languages().add(Arc::new(language));
1176 });
1177 }
1178
1179 let _rs_buffer = project
1180 .update(cx, |project, cx| {
1181 project.open_local_buffer("/a/main.rs", cx)
1182 })
1183 .await
1184 .unwrap();
1185 cx.foreground().run_until_parked();
1186 cx.foreground().start_waiting();
1187 let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1188 let rs_editor = workspace
1189 .update(cx, |workspace, cx| {
1190 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1191 })
1192 .await
1193 .unwrap()
1194 .downcast::<Editor>()
1195 .unwrap();
1196 let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1197 rs_fake_server
1198 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1199 let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
1200 async move {
1201 assert_eq!(
1202 params.text_document.uri,
1203 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1204 );
1205 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1206 Ok(Some(vec![lsp::InlayHint {
1207 position: lsp::Position::new(0, i),
1208 label: lsp::InlayHintLabel::String(i.to_string()),
1209 kind: None,
1210 text_edits: None,
1211 tooltip: None,
1212 padding_left: None,
1213 padding_right: None,
1214 data: None,
1215 }]))
1216 }
1217 })
1218 .next()
1219 .await;
1220 cx.foreground().run_until_parked();
1221 rs_editor.update(cx, |editor, cx| {
1222 let expected_layers = vec!["0".to_string()];
1223 assert_eq!(
1224 expected_layers,
1225 cached_hint_labels(editor),
1226 "Should get its first hints when opening the editor"
1227 );
1228 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1229 assert_eq!(
1230 editor.inlay_hint_cache().version,
1231 1,
1232 "Rust editor update the cache version after every cache/view change"
1233 );
1234 });
1235
1236 cx.foreground().run_until_parked();
1237 let _md_buffer = project
1238 .update(cx, |project, cx| {
1239 project.open_local_buffer("/a/other.md", cx)
1240 })
1241 .await
1242 .unwrap();
1243 cx.foreground().run_until_parked();
1244 cx.foreground().start_waiting();
1245 let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1246 let md_editor = workspace
1247 .update(cx, |workspace, cx| {
1248 workspace.open_path((worktree_id, "other.md"), None, true, cx)
1249 })
1250 .await
1251 .unwrap()
1252 .downcast::<Editor>()
1253 .unwrap();
1254 let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1255 md_fake_server
1256 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1257 let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
1258 async move {
1259 assert_eq!(
1260 params.text_document.uri,
1261 lsp::Url::from_file_path("/a/other.md").unwrap(),
1262 );
1263 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1264 Ok(Some(vec![lsp::InlayHint {
1265 position: lsp::Position::new(0, i),
1266 label: lsp::InlayHintLabel::String(i.to_string()),
1267 kind: None,
1268 text_edits: None,
1269 tooltip: None,
1270 padding_left: None,
1271 padding_right: None,
1272 data: None,
1273 }]))
1274 }
1275 })
1276 .next()
1277 .await;
1278 cx.foreground().run_until_parked();
1279 md_editor.update(cx, |editor, cx| {
1280 let expected_layers = vec!["0".to_string()];
1281 assert_eq!(
1282 expected_layers,
1283 cached_hint_labels(editor),
1284 "Markdown editor should have a separate verison, repeating Rust editor rules"
1285 );
1286 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1287 assert_eq!(editor.inlay_hint_cache().version, 1);
1288 });
1289
1290 rs_editor.update(cx, |editor, cx| {
1291 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1292 editor.handle_input("some rs change", cx);
1293 });
1294 cx.foreground().run_until_parked();
1295 rs_editor.update(cx, |editor, cx| {
1296 let expected_layers = vec!["1".to_string()];
1297 assert_eq!(
1298 expected_layers,
1299 cached_hint_labels(editor),
1300 "Rust inlay cache should change after the edit"
1301 );
1302 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1303 assert_eq!(
1304 editor.inlay_hint_cache().version,
1305 2,
1306 "Every time hint cache changes, cache version should be incremented"
1307 );
1308 });
1309 md_editor.update(cx, |editor, cx| {
1310 let expected_layers = vec!["0".to_string()];
1311 assert_eq!(
1312 expected_layers,
1313 cached_hint_labels(editor),
1314 "Markdown editor should not be affected by Rust editor changes"
1315 );
1316 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1317 assert_eq!(editor.inlay_hint_cache().version, 1);
1318 });
1319
1320 md_editor.update(cx, |editor, cx| {
1321 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1322 editor.handle_input("some md change", cx);
1323 });
1324 cx.foreground().run_until_parked();
1325 md_editor.update(cx, |editor, cx| {
1326 let expected_layers = vec!["1".to_string()];
1327 assert_eq!(
1328 expected_layers,
1329 cached_hint_labels(editor),
1330 "Rust editor should not be affected by Markdown editor changes"
1331 );
1332 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1333 assert_eq!(editor.inlay_hint_cache().version, 2);
1334 });
1335 rs_editor.update(cx, |editor, cx| {
1336 let expected_layers = vec!["1".to_string()];
1337 assert_eq!(
1338 expected_layers,
1339 cached_hint_labels(editor),
1340 "Markdown editor should also change independently"
1341 );
1342 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1343 assert_eq!(editor.inlay_hint_cache().version, 2);
1344 });
1345 }
1346
1347 #[gpui::test]
1348 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1349 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1350 init_test(cx, |settings| {
1351 settings.defaults.inlay_hints = Some(InlayHintSettings {
1352 enabled: true,
1353 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1354 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1355 show_other_hints: allowed_hint_kinds.contains(&None),
1356 })
1357 });
1358
1359 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1360 let lsp_request_count = Arc::new(AtomicU32::new(0));
1361 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1362 fake_server
1363 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1364 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1365 async move {
1366 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1367 assert_eq!(
1368 params.text_document.uri,
1369 lsp::Url::from_file_path(file_with_hints).unwrap(),
1370 );
1371 Ok(Some(vec![
1372 lsp::InlayHint {
1373 position: lsp::Position::new(0, 1),
1374 label: lsp::InlayHintLabel::String("type hint".to_string()),
1375 kind: Some(lsp::InlayHintKind::TYPE),
1376 text_edits: None,
1377 tooltip: None,
1378 padding_left: None,
1379 padding_right: None,
1380 data: None,
1381 },
1382 lsp::InlayHint {
1383 position: lsp::Position::new(0, 2),
1384 label: lsp::InlayHintLabel::String("parameter hint".to_string()),
1385 kind: Some(lsp::InlayHintKind::PARAMETER),
1386 text_edits: None,
1387 tooltip: None,
1388 padding_left: None,
1389 padding_right: None,
1390 data: None,
1391 },
1392 lsp::InlayHint {
1393 position: lsp::Position::new(0, 3),
1394 label: lsp::InlayHintLabel::String("other hint".to_string()),
1395 kind: None,
1396 text_edits: None,
1397 tooltip: None,
1398 padding_left: None,
1399 padding_right: None,
1400 data: None,
1401 },
1402 ]))
1403 }
1404 })
1405 .next()
1406 .await;
1407 cx.foreground().run_until_parked();
1408
1409 let mut edits_made = 1;
1410 editor.update(cx, |editor, cx| {
1411 assert_eq!(
1412 lsp_request_count.load(Ordering::Relaxed),
1413 1,
1414 "Should query new hints once"
1415 );
1416 assert_eq!(
1417 vec![
1418 "other hint".to_string(),
1419 "parameter hint".to_string(),
1420 "type hint".to_string(),
1421 ],
1422 cached_hint_labels(editor),
1423 "Should get its first hints when opening the editor"
1424 );
1425 assert_eq!(
1426 vec!["other hint".to_string(), "type hint".to_string()],
1427 visible_hint_labels(editor, cx)
1428 );
1429 let inlay_cache = editor.inlay_hint_cache();
1430 assert_eq!(
1431 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1432 "Cache should use editor settings to get the allowed hint kinds"
1433 );
1434 assert_eq!(
1435 inlay_cache.version, edits_made,
1436 "The editor update the cache version after every cache/view change"
1437 );
1438 });
1439
1440 fake_server
1441 .request::<lsp::request::InlayHintRefreshRequest>(())
1442 .await
1443 .expect("inlay refresh request failed");
1444 cx.foreground().run_until_parked();
1445 editor.update(cx, |editor, cx| {
1446 assert_eq!(
1447 lsp_request_count.load(Ordering::Relaxed),
1448 2,
1449 "Should load new hints twice"
1450 );
1451 assert_eq!(
1452 vec![
1453 "other hint".to_string(),
1454 "parameter hint".to_string(),
1455 "type hint".to_string(),
1456 ],
1457 cached_hint_labels(editor),
1458 "Cached hints should not change due to allowed hint kinds settings update"
1459 );
1460 assert_eq!(
1461 vec!["other hint".to_string(), "type hint".to_string()],
1462 visible_hint_labels(editor, cx)
1463 );
1464 assert_eq!(
1465 editor.inlay_hint_cache().version,
1466 edits_made,
1467 "Should not update cache version due to new loaded hints being the same"
1468 );
1469 });
1470
1471 for (new_allowed_hint_kinds, expected_visible_hints) in [
1472 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1473 (
1474 HashSet::from_iter([Some(InlayHintKind::Type)]),
1475 vec!["type hint".to_string()],
1476 ),
1477 (
1478 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1479 vec!["parameter hint".to_string()],
1480 ),
1481 (
1482 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1483 vec!["other hint".to_string(), "type hint".to_string()],
1484 ),
1485 (
1486 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1487 vec!["other hint".to_string(), "parameter hint".to_string()],
1488 ),
1489 (
1490 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1491 vec!["parameter hint".to_string(), "type hint".to_string()],
1492 ),
1493 (
1494 HashSet::from_iter([
1495 None,
1496 Some(InlayHintKind::Type),
1497 Some(InlayHintKind::Parameter),
1498 ]),
1499 vec![
1500 "other hint".to_string(),
1501 "parameter hint".to_string(),
1502 "type hint".to_string(),
1503 ],
1504 ),
1505 ] {
1506 edits_made += 1;
1507 update_test_language_settings(cx, |settings| {
1508 settings.defaults.inlay_hints = Some(InlayHintSettings {
1509 enabled: true,
1510 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1511 show_parameter_hints: new_allowed_hint_kinds
1512 .contains(&Some(InlayHintKind::Parameter)),
1513 show_other_hints: new_allowed_hint_kinds.contains(&None),
1514 })
1515 });
1516 cx.foreground().run_until_parked();
1517 editor.update(cx, |editor, cx| {
1518 assert_eq!(
1519 lsp_request_count.load(Ordering::Relaxed),
1520 2,
1521 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1522 );
1523 assert_eq!(
1524 vec![
1525 "other hint".to_string(),
1526 "parameter hint".to_string(),
1527 "type hint".to_string(),
1528 ],
1529 cached_hint_labels(editor),
1530 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1531 );
1532 assert_eq!(
1533 expected_visible_hints,
1534 visible_hint_labels(editor, cx),
1535 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1536 );
1537 let inlay_cache = editor.inlay_hint_cache();
1538 assert_eq!(
1539 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1540 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1541 );
1542 assert_eq!(
1543 inlay_cache.version, edits_made,
1544 "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
1545 );
1546 });
1547 }
1548
1549 edits_made += 1;
1550 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1551 update_test_language_settings(cx, |settings| {
1552 settings.defaults.inlay_hints = Some(InlayHintSettings {
1553 enabled: false,
1554 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1555 show_parameter_hints: another_allowed_hint_kinds
1556 .contains(&Some(InlayHintKind::Parameter)),
1557 show_other_hints: another_allowed_hint_kinds.contains(&None),
1558 })
1559 });
1560 cx.foreground().run_until_parked();
1561 editor.update(cx, |editor, cx| {
1562 assert_eq!(
1563 lsp_request_count.load(Ordering::Relaxed),
1564 2,
1565 "Should not load new hints when hints got disabled"
1566 );
1567 assert!(
1568 cached_hint_labels(editor).is_empty(),
1569 "Should clear the cache when hints got disabled"
1570 );
1571 assert!(
1572 visible_hint_labels(editor, cx).is_empty(),
1573 "Should clear visible hints when hints got disabled"
1574 );
1575 let inlay_cache = editor.inlay_hint_cache();
1576 assert_eq!(
1577 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1578 "Should update its allowed hint kinds even when hints got disabled"
1579 );
1580 assert_eq!(
1581 inlay_cache.version, edits_made,
1582 "The editor should update the cache version after hints got disabled"
1583 );
1584 });
1585
1586 fake_server
1587 .request::<lsp::request::InlayHintRefreshRequest>(())
1588 .await
1589 .expect("inlay refresh request failed");
1590 cx.foreground().run_until_parked();
1591 editor.update(cx, |editor, cx| {
1592 assert_eq!(
1593 lsp_request_count.load(Ordering::Relaxed),
1594 2,
1595 "Should not load new hints when they got disabled"
1596 );
1597 assert!(cached_hint_labels(editor).is_empty());
1598 assert!(visible_hint_labels(editor, cx).is_empty());
1599 assert_eq!(
1600 editor.inlay_hint_cache().version, edits_made,
1601 "The editor should not update the cache version after /refresh query without updates"
1602 );
1603 });
1604
1605 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1606 edits_made += 1;
1607 update_test_language_settings(cx, |settings| {
1608 settings.defaults.inlay_hints = Some(InlayHintSettings {
1609 enabled: true,
1610 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1611 show_parameter_hints: final_allowed_hint_kinds
1612 .contains(&Some(InlayHintKind::Parameter)),
1613 show_other_hints: final_allowed_hint_kinds.contains(&None),
1614 })
1615 });
1616 cx.foreground().run_until_parked();
1617 editor.update(cx, |editor, cx| {
1618 assert_eq!(
1619 lsp_request_count.load(Ordering::Relaxed),
1620 3,
1621 "Should query for new hints when they got reenabled"
1622 );
1623 assert_eq!(
1624 vec![
1625 "other hint".to_string(),
1626 "parameter hint".to_string(),
1627 "type hint".to_string(),
1628 ],
1629 cached_hint_labels(editor),
1630 "Should get its cached hints fully repopulated after the hints got reenabled"
1631 );
1632 assert_eq!(
1633 vec!["parameter hint".to_string()],
1634 visible_hint_labels(editor, cx),
1635 "Should get its visible hints repopulated and filtered after the h"
1636 );
1637 let inlay_cache = editor.inlay_hint_cache();
1638 assert_eq!(
1639 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
1640 "Cache should update editor settings when hints got reenabled"
1641 );
1642 assert_eq!(
1643 inlay_cache.version, edits_made,
1644 "Cache should update its version after hints got reenabled"
1645 );
1646 });
1647
1648 fake_server
1649 .request::<lsp::request::InlayHintRefreshRequest>(())
1650 .await
1651 .expect("inlay refresh request failed");
1652 cx.foreground().run_until_parked();
1653 editor.update(cx, |editor, cx| {
1654 assert_eq!(
1655 lsp_request_count.load(Ordering::Relaxed),
1656 4,
1657 "Should query for new hints again"
1658 );
1659 assert_eq!(
1660 vec![
1661 "other hint".to_string(),
1662 "parameter hint".to_string(),
1663 "type hint".to_string(),
1664 ],
1665 cached_hint_labels(editor),
1666 );
1667 assert_eq!(
1668 vec!["parameter hint".to_string()],
1669 visible_hint_labels(editor, cx),
1670 );
1671 assert_eq!(editor.inlay_hint_cache().version, edits_made);
1672 });
1673 }
1674
1675 #[gpui::test]
1676 async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
1677 init_test(cx, |settings| {
1678 settings.defaults.inlay_hints = Some(InlayHintSettings {
1679 enabled: true,
1680 show_type_hints: true,
1681 show_parameter_hints: true,
1682 show_other_hints: true,
1683 })
1684 });
1685
1686 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1687 let fake_server = Arc::new(fake_server);
1688 let lsp_request_count = Arc::new(AtomicU32::new(0));
1689 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1690 fake_server
1691 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1692 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1693 async move {
1694 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1695 assert_eq!(
1696 params.text_document.uri,
1697 lsp::Url::from_file_path(file_with_hints).unwrap(),
1698 );
1699 Ok(Some(vec![lsp::InlayHint {
1700 position: lsp::Position::new(0, i),
1701 label: lsp::InlayHintLabel::String(i.to_string()),
1702 kind: None,
1703 text_edits: None,
1704 tooltip: None,
1705 padding_left: None,
1706 padding_right: None,
1707 data: None,
1708 }]))
1709 }
1710 })
1711 .next()
1712 .await;
1713
1714 let mut expected_changes = Vec::new();
1715 for change_after_opening in [
1716 "initial change #1",
1717 "initial change #2",
1718 "initial change #3",
1719 ] {
1720 editor.update(cx, |editor, cx| {
1721 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1722 editor.handle_input(change_after_opening, cx);
1723 });
1724 expected_changes.push(change_after_opening);
1725 }
1726
1727 cx.foreground().run_until_parked();
1728
1729 editor.update(cx, |editor, cx| {
1730 let current_text = editor.text(cx);
1731 for change in &expected_changes {
1732 assert!(
1733 current_text.contains(change),
1734 "Should apply all changes made"
1735 );
1736 }
1737 assert_eq!(
1738 lsp_request_count.load(Ordering::Relaxed),
1739 2,
1740 "Should query new hints twice: for editor init and for the last edit that interrupted all others"
1741 );
1742 let expected_hints = vec!["2".to_string()];
1743 assert_eq!(
1744 expected_hints,
1745 cached_hint_labels(editor),
1746 "Should get hints from the last edit landed only"
1747 );
1748 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1749 assert_eq!(
1750 editor.inlay_hint_cache().version, 1,
1751 "Only one update should be registered in the cache after all cancellations"
1752 );
1753 });
1754
1755 let mut edits = Vec::new();
1756 for async_later_change in [
1757 "another change #1",
1758 "another change #2",
1759 "another change #3",
1760 ] {
1761 expected_changes.push(async_later_change);
1762 let task_editor = editor.clone();
1763 let mut task_cx = cx.clone();
1764 edits.push(cx.foreground().spawn(async move {
1765 task_editor.update(&mut task_cx, |editor, cx| {
1766 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1767 editor.handle_input(async_later_change, cx);
1768 });
1769 }));
1770 }
1771 let _ = futures::future::join_all(edits).await;
1772 cx.foreground().run_until_parked();
1773
1774 editor.update(cx, |editor, cx| {
1775 let current_text = editor.text(cx);
1776 for change in &expected_changes {
1777 assert!(
1778 current_text.contains(change),
1779 "Should apply all changes made"
1780 );
1781 }
1782 assert_eq!(
1783 lsp_request_count.load(Ordering::SeqCst),
1784 3,
1785 "Should query new hints one more time, for the last edit only"
1786 );
1787 let expected_hints = vec!["3".to_string()];
1788 assert_eq!(
1789 expected_hints,
1790 cached_hint_labels(editor),
1791 "Should get hints from the last edit landed only"
1792 );
1793 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1794 assert_eq!(
1795 editor.inlay_hint_cache().version,
1796 2,
1797 "Should update the cache version once more, for the new change"
1798 );
1799 });
1800 }
1801
1802 #[gpui::test]
1803 async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
1804 init_test(cx, |settings| {
1805 settings.defaults.inlay_hints = Some(InlayHintSettings {
1806 enabled: true,
1807 show_type_hints: true,
1808 show_parameter_hints: true,
1809 show_other_hints: true,
1810 })
1811 });
1812
1813 let mut language = Language::new(
1814 LanguageConfig {
1815 name: "Rust".into(),
1816 path_suffixes: vec!["rs".to_string()],
1817 ..Default::default()
1818 },
1819 Some(tree_sitter_rust::language()),
1820 );
1821 let mut fake_servers = language
1822 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1823 capabilities: lsp::ServerCapabilities {
1824 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1825 ..Default::default()
1826 },
1827 ..Default::default()
1828 }))
1829 .await;
1830 let fs = FakeFs::new(cx.background());
1831 fs.insert_tree(
1832 "/a",
1833 json!({
1834 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
1835 "other.rs": "// Test file",
1836 }),
1837 )
1838 .await;
1839 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1840 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
1841 let workspace = cx
1842 .add_window(|cx| Workspace::test_new(project.clone(), cx))
1843 .detach(cx);
1844 let worktree_id = workspace.update(cx, |workspace, cx| {
1845 workspace.project().read_with(cx, |project, cx| {
1846 project.worktrees(cx).next().unwrap().read(cx).id()
1847 })
1848 });
1849
1850 let _buffer = project
1851 .update(cx, |project, cx| {
1852 project.open_local_buffer("/a/main.rs", cx)
1853 })
1854 .await
1855 .unwrap();
1856 cx.foreground().run_until_parked();
1857 cx.foreground().start_waiting();
1858 let fake_server = fake_servers.next().await.unwrap();
1859 let editor = workspace
1860 .update(cx, |workspace, cx| {
1861 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1862 })
1863 .await
1864 .unwrap()
1865 .downcast::<Editor>()
1866 .unwrap();
1867 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
1868 let lsp_request_count = Arc::new(AtomicU32::new(0));
1869 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
1870 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
1871 fake_server
1872 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1873 let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
1874 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
1875 async move {
1876 assert_eq!(
1877 params.text_document.uri,
1878 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1879 );
1880
1881 task_lsp_request_ranges.lock().push(params.range);
1882 let query_start = params.range.start;
1883 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1884 Ok(Some(vec![lsp::InlayHint {
1885 position: query_start,
1886 label: lsp::InlayHintLabel::String(i.to_string()),
1887 kind: None,
1888 text_edits: None,
1889 tooltip: None,
1890 padding_left: None,
1891 padding_right: None,
1892 data: None,
1893 }]))
1894 }
1895 })
1896 .next()
1897 .await;
1898 cx.foreground().run_until_parked();
1899 editor.update(cx, |editor, cx| {
1900 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1901 ranges.sort_by_key(|range| range.start);
1902 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");
1903 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1904 assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
1905 assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
1906
1907 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
1908 "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
1909 let expected_layers = vec!["1".to_string(), "2".to_string()];
1910 assert_eq!(
1911 expected_layers,
1912 cached_hint_labels(editor),
1913 "Should have hints from both LSP requests made for a big file"
1914 );
1915 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1916 assert_eq!(
1917 editor.inlay_hint_cache().version, 2,
1918 "Both LSP queries should've bumped the cache version"
1919 );
1920 });
1921
1922 editor.update(cx, |editor, cx| {
1923 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1924 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
1925 editor.change_selections(None, cx, |s| s.select_ranges([600..600]));
1926 editor.handle_input("++++more text++++", cx);
1927 });
1928
1929 cx.foreground().run_until_parked();
1930 editor.update(cx, |editor, cx| {
1931 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
1932 ranges.sort_by_key(|range| range.start);
1933 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");
1934 assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
1935 assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end");
1936 assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning");
1937 assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning");
1938 assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line");
1939 assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent");
1940
1941 assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5,
1942 "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints");
1943 let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()];
1944 assert_eq!(expected_layers, cached_hint_labels(editor),
1945 "Should have hints from the new LSP response after edit");
1946 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1947 assert_eq!(editor.inlay_hint_cache().version, 5, "Should update the cache for every LSP response with hints added");
1948 });
1949 }
1950
1951 #[gpui::test]
1952 async fn test_multiple_excerpts_large_multibuffer(
1953 deterministic: Arc<Deterministic>,
1954 cx: &mut gpui::TestAppContext,
1955 ) {
1956 init_test(cx, |settings| {
1957 settings.defaults.inlay_hints = Some(InlayHintSettings {
1958 enabled: true,
1959 show_type_hints: true,
1960 show_parameter_hints: true,
1961 show_other_hints: true,
1962 })
1963 });
1964
1965 let mut language = Language::new(
1966 LanguageConfig {
1967 name: "Rust".into(),
1968 path_suffixes: vec!["rs".to_string()],
1969 ..Default::default()
1970 },
1971 Some(tree_sitter_rust::language()),
1972 );
1973 let mut fake_servers = language
1974 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1975 capabilities: lsp::ServerCapabilities {
1976 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1977 ..Default::default()
1978 },
1979 ..Default::default()
1980 }))
1981 .await;
1982 let language = Arc::new(language);
1983 let fs = FakeFs::new(cx.background());
1984 fs.insert_tree(
1985 "/a",
1986 json!({
1987 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
1988 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
1989 }),
1990 )
1991 .await;
1992 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1993 project.update(cx, |project, _| {
1994 project.languages().add(Arc::clone(&language))
1995 });
1996 let workspace = cx
1997 .add_window(|cx| Workspace::test_new(project.clone(), cx))
1998 .detach(cx);
1999 let worktree_id = workspace.update(cx, |workspace, cx| {
2000 workspace.project().read_with(cx, |project, cx| {
2001 project.worktrees(cx).next().unwrap().read(cx).id()
2002 })
2003 });
2004
2005 let buffer_1 = project
2006 .update(cx, |project, cx| {
2007 project.open_buffer((worktree_id, "main.rs"), cx)
2008 })
2009 .await
2010 .unwrap();
2011 let buffer_2 = project
2012 .update(cx, |project, cx| {
2013 project.open_buffer((worktree_id, "other.rs"), cx)
2014 })
2015 .await
2016 .unwrap();
2017 let multibuffer = cx.add_model(|cx| {
2018 let mut multibuffer = MultiBuffer::new(0);
2019 multibuffer.push_excerpts(
2020 buffer_1.clone(),
2021 [
2022 ExcerptRange {
2023 context: Point::new(0, 0)..Point::new(2, 0),
2024 primary: None,
2025 },
2026 ExcerptRange {
2027 context: Point::new(4, 0)..Point::new(11, 0),
2028 primary: None,
2029 },
2030 ExcerptRange {
2031 context: Point::new(22, 0)..Point::new(33, 0),
2032 primary: None,
2033 },
2034 ExcerptRange {
2035 context: Point::new(44, 0)..Point::new(55, 0),
2036 primary: None,
2037 },
2038 ExcerptRange {
2039 context: Point::new(56, 0)..Point::new(66, 0),
2040 primary: None,
2041 },
2042 ExcerptRange {
2043 context: Point::new(67, 0)..Point::new(77, 0),
2044 primary: None,
2045 },
2046 ],
2047 cx,
2048 );
2049 multibuffer.push_excerpts(
2050 buffer_2.clone(),
2051 [
2052 ExcerptRange {
2053 context: Point::new(0, 1)..Point::new(2, 1),
2054 primary: None,
2055 },
2056 ExcerptRange {
2057 context: Point::new(4, 1)..Point::new(11, 1),
2058 primary: None,
2059 },
2060 ExcerptRange {
2061 context: Point::new(22, 1)..Point::new(33, 1),
2062 primary: None,
2063 },
2064 ExcerptRange {
2065 context: Point::new(44, 1)..Point::new(55, 1),
2066 primary: None,
2067 },
2068 ExcerptRange {
2069 context: Point::new(56, 1)..Point::new(66, 1),
2070 primary: None,
2071 },
2072 ExcerptRange {
2073 context: Point::new(67, 1)..Point::new(77, 1),
2074 primary: None,
2075 },
2076 ],
2077 cx,
2078 );
2079 multibuffer
2080 });
2081
2082 deterministic.run_until_parked();
2083 cx.foreground().run_until_parked();
2084 let editor = cx
2085 .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
2086 .detach(cx);
2087 let editor_edited = Arc::new(AtomicBool::new(false));
2088 let fake_server = fake_servers.next().await.unwrap();
2089 let closure_editor_edited = Arc::clone(&editor_edited);
2090 fake_server
2091 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2092 let task_editor_edited = Arc::clone(&closure_editor_edited);
2093 async move {
2094 let hint_text = if params.text_document.uri
2095 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2096 {
2097 "main hint"
2098 } else if params.text_document.uri
2099 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2100 {
2101 "other hint"
2102 } else {
2103 panic!("unexpected uri: {:?}", params.text_document.uri);
2104 };
2105
2106 // one hint per excerpt
2107 let positions = [
2108 lsp::Position::new(0, 2),
2109 lsp::Position::new(4, 2),
2110 lsp::Position::new(22, 2),
2111 lsp::Position::new(44, 2),
2112 lsp::Position::new(56, 2),
2113 lsp::Position::new(67, 2),
2114 ];
2115 let out_of_range_hint = lsp::InlayHint {
2116 position: lsp::Position::new(
2117 params.range.start.line + 99,
2118 params.range.start.character + 99,
2119 ),
2120 label: lsp::InlayHintLabel::String(
2121 "out of excerpt range, should be ignored".to_string(),
2122 ),
2123 kind: None,
2124 text_edits: None,
2125 tooltip: None,
2126 padding_left: None,
2127 padding_right: None,
2128 data: None,
2129 };
2130
2131 let edited = task_editor_edited.load(Ordering::Acquire);
2132 Ok(Some(
2133 std::iter::once(out_of_range_hint)
2134 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2135 lsp::InlayHint {
2136 position,
2137 label: lsp::InlayHintLabel::String(format!(
2138 "{hint_text}{} #{i}",
2139 if edited { "(edited)" } else { "" },
2140 )),
2141 kind: None,
2142 text_edits: None,
2143 tooltip: None,
2144 padding_left: None,
2145 padding_right: None,
2146 data: None,
2147 }
2148 }))
2149 .collect(),
2150 ))
2151 }
2152 })
2153 .next()
2154 .await;
2155 cx.foreground().run_until_parked();
2156
2157 editor.update(cx, |editor, cx| {
2158 let expected_layers = vec![
2159 "main hint #0".to_string(),
2160 "main hint #1".to_string(),
2161 "main hint #2".to_string(),
2162 "main hint #3".to_string(),
2163 ];
2164 assert_eq!(
2165 expected_layers,
2166 cached_hint_labels(editor),
2167 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2168 );
2169 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2170 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison");
2171 });
2172
2173 editor.update(cx, |editor, cx| {
2174 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2175 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2176 });
2177 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2178 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2179 });
2180 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2181 s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
2182 });
2183 });
2184 cx.foreground().run_until_parked();
2185 editor.update(cx, |editor, cx| {
2186 let expected_layers = vec![
2187 "main hint #0".to_string(),
2188 "main hint #1".to_string(),
2189 "main hint #2".to_string(),
2190 "main hint #3".to_string(),
2191 "main hint #4".to_string(),
2192 "main hint #5".to_string(),
2193 "other hint #0".to_string(),
2194 "other hint #1".to_string(),
2195 "other hint #2".to_string(),
2196 ];
2197 assert_eq!(expected_layers, cached_hint_labels(editor),
2198 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2199 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2200 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(),
2201 "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
2202 });
2203
2204 editor.update(cx, |editor, cx| {
2205 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2206 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2207 });
2208 });
2209 cx.foreground().run_until_parked();
2210 let last_scroll_update_version = editor.update(cx, |editor, cx| {
2211 let expected_layers = vec![
2212 "main hint #0".to_string(),
2213 "main hint #1".to_string(),
2214 "main hint #2".to_string(),
2215 "main hint #3".to_string(),
2216 "main hint #4".to_string(),
2217 "main hint #5".to_string(),
2218 "other hint #0".to_string(),
2219 "other hint #1".to_string(),
2220 "other hint #2".to_string(),
2221 "other hint #3".to_string(),
2222 "other hint #4".to_string(),
2223 "other hint #5".to_string(),
2224 ];
2225 assert_eq!(expected_layers, cached_hint_labels(editor),
2226 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2227 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2228 assert_eq!(editor.inlay_hint_cache().version, expected_layers.len());
2229 expected_layers.len()
2230 });
2231
2232 editor.update(cx, |editor, cx| {
2233 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2234 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2235 });
2236 });
2237 cx.foreground().run_until_parked();
2238 editor.update(cx, |editor, cx| {
2239 let expected_layers = vec![
2240 "main hint #0".to_string(),
2241 "main hint #1".to_string(),
2242 "main hint #2".to_string(),
2243 "main hint #3".to_string(),
2244 "main hint #4".to_string(),
2245 "main hint #5".to_string(),
2246 "other hint #0".to_string(),
2247 "other hint #1".to_string(),
2248 "other hint #2".to_string(),
2249 "other hint #3".to_string(),
2250 "other hint #4".to_string(),
2251 "other hint #5".to_string(),
2252 ];
2253 assert_eq!(expected_layers, cached_hint_labels(editor),
2254 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2255 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2256 assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
2257 });
2258
2259 editor_edited.store(true, Ordering::Release);
2260 editor.update(cx, |editor, cx| {
2261 editor.change_selections(None, cx, |s| {
2262 s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
2263 });
2264 editor.handle_input("++++more text++++", cx);
2265 });
2266 cx.foreground().run_until_parked();
2267 editor.update(cx, |editor, cx| {
2268 let expected_layers = vec![
2269 "main hint(edited) #0".to_string(),
2270 "main hint(edited) #1".to_string(),
2271 "main hint(edited) #2".to_string(),
2272 "main hint(edited) #3".to_string(),
2273 "main hint(edited) #4".to_string(),
2274 "main hint(edited) #5".to_string(),
2275 "other hint(edited) #0".to_string(),
2276 "other hint(edited) #1".to_string(),
2277 ];
2278 assert_eq!(
2279 expected_layers,
2280 cached_hint_labels(editor),
2281 "After multibuffer edit, editor gets scolled back to the last selection; \
2282all hints should be invalidated and requeried for all of its visible excerpts"
2283 );
2284 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
2285 assert_eq!(
2286 editor.inlay_hint_cache().version,
2287 last_scroll_update_version + expected_layers.len() + 1,
2288 "Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal"
2289 );
2290 });
2291 }
2292
2293 #[gpui::test]
2294 async fn test_excerpts_removed(
2295 deterministic: Arc<Deterministic>,
2296 cx: &mut gpui::TestAppContext,
2297 ) {
2298 init_test(cx, |settings| {
2299 settings.defaults.inlay_hints = Some(InlayHintSettings {
2300 enabled: true,
2301 show_type_hints: false,
2302 show_parameter_hints: false,
2303 show_other_hints: false,
2304 })
2305 });
2306
2307 let mut language = Language::new(
2308 LanguageConfig {
2309 name: "Rust".into(),
2310 path_suffixes: vec!["rs".to_string()],
2311 ..Default::default()
2312 },
2313 Some(tree_sitter_rust::language()),
2314 );
2315 let mut fake_servers = language
2316 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2317 capabilities: lsp::ServerCapabilities {
2318 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2319 ..Default::default()
2320 },
2321 ..Default::default()
2322 }))
2323 .await;
2324 let language = Arc::new(language);
2325 let fs = FakeFs::new(cx.background());
2326 fs.insert_tree(
2327 "/a",
2328 json!({
2329 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2330 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2331 }),
2332 )
2333 .await;
2334 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2335 project.update(cx, |project, _| {
2336 project.languages().add(Arc::clone(&language))
2337 });
2338 let workspace = cx
2339 .add_window(|cx| Workspace::test_new(project.clone(), cx))
2340 .detach(cx);
2341 let worktree_id = workspace.update(cx, |workspace, cx| {
2342 workspace.project().read_with(cx, |project, cx| {
2343 project.worktrees(cx).next().unwrap().read(cx).id()
2344 })
2345 });
2346
2347 let buffer_1 = project
2348 .update(cx, |project, cx| {
2349 project.open_buffer((worktree_id, "main.rs"), cx)
2350 })
2351 .await
2352 .unwrap();
2353 let buffer_2 = project
2354 .update(cx, |project, cx| {
2355 project.open_buffer((worktree_id, "other.rs"), cx)
2356 })
2357 .await
2358 .unwrap();
2359 let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
2360 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2361 let buffer_1_excerpts = multibuffer.push_excerpts(
2362 buffer_1.clone(),
2363 [ExcerptRange {
2364 context: Point::new(0, 0)..Point::new(2, 0),
2365 primary: None,
2366 }],
2367 cx,
2368 );
2369 let buffer_2_excerpts = multibuffer.push_excerpts(
2370 buffer_2.clone(),
2371 [ExcerptRange {
2372 context: Point::new(0, 1)..Point::new(2, 1),
2373 primary: None,
2374 }],
2375 cx,
2376 );
2377 (buffer_1_excerpts, buffer_2_excerpts)
2378 });
2379
2380 assert!(!buffer_1_excerpts.is_empty());
2381 assert!(!buffer_2_excerpts.is_empty());
2382
2383 deterministic.run_until_parked();
2384 cx.foreground().run_until_parked();
2385 let editor = cx
2386 .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
2387 .detach(cx);
2388 let editor_edited = Arc::new(AtomicBool::new(false));
2389 let fake_server = fake_servers.next().await.unwrap();
2390 let closure_editor_edited = Arc::clone(&editor_edited);
2391 fake_server
2392 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2393 let task_editor_edited = Arc::clone(&closure_editor_edited);
2394 async move {
2395 let hint_text = if params.text_document.uri
2396 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2397 {
2398 "main hint"
2399 } else if params.text_document.uri
2400 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2401 {
2402 "other hint"
2403 } else {
2404 panic!("unexpected uri: {:?}", params.text_document.uri);
2405 };
2406
2407 let positions = [
2408 lsp::Position::new(0, 2),
2409 lsp::Position::new(4, 2),
2410 lsp::Position::new(22, 2),
2411 lsp::Position::new(44, 2),
2412 lsp::Position::new(56, 2),
2413 lsp::Position::new(67, 2),
2414 ];
2415 let out_of_range_hint = lsp::InlayHint {
2416 position: lsp::Position::new(
2417 params.range.start.line + 99,
2418 params.range.start.character + 99,
2419 ),
2420 label: lsp::InlayHintLabel::String(
2421 "out of excerpt range, should be ignored".to_string(),
2422 ),
2423 kind: None,
2424 text_edits: None,
2425 tooltip: None,
2426 padding_left: None,
2427 padding_right: None,
2428 data: None,
2429 };
2430
2431 let edited = task_editor_edited.load(Ordering::Acquire);
2432 Ok(Some(
2433 std::iter::once(out_of_range_hint)
2434 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2435 lsp::InlayHint {
2436 position,
2437 label: lsp::InlayHintLabel::String(format!(
2438 "{hint_text}{} #{i}",
2439 if edited { "(edited)" } else { "" },
2440 )),
2441 kind: None,
2442 text_edits: None,
2443 tooltip: None,
2444 padding_left: None,
2445 padding_right: None,
2446 data: None,
2447 }
2448 }))
2449 .collect(),
2450 ))
2451 }
2452 })
2453 .next()
2454 .await;
2455 cx.foreground().run_until_parked();
2456
2457 editor.update(cx, |editor, cx| {
2458 assert_eq!(
2459 vec!["main hint #0".to_string(), "other hint #0".to_string()],
2460 cached_hint_labels(editor),
2461 "Cache should update for both excerpts despite hints display was disabled"
2462 );
2463 assert!(
2464 visible_hint_labels(editor, cx).is_empty(),
2465 "All hints are disabled and should not be shown despite being present in the cache"
2466 );
2467 assert_eq!(
2468 editor.inlay_hint_cache().version,
2469 2,
2470 "Cache should update once per excerpt query"
2471 );
2472 });
2473
2474 editor.update(cx, |editor, cx| {
2475 editor.buffer().update(cx, |multibuffer, cx| {
2476 multibuffer.remove_excerpts(buffer_2_excerpts, cx)
2477 })
2478 });
2479 cx.foreground().run_until_parked();
2480 editor.update(cx, |editor, cx| {
2481 assert_eq!(
2482 vec!["main hint #0".to_string()],
2483 cached_hint_labels(editor),
2484 "For the removed excerpt, should clean corresponding cached hints"
2485 );
2486 assert!(
2487 visible_hint_labels(editor, cx).is_empty(),
2488 "All hints are disabled and should not be shown despite being present in the cache"
2489 );
2490 assert_eq!(
2491 editor.inlay_hint_cache().version,
2492 3,
2493 "Excerpt removal should trigger cache update"
2494 );
2495 });
2496
2497 update_test_language_settings(cx, |settings| {
2498 settings.defaults.inlay_hints = Some(InlayHintSettings {
2499 enabled: true,
2500 show_type_hints: true,
2501 show_parameter_hints: true,
2502 show_other_hints: true,
2503 })
2504 });
2505 cx.foreground().run_until_parked();
2506 editor.update(cx, |editor, cx| {
2507 let expected_hints = vec!["main hint #0".to_string()];
2508 assert_eq!(
2509 expected_hints,
2510 cached_hint_labels(editor),
2511 "Hint display settings change should not change the cache"
2512 );
2513 assert_eq!(
2514 expected_hints,
2515 visible_hint_labels(editor, cx),
2516 "Settings change should make cached hints visible"
2517 );
2518 assert_eq!(
2519 editor.inlay_hint_cache().version,
2520 4,
2521 "Settings change should trigger cache update"
2522 );
2523 });
2524 }
2525
2526 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
2527 cx.foreground().forbid_parking();
2528
2529 cx.update(|cx| {
2530 cx.set_global(SettingsStore::test(cx));
2531 theme::init((), cx);
2532 client::init_settings(cx);
2533 language::init(cx);
2534 Project::init_settings(cx);
2535 workspace::init_settings(cx);
2536 crate::init(cx);
2537 });
2538
2539 update_test_language_settings(cx, f);
2540 }
2541
2542 async fn prepare_test_objects(
2543 cx: &mut TestAppContext,
2544 ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
2545 let mut language = Language::new(
2546 LanguageConfig {
2547 name: "Rust".into(),
2548 path_suffixes: vec!["rs".to_string()],
2549 ..Default::default()
2550 },
2551 Some(tree_sitter_rust::language()),
2552 );
2553 let mut fake_servers = language
2554 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2555 capabilities: lsp::ServerCapabilities {
2556 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2557 ..Default::default()
2558 },
2559 ..Default::default()
2560 }))
2561 .await;
2562
2563 let fs = FakeFs::new(cx.background());
2564 fs.insert_tree(
2565 "/a",
2566 json!({
2567 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
2568 "other.rs": "// Test file",
2569 }),
2570 )
2571 .await;
2572
2573 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2574 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
2575 let workspace = cx
2576 .add_window(|cx| Workspace::test_new(project.clone(), cx))
2577 .detach(cx);
2578 let worktree_id = workspace.update(cx, |workspace, cx| {
2579 workspace.project().read_with(cx, |project, cx| {
2580 project.worktrees(cx).next().unwrap().read(cx).id()
2581 })
2582 });
2583
2584 let _buffer = project
2585 .update(cx, |project, cx| {
2586 project.open_local_buffer("/a/main.rs", cx)
2587 })
2588 .await
2589 .unwrap();
2590 cx.foreground().run_until_parked();
2591 cx.foreground().start_waiting();
2592 let fake_server = fake_servers.next().await.unwrap();
2593 let editor = workspace
2594 .update(cx, |workspace, cx| {
2595 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
2596 })
2597 .await
2598 .unwrap()
2599 .downcast::<Editor>()
2600 .unwrap();
2601
2602 ("/a/main.rs", editor, fake_server)
2603 }
2604
2605 fn cached_hint_labels(editor: &Editor) -> Vec<String> {
2606 let mut labels = Vec::new();
2607 for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
2608 let excerpt_hints = excerpt_hints.read();
2609 for (_, inlay) in excerpt_hints.hints.iter() {
2610 match &inlay.label {
2611 project::InlayHintLabel::String(s) => labels.push(s.to_string()),
2612 _ => unreachable!(),
2613 }
2614 }
2615 }
2616
2617 labels.sort();
2618 labels
2619 }
2620
2621 fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
2622 let mut hints = editor
2623 .visible_inlay_hints(cx)
2624 .into_iter()
2625 .map(|hint| hint.text.to_string())
2626 .collect::<Vec<_>>();
2627 hints.sort();
2628 hints
2629 }
2630}