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