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