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