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