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