1/// Stores and updates all data received from LSP <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint">textDocument/inlayHint</a> requests.
2/// Has nothing to do with other inlays, e.g. copilot suggestions — those are stored elsewhere.
3/// On every update, cache may query for more inlay hints and update inlays on the screen.
4///
5/// Inlays stored on screen are in [`crate::display_map::inlay_map`] and this cache is the only way to update any inlay hint data in the visible hints in the inlay map.
6/// For determining the update to the `inlay_map`, the cache requires a list of visible inlay hints — all other hints are not relevant and their separate updates are not influencing the cache work.
7///
8/// Due to the way the data is stored for both visible inlays and the cache, every inlay (and inlay hint) collection is editor-specific, so a single buffer may have multiple sets of inlays of open on different panes.
9use std::{
10 cmp,
11 ops::{ControlFlow, Range},
12 sync::Arc,
13 time::Duration,
14};
15
16use crate::{
17 display_map::Inlay, Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot,
18};
19use anyhow::Context as _;
20use clock::Global;
21use futures::future;
22use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window};
23use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
24use parking_lot::RwLock;
25use project::{InlayHint, ResolveState};
26
27use collections::{hash_map, HashMap, HashSet};
28use language::language_settings::InlayHintSettings;
29use smol::lock::Semaphore;
30use sum_tree::Bias;
31use text::{BufferId, ToOffset, ToPoint};
32use util::{post_inc, ResultExt};
33
34pub struct InlayHintCache {
35 hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
36 allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
37 version: usize,
38 pub(super) enabled: bool,
39 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: Vec<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 editor
993 .semantics_provider
994 .as_ref()?
995 .inlay_hints(buffer, fetch_range.clone(), cx)
996 })
997 .ok()
998 .flatten();
999
1000 let cached_excerpt_hints = editor.update(cx, |editor, _| {
1001 editor
1002 .inlay_hint_cache
1003 .hints
1004 .get(&query.excerpt_id)
1005 .cloned()
1006 })?;
1007
1008 let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx))?;
1009 let new_hints = match inlay_hints_fetch_task {
1010 Some(fetch_task) => {
1011 log::debug!(
1012 "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
1013 query_reason = query.reason,
1014 );
1015 log::trace!(
1016 "Currently visible hints: {visible_hints:?}, cached hints present: {}",
1017 cached_excerpt_hints.is_some(),
1018 );
1019 fetch_task.await.context("inlay hint fetch task")?
1020 }
1021 None => return Ok(()),
1022 };
1023 drop(lsp_request_guard);
1024 log::debug!(
1025 "Fetched {} hints for range {fetch_range_to_log:?}",
1026 new_hints.len()
1027 );
1028 log::trace!("Fetched hints: {new_hints:?}");
1029
1030 let background_task_buffer_snapshot = buffer_snapshot.clone();
1031 let background_fetch_range = fetch_range.clone();
1032 let new_update = cx.background_spawn(async move {
1033 calculate_hint_updates(
1034 query.excerpt_id,
1035 invalidate,
1036 background_fetch_range,
1037 new_hints,
1038 &background_task_buffer_snapshot,
1039 cached_excerpt_hints,
1040 &visible_hints,
1041 )
1042 })
1043 .await;
1044 if let Some(new_update) = new_update {
1045 log::debug!(
1046 "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
1047 new_update.remove_from_visible.len(),
1048 new_update.remove_from_cache.len(),
1049 new_update.add_to_cache.len()
1050 );
1051 log::trace!("New update: {new_update:?}");
1052 editor
1053 .update(cx, |editor, cx| {
1054 apply_hint_update(
1055 editor,
1056 new_update,
1057 query,
1058 invalidate,
1059 buffer_snapshot,
1060 multi_buffer_snapshot,
1061 cx,
1062 );
1063 })
1064 .ok();
1065 }
1066 anyhow::Ok(())
1067 })
1068}
1069
1070fn calculate_hint_updates(
1071 excerpt_id: ExcerptId,
1072 invalidate: bool,
1073 fetch_range: Range<language::Anchor>,
1074 new_excerpt_hints: Vec<InlayHint>,
1075 buffer_snapshot: &BufferSnapshot,
1076 cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
1077 visible_hints: &[Inlay],
1078) -> Option<ExcerptHintsUpdate> {
1079 let mut add_to_cache = Vec::<InlayHint>::new();
1080 let mut excerpt_hints_to_persist = HashMap::default();
1081 for new_hint in new_excerpt_hints {
1082 if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
1083 continue;
1084 }
1085 let missing_from_cache = match &cached_excerpt_hints {
1086 Some(cached_excerpt_hints) => {
1087 let cached_excerpt_hints = cached_excerpt_hints.read();
1088 match cached_excerpt_hints
1089 .ordered_hints
1090 .binary_search_by(|probe| {
1091 cached_excerpt_hints.hints_by_id[probe]
1092 .position
1093 .cmp(&new_hint.position, buffer_snapshot)
1094 }) {
1095 Ok(ix) => {
1096 let mut missing_from_cache = true;
1097 for id in &cached_excerpt_hints.ordered_hints[ix..] {
1098 let cached_hint = &cached_excerpt_hints.hints_by_id[id];
1099 if new_hint
1100 .position
1101 .cmp(&cached_hint.position, buffer_snapshot)
1102 .is_gt()
1103 {
1104 break;
1105 }
1106 if cached_hint == &new_hint {
1107 excerpt_hints_to_persist.insert(*id, cached_hint.kind);
1108 missing_from_cache = false;
1109 }
1110 }
1111 missing_from_cache
1112 }
1113 Err(_) => true,
1114 }
1115 }
1116 None => true,
1117 };
1118 if missing_from_cache {
1119 add_to_cache.push(new_hint);
1120 }
1121 }
1122
1123 let mut remove_from_visible = HashSet::default();
1124 let mut remove_from_cache = HashSet::default();
1125 if invalidate {
1126 remove_from_visible.extend(
1127 visible_hints
1128 .iter()
1129 .filter(|hint| hint.position.excerpt_id == excerpt_id)
1130 .map(|inlay_hint| inlay_hint.id)
1131 .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
1132 );
1133
1134 if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
1135 let cached_excerpt_hints = cached_excerpt_hints.read();
1136 remove_from_cache.extend(
1137 cached_excerpt_hints
1138 .ordered_hints
1139 .iter()
1140 .filter(|cached_inlay_id| {
1141 !excerpt_hints_to_persist.contains_key(cached_inlay_id)
1142 })
1143 .copied(),
1144 );
1145 remove_from_visible.extend(remove_from_cache.iter().cloned());
1146 }
1147 }
1148
1149 if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
1150 None
1151 } else {
1152 Some(ExcerptHintsUpdate {
1153 excerpt_id,
1154 remove_from_visible,
1155 remove_from_cache,
1156 add_to_cache,
1157 })
1158 }
1159}
1160
1161fn contains_position(
1162 range: &Range<language::Anchor>,
1163 position: language::Anchor,
1164 buffer_snapshot: &BufferSnapshot,
1165) -> bool {
1166 range.start.cmp(&position, buffer_snapshot).is_le()
1167 && range.end.cmp(&position, buffer_snapshot).is_ge()
1168}
1169
1170fn apply_hint_update(
1171 editor: &mut Editor,
1172 new_update: ExcerptHintsUpdate,
1173 query: ExcerptQuery,
1174 invalidate: bool,
1175 buffer_snapshot: BufferSnapshot,
1176 multi_buffer_snapshot: MultiBufferSnapshot,
1177 cx: &mut Context<Editor>,
1178) {
1179 let cached_excerpt_hints = editor
1180 .inlay_hint_cache
1181 .hints
1182 .entry(new_update.excerpt_id)
1183 .or_insert_with(|| {
1184 Arc::new(RwLock::new(CachedExcerptHints {
1185 version: query.cache_version,
1186 buffer_version: buffer_snapshot.version().clone(),
1187 buffer_id: query.buffer_id,
1188 ordered_hints: Vec::new(),
1189 hints_by_id: HashMap::default(),
1190 }))
1191 });
1192 let mut cached_excerpt_hints = cached_excerpt_hints.write();
1193 match query.cache_version.cmp(&cached_excerpt_hints.version) {
1194 cmp::Ordering::Less => return,
1195 cmp::Ordering::Greater | cmp::Ordering::Equal => {
1196 cached_excerpt_hints.version = query.cache_version;
1197 }
1198 }
1199
1200 let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
1201 cached_excerpt_hints
1202 .ordered_hints
1203 .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id));
1204 cached_excerpt_hints
1205 .hints_by_id
1206 .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
1207 let mut splice = InlaySplice::default();
1208 splice.to_remove.extend(new_update.remove_from_visible);
1209 for new_hint in new_update.add_to_cache {
1210 let insert_position = match cached_excerpt_hints
1211 .ordered_hints
1212 .binary_search_by(|probe| {
1213 cached_excerpt_hints.hints_by_id[probe]
1214 .position
1215 .cmp(&new_hint.position, &buffer_snapshot)
1216 }) {
1217 Ok(i) => {
1218 // When a hint is added to the same position where existing ones are present,
1219 // do not deduplicate it: we split hint queries into non-overlapping ranges
1220 // and each hint batch returned by the server should already contain unique hints.
1221 i + cached_excerpt_hints.ordered_hints[i..].len() + 1
1222 }
1223 Err(i) => i,
1224 };
1225
1226 let new_inlay_id = post_inc(&mut editor.next_inlay_id);
1227 if editor
1228 .inlay_hint_cache
1229 .allowed_hint_kinds
1230 .contains(&new_hint.kind)
1231 {
1232 if let Some(new_hint_position) =
1233 multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position)
1234 {
1235 splice
1236 .to_insert
1237 .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
1238 }
1239 }
1240 let new_id = InlayId::Hint(new_inlay_id);
1241 cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
1242 if cached_excerpt_hints.ordered_hints.len() <= insert_position {
1243 cached_excerpt_hints.ordered_hints.push(new_id);
1244 } else {
1245 cached_excerpt_hints
1246 .ordered_hints
1247 .insert(insert_position, new_id);
1248 }
1249
1250 cached_inlays_changed = true;
1251 }
1252 cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
1253 drop(cached_excerpt_hints);
1254
1255 if invalidate {
1256 let mut outdated_excerpt_caches = HashSet::default();
1257 for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
1258 let excerpt_hints = excerpt_hints.read();
1259 if excerpt_hints.buffer_id == query.buffer_id
1260 && excerpt_id != &query.excerpt_id
1261 && buffer_snapshot
1262 .version()
1263 .changed_since(&excerpt_hints.buffer_version)
1264 {
1265 outdated_excerpt_caches.insert(*excerpt_id);
1266 splice
1267 .to_remove
1268 .extend(excerpt_hints.ordered_hints.iter().copied());
1269 }
1270 }
1271 cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
1272 editor
1273 .inlay_hint_cache
1274 .hints
1275 .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
1276 }
1277
1278 let InlaySplice {
1279 to_remove,
1280 to_insert,
1281 } = splice;
1282 let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty();
1283 if cached_inlays_changed || displayed_inlays_changed {
1284 editor.inlay_hint_cache.version += 1;
1285 }
1286 if displayed_inlays_changed {
1287 editor.splice_inlays(&to_remove, to_insert, cx)
1288 }
1289}
1290
1291#[cfg(test)]
1292pub mod tests {
1293 use crate::editor_tests::update_test_language_settings;
1294 use crate::scroll::ScrollAmount;
1295 use crate::{scroll::Autoscroll, test::editor_lsp_test_context::rust_lang, ExcerptRange};
1296 use futures::StreamExt;
1297 use gpui::{AppContext as _, Context, SemanticVersion, TestAppContext, WindowHandle};
1298 use itertools::Itertools as _;
1299 use language::{language_settings::AllLanguageSettingsContent, Capability, FakeLspAdapter};
1300 use language::{Language, LanguageConfig, LanguageMatcher};
1301 use lsp::FakeLanguageServer;
1302 use parking_lot::Mutex;
1303 use project::{FakeFs, Project};
1304 use serde_json::json;
1305 use settings::SettingsStore;
1306 use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
1307 use text::Point;
1308 use util::path;
1309
1310 use super::*;
1311
1312 #[gpui::test]
1313 async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
1314 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1315 init_test(cx, |settings| {
1316 settings.defaults.inlay_hints = Some(InlayHintSettings {
1317 enabled: true,
1318 edit_debounce_ms: 0,
1319 scroll_debounce_ms: 0,
1320 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1321 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1322 show_other_hints: allowed_hint_kinds.contains(&None),
1323 show_background: false,
1324 toggle_on_modifiers_press: None,
1325 })
1326 });
1327 let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1328 let lsp_request_count = Arc::new(AtomicU32::new(0));
1329 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1330 let task_lsp_request_count = Arc::clone(&lsp_request_count);
1331 async move {
1332 let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1;
1333 assert_eq!(
1334 params.text_document.uri,
1335 lsp::Url::from_file_path(file_with_hints).unwrap(),
1336 );
1337 Ok(Some(vec![lsp::InlayHint {
1338 position: lsp::Position::new(0, i),
1339 label: lsp::InlayHintLabel::String(i.to_string()),
1340 kind: None,
1341 text_edits: None,
1342 tooltip: None,
1343 padding_left: None,
1344 padding_right: None,
1345 data: None,
1346 }]))
1347 }
1348 });
1349 })
1350 .await;
1351 cx.executor().run_until_parked();
1352
1353 editor
1354 .update(cx, |editor, _window, cx| {
1355 let expected_hints = vec!["1".to_string()];
1356 assert_eq!(
1357 expected_hints,
1358 cached_hint_labels(editor),
1359 "Should get its first hints when opening the editor"
1360 );
1361 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1362 let inlay_cache = editor.inlay_hint_cache();
1363 assert_eq!(
1364 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1365 "Cache should use editor settings to get the allowed hint kinds"
1366 );
1367 })
1368 .unwrap();
1369
1370 editor
1371 .update(cx, |editor, window, cx| {
1372 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1373 editor.handle_input("some change", window, cx);
1374 })
1375 .unwrap();
1376 cx.executor().run_until_parked();
1377 editor
1378 .update(cx, |editor, _window, cx| {
1379 let expected_hints = vec!["2".to_string()];
1380 assert_eq!(
1381 expected_hints,
1382 cached_hint_labels(editor),
1383 "Should get new hints after an edit"
1384 );
1385 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1386 let inlay_cache = editor.inlay_hint_cache();
1387 assert_eq!(
1388 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1389 "Cache should use editor settings to get the allowed hint kinds"
1390 );
1391 })
1392 .unwrap();
1393
1394 fake_server
1395 .request::<lsp::request::InlayHintRefreshRequest>(())
1396 .await
1397 .expect("inlay refresh request failed");
1398 cx.executor().run_until_parked();
1399 editor
1400 .update(cx, |editor, _window, cx| {
1401 let expected_hints = vec!["3".to_string()];
1402 assert_eq!(
1403 expected_hints,
1404 cached_hint_labels(editor),
1405 "Should get new hints after hint refresh/ request"
1406 );
1407 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1408 let inlay_cache = editor.inlay_hint_cache();
1409 assert_eq!(
1410 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1411 "Cache should use editor settings to get the allowed hint kinds"
1412 );
1413 })
1414 .unwrap();
1415 }
1416
1417 #[gpui::test]
1418 async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
1419 init_test(cx, |settings| {
1420 settings.defaults.inlay_hints = Some(InlayHintSettings {
1421 enabled: true,
1422 edit_debounce_ms: 0,
1423 scroll_debounce_ms: 0,
1424 show_type_hints: true,
1425 show_parameter_hints: true,
1426 show_other_hints: true,
1427 show_background: false,
1428 toggle_on_modifiers_press: None,
1429 })
1430 });
1431
1432 let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1433 let lsp_request_count = Arc::new(AtomicU32::new(0));
1434 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1435 let task_lsp_request_count = Arc::clone(&lsp_request_count);
1436 async move {
1437 assert_eq!(
1438 params.text_document.uri,
1439 lsp::Url::from_file_path(file_with_hints).unwrap(),
1440 );
1441 let current_call_id =
1442 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1443 Ok(Some(vec![lsp::InlayHint {
1444 position: lsp::Position::new(0, current_call_id),
1445 label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1446 kind: None,
1447 text_edits: None,
1448 tooltip: None,
1449 padding_left: None,
1450 padding_right: None,
1451 data: None,
1452 }]))
1453 }
1454 });
1455 })
1456 .await;
1457 cx.executor().run_until_parked();
1458
1459 editor
1460 .update(cx, |editor, _, cx| {
1461 let expected_hints = vec!["0".to_string()];
1462 assert_eq!(
1463 expected_hints,
1464 cached_hint_labels(editor),
1465 "Should get its first hints when opening the editor"
1466 );
1467 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1468 })
1469 .unwrap();
1470
1471 let progress_token = "test_progress_token";
1472 fake_server
1473 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
1474 token: lsp::ProgressToken::String(progress_token.to_string()),
1475 })
1476 .await
1477 .expect("work done progress create request failed");
1478 cx.executor().run_until_parked();
1479 fake_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1480 token: lsp::ProgressToken::String(progress_token.to_string()),
1481 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1482 lsp::WorkDoneProgressBegin::default(),
1483 )),
1484 });
1485 cx.executor().run_until_parked();
1486
1487 editor
1488 .update(cx, |editor, _, cx| {
1489 let expected_hints = vec!["0".to_string()];
1490 assert_eq!(
1491 expected_hints,
1492 cached_hint_labels(editor),
1493 "Should not update hints while the work task is running"
1494 );
1495 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1496 })
1497 .unwrap();
1498
1499 fake_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1500 token: lsp::ProgressToken::String(progress_token.to_string()),
1501 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1502 lsp::WorkDoneProgressEnd::default(),
1503 )),
1504 });
1505 cx.executor().run_until_parked();
1506
1507 editor
1508 .update(cx, |editor, _, cx| {
1509 let expected_hints = vec!["1".to_string()];
1510 assert_eq!(
1511 expected_hints,
1512 cached_hint_labels(editor),
1513 "New hints should be queried after the work task is done"
1514 );
1515 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1516 })
1517 .unwrap();
1518 }
1519
1520 #[gpui::test]
1521 async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1522 init_test(cx, |settings| {
1523 settings.defaults.inlay_hints = Some(InlayHintSettings {
1524 enabled: true,
1525 edit_debounce_ms: 0,
1526 scroll_debounce_ms: 0,
1527 show_type_hints: true,
1528 show_parameter_hints: true,
1529 show_other_hints: true,
1530 show_background: false,
1531 toggle_on_modifiers_press: None,
1532 })
1533 });
1534
1535 let fs = FakeFs::new(cx.background_executor.clone());
1536 fs.insert_tree(
1537 path!("/a"),
1538 json!({
1539 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1540 "other.md": "Test md file with some text",
1541 }),
1542 )
1543 .await;
1544
1545 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1546
1547 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1548 let mut rs_fake_servers = None;
1549 let mut md_fake_servers = None;
1550 for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1551 language_registry.add(Arc::new(Language::new(
1552 LanguageConfig {
1553 name: name.into(),
1554 matcher: LanguageMatcher {
1555 path_suffixes: vec![path_suffix.to_string()],
1556 ..Default::default()
1557 },
1558 ..Default::default()
1559 },
1560 Some(tree_sitter_rust::LANGUAGE.into()),
1561 )));
1562 let fake_servers = language_registry.register_fake_lsp(
1563 name,
1564 FakeLspAdapter {
1565 name,
1566 capabilities: lsp::ServerCapabilities {
1567 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1568 ..Default::default()
1569 },
1570 initializer: Some(Box::new({
1571 move |fake_server| {
1572 let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1573 let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1574 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
1575 move |params, _| {
1576 let i = match name {
1577 "Rust" => {
1578 assert_eq!(
1579 params.text_document.uri,
1580 lsp::Url::from_file_path(path!("/a/main.rs"))
1581 .unwrap(),
1582 );
1583 rs_lsp_request_count.fetch_add(1, Ordering::Release) + 1
1584 }
1585 "Markdown" => {
1586 assert_eq!(
1587 params.text_document.uri,
1588 lsp::Url::from_file_path(path!("/a/other.md"))
1589 .unwrap(),
1590 );
1591 md_lsp_request_count.fetch_add(1, Ordering::Release) + 1
1592 }
1593 unexpected => panic!("Unexpected language: {unexpected}"),
1594 };
1595
1596 async move {
1597 let query_start = params.range.start;
1598 Ok(Some(vec![lsp::InlayHint {
1599 position: query_start,
1600 label: lsp::InlayHintLabel::String(i.to_string()),
1601 kind: None,
1602 text_edits: None,
1603 tooltip: None,
1604 padding_left: None,
1605 padding_right: None,
1606 data: None,
1607 }]))
1608 }
1609 },
1610 );
1611 }
1612 })),
1613 ..Default::default()
1614 },
1615 );
1616 match name {
1617 "Rust" => rs_fake_servers = Some(fake_servers),
1618 "Markdown" => md_fake_servers = Some(fake_servers),
1619 _ => unreachable!(),
1620 }
1621 }
1622
1623 let rs_buffer = project
1624 .update(cx, |project, cx| {
1625 project.open_local_buffer(path!("/a/main.rs"), cx)
1626 })
1627 .await
1628 .unwrap();
1629 let rs_editor = cx.add_window(|window, cx| {
1630 Editor::for_buffer(rs_buffer, Some(project.clone()), window, cx)
1631 });
1632 cx.executor().run_until_parked();
1633
1634 let _rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1635 cx.executor().run_until_parked();
1636 rs_editor
1637 .update(cx, |editor, _window, cx| {
1638 let expected_hints = vec!["1".to_string()];
1639 assert_eq!(
1640 expected_hints,
1641 cached_hint_labels(editor),
1642 "Should get its first hints when opening the editor"
1643 );
1644 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1645 })
1646 .unwrap();
1647
1648 cx.executor().run_until_parked();
1649 let md_buffer = project
1650 .update(cx, |project, cx| {
1651 project.open_local_buffer(path!("/a/other.md"), cx)
1652 })
1653 .await
1654 .unwrap();
1655 let md_editor =
1656 cx.add_window(|window, cx| Editor::for_buffer(md_buffer, Some(project), window, cx));
1657 cx.executor().run_until_parked();
1658
1659 let _md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1660 cx.executor().run_until_parked();
1661 md_editor
1662 .update(cx, |editor, _window, cx| {
1663 let expected_hints = vec!["1".to_string()];
1664 assert_eq!(
1665 expected_hints,
1666 cached_hint_labels(editor),
1667 "Markdown editor should have a separate version, repeating Rust editor rules"
1668 );
1669 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1670 })
1671 .unwrap();
1672
1673 rs_editor
1674 .update(cx, |editor, window, cx| {
1675 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1676 editor.handle_input("some rs change", window, cx);
1677 })
1678 .unwrap();
1679 cx.executor().run_until_parked();
1680 rs_editor
1681 .update(cx, |editor, _window, cx| {
1682 // TODO: Here, we do not get "2", because inserting another language server will trigger `RefreshInlayHints` event from the `LspStore`
1683 // A project is listened in every editor, so each of them will react to this event.
1684 //
1685 // We do not have language server IDs for remote projects, so cannot easily say on the editor level,
1686 // whether we should ignore a particular `RefreshInlayHints` event.
1687 let expected_hints = vec!["3".to_string()];
1688 assert_eq!(
1689 expected_hints,
1690 cached_hint_labels(editor),
1691 "Rust inlay cache should change after the edit"
1692 );
1693 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1694 })
1695 .unwrap();
1696 md_editor
1697 .update(cx, |editor, _window, cx| {
1698 let expected_hints = vec!["1".to_string()];
1699 assert_eq!(
1700 expected_hints,
1701 cached_hint_labels(editor),
1702 "Markdown editor should not be affected by Rust editor changes"
1703 );
1704 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1705 })
1706 .unwrap();
1707
1708 md_editor
1709 .update(cx, |editor, window, cx| {
1710 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1711 editor.handle_input("some md change", window, cx);
1712 })
1713 .unwrap();
1714 cx.executor().run_until_parked();
1715 md_editor
1716 .update(cx, |editor, _window, cx| {
1717 let expected_hints = vec!["2".to_string()];
1718 assert_eq!(
1719 expected_hints,
1720 cached_hint_labels(editor),
1721 "Rust editor should not be affected by Markdown editor changes"
1722 );
1723 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1724 })
1725 .unwrap();
1726 rs_editor
1727 .update(cx, |editor, _window, cx| {
1728 let expected_hints = vec!["3".to_string()];
1729 assert_eq!(
1730 expected_hints,
1731 cached_hint_labels(editor),
1732 "Markdown editor should also change independently"
1733 );
1734 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1735 })
1736 .unwrap();
1737 }
1738
1739 #[gpui::test]
1740 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1741 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1742 init_test(cx, |settings| {
1743 settings.defaults.inlay_hints = Some(InlayHintSettings {
1744 enabled: true,
1745 edit_debounce_ms: 0,
1746 scroll_debounce_ms: 0,
1747 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1748 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1749 show_other_hints: allowed_hint_kinds.contains(&None),
1750 show_background: false,
1751 toggle_on_modifiers_press: None,
1752 })
1753 });
1754
1755 let lsp_request_count = Arc::new(AtomicUsize::new(0));
1756 let (_, editor, fake_server) = prepare_test_objects(cx, {
1757 let lsp_request_count = lsp_request_count.clone();
1758 move |fake_server, file_with_hints| {
1759 let lsp_request_count = lsp_request_count.clone();
1760 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
1761 move |params, _| {
1762 lsp_request_count.fetch_add(1, Ordering::Release);
1763 async move {
1764 assert_eq!(
1765 params.text_document.uri,
1766 lsp::Url::from_file_path(file_with_hints).unwrap(),
1767 );
1768 Ok(Some(vec![
1769 lsp::InlayHint {
1770 position: lsp::Position::new(0, 1),
1771 label: lsp::InlayHintLabel::String("type hint".to_string()),
1772 kind: Some(lsp::InlayHintKind::TYPE),
1773 text_edits: None,
1774 tooltip: None,
1775 padding_left: None,
1776 padding_right: None,
1777 data: None,
1778 },
1779 lsp::InlayHint {
1780 position: lsp::Position::new(0, 2),
1781 label: lsp::InlayHintLabel::String(
1782 "parameter hint".to_string(),
1783 ),
1784 kind: Some(lsp::InlayHintKind::PARAMETER),
1785 text_edits: None,
1786 tooltip: None,
1787 padding_left: None,
1788 padding_right: None,
1789 data: None,
1790 },
1791 lsp::InlayHint {
1792 position: lsp::Position::new(0, 3),
1793 label: lsp::InlayHintLabel::String("other hint".to_string()),
1794 kind: None,
1795 text_edits: None,
1796 tooltip: None,
1797 padding_left: None,
1798 padding_right: None,
1799 data: None,
1800 },
1801 ]))
1802 }
1803 },
1804 );
1805 }
1806 })
1807 .await;
1808 cx.executor().run_until_parked();
1809
1810 editor
1811 .update(cx, |editor, _, cx| {
1812 assert_eq!(
1813 lsp_request_count.load(Ordering::Relaxed),
1814 1,
1815 "Should query new hints once"
1816 );
1817 assert_eq!(
1818 vec![
1819 "type hint".to_string(),
1820 "parameter hint".to_string(),
1821 "other hint".to_string(),
1822 ],
1823 cached_hint_labels(editor),
1824 "Should get its first hints when opening the editor"
1825 );
1826 assert_eq!(
1827 vec!["type hint".to_string(), "other hint".to_string()],
1828 visible_hint_labels(editor, cx)
1829 );
1830 let inlay_cache = editor.inlay_hint_cache();
1831 assert_eq!(
1832 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1833 "Cache should use editor settings to get the allowed hint kinds"
1834 );
1835 })
1836 .unwrap();
1837
1838 fake_server
1839 .request::<lsp::request::InlayHintRefreshRequest>(())
1840 .await
1841 .expect("inlay refresh request failed");
1842 cx.executor().run_until_parked();
1843 editor
1844 .update(cx, |editor, _, cx| {
1845 assert_eq!(
1846 lsp_request_count.load(Ordering::Relaxed),
1847 2,
1848 "Should load new hints twice"
1849 );
1850 assert_eq!(
1851 vec![
1852 "type hint".to_string(),
1853 "parameter hint".to_string(),
1854 "other hint".to_string(),
1855 ],
1856 cached_hint_labels(editor),
1857 "Cached hints should not change due to allowed hint kinds settings update"
1858 );
1859 assert_eq!(
1860 vec!["type hint".to_string(), "other hint".to_string()],
1861 visible_hint_labels(editor, cx)
1862 );
1863 })
1864 .unwrap();
1865
1866 for (new_allowed_hint_kinds, expected_visible_hints) in [
1867 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1868 (
1869 HashSet::from_iter([Some(InlayHintKind::Type)]),
1870 vec!["type hint".to_string()],
1871 ),
1872 (
1873 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1874 vec!["parameter hint".to_string()],
1875 ),
1876 (
1877 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1878 vec!["type hint".to_string(), "other hint".to_string()],
1879 ),
1880 (
1881 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1882 vec!["parameter hint".to_string(), "other hint".to_string()],
1883 ),
1884 (
1885 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1886 vec!["type hint".to_string(), "parameter hint".to_string()],
1887 ),
1888 (
1889 HashSet::from_iter([
1890 None,
1891 Some(InlayHintKind::Type),
1892 Some(InlayHintKind::Parameter),
1893 ]),
1894 vec![
1895 "type hint".to_string(),
1896 "parameter hint".to_string(),
1897 "other hint".to_string(),
1898 ],
1899 ),
1900 ] {
1901 update_test_language_settings(cx, |settings| {
1902 settings.defaults.inlay_hints = Some(InlayHintSettings {
1903 enabled: true,
1904 edit_debounce_ms: 0,
1905 scroll_debounce_ms: 0,
1906 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1907 show_parameter_hints: new_allowed_hint_kinds
1908 .contains(&Some(InlayHintKind::Parameter)),
1909 show_other_hints: new_allowed_hint_kinds.contains(&None),
1910 show_background: false,
1911 toggle_on_modifiers_press: None,
1912 })
1913 });
1914 cx.executor().run_until_parked();
1915 editor.update(cx, |editor, _, cx| {
1916 assert_eq!(
1917 lsp_request_count.load(Ordering::Relaxed),
1918 2,
1919 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1920 );
1921 assert_eq!(
1922 vec![
1923 "type hint".to_string(),
1924 "parameter hint".to_string(),
1925 "other hint".to_string(),
1926 ],
1927 cached_hint_labels(editor),
1928 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1929 );
1930 assert_eq!(
1931 expected_visible_hints,
1932 visible_hint_labels(editor, cx),
1933 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1934 );
1935 let inlay_cache = editor.inlay_hint_cache();
1936 assert_eq!(
1937 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1938 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1939 );
1940 }).unwrap();
1941 }
1942
1943 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1944 update_test_language_settings(cx, |settings| {
1945 settings.defaults.inlay_hints = Some(InlayHintSettings {
1946 enabled: false,
1947 edit_debounce_ms: 0,
1948 scroll_debounce_ms: 0,
1949 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1950 show_parameter_hints: another_allowed_hint_kinds
1951 .contains(&Some(InlayHintKind::Parameter)),
1952 show_other_hints: another_allowed_hint_kinds.contains(&None),
1953 show_background: false,
1954 toggle_on_modifiers_press: None,
1955 })
1956 });
1957 cx.executor().run_until_parked();
1958 editor
1959 .update(cx, |editor, _, cx| {
1960 assert_eq!(
1961 lsp_request_count.load(Ordering::Relaxed),
1962 2,
1963 "Should not load new hints when hints got disabled"
1964 );
1965 assert!(
1966 cached_hint_labels(editor).is_empty(),
1967 "Should clear the cache when hints got disabled"
1968 );
1969 assert!(
1970 visible_hint_labels(editor, cx).is_empty(),
1971 "Should clear visible hints when hints got disabled"
1972 );
1973 let inlay_cache = editor.inlay_hint_cache();
1974 assert_eq!(
1975 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1976 "Should update its allowed hint kinds even when hints got disabled"
1977 );
1978 })
1979 .unwrap();
1980
1981 fake_server
1982 .request::<lsp::request::InlayHintRefreshRequest>(())
1983 .await
1984 .expect("inlay refresh request failed");
1985 cx.executor().run_until_parked();
1986 editor
1987 .update(cx, |editor, _window, cx| {
1988 assert_eq!(
1989 lsp_request_count.load(Ordering::Relaxed),
1990 2,
1991 "Should not load new hints when they got disabled"
1992 );
1993 assert!(cached_hint_labels(editor).is_empty());
1994 assert!(visible_hint_labels(editor, cx).is_empty());
1995 })
1996 .unwrap();
1997
1998 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
1999 update_test_language_settings(cx, |settings| {
2000 settings.defaults.inlay_hints = Some(InlayHintSettings {
2001 enabled: true,
2002 edit_debounce_ms: 0,
2003 scroll_debounce_ms: 0,
2004 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
2005 show_parameter_hints: final_allowed_hint_kinds
2006 .contains(&Some(InlayHintKind::Parameter)),
2007 show_other_hints: final_allowed_hint_kinds.contains(&None),
2008 show_background: false,
2009 toggle_on_modifiers_press: None,
2010 })
2011 });
2012 cx.executor().run_until_parked();
2013 editor
2014 .update(cx, |editor, _, cx| {
2015 assert_eq!(
2016 lsp_request_count.load(Ordering::Relaxed),
2017 3,
2018 "Should query for new hints when they got re-enabled"
2019 );
2020 assert_eq!(
2021 vec![
2022 "type hint".to_string(),
2023 "parameter hint".to_string(),
2024 "other hint".to_string(),
2025 ],
2026 cached_hint_labels(editor),
2027 "Should get its cached hints fully repopulated after the hints got re-enabled"
2028 );
2029 assert_eq!(
2030 vec!["parameter hint".to_string()],
2031 visible_hint_labels(editor, cx),
2032 "Should get its visible hints repopulated and filtered after the h"
2033 );
2034 let inlay_cache = editor.inlay_hint_cache();
2035 assert_eq!(
2036 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
2037 "Cache should update editor settings when hints got re-enabled"
2038 );
2039 })
2040 .unwrap();
2041
2042 fake_server
2043 .request::<lsp::request::InlayHintRefreshRequest>(())
2044 .await
2045 .expect("inlay refresh request failed");
2046 cx.executor().run_until_parked();
2047 editor
2048 .update(cx, |editor, _, cx| {
2049 assert_eq!(
2050 lsp_request_count.load(Ordering::Relaxed),
2051 4,
2052 "Should query for new hints again"
2053 );
2054 assert_eq!(
2055 vec![
2056 "type hint".to_string(),
2057 "parameter hint".to_string(),
2058 "other hint".to_string(),
2059 ],
2060 cached_hint_labels(editor),
2061 );
2062 assert_eq!(
2063 vec!["parameter hint".to_string()],
2064 visible_hint_labels(editor, cx),
2065 );
2066 })
2067 .unwrap();
2068 }
2069
2070 #[gpui::test]
2071 async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
2072 init_test(cx, |settings| {
2073 settings.defaults.inlay_hints = Some(InlayHintSettings {
2074 enabled: true,
2075 edit_debounce_ms: 0,
2076 scroll_debounce_ms: 0,
2077 show_type_hints: true,
2078 show_parameter_hints: true,
2079 show_other_hints: true,
2080 show_background: false,
2081 toggle_on_modifiers_press: None,
2082 })
2083 });
2084
2085 let lsp_request_count = Arc::new(AtomicU32::new(0));
2086 let (_, editor, _) = prepare_test_objects(cx, {
2087 let lsp_request_count = lsp_request_count.clone();
2088 move |fake_server, file_with_hints| {
2089 let lsp_request_count = lsp_request_count.clone();
2090 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
2091 move |params, _| {
2092 let lsp_request_count = lsp_request_count.clone();
2093 async move {
2094 let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
2095 assert_eq!(
2096 params.text_document.uri,
2097 lsp::Url::from_file_path(file_with_hints).unwrap(),
2098 );
2099 Ok(Some(vec![lsp::InlayHint {
2100 position: lsp::Position::new(0, i),
2101 label: lsp::InlayHintLabel::String(i.to_string()),
2102 kind: None,
2103 text_edits: None,
2104 tooltip: None,
2105 padding_left: None,
2106 padding_right: None,
2107 data: None,
2108 }]))
2109 }
2110 },
2111 );
2112 }
2113 })
2114 .await;
2115
2116 let mut expected_changes = Vec::new();
2117 for change_after_opening in [
2118 "initial change #1",
2119 "initial change #2",
2120 "initial change #3",
2121 ] {
2122 editor
2123 .update(cx, |editor, window, cx| {
2124 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
2125 editor.handle_input(change_after_opening, window, cx);
2126 })
2127 .unwrap();
2128 expected_changes.push(change_after_opening);
2129 }
2130
2131 cx.executor().run_until_parked();
2132
2133 editor
2134 .update(cx, |editor, _window, cx| {
2135 let current_text = editor.text(cx);
2136 for change in &expected_changes {
2137 assert!(
2138 current_text.contains(change),
2139 "Should apply all changes made"
2140 );
2141 }
2142 assert_eq!(
2143 lsp_request_count.load(Ordering::Relaxed),
2144 2,
2145 "Should query new hints twice: for editor init and for the last edit that interrupted all others"
2146 );
2147 let expected_hints = vec!["2".to_string()];
2148 assert_eq!(
2149 expected_hints,
2150 cached_hint_labels(editor),
2151 "Should get hints from the last edit landed only"
2152 );
2153 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2154 })
2155 .unwrap();
2156
2157 let mut edits = Vec::new();
2158 for async_later_change in [
2159 "another change #1",
2160 "another change #2",
2161 "another change #3",
2162 ] {
2163 expected_changes.push(async_later_change);
2164 let task_editor = editor;
2165 edits.push(cx.spawn(|mut cx| async move {
2166 task_editor
2167 .update(&mut cx, |editor, window, cx| {
2168 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
2169 editor.handle_input(async_later_change, window, cx);
2170 })
2171 .unwrap();
2172 }));
2173 }
2174 let _ = future::join_all(edits).await;
2175 cx.executor().run_until_parked();
2176
2177 editor
2178 .update(cx, |editor, _, cx| {
2179 let current_text = editor.text(cx);
2180 for change in &expected_changes {
2181 assert!(
2182 current_text.contains(change),
2183 "Should apply all changes made"
2184 );
2185 }
2186 assert_eq!(
2187 lsp_request_count.load(Ordering::SeqCst),
2188 3,
2189 "Should query new hints one more time, for the last edit only"
2190 );
2191 let expected_hints = vec!["3".to_string()];
2192 assert_eq!(
2193 expected_hints,
2194 cached_hint_labels(editor),
2195 "Should get hints from the last edit landed only"
2196 );
2197 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2198 })
2199 .unwrap();
2200 }
2201
2202 #[gpui::test(iterations = 10)]
2203 async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
2204 init_test(cx, |settings| {
2205 settings.defaults.inlay_hints = Some(InlayHintSettings {
2206 enabled: true,
2207 edit_debounce_ms: 0,
2208 scroll_debounce_ms: 0,
2209 show_type_hints: true,
2210 show_parameter_hints: true,
2211 show_other_hints: true,
2212 show_background: false,
2213 toggle_on_modifiers_press: None,
2214 })
2215 });
2216
2217 let fs = FakeFs::new(cx.background_executor.clone());
2218 fs.insert_tree(
2219 path!("/a"),
2220 json!({
2221 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
2222 "other.rs": "// Test file",
2223 }),
2224 )
2225 .await;
2226
2227 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2228
2229 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2230 language_registry.add(rust_lang());
2231
2232 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2233 let lsp_request_count = Arc::new(AtomicUsize::new(0));
2234 let mut fake_servers = language_registry.register_fake_lsp(
2235 "Rust",
2236 FakeLspAdapter {
2237 capabilities: lsp::ServerCapabilities {
2238 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2239 ..Default::default()
2240 },
2241 initializer: Some(Box::new({
2242 let lsp_request_ranges = lsp_request_ranges.clone();
2243 let lsp_request_count = lsp_request_count.clone();
2244 move |fake_server| {
2245 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
2246 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2247 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
2248 move |params, _| {
2249 let task_lsp_request_ranges =
2250 Arc::clone(&closure_lsp_request_ranges);
2251 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2252 async move {
2253 assert_eq!(
2254 params.text_document.uri,
2255 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
2256 );
2257
2258 task_lsp_request_ranges.lock().push(params.range);
2259 task_lsp_request_count.fetch_add(1, Ordering::Release);
2260 Ok(Some(vec![lsp::InlayHint {
2261 position: params.range.end,
2262 label: lsp::InlayHintLabel::String(
2263 params.range.end.line.to_string(),
2264 ),
2265 kind: None,
2266 text_edits: None,
2267 tooltip: None,
2268 padding_left: None,
2269 padding_right: None,
2270 data: None,
2271 }]))
2272 }
2273 },
2274 );
2275 }
2276 })),
2277 ..Default::default()
2278 },
2279 );
2280
2281 let buffer = project
2282 .update(cx, |project, cx| {
2283 project.open_local_buffer(path!("/a/main.rs"), cx)
2284 })
2285 .await
2286 .unwrap();
2287 let editor =
2288 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
2289
2290 cx.executor().run_until_parked();
2291
2292 let _fake_server = fake_servers.next().await.unwrap();
2293
2294 // in large buffers, requests are made for more than visible range of a buffer.
2295 // invisible parts are queried later, to avoid excessive requests on quick typing.
2296 // wait the timeout needed to get all requests.
2297 cx.executor().advance_clock(Duration::from_millis(
2298 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2299 ));
2300 cx.executor().run_until_parked();
2301 let initial_visible_range = editor_visible_range(&editor, cx);
2302 let lsp_initial_visible_range = lsp::Range::new(
2303 lsp::Position::new(
2304 initial_visible_range.start.row,
2305 initial_visible_range.start.column,
2306 ),
2307 lsp::Position::new(
2308 initial_visible_range.end.row,
2309 initial_visible_range.end.column,
2310 ),
2311 );
2312 let expected_initial_query_range_end =
2313 lsp::Position::new(initial_visible_range.end.row * 2, 2);
2314 let mut expected_invisible_query_start = lsp_initial_visible_range.end;
2315 expected_invisible_query_start.character += 1;
2316 editor.update(cx, |editor, _window, cx| {
2317 let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2318 assert_eq!(ranges.len(), 2,
2319 "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:?}");
2320 let visible_query_range = &ranges[0];
2321 assert_eq!(visible_query_range.start, lsp_initial_visible_range.start);
2322 assert_eq!(visible_query_range.end, lsp_initial_visible_range.end);
2323 let invisible_query_range = &ranges[1];
2324
2325 assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document");
2326 assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document");
2327
2328 let requests_count = lsp_request_count.load(Ordering::Acquire);
2329 assert_eq!(requests_count, 2, "Visible + invisible request");
2330 let expected_hints = vec!["47".to_string(), "94".to_string()];
2331 assert_eq!(
2332 expected_hints,
2333 cached_hint_labels(editor),
2334 "Should have hints from both LSP requests made for a big file"
2335 );
2336 assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
2337 }).unwrap();
2338
2339 editor
2340 .update(cx, |editor, window, cx| {
2341 editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2342 })
2343 .unwrap();
2344 cx.executor().run_until_parked();
2345 editor
2346 .update(cx, |editor, window, cx| {
2347 editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2348 })
2349 .unwrap();
2350 cx.executor().advance_clock(Duration::from_millis(
2351 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2352 ));
2353 cx.executor().run_until_parked();
2354 let visible_range_after_scrolls = editor_visible_range(&editor, cx);
2355 let visible_line_count = editor
2356 .update(cx, |editor, _window, _| {
2357 editor.visible_line_count().unwrap()
2358 })
2359 .unwrap();
2360 let selection_in_cached_range = editor
2361 .update(cx, |editor, _window, cx| {
2362 let ranges = lsp_request_ranges
2363 .lock()
2364 .drain(..)
2365 .sorted_by_key(|r| r.start)
2366 .collect::<Vec<_>>();
2367 assert_eq!(
2368 ranges.len(),
2369 2,
2370 "Should query 2 ranges after both scrolls, but got: {ranges:?}"
2371 );
2372 let first_scroll = &ranges[0];
2373 let second_scroll = &ranges[1];
2374 assert_eq!(
2375 first_scroll.end, second_scroll.start,
2376 "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
2377 );
2378 assert_eq!(
2379 first_scroll.start, expected_initial_query_range_end,
2380 "First scroll should start the query right after the end of the original scroll",
2381 );
2382 assert_eq!(
2383 second_scroll.end,
2384 lsp::Position::new(
2385 visible_range_after_scrolls.end.row
2386 + visible_line_count.ceil() as u32,
2387 1,
2388 ),
2389 "Second scroll should query one more screen down after the end of the visible range"
2390 );
2391
2392 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2393 assert_eq!(lsp_requests, 4, "Should query for hints after every scroll");
2394 let expected_hints = vec![
2395 "47".to_string(),
2396 "94".to_string(),
2397 "139".to_string(),
2398 "184".to_string(),
2399 ];
2400 assert_eq!(
2401 expected_hints,
2402 cached_hint_labels(editor),
2403 "Should have hints from the new LSP response after the edit"
2404 );
2405 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2406
2407 let mut selection_in_cached_range = visible_range_after_scrolls.end;
2408 selection_in_cached_range.row -= visible_line_count.ceil() as u32;
2409 selection_in_cached_range
2410 })
2411 .unwrap();
2412
2413 editor
2414 .update(cx, |editor, window, cx| {
2415 editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
2416 s.select_ranges([selection_in_cached_range..selection_in_cached_range])
2417 });
2418 })
2419 .unwrap();
2420 cx.executor().advance_clock(Duration::from_millis(
2421 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2422 ));
2423 cx.executor().run_until_parked();
2424 editor.update(cx, |_, _, _| {
2425 let ranges = lsp_request_ranges
2426 .lock()
2427 .drain(..)
2428 .sorted_by_key(|r| r.start)
2429 .collect::<Vec<_>>();
2430 assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
2431 assert_eq!(lsp_request_count.load(Ordering::Acquire), 4);
2432 }).unwrap();
2433
2434 editor
2435 .update(cx, |editor, window, cx| {
2436 editor.handle_input("++++more text++++", window, cx);
2437 })
2438 .unwrap();
2439 cx.executor().advance_clock(Duration::from_millis(
2440 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2441 ));
2442 cx.executor().run_until_parked();
2443 editor.update(cx, |editor, _window, cx| {
2444 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2445 ranges.sort_by_key(|r| r.start);
2446
2447 assert_eq!(ranges.len(), 3,
2448 "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
2449 let above_query_range = &ranges[0];
2450 let visible_query_range = &ranges[1];
2451 let below_query_range = &ranges[2];
2452 assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
2453 "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");
2454 assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line,
2455 "Visible range {visible_query_range:?} should be before below range {below_query_range:?}");
2456 assert!(above_query_range.start.line < selection_in_cached_range.row,
2457 "Hints should be queried with the selected range after the query range start");
2458 assert!(below_query_range.end.line > selection_in_cached_range.row,
2459 "Hints should be queried with the selected range before the query range end");
2460 assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32,
2461 "Hints query range should contain one more screen before");
2462 assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32,
2463 "Hints query range should contain one more screen after");
2464
2465 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2466 assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried");
2467 let expected_hints = vec!["67".to_string(), "115".to_string(), "163".to_string()];
2468 assert_eq!(expected_hints, cached_hint_labels(editor),
2469 "Should have hints from the new LSP response after the edit");
2470 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2471 }).unwrap();
2472 }
2473
2474 fn editor_visible_range(
2475 editor: &WindowHandle<Editor>,
2476 cx: &mut gpui::TestAppContext,
2477 ) -> Range<Point> {
2478 let ranges = editor
2479 .update(cx, |editor, _window, cx| {
2480 editor.excerpts_for_inlay_hints_query(None, cx)
2481 })
2482 .unwrap();
2483 assert_eq!(
2484 ranges.len(),
2485 1,
2486 "Single buffer should produce a single excerpt with visible range"
2487 );
2488 let (_, (excerpt_buffer, _, excerpt_visible_range)) = ranges.into_iter().next().unwrap();
2489 excerpt_buffer.update(cx, |buffer, _| {
2490 let snapshot = buffer.snapshot();
2491 let start = buffer
2492 .anchor_before(excerpt_visible_range.start)
2493 .to_point(&snapshot);
2494 let end = buffer
2495 .anchor_after(excerpt_visible_range.end)
2496 .to_point(&snapshot);
2497 start..end
2498 })
2499 }
2500
2501 #[gpui::test]
2502 async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
2503 init_test(cx, |settings| {
2504 settings.defaults.inlay_hints = Some(InlayHintSettings {
2505 enabled: true,
2506 edit_debounce_ms: 0,
2507 scroll_debounce_ms: 0,
2508 show_type_hints: true,
2509 show_parameter_hints: true,
2510 show_other_hints: true,
2511 show_background: false,
2512 toggle_on_modifiers_press: None,
2513 })
2514 });
2515
2516 let fs = FakeFs::new(cx.background_executor.clone());
2517 fs.insert_tree(
2518 path!("/a"),
2519 json!({
2520 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2521 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2522 }),
2523 )
2524 .await;
2525
2526 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2527
2528 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2529 let language = rust_lang();
2530 language_registry.add(language);
2531 let mut fake_servers = language_registry.register_fake_lsp(
2532 "Rust",
2533 FakeLspAdapter {
2534 capabilities: lsp::ServerCapabilities {
2535 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2536 ..Default::default()
2537 },
2538 ..Default::default()
2539 },
2540 );
2541
2542 let (buffer_1, _handle1) = project
2543 .update(cx, |project, cx| {
2544 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2545 })
2546 .await
2547 .unwrap();
2548 let (buffer_2, _handle2) = project
2549 .update(cx, |project, cx| {
2550 project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2551 })
2552 .await
2553 .unwrap();
2554 let multibuffer = cx.new(|cx| {
2555 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2556 multibuffer.push_excerpts(
2557 buffer_1.clone(),
2558 [
2559 ExcerptRange {
2560 context: Point::new(0, 0)..Point::new(2, 0),
2561 primary: None,
2562 },
2563 ExcerptRange {
2564 context: Point::new(4, 0)..Point::new(11, 0),
2565 primary: None,
2566 },
2567 ExcerptRange {
2568 context: Point::new(22, 0)..Point::new(33, 0),
2569 primary: None,
2570 },
2571 ExcerptRange {
2572 context: Point::new(44, 0)..Point::new(55, 0),
2573 primary: None,
2574 },
2575 ExcerptRange {
2576 context: Point::new(56, 0)..Point::new(66, 0),
2577 primary: None,
2578 },
2579 ExcerptRange {
2580 context: Point::new(67, 0)..Point::new(77, 0),
2581 primary: None,
2582 },
2583 ],
2584 cx,
2585 );
2586 multibuffer.push_excerpts(
2587 buffer_2.clone(),
2588 [
2589 ExcerptRange {
2590 context: Point::new(0, 1)..Point::new(2, 1),
2591 primary: None,
2592 },
2593 ExcerptRange {
2594 context: Point::new(4, 1)..Point::new(11, 1),
2595 primary: None,
2596 },
2597 ExcerptRange {
2598 context: Point::new(22, 1)..Point::new(33, 1),
2599 primary: None,
2600 },
2601 ExcerptRange {
2602 context: Point::new(44, 1)..Point::new(55, 1),
2603 primary: None,
2604 },
2605 ExcerptRange {
2606 context: Point::new(56, 1)..Point::new(66, 1),
2607 primary: None,
2608 },
2609 ExcerptRange {
2610 context: Point::new(67, 1)..Point::new(77, 1),
2611 primary: None,
2612 },
2613 ],
2614 cx,
2615 );
2616 multibuffer
2617 });
2618
2619 cx.executor().run_until_parked();
2620 let editor = cx.add_window(|window, cx| {
2621 Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2622 });
2623
2624 let editor_edited = Arc::new(AtomicBool::new(false));
2625 let fake_server = fake_servers.next().await.unwrap();
2626 let closure_editor_edited = Arc::clone(&editor_edited);
2627 fake_server
2628 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2629 let task_editor_edited = Arc::clone(&closure_editor_edited);
2630 async move {
2631 let hint_text = if params.text_document.uri
2632 == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
2633 {
2634 "main hint"
2635 } else if params.text_document.uri
2636 == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
2637 {
2638 "other hint"
2639 } else {
2640 panic!("unexpected uri: {:?}", params.text_document.uri);
2641 };
2642
2643 // one hint per excerpt
2644 let positions = [
2645 lsp::Position::new(0, 2),
2646 lsp::Position::new(4, 2),
2647 lsp::Position::new(22, 2),
2648 lsp::Position::new(44, 2),
2649 lsp::Position::new(56, 2),
2650 lsp::Position::new(67, 2),
2651 ];
2652 let out_of_range_hint = lsp::InlayHint {
2653 position: lsp::Position::new(
2654 params.range.start.line + 99,
2655 params.range.start.character + 99,
2656 ),
2657 label: lsp::InlayHintLabel::String(
2658 "out of excerpt range, should be ignored".to_string(),
2659 ),
2660 kind: None,
2661 text_edits: None,
2662 tooltip: None,
2663 padding_left: None,
2664 padding_right: None,
2665 data: None,
2666 };
2667
2668 let edited = task_editor_edited.load(Ordering::Acquire);
2669 Ok(Some(
2670 std::iter::once(out_of_range_hint)
2671 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2672 lsp::InlayHint {
2673 position,
2674 label: lsp::InlayHintLabel::String(format!(
2675 "{hint_text}{E} #{i}",
2676 E = if edited { "(edited)" } else { "" },
2677 )),
2678 kind: None,
2679 text_edits: None,
2680 tooltip: None,
2681 padding_left: None,
2682 padding_right: None,
2683 data: None,
2684 }
2685 }))
2686 .collect(),
2687 ))
2688 }
2689 })
2690 .next()
2691 .await;
2692 cx.executor().run_until_parked();
2693
2694 editor
2695 .update(cx, |editor, _window, cx| {
2696 let expected_hints = vec![
2697 "main hint #0".to_string(),
2698 "main hint #1".to_string(),
2699 "main hint #2".to_string(),
2700 "main hint #3".to_string(),
2701 "main hint #4".to_string(),
2702 "main hint #5".to_string(),
2703 ];
2704 assert_eq!(
2705 expected_hints,
2706 sorted_cached_hint_labels(editor),
2707 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2708 );
2709 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2710 })
2711 .unwrap();
2712
2713 editor
2714 .update(cx, |editor, window, cx| {
2715 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2716 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2717 });
2718 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2719 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2720 });
2721 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2722 s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
2723 });
2724 })
2725 .unwrap();
2726 cx.executor().run_until_parked();
2727 editor
2728 .update(cx, |editor, _window, cx| {
2729 let expected_hints = vec![
2730 "main hint #0".to_string(),
2731 "main hint #1".to_string(),
2732 "main hint #2".to_string(),
2733 "main hint #3".to_string(),
2734 "main hint #4".to_string(),
2735 "main hint #5".to_string(),
2736 "other hint #0".to_string(),
2737 "other hint #1".to_string(),
2738 "other hint #2".to_string(),
2739 ];
2740 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2741 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2742 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2743 })
2744 .unwrap();
2745
2746 editor
2747 .update(cx, |editor, window, cx| {
2748 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2749 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2750 });
2751 })
2752 .unwrap();
2753 cx.executor().advance_clock(Duration::from_millis(
2754 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2755 ));
2756 cx.executor().run_until_parked();
2757 editor
2758 .update(cx, |editor, _window, cx| {
2759 let expected_hints = vec![
2760 "main hint #0".to_string(),
2761 "main hint #1".to_string(),
2762 "main hint #2".to_string(),
2763 "main hint #3".to_string(),
2764 "main hint #4".to_string(),
2765 "main hint #5".to_string(),
2766 "other hint #0".to_string(),
2767 "other hint #1".to_string(),
2768 "other hint #2".to_string(),
2769 "other hint #3".to_string(),
2770 "other hint #4".to_string(),
2771 "other hint #5".to_string(),
2772 ];
2773 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2774 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2775 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2776 })
2777 .unwrap();
2778
2779 editor
2780 .update(cx, |editor, window, cx| {
2781 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2782 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2783 });
2784 })
2785 .unwrap();
2786 cx.executor().advance_clock(Duration::from_millis(
2787 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2788 ));
2789 cx.executor().run_until_parked();
2790 editor
2791 .update(cx, |editor, _window, cx| {
2792 let expected_hints = vec![
2793 "main hint #0".to_string(),
2794 "main hint #1".to_string(),
2795 "main hint #2".to_string(),
2796 "main hint #3".to_string(),
2797 "main hint #4".to_string(),
2798 "main hint #5".to_string(),
2799 "other hint #0".to_string(),
2800 "other hint #1".to_string(),
2801 "other hint #2".to_string(),
2802 "other hint #3".to_string(),
2803 "other hint #4".to_string(),
2804 "other hint #5".to_string(),
2805 ];
2806 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2807 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2808 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2809 })
2810 .unwrap();
2811
2812 editor_edited.store(true, Ordering::Release);
2813 editor
2814 .update(cx, |editor, window, cx| {
2815 editor.change_selections(None, window, cx, |s| {
2816 s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
2817 });
2818 editor.handle_input("++++more text++++", window, cx);
2819 })
2820 .unwrap();
2821 cx.executor().run_until_parked();
2822 editor
2823 .update(cx, |editor, _window, cx| {
2824 let expected_hints = vec![
2825 "main hint #0".to_string(),
2826 "main hint #1".to_string(),
2827 "main hint #2".to_string(),
2828 "main hint #3".to_string(),
2829 "main hint #4".to_string(),
2830 "main hint #5".to_string(),
2831 "other hint(edited) #0".to_string(),
2832 "other hint(edited) #1".to_string(),
2833 ];
2834 assert_eq!(
2835 expected_hints,
2836 sorted_cached_hint_labels(editor),
2837 "After multibuffer edit, editor gets scrolled back to the last selection; \
2838 all hints should be invalidated and required for all of its visible excerpts"
2839 );
2840 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2841 })
2842 .unwrap();
2843 }
2844
2845 #[gpui::test]
2846 async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
2847 init_test(cx, |settings| {
2848 settings.defaults.inlay_hints = Some(InlayHintSettings {
2849 enabled: true,
2850 edit_debounce_ms: 0,
2851 scroll_debounce_ms: 0,
2852 show_type_hints: false,
2853 show_parameter_hints: false,
2854 show_other_hints: false,
2855 show_background: false,
2856 toggle_on_modifiers_press: None,
2857 })
2858 });
2859
2860 let fs = FakeFs::new(cx.background_executor.clone());
2861 fs.insert_tree(
2862 path!("/a"),
2863 json!({
2864 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2865 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2866 }),
2867 )
2868 .await;
2869
2870 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2871
2872 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2873 language_registry.add(rust_lang());
2874 let mut fake_servers = language_registry.register_fake_lsp(
2875 "Rust",
2876 FakeLspAdapter {
2877 capabilities: lsp::ServerCapabilities {
2878 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2879 ..Default::default()
2880 },
2881 ..Default::default()
2882 },
2883 );
2884
2885 let (buffer_1, _handle) = project
2886 .update(cx, |project, cx| {
2887 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2888 })
2889 .await
2890 .unwrap();
2891 let (buffer_2, _handle2) = project
2892 .update(cx, |project, cx| {
2893 project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2894 })
2895 .await
2896 .unwrap();
2897 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2898 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2899 let buffer_1_excerpts = multibuffer.push_excerpts(
2900 buffer_1.clone(),
2901 [ExcerptRange {
2902 context: Point::new(0, 0)..Point::new(2, 0),
2903 primary: None,
2904 }],
2905 cx,
2906 );
2907 let buffer_2_excerpts = multibuffer.push_excerpts(
2908 buffer_2.clone(),
2909 [ExcerptRange {
2910 context: Point::new(0, 1)..Point::new(2, 1),
2911 primary: None,
2912 }],
2913 cx,
2914 );
2915 (buffer_1_excerpts, buffer_2_excerpts)
2916 });
2917
2918 assert!(!buffer_1_excerpts.is_empty());
2919 assert!(!buffer_2_excerpts.is_empty());
2920
2921 cx.executor().run_until_parked();
2922 let editor = cx.add_window(|window, cx| {
2923 Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2924 });
2925 let editor_edited = Arc::new(AtomicBool::new(false));
2926 let fake_server = fake_servers.next().await.unwrap();
2927 let closure_editor_edited = Arc::clone(&editor_edited);
2928 fake_server
2929 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2930 let task_editor_edited = Arc::clone(&closure_editor_edited);
2931 async move {
2932 let hint_text = if params.text_document.uri
2933 == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
2934 {
2935 "main hint"
2936 } else if params.text_document.uri
2937 == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
2938 {
2939 "other hint"
2940 } else {
2941 panic!("unexpected uri: {:?}", params.text_document.uri);
2942 };
2943
2944 let positions = [
2945 lsp::Position::new(0, 2),
2946 lsp::Position::new(4, 2),
2947 lsp::Position::new(22, 2),
2948 lsp::Position::new(44, 2),
2949 lsp::Position::new(56, 2),
2950 lsp::Position::new(67, 2),
2951 ];
2952 let out_of_range_hint = lsp::InlayHint {
2953 position: lsp::Position::new(
2954 params.range.start.line + 99,
2955 params.range.start.character + 99,
2956 ),
2957 label: lsp::InlayHintLabel::String(
2958 "out of excerpt range, should be ignored".to_string(),
2959 ),
2960 kind: None,
2961 text_edits: None,
2962 tooltip: None,
2963 padding_left: None,
2964 padding_right: None,
2965 data: None,
2966 };
2967
2968 let edited = task_editor_edited.load(Ordering::Acquire);
2969 Ok(Some(
2970 std::iter::once(out_of_range_hint)
2971 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2972 lsp::InlayHint {
2973 position,
2974 label: lsp::InlayHintLabel::String(format!(
2975 "{hint_text}{} #{i}",
2976 if edited { "(edited)" } else { "" },
2977 )),
2978 kind: None,
2979 text_edits: None,
2980 tooltip: None,
2981 padding_left: None,
2982 padding_right: None,
2983 data: None,
2984 }
2985 }))
2986 .collect(),
2987 ))
2988 }
2989 })
2990 .next()
2991 .await;
2992 cx.executor().run_until_parked();
2993 editor
2994 .update(cx, |editor, _, cx| {
2995 assert_eq!(
2996 vec!["main hint #0".to_string(), "other hint #0".to_string()],
2997 sorted_cached_hint_labels(editor),
2998 "Cache should update for both excerpts despite hints display was disabled"
2999 );
3000 assert!(
3001 visible_hint_labels(editor, cx).is_empty(),
3002 "All hints are disabled and should not be shown despite being present in the cache"
3003 );
3004 })
3005 .unwrap();
3006
3007 editor
3008 .update(cx, |editor, _, cx| {
3009 editor.buffer().update(cx, |multibuffer, cx| {
3010 multibuffer.remove_excerpts(buffer_2_excerpts, cx)
3011 })
3012 })
3013 .unwrap();
3014 cx.executor().run_until_parked();
3015 editor
3016 .update(cx, |editor, _, cx| {
3017 assert_eq!(
3018 vec!["main hint #0".to_string()],
3019 cached_hint_labels(editor),
3020 "For the removed excerpt, should clean corresponding cached hints"
3021 );
3022 assert!(
3023 visible_hint_labels(editor, cx).is_empty(),
3024 "All hints are disabled and should not be shown despite being present in the cache"
3025 );
3026 })
3027 .unwrap();
3028
3029 update_test_language_settings(cx, |settings| {
3030 settings.defaults.inlay_hints = Some(InlayHintSettings {
3031 enabled: true,
3032 edit_debounce_ms: 0,
3033 scroll_debounce_ms: 0,
3034 show_type_hints: true,
3035 show_parameter_hints: true,
3036 show_other_hints: true,
3037 show_background: false,
3038 toggle_on_modifiers_press: None,
3039 })
3040 });
3041 cx.executor().run_until_parked();
3042 editor
3043 .update(cx, |editor, _, cx| {
3044 let expected_hints = vec!["main hint #0".to_string()];
3045 assert_eq!(
3046 expected_hints,
3047 cached_hint_labels(editor),
3048 "Hint display settings change should not change the cache"
3049 );
3050 assert_eq!(
3051 expected_hints,
3052 visible_hint_labels(editor, cx),
3053 "Settings change should make cached hints visible"
3054 );
3055 })
3056 .unwrap();
3057 }
3058
3059 #[gpui::test]
3060 async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
3061 init_test(cx, |settings| {
3062 settings.defaults.inlay_hints = Some(InlayHintSettings {
3063 enabled: true,
3064 edit_debounce_ms: 0,
3065 scroll_debounce_ms: 0,
3066 show_type_hints: true,
3067 show_parameter_hints: true,
3068 show_other_hints: true,
3069 show_background: false,
3070 toggle_on_modifiers_press: None,
3071 })
3072 });
3073
3074 let fs = FakeFs::new(cx.background_executor.clone());
3075 fs.insert_tree(
3076 path!("/a"),
3077 json!({
3078 "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)),
3079 "other.rs": "// Test file",
3080 }),
3081 )
3082 .await;
3083
3084 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3085
3086 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3087 language_registry.add(rust_lang());
3088 language_registry.register_fake_lsp(
3089 "Rust",
3090 FakeLspAdapter {
3091 capabilities: lsp::ServerCapabilities {
3092 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3093 ..Default::default()
3094 },
3095 initializer: Some(Box::new(move |fake_server| {
3096 let lsp_request_count = Arc::new(AtomicU32::new(0));
3097 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
3098 move |params, _| {
3099 let i = lsp_request_count.fetch_add(1, Ordering::Release) + 1;
3100 async move {
3101 assert_eq!(
3102 params.text_document.uri,
3103 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3104 );
3105 let query_start = params.range.start;
3106 Ok(Some(vec![lsp::InlayHint {
3107 position: query_start,
3108 label: lsp::InlayHintLabel::String(i.to_string()),
3109 kind: None,
3110 text_edits: None,
3111 tooltip: None,
3112 padding_left: None,
3113 padding_right: None,
3114 data: None,
3115 }]))
3116 }
3117 },
3118 );
3119 })),
3120 ..Default::default()
3121 },
3122 );
3123
3124 let buffer = project
3125 .update(cx, |project, cx| {
3126 project.open_local_buffer(path!("/a/main.rs"), cx)
3127 })
3128 .await
3129 .unwrap();
3130 let editor =
3131 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3132
3133 cx.executor().run_until_parked();
3134 editor
3135 .update(cx, |editor, window, cx| {
3136 editor.change_selections(None, window, cx, |s| {
3137 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3138 })
3139 })
3140 .unwrap();
3141 cx.executor().run_until_parked();
3142 editor
3143 .update(cx, |editor, _, cx| {
3144 let expected_hints = vec!["1".to_string()];
3145 assert_eq!(expected_hints, cached_hint_labels(editor));
3146 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3147 })
3148 .unwrap();
3149 }
3150
3151 #[gpui::test]
3152 async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
3153 init_test(cx, |settings| {
3154 settings.defaults.inlay_hints = Some(InlayHintSettings {
3155 enabled: false,
3156 edit_debounce_ms: 0,
3157 scroll_debounce_ms: 0,
3158 show_type_hints: true,
3159 show_parameter_hints: true,
3160 show_other_hints: true,
3161 show_background: false,
3162 toggle_on_modifiers_press: None,
3163 })
3164 });
3165
3166 let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3167 let lsp_request_count = Arc::new(AtomicU32::new(0));
3168 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
3169 let lsp_request_count = lsp_request_count.clone();
3170 async move {
3171 assert_eq!(
3172 params.text_document.uri,
3173 lsp::Url::from_file_path(file_with_hints).unwrap(),
3174 );
3175
3176 let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
3177 Ok(Some(vec![lsp::InlayHint {
3178 position: lsp::Position::new(0, i),
3179 label: lsp::InlayHintLabel::String(i.to_string()),
3180 kind: None,
3181 text_edits: None,
3182 tooltip: None,
3183 padding_left: None,
3184 padding_right: None,
3185 data: None,
3186 }]))
3187 }
3188 });
3189 })
3190 .await;
3191
3192 editor
3193 .update(cx, |editor, window, cx| {
3194 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3195 })
3196 .unwrap();
3197
3198 cx.executor().run_until_parked();
3199 editor
3200 .update(cx, |editor, _, cx| {
3201 let expected_hints = vec!["1".to_string()];
3202 assert_eq!(
3203 expected_hints,
3204 cached_hint_labels(editor),
3205 "Should display inlays after toggle despite them disabled in settings"
3206 );
3207 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3208 })
3209 .unwrap();
3210
3211 editor
3212 .update(cx, |editor, window, cx| {
3213 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3214 })
3215 .unwrap();
3216 cx.executor().run_until_parked();
3217 editor
3218 .update(cx, |editor, _, cx| {
3219 assert!(
3220 cached_hint_labels(editor).is_empty(),
3221 "Should clear hints after 2nd toggle"
3222 );
3223 assert!(visible_hint_labels(editor, cx).is_empty());
3224 })
3225 .unwrap();
3226
3227 update_test_language_settings(cx, |settings| {
3228 settings.defaults.inlay_hints = Some(InlayHintSettings {
3229 enabled: true,
3230 edit_debounce_ms: 0,
3231 scroll_debounce_ms: 0,
3232 show_type_hints: true,
3233 show_parameter_hints: true,
3234 show_other_hints: true,
3235 show_background: false,
3236 toggle_on_modifiers_press: None,
3237 })
3238 });
3239 cx.executor().run_until_parked();
3240 editor
3241 .update(cx, |editor, _, cx| {
3242 let expected_hints = vec!["2".to_string()];
3243 assert_eq!(
3244 expected_hints,
3245 cached_hint_labels(editor),
3246 "Should query LSP hints for the 2nd time after enabling hints in settings"
3247 );
3248 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3249 })
3250 .unwrap();
3251
3252 editor
3253 .update(cx, |editor, window, cx| {
3254 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3255 })
3256 .unwrap();
3257 cx.executor().run_until_parked();
3258 editor
3259 .update(cx, |editor, _, cx| {
3260 assert!(
3261 cached_hint_labels(editor).is_empty(),
3262 "Should clear hints after enabling in settings and a 3rd toggle"
3263 );
3264 assert!(visible_hint_labels(editor, cx).is_empty());
3265 })
3266 .unwrap();
3267
3268 editor
3269 .update(cx, |editor, window, cx| {
3270 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3271 })
3272 .unwrap();
3273 cx.executor().run_until_parked();
3274 editor.update(cx, |editor, _, cx| {
3275 let expected_hints = vec!["3".to_string()];
3276 assert_eq!(
3277 expected_hints,
3278 cached_hint_labels(editor),
3279 "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
3280 );
3281 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3282 }).unwrap();
3283 }
3284
3285 #[gpui::test]
3286 async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) {
3287 init_test(cx, |settings| {
3288 settings.defaults.inlay_hints = Some(InlayHintSettings {
3289 enabled: true,
3290 edit_debounce_ms: 0,
3291 scroll_debounce_ms: 0,
3292 show_type_hints: true,
3293 show_parameter_hints: true,
3294 show_other_hints: true,
3295 show_background: false,
3296 toggle_on_modifiers_press: None,
3297 })
3298 });
3299
3300 let fs = FakeFs::new(cx.background_executor.clone());
3301 fs.insert_tree(
3302 path!("/a"),
3303 json!({
3304 "main.rs": "fn main() {
3305 let x = 42;
3306 std::thread::scope(|s| {
3307 s.spawn(|| {
3308 let _x = x;
3309 });
3310 });
3311 }",
3312 "other.rs": "// Test file",
3313 }),
3314 )
3315 .await;
3316
3317 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3318
3319 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3320 language_registry.add(rust_lang());
3321 language_registry.register_fake_lsp(
3322 "Rust",
3323 FakeLspAdapter {
3324 capabilities: lsp::ServerCapabilities {
3325 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3326 ..Default::default()
3327 },
3328 initializer: Some(Box::new(move |fake_server| {
3329 fake_server.handle_request::<lsp::request::InlayHintRequest, _, _>(
3330 move |params, _| async move {
3331 assert_eq!(
3332 params.text_document.uri,
3333 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3334 );
3335 Ok(Some(
3336 serde_json::from_value(json!([
3337 {
3338 "position": {
3339 "line": 3,
3340 "character": 16
3341 },
3342 "label": "move",
3343 "paddingLeft": false,
3344 "paddingRight": false
3345 },
3346 {
3347 "position": {
3348 "line": 3,
3349 "character": 16
3350 },
3351 "label": "(",
3352 "paddingLeft": false,
3353 "paddingRight": false
3354 },
3355 {
3356 "position": {
3357 "line": 3,
3358 "character": 16
3359 },
3360 "label": [
3361 {
3362 "value": "&x"
3363 }
3364 ],
3365 "paddingLeft": false,
3366 "paddingRight": false,
3367 "data": {
3368 "file_id": 0
3369 }
3370 },
3371 {
3372 "position": {
3373 "line": 3,
3374 "character": 16
3375 },
3376 "label": ")",
3377 "paddingLeft": false,
3378 "paddingRight": true
3379 },
3380 // not a correct syntax, but checks that same symbols at the same place
3381 // are not deduplicated
3382 {
3383 "position": {
3384 "line": 3,
3385 "character": 16
3386 },
3387 "label": ")",
3388 "paddingLeft": false,
3389 "paddingRight": true
3390 },
3391 ]))
3392 .unwrap(),
3393 ))
3394 },
3395 );
3396 })),
3397 ..FakeLspAdapter::default()
3398 },
3399 );
3400
3401 let buffer = project
3402 .update(cx, |project, cx| {
3403 project.open_local_buffer(path!("/a/main.rs"), cx)
3404 })
3405 .await
3406 .unwrap();
3407 let editor =
3408 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3409
3410 cx.executor().run_until_parked();
3411 editor
3412 .update(cx, |editor, window, cx| {
3413 editor.change_selections(None, window, cx, |s| {
3414 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3415 })
3416 })
3417 .unwrap();
3418 cx.executor().run_until_parked();
3419 editor
3420 .update(cx, |editor, _window, cx| {
3421 let expected_hints = vec![
3422 "move".to_string(),
3423 "(".to_string(),
3424 "&x".to_string(),
3425 ") ".to_string(),
3426 ") ".to_string(),
3427 ];
3428 assert_eq!(
3429 expected_hints,
3430 cached_hint_labels(editor),
3431 "Editor inlay hints should repeat server's order when placed at the same spot"
3432 );
3433 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3434 })
3435 .unwrap();
3436 }
3437
3438 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
3439 cx.update(|cx| {
3440 let settings_store = SettingsStore::test(cx);
3441 cx.set_global(settings_store);
3442 theme::init(theme::LoadThemes::JustBase, cx);
3443 release_channel::init(SemanticVersion::default(), cx);
3444 client::init_settings(cx);
3445 language::init(cx);
3446 Project::init_settings(cx);
3447 workspace::init_settings(cx);
3448 crate::init(cx);
3449 });
3450
3451 update_test_language_settings(cx, f);
3452 }
3453
3454 async fn prepare_test_objects(
3455 cx: &mut TestAppContext,
3456 initialize: impl 'static + Send + Fn(&mut FakeLanguageServer, &'static str) + Send + Sync,
3457 ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
3458 let fs = FakeFs::new(cx.background_executor.clone());
3459 fs.insert_tree(
3460 path!("/a"),
3461 json!({
3462 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
3463 "other.rs": "// Test file",
3464 }),
3465 )
3466 .await;
3467
3468 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3469 let file_path = path!("/a/main.rs");
3470
3471 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3472 language_registry.add(rust_lang());
3473 let mut fake_servers = language_registry.register_fake_lsp(
3474 "Rust",
3475 FakeLspAdapter {
3476 capabilities: lsp::ServerCapabilities {
3477 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3478 ..Default::default()
3479 },
3480 initializer: Some(Box::new(move |server| initialize(server, file_path))),
3481 ..Default::default()
3482 },
3483 );
3484
3485 let buffer = project
3486 .update(cx, |project, cx| {
3487 project.open_local_buffer(path!("/a/main.rs"), cx)
3488 })
3489 .await
3490 .unwrap();
3491 let editor =
3492 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3493
3494 editor
3495 .update(cx, |editor, _, cx| {
3496 assert!(cached_hint_labels(editor).is_empty());
3497 assert!(visible_hint_labels(editor, cx).is_empty());
3498 })
3499 .unwrap();
3500
3501 cx.executor().run_until_parked();
3502 let fake_server = fake_servers.next().await.unwrap();
3503 (file_path, editor, fake_server)
3504 }
3505
3506 // 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.
3507 // Ensure a stable order for testing.
3508 fn sorted_cached_hint_labels(editor: &Editor) -> Vec<String> {
3509 let mut labels = cached_hint_labels(editor);
3510 labels.sort();
3511 labels
3512 }
3513
3514 pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
3515 let mut labels = Vec::new();
3516 for excerpt_hints in editor.inlay_hint_cache().hints.values() {
3517 let excerpt_hints = excerpt_hints.read();
3518 for id in &excerpt_hints.ordered_hints {
3519 let hint = &excerpt_hints.hints_by_id[id];
3520 let mut label = hint.text();
3521 if hint.padding_left {
3522 label.insert(0, ' ');
3523 }
3524 if hint.padding_right {
3525 label.push_str(" ");
3526 }
3527 labels.push(label);
3528 }
3529 }
3530
3531 labels
3532 }
3533
3534 pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
3535 editor
3536 .visible_inlay_hints(cx)
3537 .into_iter()
3538 .map(|hint| hint.text.to_string())
3539 .collect()
3540 }
3541}