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