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 let project = Project::test(fs, ["/a".as_ref()], cx).await;
1557
1558 let mut rs_fake_servers = None;
1559 let mut md_fake_servers = None;
1560 for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1561 let mut language = Language::new(
1562 LanguageConfig {
1563 name: name.into(),
1564 matcher: LanguageMatcher {
1565 path_suffixes: vec![path_suffix.to_string()],
1566 ..Default::default()
1567 },
1568 ..Default::default()
1569 },
1570 Some(tree_sitter_rust::language()),
1571 );
1572 let fake_servers = language
1573 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1574 name,
1575 capabilities: lsp::ServerCapabilities {
1576 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1577 ..Default::default()
1578 },
1579 ..Default::default()
1580 }))
1581 .await;
1582 match name {
1583 "Rust" => rs_fake_servers = Some(fake_servers),
1584 "Markdown" => md_fake_servers = Some(fake_servers),
1585 _ => unreachable!(),
1586 }
1587 project.update(cx, |project, _| {
1588 project.languages().add(Arc::new(language));
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.clone();
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 mut language = Language::new(
2257 LanguageConfig {
2258 name: "Rust".into(),
2259 matcher: LanguageMatcher {
2260 path_suffixes: vec!["rs".to_string()],
2261 ..Default::default()
2262 },
2263 ..Default::default()
2264 },
2265 Some(tree_sitter_rust::language()),
2266 );
2267 let mut fake_servers = language
2268 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2269 capabilities: lsp::ServerCapabilities {
2270 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2271 ..Default::default()
2272 },
2273 ..Default::default()
2274 }))
2275 .await;
2276 let fs = FakeFs::new(cx.background_executor.clone());
2277 fs.insert_tree(
2278 "/a",
2279 json!({
2280 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
2281 "other.rs": "// Test file",
2282 }),
2283 )
2284 .await;
2285 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2286 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
2287 let buffer = project
2288 .update(cx, |project, cx| {
2289 project.open_local_buffer("/a/main.rs", cx)
2290 })
2291 .await
2292 .unwrap();
2293 cx.executor().run_until_parked();
2294 cx.executor().start_waiting();
2295 let fake_server = fake_servers.next().await.unwrap();
2296 let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx));
2297 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2298 let lsp_request_count = Arc::new(AtomicUsize::new(0));
2299 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
2300 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2301 fake_server
2302 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2303 let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
2304 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2305 async move {
2306 assert_eq!(
2307 params.text_document.uri,
2308 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2309 );
2310
2311 task_lsp_request_ranges.lock().push(params.range);
2312 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
2313 Ok(Some(vec![lsp::InlayHint {
2314 position: params.range.end,
2315 label: lsp::InlayHintLabel::String(i.to_string()),
2316 kind: None,
2317 text_edits: None,
2318 tooltip: None,
2319 padding_left: None,
2320 padding_right: None,
2321 data: None,
2322 }]))
2323 }
2324 })
2325 .next()
2326 .await;
2327
2328 fn editor_visible_range(
2329 editor: &WindowHandle<Editor>,
2330 cx: &mut gpui::TestAppContext,
2331 ) -> Range<Point> {
2332 let ranges = editor
2333 .update(cx, |editor, cx| {
2334 editor.excerpts_for_inlay_hints_query(None, cx)
2335 })
2336 .unwrap();
2337 assert_eq!(
2338 ranges.len(),
2339 1,
2340 "Single buffer should produce a single excerpt with visible range"
2341 );
2342 let (_, (excerpt_buffer, _, excerpt_visible_range)) =
2343 ranges.into_iter().next().unwrap();
2344 excerpt_buffer.update(cx, |buffer, _| {
2345 let snapshot = buffer.snapshot();
2346 let start = buffer
2347 .anchor_before(excerpt_visible_range.start)
2348 .to_point(&snapshot);
2349 let end = buffer
2350 .anchor_after(excerpt_visible_range.end)
2351 .to_point(&snapshot);
2352 start..end
2353 })
2354 }
2355
2356 // in large buffers, requests are made for more than visible range of a buffer.
2357 // invisible parts are queried later, to avoid excessive requests on quick typing.
2358 // wait the timeout needed to get all requests.
2359 cx.executor().advance_clock(Duration::from_millis(
2360 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2361 ));
2362 cx.executor().run_until_parked();
2363 let initial_visible_range = editor_visible_range(&editor, cx);
2364 let lsp_initial_visible_range = lsp::Range::new(
2365 lsp::Position::new(
2366 initial_visible_range.start.row,
2367 initial_visible_range.start.column,
2368 ),
2369 lsp::Position::new(
2370 initial_visible_range.end.row,
2371 initial_visible_range.end.column,
2372 ),
2373 );
2374 let expected_initial_query_range_end =
2375 lsp::Position::new(initial_visible_range.end.row * 2, 2);
2376 let mut expected_invisible_query_start = lsp_initial_visible_range.end;
2377 expected_invisible_query_start.character += 1;
2378 editor.update(cx, |editor, cx| {
2379 let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2380 assert_eq!(ranges.len(), 2,
2381 "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:?}");
2382 let visible_query_range = &ranges[0];
2383 assert_eq!(visible_query_range.start, lsp_initial_visible_range.start);
2384 assert_eq!(visible_query_range.end, lsp_initial_visible_range.end);
2385 let invisible_query_range = &ranges[1];
2386
2387 assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document");
2388 assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document");
2389
2390 let requests_count = lsp_request_count.load(Ordering::Acquire);
2391 assert_eq!(requests_count, 2, "Visible + invisible request");
2392 let expected_hints = vec!["1".to_string(), "2".to_string()];
2393 assert_eq!(
2394 expected_hints,
2395 cached_hint_labels(editor),
2396 "Should have hints from both LSP requests made for a big file"
2397 );
2398 assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
2399 assert_eq!(
2400 editor.inlay_hint_cache().version, requests_count,
2401 "LSP queries should've bumped the cache version"
2402 );
2403 }).unwrap();
2404
2405 editor
2406 .update(cx, |editor, cx| {
2407 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
2408 })
2409 .unwrap();
2410 cx.executor().run_until_parked();
2411 editor
2412 .update(cx, |editor, cx| {
2413 editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
2414 })
2415 .unwrap();
2416 cx.executor().advance_clock(Duration::from_millis(
2417 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2418 ));
2419 cx.executor().run_until_parked();
2420 let visible_range_after_scrolls = editor_visible_range(&editor, cx);
2421 let visible_line_count = editor
2422 .update(cx, |editor, _| editor.visible_line_count().unwrap())
2423 .unwrap();
2424 let selection_in_cached_range = editor
2425 .update(cx, |editor, cx| {
2426 let ranges = lsp_request_ranges
2427 .lock()
2428 .drain(..)
2429 .sorted_by_key(|r| r.start)
2430 .collect::<Vec<_>>();
2431 assert_eq!(
2432 ranges.len(),
2433 2,
2434 "Should query 2 ranges after both scrolls, but got: {ranges:?}"
2435 );
2436 let first_scroll = &ranges[0];
2437 let second_scroll = &ranges[1];
2438 assert_eq!(
2439 first_scroll.end, second_scroll.start,
2440 "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
2441 );
2442 assert_eq!(
2443 first_scroll.start, expected_initial_query_range_end,
2444 "First scroll should start the query right after the end of the original scroll",
2445 );
2446 assert_eq!(
2447 second_scroll.end,
2448 lsp::Position::new(
2449 visible_range_after_scrolls.end.row
2450 + visible_line_count.ceil() as u32,
2451 1,
2452 ),
2453 "Second scroll should query one more screen down after the end of the visible range"
2454 );
2455
2456 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2457 assert_eq!(lsp_requests, 4, "Should query for hints after every scroll");
2458 let expected_hints = vec![
2459 "1".to_string(),
2460 "2".to_string(),
2461 "3".to_string(),
2462 "4".to_string(),
2463 ];
2464 assert_eq!(
2465 expected_hints,
2466 cached_hint_labels(editor),
2467 "Should have hints from the new LSP response after the edit"
2468 );
2469 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2470 assert_eq!(
2471 editor.inlay_hint_cache().version,
2472 lsp_requests,
2473 "Should update the cache for every LSP response with hints added"
2474 );
2475
2476 let mut selection_in_cached_range = visible_range_after_scrolls.end;
2477 selection_in_cached_range.row -= visible_line_count.ceil() as u32;
2478 selection_in_cached_range
2479 })
2480 .unwrap();
2481
2482 editor
2483 .update(cx, |editor, cx| {
2484 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
2485 s.select_ranges([selection_in_cached_range..selection_in_cached_range])
2486 });
2487 })
2488 .unwrap();
2489 cx.executor().advance_clock(Duration::from_millis(
2490 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2491 ));
2492 cx.executor().run_until_parked();
2493 editor.update(cx, |_, _| {
2494 let ranges = lsp_request_ranges
2495 .lock()
2496 .drain(..)
2497 .sorted_by_key(|r| r.start)
2498 .collect::<Vec<_>>();
2499 assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
2500 assert_eq!(lsp_request_count.load(Ordering::Acquire), 4);
2501 }).unwrap();
2502
2503 editor
2504 .update(cx, |editor, cx| {
2505 editor.handle_input("++++more text++++", cx);
2506 })
2507 .unwrap();
2508 cx.executor().advance_clock(Duration::from_millis(
2509 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2510 ));
2511 cx.executor().run_until_parked();
2512 editor.update(cx, |editor, cx| {
2513 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2514 ranges.sort_by_key(|r| r.start);
2515
2516 assert_eq!(ranges.len(), 3,
2517 "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
2518 let above_query_range = &ranges[0];
2519 let visible_query_range = &ranges[1];
2520 let below_query_range = &ranges[2];
2521 assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
2522 "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");
2523 assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line,
2524 "Visible range {visible_query_range:?} should be before below range {below_query_range:?}");
2525 assert!(above_query_range.start.line < selection_in_cached_range.row,
2526 "Hints should be queried with the selected range after the query range start");
2527 assert!(below_query_range.end.line > selection_in_cached_range.row,
2528 "Hints should be queried with the selected range before the query range end");
2529 assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32,
2530 "Hints query range should contain one more screen before");
2531 assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32,
2532 "Hints query range should contain one more screen after");
2533
2534 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2535 assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried");
2536 let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()];
2537 assert_eq!(expected_hints, cached_hint_labels(editor),
2538 "Should have hints from the new LSP response after the edit");
2539 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2540 assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added");
2541 }).unwrap();
2542 }
2543
2544 #[gpui::test(iterations = 30)]
2545 async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
2546 init_test(cx, |settings| {
2547 settings.defaults.inlay_hints = Some(InlayHintSettings {
2548 enabled: true,
2549 edit_debounce_ms: 0,
2550 scroll_debounce_ms: 0,
2551 show_type_hints: true,
2552 show_parameter_hints: true,
2553 show_other_hints: true,
2554 })
2555 });
2556
2557 let mut language = Language::new(
2558 LanguageConfig {
2559 name: "Rust".into(),
2560 matcher: LanguageMatcher {
2561 path_suffixes: vec!["rs".to_string()],
2562 ..Default::default()
2563 },
2564 ..Default::default()
2565 },
2566 Some(tree_sitter_rust::language()),
2567 );
2568 let mut fake_servers = language
2569 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2570 capabilities: lsp::ServerCapabilities {
2571 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2572 ..Default::default()
2573 },
2574 ..Default::default()
2575 }))
2576 .await;
2577 let language = Arc::new(language);
2578 let fs = FakeFs::new(cx.background_executor.clone());
2579 fs.insert_tree(
2580 "/a",
2581 json!({
2582 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2583 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2584 }),
2585 )
2586 .await;
2587 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2588 project.update(cx, |project, _| {
2589 project.languages().add(Arc::clone(&language))
2590 });
2591 let worktree_id = project.update(cx, |project, cx| {
2592 project.worktrees().next().unwrap().read(cx).id()
2593 });
2594
2595 let buffer_1 = project
2596 .update(cx, |project, cx| {
2597 project.open_buffer((worktree_id, "main.rs"), cx)
2598 })
2599 .await
2600 .unwrap();
2601 let buffer_2 = project
2602 .update(cx, |project, cx| {
2603 project.open_buffer((worktree_id, "other.rs"), cx)
2604 })
2605 .await
2606 .unwrap();
2607 let multibuffer = cx.new_model(|cx| {
2608 let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
2609 multibuffer.push_excerpts(
2610 buffer_1.clone(),
2611 [
2612 ExcerptRange {
2613 context: Point::new(0, 0)..Point::new(2, 0),
2614 primary: None,
2615 },
2616 ExcerptRange {
2617 context: Point::new(4, 0)..Point::new(11, 0),
2618 primary: None,
2619 },
2620 ExcerptRange {
2621 context: Point::new(22, 0)..Point::new(33, 0),
2622 primary: None,
2623 },
2624 ExcerptRange {
2625 context: Point::new(44, 0)..Point::new(55, 0),
2626 primary: None,
2627 },
2628 ExcerptRange {
2629 context: Point::new(56, 0)..Point::new(66, 0),
2630 primary: None,
2631 },
2632 ExcerptRange {
2633 context: Point::new(67, 0)..Point::new(77, 0),
2634 primary: None,
2635 },
2636 ],
2637 cx,
2638 );
2639 multibuffer.push_excerpts(
2640 buffer_2.clone(),
2641 [
2642 ExcerptRange {
2643 context: Point::new(0, 1)..Point::new(2, 1),
2644 primary: None,
2645 },
2646 ExcerptRange {
2647 context: Point::new(4, 1)..Point::new(11, 1),
2648 primary: None,
2649 },
2650 ExcerptRange {
2651 context: Point::new(22, 1)..Point::new(33, 1),
2652 primary: None,
2653 },
2654 ExcerptRange {
2655 context: Point::new(44, 1)..Point::new(55, 1),
2656 primary: None,
2657 },
2658 ExcerptRange {
2659 context: Point::new(56, 1)..Point::new(66, 1),
2660 primary: None,
2661 },
2662 ExcerptRange {
2663 context: Point::new(67, 1)..Point::new(77, 1),
2664 primary: None,
2665 },
2666 ],
2667 cx,
2668 );
2669 multibuffer
2670 });
2671
2672 cx.executor().run_until_parked();
2673 let editor =
2674 cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
2675 let editor_edited = Arc::new(AtomicBool::new(false));
2676 let fake_server = fake_servers.next().await.unwrap();
2677 let closure_editor_edited = Arc::clone(&editor_edited);
2678 fake_server
2679 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2680 let task_editor_edited = Arc::clone(&closure_editor_edited);
2681 async move {
2682 let hint_text = if params.text_document.uri
2683 == lsp::Url::from_file_path("/a/main.rs").unwrap()
2684 {
2685 "main hint"
2686 } else if params.text_document.uri
2687 == lsp::Url::from_file_path("/a/other.rs").unwrap()
2688 {
2689 "other hint"
2690 } else {
2691 panic!("unexpected uri: {:?}", params.text_document.uri);
2692 };
2693
2694 // one hint per excerpt
2695 let positions = [
2696 lsp::Position::new(0, 2),
2697 lsp::Position::new(4, 2),
2698 lsp::Position::new(22, 2),
2699 lsp::Position::new(44, 2),
2700 lsp::Position::new(56, 2),
2701 lsp::Position::new(67, 2),
2702 ];
2703 let out_of_range_hint = lsp::InlayHint {
2704 position: lsp::Position::new(
2705 params.range.start.line + 99,
2706 params.range.start.character + 99,
2707 ),
2708 label: lsp::InlayHintLabel::String(
2709 "out of excerpt range, should be ignored".to_string(),
2710 ),
2711 kind: None,
2712 text_edits: None,
2713 tooltip: None,
2714 padding_left: None,
2715 padding_right: None,
2716 data: None,
2717 };
2718
2719 let edited = task_editor_edited.load(Ordering::Acquire);
2720 Ok(Some(
2721 std::iter::once(out_of_range_hint)
2722 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2723 lsp::InlayHint {
2724 position,
2725 label: lsp::InlayHintLabel::String(format!(
2726 "{hint_text}{} #{i}",
2727 if edited { "(edited)" } else { "" },
2728 )),
2729 kind: None,
2730 text_edits: None,
2731 tooltip: None,
2732 padding_left: None,
2733 padding_right: None,
2734 data: None,
2735 }
2736 }))
2737 .collect(),
2738 ))
2739 }
2740 })
2741 .next()
2742 .await;
2743 cx.executor().run_until_parked();
2744
2745 editor.update(cx, |editor, cx| {
2746 let expected_hints = vec![
2747 "main hint #0".to_string(),
2748 "main hint #1".to_string(),
2749 "main hint #2".to_string(),
2750 "main hint #3".to_string(),
2751 "main hint #4".to_string(),
2752 "main hint #5".to_string(),
2753 ];
2754 assert_eq!(
2755 expected_hints,
2756 cached_hint_labels(editor),
2757 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2758 );
2759 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2760 assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the version");
2761 }).unwrap();
2762
2763 editor
2764 .update(cx, |editor, cx| {
2765 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2766 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2767 });
2768 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2769 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2770 });
2771 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2772 s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
2773 });
2774 })
2775 .unwrap();
2776 cx.executor().run_until_parked();
2777 editor.update(cx, |editor, cx| {
2778 let expected_hints = vec![
2779 "main hint #0".to_string(),
2780 "main hint #1".to_string(),
2781 "main hint #2".to_string(),
2782 "main hint #3".to_string(),
2783 "main hint #4".to_string(),
2784 "main hint #5".to_string(),
2785 "other hint #0".to_string(),
2786 "other hint #1".to_string(),
2787 "other hint #2".to_string(),
2788 ];
2789 assert_eq!(expected_hints, cached_hint_labels(editor),
2790 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2791 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2792 assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
2793 "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
2794 }).unwrap();
2795
2796 editor
2797 .update(cx, |editor, cx| {
2798 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2799 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2800 });
2801 })
2802 .unwrap();
2803 cx.executor().advance_clock(Duration::from_millis(
2804 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2805 ));
2806 cx.executor().run_until_parked();
2807 let last_scroll_update_version = editor.update(cx, |editor, cx| {
2808 let expected_hints = vec![
2809 "main hint #0".to_string(),
2810 "main hint #1".to_string(),
2811 "main hint #2".to_string(),
2812 "main hint #3".to_string(),
2813 "main hint #4".to_string(),
2814 "main hint #5".to_string(),
2815 "other hint #0".to_string(),
2816 "other hint #1".to_string(),
2817 "other hint #2".to_string(),
2818 "other hint #3".to_string(),
2819 "other hint #4".to_string(),
2820 "other hint #5".to_string(),
2821 ];
2822 assert_eq!(expected_hints, cached_hint_labels(editor),
2823 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2824 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2825 assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
2826 expected_hints.len()
2827 }).unwrap();
2828
2829 editor
2830 .update(cx, |editor, cx| {
2831 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
2832 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2833 });
2834 })
2835 .unwrap();
2836 cx.executor().advance_clock(Duration::from_millis(
2837 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2838 ));
2839 cx.executor().run_until_parked();
2840 editor.update(cx, |editor, cx| {
2841 let expected_hints = vec![
2842 "main hint #0".to_string(),
2843 "main hint #1".to_string(),
2844 "main hint #2".to_string(),
2845 "main hint #3".to_string(),
2846 "main hint #4".to_string(),
2847 "main hint #5".to_string(),
2848 "other hint #0".to_string(),
2849 "other hint #1".to_string(),
2850 "other hint #2".to_string(),
2851 "other hint #3".to_string(),
2852 "other hint #4".to_string(),
2853 "other hint #5".to_string(),
2854 ];
2855 assert_eq!(expected_hints, cached_hint_labels(editor),
2856 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2857 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2858 assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scrolled buffer");
2859 }).unwrap();
2860
2861 editor_edited.store(true, Ordering::Release);
2862 editor
2863 .update(cx, |editor, cx| {
2864 editor.change_selections(None, cx, |s| {
2865 s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
2866 });
2867 editor.handle_input("++++more text++++", cx);
2868 })
2869 .unwrap();
2870 cx.executor().run_until_parked();
2871 editor.update(cx, |editor, cx| {
2872 let expected_hints = vec![
2873 "main hint #0".to_string(),
2874 "main hint #1".to_string(),
2875 "main hint #2".to_string(),
2876 "main hint #3".to_string(),
2877 "main hint #4".to_string(),
2878 "main hint #5".to_string(),
2879 "other hint(edited) #0".to_string(),
2880 "other hint(edited) #1".to_string(),
2881 ];
2882 assert_eq!(
2883 expected_hints,
2884 cached_hint_labels(editor),
2885 "After multibuffer edit, editor gets scrolled back to the last selection; \
2886 all hints should be invalidated and required for all of its visible excerpts"
2887 );
2888 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2889
2890 let current_cache_version = editor.inlay_hint_cache().version;
2891 // We expect two new hints for the excerpts from `other.rs`:
2892 let expected_version = last_scroll_update_version + 2;
2893 assert_eq!(
2894 current_cache_version,
2895 expected_version,
2896 "We should have updated cache N times == N of new hints arrived (separately from each edited excerpt)"
2897 );
2898 }).unwrap();
2899 }
2900
2901 #[gpui::test]
2902 async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
2903 init_test(cx, |settings| {
2904 settings.defaults.inlay_hints = Some(InlayHintSettings {
2905 enabled: true,
2906 edit_debounce_ms: 0,
2907 scroll_debounce_ms: 0,
2908 show_type_hints: false,
2909 show_parameter_hints: false,
2910 show_other_hints: false,
2911 })
2912 });
2913
2914 let mut language = Language::new(
2915 LanguageConfig {
2916 name: "Rust".into(),
2917 matcher: LanguageMatcher {
2918 path_suffixes: vec!["rs".to_string()],
2919 ..Default::default()
2920 },
2921 ..Default::default()
2922 },
2923 Some(tree_sitter_rust::language()),
2924 );
2925 let mut fake_servers = language
2926 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2927 capabilities: lsp::ServerCapabilities {
2928 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2929 ..Default::default()
2930 },
2931 ..Default::default()
2932 }))
2933 .await;
2934 let language = Arc::new(language);
2935 let fs = FakeFs::new(cx.background_executor.clone());
2936 fs.insert_tree(
2937 "/a",
2938 json!({
2939 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2940 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2941 }),
2942 )
2943 .await;
2944 let project = Project::test(fs, ["/a".as_ref()], cx).await;
2945 project.update(cx, |project, _| {
2946 project.languages().add(Arc::clone(&language))
2947 });
2948 let worktree_id = project.update(cx, |project, cx| {
2949 project.worktrees().next().unwrap().read(cx).id()
2950 });
2951
2952 let buffer_1 = project
2953 .update(cx, |project, cx| {
2954 project.open_buffer((worktree_id, "main.rs"), cx)
2955 })
2956 .await
2957 .unwrap();
2958 let buffer_2 = project
2959 .update(cx, |project, cx| {
2960 project.open_buffer((worktree_id, "other.rs"), cx)
2961 })
2962 .await
2963 .unwrap();
2964 let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
2965 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2966 let buffer_1_excerpts = multibuffer.push_excerpts(
2967 buffer_1.clone(),
2968 [ExcerptRange {
2969 context: Point::new(0, 0)..Point::new(2, 0),
2970 primary: None,
2971 }],
2972 cx,
2973 );
2974 let buffer_2_excerpts = multibuffer.push_excerpts(
2975 buffer_2.clone(),
2976 [ExcerptRange {
2977 context: Point::new(0, 1)..Point::new(2, 1),
2978 primary: None,
2979 }],
2980 cx,
2981 );
2982 (buffer_1_excerpts, buffer_2_excerpts)
2983 });
2984
2985 assert!(!buffer_1_excerpts.is_empty());
2986 assert!(!buffer_2_excerpts.is_empty());
2987
2988 cx.executor().run_until_parked();
2989 let editor =
2990 cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
2991 let editor_edited = Arc::new(AtomicBool::new(false));
2992 let fake_server = fake_servers.next().await.unwrap();
2993 let closure_editor_edited = Arc::clone(&editor_edited);
2994 fake_server
2995 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2996 let task_editor_edited = Arc::clone(&closure_editor_edited);
2997 async move {
2998 let hint_text = if params.text_document.uri
2999 == lsp::Url::from_file_path("/a/main.rs").unwrap()
3000 {
3001 "main hint"
3002 } else if params.text_document.uri
3003 == lsp::Url::from_file_path("/a/other.rs").unwrap()
3004 {
3005 "other hint"
3006 } else {
3007 panic!("unexpected uri: {:?}", params.text_document.uri);
3008 };
3009
3010 let positions = [
3011 lsp::Position::new(0, 2),
3012 lsp::Position::new(4, 2),
3013 lsp::Position::new(22, 2),
3014 lsp::Position::new(44, 2),
3015 lsp::Position::new(56, 2),
3016 lsp::Position::new(67, 2),
3017 ];
3018 let out_of_range_hint = lsp::InlayHint {
3019 position: lsp::Position::new(
3020 params.range.start.line + 99,
3021 params.range.start.character + 99,
3022 ),
3023 label: lsp::InlayHintLabel::String(
3024 "out of excerpt range, should be ignored".to_string(),
3025 ),
3026 kind: None,
3027 text_edits: None,
3028 tooltip: None,
3029 padding_left: None,
3030 padding_right: None,
3031 data: None,
3032 };
3033
3034 let edited = task_editor_edited.load(Ordering::Acquire);
3035 Ok(Some(
3036 std::iter::once(out_of_range_hint)
3037 .chain(positions.into_iter().enumerate().map(|(i, position)| {
3038 lsp::InlayHint {
3039 position,
3040 label: lsp::InlayHintLabel::String(format!(
3041 "{hint_text}{} #{i}",
3042 if edited { "(edited)" } else { "" },
3043 )),
3044 kind: None,
3045 text_edits: None,
3046 tooltip: None,
3047 padding_left: None,
3048 padding_right: None,
3049 data: None,
3050 }
3051 }))
3052 .collect(),
3053 ))
3054 }
3055 })
3056 .next()
3057 .await;
3058 cx.executor().run_until_parked();
3059
3060 editor
3061 .update(cx, |editor, cx| {
3062 assert_eq!(
3063 vec!["main hint #0".to_string(), "other hint #0".to_string()],
3064 cached_hint_labels(editor),
3065 "Cache should update for both excerpts despite hints display was disabled"
3066 );
3067 assert!(
3068 visible_hint_labels(editor, cx).is_empty(),
3069 "All hints are disabled and should not be shown despite being present in the cache"
3070 );
3071 assert_eq!(
3072 editor.inlay_hint_cache().version,
3073 2,
3074 "Cache should update once per excerpt query"
3075 );
3076 })
3077 .unwrap();
3078
3079 editor
3080 .update(cx, |editor, cx| {
3081 editor.buffer().update(cx, |multibuffer, cx| {
3082 multibuffer.remove_excerpts(buffer_2_excerpts, cx)
3083 })
3084 })
3085 .unwrap();
3086 cx.executor().run_until_parked();
3087 editor
3088 .update(cx, |editor, cx| {
3089 assert_eq!(
3090 vec!["main hint #0".to_string()],
3091 cached_hint_labels(editor),
3092 "For the removed excerpt, should clean corresponding cached hints"
3093 );
3094 assert!(
3095 visible_hint_labels(editor, cx).is_empty(),
3096 "All hints are disabled and should not be shown despite being present in the cache"
3097 );
3098 assert_eq!(
3099 editor.inlay_hint_cache().version,
3100 3,
3101 "Excerpt removal should trigger a cache update"
3102 );
3103 })
3104 .unwrap();
3105
3106 update_test_language_settings(cx, |settings| {
3107 settings.defaults.inlay_hints = Some(InlayHintSettings {
3108 enabled: true,
3109 edit_debounce_ms: 0,
3110 scroll_debounce_ms: 0,
3111 show_type_hints: true,
3112 show_parameter_hints: true,
3113 show_other_hints: true,
3114 })
3115 });
3116 cx.executor().run_until_parked();
3117 editor
3118 .update(cx, |editor, cx| {
3119 let expected_hints = vec!["main hint #0".to_string()];
3120 assert_eq!(
3121 expected_hints,
3122 cached_hint_labels(editor),
3123 "Hint display settings change should not change the cache"
3124 );
3125 assert_eq!(
3126 expected_hints,
3127 visible_hint_labels(editor, cx),
3128 "Settings change should make cached hints visible"
3129 );
3130 assert_eq!(
3131 editor.inlay_hint_cache().version,
3132 4,
3133 "Settings change should trigger a cache update"
3134 );
3135 })
3136 .unwrap();
3137 }
3138
3139 #[gpui::test]
3140 async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
3141 init_test(cx, |settings| {
3142 settings.defaults.inlay_hints = Some(InlayHintSettings {
3143 enabled: true,
3144 edit_debounce_ms: 0,
3145 scroll_debounce_ms: 0,
3146 show_type_hints: true,
3147 show_parameter_hints: true,
3148 show_other_hints: true,
3149 })
3150 });
3151
3152 let mut language = Language::new(
3153 LanguageConfig {
3154 name: "Rust".into(),
3155 matcher: LanguageMatcher {
3156 path_suffixes: vec!["rs".to_string()],
3157 ..Default::default()
3158 },
3159 ..Default::default()
3160 },
3161 Some(tree_sitter_rust::language()),
3162 );
3163 let mut fake_servers = language
3164 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3165 capabilities: lsp::ServerCapabilities {
3166 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3167 ..Default::default()
3168 },
3169 ..Default::default()
3170 }))
3171 .await;
3172 let fs = FakeFs::new(cx.background_executor.clone());
3173 fs.insert_tree(
3174 "/a",
3175 json!({
3176 "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)),
3177 "other.rs": "// Test file",
3178 }),
3179 )
3180 .await;
3181 let project = Project::test(fs, ["/a".as_ref()], cx).await;
3182 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
3183 let buffer = project
3184 .update(cx, |project, cx| {
3185 project.open_local_buffer("/a/main.rs", cx)
3186 })
3187 .await
3188 .unwrap();
3189 cx.executor().run_until_parked();
3190 cx.executor().start_waiting();
3191 let fake_server = fake_servers.next().await.unwrap();
3192 let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx));
3193 let lsp_request_count = Arc::new(AtomicU32::new(0));
3194 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
3195 fake_server
3196 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
3197 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
3198 async move {
3199 assert_eq!(
3200 params.text_document.uri,
3201 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3202 );
3203 let query_start = params.range.start;
3204 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
3205 Ok(Some(vec![lsp::InlayHint {
3206 position: query_start,
3207 label: lsp::InlayHintLabel::String(i.to_string()),
3208 kind: None,
3209 text_edits: None,
3210 tooltip: None,
3211 padding_left: None,
3212 padding_right: None,
3213 data: None,
3214 }]))
3215 }
3216 })
3217 .next()
3218 .await;
3219
3220 cx.executor().run_until_parked();
3221 editor
3222 .update(cx, |editor, cx| {
3223 editor.change_selections(None, cx, |s| {
3224 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3225 })
3226 })
3227 .unwrap();
3228 cx.executor().run_until_parked();
3229 editor
3230 .update(cx, |editor, cx| {
3231 let expected_hints = vec!["1".to_string()];
3232 assert_eq!(expected_hints, cached_hint_labels(editor));
3233 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3234 assert_eq!(editor.inlay_hint_cache().version, 1);
3235 })
3236 .unwrap();
3237 }
3238
3239 #[gpui::test]
3240 async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
3241 init_test(cx, |settings| {
3242 settings.defaults.inlay_hints = Some(InlayHintSettings {
3243 enabled: false,
3244 edit_debounce_ms: 0,
3245 scroll_debounce_ms: 0,
3246 show_type_hints: true,
3247 show_parameter_hints: true,
3248 show_other_hints: true,
3249 })
3250 });
3251
3252 let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
3253
3254 editor
3255 .update(cx, |editor, cx| {
3256 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
3257 })
3258 .unwrap();
3259 cx.executor().start_waiting();
3260 let lsp_request_count = Arc::new(AtomicU32::new(0));
3261 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
3262 fake_server
3263 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
3264 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
3265 async move {
3266 assert_eq!(
3267 params.text_document.uri,
3268 lsp::Url::from_file_path(file_with_hints).unwrap(),
3269 );
3270
3271 let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
3272 Ok(Some(vec![lsp::InlayHint {
3273 position: lsp::Position::new(0, i),
3274 label: lsp::InlayHintLabel::String(i.to_string()),
3275 kind: None,
3276 text_edits: None,
3277 tooltip: None,
3278 padding_left: None,
3279 padding_right: None,
3280 data: None,
3281 }]))
3282 }
3283 })
3284 .next()
3285 .await;
3286 cx.executor().run_until_parked();
3287 editor
3288 .update(cx, |editor, cx| {
3289 let expected_hints = vec!["1".to_string()];
3290 assert_eq!(
3291 expected_hints,
3292 cached_hint_labels(editor),
3293 "Should display inlays after toggle despite them disabled in settings"
3294 );
3295 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3296 assert_eq!(
3297 editor.inlay_hint_cache().version,
3298 1,
3299 "First toggle should be cache's first update"
3300 );
3301 })
3302 .unwrap();
3303
3304 editor
3305 .update(cx, |editor, cx| {
3306 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
3307 })
3308 .unwrap();
3309 cx.executor().run_until_parked();
3310 editor
3311 .update(cx, |editor, cx| {
3312 assert!(
3313 cached_hint_labels(editor).is_empty(),
3314 "Should clear hints after 2nd toggle"
3315 );
3316 assert!(visible_hint_labels(editor, cx).is_empty());
3317 assert_eq!(editor.inlay_hint_cache().version, 2);
3318 })
3319 .unwrap();
3320
3321 update_test_language_settings(cx, |settings| {
3322 settings.defaults.inlay_hints = Some(InlayHintSettings {
3323 enabled: true,
3324 edit_debounce_ms: 0,
3325 scroll_debounce_ms: 0,
3326 show_type_hints: true,
3327 show_parameter_hints: true,
3328 show_other_hints: true,
3329 })
3330 });
3331 cx.executor().run_until_parked();
3332 editor
3333 .update(cx, |editor, cx| {
3334 let expected_hints = vec!["2".to_string()];
3335 assert_eq!(
3336 expected_hints,
3337 cached_hint_labels(editor),
3338 "Should query LSP hints for the 2nd time after enabling hints in settings"
3339 );
3340 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3341 assert_eq!(editor.inlay_hint_cache().version, 3);
3342 })
3343 .unwrap();
3344
3345 editor
3346 .update(cx, |editor, cx| {
3347 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
3348 })
3349 .unwrap();
3350 cx.executor().run_until_parked();
3351 editor
3352 .update(cx, |editor, cx| {
3353 assert!(
3354 cached_hint_labels(editor).is_empty(),
3355 "Should clear hints after enabling in settings and a 3rd toggle"
3356 );
3357 assert!(visible_hint_labels(editor, cx).is_empty());
3358 assert_eq!(editor.inlay_hint_cache().version, 4);
3359 })
3360 .unwrap();
3361
3362 editor
3363 .update(cx, |editor, cx| {
3364 editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
3365 })
3366 .unwrap();
3367 cx.executor().run_until_parked();
3368 editor.update(cx, |editor, cx| {
3369 let expected_hints = vec!["3".to_string()];
3370 assert_eq!(
3371 expected_hints,
3372 cached_hint_labels(editor),
3373 "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
3374 );
3375 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3376 assert_eq!(editor.inlay_hint_cache().version, 5);
3377 }).unwrap();
3378 }
3379
3380 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
3381 cx.update(|cx| {
3382 let settings_store = SettingsStore::test(cx);
3383 cx.set_global(settings_store);
3384 theme::init(theme::LoadThemes::JustBase, cx);
3385 release_channel::init("0.0.0", cx);
3386 client::init_settings(cx);
3387 language::init(cx);
3388 Project::init_settings(cx);
3389 workspace::init_settings(cx);
3390 crate::init(cx);
3391 });
3392
3393 update_test_language_settings(cx, f);
3394 }
3395
3396 async fn prepare_test_objects(
3397 cx: &mut TestAppContext,
3398 ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
3399 let mut language = Language::new(
3400 LanguageConfig {
3401 name: "Rust".into(),
3402 matcher: LanguageMatcher {
3403 path_suffixes: vec!["rs".to_string()],
3404 ..Default::default()
3405 },
3406 ..Default::default()
3407 },
3408 Some(tree_sitter_rust::language()),
3409 );
3410 let mut fake_servers = language
3411 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3412 capabilities: lsp::ServerCapabilities {
3413 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3414 ..Default::default()
3415 },
3416 ..Default::default()
3417 }))
3418 .await;
3419
3420 let fs = FakeFs::new(cx.background_executor.clone());
3421 fs.insert_tree(
3422 "/a",
3423 json!({
3424 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
3425 "other.rs": "// Test file",
3426 }),
3427 )
3428 .await;
3429
3430 let project = Project::test(fs, ["/a".as_ref()], cx).await;
3431 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
3432 let buffer = project
3433 .update(cx, |project, cx| {
3434 project.open_local_buffer("/a/main.rs", cx)
3435 })
3436 .await
3437 .unwrap();
3438 cx.executor().run_until_parked();
3439 cx.executor().start_waiting();
3440 let fake_server = fake_servers.next().await.unwrap();
3441 let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx));
3442
3443 editor
3444 .update(cx, |editor, cx| {
3445 assert!(cached_hint_labels(editor).is_empty());
3446 assert!(visible_hint_labels(editor, cx).is_empty());
3447 assert_eq!(editor.inlay_hint_cache().version, 0);
3448 })
3449 .unwrap();
3450
3451 ("/a/main.rs", editor, fake_server)
3452 }
3453
3454 pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
3455 let mut labels = Vec::new();
3456 for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
3457 let excerpt_hints = excerpt_hints.read();
3458 for id in &excerpt_hints.ordered_hints {
3459 labels.push(excerpt_hints.hints_by_id[id].text());
3460 }
3461 }
3462
3463 labels.sort();
3464 labels
3465 }
3466
3467 pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, Editor>) -> Vec<String> {
3468 let mut hints = editor
3469 .visible_inlay_hints(cx)
3470 .into_iter()
3471 .map(|hint| hint.text.to_string())
3472 .collect::<Vec<_>>();
3473 hints.sort();
3474 hints
3475 }
3476}