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