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