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