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 buffer_snapshot,
789 multi_buffer_snapshot,
790 cx,
791 );
792 })
793 .ok();
794 }
795 Ok(())
796}
797
798fn calculate_hint_updates(
799 excerpt_id: ExcerptId,
800 invalidate: bool,
801 fetch_range: Range<language::Anchor>,
802 new_excerpt_hints: Vec<InlayHint>,
803 buffer_snapshot: &BufferSnapshot,
804 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
805 visible_hints: &[Inlay],
806) -> Option<ExcerptHintsUpdate> {
807 let mut add_to_cache = Vec::<InlayHint>::new();
808 let mut excerpt_hints_to_persist = HashMap::default();
809 for new_hint in new_excerpt_hints {
810 if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
811 continue;
812 }
813 let missing_from_cache = match &cached_excerpt_hints {
814 Some(cached_excerpt_hints) => {
815 let cached_excerpt_hints = cached_excerpt_hints.read();
816 match cached_excerpt_hints.hints.binary_search_by(|probe| {
817 probe.1.position.cmp(&new_hint.position, buffer_snapshot)
818 }) {
819 Ok(ix) => {
820 let mut missing_from_cache = true;
821 for (cached_inlay_id, cached_hint) in &cached_excerpt_hints.hints[ix..] {
822 if new_hint
823 .position
824 .cmp(&cached_hint.position, buffer_snapshot)
825 .is_gt()
826 {
827 break;
828 }
829 if cached_hint == &new_hint {
830 excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
831 missing_from_cache = false;
832 }
833 }
834 missing_from_cache
835 }
836 Err(_) => true,
837 }
838 }
839 None => true,
840 };
841 if missing_from_cache {
842 add_to_cache.push(new_hint);
843 }
844 }
845
846 let mut remove_from_visible = Vec::new();
847 let mut remove_from_cache = HashSet::default();
848 if invalidate {
849 remove_from_visible.extend(
850 visible_hints
851 .iter()
852 .filter(|hint| hint.position.excerpt_id == excerpt_id)
853 .map(|inlay_hint| inlay_hint.id)
854 .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
855 );
856
857 if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
858 let cached_excerpt_hints = cached_excerpt_hints.read();
859 remove_from_cache.extend(
860 cached_excerpt_hints
861 .hints
862 .iter()
863 .filter(|(cached_inlay_id, _)| {
864 !excerpt_hints_to_persist.contains_key(cached_inlay_id)
865 })
866 .map(|(cached_inlay_id, _)| *cached_inlay_id),
867 );
868 }
869 }
870
871 if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
872 None
873 } else {
874 Some(ExcerptHintsUpdate {
875 excerpt_id,
876 remove_from_visible,
877 remove_from_cache,
878 add_to_cache,
879 })
880 }
881}
882
883fn contains_position(
884 range: &Range<language::Anchor>,
885 position: language::Anchor,
886 buffer_snapshot: &BufferSnapshot,
887) -> bool {
888 range.start.cmp(&position, buffer_snapshot).is_le()
889 && range.end.cmp(&position, buffer_snapshot).is_ge()
890}
891
892fn apply_hint_update(
893 editor: &mut Editor,
894 new_update: ExcerptHintsUpdate,
895 query: ExcerptQuery,
896 buffer_snapshot: BufferSnapshot,
897 multi_buffer_snapshot: MultiBufferSnapshot,
898 cx: &mut ViewContext<'_, '_, Editor>,
899) {
900 let cached_excerpt_hints = editor
901 .inlay_hint_cache
902 .hints
903 .entry(new_update.excerpt_id)
904 .or_insert_with(|| {
905 Arc::new(RwLock::new(CachedExcerptHints {
906 version: query.cache_version,
907 buffer_version: buffer_snapshot.version().clone(),
908 buffer_id: query.buffer_id,
909 hints: Vec::new(),
910 }))
911 });
912 let mut cached_excerpt_hints = cached_excerpt_hints.write();
913 match query.cache_version.cmp(&cached_excerpt_hints.version) {
914 cmp::Ordering::Less => return,
915 cmp::Ordering::Greater | cmp::Ordering::Equal => {
916 cached_excerpt_hints.version = query.cache_version;
917 }
918 }
919
920 let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
921 cached_excerpt_hints
922 .hints
923 .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
924 let mut splice = InlaySplice {
925 to_remove: new_update.remove_from_visible,
926 to_insert: Vec::new(),
927 };
928 for new_hint in new_update.add_to_cache {
929 let cached_hints = &mut cached_excerpt_hints.hints;
930 let insert_position = match cached_hints
931 .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot))
932 {
933 Ok(i) => {
934 let mut insert_position = Some(i);
935 for (_, cached_hint) in &cached_hints[i..] {
936 if new_hint
937 .position
938 .cmp(&cached_hint.position, &buffer_snapshot)
939 .is_gt()
940 {
941 break;
942 }
943 if cached_hint.text() == new_hint.text() {
944 insert_position = None;
945 break;
946 }
947 }
948 insert_position
949 }
950 Err(i) => Some(i),
951 };
952
953 if let Some(insert_position) = insert_position {
954 let new_inlay_id = post_inc(&mut editor.next_inlay_id);
955 if editor
956 .inlay_hint_cache
957 .allowed_hint_kinds
958 .contains(&new_hint.kind)
959 {
960 let new_hint_position =
961 multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position);
962 splice
963 .to_insert
964 .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
965 }
966 cached_hints.insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint));
967 cached_inlays_changed = true;
968 }
969 }
970 cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
971 drop(cached_excerpt_hints);
972
973 if query.invalidate.should_invalidate() {
974 let mut outdated_excerpt_caches = HashSet::default();
975 for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
976 let excerpt_hints = excerpt_hints.read();
977 if excerpt_hints.buffer_id == query.buffer_id
978 && excerpt_id != &query.excerpt_id
979 && buffer_snapshot
980 .version()
981 .changed_since(&excerpt_hints.buffer_version)
982 {
983 outdated_excerpt_caches.insert(*excerpt_id);
984 splice
985 .to_remove
986 .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
987 }
988 }
989 cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
990 editor
991 .inlay_hint_cache
992 .hints
993 .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
994 }
995
996 let InlaySplice {
997 to_remove,
998 to_insert,
999 } = splice;
1000 let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty();
1001 if cached_inlays_changed || displayed_inlays_changed {
1002 editor.inlay_hint_cache.version += 1;
1003 }
1004 if displayed_inlays_changed {
1005 editor.splice_inlay_hints(to_remove, to_insert, cx)
1006 }
1007}
1008
1009#[cfg(test)]
1010pub mod tests {
1011 use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
1012
1013 use crate::{
1014 scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
1015 serde_json::json,
1016 ExcerptRange,
1017 };
1018 use futures::StreamExt;
1019 use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
1020 use itertools::Itertools;
1021 use language::{
1022 language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
1023 };
1024 use lsp::FakeLanguageServer;
1025 use parking_lot::Mutex;
1026 use project::{FakeFs, Project};
1027 use settings::SettingsStore;
1028 use text::{Point, ToPoint};
1029 use workspace::Workspace;
1030
1031 use crate::editor_tests::update_test_language_settings;
1032
1033 use super::*;
1034
1035 #[gpui::test]
1036 async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
1037 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1038 init_test(cx, |settings| {
1039 settings.defaults.inlay_hints = Some(InlayHintSettings {
1040 enabled: true,
1041 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1042 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1043 show_other_hints: allowed_hint_kinds.contains(&None),
1044 })
1045 });
1046
1047 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1048 let lsp_request_count = Arc::new(AtomicU32::new(0));
1049 fake_server
1050 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1051 let task_lsp_request_count = Arc::clone(&lsp_request_count);
1052 async move {
1053 assert_eq!(
1054 params.text_document.uri,
1055 lsp::Url::from_file_path(file_with_hints).unwrap(),
1056 );
1057 let current_call_id =
1058 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1059 let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
1060 for _ in 0..2 {
1061 let mut i = current_call_id;
1062 loop {
1063 new_hints.push(lsp::InlayHint {
1064 position: lsp::Position::new(0, i),
1065 label: lsp::InlayHintLabel::String(i.to_string()),
1066 kind: None,
1067 text_edits: None,
1068 tooltip: None,
1069 padding_left: None,
1070 padding_right: None,
1071 data: None,
1072 });
1073 if i == 0 {
1074 break;
1075 }
1076 i -= 1;
1077 }
1078 }
1079
1080 Ok(Some(new_hints))
1081 }
1082 })
1083 .next()
1084 .await;
1085 cx.foreground().run_until_parked();
1086
1087 let mut edits_made = 1;
1088 editor.update(cx, |editor, cx| {
1089 let expected_hints = vec!["0".to_string()];
1090 assert_eq!(
1091 expected_hints,
1092 cached_hint_labels(editor),
1093 "Should get its first hints when opening the editor"
1094 );
1095 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1096 let inlay_cache = editor.inlay_hint_cache();
1097 assert_eq!(
1098 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1099 "Cache should use editor settings to get the allowed hint kinds"
1100 );
1101 assert_eq!(
1102 inlay_cache.version, edits_made,
1103 "The editor update the cache version after every cache/view change"
1104 );
1105 });
1106
1107 editor.update(cx, |editor, cx| {
1108 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1109 editor.handle_input("some change", cx);
1110 edits_made += 1;
1111 });
1112 cx.foreground().run_until_parked();
1113 editor.update(cx, |editor, cx| {
1114 let expected_hints = vec!["0".to_string(), "1".to_string()];
1115 assert_eq!(
1116 expected_hints,
1117 cached_hint_labels(editor),
1118 "Should get new hints after an edit"
1119 );
1120 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1121 let inlay_cache = editor.inlay_hint_cache();
1122 assert_eq!(
1123 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1124 "Cache should use editor settings to get the allowed hint kinds"
1125 );
1126 assert_eq!(
1127 inlay_cache.version, edits_made,
1128 "The editor update the cache version after every cache/view change"
1129 );
1130 });
1131
1132 fake_server
1133 .request::<lsp::request::InlayHintRefreshRequest>(())
1134 .await
1135 .expect("inlay refresh request failed");
1136 edits_made += 1;
1137 cx.foreground().run_until_parked();
1138 editor.update(cx, |editor, cx| {
1139 let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()];
1140 assert_eq!(
1141 expected_hints,
1142 cached_hint_labels(editor),
1143 "Should get new hints after hint refresh/ request"
1144 );
1145 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1146 let inlay_cache = editor.inlay_hint_cache();
1147 assert_eq!(
1148 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1149 "Cache should use editor settings to get the allowed hint kinds"
1150 );
1151 assert_eq!(
1152 inlay_cache.version, edits_made,
1153 "The editor update the cache version after every cache/view change"
1154 );
1155 });
1156 }
1157
1158 #[gpui::test]
1159 async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
1160 init_test(cx, |settings| {
1161 settings.defaults.inlay_hints = Some(InlayHintSettings {
1162 enabled: true,
1163 show_type_hints: true,
1164 show_parameter_hints: true,
1165 show_other_hints: true,
1166 })
1167 });
1168
1169 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1170 let lsp_request_count = Arc::new(AtomicU32::new(0));
1171 fake_server
1172 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1173 let task_lsp_request_count = Arc::clone(&lsp_request_count);
1174 async move {
1175 assert_eq!(
1176 params.text_document.uri,
1177 lsp::Url::from_file_path(file_with_hints).unwrap(),
1178 );
1179 let current_call_id =
1180 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1181 Ok(Some(vec![lsp::InlayHint {
1182 position: lsp::Position::new(0, current_call_id),
1183 label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1184 kind: None,
1185 text_edits: None,
1186 tooltip: None,
1187 padding_left: None,
1188 padding_right: None,
1189 data: None,
1190 }]))
1191 }
1192 })
1193 .next()
1194 .await;
1195 cx.foreground().run_until_parked();
1196
1197 let mut edits_made = 1;
1198 editor.update(cx, |editor, cx| {
1199 let expected_hints = vec!["0".to_string()];
1200 assert_eq!(
1201 expected_hints,
1202 cached_hint_labels(editor),
1203 "Should get its first hints when opening the editor"
1204 );
1205 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1206 assert_eq!(
1207 editor.inlay_hint_cache().version,
1208 edits_made,
1209 "The editor update the cache version after every cache/view change"
1210 );
1211 });
1212
1213 let progress_token = "test_progress_token";
1214 fake_server
1215 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
1216 token: lsp::ProgressToken::String(progress_token.to_string()),
1217 })
1218 .await
1219 .expect("work done progress create request failed");
1220 cx.foreground().run_until_parked();
1221 fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1222 token: lsp::ProgressToken::String(progress_token.to_string()),
1223 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1224 lsp::WorkDoneProgressBegin::default(),
1225 )),
1226 });
1227 cx.foreground().run_until_parked();
1228
1229 editor.update(cx, |editor, cx| {
1230 let expected_hints = vec!["0".to_string()];
1231 assert_eq!(
1232 expected_hints,
1233 cached_hint_labels(editor),
1234 "Should not update hints while the work task is running"
1235 );
1236 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1237 assert_eq!(
1238 editor.inlay_hint_cache().version,
1239 edits_made,
1240 "Should not update the cache while the work task is running"
1241 );
1242 });
1243
1244 fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1245 token: lsp::ProgressToken::String(progress_token.to_string()),
1246 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1247 lsp::WorkDoneProgressEnd::default(),
1248 )),
1249 });
1250 cx.foreground().run_until_parked();
1251
1252 edits_made += 1;
1253 editor.update(cx, |editor, cx| {
1254 let expected_hints = vec!["1".to_string()];
1255 assert_eq!(
1256 expected_hints,
1257 cached_hint_labels(editor),
1258 "New hints should be queried after the work task is done"
1259 );
1260 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1261 assert_eq!(
1262 editor.inlay_hint_cache().version,
1263 edits_made,
1264 "Cache version should udpate once after the work task is done"
1265 );
1266 });
1267 }
1268
1269 #[gpui::test]
1270 async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1271 init_test(cx, |settings| {
1272 settings.defaults.inlay_hints = Some(InlayHintSettings {
1273 enabled: true,
1274 show_type_hints: true,
1275 show_parameter_hints: true,
1276 show_other_hints: true,
1277 })
1278 });
1279
1280 let fs = FakeFs::new(cx.background());
1281 fs.insert_tree(
1282 "/a",
1283 json!({
1284 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1285 "other.md": "Test md file with some text",
1286 }),
1287 )
1288 .await;
1289 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1290 let workspace = cx
1291 .add_window(|cx| Workspace::test_new(project.clone(), cx))
1292 .root(cx);
1293 let worktree_id = workspace.update(cx, |workspace, cx| {
1294 workspace.project().read_with(cx, |project, cx| {
1295 project.worktrees(cx).next().unwrap().read(cx).id()
1296 })
1297 });
1298
1299 let mut rs_fake_servers = None;
1300 let mut md_fake_servers = None;
1301 for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1302 let mut language = Language::new(
1303 LanguageConfig {
1304 name: name.into(),
1305 path_suffixes: vec![path_suffix.to_string()],
1306 ..Default::default()
1307 },
1308 Some(tree_sitter_rust::language()),
1309 );
1310 let fake_servers = language
1311 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1312 name,
1313 capabilities: lsp::ServerCapabilities {
1314 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1315 ..Default::default()
1316 },
1317 ..Default::default()
1318 }))
1319 .await;
1320 match name {
1321 "Rust" => rs_fake_servers = Some(fake_servers),
1322 "Markdown" => md_fake_servers = Some(fake_servers),
1323 _ => unreachable!(),
1324 }
1325 project.update(cx, |project, _| {
1326 project.languages().add(Arc::new(language));
1327 });
1328 }
1329
1330 let _rs_buffer = project
1331 .update(cx, |project, cx| {
1332 project.open_local_buffer("/a/main.rs", cx)
1333 })
1334 .await
1335 .unwrap();
1336 cx.foreground().run_until_parked();
1337 cx.foreground().start_waiting();
1338 let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1339 let rs_editor = workspace
1340 .update(cx, |workspace, cx| {
1341 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1342 })
1343 .await
1344 .unwrap()
1345 .downcast::<Editor>()
1346 .unwrap();
1347 let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1348 rs_fake_server
1349 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1350 let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
1351 async move {
1352 assert_eq!(
1353 params.text_document.uri,
1354 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1355 );
1356 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1357 Ok(Some(vec![lsp::InlayHint {
1358 position: lsp::Position::new(0, i),
1359 label: lsp::InlayHintLabel::String(i.to_string()),
1360 kind: None,
1361 text_edits: None,
1362 tooltip: None,
1363 padding_left: None,
1364 padding_right: None,
1365 data: None,
1366 }]))
1367 }
1368 })
1369 .next()
1370 .await;
1371 cx.foreground().run_until_parked();
1372 rs_editor.update(cx, |editor, cx| {
1373 let expected_hints = vec!["0".to_string()];
1374 assert_eq!(
1375 expected_hints,
1376 cached_hint_labels(editor),
1377 "Should get its first hints when opening the editor"
1378 );
1379 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1380 assert_eq!(
1381 editor.inlay_hint_cache().version,
1382 1,
1383 "Rust editor update the cache version after every cache/view change"
1384 );
1385 });
1386
1387 cx.foreground().run_until_parked();
1388 let _md_buffer = project
1389 .update(cx, |project, cx| {
1390 project.open_local_buffer("/a/other.md", cx)
1391 })
1392 .await
1393 .unwrap();
1394 cx.foreground().run_until_parked();
1395 cx.foreground().start_waiting();
1396 let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1397 let md_editor = workspace
1398 .update(cx, |workspace, cx| {
1399 workspace.open_path((worktree_id, "other.md"), None, true, cx)
1400 })
1401 .await
1402 .unwrap()
1403 .downcast::<Editor>()
1404 .unwrap();
1405 let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1406 md_fake_server
1407 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1408 let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
1409 async move {
1410 assert_eq!(
1411 params.text_document.uri,
1412 lsp::Url::from_file_path("/a/other.md").unwrap(),
1413 );
1414 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1415 Ok(Some(vec![lsp::InlayHint {
1416 position: lsp::Position::new(0, i),
1417 label: lsp::InlayHintLabel::String(i.to_string()),
1418 kind: None,
1419 text_edits: None,
1420 tooltip: None,
1421 padding_left: None,
1422 padding_right: None,
1423 data: None,
1424 }]))
1425 }
1426 })
1427 .next()
1428 .await;
1429 cx.foreground().run_until_parked();
1430 md_editor.update(cx, |editor, cx| {
1431 let expected_hints = vec!["0".to_string()];
1432 assert_eq!(
1433 expected_hints,
1434 cached_hint_labels(editor),
1435 "Markdown editor should have a separate verison, repeating Rust editor rules"
1436 );
1437 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1438 assert_eq!(editor.inlay_hint_cache().version, 1);
1439 });
1440
1441 rs_editor.update(cx, |editor, cx| {
1442 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1443 editor.handle_input("some rs change", cx);
1444 });
1445 cx.foreground().run_until_parked();
1446 rs_editor.update(cx, |editor, cx| {
1447 let expected_hints = vec!["1".to_string()];
1448 assert_eq!(
1449 expected_hints,
1450 cached_hint_labels(editor),
1451 "Rust inlay cache should change after the edit"
1452 );
1453 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1454 assert_eq!(
1455 editor.inlay_hint_cache().version,
1456 2,
1457 "Every time hint cache changes, cache version should be incremented"
1458 );
1459 });
1460 md_editor.update(cx, |editor, cx| {
1461 let expected_hints = vec!["0".to_string()];
1462 assert_eq!(
1463 expected_hints,
1464 cached_hint_labels(editor),
1465 "Markdown editor should not be affected by Rust editor changes"
1466 );
1467 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1468 assert_eq!(editor.inlay_hint_cache().version, 1);
1469 });
1470
1471 md_editor.update(cx, |editor, cx| {
1472 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1473 editor.handle_input("some md change", cx);
1474 });
1475 cx.foreground().run_until_parked();
1476 md_editor.update(cx, |editor, cx| {
1477 let expected_hints = vec!["1".to_string()];
1478 assert_eq!(
1479 expected_hints,
1480 cached_hint_labels(editor),
1481 "Rust editor should not be affected by Markdown editor changes"
1482 );
1483 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1484 assert_eq!(editor.inlay_hint_cache().version, 2);
1485 });
1486 rs_editor.update(cx, |editor, cx| {
1487 let expected_hints = vec!["1".to_string()];
1488 assert_eq!(
1489 expected_hints,
1490 cached_hint_labels(editor),
1491 "Markdown editor should also change independently"
1492 );
1493 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1494 assert_eq!(editor.inlay_hint_cache().version, 2);
1495 });
1496 }
1497
1498 #[gpui::test]
1499 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1500 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1501 init_test(cx, |settings| {
1502 settings.defaults.inlay_hints = Some(InlayHintSettings {
1503 enabled: true,
1504 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1505 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1506 show_other_hints: allowed_hint_kinds.contains(&None),
1507 })
1508 });
1509
1510 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1511 let lsp_request_count = Arc::new(AtomicU32::new(0));
1512 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1513 fake_server
1514 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1515 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1516 async move {
1517 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1518 assert_eq!(
1519 params.text_document.uri,
1520 lsp::Url::from_file_path(file_with_hints).unwrap(),
1521 );
1522 Ok(Some(vec![
1523 lsp::InlayHint {
1524 position: lsp::Position::new(0, 1),
1525 label: lsp::InlayHintLabel::String("type hint".to_string()),
1526 kind: Some(lsp::InlayHintKind::TYPE),
1527 text_edits: None,
1528 tooltip: None,
1529 padding_left: None,
1530 padding_right: None,
1531 data: None,
1532 },
1533 lsp::InlayHint {
1534 position: lsp::Position::new(0, 2),
1535 label: lsp::InlayHintLabel::String("parameter hint".to_string()),
1536 kind: Some(lsp::InlayHintKind::PARAMETER),
1537 text_edits: None,
1538 tooltip: None,
1539 padding_left: None,
1540 padding_right: None,
1541 data: None,
1542 },
1543 lsp::InlayHint {
1544 position: lsp::Position::new(0, 3),
1545 label: lsp::InlayHintLabel::String("other hint".to_string()),
1546 kind: None,
1547 text_edits: None,
1548 tooltip: None,
1549 padding_left: None,
1550 padding_right: None,
1551 data: None,
1552 },
1553 ]))
1554 }
1555 })
1556 .next()
1557 .await;
1558 cx.foreground().run_until_parked();
1559
1560 let mut edits_made = 1;
1561 editor.update(cx, |editor, cx| {
1562 assert_eq!(
1563 lsp_request_count.load(Ordering::Relaxed),
1564 1,
1565 "Should query new hints once"
1566 );
1567 assert_eq!(
1568 vec![
1569 "other hint".to_string(),
1570 "parameter hint".to_string(),
1571 "type hint".to_string(),
1572 ],
1573 cached_hint_labels(editor),
1574 "Should get its first hints when opening the editor"
1575 );
1576 assert_eq!(
1577 vec!["other hint".to_string(), "type hint".to_string()],
1578 visible_hint_labels(editor, cx)
1579 );
1580 let inlay_cache = editor.inlay_hint_cache();
1581 assert_eq!(
1582 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1583 "Cache should use editor settings to get the allowed hint kinds"
1584 );
1585 assert_eq!(
1586 inlay_cache.version, edits_made,
1587 "The editor update the cache version after every cache/view change"
1588 );
1589 });
1590
1591 fake_server
1592 .request::<lsp::request::InlayHintRefreshRequest>(())
1593 .await
1594 .expect("inlay refresh request failed");
1595 cx.foreground().run_until_parked();
1596 editor.update(cx, |editor, cx| {
1597 assert_eq!(
1598 lsp_request_count.load(Ordering::Relaxed),
1599 2,
1600 "Should load new hints twice"
1601 );
1602 assert_eq!(
1603 vec![
1604 "other hint".to_string(),
1605 "parameter hint".to_string(),
1606 "type hint".to_string(),
1607 ],
1608 cached_hint_labels(editor),
1609 "Cached hints should not change due to allowed hint kinds settings update"
1610 );
1611 assert_eq!(
1612 vec!["other hint".to_string(), "type hint".to_string()],
1613 visible_hint_labels(editor, cx)
1614 );
1615 assert_eq!(
1616 editor.inlay_hint_cache().version,
1617 edits_made,
1618 "Should not update cache version due to new loaded hints being the same"
1619 );
1620 });
1621
1622 for (new_allowed_hint_kinds, expected_visible_hints) in [
1623 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1624 (
1625 HashSet::from_iter([Some(InlayHintKind::Type)]),
1626 vec!["type hint".to_string()],
1627 ),
1628 (
1629 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1630 vec!["parameter hint".to_string()],
1631 ),
1632 (
1633 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1634 vec!["other hint".to_string(), "type hint".to_string()],
1635 ),
1636 (
1637 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1638 vec!["other hint".to_string(), "parameter hint".to_string()],
1639 ),
1640 (
1641 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1642 vec!["parameter hint".to_string(), "type hint".to_string()],
1643 ),
1644 (
1645 HashSet::from_iter([
1646 None,
1647 Some(InlayHintKind::Type),
1648 Some(InlayHintKind::Parameter),
1649 ]),
1650 vec![
1651 "other hint".to_string(),
1652 "parameter hint".to_string(),
1653 "type hint".to_string(),
1654 ],
1655 ),
1656 ] {
1657 edits_made += 1;
1658 update_test_language_settings(cx, |settings| {
1659 settings.defaults.inlay_hints = Some(InlayHintSettings {
1660 enabled: true,
1661 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1662 show_parameter_hints: new_allowed_hint_kinds
1663 .contains(&Some(InlayHintKind::Parameter)),
1664 show_other_hints: new_allowed_hint_kinds.contains(&None),
1665 })
1666 });
1667 cx.foreground().run_until_parked();
1668 editor.update(cx, |editor, cx| {
1669 assert_eq!(
1670 lsp_request_count.load(Ordering::Relaxed),
1671 2,
1672 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1673 );
1674 assert_eq!(
1675 vec![
1676 "other hint".to_string(),
1677 "parameter hint".to_string(),
1678 "type hint".to_string(),
1679 ],
1680 cached_hint_labels(editor),
1681 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1682 );
1683 assert_eq!(
1684 expected_visible_hints,
1685 visible_hint_labels(editor, cx),
1686 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1687 );
1688 let inlay_cache = editor.inlay_hint_cache();
1689 assert_eq!(
1690 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1691 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1692 );
1693 assert_eq!(
1694 inlay_cache.version, edits_made,
1695 "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
1696 );
1697 });
1698 }
1699
1700 edits_made += 1;
1701 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1702 update_test_language_settings(cx, |settings| {
1703 settings.defaults.inlay_hints = Some(InlayHintSettings {
1704 enabled: false,
1705 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1706 show_parameter_hints: another_allowed_hint_kinds
1707 .contains(&Some(InlayHintKind::Parameter)),
1708 show_other_hints: another_allowed_hint_kinds.contains(&None),
1709 })
1710 });
1711 cx.foreground().run_until_parked();
1712 editor.update(cx, |editor, cx| {
1713 assert_eq!(
1714 lsp_request_count.load(Ordering::Relaxed),
1715 2,
1716 "Should not load new hints when hints got disabled"
1717 );
1718 assert!(
1719 cached_hint_labels(editor).is_empty(),
1720 "Should clear the cache when hints got disabled"
1721 );
1722 assert!(
1723 visible_hint_labels(editor, cx).is_empty(),
1724 "Should clear visible hints when hints got disabled"
1725 );
1726 let inlay_cache = editor.inlay_hint_cache();
1727 assert_eq!(
1728 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1729 "Should update its allowed hint kinds even when hints got disabled"
1730 );
1731 assert_eq!(
1732 inlay_cache.version, edits_made,
1733 "The editor should update the cache version after hints got disabled"
1734 );
1735 });
1736
1737 fake_server
1738 .request::<lsp::request::InlayHintRefreshRequest>(())
1739 .await
1740 .expect("inlay refresh request failed");
1741 cx.foreground().run_until_parked();
1742 editor.update(cx, |editor, cx| {
1743 assert_eq!(
1744 lsp_request_count.load(Ordering::Relaxed),
1745 2,
1746 "Should not load new hints when they got disabled"
1747 );
1748 assert!(cached_hint_labels(editor).is_empty());
1749 assert!(visible_hint_labels(editor, cx).is_empty());
1750 assert_eq!(
1751 editor.inlay_hint_cache().version, edits_made,
1752 "The editor should not update the cache version after /refresh query without updates"
1753 );
1754 });
1755
1756 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1757 edits_made += 1;
1758 update_test_language_settings(cx, |settings| {
1759 settings.defaults.inlay_hints = Some(InlayHintSettings {
1760 enabled: true,
1761 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1762 show_parameter_hints: final_allowed_hint_kinds
1763 .contains(&Some(InlayHintKind::Parameter)),
1764 show_other_hints: final_allowed_hint_kinds.contains(&None),
1765 })
1766 });
1767 cx.foreground().run_until_parked();
1768 editor.update(cx, |editor, cx| {
1769 assert_eq!(
1770 lsp_request_count.load(Ordering::Relaxed),
1771 3,
1772 "Should query for new hints when they got reenabled"
1773 );
1774 assert_eq!(
1775 vec![
1776 "other hint".to_string(),
1777 "parameter hint".to_string(),
1778 "type hint".to_string(),
1779 ],
1780 cached_hint_labels(editor),
1781 "Should get its cached hints fully repopulated after the hints got reenabled"
1782 );
1783 assert_eq!(
1784 vec!["parameter hint".to_string()],
1785 visible_hint_labels(editor, cx),
1786 "Should get its visible hints repopulated and filtered after the h"
1787 );
1788 let inlay_cache = editor.inlay_hint_cache();
1789 assert_eq!(
1790 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
1791 "Cache should update editor settings when hints got reenabled"
1792 );
1793 assert_eq!(
1794 inlay_cache.version, edits_made,
1795 "Cache should update its version after hints got reenabled"
1796 );
1797 });
1798
1799 fake_server
1800 .request::<lsp::request::InlayHintRefreshRequest>(())
1801 .await
1802 .expect("inlay refresh request failed");
1803 cx.foreground().run_until_parked();
1804 editor.update(cx, |editor, cx| {
1805 assert_eq!(
1806 lsp_request_count.load(Ordering::Relaxed),
1807 4,
1808 "Should query for new hints again"
1809 );
1810 assert_eq!(
1811 vec![
1812 "other hint".to_string(),
1813 "parameter hint".to_string(),
1814 "type hint".to_string(),
1815 ],
1816 cached_hint_labels(editor),
1817 );
1818 assert_eq!(
1819 vec!["parameter hint".to_string()],
1820 visible_hint_labels(editor, cx),
1821 );
1822 assert_eq!(editor.inlay_hint_cache().version, edits_made);
1823 });
1824 }
1825
1826 #[gpui::test]
1827 async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
1828 init_test(cx, |settings| {
1829 settings.defaults.inlay_hints = Some(InlayHintSettings {
1830 enabled: true,
1831 show_type_hints: true,
1832 show_parameter_hints: true,
1833 show_other_hints: true,
1834 })
1835 });
1836
1837 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1838 let fake_server = Arc::new(fake_server);
1839 let lsp_request_count = Arc::new(AtomicU32::new(0));
1840 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1841 fake_server
1842 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1843 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1844 async move {
1845 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
1846 assert_eq!(
1847 params.text_document.uri,
1848 lsp::Url::from_file_path(file_with_hints).unwrap(),
1849 );
1850 Ok(Some(vec![lsp::InlayHint {
1851 position: lsp::Position::new(0, i),
1852 label: lsp::InlayHintLabel::String(i.to_string()),
1853 kind: None,
1854 text_edits: None,
1855 tooltip: None,
1856 padding_left: None,
1857 padding_right: None,
1858 data: None,
1859 }]))
1860 }
1861 })
1862 .next()
1863 .await;
1864
1865 let mut expected_changes = Vec::new();
1866 for change_after_opening in [
1867 "initial change #1",
1868 "initial change #2",
1869 "initial change #3",
1870 ] {
1871 editor.update(cx, |editor, cx| {
1872 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1873 editor.handle_input(change_after_opening, cx);
1874 });
1875 expected_changes.push(change_after_opening);
1876 }
1877
1878 cx.foreground().run_until_parked();
1879
1880 editor.update(cx, |editor, cx| {
1881 let current_text = editor.text(cx);
1882 for change in &expected_changes {
1883 assert!(
1884 current_text.contains(change),
1885 "Should apply all changes made"
1886 );
1887 }
1888 assert_eq!(
1889 lsp_request_count.load(Ordering::Relaxed),
1890 2,
1891 "Should query new hints twice: for editor init and for the last edit that interrupted all others"
1892 );
1893 let expected_hints = vec!["2".to_string()];
1894 assert_eq!(
1895 expected_hints,
1896 cached_hint_labels(editor),
1897 "Should get hints from the last edit landed only"
1898 );
1899 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1900 assert_eq!(
1901 editor.inlay_hint_cache().version, 1,
1902 "Only one update should be registered in the cache after all cancellations"
1903 );
1904 });
1905
1906 let mut edits = Vec::new();
1907 for async_later_change in [
1908 "another change #1",
1909 "another change #2",
1910 "another change #3",
1911 ] {
1912 expected_changes.push(async_later_change);
1913 let task_editor = editor.clone();
1914 let mut task_cx = cx.clone();
1915 edits.push(cx.foreground().spawn(async move {
1916 task_editor.update(&mut task_cx, |editor, cx| {
1917 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1918 editor.handle_input(async_later_change, cx);
1919 });
1920 }));
1921 }
1922 let _ = future::join_all(edits).await;
1923 cx.foreground().run_until_parked();
1924
1925 editor.update(cx, |editor, cx| {
1926 let current_text = editor.text(cx);
1927 for change in &expected_changes {
1928 assert!(
1929 current_text.contains(change),
1930 "Should apply all changes made"
1931 );
1932 }
1933 assert_eq!(
1934 lsp_request_count.load(Ordering::SeqCst),
1935 3,
1936 "Should query new hints one more time, for the last edit only"
1937 );
1938 let expected_hints = vec!["3".to_string()];
1939 assert_eq!(
1940 expected_hints,
1941 cached_hint_labels(editor),
1942 "Should get hints from the last edit landed only"
1943 );
1944 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1945 assert_eq!(
1946 editor.inlay_hint_cache().version,
1947 2,
1948 "Should update the cache version once more, for the new change"
1949 );
1950 });
1951 }
1952
1953 #[gpui::test]
1954 async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
1955 init_test(cx, |settings| {
1956 settings.defaults.inlay_hints = Some(InlayHintSettings {
1957 enabled: true,
1958 show_type_hints: true,
1959 show_parameter_hints: true,
1960 show_other_hints: true,
1961 })
1962 });
1963
1964 let mut language = Language::new(
1965 LanguageConfig {
1966 name: "Rust".into(),
1967 path_suffixes: vec!["rs".to_string()],
1968 ..Default::default()
1969 },
1970 Some(tree_sitter_rust::language()),
1971 );
1972 let mut fake_servers = language
1973 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1974 capabilities: lsp::ServerCapabilities {
1975 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1976 ..Default::default()
1977 },
1978 ..Default::default()
1979 }))
1980 .await;
1981 let fs = FakeFs::new(cx.background());
1982 fs.insert_tree(
1983 "/a",
1984 json!({
1985 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
1986 "other.rs": "// Test file",
1987 }),
1988 )
1989 .await;
1990 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1991 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
1992 let workspace = cx
1993 .add_window(|cx| Workspace::test_new(project.clone(), cx))
1994 .root(cx);
1995 let worktree_id = workspace.update(cx, |workspace, cx| {
1996 workspace.project().read_with(cx, |project, cx| {
1997 project.worktrees(cx).next().unwrap().read(cx).id()
1998 })
1999 });
2000
2001 let _buffer = project
2002 .update(cx, |project, cx| {
2003 project.open_local_buffer("/a/main.rs", cx)
2004 })
2005 .await
2006 .unwrap();
2007 cx.foreground().run_until_parked();
2008 cx.foreground().start_waiting();
2009 let fake_server = fake_servers.next().await.unwrap();
2010 let editor = workspace
2011 .update(cx, |workspace, cx| {
2012 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
2013 })
2014 .await
2015 .unwrap()
2016 .downcast::<Editor>()
2017 .unwrap();
2018 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2019 let lsp_request_count = Arc::new(AtomicUsize::new(0));
2020 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
2021 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2022 fake_server
2023 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2024 let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
2025 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2026 async move {
2027 assert_eq!(
2028 params.text_document.uri,
2029 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2030 );
2031
2032 task_lsp_request_ranges.lock().push(params.range);
2033 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
2034 Ok(Some(vec![lsp::InlayHint {
2035 position: params.range.end,
2036 label: lsp::InlayHintLabel::String(i.to_string()),
2037 kind: None,
2038 text_edits: None,
2039 tooltip: None,
2040 padding_left: None,
2041 padding_right: None,
2042 data: None,
2043 }]))
2044 }
2045 })
2046 .next()
2047 .await;
2048 fn editor_visible_range(
2049 editor: &ViewHandle<Editor>,
2050 cx: &mut gpui::TestAppContext,
2051 ) -> Range<Point> {
2052 let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx));
2053 assert_eq!(
2054 ranges.len(),
2055 1,
2056 "Single buffer should produce a single excerpt with visible range"
2057 );
2058 let (_, (excerpt_buffer, _, excerpt_visible_range)) =
2059 ranges.into_iter().next().unwrap();
2060 excerpt_buffer.update(cx, |buffer, _| {
2061 let snapshot = buffer.snapshot();
2062 let start = buffer
2063 .anchor_before(excerpt_visible_range.start)
2064 .to_point(&snapshot);
2065 let end = buffer
2066 .anchor_after(excerpt_visible_range.end)
2067 .to_point(&snapshot);
2068 start..end
2069 })
2070 }
2071
2072 // in large buffers, requests are made for more than visible range of a buffer.
2073 // invisible parts are queried later, to avoid excessive requests on quick typing.
2074 // wait the timeout needed to get all requests.
2075 cx.foreground().advance_clock(Duration::from_millis(
2076 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2077 ));
2078 cx.foreground().run_until_parked();
2079 let initial_visible_range = editor_visible_range(&editor, cx);
2080 let lsp_initial_visible_range = lsp::Range::new(
2081 lsp::Position::new(
2082 initial_visible_range.start.row,
2083 initial_visible_range.start.column,
2084 ),
2085 lsp::Position::new(
2086 initial_visible_range.end.row,
2087 initial_visible_range.end.column,
2088 ),
2089 );
2090 let expected_initial_query_range_end =
2091 lsp::Position::new(initial_visible_range.end.row * 2, 2);
2092 let mut expected_invisible_query_start = lsp_initial_visible_range.end;
2093 expected_invisible_query_start.character += 1;
2094 editor.update(cx, |editor, cx| {
2095 let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2096 assert_eq!(ranges.len(), 2,
2097 "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:?}");
2098 let visible_query_range = &ranges[0];
2099 assert_eq!(visible_query_range.start, lsp_initial_visible_range.start);
2100 assert_eq!(visible_query_range.end, lsp_initial_visible_range.end);
2101 let invisible_query_range = &ranges[1];
2102
2103 assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document");
2104 assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document");
2105
2106 let requests_count = lsp_request_count.load(Ordering::Acquire);
2107 assert_eq!(requests_count, 2, "Visible + invisible request");
2108 let expected_hints = vec!["1".to_string(), "2".to_string()];
2109 assert_eq!(
2110 expected_hints,
2111 cached_hint_labels(editor),
2112 "Should have hints from both LSP requests made for a big file"
2113 );
2114 assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
2115 assert_eq!(
2116 editor.inlay_hint_cache().version, requests_count,
2117 "LSP queries should've bumped the cache version"
2118 );
2119 });
2120
2121 editor.update(cx, |editor, cx| {
2122 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
2123 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
2124 });
2125 cx.foreground().advance_clock(Duration::from_millis(
2126 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2127 ));
2128 cx.foreground().run_until_parked();
2129 let visible_range_after_scrolls = editor_visible_range(&editor, cx);
2130 let visible_line_count =
2131 editor.update(cx, |editor, _| editor.visible_line_count().unwrap());
2132 let selection_in_cached_range = editor.update(cx, |editor, cx| {
2133 let ranges = lsp_request_ranges
2134 .lock()
2135 .drain(..)
2136 .sorted_by_key(|r| r.start)
2137 .collect::<Vec<_>>();
2138 assert_eq!(
2139 ranges.len(),
2140 2,
2141 "Should query 2 ranges after both scrolls, but got: {ranges:?}"
2142 );
2143 let first_scroll = &ranges[0];
2144 let second_scroll = &ranges[1];
2145 assert_eq!(
2146 first_scroll.end, second_scroll.start,
2147 "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
2148 );
2149 assert_eq!(
2150 first_scroll.start, expected_initial_query_range_end,
2151 "First scroll should start the query right after the end of the original scroll",
2152 );
2153 assert_eq!(
2154 second_scroll.end,
2155 lsp::Position::new(
2156 visible_range_after_scrolls.end.row
2157 + visible_line_count.ceil() as u32,
2158 1,
2159 ),
2160 "Second scroll should query one more screen down after the end of the visible range"
2161 );
2162
2163 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2164 assert_eq!(lsp_requests, 4, "Should query for hints after every scroll");
2165 let expected_hints = vec![
2166 "1".to_string(),
2167 "2".to_string(),
2168 "3".to_string(),
2169 "4".to_string(),
2170 ];
2171 assert_eq!(
2172 expected_hints,
2173 cached_hint_labels(editor),
2174 "Should have hints from the new LSP response after the edit"
2175 );
2176 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2177 assert_eq!(
2178 editor.inlay_hint_cache().version,
2179 lsp_requests,
2180 "Should update the cache for every LSP response with hints added"
2181 );
2182
2183 let mut selection_in_cached_range = visible_range_after_scrolls.end;
2184 selection_in_cached_range.row -= visible_line_count.ceil() as u32;
2185 selection_in_cached_range
2186 });
2187
2188 editor.update(cx, |editor, cx| {
2189 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
2190 s.select_ranges([selection_in_cached_range..selection_in_cached_range])
2191 });
2192 });
2193 cx.foreground().advance_clock(Duration::from_millis(
2194 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2195 ));
2196 cx.foreground().run_until_parked();
2197 editor.update(cx, |_, _| {
2198 let ranges = lsp_request_ranges
2199 .lock()
2200 .drain(..)
2201 .sorted_by_key(|r| r.start)
2202 .collect::<Vec<_>>();
2203 assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
2204 assert_eq!(lsp_request_count.load(Ordering::Acquire), 4);
2205 });
2206
2207 editor.update(cx, |editor, cx| {
2208 editor.handle_input("++++more text++++", cx);
2209 });
2210 cx.foreground().advance_clock(Duration::from_millis(
2211 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2212 ));
2213 cx.foreground().run_until_parked();
2214 editor.update(cx, |editor, cx| {
2215 let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2216 assert_eq!(ranges.len(), 3,
2217 "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
2218 let visible_query_range = &ranges[0];
2219 let above_query_range = &ranges[1];
2220 let below_query_range = &ranges[2];
2221 assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
2222 "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");
2223 assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line,
2224 "Visible range {visible_query_range:?} should be before below range {below_query_range:?}");
2225 assert!(above_query_range.start.line < selection_in_cached_range.row,
2226 "Hints should be queried with the selected range after the query range start");
2227 assert!(below_query_range.end.line > selection_in_cached_range.row,
2228 "Hints should be queried with the selected range before the query range end");
2229 assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32,
2230 "Hints query range should contain one more screen before");
2231 assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32,
2232 "Hints query range should contain one more screen after");
2233
2234 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2235 assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried");
2236 let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()];
2237 assert_eq!(expected_hints, cached_hint_labels(editor),
2238 "Should have hints from the new LSP response after the edit");
2239 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2240 assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added");
2241 });
2242 }
2243
2244 #[gpui::test(iterations = 10)]
2245 async fn test_multiple_excerpts_large_multibuffer(
2246 deterministic: Arc<Deterministic>,
2247 cx: &mut gpui::TestAppContext,
2248 ) {
2249 init_test(cx, |settings| {
2250 settings.defaults.inlay_hints = Some(InlayHintSettings {
2251 enabled: true,
2252 show_type_hints: true,
2253 show_parameter_hints: true,
2254 show_other_hints: true,
2255 })
2256 });
2257
2258 let mut language = Language::new(
2259 LanguageConfig {
2260 name: "Rust".into(),
2261 path_suffixes: vec!["rs".to_string()],
2262 ..Default::default()
2263 },
2264 Some(tree_sitter_rust::language()),
2265 );
2266 let mut fake_servers = language
2267 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2268 capabilities: lsp::ServerCapabilities {
2269 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2270 ..Default::default()
2271 },
2272 ..Default::default()
2273 }))
2274 .await;
2275 let language = Arc::new(language);
2276 let fs = FakeFs::new(cx.background());
2277 fs.insert_tree(
2278 "/a",
2279 json!({
2280 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2281 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2282 }),
2283 )
2284 .await;
2285 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2286 project.update(cx, |project, _| {
2287 project.languages().add(Arc::clone(&language))
2288 });
2289 let workspace = cx
2290 .add_window(|cx| Workspace::test_new(project.clone(), cx))
2291 .root(cx);
2292 let worktree_id = workspace.update(cx, |workspace, cx| {
2293 workspace.project().read_with(cx, |project, cx| {
2294 project.worktrees(cx).next().unwrap().read(cx).id()
2295 })
2296 });
2297
2298 let buffer_1 = project
2299 .update(cx, |project, cx| {
2300 project.open_buffer((worktree_id, "main.rs"), cx)
2301 })
2302 .await
2303 .unwrap();
2304 let buffer_2 = project
2305 .update(cx, |project, cx| {
2306 project.open_buffer((worktree_id, "other.rs"), cx)
2307 })
2308 .await
2309 .unwrap();
2310 let multibuffer = cx.add_model(|cx| {
2311 let mut multibuffer = MultiBuffer::new(0);
2312 multibuffer.push_excerpts(
2313 buffer_1.clone(),
2314 [
2315 ExcerptRange {
2316 context: Point::new(0, 0)..Point::new(2, 0),
2317 primary: None,
2318 },
2319 ExcerptRange {
2320 context: Point::new(4, 0)..Point::new(11, 0),
2321 primary: None,
2322 },
2323 ExcerptRange {
2324 context: Point::new(22, 0)..Point::new(33, 0),
2325 primary: None,
2326 },
2327 ExcerptRange {
2328 context: Point::new(44, 0)..Point::new(55, 0),
2329 primary: None,
2330 },
2331 ExcerptRange {
2332 context: Point::new(56, 0)..Point::new(66, 0),
2333 primary: None,
2334 },
2335 ExcerptRange {
2336 context: Point::new(67, 0)..Point::new(77, 0),
2337 primary: None,
2338 },
2339 ],
2340 cx,
2341 );
2342 multibuffer.push_excerpts(
2343 buffer_2.clone(),
2344 [
2345 ExcerptRange {
2346 context: Point::new(0, 1)..Point::new(2, 1),
2347 primary: None,
2348 },
2349 ExcerptRange {
2350 context: Point::new(4, 1)..Point::new(11, 1),
2351 primary: None,
2352 },
2353 ExcerptRange {
2354 context: Point::new(22, 1)..Point::new(33, 1),
2355 primary: None,
2356 },
2357 ExcerptRange {
2358 context: Point::new(44, 1)..Point::new(55, 1),
2359 primary: None,
2360 },
2361 ExcerptRange {
2362 context: Point::new(56, 1)..Point::new(66, 1),
2363 primary: None,
2364 },
2365 ExcerptRange {
2366 context: Point::new(67, 1)..Point::new(77, 1),
2367 primary: None,
2368 },
2369 ],
2370 cx,
2371 );
2372 multibuffer
2373 });
2374
2375 deterministic.run_until_parked();
2376 cx.foreground().run_until_parked();
2377 let editor = cx
2378 .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
2379 .root(cx);
2380 let editor_edited = Arc::new(AtomicBool::new(false));
2381 let fake_server = fake_servers.next().await.unwrap();
2382 let closure_editor_edited = Arc::clone(&editor_edited);
2383 fake_server
2384 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2385 let task_editor_edited = Arc::clone(&closure_editor_edited);
2386 async move {
2387 let hint_text = if params.text_document.uri
2388 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2389 {
2390 "main hint"
2391 } else if params.text_document.uri
2392 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2393 {
2394 "other hint"
2395 } else {
2396 panic!("unexpected uri: {:?}", params.text_document.uri);
2397 };
2398
2399 // one hint per excerpt
2400 let positions = [
2401 lsp::Position::new(0, 2),
2402 lsp::Position::new(4, 2),
2403 lsp::Position::new(22, 2),
2404 lsp::Position::new(44, 2),
2405 lsp::Position::new(56, 2),
2406 lsp::Position::new(67, 2),
2407 ];
2408 let out_of_range_hint = lsp::InlayHint {
2409 position: lsp::Position::new(
2410 params.range.start.line + 99,
2411 params.range.start.character + 99,
2412 ),
2413 label: lsp::InlayHintLabel::String(
2414 "out of excerpt range, should be ignored".to_string(),
2415 ),
2416 kind: None,
2417 text_edits: None,
2418 tooltip: None,
2419 padding_left: None,
2420 padding_right: None,
2421 data: None,
2422 };
2423
2424 let edited = task_editor_edited.load(Ordering::Acquire);
2425 Ok(Some(
2426 std::iter::once(out_of_range_hint)
2427 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2428 lsp::InlayHint {
2429 position,
2430 label: lsp::InlayHintLabel::String(format!(
2431 "{hint_text}{} #{i}",
2432 if edited { "(edited)" } else { "" },
2433 )),
2434 kind: None,
2435 text_edits: None,
2436 tooltip: None,
2437 padding_left: None,
2438 padding_right: None,
2439 data: None,
2440 }
2441 }))
2442 .collect(),
2443 ))
2444 }
2445 })
2446 .next()
2447 .await;
2448 cx.foreground().run_until_parked();
2449
2450 editor.update(cx, |editor, cx| {
2451 let expected_hints = vec![
2452 "main hint #0".to_string(),
2453 "main hint #1".to_string(),
2454 "main hint #2".to_string(),
2455 "main hint #3".to_string(),
2456 ];
2457 assert_eq!(
2458 expected_hints,
2459 cached_hint_labels(editor),
2460 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2461 );
2462 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2463 assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison");
2464 });
2465
2466 editor.update(cx, |editor, cx| {
2467 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2468 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2469 });
2470 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2471 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2472 });
2473 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2474 s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
2475 });
2476 });
2477 cx.foreground().run_until_parked();
2478 editor.update(cx, |editor, cx| {
2479 let expected_hints = vec![
2480 "main hint #0".to_string(),
2481 "main hint #1".to_string(),
2482 "main hint #2".to_string(),
2483 "main hint #3".to_string(),
2484 "main hint #4".to_string(),
2485 "main hint #5".to_string(),
2486 "other hint #0".to_string(),
2487 "other hint #1".to_string(),
2488 "other hint #2".to_string(),
2489 ];
2490 assert_eq!(expected_hints, cached_hint_labels(editor),
2491 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2492 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2493 assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
2494 "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
2495 });
2496
2497 editor.update(cx, |editor, cx| {
2498 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2499 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2500 });
2501 });
2502 cx.foreground().advance_clock(Duration::from_millis(
2503 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2504 ));
2505 cx.foreground().run_until_parked();
2506 let last_scroll_update_version = editor.update(cx, |editor, cx| {
2507 let expected_hints = vec![
2508 "main hint #0".to_string(),
2509 "main hint #1".to_string(),
2510 "main hint #2".to_string(),
2511 "main hint #3".to_string(),
2512 "main hint #4".to_string(),
2513 "main hint #5".to_string(),
2514 "other hint #0".to_string(),
2515 "other hint #1".to_string(),
2516 "other hint #2".to_string(),
2517 "other hint #3".to_string(),
2518 "other hint #4".to_string(),
2519 "other hint #5".to_string(),
2520 ];
2521 assert_eq!(expected_hints, cached_hint_labels(editor),
2522 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2523 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2524 assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
2525 expected_hints.len()
2526 });
2527
2528 editor.update(cx, |editor, cx| {
2529 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2530 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2531 });
2532 });
2533 cx.foreground().run_until_parked();
2534 editor.update(cx, |editor, cx| {
2535 let expected_hints = vec![
2536 "main hint #0".to_string(),
2537 "main hint #1".to_string(),
2538 "main hint #2".to_string(),
2539 "main hint #3".to_string(),
2540 "main hint #4".to_string(),
2541 "main hint #5".to_string(),
2542 "other hint #0".to_string(),
2543 "other hint #1".to_string(),
2544 "other hint #2".to_string(),
2545 "other hint #3".to_string(),
2546 "other hint #4".to_string(),
2547 "other hint #5".to_string(),
2548 ];
2549 assert_eq!(expected_hints, cached_hint_labels(editor),
2550 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2551 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2552 assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
2553 });
2554
2555 editor_edited.store(true, Ordering::Release);
2556 editor.update(cx, |editor, cx| {
2557 editor.change_selections(None, cx, |s| {
2558 s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
2559 });
2560 editor.handle_input("++++more text++++", cx);
2561 });
2562 cx.foreground().run_until_parked();
2563 editor.update(cx, |editor, cx| {
2564 let expected_hints = vec![
2565 "main hint(edited) #0".to_string(),
2566 "main hint(edited) #1".to_string(),
2567 "main hint(edited) #2".to_string(),
2568 "main hint(edited) #3".to_string(),
2569 "main hint(edited) #4".to_string(),
2570 "main hint(edited) #5".to_string(),
2571 "other hint(edited) #0".to_string(),
2572 "other hint(edited) #1".to_string(),
2573 ];
2574 assert_eq!(
2575 expected_hints,
2576 cached_hint_labels(editor),
2577 "After multibuffer edit, editor gets scolled back to the last selection; \
2578all hints should be invalidated and requeried for all of its visible excerpts"
2579 );
2580 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2581
2582 let current_cache_version = editor.inlay_hint_cache().version;
2583 let minimum_expected_version = last_scroll_update_version + expected_hints.len();
2584 assert!(
2585 current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
2586 "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update"
2587 );
2588 });
2589 }
2590
2591 #[gpui::test]
2592 async fn test_excerpts_removed(
2593 deterministic: Arc<Deterministic>,
2594 cx: &mut gpui::TestAppContext,
2595 ) {
2596 init_test(cx, |settings| {
2597 settings.defaults.inlay_hints = Some(InlayHintSettings {
2598 enabled: true,
2599 show_type_hints: false,
2600 show_parameter_hints: false,
2601 show_other_hints: false,
2602 })
2603 });
2604
2605 let mut language = Language::new(
2606 LanguageConfig {
2607 name: "Rust".into(),
2608 path_suffixes: vec!["rs".to_string()],
2609 ..Default::default()
2610 },
2611 Some(tree_sitter_rust::language()),
2612 );
2613 let mut fake_servers = language
2614 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2615 capabilities: lsp::ServerCapabilities {
2616 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2617 ..Default::default()
2618 },
2619 ..Default::default()
2620 }))
2621 .await;
2622 let language = Arc::new(language);
2623 let fs = FakeFs::new(cx.background());
2624 fs.insert_tree(
2625 "/a",
2626 json!({
2627 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2628 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2629 }),
2630 )
2631 .await;
2632 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2633 project.update(cx, |project, _| {
2634 project.languages().add(Arc::clone(&language))
2635 });
2636 let workspace = cx
2637 .add_window(|cx| Workspace::test_new(project.clone(), cx))
2638 .root(cx);
2639 let worktree_id = workspace.update(cx, |workspace, cx| {
2640 workspace.project().read_with(cx, |project, cx| {
2641 project.worktrees(cx).next().unwrap().read(cx).id()
2642 })
2643 });
2644
2645 let buffer_1 = project
2646 .update(cx, |project, cx| {
2647 project.open_buffer((worktree_id, "main.rs"), cx)
2648 })
2649 .await
2650 .unwrap();
2651 let buffer_2 = project
2652 .update(cx, |project, cx| {
2653 project.open_buffer((worktree_id, "other.rs"), cx)
2654 })
2655 .await
2656 .unwrap();
2657 let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
2658 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2659 let buffer_1_excerpts = multibuffer.push_excerpts(
2660 buffer_1.clone(),
2661 [ExcerptRange {
2662 context: Point::new(0, 0)..Point::new(2, 0),
2663 primary: None,
2664 }],
2665 cx,
2666 );
2667 let buffer_2_excerpts = multibuffer.push_excerpts(
2668 buffer_2.clone(),
2669 [ExcerptRange {
2670 context: Point::new(0, 1)..Point::new(2, 1),
2671 primary: None,
2672 }],
2673 cx,
2674 );
2675 (buffer_1_excerpts, buffer_2_excerpts)
2676 });
2677
2678 assert!(!buffer_1_excerpts.is_empty());
2679 assert!(!buffer_2_excerpts.is_empty());
2680
2681 deterministic.run_until_parked();
2682 cx.foreground().run_until_parked();
2683 let editor = cx
2684 .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
2685 .root(cx);
2686 let editor_edited = Arc::new(AtomicBool::new(false));
2687 let fake_server = fake_servers.next().await.unwrap();
2688 let closure_editor_edited = Arc::clone(&editor_edited);
2689 fake_server
2690 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2691 let task_editor_edited = Arc::clone(&closure_editor_edited);
2692 async move {
2693 let hint_text = if params.text_document.uri
2694 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2695 {
2696 "main hint"
2697 } else if params.text_document.uri
2698 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2699 {
2700 "other hint"
2701 } else {
2702 panic!("unexpected uri: {:?}", params.text_document.uri);
2703 };
2704
2705 let positions = [
2706 lsp::Position::new(0, 2),
2707 lsp::Position::new(4, 2),
2708 lsp::Position::new(22, 2),
2709 lsp::Position::new(44, 2),
2710 lsp::Position::new(56, 2),
2711 lsp::Position::new(67, 2),
2712 ];
2713 let out_of_range_hint = lsp::InlayHint {
2714 position: lsp::Position::new(
2715 params.range.start.line + 99,
2716 params.range.start.character + 99,
2717 ),
2718 label: lsp::InlayHintLabel::String(
2719 "out of excerpt range, should be ignored".to_string(),
2720 ),
2721 kind: None,
2722 text_edits: None,
2723 tooltip: None,
2724 padding_left: None,
2725 padding_right: None,
2726 data: None,
2727 };
2728
2729 let edited = task_editor_edited.load(Ordering::Acquire);
2730 Ok(Some(
2731 std::iter::once(out_of_range_hint)
2732 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2733 lsp::InlayHint {
2734 position,
2735 label: lsp::InlayHintLabel::String(format!(
2736 "{hint_text}{} #{i}",
2737 if edited { "(edited)" } else { "" },
2738 )),
2739 kind: None,
2740 text_edits: None,
2741 tooltip: None,
2742 padding_left: None,
2743 padding_right: None,
2744 data: None,
2745 }
2746 }))
2747 .collect(),
2748 ))
2749 }
2750 })
2751 .next()
2752 .await;
2753 cx.foreground().run_until_parked();
2754
2755 editor.update(cx, |editor, cx| {
2756 assert_eq!(
2757 vec!["main hint #0".to_string(), "other hint #0".to_string()],
2758 cached_hint_labels(editor),
2759 "Cache should update for both excerpts despite hints display was disabled"
2760 );
2761 assert!(
2762 visible_hint_labels(editor, cx).is_empty(),
2763 "All hints are disabled and should not be shown despite being present in the cache"
2764 );
2765 assert_eq!(
2766 editor.inlay_hint_cache().version,
2767 2,
2768 "Cache should update once per excerpt query"
2769 );
2770 });
2771
2772 editor.update(cx, |editor, cx| {
2773 editor.buffer().update(cx, |multibuffer, cx| {
2774 multibuffer.remove_excerpts(buffer_2_excerpts, cx)
2775 })
2776 });
2777 cx.foreground().run_until_parked();
2778 editor.update(cx, |editor, cx| {
2779 assert_eq!(
2780 vec!["main hint #0".to_string()],
2781 cached_hint_labels(editor),
2782 "For the removed excerpt, should clean corresponding cached hints"
2783 );
2784 assert!(
2785 visible_hint_labels(editor, cx).is_empty(),
2786 "All hints are disabled and should not be shown despite being present in the cache"
2787 );
2788 assert_eq!(
2789 editor.inlay_hint_cache().version,
2790 2,
2791 "Excerpt removal should trigger a cache update"
2792 );
2793 });
2794
2795 update_test_language_settings(cx, |settings| {
2796 settings.defaults.inlay_hints = Some(InlayHintSettings {
2797 enabled: true,
2798 show_type_hints: true,
2799 show_parameter_hints: true,
2800 show_other_hints: true,
2801 })
2802 });
2803 cx.foreground().run_until_parked();
2804 editor.update(cx, |editor, cx| {
2805 let expected_hints = vec!["main hint #0".to_string()];
2806 assert_eq!(
2807 expected_hints,
2808 cached_hint_labels(editor),
2809 "Hint display settings change should not change the cache"
2810 );
2811 assert_eq!(
2812 expected_hints,
2813 visible_hint_labels(editor, cx),
2814 "Settings change should make cached hints visible"
2815 );
2816 assert_eq!(
2817 editor.inlay_hint_cache().version,
2818 3,
2819 "Settings change should trigger a cache update"
2820 );
2821 });
2822 }
2823
2824 #[gpui::test]
2825 async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
2826 init_test(cx, |settings| {
2827 settings.defaults.inlay_hints = Some(InlayHintSettings {
2828 enabled: true,
2829 show_type_hints: true,
2830 show_parameter_hints: true,
2831 show_other_hints: true,
2832 })
2833 });
2834
2835 let mut language = Language::new(
2836 LanguageConfig {
2837 name: "Rust".into(),
2838 path_suffixes: vec!["rs".to_string()],
2839 ..Default::default()
2840 },
2841 Some(tree_sitter_rust::language()),
2842 );
2843 let mut fake_servers = language
2844 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2845 capabilities: lsp::ServerCapabilities {
2846 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2847 ..Default::default()
2848 },
2849 ..Default::default()
2850 }))
2851 .await;
2852 let fs = FakeFs::new(cx.background());
2853 fs.insert_tree(
2854 "/a",
2855 json!({
2856 "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)),
2857 "other.rs": "// Test file",
2858 }),
2859 )
2860 .await;
2861 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2862 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
2863 let workspace = cx
2864 .add_window(|cx| Workspace::test_new(project.clone(), cx))
2865 .root(cx);
2866 let worktree_id = workspace.update(cx, |workspace, cx| {
2867 workspace.project().read_with(cx, |project, cx| {
2868 project.worktrees(cx).next().unwrap().read(cx).id()
2869 })
2870 });
2871
2872 let _buffer = project
2873 .update(cx, |project, cx| {
2874 project.open_local_buffer("/a/main.rs", cx)
2875 })
2876 .await
2877 .unwrap();
2878 cx.foreground().run_until_parked();
2879 cx.foreground().start_waiting();
2880 let fake_server = fake_servers.next().await.unwrap();
2881 let editor = workspace
2882 .update(cx, |workspace, cx| {
2883 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
2884 })
2885 .await
2886 .unwrap()
2887 .downcast::<Editor>()
2888 .unwrap();
2889 let lsp_request_count = Arc::new(AtomicU32::new(0));
2890 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2891 fake_server
2892 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2893 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2894 async move {
2895 assert_eq!(
2896 params.text_document.uri,
2897 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2898 );
2899 let query_start = params.range.start;
2900 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
2901 Ok(Some(vec![lsp::InlayHint {
2902 position: query_start,
2903 label: lsp::InlayHintLabel::String(i.to_string()),
2904 kind: None,
2905 text_edits: None,
2906 tooltip: None,
2907 padding_left: None,
2908 padding_right: None,
2909 data: None,
2910 }]))
2911 }
2912 })
2913 .next()
2914 .await;
2915
2916 cx.foreground().run_until_parked();
2917 editor.update(cx, |editor, cx| {
2918 editor.change_selections(None, cx, |s| {
2919 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
2920 })
2921 });
2922 cx.foreground().run_until_parked();
2923 editor.update(cx, |editor, cx| {
2924 let expected_hints = vec!["1".to_string()];
2925 assert_eq!(expected_hints, cached_hint_labels(editor));
2926 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2927 assert_eq!(editor.inlay_hint_cache().version, 1);
2928 });
2929 }
2930
2931 #[gpui::test]
2932 async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
2933 init_test(cx, |settings| {
2934 settings.defaults.inlay_hints = Some(InlayHintSettings {
2935 enabled: false,
2936 show_type_hints: true,
2937 show_parameter_hints: true,
2938 show_other_hints: true,
2939 })
2940 });
2941
2942 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
2943
2944 editor.update(cx, |editor, cx| {
2945 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
2946 });
2947 cx.foreground().start_waiting();
2948 let lsp_request_count = Arc::new(AtomicU32::new(0));
2949 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2950 fake_server
2951 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2952 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2953 async move {
2954 assert_eq!(
2955 params.text_document.uri,
2956 lsp::Url::from_file_path(file_with_hints).unwrap(),
2957 );
2958
2959 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
2960 Ok(Some(vec![lsp::InlayHint {
2961 position: lsp::Position::new(0, i),
2962 label: lsp::InlayHintLabel::String(i.to_string()),
2963 kind: None,
2964 text_edits: None,
2965 tooltip: None,
2966 padding_left: None,
2967 padding_right: None,
2968 data: None,
2969 }]))
2970 }
2971 })
2972 .next()
2973 .await;
2974 cx.foreground().run_until_parked();
2975 editor.update(cx, |editor, cx| {
2976 let expected_hints = vec!["1".to_string()];
2977 assert_eq!(
2978 expected_hints,
2979 cached_hint_labels(editor),
2980 "Should display inlays after toggle despite them disabled in settings"
2981 );
2982 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2983 assert_eq!(
2984 editor.inlay_hint_cache().version,
2985 1,
2986 "First toggle should be cache's first update"
2987 );
2988 });
2989
2990 editor.update(cx, |editor, cx| {
2991 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
2992 });
2993 cx.foreground().run_until_parked();
2994 editor.update(cx, |editor, cx| {
2995 assert!(
2996 cached_hint_labels(editor).is_empty(),
2997 "Should clear hints after 2nd toggle"
2998 );
2999 assert!(visible_hint_labels(editor, cx).is_empty());
3000 assert_eq!(editor.inlay_hint_cache().version, 2);
3001 });
3002
3003 update_test_language_settings(cx, |settings| {
3004 settings.defaults.inlay_hints = Some(InlayHintSettings {
3005 enabled: true,
3006 show_type_hints: true,
3007 show_parameter_hints: true,
3008 show_other_hints: true,
3009 })
3010 });
3011 cx.foreground().run_until_parked();
3012 editor.update(cx, |editor, cx| {
3013 let expected_hints = vec!["2".to_string()];
3014 assert_eq!(
3015 expected_hints,
3016 cached_hint_labels(editor),
3017 "Should query LSP hints for the 2nd time after enabling hints in settings"
3018 );
3019 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3020 assert_eq!(editor.inlay_hint_cache().version, 3);
3021 });
3022
3023 editor.update(cx, |editor, cx| {
3024 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
3025 });
3026 cx.foreground().run_until_parked();
3027 editor.update(cx, |editor, cx| {
3028 assert!(
3029 cached_hint_labels(editor).is_empty(),
3030 "Should clear hints after enabling in settings and a 3rd toggle"
3031 );
3032 assert!(visible_hint_labels(editor, cx).is_empty());
3033 assert_eq!(editor.inlay_hint_cache().version, 4);
3034 });
3035
3036 editor.update(cx, |editor, cx| {
3037 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
3038 });
3039 cx.foreground().run_until_parked();
3040 editor.update(cx, |editor, cx| {
3041 let expected_hints = vec!["3".to_string()];
3042 assert_eq!(
3043 expected_hints,
3044 cached_hint_labels(editor),
3045 "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
3046 );
3047 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3048 assert_eq!(editor.inlay_hint_cache().version, 5);
3049 });
3050 }
3051
3052 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
3053 cx.foreground().forbid_parking();
3054
3055 cx.update(|cx| {
3056 cx.set_global(SettingsStore::test(cx));
3057 theme::init((), cx);
3058 client::init_settings(cx);
3059 language::init(cx);
3060 Project::init_settings(cx);
3061 workspace::init_settings(cx);
3062 crate::init(cx);
3063 });
3064
3065 update_test_language_settings(cx, f);
3066 }
3067
3068 async fn prepare_test_objects(
3069 cx: &mut TestAppContext,
3070 ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
3071 let mut language = Language::new(
3072 LanguageConfig {
3073 name: "Rust".into(),
3074 path_suffixes: vec!["rs".to_string()],
3075 ..Default::default()
3076 },
3077 Some(tree_sitter_rust::language()),
3078 );
3079 let mut fake_servers = language
3080 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3081 capabilities: lsp::ServerCapabilities {
3082 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3083 ..Default::default()
3084 },
3085 ..Default::default()
3086 }))
3087 .await;
3088
3089 let fs = FakeFs::new(cx.background());
3090 fs.insert_tree(
3091 "/a",
3092 json!({
3093 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
3094 "other.rs": "// Test file",
3095 }),
3096 )
3097 .await;
3098
3099 let project = Project::test(fs, ["/a".as_ref()], cx).await;
3100 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
3101 let workspace = cx
3102 .add_window(|cx| Workspace::test_new(project.clone(), cx))
3103 .root(cx);
3104 let worktree_id = workspace.update(cx, |workspace, cx| {
3105 workspace.project().read_with(cx, |project, cx| {
3106 project.worktrees(cx).next().unwrap().read(cx).id()
3107 })
3108 });
3109
3110 let _buffer = project
3111 .update(cx, |project, cx| {
3112 project.open_local_buffer("/a/main.rs", cx)
3113 })
3114 .await
3115 .unwrap();
3116 cx.foreground().run_until_parked();
3117 cx.foreground().start_waiting();
3118 let fake_server = fake_servers.next().await.unwrap();
3119 let editor = workspace
3120 .update(cx, |workspace, cx| {
3121 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
3122 })
3123 .await
3124 .unwrap()
3125 .downcast::<Editor>()
3126 .unwrap();
3127
3128 editor.update(cx, |editor, cx| {
3129 assert!(cached_hint_labels(editor).is_empty());
3130 assert!(visible_hint_labels(editor, cx).is_empty());
3131 assert_eq!(editor.inlay_hint_cache().version, 0);
3132 });
3133
3134 ("/a/main.rs", editor, fake_server)
3135 }
3136
3137 pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
3138 let mut labels = Vec::new();
3139 for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
3140 for (_, inlay) in &excerpt_hints.read().hints {
3141 labels.push(inlay.text());
3142 }
3143 }
3144
3145 labels.sort();
3146 labels
3147 }
3148
3149 pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
3150 let mut hints = editor
3151 .visible_inlay_hints(cx)
3152 .into_iter()
3153 .map(|hint| hint.text.to_string())
3154 .collect::<Vec<_>>();
3155 hints.sort();
3156 hints
3157 }
3158}