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, LanguageMatcher,
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 matcher: LanguageMatcher {
1533 path_suffixes: vec![path_suffix.to_string()],
1534 ..Default::default()
1535 },
1536 ..Default::default()
1537 },
1538 Some(tree_sitter_rust::language()),
1539 );
1540 let fake_servers = language
1541 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1542 name,
1543 capabilities: lsp::ServerCapabilities {
1544 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1545 ..Default::default()
1546 },
1547 ..Default::default()
1548 }))
1549 .await;
1550 match name {
1551 "Rust" => rs_fake_servers = Some(fake_servers),
1552 "Markdown" => md_fake_servers = Some(fake_servers),
1553 _ => unreachable!(),
1554 }
1555 project.update(cx, |project, _| {
1556 project.languages().add(Arc::new(language));
1557 });
1558 }
1559
1560 let rs_buffer = project
1561 .update(cx, |project, cx| {
1562 project.open_local_buffer("/a/main.rs", cx)
1563 })
1564 .await
1565 .unwrap();
1566 cx.executor().run_until_parked();
1567 cx.executor().start_waiting();
1568 let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1569 let rs_editor =
1570 cx.add_window(|cx| Editor::for_buffer(rs_buffer, Some(project.clone()), cx));
1571 let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1572 rs_fake_server
1573 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1574 let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
1575 async move {
1576 assert_eq!(
1577 params.text_document.uri,
1578 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1579 );
1580 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1581 Ok(Some(vec![lsp::InlayHint {
1582 position: lsp::Position::new(0, i),
1583 label: lsp::InlayHintLabel::String(i.to_string()),
1584 kind: None,
1585 text_edits: None,
1586 tooltip: None,
1587 padding_left: None,
1588 padding_right: None,
1589 data: None,
1590 }]))
1591 }
1592 })
1593 .next()
1594 .await;
1595 cx.executor().run_until_parked();
1596 rs_editor
1597 .update(cx, |editor, cx| {
1598 let expected_hints = vec!["0".to_string()];
1599 assert_eq!(
1600 expected_hints,
1601 cached_hint_labels(editor),
1602 "Should get its first hints when opening the editor"
1603 );
1604 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1605 assert_eq!(
1606 editor.inlay_hint_cache().version,
1607 1,
1608 "Rust editor update the cache version after every cache/view change"
1609 );
1610 })
1611 .unwrap();
1612
1613 cx.executor().run_until_parked();
1614 let md_buffer = project
1615 .update(cx, |project, cx| {
1616 project.open_local_buffer("/a/other.md", cx)
1617 })
1618 .await
1619 .unwrap();
1620 cx.executor().run_until_parked();
1621 cx.executor().start_waiting();
1622 let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1623 let md_editor = cx.add_window(|cx| Editor::for_buffer(md_buffer, Some(project), cx));
1624 let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1625 md_fake_server
1626 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1627 let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
1628 async move {
1629 assert_eq!(
1630 params.text_document.uri,
1631 lsp::Url::from_file_path("/a/other.md").unwrap(),
1632 );
1633 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1634 Ok(Some(vec![lsp::InlayHint {
1635 position: lsp::Position::new(0, i),
1636 label: lsp::InlayHintLabel::String(i.to_string()),
1637 kind: None,
1638 text_edits: None,
1639 tooltip: None,
1640 padding_left: None,
1641 padding_right: None,
1642 data: None,
1643 }]))
1644 }
1645 })
1646 .next()
1647 .await;
1648 cx.executor().run_until_parked();
1649 md_editor
1650 .update(cx, |editor, cx| {
1651 let expected_hints = vec!["0".to_string()];
1652 assert_eq!(
1653 expected_hints,
1654 cached_hint_labels(editor),
1655 "Markdown editor should have a separate version, repeating Rust editor rules"
1656 );
1657 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1658 assert_eq!(editor.inlay_hint_cache().version, 1);
1659 })
1660 .unwrap();
1661
1662 rs_editor
1663 .update(cx, |editor, cx| {
1664 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1665 editor.handle_input("some rs change", cx);
1666 })
1667 .unwrap();
1668 cx.executor().run_until_parked();
1669 rs_editor
1670 .update(cx, |editor, cx| {
1671 let expected_hints = vec!["1".to_string()];
1672 assert_eq!(
1673 expected_hints,
1674 cached_hint_labels(editor),
1675 "Rust inlay cache should change after the edit"
1676 );
1677 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1678 assert_eq!(
1679 editor.inlay_hint_cache().version,
1680 2,
1681 "Every time hint cache changes, cache version should be incremented"
1682 );
1683 })
1684 .unwrap();
1685 md_editor
1686 .update(cx, |editor, cx| {
1687 let expected_hints = vec!["0".to_string()];
1688 assert_eq!(
1689 expected_hints,
1690 cached_hint_labels(editor),
1691 "Markdown editor should not be affected by Rust editor changes"
1692 );
1693 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1694 assert_eq!(editor.inlay_hint_cache().version, 1);
1695 })
1696 .unwrap();
1697
1698 md_editor
1699 .update(cx, |editor, cx| {
1700 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1701 editor.handle_input("some md change", cx);
1702 })
1703 .unwrap();
1704 cx.executor().run_until_parked();
1705 md_editor
1706 .update(cx, |editor, cx| {
1707 let expected_hints = vec!["1".to_string()];
1708 assert_eq!(
1709 expected_hints,
1710 cached_hint_labels(editor),
1711 "Rust editor should not be affected by Markdown editor changes"
1712 );
1713 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1714 assert_eq!(editor.inlay_hint_cache().version, 2);
1715 })
1716 .unwrap();
1717 rs_editor
1718 .update(cx, |editor, cx| {
1719 let expected_hints = vec!["1".to_string()];
1720 assert_eq!(
1721 expected_hints,
1722 cached_hint_labels(editor),
1723 "Markdown editor should also change independently"
1724 );
1725 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1726 assert_eq!(editor.inlay_hint_cache().version, 2);
1727 })
1728 .unwrap();
1729 }
1730
1731 #[gpui::test]
1732 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1733 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1734 init_test(cx, |settings| {
1735 settings.defaults.inlay_hints = Some(InlayHintSettings {
1736 enabled: true,
1737 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1738 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1739 show_other_hints: allowed_hint_kinds.contains(&None),
1740 })
1741 });
1742
1743 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
1744 let lsp_request_count = Arc::new(AtomicU32::new(0));
1745 let another_lsp_request_count = Arc::clone(&lsp_request_count);
1746 fake_server
1747 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1748 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
1749 async move {
1750 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1751 assert_eq!(
1752 params.text_document.uri,
1753 lsp::Url::from_file_path(file_with_hints).unwrap(),
1754 );
1755 Ok(Some(vec![
1756 lsp::InlayHint {
1757 position: lsp::Position::new(0, 1),
1758 label: lsp::InlayHintLabel::String("type hint".to_string()),
1759 kind: Some(lsp::InlayHintKind::TYPE),
1760 text_edits: None,
1761 tooltip: None,
1762 padding_left: None,
1763 padding_right: None,
1764 data: None,
1765 },
1766 lsp::InlayHint {
1767 position: lsp::Position::new(0, 2),
1768 label: lsp::InlayHintLabel::String("parameter hint".to_string()),
1769 kind: Some(lsp::InlayHintKind::PARAMETER),
1770 text_edits: None,
1771 tooltip: None,
1772 padding_left: None,
1773 padding_right: None,
1774 data: None,
1775 },
1776 lsp::InlayHint {
1777 position: lsp::Position::new(0, 3),
1778 label: lsp::InlayHintLabel::String("other hint".to_string()),
1779 kind: None,
1780 text_edits: None,
1781 tooltip: None,
1782 padding_left: None,
1783 padding_right: None,
1784 data: None,
1785 },
1786 ]))
1787 }
1788 })
1789 .next()
1790 .await;
1791 cx.executor().run_until_parked();
1792
1793 let mut edits_made = 1;
1794 editor
1795 .update(cx, |editor, cx| {
1796 assert_eq!(
1797 lsp_request_count.load(Ordering::Relaxed),
1798 1,
1799 "Should query new hints once"
1800 );
1801 assert_eq!(
1802 vec![
1803 "other hint".to_string(),
1804 "parameter hint".to_string(),
1805 "type hint".to_string(),
1806 ],
1807 cached_hint_labels(editor),
1808 "Should get its first hints when opening the editor"
1809 );
1810 assert_eq!(
1811 vec!["other hint".to_string(), "type hint".to_string()],
1812 visible_hint_labels(editor, cx)
1813 );
1814 let inlay_cache = editor.inlay_hint_cache();
1815 assert_eq!(
1816 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1817 "Cache should use editor settings to get the allowed hint kinds"
1818 );
1819 assert_eq!(
1820 inlay_cache.version, edits_made,
1821 "The editor update the cache version after every cache/view change"
1822 );
1823 })
1824 .unwrap();
1825
1826 fake_server
1827 .request::<lsp::request::InlayHintRefreshRequest>(())
1828 .await
1829 .expect("inlay refresh request failed");
1830 cx.executor().run_until_parked();
1831 editor
1832 .update(cx, |editor, cx| {
1833 assert_eq!(
1834 lsp_request_count.load(Ordering::Relaxed),
1835 2,
1836 "Should load new hints twice"
1837 );
1838 assert_eq!(
1839 vec![
1840 "other hint".to_string(),
1841 "parameter hint".to_string(),
1842 "type hint".to_string(),
1843 ],
1844 cached_hint_labels(editor),
1845 "Cached hints should not change due to allowed hint kinds settings update"
1846 );
1847 assert_eq!(
1848 vec!["other hint".to_string(), "type hint".to_string()],
1849 visible_hint_labels(editor, cx)
1850 );
1851 assert_eq!(
1852 editor.inlay_hint_cache().version,
1853 edits_made,
1854 "Should not update cache version due to new loaded hints being the same"
1855 );
1856 })
1857 .unwrap();
1858
1859 for (new_allowed_hint_kinds, expected_visible_hints) in [
1860 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1861 (
1862 HashSet::from_iter([Some(InlayHintKind::Type)]),
1863 vec!["type hint".to_string()],
1864 ),
1865 (
1866 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1867 vec!["parameter hint".to_string()],
1868 ),
1869 (
1870 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1871 vec!["other hint".to_string(), "type hint".to_string()],
1872 ),
1873 (
1874 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1875 vec!["other hint".to_string(), "parameter hint".to_string()],
1876 ),
1877 (
1878 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1879 vec!["parameter hint".to_string(), "type hint".to_string()],
1880 ),
1881 (
1882 HashSet::from_iter([
1883 None,
1884 Some(InlayHintKind::Type),
1885 Some(InlayHintKind::Parameter),
1886 ]),
1887 vec![
1888 "other hint".to_string(),
1889 "parameter hint".to_string(),
1890 "type hint".to_string(),
1891 ],
1892 ),
1893 ] {
1894 edits_made += 1;
1895 update_test_language_settings(cx, |settings| {
1896 settings.defaults.inlay_hints = Some(InlayHintSettings {
1897 enabled: true,
1898 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1899 show_parameter_hints: new_allowed_hint_kinds
1900 .contains(&Some(InlayHintKind::Parameter)),
1901 show_other_hints: new_allowed_hint_kinds.contains(&None),
1902 })
1903 });
1904 cx.executor().run_until_parked();
1905 editor.update(cx, |editor, cx| {
1906 assert_eq!(
1907 lsp_request_count.load(Ordering::Relaxed),
1908 2,
1909 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1910 );
1911 assert_eq!(
1912 vec![
1913 "other hint".to_string(),
1914 "parameter hint".to_string(),
1915 "type hint".to_string(),
1916 ],
1917 cached_hint_labels(editor),
1918 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1919 );
1920 assert_eq!(
1921 expected_visible_hints,
1922 visible_hint_labels(editor, cx),
1923 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1924 );
1925 let inlay_cache = editor.inlay_hint_cache();
1926 assert_eq!(
1927 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1928 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1929 );
1930 assert_eq!(
1931 inlay_cache.version, edits_made,
1932 "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
1933 );
1934 }).unwrap();
1935 }
1936
1937 edits_made += 1;
1938 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1939 update_test_language_settings(cx, |settings| {
1940 settings.defaults.inlay_hints = Some(InlayHintSettings {
1941 enabled: false,
1942 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1943 show_parameter_hints: another_allowed_hint_kinds
1944 .contains(&Some(InlayHintKind::Parameter)),
1945 show_other_hints: another_allowed_hint_kinds.contains(&None),
1946 })
1947 });
1948 cx.executor().run_until_parked();
1949 editor
1950 .update(cx, |editor, cx| {
1951 assert_eq!(
1952 lsp_request_count.load(Ordering::Relaxed),
1953 2,
1954 "Should not load new hints when hints got disabled"
1955 );
1956 assert!(
1957 cached_hint_labels(editor).is_empty(),
1958 "Should clear the cache when hints got disabled"
1959 );
1960 assert!(
1961 visible_hint_labels(editor, cx).is_empty(),
1962 "Should clear visible hints when hints got disabled"
1963 );
1964 let inlay_cache = editor.inlay_hint_cache();
1965 assert_eq!(
1966 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1967 "Should update its allowed hint kinds even when hints got disabled"
1968 );
1969 assert_eq!(
1970 inlay_cache.version, edits_made,
1971 "The editor should update the cache version after hints got disabled"
1972 );
1973 })
1974 .unwrap();
1975
1976 fake_server
1977 .request::<lsp::request::InlayHintRefreshRequest>(())
1978 .await
1979 .expect("inlay refresh request failed");
1980 cx.executor().run_until_parked();
1981 editor.update(cx, |editor, cx| {
1982 assert_eq!(
1983 lsp_request_count.load(Ordering::Relaxed),
1984 2,
1985 "Should not load new hints when they got disabled"
1986 );
1987 assert!(cached_hint_labels(editor).is_empty());
1988 assert!(visible_hint_labels(editor, cx).is_empty());
1989 assert_eq!(
1990 editor.inlay_hint_cache().version, edits_made,
1991 "The editor should not update the cache version after /refresh query without updates"
1992 );
1993 }).unwrap();
1994
1995 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1996 edits_made += 1;
1997 update_test_language_settings(cx, |settings| {
1998 settings.defaults.inlay_hints = Some(InlayHintSettings {
1999 enabled: true,
2000 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
2001 show_parameter_hints: final_allowed_hint_kinds
2002 .contains(&Some(InlayHintKind::Parameter)),
2003 show_other_hints: final_allowed_hint_kinds.contains(&None),
2004 })
2005 });
2006 cx.executor().run_until_parked();
2007 editor
2008 .update(cx, |editor, cx| {
2009 assert_eq!(
2010 lsp_request_count.load(Ordering::Relaxed),
2011 3,
2012 "Should query for new hints when they got re-enabled"
2013 );
2014 assert_eq!(
2015 vec![
2016 "other hint".to_string(),
2017 "parameter hint".to_string(),
2018 "type hint".to_string(),
2019 ],
2020 cached_hint_labels(editor),
2021 "Should get its cached hints fully repopulated after the hints got re-enabled"
2022 );
2023 assert_eq!(
2024 vec!["parameter hint".to_string()],
2025 visible_hint_labels(editor, cx),
2026 "Should get its visible hints repopulated and filtered after the h"
2027 );
2028 let inlay_cache = editor.inlay_hint_cache();
2029 assert_eq!(
2030 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
2031 "Cache should update editor settings when hints got re-enabled"
2032 );
2033 assert_eq!(
2034 inlay_cache.version, edits_made,
2035 "Cache should update its version after hints got re-enabled"
2036 );
2037 })
2038 .unwrap();
2039
2040 fake_server
2041 .request::<lsp::request::InlayHintRefreshRequest>(())
2042 .await
2043 .expect("inlay refresh request failed");
2044 cx.executor().run_until_parked();
2045 editor
2046 .update(cx, |editor, cx| {
2047 assert_eq!(
2048 lsp_request_count.load(Ordering::Relaxed),
2049 4,
2050 "Should query for new hints again"
2051 );
2052 assert_eq!(
2053 vec![
2054 "other hint".to_string(),
2055 "parameter hint".to_string(),
2056 "type hint".to_string(),
2057 ],
2058 cached_hint_labels(editor),
2059 );
2060 assert_eq!(
2061 vec!["parameter hint".to_string()],
2062 visible_hint_labels(editor, cx),
2063 );
2064 assert_eq!(editor.inlay_hint_cache().version, edits_made);
2065 })
2066 .unwrap();
2067 }
2068
2069 #[gpui::test]
2070 async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
2071 init_test(cx, |settings| {
2072 settings.defaults.inlay_hints = Some(InlayHintSettings {
2073 enabled: true,
2074 show_type_hints: true,
2075 show_parameter_hints: true,
2076 show_other_hints: true,
2077 })
2078 });
2079
2080 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
2081 let fake_server = Arc::new(fake_server);
2082 let lsp_request_count = Arc::new(AtomicU32::new(0));
2083 let another_lsp_request_count = Arc::clone(&lsp_request_count);
2084 fake_server
2085 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2086 let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
2087 async move {
2088 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
2089 assert_eq!(
2090 params.text_document.uri,
2091 lsp::Url::from_file_path(file_with_hints).unwrap(),
2092 );
2093 Ok(Some(vec![lsp::InlayHint {
2094 position: lsp::Position::new(0, i),
2095 label: lsp::InlayHintLabel::String(i.to_string()),
2096 kind: None,
2097 text_edits: None,
2098 tooltip: None,
2099 padding_left: None,
2100 padding_right: None,
2101 data: None,
2102 }]))
2103 }
2104 })
2105 .next()
2106 .await;
2107
2108 let mut expected_changes = Vec::new();
2109 for change_after_opening in [
2110 "initial change #1",
2111 "initial change #2",
2112 "initial change #3",
2113 ] {
2114 editor
2115 .update(cx, |editor, cx| {
2116 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
2117 editor.handle_input(change_after_opening, cx);
2118 })
2119 .unwrap();
2120 expected_changes.push(change_after_opening);
2121 }
2122
2123 cx.executor().run_until_parked();
2124
2125 editor.update(cx, |editor, cx| {
2126 let current_text = editor.text(cx);
2127 for change in &expected_changes {
2128 assert!(
2129 current_text.contains(change),
2130 "Should apply all changes made"
2131 );
2132 }
2133 assert_eq!(
2134 lsp_request_count.load(Ordering::Relaxed),
2135 2,
2136 "Should query new hints twice: for editor init and for the last edit that interrupted all others"
2137 );
2138 let expected_hints = vec!["2".to_string()];
2139 assert_eq!(
2140 expected_hints,
2141 cached_hint_labels(editor),
2142 "Should get hints from the last edit landed only"
2143 );
2144 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2145 assert_eq!(
2146 editor.inlay_hint_cache().version, 1,
2147 "Only one update should be registered in the cache after all cancellations"
2148 );
2149 }).unwrap();
2150
2151 let mut edits = Vec::new();
2152 for async_later_change in [
2153 "another change #1",
2154 "another change #2",
2155 "another change #3",
2156 ] {
2157 expected_changes.push(async_later_change);
2158 let task_editor = editor.clone();
2159 edits.push(cx.spawn(|mut cx| async move {
2160 task_editor
2161 .update(&mut cx, |editor, cx| {
2162 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
2163 editor.handle_input(async_later_change, cx);
2164 })
2165 .unwrap();
2166 }));
2167 }
2168 let _ = future::join_all(edits).await;
2169 cx.executor().run_until_parked();
2170
2171 editor
2172 .update(cx, |editor, cx| {
2173 let current_text = editor.text(cx);
2174 for change in &expected_changes {
2175 assert!(
2176 current_text.contains(change),
2177 "Should apply all changes made"
2178 );
2179 }
2180 assert_eq!(
2181 lsp_request_count.load(Ordering::SeqCst),
2182 3,
2183 "Should query new hints one more time, for the last edit only"
2184 );
2185 let expected_hints = vec!["3".to_string()];
2186 assert_eq!(
2187 expected_hints,
2188 cached_hint_labels(editor),
2189 "Should get hints from the last edit landed only"
2190 );
2191 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2192 assert_eq!(
2193 editor.inlay_hint_cache().version,
2194 2,
2195 "Should update the cache version once more, for the new change"
2196 );
2197 })
2198 .unwrap();
2199 }
2200
2201 #[gpui::test(iterations = 10)]
2202 async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
2203 init_test(cx, |settings| {
2204 settings.defaults.inlay_hints = Some(InlayHintSettings {
2205 enabled: true,
2206 show_type_hints: true,
2207 show_parameter_hints: true,
2208 show_other_hints: true,
2209 })
2210 });
2211
2212 let mut language = Language::new(
2213 LanguageConfig {
2214 name: "Rust".into(),
2215 matcher: LanguageMatcher {
2216 path_suffixes: vec!["rs".to_string()],
2217 ..Default::default()
2218 },
2219 ..Default::default()
2220 },
2221 Some(tree_sitter_rust::language()),
2222 );
2223 let mut fake_servers = language
2224 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2225 capabilities: lsp::ServerCapabilities {
2226 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2227 ..Default::default()
2228 },
2229 ..Default::default()
2230 }))
2231 .await;
2232 let fs = FakeFs::new(cx.background_executor.clone());
2233 fs.insert_tree(
2234 "/a",
2235 json!({
2236 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
2237 "other.rs": "// Test file",
2238 }),
2239 )
2240 .await;
2241 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2242 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
2243 let buffer = project
2244 .update(cx, |project, cx| {
2245 project.open_local_buffer("/a/main.rs", cx)
2246 })
2247 .await
2248 .unwrap();
2249 cx.executor().run_until_parked();
2250 cx.executor().start_waiting();
2251 let fake_server = fake_servers.next().await.unwrap();
2252 let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx));
2253 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2254 let lsp_request_count = Arc::new(AtomicUsize::new(0));
2255 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
2256 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2257 fake_server
2258 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2259 let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
2260 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2261 async move {
2262 assert_eq!(
2263 params.text_document.uri,
2264 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2265 );
2266
2267 task_lsp_request_ranges.lock().push(params.range);
2268 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
2269 Ok(Some(vec![lsp::InlayHint {
2270 position: params.range.end,
2271 label: lsp::InlayHintLabel::String(i.to_string()),
2272 kind: None,
2273 text_edits: None,
2274 tooltip: None,
2275 padding_left: None,
2276 padding_right: None,
2277 data: None,
2278 }]))
2279 }
2280 })
2281 .next()
2282 .await;
2283
2284 fn editor_visible_range(
2285 editor: &WindowHandle<Editor>,
2286 cx: &mut gpui::TestAppContext,
2287 ) -> Range<Point> {
2288 let ranges = editor
2289 .update(cx, |editor, cx| {
2290 editor.excerpts_for_inlay_hints_query(None, cx)
2291 })
2292 .unwrap();
2293 assert_eq!(
2294 ranges.len(),
2295 1,
2296 "Single buffer should produce a single excerpt with visible range"
2297 );
2298 let (_, (excerpt_buffer, _, excerpt_visible_range)) =
2299 ranges.into_iter().next().unwrap();
2300 excerpt_buffer.update(cx, |buffer, _| {
2301 let snapshot = buffer.snapshot();
2302 let start = buffer
2303 .anchor_before(excerpt_visible_range.start)
2304 .to_point(&snapshot);
2305 let end = buffer
2306 .anchor_after(excerpt_visible_range.end)
2307 .to_point(&snapshot);
2308 start..end
2309 })
2310 }
2311
2312 // in large buffers, requests are made for more than visible range of a buffer.
2313 // invisible parts are queried later, to avoid excessive requests on quick typing.
2314 // wait the timeout needed to get all requests.
2315 cx.executor().advance_clock(Duration::from_millis(
2316 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2317 ));
2318 cx.executor().run_until_parked();
2319 let initial_visible_range = editor_visible_range(&editor, cx);
2320 let lsp_initial_visible_range = lsp::Range::new(
2321 lsp::Position::new(
2322 initial_visible_range.start.row,
2323 initial_visible_range.start.column,
2324 ),
2325 lsp::Position::new(
2326 initial_visible_range.end.row,
2327 initial_visible_range.end.column,
2328 ),
2329 );
2330 let expected_initial_query_range_end =
2331 lsp::Position::new(initial_visible_range.end.row * 2, 2);
2332 let mut expected_invisible_query_start = lsp_initial_visible_range.end;
2333 expected_invisible_query_start.character += 1;
2334 editor.update(cx, |editor, cx| {
2335 let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2336 assert_eq!(ranges.len(), 2,
2337 "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:?}");
2338 let visible_query_range = &ranges[0];
2339 assert_eq!(visible_query_range.start, lsp_initial_visible_range.start);
2340 assert_eq!(visible_query_range.end, lsp_initial_visible_range.end);
2341 let invisible_query_range = &ranges[1];
2342
2343 assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document");
2344 assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document");
2345
2346 let requests_count = lsp_request_count.load(Ordering::Acquire);
2347 assert_eq!(requests_count, 2, "Visible + invisible request");
2348 let expected_hints = vec!["1".to_string(), "2".to_string()];
2349 assert_eq!(
2350 expected_hints,
2351 cached_hint_labels(editor),
2352 "Should have hints from both LSP requests made for a big file"
2353 );
2354 assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
2355 assert_eq!(
2356 editor.inlay_hint_cache().version, requests_count,
2357 "LSP queries should've bumped the cache version"
2358 );
2359 }).unwrap();
2360
2361 editor
2362 .update(cx, |editor, cx| {
2363 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
2364 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
2365 })
2366 .unwrap();
2367 cx.executor().advance_clock(Duration::from_millis(
2368 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2369 ));
2370 cx.executor().run_until_parked();
2371 let visible_range_after_scrolls = editor_visible_range(&editor, cx);
2372 let visible_line_count = editor
2373 .update(cx, |editor, _| editor.visible_line_count().unwrap())
2374 .unwrap();
2375 let selection_in_cached_range = editor
2376 .update(cx, |editor, cx| {
2377 let ranges = lsp_request_ranges
2378 .lock()
2379 .drain(..)
2380 .sorted_by_key(|r| r.start)
2381 .collect::<Vec<_>>();
2382 assert_eq!(
2383 ranges.len(),
2384 2,
2385 "Should query 2 ranges after both scrolls, but got: {ranges:?}"
2386 );
2387 let first_scroll = &ranges[0];
2388 let second_scroll = &ranges[1];
2389 assert_eq!(
2390 first_scroll.end, second_scroll.start,
2391 "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
2392 );
2393 assert_eq!(
2394 first_scroll.start, expected_initial_query_range_end,
2395 "First scroll should start the query right after the end of the original scroll",
2396 );
2397 assert_eq!(
2398 second_scroll.end,
2399 lsp::Position::new(
2400 visible_range_after_scrolls.end.row
2401 + visible_line_count.ceil() as u32,
2402 1,
2403 ),
2404 "Second scroll should query one more screen down after the end of the visible range"
2405 );
2406
2407 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2408 assert_eq!(lsp_requests, 4, "Should query for hints after every scroll");
2409 let expected_hints = vec![
2410 "1".to_string(),
2411 "2".to_string(),
2412 "3".to_string(),
2413 "4".to_string(),
2414 ];
2415 assert_eq!(
2416 expected_hints,
2417 cached_hint_labels(editor),
2418 "Should have hints from the new LSP response after the edit"
2419 );
2420 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2421 assert_eq!(
2422 editor.inlay_hint_cache().version,
2423 lsp_requests,
2424 "Should update the cache for every LSP response with hints added"
2425 );
2426
2427 let mut selection_in_cached_range = visible_range_after_scrolls.end;
2428 selection_in_cached_range.row -= visible_line_count.ceil() as u32;
2429 selection_in_cached_range
2430 })
2431 .unwrap();
2432
2433 editor
2434 .update(cx, |editor, cx| {
2435 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
2436 s.select_ranges([selection_in_cached_range..selection_in_cached_range])
2437 });
2438 })
2439 .unwrap();
2440 cx.executor().advance_clock(Duration::from_millis(
2441 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2442 ));
2443 cx.executor().run_until_parked();
2444 editor.update(cx, |_, _| {
2445 let ranges = lsp_request_ranges
2446 .lock()
2447 .drain(..)
2448 .sorted_by_key(|r| r.start)
2449 .collect::<Vec<_>>();
2450 assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
2451 assert_eq!(lsp_request_count.load(Ordering::Acquire), 4);
2452 }).unwrap();
2453
2454 editor
2455 .update(cx, |editor, cx| {
2456 editor.handle_input("++++more text++++", cx);
2457 })
2458 .unwrap();
2459 cx.executor().advance_clock(Duration::from_millis(
2460 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2461 ));
2462 cx.executor().run_until_parked();
2463 editor.update(cx, |editor, cx| {
2464 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2465 ranges.sort_by_key(|r| r.start);
2466
2467 assert_eq!(ranges.len(), 3,
2468 "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
2469 let above_query_range = &ranges[0];
2470 let visible_query_range = &ranges[1];
2471 let below_query_range = &ranges[2];
2472 assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
2473 "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");
2474 assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line,
2475 "Visible range {visible_query_range:?} should be before below range {below_query_range:?}");
2476 assert!(above_query_range.start.line < selection_in_cached_range.row,
2477 "Hints should be queried with the selected range after the query range start");
2478 assert!(below_query_range.end.line > selection_in_cached_range.row,
2479 "Hints should be queried with the selected range before the query range end");
2480 assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32,
2481 "Hints query range should contain one more screen before");
2482 assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32,
2483 "Hints query range should contain one more screen after");
2484
2485 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2486 assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried");
2487 let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()];
2488 assert_eq!(expected_hints, cached_hint_labels(editor),
2489 "Should have hints from the new LSP response after the edit");
2490 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2491 assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added");
2492 }).unwrap();
2493 }
2494
2495 #[gpui::test(iterations = 30)]
2496 async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
2497 init_test(cx, |settings| {
2498 settings.defaults.inlay_hints = Some(InlayHintSettings {
2499 enabled: true,
2500 show_type_hints: true,
2501 show_parameter_hints: true,
2502 show_other_hints: true,
2503 })
2504 });
2505
2506 let mut language = Language::new(
2507 LanguageConfig {
2508 name: "Rust".into(),
2509 matcher: LanguageMatcher {
2510 path_suffixes: vec!["rs".to_string()],
2511 ..Default::default()
2512 },
2513 ..Default::default()
2514 },
2515 Some(tree_sitter_rust::language()),
2516 );
2517 let mut fake_servers = language
2518 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2519 capabilities: lsp::ServerCapabilities {
2520 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2521 ..Default::default()
2522 },
2523 ..Default::default()
2524 }))
2525 .await;
2526 let language = Arc::new(language);
2527 let fs = FakeFs::new(cx.background_executor.clone());
2528 fs.insert_tree(
2529 "/a",
2530 json!({
2531 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2532 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2533 }),
2534 )
2535 .await;
2536 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2537 project.update(cx, |project, _| {
2538 project.languages().add(Arc::clone(&language))
2539 });
2540 let worktree_id = project.update(cx, |project, cx| {
2541 project.worktrees().next().unwrap().read(cx).id()
2542 });
2543
2544 let buffer_1 = project
2545 .update(cx, |project, cx| {
2546 project.open_buffer((worktree_id, "main.rs"), cx)
2547 })
2548 .await
2549 .unwrap();
2550 let buffer_2 = project
2551 .update(cx, |project, cx| {
2552 project.open_buffer((worktree_id, "other.rs"), cx)
2553 })
2554 .await
2555 .unwrap();
2556 let multibuffer = cx.new_model(|cx| {
2557 let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
2558 multibuffer.push_excerpts(
2559 buffer_1.clone(),
2560 [
2561 ExcerptRange {
2562 context: Point::new(0, 0)..Point::new(2, 0),
2563 primary: None,
2564 },
2565 ExcerptRange {
2566 context: Point::new(4, 0)..Point::new(11, 0),
2567 primary: None,
2568 },
2569 ExcerptRange {
2570 context: Point::new(22, 0)..Point::new(33, 0),
2571 primary: None,
2572 },
2573 ExcerptRange {
2574 context: Point::new(44, 0)..Point::new(55, 0),
2575 primary: None,
2576 },
2577 ExcerptRange {
2578 context: Point::new(56, 0)..Point::new(66, 0),
2579 primary: None,
2580 },
2581 ExcerptRange {
2582 context: Point::new(67, 0)..Point::new(77, 0),
2583 primary: None,
2584 },
2585 ],
2586 cx,
2587 );
2588 multibuffer.push_excerpts(
2589 buffer_2.clone(),
2590 [
2591 ExcerptRange {
2592 context: Point::new(0, 1)..Point::new(2, 1),
2593 primary: None,
2594 },
2595 ExcerptRange {
2596 context: Point::new(4, 1)..Point::new(11, 1),
2597 primary: None,
2598 },
2599 ExcerptRange {
2600 context: Point::new(22, 1)..Point::new(33, 1),
2601 primary: None,
2602 },
2603 ExcerptRange {
2604 context: Point::new(44, 1)..Point::new(55, 1),
2605 primary: None,
2606 },
2607 ExcerptRange {
2608 context: Point::new(56, 1)..Point::new(66, 1),
2609 primary: None,
2610 },
2611 ExcerptRange {
2612 context: Point::new(67, 1)..Point::new(77, 1),
2613 primary: None,
2614 },
2615 ],
2616 cx,
2617 );
2618 multibuffer
2619 });
2620
2621 cx.executor().run_until_parked();
2622 let editor =
2623 cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
2624 let editor_edited = Arc::new(AtomicBool::new(false));
2625 let fake_server = fake_servers.next().await.unwrap();
2626 let closure_editor_edited = Arc::clone(&editor_edited);
2627 fake_server
2628 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2629 let task_editor_edited = Arc::clone(&closure_editor_edited);
2630 async move {
2631 let hint_text = if params.text_document.uri
2632 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2633 {
2634 "main hint"
2635 } else if params.text_document.uri
2636 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2637 {
2638 "other hint"
2639 } else {
2640 panic!("unexpected uri: {:?}", params.text_document.uri);
2641 };
2642
2643 // one hint per excerpt
2644 let positions = [
2645 lsp::Position::new(0, 2),
2646 lsp::Position::new(4, 2),
2647 lsp::Position::new(22, 2),
2648 lsp::Position::new(44, 2),
2649 lsp::Position::new(56, 2),
2650 lsp::Position::new(67, 2),
2651 ];
2652 let out_of_range_hint = lsp::InlayHint {
2653 position: lsp::Position::new(
2654 params.range.start.line + 99,
2655 params.range.start.character + 99,
2656 ),
2657 label: lsp::InlayHintLabel::String(
2658 "out of excerpt range, should be ignored".to_string(),
2659 ),
2660 kind: None,
2661 text_edits: None,
2662 tooltip: None,
2663 padding_left: None,
2664 padding_right: None,
2665 data: None,
2666 };
2667
2668 let edited = task_editor_edited.load(Ordering::Acquire);
2669 Ok(Some(
2670 std::iter::once(out_of_range_hint)
2671 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2672 lsp::InlayHint {
2673 position,
2674 label: lsp::InlayHintLabel::String(format!(
2675 "{hint_text}{} #{i}",
2676 if edited { "(edited)" } else { "" },
2677 )),
2678 kind: None,
2679 text_edits: None,
2680 tooltip: None,
2681 padding_left: None,
2682 padding_right: None,
2683 data: None,
2684 }
2685 }))
2686 .collect(),
2687 ))
2688 }
2689 })
2690 .next()
2691 .await;
2692 cx.executor().run_until_parked();
2693
2694 editor.update(cx, |editor, cx| {
2695 let expected_hints = vec![
2696 "main hint #0".to_string(),
2697 "main hint #1".to_string(),
2698 "main hint #2".to_string(),
2699 "main hint #3".to_string(),
2700 "main hint #4".to_string(),
2701 "main hint #5".to_string(),
2702 ];
2703 assert_eq!(
2704 expected_hints,
2705 cached_hint_labels(editor),
2706 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2707 );
2708 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2709 assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the version");
2710 }).unwrap();
2711
2712 editor
2713 .update(cx, |editor, cx| {
2714 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2715 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2716 });
2717 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2718 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2719 });
2720 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2721 s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
2722 });
2723 })
2724 .unwrap();
2725 cx.executor().run_until_parked();
2726 editor.update(cx, |editor, cx| {
2727 let expected_hints = vec![
2728 "main hint #0".to_string(),
2729 "main hint #1".to_string(),
2730 "main hint #2".to_string(),
2731 "main hint #3".to_string(),
2732 "main hint #4".to_string(),
2733 "main hint #5".to_string(),
2734 "other hint #0".to_string(),
2735 "other hint #1".to_string(),
2736 "other hint #2".to_string(),
2737 ];
2738 assert_eq!(expected_hints, cached_hint_labels(editor),
2739 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2740 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2741 assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
2742 "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
2743 }).unwrap();
2744
2745 editor
2746 .update(cx, |editor, cx| {
2747 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2748 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2749 });
2750 })
2751 .unwrap();
2752 cx.executor().advance_clock(Duration::from_millis(
2753 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2754 ));
2755 cx.executor().run_until_parked();
2756 let last_scroll_update_version = editor.update(cx, |editor, cx| {
2757 let expected_hints = vec![
2758 "main hint #0".to_string(),
2759 "main hint #1".to_string(),
2760 "main hint #2".to_string(),
2761 "main hint #3".to_string(),
2762 "main hint #4".to_string(),
2763 "main hint #5".to_string(),
2764 "other hint #0".to_string(),
2765 "other hint #1".to_string(),
2766 "other hint #2".to_string(),
2767 "other hint #3".to_string(),
2768 "other hint #4".to_string(),
2769 "other hint #5".to_string(),
2770 ];
2771 assert_eq!(expected_hints, cached_hint_labels(editor),
2772 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2773 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2774 assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
2775 expected_hints.len()
2776 }).unwrap();
2777
2778 editor
2779 .update(cx, |editor, cx| {
2780 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2781 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2782 });
2783 })
2784 .unwrap();
2785 cx.executor().run_until_parked();
2786 editor.update(cx, |editor, cx| {
2787 let expected_hints = vec![
2788 "main hint #0".to_string(),
2789 "main hint #1".to_string(),
2790 "main hint #2".to_string(),
2791 "main hint #3".to_string(),
2792 "main hint #4".to_string(),
2793 "main hint #5".to_string(),
2794 "other hint #0".to_string(),
2795 "other hint #1".to_string(),
2796 "other hint #2".to_string(),
2797 "other hint #3".to_string(),
2798 "other hint #4".to_string(),
2799 "other hint #5".to_string(),
2800 ];
2801 assert_eq!(expected_hints, cached_hint_labels(editor),
2802 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2803 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2804 assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scrolled buffer");
2805 }).unwrap();
2806
2807 editor_edited.store(true, Ordering::Release);
2808 editor
2809 .update(cx, |editor, cx| {
2810 editor.change_selections(None, cx, |s| {
2811 s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
2812 });
2813 editor.handle_input("++++more text++++", cx);
2814 })
2815 .unwrap();
2816 cx.executor().run_until_parked();
2817 editor.update(cx, |editor, cx| {
2818 let expected_hints = vec![
2819 "main hint(edited) #0".to_string(),
2820 "main hint(edited) #1".to_string(),
2821 "main hint(edited) #2".to_string(),
2822 "main hint(edited) #3".to_string(),
2823 "main hint(edited) #4".to_string(),
2824 "main hint(edited) #5".to_string(),
2825 "other hint(edited) #0".to_string(),
2826 "other hint(edited) #1".to_string(),
2827 ];
2828 assert_eq!(
2829 expected_hints,
2830 cached_hint_labels(editor),
2831 "After multibuffer edit, editor gets scrolled back to the last selection; \
2832 all hints should be invalidated and required for all of its visible excerpts"
2833 );
2834 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2835
2836 let current_cache_version = editor.inlay_hint_cache().version;
2837 let expected_version = last_scroll_update_version + expected_hints.len();
2838 assert!(
2839 current_cache_version == expected_version || current_cache_version == expected_version + 1 ,
2840 // TODO we sometimes get an extra cache version bump, why?
2841 "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"
2842 );
2843 }).unwrap();
2844 }
2845
2846 #[gpui::test]
2847 async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
2848 init_test(cx, |settings| {
2849 settings.defaults.inlay_hints = Some(InlayHintSettings {
2850 enabled: true,
2851 show_type_hints: false,
2852 show_parameter_hints: false,
2853 show_other_hints: false,
2854 })
2855 });
2856
2857 let mut language = Language::new(
2858 LanguageConfig {
2859 name: "Rust".into(),
2860 matcher: LanguageMatcher {
2861 path_suffixes: vec!["rs".to_string()],
2862 ..Default::default()
2863 },
2864 ..Default::default()
2865 },
2866 Some(tree_sitter_rust::language()),
2867 );
2868 let mut fake_servers = language
2869 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2870 capabilities: lsp::ServerCapabilities {
2871 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2872 ..Default::default()
2873 },
2874 ..Default::default()
2875 }))
2876 .await;
2877 let language = Arc::new(language);
2878 let fs = FakeFs::new(cx.background_executor.clone());
2879 fs.insert_tree(
2880 "/a",
2881 json!({
2882 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2883 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2884 }),
2885 )
2886 .await;
2887 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2888 project.update(cx, |project, _| {
2889 project.languages().add(Arc::clone(&language))
2890 });
2891 let worktree_id = project.update(cx, |project, cx| {
2892 project.worktrees().next().unwrap().read(cx).id()
2893 });
2894
2895 let buffer_1 = project
2896 .update(cx, |project, cx| {
2897 project.open_buffer((worktree_id, "main.rs"), cx)
2898 })
2899 .await
2900 .unwrap();
2901 let buffer_2 = project
2902 .update(cx, |project, cx| {
2903 project.open_buffer((worktree_id, "other.rs"), cx)
2904 })
2905 .await
2906 .unwrap();
2907 let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
2908 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2909 let buffer_1_excerpts = multibuffer.push_excerpts(
2910 buffer_1.clone(),
2911 [ExcerptRange {
2912 context: Point::new(0, 0)..Point::new(2, 0),
2913 primary: None,
2914 }],
2915 cx,
2916 );
2917 let buffer_2_excerpts = multibuffer.push_excerpts(
2918 buffer_2.clone(),
2919 [ExcerptRange {
2920 context: Point::new(0, 1)..Point::new(2, 1),
2921 primary: None,
2922 }],
2923 cx,
2924 );
2925 (buffer_1_excerpts, buffer_2_excerpts)
2926 });
2927
2928 assert!(!buffer_1_excerpts.is_empty());
2929 assert!(!buffer_2_excerpts.is_empty());
2930
2931 cx.executor().run_until_parked();
2932 let editor =
2933 cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
2934 let editor_edited = Arc::new(AtomicBool::new(false));
2935 let fake_server = fake_servers.next().await.unwrap();
2936 let closure_editor_edited = Arc::clone(&editor_edited);
2937 fake_server
2938 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2939 let task_editor_edited = Arc::clone(&closure_editor_edited);
2940 async move {
2941 let hint_text = if params.text_document.uri
2942 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2943 {
2944 "main hint"
2945 } else if params.text_document.uri
2946 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2947 {
2948 "other hint"
2949 } else {
2950 panic!("unexpected uri: {:?}", params.text_document.uri);
2951 };
2952
2953 let positions = [
2954 lsp::Position::new(0, 2),
2955 lsp::Position::new(4, 2),
2956 lsp::Position::new(22, 2),
2957 lsp::Position::new(44, 2),
2958 lsp::Position::new(56, 2),
2959 lsp::Position::new(67, 2),
2960 ];
2961 let out_of_range_hint = lsp::InlayHint {
2962 position: lsp::Position::new(
2963 params.range.start.line + 99,
2964 params.range.start.character + 99,
2965 ),
2966 label: lsp::InlayHintLabel::String(
2967 "out of excerpt range, should be ignored".to_string(),
2968 ),
2969 kind: None,
2970 text_edits: None,
2971 tooltip: None,
2972 padding_left: None,
2973 padding_right: None,
2974 data: None,
2975 };
2976
2977 let edited = task_editor_edited.load(Ordering::Acquire);
2978 Ok(Some(
2979 std::iter::once(out_of_range_hint)
2980 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2981 lsp::InlayHint {
2982 position,
2983 label: lsp::InlayHintLabel::String(format!(
2984 "{hint_text}{} #{i}",
2985 if edited { "(edited)" } else { "" },
2986 )),
2987 kind: None,
2988 text_edits: None,
2989 tooltip: None,
2990 padding_left: None,
2991 padding_right: None,
2992 data: None,
2993 }
2994 }))
2995 .collect(),
2996 ))
2997 }
2998 })
2999 .next()
3000 .await;
3001 cx.executor().run_until_parked();
3002
3003 editor
3004 .update(cx, |editor, cx| {
3005 assert_eq!(
3006 vec!["main hint #0".to_string(), "other hint #0".to_string()],
3007 cached_hint_labels(editor),
3008 "Cache should update for both excerpts despite hints display was disabled"
3009 );
3010 assert!(
3011 visible_hint_labels(editor, cx).is_empty(),
3012 "All hints are disabled and should not be shown despite being present in the cache"
3013 );
3014 assert_eq!(
3015 editor.inlay_hint_cache().version,
3016 2,
3017 "Cache should update once per excerpt query"
3018 );
3019 })
3020 .unwrap();
3021
3022 editor
3023 .update(cx, |editor, cx| {
3024 editor.buffer().update(cx, |multibuffer, cx| {
3025 multibuffer.remove_excerpts(buffer_2_excerpts, cx)
3026 })
3027 })
3028 .unwrap();
3029 cx.executor().run_until_parked();
3030 editor
3031 .update(cx, |editor, cx| {
3032 assert_eq!(
3033 vec!["main hint #0".to_string()],
3034 cached_hint_labels(editor),
3035 "For the removed excerpt, should clean corresponding cached hints"
3036 );
3037 assert!(
3038 visible_hint_labels(editor, cx).is_empty(),
3039 "All hints are disabled and should not be shown despite being present in the cache"
3040 );
3041 assert_eq!(
3042 editor.inlay_hint_cache().version,
3043 3,
3044 "Excerpt removal should trigger a cache update"
3045 );
3046 })
3047 .unwrap();
3048
3049 update_test_language_settings(cx, |settings| {
3050 settings.defaults.inlay_hints = Some(InlayHintSettings {
3051 enabled: true,
3052 show_type_hints: true,
3053 show_parameter_hints: true,
3054 show_other_hints: true,
3055 })
3056 });
3057 cx.executor().run_until_parked();
3058 editor
3059 .update(cx, |editor, cx| {
3060 let expected_hints = vec!["main hint #0".to_string()];
3061 assert_eq!(
3062 expected_hints,
3063 cached_hint_labels(editor),
3064 "Hint display settings change should not change the cache"
3065 );
3066 assert_eq!(
3067 expected_hints,
3068 visible_hint_labels(editor, cx),
3069 "Settings change should make cached hints visible"
3070 );
3071 assert_eq!(
3072 editor.inlay_hint_cache().version,
3073 4,
3074 "Settings change should trigger a cache update"
3075 );
3076 })
3077 .unwrap();
3078 }
3079
3080 #[gpui::test]
3081 async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
3082 init_test(cx, |settings| {
3083 settings.defaults.inlay_hints = Some(InlayHintSettings {
3084 enabled: true,
3085 show_type_hints: true,
3086 show_parameter_hints: true,
3087 show_other_hints: true,
3088 })
3089 });
3090
3091 let mut language = Language::new(
3092 LanguageConfig {
3093 name: "Rust".into(),
3094 matcher: LanguageMatcher {
3095 path_suffixes: vec!["rs".to_string()],
3096 ..Default::default()
3097 },
3098 ..Default::default()
3099 },
3100 Some(tree_sitter_rust::language()),
3101 );
3102 let mut fake_servers = language
3103 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3104 capabilities: lsp::ServerCapabilities {
3105 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3106 ..Default::default()
3107 },
3108 ..Default::default()
3109 }))
3110 .await;
3111 let fs = FakeFs::new(cx.background_executor.clone());
3112 fs.insert_tree(
3113 "/a",
3114 json!({
3115 "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)),
3116 "other.rs": "// Test file",
3117 }),
3118 )
3119 .await;
3120 let project = Project::test(fs, ["/a".as_ref()], cx).await;
3121 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
3122 let buffer = project
3123 .update(cx, |project, cx| {
3124 project.open_local_buffer("/a/main.rs", cx)
3125 })
3126 .await
3127 .unwrap();
3128 cx.executor().run_until_parked();
3129 cx.executor().start_waiting();
3130 let fake_server = fake_servers.next().await.unwrap();
3131 let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx));
3132 let lsp_request_count = Arc::new(AtomicU32::new(0));
3133 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
3134 fake_server
3135 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
3136 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
3137 async move {
3138 assert_eq!(
3139 params.text_document.uri,
3140 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3141 );
3142 let query_start = params.range.start;
3143 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
3144 Ok(Some(vec![lsp::InlayHint {
3145 position: query_start,
3146 label: lsp::InlayHintLabel::String(i.to_string()),
3147 kind: None,
3148 text_edits: None,
3149 tooltip: None,
3150 padding_left: None,
3151 padding_right: None,
3152 data: None,
3153 }]))
3154 }
3155 })
3156 .next()
3157 .await;
3158
3159 cx.executor().run_until_parked();
3160 editor
3161 .update(cx, |editor, cx| {
3162 editor.change_selections(None, cx, |s| {
3163 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3164 })
3165 })
3166 .unwrap();
3167 cx.executor().run_until_parked();
3168 editor
3169 .update(cx, |editor, cx| {
3170 let expected_hints = vec!["1".to_string()];
3171 assert_eq!(expected_hints, cached_hint_labels(editor));
3172 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3173 assert_eq!(editor.inlay_hint_cache().version, 1);
3174 })
3175 .unwrap();
3176 }
3177
3178 #[gpui::test]
3179 async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
3180 init_test(cx, |settings| {
3181 settings.defaults.inlay_hints = Some(InlayHintSettings {
3182 enabled: false,
3183 show_type_hints: true,
3184 show_parameter_hints: true,
3185 show_other_hints: true,
3186 })
3187 });
3188
3189 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
3190
3191 editor
3192 .update(cx, |editor, cx| {
3193 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
3194 })
3195 .unwrap();
3196 cx.executor().start_waiting();
3197 let lsp_request_count = Arc::new(AtomicU32::new(0));
3198 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
3199 fake_server
3200 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
3201 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
3202 async move {
3203 assert_eq!(
3204 params.text_document.uri,
3205 lsp::Url::from_file_path(file_with_hints).unwrap(),
3206 );
3207
3208 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
3209 Ok(Some(vec![lsp::InlayHint {
3210 position: lsp::Position::new(0, i),
3211 label: lsp::InlayHintLabel::String(i.to_string()),
3212 kind: None,
3213 text_edits: None,
3214 tooltip: None,
3215 padding_left: None,
3216 padding_right: None,
3217 data: None,
3218 }]))
3219 }
3220 })
3221 .next()
3222 .await;
3223 cx.executor().run_until_parked();
3224 editor
3225 .update(cx, |editor, cx| {
3226 let expected_hints = vec!["1".to_string()];
3227 assert_eq!(
3228 expected_hints,
3229 cached_hint_labels(editor),
3230 "Should display inlays after toggle despite them disabled in settings"
3231 );
3232 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3233 assert_eq!(
3234 editor.inlay_hint_cache().version,
3235 1,
3236 "First toggle should be cache's first update"
3237 );
3238 })
3239 .unwrap();
3240
3241 editor
3242 .update(cx, |editor, cx| {
3243 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
3244 })
3245 .unwrap();
3246 cx.executor().run_until_parked();
3247 editor
3248 .update(cx, |editor, cx| {
3249 assert!(
3250 cached_hint_labels(editor).is_empty(),
3251 "Should clear hints after 2nd toggle"
3252 );
3253 assert!(visible_hint_labels(editor, cx).is_empty());
3254 assert_eq!(editor.inlay_hint_cache().version, 2);
3255 })
3256 .unwrap();
3257
3258 update_test_language_settings(cx, |settings| {
3259 settings.defaults.inlay_hints = Some(InlayHintSettings {
3260 enabled: true,
3261 show_type_hints: true,
3262 show_parameter_hints: true,
3263 show_other_hints: true,
3264 })
3265 });
3266 cx.executor().run_until_parked();
3267 editor
3268 .update(cx, |editor, cx| {
3269 let expected_hints = vec!["2".to_string()];
3270 assert_eq!(
3271 expected_hints,
3272 cached_hint_labels(editor),
3273 "Should query LSP hints for the 2nd time after enabling hints in settings"
3274 );
3275 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3276 assert_eq!(editor.inlay_hint_cache().version, 3);
3277 })
3278 .unwrap();
3279
3280 editor
3281 .update(cx, |editor, cx| {
3282 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
3283 })
3284 .unwrap();
3285 cx.executor().run_until_parked();
3286 editor
3287 .update(cx, |editor, cx| {
3288 assert!(
3289 cached_hint_labels(editor).is_empty(),
3290 "Should clear hints after enabling in settings and a 3rd toggle"
3291 );
3292 assert!(visible_hint_labels(editor, cx).is_empty());
3293 assert_eq!(editor.inlay_hint_cache().version, 4);
3294 })
3295 .unwrap();
3296
3297 editor
3298 .update(cx, |editor, cx| {
3299 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
3300 })
3301 .unwrap();
3302 cx.executor().run_until_parked();
3303 editor.update(cx, |editor, cx| {
3304 let expected_hints = vec!["3".to_string()];
3305 assert_eq!(
3306 expected_hints,
3307 cached_hint_labels(editor),
3308 "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
3309 );
3310 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3311 assert_eq!(editor.inlay_hint_cache().version, 5);
3312 }).unwrap();
3313 }
3314
3315 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
3316 cx.update(|cx| {
3317 let settings_store = SettingsStore::test(cx);
3318 cx.set_global(settings_store);
3319 theme::init(theme::LoadThemes::JustBase, cx);
3320 release_channel::init("0.0.0", cx);
3321 client::init_settings(cx);
3322 language::init(cx);
3323 Project::init_settings(cx);
3324 workspace::init_settings(cx);
3325 crate::init(cx);
3326 });
3327
3328 update_test_language_settings(cx, f);
3329 }
3330
3331 async fn prepare_test_objects(
3332 cx: &mut TestAppContext,
3333 ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
3334 let mut language = Language::new(
3335 LanguageConfig {
3336 name: "Rust".into(),
3337 matcher: LanguageMatcher {
3338 path_suffixes: vec!["rs".to_string()],
3339 ..Default::default()
3340 },
3341 ..Default::default()
3342 },
3343 Some(tree_sitter_rust::language()),
3344 );
3345 let mut fake_servers = language
3346 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3347 capabilities: lsp::ServerCapabilities {
3348 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3349 ..Default::default()
3350 },
3351 ..Default::default()
3352 }))
3353 .await;
3354
3355 let fs = FakeFs::new(cx.background_executor.clone());
3356 fs.insert_tree(
3357 "/a",
3358 json!({
3359 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
3360 "other.rs": "// Test file",
3361 }),
3362 )
3363 .await;
3364
3365 let project = Project::test(fs, ["/a".as_ref()], cx).await;
3366 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
3367 let buffer = project
3368 .update(cx, |project, cx| {
3369 project.open_local_buffer("/a/main.rs", cx)
3370 })
3371 .await
3372 .unwrap();
3373 cx.executor().run_until_parked();
3374 cx.executor().start_waiting();
3375 let fake_server = fake_servers.next().await.unwrap();
3376 let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx));
3377
3378 editor
3379 .update(cx, |editor, cx| {
3380 assert!(cached_hint_labels(editor).is_empty());
3381 assert!(visible_hint_labels(editor, cx).is_empty());
3382 assert_eq!(editor.inlay_hint_cache().version, 0);
3383 })
3384 .unwrap();
3385
3386 ("/a/main.rs", editor, fake_server)
3387 }
3388
3389 pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
3390 let mut labels = Vec::new();
3391 for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
3392 let excerpt_hints = excerpt_hints.read();
3393 for id in &excerpt_hints.ordered_hints {
3394 labels.push(excerpt_hints.hints_by_id[id].text());
3395 }
3396 }
3397
3398 labels.sort();
3399 labels
3400 }
3401
3402 pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, Editor>) -> Vec<String> {
3403 let mut hints = editor
3404 .visible_inlay_hints(cx)
3405 .into_iter()
3406 .map(|hint| hint.text.to_string())
3407 .collect::<Vec<_>>();
3408 hints.sort();
3409 hints
3410 }
3411}