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