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