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