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