1/// Stores and updates all data received from LSP <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint">textDocument/inlayHint</a> requests.
2/// Has nothing to do with other inlays, e.g. copilot suggestions — those are stored elsewhere.
3/// On every update, cache may query for more inlay hints and update inlays on the screen.
4///
5/// Inlays stored on screen are in [`crate::display_map::inlay_map`] and this cache is the only way to update any inlay hint data in the visible hints in the inlay map.
6/// For determining the update to the `inlay_map`, the cache requires a list of visible inlay hints — all other hints are not relevant and their separate updates are not influencing the cache work.
7///
8/// Due to the way the data is stored for both visible inlays and the cache, every inlay (and inlay hint) collection is editor-specific, so a single buffer may have multiple sets of inlays of open on different panes.
9use std::{
10 cmp,
11 ops::{ControlFlow, Range},
12 sync::Arc,
13 time::Duration,
14};
15
16use crate::{
17 Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot, display_map::Inlay,
18};
19use anyhow::Context as _;
20use clock::Global;
21use futures::future;
22use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window};
23use language::{Buffer, BufferSnapshot, language_settings::InlayHintKind};
24use parking_lot::RwLock;
25use project::{InlayHint, ResolveState};
26
27use collections::{HashMap, HashSet, hash_map};
28use language::language_settings::InlayHintSettings;
29use smol::lock::Semaphore;
30use sum_tree::Bias;
31use text::{BufferId, ToOffset, ToPoint};
32use util::{ResultExt, post_inc};
33
34pub struct InlayHintCache {
35 hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
36 allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
37 version: usize,
38 pub(super) enabled: bool,
39 modifiers_override: bool,
40 enabled_in_settings: bool,
41 update_tasks: HashMap<ExcerptId, TasksForRanges>,
42 refresh_task: Task<()>,
43 invalidate_debounce: Option<Duration>,
44 append_debounce: Option<Duration>,
45 lsp_request_limiter: Arc<Semaphore>,
46}
47
48#[derive(Debug)]
49struct TasksForRanges {
50 tasks: Vec<Task<()>>,
51 sorted_ranges: Vec<Range<language::Anchor>>,
52}
53
54#[derive(Debug)]
55struct CachedExcerptHints {
56 version: usize,
57 buffer_version: Global,
58 buffer_id: BufferId,
59 ordered_hints: Vec<InlayId>,
60 hints_by_id: HashMap<InlayId, InlayHint>,
61}
62
63/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts.
64#[derive(Debug, Clone, Copy)]
65pub(super) enum InvalidationStrategy {
66 /// Hints reset is <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_inlayHint_refresh">requested</a> by the LSP server.
67 /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation.
68 ///
69 /// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise.
70 RefreshRequested,
71 /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place.
72 /// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence.
73 BufferEdited,
74 /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position.
75 /// No invalidation should be done at all, all new hints are added to the cache.
76 ///
77 /// A special case is the settings change: in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other).
78 /// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen.
79 None,
80}
81
82/// A splice to send into the `inlay_map` for updating the visible inlays on the screen.
83/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes.
84/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead.
85/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.
86#[derive(Debug, Default)]
87pub(super) struct InlaySplice {
88 pub to_remove: Vec<InlayId>,
89 pub to_insert: Vec<Inlay>,
90}
91
92#[derive(Debug)]
93struct ExcerptHintsUpdate {
94 excerpt_id: ExcerptId,
95 remove_from_visible: HashSet<InlayId>,
96 remove_from_cache: HashSet<InlayId>,
97 add_to_cache: Vec<InlayHint>,
98}
99
100#[derive(Debug, Clone, Copy)]
101struct ExcerptQuery {
102 buffer_id: BufferId,
103 excerpt_id: ExcerptId,
104 cache_version: usize,
105 invalidate: InvalidationStrategy,
106 reason: &'static str,
107}
108
109impl InvalidationStrategy {
110 fn should_invalidate(&self) -> bool {
111 matches!(
112 self,
113 InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
114 )
115 }
116}
117
118impl TasksForRanges {
119 fn new(query_ranges: QueryRanges, task: Task<()>) -> Self {
120 Self {
121 tasks: vec![task],
122 sorted_ranges: query_ranges.into_sorted_query_ranges(),
123 }
124 }
125
126 fn update_cached_tasks(
127 &mut self,
128 buffer_snapshot: &BufferSnapshot,
129 query_ranges: QueryRanges,
130 invalidate: InvalidationStrategy,
131 spawn_task: impl FnOnce(QueryRanges) -> Task<()>,
132 ) {
133 let query_ranges = if invalidate.should_invalidate() {
134 self.tasks.clear();
135 self.sorted_ranges = query_ranges.clone().into_sorted_query_ranges();
136 query_ranges
137 } else {
138 let mut non_cached_query_ranges = query_ranges;
139 non_cached_query_ranges.before_visible = non_cached_query_ranges
140 .before_visible
141 .into_iter()
142 .flat_map(|query_range| {
143 self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
144 })
145 .collect();
146 non_cached_query_ranges.visible = non_cached_query_ranges
147 .visible
148 .into_iter()
149 .flat_map(|query_range| {
150 self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
151 })
152 .collect();
153 non_cached_query_ranges.after_visible = non_cached_query_ranges
154 .after_visible
155 .into_iter()
156 .flat_map(|query_range| {
157 self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
158 })
159 .collect();
160 non_cached_query_ranges
161 };
162
163 if !query_ranges.is_empty() {
164 self.tasks.push(spawn_task(query_ranges));
165 }
166 }
167
168 fn remove_cached_ranges_from_query(
169 &mut self,
170 buffer_snapshot: &BufferSnapshot,
171 query_range: Range<language::Anchor>,
172 ) -> Vec<Range<language::Anchor>> {
173 let mut ranges_to_query = Vec::new();
174 let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
175 for cached_range in self
176 .sorted_ranges
177 .iter_mut()
178 .skip_while(|cached_range| {
179 cached_range
180 .end
181 .cmp(&query_range.start, buffer_snapshot)
182 .is_lt()
183 })
184 .take_while(|cached_range| {
185 cached_range
186 .start
187 .cmp(&query_range.end, buffer_snapshot)
188 .is_le()
189 })
190 {
191 match latest_cached_range {
192 Some(latest_cached_range) => {
193 if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset
194 {
195 ranges_to_query.push(latest_cached_range.end..cached_range.start);
196 cached_range.start = latest_cached_range.end;
197 }
198 }
199 None => {
200 if query_range
201 .start
202 .cmp(&cached_range.start, buffer_snapshot)
203 .is_lt()
204 {
205 ranges_to_query.push(query_range.start..cached_range.start);
206 cached_range.start = query_range.start;
207 }
208 }
209 }
210 latest_cached_range = Some(cached_range);
211 }
212
213 match latest_cached_range {
214 Some(latest_cached_range) => {
215 if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset {
216 ranges_to_query.push(latest_cached_range.end..query_range.end);
217 latest_cached_range.end = query_range.end;
218 }
219 }
220 None => {
221 ranges_to_query.push(query_range.clone());
222 self.sorted_ranges.push(query_range);
223 self.sorted_ranges
224 .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot));
225 }
226 }
227
228 ranges_to_query
229 }
230
231 fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range<language::Anchor>) {
232 self.sorted_ranges = self
233 .sorted_ranges
234 .drain(..)
235 .filter_map(|mut cached_range| {
236 if cached_range.start.cmp(&range.end, buffer).is_gt()
237 || cached_range.end.cmp(&range.start, buffer).is_lt()
238 {
239 Some(vec![cached_range])
240 } else if cached_range.start.cmp(&range.start, buffer).is_ge()
241 && cached_range.end.cmp(&range.end, buffer).is_le()
242 {
243 None
244 } else if range.start.cmp(&cached_range.start, buffer).is_ge()
245 && range.end.cmp(&cached_range.end, buffer).is_le()
246 {
247 Some(vec![
248 cached_range.start..range.start,
249 range.end..cached_range.end,
250 ])
251 } else if cached_range.start.cmp(&range.start, buffer).is_ge() {
252 cached_range.start = range.end;
253 Some(vec![cached_range])
254 } else {
255 cached_range.end = range.start;
256 Some(vec![cached_range])
257 }
258 })
259 .flatten()
260 .collect();
261 }
262}
263
264impl InlayHintCache {
265 pub(super) fn new(inlay_hint_settings: InlayHintSettings) -> Self {
266 Self {
267 allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
268 enabled: inlay_hint_settings.enabled,
269 modifiers_override: false,
270 enabled_in_settings: inlay_hint_settings.enabled,
271 hints: HashMap::default(),
272 update_tasks: HashMap::default(),
273 refresh_task: Task::ready(()),
274 invalidate_debounce: debounce_value(inlay_hint_settings.edit_debounce_ms),
275 append_debounce: debounce_value(inlay_hint_settings.scroll_debounce_ms),
276 version: 0,
277 lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)),
278 }
279 }
280
281 /// Checks inlay hint settings for enabled hint kinds and general enabled state.
282 /// Generates corresponding inlay_map splice updates on settings changes.
283 /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries.
284 pub(super) fn update_settings(
285 &mut self,
286 multi_buffer: &Entity<MultiBuffer>,
287 new_hint_settings: InlayHintSettings,
288 visible_hints: Vec<Inlay>,
289 cx: &mut Context<Editor>,
290 ) -> ControlFlow<Option<InlaySplice>> {
291 let old_enabled = self.enabled;
292 // If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay
293 // hint visibility changes when other settings change (such as theme).
294 //
295 // Another option might be to store whether the user has manually toggled inlay hint
296 // visibility, and prefer this. This could lead to confusion as it means inlay hint
297 // visibility would not change when updating the setting if they were ever toggled.
298 if new_hint_settings.enabled != self.enabled_in_settings {
299 self.enabled = new_hint_settings.enabled;
300 self.enabled_in_settings = new_hint_settings.enabled;
301 self.modifiers_override = false;
302 };
303 self.invalidate_debounce = debounce_value(new_hint_settings.edit_debounce_ms);
304 self.append_debounce = debounce_value(new_hint_settings.scroll_debounce_ms);
305 let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
306 match (old_enabled, self.enabled) {
307 (false, false) => {
308 self.allowed_hint_kinds = new_allowed_hint_kinds;
309 ControlFlow::Break(None)
310 }
311 (true, true) => {
312 if new_allowed_hint_kinds == self.allowed_hint_kinds {
313 ControlFlow::Break(None)
314 } else {
315 let new_splice = self.new_allowed_hint_kinds_splice(
316 multi_buffer,
317 &visible_hints,
318 &new_allowed_hint_kinds,
319 cx,
320 );
321 if new_splice.is_some() {
322 self.version += 1;
323 self.allowed_hint_kinds = new_allowed_hint_kinds;
324 }
325 ControlFlow::Break(new_splice)
326 }
327 }
328 (true, false) => {
329 self.modifiers_override = false;
330 self.allowed_hint_kinds = new_allowed_hint_kinds;
331 if self.hints.is_empty() {
332 ControlFlow::Break(None)
333 } else {
334 self.clear();
335 ControlFlow::Break(Some(InlaySplice {
336 to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
337 to_insert: Vec::new(),
338 }))
339 }
340 }
341 (false, true) => {
342 self.modifiers_override = false;
343 self.allowed_hint_kinds = new_allowed_hint_kinds;
344 ControlFlow::Continue(())
345 }
346 }
347 }
348
349 pub(super) fn modifiers_override(&mut self, new_override: bool) -> Option<bool> {
350 if self.modifiers_override == new_override {
351 return None;
352 }
353 self.modifiers_override = new_override;
354 if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
355 {
356 self.clear();
357 Some(false)
358 } else {
359 Some(true)
360 }
361 }
362
363 pub(super) fn toggle(&mut self, enabled: bool) -> bool {
364 if self.enabled == enabled {
365 return false;
366 }
367 self.enabled = enabled;
368 self.modifiers_override = false;
369 if !enabled {
370 self.clear();
371 }
372 true
373 }
374
375 /// If needed, queries LSP for new inlay hints, using the invalidation strategy given.
376 /// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first,
377 /// followed by the delayed queries of the same range above and below the visible one.
378 /// This way, subsequent refresh invocations are less likely to trigger LSP queries for the invisible ranges.
379 pub(super) fn spawn_hint_refresh(
380 &mut self,
381 reason_description: &'static str,
382 excerpts_to_query: HashMap<ExcerptId, (Entity<Buffer>, Global, Range<usize>)>,
383 invalidate: InvalidationStrategy,
384 ignore_debounce: bool,
385 cx: &mut Context<Editor>,
386 ) -> Option<InlaySplice> {
387 if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
388 {
389 return None;
390 }
391 let mut invalidated_hints = Vec::new();
392 if invalidate.should_invalidate() {
393 self.update_tasks
394 .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
395 self.hints.retain(|cached_excerpt, cached_hints| {
396 let retain = excerpts_to_query.contains_key(cached_excerpt);
397 if !retain {
398 invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied());
399 }
400 retain
401 });
402 }
403 if excerpts_to_query.is_empty() && invalidated_hints.is_empty() {
404 return None;
405 }
406
407 let cache_version = self.version + 1;
408 let debounce_duration = if ignore_debounce {
409 None
410 } else if invalidate.should_invalidate() {
411 self.invalidate_debounce
412 } else {
413 self.append_debounce
414 };
415 self.refresh_task = cx.spawn(async move |editor, cx| {
416 if let Some(debounce_duration) = debounce_duration {
417 cx.background_executor().timer(debounce_duration).await;
418 }
419
420 editor
421 .update(cx, |editor, cx| {
422 spawn_new_update_tasks(
423 editor,
424 reason_description,
425 excerpts_to_query,
426 invalidate,
427 cache_version,
428 cx,
429 )
430 })
431 .ok();
432 });
433
434 if invalidated_hints.is_empty() {
435 None
436 } else {
437 Some(InlaySplice {
438 to_remove: invalidated_hints,
439 to_insert: Vec::new(),
440 })
441 }
442 }
443
444 fn new_allowed_hint_kinds_splice(
445 &self,
446 multi_buffer: &Entity<MultiBuffer>,
447 visible_hints: &[Inlay],
448 new_kinds: &HashSet<Option<InlayHintKind>>,
449 cx: &mut Context<Editor>,
450 ) -> Option<InlaySplice> {
451 let old_kinds = &self.allowed_hint_kinds;
452 if new_kinds == old_kinds {
453 return None;
454 }
455
456 let mut to_remove = Vec::new();
457 let mut to_insert = Vec::new();
458 let mut shown_hints_to_remove = visible_hints.iter().fold(
459 HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
460 |mut current_hints, inlay| {
461 current_hints
462 .entry(inlay.position.excerpt_id)
463 .or_default()
464 .push((inlay.position, inlay.id));
465 current_hints
466 },
467 );
468
469 let multi_buffer = multi_buffer.read(cx);
470 let multi_buffer_snapshot = multi_buffer.snapshot(cx);
471
472 for (excerpt_id, excerpt_cached_hints) in &self.hints {
473 let shown_excerpt_hints_to_remove =
474 shown_hints_to_remove.entry(*excerpt_id).or_default();
475 let excerpt_cached_hints = excerpt_cached_hints.read();
476 let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
477 shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
478 let Some(buffer) = shown_anchor
479 .buffer_id
480 .and_then(|buffer_id| multi_buffer.buffer(buffer_id))
481 else {
482 return false;
483 };
484 let buffer_snapshot = buffer.read(cx).snapshot();
485 loop {
486 match excerpt_cache.peek() {
487 Some(&cached_hint_id) => {
488 let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
489 if cached_hint_id == shown_hint_id {
490 excerpt_cache.next();
491 return !new_kinds.contains(&cached_hint.kind);
492 }
493
494 match cached_hint
495 .position
496 .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
497 {
498 cmp::Ordering::Less | cmp::Ordering::Equal => {
499 if !old_kinds.contains(&cached_hint.kind)
500 && new_kinds.contains(&cached_hint.kind)
501 {
502 if let Some(anchor) = multi_buffer_snapshot
503 .anchor_in_excerpt(*excerpt_id, cached_hint.position)
504 {
505 to_insert.push(Inlay::hint(
506 cached_hint_id.id(),
507 anchor,
508 cached_hint,
509 ));
510 }
511 }
512 excerpt_cache.next();
513 }
514 cmp::Ordering::Greater => return true,
515 }
516 }
517 None => return true,
518 }
519 }
520 });
521
522 for cached_hint_id in excerpt_cache {
523 let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
524 let cached_hint_kind = maybe_missed_cached_hint.kind;
525 if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
526 if let Some(anchor) = multi_buffer_snapshot
527 .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position)
528 {
529 to_insert.push(Inlay::hint(
530 cached_hint_id.id(),
531 anchor,
532 maybe_missed_cached_hint,
533 ));
534 }
535 }
536 }
537 }
538
539 to_remove.extend(
540 shown_hints_to_remove
541 .into_values()
542 .flatten()
543 .map(|(_, hint_id)| hint_id),
544 );
545 if to_remove.is_empty() && to_insert.is_empty() {
546 None
547 } else {
548 Some(InlaySplice {
549 to_remove,
550 to_insert,
551 })
552 }
553 }
554
555 /// Completely forget of certain excerpts that were removed from the multibuffer.
556 pub(super) fn remove_excerpts(
557 &mut self,
558 excerpts_removed: 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::{ExcerptRange, scroll::Autoscroll, test::editor_lsp_test_context::rust_lang};
1296 use futures::StreamExt;
1297 use gpui::{AppContext as _, Context, SemanticVersion, TestAppContext, WindowHandle};
1298 use itertools::Itertools as _;
1299 use language::{Capability, FakeLspAdapter, language_settings::AllLanguageSettingsContent};
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.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1330 move |params, _| {
1331 let task_lsp_request_count = Arc::clone(&lsp_request_count);
1332 async move {
1333 let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1;
1334 assert_eq!(
1335 params.text_document.uri,
1336 lsp::Url::from_file_path(file_with_hints).unwrap(),
1337 );
1338 Ok(Some(vec![lsp::InlayHint {
1339 position: lsp::Position::new(0, i),
1340 label: lsp::InlayHintLabel::String(i.to_string()),
1341 kind: None,
1342 text_edits: None,
1343 tooltip: None,
1344 padding_left: None,
1345 padding_right: None,
1346 data: None,
1347 }]))
1348 }
1349 },
1350 );
1351 })
1352 .await;
1353 cx.executor().run_until_parked();
1354
1355 editor
1356 .update(cx, |editor, _window, cx| {
1357 let expected_hints = vec!["1".to_string()];
1358 assert_eq!(
1359 expected_hints,
1360 cached_hint_labels(editor),
1361 "Should get its first hints when opening the editor"
1362 );
1363 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1364 let inlay_cache = editor.inlay_hint_cache();
1365 assert_eq!(
1366 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1367 "Cache should use editor settings to get the allowed hint kinds"
1368 );
1369 })
1370 .unwrap();
1371
1372 editor
1373 .update(cx, |editor, window, cx| {
1374 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1375 editor.handle_input("some change", window, cx);
1376 })
1377 .unwrap();
1378 cx.executor().run_until_parked();
1379 editor
1380 .update(cx, |editor, _window, cx| {
1381 let expected_hints = vec!["2".to_string()];
1382 assert_eq!(
1383 expected_hints,
1384 cached_hint_labels(editor),
1385 "Should get new hints after an edit"
1386 );
1387 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1388 let inlay_cache = editor.inlay_hint_cache();
1389 assert_eq!(
1390 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1391 "Cache should use editor settings to get the allowed hint kinds"
1392 );
1393 })
1394 .unwrap();
1395
1396 fake_server
1397 .request::<lsp::request::InlayHintRefreshRequest>(())
1398 .await
1399 .expect("inlay refresh request failed");
1400 cx.executor().run_until_parked();
1401 editor
1402 .update(cx, |editor, _window, cx| {
1403 let expected_hints = vec!["3".to_string()];
1404 assert_eq!(
1405 expected_hints,
1406 cached_hint_labels(editor),
1407 "Should get new hints after hint refresh/ request"
1408 );
1409 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1410 let inlay_cache = editor.inlay_hint_cache();
1411 assert_eq!(
1412 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1413 "Cache should use editor settings to get the allowed hint kinds"
1414 );
1415 })
1416 .unwrap();
1417 }
1418
1419 #[gpui::test]
1420 async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
1421 init_test(cx, |settings| {
1422 settings.defaults.inlay_hints = Some(InlayHintSettings {
1423 enabled: true,
1424 edit_debounce_ms: 0,
1425 scroll_debounce_ms: 0,
1426 show_type_hints: true,
1427 show_parameter_hints: true,
1428 show_other_hints: true,
1429 show_background: false,
1430 toggle_on_modifiers_press: None,
1431 })
1432 });
1433
1434 let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
1435 let lsp_request_count = Arc::new(AtomicU32::new(0));
1436 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1437 move |params, _| {
1438 let task_lsp_request_count = Arc::clone(&lsp_request_count);
1439 async move {
1440 assert_eq!(
1441 params.text_document.uri,
1442 lsp::Url::from_file_path(file_with_hints).unwrap(),
1443 );
1444 let current_call_id =
1445 Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
1446 Ok(Some(vec![lsp::InlayHint {
1447 position: lsp::Position::new(0, current_call_id),
1448 label: lsp::InlayHintLabel::String(current_call_id.to_string()),
1449 kind: None,
1450 text_edits: None,
1451 tooltip: None,
1452 padding_left: None,
1453 padding_right: None,
1454 data: None,
1455 }]))
1456 }
1457 },
1458 );
1459 })
1460 .await;
1461 cx.executor().run_until_parked();
1462
1463 editor
1464 .update(cx, |editor, _, cx| {
1465 let expected_hints = vec!["0".to_string()];
1466 assert_eq!(
1467 expected_hints,
1468 cached_hint_labels(editor),
1469 "Should get its first hints when opening the editor"
1470 );
1471 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1472 })
1473 .unwrap();
1474
1475 let progress_token = "test_progress_token";
1476 fake_server
1477 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
1478 token: lsp::ProgressToken::String(progress_token.to_string()),
1479 })
1480 .await
1481 .expect("work done progress create request failed");
1482 cx.executor().run_until_parked();
1483 fake_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1484 token: lsp::ProgressToken::String(progress_token.to_string()),
1485 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
1486 lsp::WorkDoneProgressBegin::default(),
1487 )),
1488 });
1489 cx.executor().run_until_parked();
1490
1491 editor
1492 .update(cx, |editor, _, cx| {
1493 let expected_hints = vec!["0".to_string()];
1494 assert_eq!(
1495 expected_hints,
1496 cached_hint_labels(editor),
1497 "Should not update hints while the work task is running"
1498 );
1499 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1500 })
1501 .unwrap();
1502
1503 fake_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1504 token: lsp::ProgressToken::String(progress_token.to_string()),
1505 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
1506 lsp::WorkDoneProgressEnd::default(),
1507 )),
1508 });
1509 cx.executor().run_until_parked();
1510
1511 editor
1512 .update(cx, |editor, _, cx| {
1513 let expected_hints = vec!["1".to_string()];
1514 assert_eq!(
1515 expected_hints,
1516 cached_hint_labels(editor),
1517 "New hints should be queried after the work task is done"
1518 );
1519 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1520 })
1521 .unwrap();
1522 }
1523
1524 #[gpui::test]
1525 async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
1526 init_test(cx, |settings| {
1527 settings.defaults.inlay_hints = Some(InlayHintSettings {
1528 enabled: true,
1529 edit_debounce_ms: 0,
1530 scroll_debounce_ms: 0,
1531 show_type_hints: true,
1532 show_parameter_hints: true,
1533 show_other_hints: true,
1534 show_background: false,
1535 toggle_on_modifiers_press: None,
1536 })
1537 });
1538
1539 let fs = FakeFs::new(cx.background_executor.clone());
1540 fs.insert_tree(
1541 path!("/a"),
1542 json!({
1543 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
1544 "other.md": "Test md file with some text",
1545 }),
1546 )
1547 .await;
1548
1549 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1550
1551 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1552 let mut rs_fake_servers = None;
1553 let mut md_fake_servers = None;
1554 for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
1555 language_registry.add(Arc::new(Language::new(
1556 LanguageConfig {
1557 name: name.into(),
1558 matcher: LanguageMatcher {
1559 path_suffixes: vec![path_suffix.to_string()],
1560 ..Default::default()
1561 },
1562 ..Default::default()
1563 },
1564 Some(tree_sitter_rust::LANGUAGE.into()),
1565 )));
1566 let fake_servers = language_registry.register_fake_lsp(
1567 name,
1568 FakeLspAdapter {
1569 name,
1570 capabilities: lsp::ServerCapabilities {
1571 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1572 ..Default::default()
1573 },
1574 initializer: Some(Box::new({
1575 move |fake_server| {
1576 let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
1577 let md_lsp_request_count = Arc::new(AtomicU32::new(0));
1578 fake_server
1579 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1580 move |params, _| {
1581 let i = match name {
1582 "Rust" => {
1583 assert_eq!(
1584 params.text_document.uri,
1585 lsp::Url::from_file_path(path!("/a/main.rs"))
1586 .unwrap(),
1587 );
1588 rs_lsp_request_count.fetch_add(1, Ordering::Release)
1589 + 1
1590 }
1591 "Markdown" => {
1592 assert_eq!(
1593 params.text_document.uri,
1594 lsp::Url::from_file_path(path!("/a/other.md"))
1595 .unwrap(),
1596 );
1597 md_lsp_request_count.fetch_add(1, Ordering::Release)
1598 + 1
1599 }
1600 unexpected => {
1601 panic!("Unexpected language: {unexpected}")
1602 }
1603 };
1604
1605 async move {
1606 let query_start = params.range.start;
1607 Ok(Some(vec![lsp::InlayHint {
1608 position: query_start,
1609 label: lsp::InlayHintLabel::String(i.to_string()),
1610 kind: None,
1611 text_edits: None,
1612 tooltip: None,
1613 padding_left: None,
1614 padding_right: None,
1615 data: None,
1616 }]))
1617 }
1618 },
1619 );
1620 }
1621 })),
1622 ..Default::default()
1623 },
1624 );
1625 match name {
1626 "Rust" => rs_fake_servers = Some(fake_servers),
1627 "Markdown" => md_fake_servers = Some(fake_servers),
1628 _ => unreachable!(),
1629 }
1630 }
1631
1632 let rs_buffer = project
1633 .update(cx, |project, cx| {
1634 project.open_local_buffer(path!("/a/main.rs"), cx)
1635 })
1636 .await
1637 .unwrap();
1638 let rs_editor = cx.add_window(|window, cx| {
1639 Editor::for_buffer(rs_buffer, Some(project.clone()), window, cx)
1640 });
1641 cx.executor().run_until_parked();
1642
1643 let _rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
1644 cx.executor().run_until_parked();
1645 rs_editor
1646 .update(cx, |editor, _window, cx| {
1647 let expected_hints = vec!["1".to_string()];
1648 assert_eq!(
1649 expected_hints,
1650 cached_hint_labels(editor),
1651 "Should get its first hints when opening the editor"
1652 );
1653 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1654 })
1655 .unwrap();
1656
1657 cx.executor().run_until_parked();
1658 let md_buffer = project
1659 .update(cx, |project, cx| {
1660 project.open_local_buffer(path!("/a/other.md"), cx)
1661 })
1662 .await
1663 .unwrap();
1664 let md_editor =
1665 cx.add_window(|window, cx| Editor::for_buffer(md_buffer, Some(project), window, cx));
1666 cx.executor().run_until_parked();
1667
1668 let _md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
1669 cx.executor().run_until_parked();
1670 md_editor
1671 .update(cx, |editor, _window, cx| {
1672 let expected_hints = vec!["1".to_string()];
1673 assert_eq!(
1674 expected_hints,
1675 cached_hint_labels(editor),
1676 "Markdown editor should have a separate version, repeating Rust editor rules"
1677 );
1678 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1679 })
1680 .unwrap();
1681
1682 rs_editor
1683 .update(cx, |editor, window, cx| {
1684 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1685 editor.handle_input("some rs change", window, cx);
1686 })
1687 .unwrap();
1688 cx.executor().run_until_parked();
1689 rs_editor
1690 .update(cx, |editor, _window, cx| {
1691 // TODO: Here, we do not get "2", because inserting another language server will trigger `RefreshInlayHints` event from the `LspStore`
1692 // A project is listened in every editor, so each of them will react to this event.
1693 //
1694 // We do not have language server IDs for remote projects, so cannot easily say on the editor level,
1695 // whether we should ignore a particular `RefreshInlayHints` event.
1696 let expected_hints = vec!["3".to_string()];
1697 assert_eq!(
1698 expected_hints,
1699 cached_hint_labels(editor),
1700 "Rust inlay cache should change after the edit"
1701 );
1702 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1703 })
1704 .unwrap();
1705 md_editor
1706 .update(cx, |editor, _window, cx| {
1707 let expected_hints = vec!["1".to_string()];
1708 assert_eq!(
1709 expected_hints,
1710 cached_hint_labels(editor),
1711 "Markdown editor should not be affected by Rust editor changes"
1712 );
1713 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1714 })
1715 .unwrap();
1716
1717 md_editor
1718 .update(cx, |editor, window, cx| {
1719 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1720 editor.handle_input("some md change", window, cx);
1721 })
1722 .unwrap();
1723 cx.executor().run_until_parked();
1724 md_editor
1725 .update(cx, |editor, _window, cx| {
1726 let expected_hints = vec!["2".to_string()];
1727 assert_eq!(
1728 expected_hints,
1729 cached_hint_labels(editor),
1730 "Rust editor should not be affected by Markdown editor changes"
1731 );
1732 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1733 })
1734 .unwrap();
1735 rs_editor
1736 .update(cx, |editor, _window, cx| {
1737 let expected_hints = vec!["3".to_string()];
1738 assert_eq!(
1739 expected_hints,
1740 cached_hint_labels(editor),
1741 "Markdown editor should also change independently"
1742 );
1743 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
1744 })
1745 .unwrap();
1746 }
1747
1748 #[gpui::test]
1749 async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
1750 let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
1751 init_test(cx, |settings| {
1752 settings.defaults.inlay_hints = Some(InlayHintSettings {
1753 enabled: true,
1754 edit_debounce_ms: 0,
1755 scroll_debounce_ms: 0,
1756 show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1757 show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
1758 show_other_hints: allowed_hint_kinds.contains(&None),
1759 show_background: false,
1760 toggle_on_modifiers_press: None,
1761 })
1762 });
1763
1764 let lsp_request_count = Arc::new(AtomicUsize::new(0));
1765 let (_, editor, fake_server) = prepare_test_objects(cx, {
1766 let lsp_request_count = lsp_request_count.clone();
1767 move |fake_server, file_with_hints| {
1768 let lsp_request_count = lsp_request_count.clone();
1769 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1770 move |params, _| {
1771 lsp_request_count.fetch_add(1, Ordering::Release);
1772 async move {
1773 assert_eq!(
1774 params.text_document.uri,
1775 lsp::Url::from_file_path(file_with_hints).unwrap(),
1776 );
1777 Ok(Some(vec![
1778 lsp::InlayHint {
1779 position: lsp::Position::new(0, 1),
1780 label: lsp::InlayHintLabel::String("type hint".to_string()),
1781 kind: Some(lsp::InlayHintKind::TYPE),
1782 text_edits: None,
1783 tooltip: None,
1784 padding_left: None,
1785 padding_right: None,
1786 data: None,
1787 },
1788 lsp::InlayHint {
1789 position: lsp::Position::new(0, 2),
1790 label: lsp::InlayHintLabel::String(
1791 "parameter hint".to_string(),
1792 ),
1793 kind: Some(lsp::InlayHintKind::PARAMETER),
1794 text_edits: None,
1795 tooltip: None,
1796 padding_left: None,
1797 padding_right: None,
1798 data: None,
1799 },
1800 lsp::InlayHint {
1801 position: lsp::Position::new(0, 3),
1802 label: lsp::InlayHintLabel::String("other hint".to_string()),
1803 kind: None,
1804 text_edits: None,
1805 tooltip: None,
1806 padding_left: None,
1807 padding_right: None,
1808 data: None,
1809 },
1810 ]))
1811 }
1812 },
1813 );
1814 }
1815 })
1816 .await;
1817 cx.executor().run_until_parked();
1818
1819 editor
1820 .update(cx, |editor, _, cx| {
1821 assert_eq!(
1822 lsp_request_count.load(Ordering::Relaxed),
1823 1,
1824 "Should query new hints once"
1825 );
1826 assert_eq!(
1827 vec![
1828 "type hint".to_string(),
1829 "parameter hint".to_string(),
1830 "other hint".to_string(),
1831 ],
1832 cached_hint_labels(editor),
1833 "Should get its first hints when opening the editor"
1834 );
1835 assert_eq!(
1836 vec!["type hint".to_string(), "other hint".to_string()],
1837 visible_hint_labels(editor, cx)
1838 );
1839 let inlay_cache = editor.inlay_hint_cache();
1840 assert_eq!(
1841 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
1842 "Cache should use editor settings to get the allowed hint kinds"
1843 );
1844 })
1845 .unwrap();
1846
1847 fake_server
1848 .request::<lsp::request::InlayHintRefreshRequest>(())
1849 .await
1850 .expect("inlay refresh request failed");
1851 cx.executor().run_until_parked();
1852 editor
1853 .update(cx, |editor, _, cx| {
1854 assert_eq!(
1855 lsp_request_count.load(Ordering::Relaxed),
1856 2,
1857 "Should load new hints twice"
1858 );
1859 assert_eq!(
1860 vec![
1861 "type hint".to_string(),
1862 "parameter hint".to_string(),
1863 "other hint".to_string(),
1864 ],
1865 cached_hint_labels(editor),
1866 "Cached hints should not change due to allowed hint kinds settings update"
1867 );
1868 assert_eq!(
1869 vec!["type hint".to_string(), "other hint".to_string()],
1870 visible_hint_labels(editor, cx)
1871 );
1872 })
1873 .unwrap();
1874
1875 for (new_allowed_hint_kinds, expected_visible_hints) in [
1876 (HashSet::from_iter([None]), vec!["other hint".to_string()]),
1877 (
1878 HashSet::from_iter([Some(InlayHintKind::Type)]),
1879 vec!["type hint".to_string()],
1880 ),
1881 (
1882 HashSet::from_iter([Some(InlayHintKind::Parameter)]),
1883 vec!["parameter hint".to_string()],
1884 ),
1885 (
1886 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
1887 vec!["type hint".to_string(), "other hint".to_string()],
1888 ),
1889 (
1890 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
1891 vec!["parameter hint".to_string(), "other hint".to_string()],
1892 ),
1893 (
1894 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
1895 vec!["type hint".to_string(), "parameter hint".to_string()],
1896 ),
1897 (
1898 HashSet::from_iter([
1899 None,
1900 Some(InlayHintKind::Type),
1901 Some(InlayHintKind::Parameter),
1902 ]),
1903 vec![
1904 "type hint".to_string(),
1905 "parameter hint".to_string(),
1906 "other hint".to_string(),
1907 ],
1908 ),
1909 ] {
1910 update_test_language_settings(cx, |settings| {
1911 settings.defaults.inlay_hints = Some(InlayHintSettings {
1912 enabled: true,
1913 edit_debounce_ms: 0,
1914 scroll_debounce_ms: 0,
1915 show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1916 show_parameter_hints: new_allowed_hint_kinds
1917 .contains(&Some(InlayHintKind::Parameter)),
1918 show_other_hints: new_allowed_hint_kinds.contains(&None),
1919 show_background: false,
1920 toggle_on_modifiers_press: None,
1921 })
1922 });
1923 cx.executor().run_until_parked();
1924 editor.update(cx, |editor, _, cx| {
1925 assert_eq!(
1926 lsp_request_count.load(Ordering::Relaxed),
1927 2,
1928 "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
1929 );
1930 assert_eq!(
1931 vec![
1932 "type hint".to_string(),
1933 "parameter hint".to_string(),
1934 "other hint".to_string(),
1935 ],
1936 cached_hint_labels(editor),
1937 "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1938 );
1939 assert_eq!(
1940 expected_visible_hints,
1941 visible_hint_labels(editor, cx),
1942 "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
1943 );
1944 let inlay_cache = editor.inlay_hint_cache();
1945 assert_eq!(
1946 inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
1947 "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
1948 );
1949 }).unwrap();
1950 }
1951
1952 let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
1953 update_test_language_settings(cx, |settings| {
1954 settings.defaults.inlay_hints = Some(InlayHintSettings {
1955 enabled: false,
1956 edit_debounce_ms: 0,
1957 scroll_debounce_ms: 0,
1958 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
1959 show_parameter_hints: another_allowed_hint_kinds
1960 .contains(&Some(InlayHintKind::Parameter)),
1961 show_other_hints: another_allowed_hint_kinds.contains(&None),
1962 show_background: false,
1963 toggle_on_modifiers_press: None,
1964 })
1965 });
1966 cx.executor().run_until_parked();
1967 editor
1968 .update(cx, |editor, _, cx| {
1969 assert_eq!(
1970 lsp_request_count.load(Ordering::Relaxed),
1971 2,
1972 "Should not load new hints when hints got disabled"
1973 );
1974 assert!(
1975 cached_hint_labels(editor).is_empty(),
1976 "Should clear the cache when hints got disabled"
1977 );
1978 assert!(
1979 visible_hint_labels(editor, cx).is_empty(),
1980 "Should clear visible hints when hints got disabled"
1981 );
1982 let inlay_cache = editor.inlay_hint_cache();
1983 assert_eq!(
1984 inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
1985 "Should update its allowed hint kinds even when hints got disabled"
1986 );
1987 })
1988 .unwrap();
1989
1990 fake_server
1991 .request::<lsp::request::InlayHintRefreshRequest>(())
1992 .await
1993 .expect("inlay refresh request failed");
1994 cx.executor().run_until_parked();
1995 editor
1996 .update(cx, |editor, _window, cx| {
1997 assert_eq!(
1998 lsp_request_count.load(Ordering::Relaxed),
1999 2,
2000 "Should not load new hints when they got disabled"
2001 );
2002 assert!(cached_hint_labels(editor).is_empty());
2003 assert!(visible_hint_labels(editor, cx).is_empty());
2004 })
2005 .unwrap();
2006
2007 let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
2008 update_test_language_settings(cx, |settings| {
2009 settings.defaults.inlay_hints = Some(InlayHintSettings {
2010 enabled: true,
2011 edit_debounce_ms: 0,
2012 scroll_debounce_ms: 0,
2013 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
2014 show_parameter_hints: final_allowed_hint_kinds
2015 .contains(&Some(InlayHintKind::Parameter)),
2016 show_other_hints: final_allowed_hint_kinds.contains(&None),
2017 show_background: false,
2018 toggle_on_modifiers_press: None,
2019 })
2020 });
2021 cx.executor().run_until_parked();
2022 editor
2023 .update(cx, |editor, _, cx| {
2024 assert_eq!(
2025 lsp_request_count.load(Ordering::Relaxed),
2026 3,
2027 "Should query for new hints when they got re-enabled"
2028 );
2029 assert_eq!(
2030 vec![
2031 "type hint".to_string(),
2032 "parameter hint".to_string(),
2033 "other hint".to_string(),
2034 ],
2035 cached_hint_labels(editor),
2036 "Should get its cached hints fully repopulated after the hints got re-enabled"
2037 );
2038 assert_eq!(
2039 vec!["parameter hint".to_string()],
2040 visible_hint_labels(editor, cx),
2041 "Should get its visible hints repopulated and filtered after the h"
2042 );
2043 let inlay_cache = editor.inlay_hint_cache();
2044 assert_eq!(
2045 inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
2046 "Cache should update editor settings when hints got re-enabled"
2047 );
2048 })
2049 .unwrap();
2050
2051 fake_server
2052 .request::<lsp::request::InlayHintRefreshRequest>(())
2053 .await
2054 .expect("inlay refresh request failed");
2055 cx.executor().run_until_parked();
2056 editor
2057 .update(cx, |editor, _, cx| {
2058 assert_eq!(
2059 lsp_request_count.load(Ordering::Relaxed),
2060 4,
2061 "Should query for new hints again"
2062 );
2063 assert_eq!(
2064 vec![
2065 "type hint".to_string(),
2066 "parameter hint".to_string(),
2067 "other hint".to_string(),
2068 ],
2069 cached_hint_labels(editor),
2070 );
2071 assert_eq!(
2072 vec!["parameter hint".to_string()],
2073 visible_hint_labels(editor, cx),
2074 );
2075 })
2076 .unwrap();
2077 }
2078
2079 #[gpui::test]
2080 async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
2081 init_test(cx, |settings| {
2082 settings.defaults.inlay_hints = Some(InlayHintSettings {
2083 enabled: true,
2084 edit_debounce_ms: 0,
2085 scroll_debounce_ms: 0,
2086 show_type_hints: true,
2087 show_parameter_hints: true,
2088 show_other_hints: true,
2089 show_background: false,
2090 toggle_on_modifiers_press: None,
2091 })
2092 });
2093
2094 let lsp_request_count = Arc::new(AtomicU32::new(0));
2095 let (_, editor, _) = prepare_test_objects(cx, {
2096 let lsp_request_count = lsp_request_count.clone();
2097 move |fake_server, file_with_hints| {
2098 let lsp_request_count = lsp_request_count.clone();
2099 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2100 move |params, _| {
2101 let lsp_request_count = lsp_request_count.clone();
2102 async move {
2103 let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
2104 assert_eq!(
2105 params.text_document.uri,
2106 lsp::Url::from_file_path(file_with_hints).unwrap(),
2107 );
2108 Ok(Some(vec![lsp::InlayHint {
2109 position: lsp::Position::new(0, i),
2110 label: lsp::InlayHintLabel::String(i.to_string()),
2111 kind: None,
2112 text_edits: None,
2113 tooltip: None,
2114 padding_left: None,
2115 padding_right: None,
2116 data: None,
2117 }]))
2118 }
2119 },
2120 );
2121 }
2122 })
2123 .await;
2124
2125 let mut expected_changes = Vec::new();
2126 for change_after_opening in [
2127 "initial change #1",
2128 "initial change #2",
2129 "initial change #3",
2130 ] {
2131 editor
2132 .update(cx, |editor, window, cx| {
2133 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
2134 editor.handle_input(change_after_opening, window, cx);
2135 })
2136 .unwrap();
2137 expected_changes.push(change_after_opening);
2138 }
2139
2140 cx.executor().run_until_parked();
2141
2142 editor
2143 .update(cx, |editor, _window, cx| {
2144 let current_text = editor.text(cx);
2145 for change in &expected_changes {
2146 assert!(
2147 current_text.contains(change),
2148 "Should apply all changes made"
2149 );
2150 }
2151 assert_eq!(
2152 lsp_request_count.load(Ordering::Relaxed),
2153 2,
2154 "Should query new hints twice: for editor init and for the last edit that interrupted all others"
2155 );
2156 let expected_hints = vec!["2".to_string()];
2157 assert_eq!(
2158 expected_hints,
2159 cached_hint_labels(editor),
2160 "Should get hints from the last edit landed only"
2161 );
2162 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2163 })
2164 .unwrap();
2165
2166 let mut edits = Vec::new();
2167 for async_later_change in [
2168 "another change #1",
2169 "another change #2",
2170 "another change #3",
2171 ] {
2172 expected_changes.push(async_later_change);
2173 let task_editor = editor;
2174 edits.push(cx.spawn(|mut cx| async move {
2175 task_editor
2176 .update(&mut cx, |editor, window, cx| {
2177 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
2178 editor.handle_input(async_later_change, window, cx);
2179 })
2180 .unwrap();
2181 }));
2182 }
2183 let _ = future::join_all(edits).await;
2184 cx.executor().run_until_parked();
2185
2186 editor
2187 .update(cx, |editor, _, cx| {
2188 let current_text = editor.text(cx);
2189 for change in &expected_changes {
2190 assert!(
2191 current_text.contains(change),
2192 "Should apply all changes made"
2193 );
2194 }
2195 assert_eq!(
2196 lsp_request_count.load(Ordering::SeqCst),
2197 3,
2198 "Should query new hints one more time, for the last edit only"
2199 );
2200 let expected_hints = vec!["3".to_string()];
2201 assert_eq!(
2202 expected_hints,
2203 cached_hint_labels(editor),
2204 "Should get hints from the last edit landed only"
2205 );
2206 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2207 })
2208 .unwrap();
2209 }
2210
2211 #[gpui::test(iterations = 10)]
2212 async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
2213 init_test(cx, |settings| {
2214 settings.defaults.inlay_hints = Some(InlayHintSettings {
2215 enabled: true,
2216 edit_debounce_ms: 0,
2217 scroll_debounce_ms: 0,
2218 show_type_hints: true,
2219 show_parameter_hints: true,
2220 show_other_hints: true,
2221 show_background: false,
2222 toggle_on_modifiers_press: None,
2223 })
2224 });
2225
2226 let fs = FakeFs::new(cx.background_executor.clone());
2227 fs.insert_tree(
2228 path!("/a"),
2229 json!({
2230 "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
2231 "other.rs": "// Test file",
2232 }),
2233 )
2234 .await;
2235
2236 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2237
2238 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2239 language_registry.add(rust_lang());
2240
2241 let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
2242 let lsp_request_count = Arc::new(AtomicUsize::new(0));
2243 let mut fake_servers = language_registry.register_fake_lsp(
2244 "Rust",
2245 FakeLspAdapter {
2246 capabilities: lsp::ServerCapabilities {
2247 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2248 ..Default::default()
2249 },
2250 initializer: Some(Box::new({
2251 let lsp_request_ranges = lsp_request_ranges.clone();
2252 let lsp_request_count = lsp_request_count.clone();
2253 move |fake_server| {
2254 let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
2255 let closure_lsp_request_count = Arc::clone(&lsp_request_count);
2256 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2257 move |params, _| {
2258 let task_lsp_request_ranges =
2259 Arc::clone(&closure_lsp_request_ranges);
2260 let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
2261 async move {
2262 assert_eq!(
2263 params.text_document.uri,
2264 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
2265 );
2266
2267 task_lsp_request_ranges.lock().push(params.range);
2268 task_lsp_request_count.fetch_add(1, Ordering::Release);
2269 Ok(Some(vec![lsp::InlayHint {
2270 position: params.range.end,
2271 label: lsp::InlayHintLabel::String(
2272 params.range.end.line.to_string(),
2273 ),
2274 kind: None,
2275 text_edits: None,
2276 tooltip: None,
2277 padding_left: None,
2278 padding_right: None,
2279 data: None,
2280 }]))
2281 }
2282 },
2283 );
2284 }
2285 })),
2286 ..Default::default()
2287 },
2288 );
2289
2290 let buffer = project
2291 .update(cx, |project, cx| {
2292 project.open_local_buffer(path!("/a/main.rs"), cx)
2293 })
2294 .await
2295 .unwrap();
2296 let editor =
2297 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
2298
2299 cx.executor().run_until_parked();
2300
2301 let _fake_server = fake_servers.next().await.unwrap();
2302
2303 // in large buffers, requests are made for more than visible range of a buffer.
2304 // invisible parts are queried later, to avoid excessive requests on quick typing.
2305 // wait the timeout needed to get all requests.
2306 cx.executor().advance_clock(Duration::from_millis(
2307 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2308 ));
2309 cx.executor().run_until_parked();
2310 let initial_visible_range = editor_visible_range(&editor, cx);
2311 let lsp_initial_visible_range = lsp::Range::new(
2312 lsp::Position::new(
2313 initial_visible_range.start.row,
2314 initial_visible_range.start.column,
2315 ),
2316 lsp::Position::new(
2317 initial_visible_range.end.row,
2318 initial_visible_range.end.column,
2319 ),
2320 );
2321 let expected_initial_query_range_end =
2322 lsp::Position::new(initial_visible_range.end.row * 2, 2);
2323 let mut expected_invisible_query_start = lsp_initial_visible_range.end;
2324 expected_invisible_query_start.character += 1;
2325 editor.update(cx, |editor, _window, cx| {
2326 let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2327 assert_eq!(ranges.len(), 2,
2328 "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:?}");
2329 let visible_query_range = &ranges[0];
2330 assert_eq!(visible_query_range.start, lsp_initial_visible_range.start);
2331 assert_eq!(visible_query_range.end, lsp_initial_visible_range.end);
2332 let invisible_query_range = &ranges[1];
2333
2334 assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document");
2335 assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document");
2336
2337 let requests_count = lsp_request_count.load(Ordering::Acquire);
2338 assert_eq!(requests_count, 2, "Visible + invisible request");
2339 let expected_hints = vec!["47".to_string(), "94".to_string()];
2340 assert_eq!(
2341 expected_hints,
2342 cached_hint_labels(editor),
2343 "Should have hints from both LSP requests made for a big file"
2344 );
2345 assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
2346 }).unwrap();
2347
2348 editor
2349 .update(cx, |editor, window, cx| {
2350 editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2351 })
2352 .unwrap();
2353 cx.executor().run_until_parked();
2354 editor
2355 .update(cx, |editor, window, cx| {
2356 editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
2357 })
2358 .unwrap();
2359 cx.executor().advance_clock(Duration::from_millis(
2360 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2361 ));
2362 cx.executor().run_until_parked();
2363 let visible_range_after_scrolls = editor_visible_range(&editor, cx);
2364 let visible_line_count = editor
2365 .update(cx, |editor, _window, _| {
2366 editor.visible_line_count().unwrap()
2367 })
2368 .unwrap();
2369 let selection_in_cached_range = editor
2370 .update(cx, |editor, _window, cx| {
2371 let ranges = lsp_request_ranges
2372 .lock()
2373 .drain(..)
2374 .sorted_by_key(|r| r.start)
2375 .collect::<Vec<_>>();
2376 assert_eq!(
2377 ranges.len(),
2378 2,
2379 "Should query 2 ranges after both scrolls, but got: {ranges:?}"
2380 );
2381 let first_scroll = &ranges[0];
2382 let second_scroll = &ranges[1];
2383 assert_eq!(
2384 first_scroll.end, second_scroll.start,
2385 "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
2386 );
2387 assert_eq!(
2388 first_scroll.start, expected_initial_query_range_end,
2389 "First scroll should start the query right after the end of the original scroll",
2390 );
2391 assert_eq!(
2392 second_scroll.end,
2393 lsp::Position::new(
2394 visible_range_after_scrolls.end.row
2395 + visible_line_count.ceil() as u32,
2396 1,
2397 ),
2398 "Second scroll should query one more screen down after the end of the visible range"
2399 );
2400
2401 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2402 assert_eq!(lsp_requests, 4, "Should query for hints after every scroll");
2403 let expected_hints = vec![
2404 "47".to_string(),
2405 "94".to_string(),
2406 "139".to_string(),
2407 "184".to_string(),
2408 ];
2409 assert_eq!(
2410 expected_hints,
2411 cached_hint_labels(editor),
2412 "Should have hints from the new LSP response after the edit"
2413 );
2414 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2415
2416 let mut selection_in_cached_range = visible_range_after_scrolls.end;
2417 selection_in_cached_range.row -= visible_line_count.ceil() as u32;
2418 selection_in_cached_range
2419 })
2420 .unwrap();
2421
2422 editor
2423 .update(cx, |editor, window, cx| {
2424 editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
2425 s.select_ranges([selection_in_cached_range..selection_in_cached_range])
2426 });
2427 })
2428 .unwrap();
2429 cx.executor().advance_clock(Duration::from_millis(
2430 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2431 ));
2432 cx.executor().run_until_parked();
2433 editor.update(cx, |_, _, _| {
2434 let ranges = lsp_request_ranges
2435 .lock()
2436 .drain(..)
2437 .sorted_by_key(|r| r.start)
2438 .collect::<Vec<_>>();
2439 assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
2440 assert_eq!(lsp_request_count.load(Ordering::Acquire), 4);
2441 }).unwrap();
2442
2443 editor
2444 .update(cx, |editor, window, cx| {
2445 editor.handle_input("++++more text++++", window, cx);
2446 })
2447 .unwrap();
2448 cx.executor().advance_clock(Duration::from_millis(
2449 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2450 ));
2451 cx.executor().run_until_parked();
2452 editor.update(cx, |editor, _window, cx| {
2453 let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
2454 ranges.sort_by_key(|r| r.start);
2455
2456 assert_eq!(ranges.len(), 3,
2457 "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
2458 let above_query_range = &ranges[0];
2459 let visible_query_range = &ranges[1];
2460 let below_query_range = &ranges[2];
2461 assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
2462 "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");
2463 assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line,
2464 "Visible range {visible_query_range:?} should be before below range {below_query_range:?}");
2465 assert!(above_query_range.start.line < selection_in_cached_range.row,
2466 "Hints should be queried with the selected range after the query range start");
2467 assert!(below_query_range.end.line > selection_in_cached_range.row,
2468 "Hints should be queried with the selected range before the query range end");
2469 assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32,
2470 "Hints query range should contain one more screen before");
2471 assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32,
2472 "Hints query range should contain one more screen after");
2473
2474 let lsp_requests = lsp_request_count.load(Ordering::Acquire);
2475 assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried");
2476 let expected_hints = vec!["67".to_string(), "115".to_string(), "163".to_string()];
2477 assert_eq!(expected_hints, cached_hint_labels(editor),
2478 "Should have hints from the new LSP response after the edit");
2479 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2480 }).unwrap();
2481 }
2482
2483 fn editor_visible_range(
2484 editor: &WindowHandle<Editor>,
2485 cx: &mut gpui::TestAppContext,
2486 ) -> Range<Point> {
2487 let ranges = editor
2488 .update(cx, |editor, _window, cx| {
2489 editor.excerpts_for_inlay_hints_query(None, cx)
2490 })
2491 .unwrap();
2492 assert_eq!(
2493 ranges.len(),
2494 1,
2495 "Single buffer should produce a single excerpt with visible range"
2496 );
2497 let (_, (excerpt_buffer, _, excerpt_visible_range)) = ranges.into_iter().next().unwrap();
2498 excerpt_buffer.update(cx, |buffer, _| {
2499 let snapshot = buffer.snapshot();
2500 let start = buffer
2501 .anchor_before(excerpt_visible_range.start)
2502 .to_point(&snapshot);
2503 let end = buffer
2504 .anchor_after(excerpt_visible_range.end)
2505 .to_point(&snapshot);
2506 start..end
2507 })
2508 }
2509
2510 #[gpui::test]
2511 async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
2512 init_test(cx, |settings| {
2513 settings.defaults.inlay_hints = Some(InlayHintSettings {
2514 enabled: true,
2515 edit_debounce_ms: 0,
2516 scroll_debounce_ms: 0,
2517 show_type_hints: true,
2518 show_parameter_hints: true,
2519 show_other_hints: true,
2520 show_background: false,
2521 toggle_on_modifiers_press: None,
2522 })
2523 });
2524
2525 let fs = FakeFs::new(cx.background_executor.clone());
2526 fs.insert_tree(
2527 path!("/a"),
2528 json!({
2529 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2530 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2531 }),
2532 )
2533 .await;
2534
2535 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2536
2537 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2538 let language = rust_lang();
2539 language_registry.add(language);
2540 let mut fake_servers = language_registry.register_fake_lsp(
2541 "Rust",
2542 FakeLspAdapter {
2543 capabilities: lsp::ServerCapabilities {
2544 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2545 ..Default::default()
2546 },
2547 ..Default::default()
2548 },
2549 );
2550
2551 let (buffer_1, _handle1) = project
2552 .update(cx, |project, cx| {
2553 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2554 })
2555 .await
2556 .unwrap();
2557 let (buffer_2, _handle2) = project
2558 .update(cx, |project, cx| {
2559 project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2560 })
2561 .await
2562 .unwrap();
2563 let multibuffer = cx.new(|cx| {
2564 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2565 multibuffer.push_excerpts(
2566 buffer_1.clone(),
2567 [
2568 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
2569 ExcerptRange::new(Point::new(4, 0)..Point::new(11, 0)),
2570 ExcerptRange::new(Point::new(22, 0)..Point::new(33, 0)),
2571 ExcerptRange::new(Point::new(44, 0)..Point::new(55, 0)),
2572 ExcerptRange::new(Point::new(56, 0)..Point::new(66, 0)),
2573 ExcerptRange::new(Point::new(67, 0)..Point::new(77, 0)),
2574 ],
2575 cx,
2576 );
2577 multibuffer.push_excerpts(
2578 buffer_2.clone(),
2579 [
2580 ExcerptRange::new(Point::new(0, 1)..Point::new(2, 1)),
2581 ExcerptRange::new(Point::new(4, 1)..Point::new(11, 1)),
2582 ExcerptRange::new(Point::new(22, 1)..Point::new(33, 1)),
2583 ExcerptRange::new(Point::new(44, 1)..Point::new(55, 1)),
2584 ExcerptRange::new(Point::new(56, 1)..Point::new(66, 1)),
2585 ExcerptRange::new(Point::new(67, 1)..Point::new(77, 1)),
2586 ],
2587 cx,
2588 );
2589 multibuffer
2590 });
2591
2592 cx.executor().run_until_parked();
2593 let editor = cx.add_window(|window, cx| {
2594 Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2595 });
2596
2597 let editor_edited = Arc::new(AtomicBool::new(false));
2598 let fake_server = fake_servers.next().await.unwrap();
2599 let closure_editor_edited = Arc::clone(&editor_edited);
2600 fake_server
2601 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2602 let task_editor_edited = Arc::clone(&closure_editor_edited);
2603 async move {
2604 let hint_text = if params.text_document.uri
2605 == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
2606 {
2607 "main hint"
2608 } else if params.text_document.uri
2609 == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
2610 {
2611 "other hint"
2612 } else {
2613 panic!("unexpected uri: {:?}", params.text_document.uri);
2614 };
2615
2616 // one hint per excerpt
2617 let positions = [
2618 lsp::Position::new(0, 2),
2619 lsp::Position::new(4, 2),
2620 lsp::Position::new(22, 2),
2621 lsp::Position::new(44, 2),
2622 lsp::Position::new(56, 2),
2623 lsp::Position::new(67, 2),
2624 ];
2625 let out_of_range_hint = lsp::InlayHint {
2626 position: lsp::Position::new(
2627 params.range.start.line + 99,
2628 params.range.start.character + 99,
2629 ),
2630 label: lsp::InlayHintLabel::String(
2631 "out of excerpt range, should be ignored".to_string(),
2632 ),
2633 kind: None,
2634 text_edits: None,
2635 tooltip: None,
2636 padding_left: None,
2637 padding_right: None,
2638 data: None,
2639 };
2640
2641 let edited = task_editor_edited.load(Ordering::Acquire);
2642 Ok(Some(
2643 std::iter::once(out_of_range_hint)
2644 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2645 lsp::InlayHint {
2646 position,
2647 label: lsp::InlayHintLabel::String(format!(
2648 "{hint_text}{E} #{i}",
2649 E = if edited { "(edited)" } else { "" },
2650 )),
2651 kind: None,
2652 text_edits: None,
2653 tooltip: None,
2654 padding_left: None,
2655 padding_right: None,
2656 data: None,
2657 }
2658 }))
2659 .collect(),
2660 ))
2661 }
2662 })
2663 .next()
2664 .await;
2665 cx.executor().run_until_parked();
2666
2667 editor
2668 .update(cx, |editor, _window, cx| {
2669 let expected_hints = vec![
2670 "main hint #0".to_string(),
2671 "main hint #1".to_string(),
2672 "main hint #2".to_string(),
2673 "main hint #3".to_string(),
2674 "main hint #4".to_string(),
2675 "main hint #5".to_string(),
2676 ];
2677 assert_eq!(
2678 expected_hints,
2679 sorted_cached_hint_labels(editor),
2680 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2681 );
2682 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2683 })
2684 .unwrap();
2685
2686 editor
2687 .update(cx, |editor, window, cx| {
2688 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2689 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2690 });
2691 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2692 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2693 });
2694 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2695 s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
2696 });
2697 })
2698 .unwrap();
2699 cx.executor().run_until_parked();
2700 editor
2701 .update(cx, |editor, _window, cx| {
2702 let expected_hints = vec![
2703 "main hint #0".to_string(),
2704 "main hint #1".to_string(),
2705 "main hint #2".to_string(),
2706 "main hint #3".to_string(),
2707 "main hint #4".to_string(),
2708 "main hint #5".to_string(),
2709 "other hint #0".to_string(),
2710 "other hint #1".to_string(),
2711 "other hint #2".to_string(),
2712 ];
2713 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2714 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2715 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2716 })
2717 .unwrap();
2718
2719 editor
2720 .update(cx, |editor, window, cx| {
2721 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2722 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2723 });
2724 })
2725 .unwrap();
2726 cx.executor().advance_clock(Duration::from_millis(
2727 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2728 ));
2729 cx.executor().run_until_parked();
2730 editor
2731 .update(cx, |editor, _window, cx| {
2732 let expected_hints = vec![
2733 "main hint #0".to_string(),
2734 "main hint #1".to_string(),
2735 "main hint #2".to_string(),
2736 "main hint #3".to_string(),
2737 "main hint #4".to_string(),
2738 "main hint #5".to_string(),
2739 "other hint #0".to_string(),
2740 "other hint #1".to_string(),
2741 "other hint #2".to_string(),
2742 "other hint #3".to_string(),
2743 "other hint #4".to_string(),
2744 "other hint #5".to_string(),
2745 ];
2746 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2747 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2748 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2749 })
2750 .unwrap();
2751
2752 editor
2753 .update(cx, |editor, window, cx| {
2754 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2755 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2756 });
2757 })
2758 .unwrap();
2759 cx.executor().advance_clock(Duration::from_millis(
2760 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2761 ));
2762 cx.executor().run_until_parked();
2763 editor
2764 .update(cx, |editor, _window, cx| {
2765 let expected_hints = vec![
2766 "main hint #0".to_string(),
2767 "main hint #1".to_string(),
2768 "main hint #2".to_string(),
2769 "main hint #3".to_string(),
2770 "main hint #4".to_string(),
2771 "main hint #5".to_string(),
2772 "other hint #0".to_string(),
2773 "other hint #1".to_string(),
2774 "other hint #2".to_string(),
2775 "other hint #3".to_string(),
2776 "other hint #4".to_string(),
2777 "other hint #5".to_string(),
2778 ];
2779 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2780 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2781 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2782 })
2783 .unwrap();
2784
2785 editor_edited.store(true, Ordering::Release);
2786 editor
2787 .update(cx, |editor, window, cx| {
2788 editor.change_selections(None, window, cx, |s| {
2789 s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
2790 });
2791 editor.handle_input("++++more text++++", window, cx);
2792 })
2793 .unwrap();
2794 cx.executor().run_until_parked();
2795 editor
2796 .update(cx, |editor, _window, cx| {
2797 let expected_hints = vec![
2798 "main hint #0".to_string(),
2799 "main hint #1".to_string(),
2800 "main hint #2".to_string(),
2801 "main hint #3".to_string(),
2802 "main hint #4".to_string(),
2803 "main hint #5".to_string(),
2804 "other hint(edited) #0".to_string(),
2805 "other hint(edited) #1".to_string(),
2806 ];
2807 assert_eq!(
2808 expected_hints,
2809 sorted_cached_hint_labels(editor),
2810 "After multibuffer edit, editor gets scrolled back to the last selection; \
2811 all hints should be invalidated and required for all of its visible excerpts"
2812 );
2813 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2814 })
2815 .unwrap();
2816 }
2817
2818 #[gpui::test]
2819 async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
2820 init_test(cx, |settings| {
2821 settings.defaults.inlay_hints = Some(InlayHintSettings {
2822 enabled: true,
2823 edit_debounce_ms: 0,
2824 scroll_debounce_ms: 0,
2825 show_type_hints: false,
2826 show_parameter_hints: false,
2827 show_other_hints: false,
2828 show_background: false,
2829 toggle_on_modifiers_press: None,
2830 })
2831 });
2832
2833 let fs = FakeFs::new(cx.background_executor.clone());
2834 fs.insert_tree(
2835 path!("/a"),
2836 json!({
2837 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2838 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2839 }),
2840 )
2841 .await;
2842
2843 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2844
2845 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2846 language_registry.add(rust_lang());
2847 let mut fake_servers = language_registry.register_fake_lsp(
2848 "Rust",
2849 FakeLspAdapter {
2850 capabilities: lsp::ServerCapabilities {
2851 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2852 ..Default::default()
2853 },
2854 ..Default::default()
2855 },
2856 );
2857
2858 let (buffer_1, _handle) = project
2859 .update(cx, |project, cx| {
2860 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2861 })
2862 .await
2863 .unwrap();
2864 let (buffer_2, _handle2) = project
2865 .update(cx, |project, cx| {
2866 project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2867 })
2868 .await
2869 .unwrap();
2870 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2871 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2872 let buffer_1_excerpts = multibuffer.push_excerpts(
2873 buffer_1.clone(),
2874 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
2875 cx,
2876 );
2877 let buffer_2_excerpts = multibuffer.push_excerpts(
2878 buffer_2.clone(),
2879 [ExcerptRange::new(Point::new(0, 1)..Point::new(2, 1))],
2880 cx,
2881 );
2882 (buffer_1_excerpts, buffer_2_excerpts)
2883 });
2884
2885 assert!(!buffer_1_excerpts.is_empty());
2886 assert!(!buffer_2_excerpts.is_empty());
2887
2888 cx.executor().run_until_parked();
2889 let editor = cx.add_window(|window, cx| {
2890 Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2891 });
2892 let editor_edited = Arc::new(AtomicBool::new(false));
2893 let fake_server = fake_servers.next().await.unwrap();
2894 let closure_editor_edited = Arc::clone(&editor_edited);
2895 fake_server
2896 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2897 let task_editor_edited = Arc::clone(&closure_editor_edited);
2898 async move {
2899 let hint_text = if params.text_document.uri
2900 == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
2901 {
2902 "main hint"
2903 } else if params.text_document.uri
2904 == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
2905 {
2906 "other hint"
2907 } else {
2908 panic!("unexpected uri: {:?}", params.text_document.uri);
2909 };
2910
2911 let positions = [
2912 lsp::Position::new(0, 2),
2913 lsp::Position::new(4, 2),
2914 lsp::Position::new(22, 2),
2915 lsp::Position::new(44, 2),
2916 lsp::Position::new(56, 2),
2917 lsp::Position::new(67, 2),
2918 ];
2919 let out_of_range_hint = lsp::InlayHint {
2920 position: lsp::Position::new(
2921 params.range.start.line + 99,
2922 params.range.start.character + 99,
2923 ),
2924 label: lsp::InlayHintLabel::String(
2925 "out of excerpt range, should be ignored".to_string(),
2926 ),
2927 kind: None,
2928 text_edits: None,
2929 tooltip: None,
2930 padding_left: None,
2931 padding_right: None,
2932 data: None,
2933 };
2934
2935 let edited = task_editor_edited.load(Ordering::Acquire);
2936 Ok(Some(
2937 std::iter::once(out_of_range_hint)
2938 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2939 lsp::InlayHint {
2940 position,
2941 label: lsp::InlayHintLabel::String(format!(
2942 "{hint_text}{} #{i}",
2943 if edited { "(edited)" } else { "" },
2944 )),
2945 kind: None,
2946 text_edits: None,
2947 tooltip: None,
2948 padding_left: None,
2949 padding_right: None,
2950 data: None,
2951 }
2952 }))
2953 .collect(),
2954 ))
2955 }
2956 })
2957 .next()
2958 .await;
2959 cx.executor().run_until_parked();
2960 editor
2961 .update(cx, |editor, _, cx| {
2962 assert_eq!(
2963 vec!["main hint #0".to_string(), "other hint #0".to_string()],
2964 sorted_cached_hint_labels(editor),
2965 "Cache should update for both excerpts despite hints display was disabled"
2966 );
2967 assert!(
2968 visible_hint_labels(editor, cx).is_empty(),
2969 "All hints are disabled and should not be shown despite being present in the cache"
2970 );
2971 })
2972 .unwrap();
2973
2974 editor
2975 .update(cx, |editor, _, cx| {
2976 editor.buffer().update(cx, |multibuffer, cx| {
2977 multibuffer.remove_excerpts(buffer_2_excerpts, cx)
2978 })
2979 })
2980 .unwrap();
2981 cx.executor().run_until_parked();
2982 editor
2983 .update(cx, |editor, _, cx| {
2984 assert_eq!(
2985 vec!["main hint #0".to_string()],
2986 cached_hint_labels(editor),
2987 "For the removed excerpt, should clean corresponding cached hints"
2988 );
2989 assert!(
2990 visible_hint_labels(editor, cx).is_empty(),
2991 "All hints are disabled and should not be shown despite being present in the cache"
2992 );
2993 })
2994 .unwrap();
2995
2996 update_test_language_settings(cx, |settings| {
2997 settings.defaults.inlay_hints = Some(InlayHintSettings {
2998 enabled: true,
2999 edit_debounce_ms: 0,
3000 scroll_debounce_ms: 0,
3001 show_type_hints: true,
3002 show_parameter_hints: true,
3003 show_other_hints: true,
3004 show_background: false,
3005 toggle_on_modifiers_press: None,
3006 })
3007 });
3008 cx.executor().run_until_parked();
3009 editor
3010 .update(cx, |editor, _, cx| {
3011 let expected_hints = vec!["main hint #0".to_string()];
3012 assert_eq!(
3013 expected_hints,
3014 cached_hint_labels(editor),
3015 "Hint display settings change should not change the cache"
3016 );
3017 assert_eq!(
3018 expected_hints,
3019 visible_hint_labels(editor, cx),
3020 "Settings change should make cached hints visible"
3021 );
3022 })
3023 .unwrap();
3024 }
3025
3026 #[gpui::test]
3027 async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
3028 init_test(cx, |settings| {
3029 settings.defaults.inlay_hints = Some(InlayHintSettings {
3030 enabled: true,
3031 edit_debounce_ms: 0,
3032 scroll_debounce_ms: 0,
3033 show_type_hints: true,
3034 show_parameter_hints: true,
3035 show_other_hints: true,
3036 show_background: false,
3037 toggle_on_modifiers_press: None,
3038 })
3039 });
3040
3041 let fs = FakeFs::new(cx.background_executor.clone());
3042 fs.insert_tree(
3043 path!("/a"),
3044 json!({
3045 "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)),
3046 "other.rs": "// Test file",
3047 }),
3048 )
3049 .await;
3050
3051 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3052
3053 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3054 language_registry.add(rust_lang());
3055 language_registry.register_fake_lsp(
3056 "Rust",
3057 FakeLspAdapter {
3058 capabilities: lsp::ServerCapabilities {
3059 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3060 ..Default::default()
3061 },
3062 initializer: Some(Box::new(move |fake_server| {
3063 let lsp_request_count = Arc::new(AtomicU32::new(0));
3064 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3065 move |params, _| {
3066 let i = lsp_request_count.fetch_add(1, Ordering::Release) + 1;
3067 async move {
3068 assert_eq!(
3069 params.text_document.uri,
3070 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3071 );
3072 let query_start = params.range.start;
3073 Ok(Some(vec![lsp::InlayHint {
3074 position: query_start,
3075 label: lsp::InlayHintLabel::String(i.to_string()),
3076 kind: None,
3077 text_edits: None,
3078 tooltip: None,
3079 padding_left: None,
3080 padding_right: None,
3081 data: None,
3082 }]))
3083 }
3084 },
3085 );
3086 })),
3087 ..Default::default()
3088 },
3089 );
3090
3091 let buffer = project
3092 .update(cx, |project, cx| {
3093 project.open_local_buffer(path!("/a/main.rs"), cx)
3094 })
3095 .await
3096 .unwrap();
3097 let editor =
3098 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3099
3100 cx.executor().run_until_parked();
3101 editor
3102 .update(cx, |editor, window, cx| {
3103 editor.change_selections(None, window, cx, |s| {
3104 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3105 })
3106 })
3107 .unwrap();
3108 cx.executor().run_until_parked();
3109 editor
3110 .update(cx, |editor, _, cx| {
3111 let expected_hints = vec!["1".to_string()];
3112 assert_eq!(expected_hints, cached_hint_labels(editor));
3113 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3114 })
3115 .unwrap();
3116 }
3117
3118 #[gpui::test]
3119 async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
3120 init_test(cx, |settings| {
3121 settings.defaults.inlay_hints = Some(InlayHintSettings {
3122 enabled: false,
3123 edit_debounce_ms: 0,
3124 scroll_debounce_ms: 0,
3125 show_type_hints: true,
3126 show_parameter_hints: true,
3127 show_other_hints: true,
3128 show_background: false,
3129 toggle_on_modifiers_press: None,
3130 })
3131 });
3132
3133 let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3134 let lsp_request_count = Arc::new(AtomicU32::new(0));
3135 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3136 move |params, _| {
3137 let lsp_request_count = lsp_request_count.clone();
3138 async move {
3139 assert_eq!(
3140 params.text_document.uri,
3141 lsp::Url::from_file_path(file_with_hints).unwrap(),
3142 );
3143
3144 let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
3145 Ok(Some(vec![lsp::InlayHint {
3146 position: lsp::Position::new(0, i),
3147 label: lsp::InlayHintLabel::String(i.to_string()),
3148 kind: None,
3149 text_edits: None,
3150 tooltip: None,
3151 padding_left: None,
3152 padding_right: None,
3153 data: None,
3154 }]))
3155 }
3156 },
3157 );
3158 })
3159 .await;
3160
3161 editor
3162 .update(cx, |editor, window, cx| {
3163 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3164 })
3165 .unwrap();
3166
3167 cx.executor().run_until_parked();
3168 editor
3169 .update(cx, |editor, _, cx| {
3170 let expected_hints = vec!["1".to_string()];
3171 assert_eq!(
3172 expected_hints,
3173 cached_hint_labels(editor),
3174 "Should display inlays after toggle despite them disabled in settings"
3175 );
3176 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3177 })
3178 .unwrap();
3179
3180 editor
3181 .update(cx, |editor, window, cx| {
3182 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3183 })
3184 .unwrap();
3185 cx.executor().run_until_parked();
3186 editor
3187 .update(cx, |editor, _, cx| {
3188 assert!(
3189 cached_hint_labels(editor).is_empty(),
3190 "Should clear hints after 2nd toggle"
3191 );
3192 assert!(visible_hint_labels(editor, cx).is_empty());
3193 })
3194 .unwrap();
3195
3196 update_test_language_settings(cx, |settings| {
3197 settings.defaults.inlay_hints = Some(InlayHintSettings {
3198 enabled: true,
3199 edit_debounce_ms: 0,
3200 scroll_debounce_ms: 0,
3201 show_type_hints: true,
3202 show_parameter_hints: true,
3203 show_other_hints: true,
3204 show_background: false,
3205 toggle_on_modifiers_press: None,
3206 })
3207 });
3208 cx.executor().run_until_parked();
3209 editor
3210 .update(cx, |editor, _, cx| {
3211 let expected_hints = vec!["2".to_string()];
3212 assert_eq!(
3213 expected_hints,
3214 cached_hint_labels(editor),
3215 "Should query LSP hints for the 2nd time after enabling hints in settings"
3216 );
3217 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3218 })
3219 .unwrap();
3220
3221 editor
3222 .update(cx, |editor, window, cx| {
3223 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3224 })
3225 .unwrap();
3226 cx.executor().run_until_parked();
3227 editor
3228 .update(cx, |editor, _, cx| {
3229 assert!(
3230 cached_hint_labels(editor).is_empty(),
3231 "Should clear hints after enabling in settings and a 3rd toggle"
3232 );
3233 assert!(visible_hint_labels(editor, cx).is_empty());
3234 })
3235 .unwrap();
3236
3237 editor
3238 .update(cx, |editor, window, cx| {
3239 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3240 })
3241 .unwrap();
3242 cx.executor().run_until_parked();
3243 editor.update(cx, |editor, _, cx| {
3244 let expected_hints = vec!["3".to_string()];
3245 assert_eq!(
3246 expected_hints,
3247 cached_hint_labels(editor),
3248 "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
3249 );
3250 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3251 }).unwrap();
3252 }
3253
3254 #[gpui::test]
3255 async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) {
3256 init_test(cx, |settings| {
3257 settings.defaults.inlay_hints = Some(InlayHintSettings {
3258 enabled: true,
3259 edit_debounce_ms: 0,
3260 scroll_debounce_ms: 0,
3261 show_type_hints: true,
3262 show_parameter_hints: true,
3263 show_other_hints: true,
3264 show_background: false,
3265 toggle_on_modifiers_press: None,
3266 })
3267 });
3268
3269 let fs = FakeFs::new(cx.background_executor.clone());
3270 fs.insert_tree(
3271 path!("/a"),
3272 json!({
3273 "main.rs": "fn main() {
3274 let x = 42;
3275 std::thread::scope(|s| {
3276 s.spawn(|| {
3277 let _x = x;
3278 });
3279 });
3280 }",
3281 "other.rs": "// Test file",
3282 }),
3283 )
3284 .await;
3285
3286 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3287
3288 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3289 language_registry.add(rust_lang());
3290 language_registry.register_fake_lsp(
3291 "Rust",
3292 FakeLspAdapter {
3293 capabilities: lsp::ServerCapabilities {
3294 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3295 ..Default::default()
3296 },
3297 initializer: Some(Box::new(move |fake_server| {
3298 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3299 move |params, _| async move {
3300 assert_eq!(
3301 params.text_document.uri,
3302 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3303 );
3304 Ok(Some(
3305 serde_json::from_value(json!([
3306 {
3307 "position": {
3308 "line": 3,
3309 "character": 16
3310 },
3311 "label": "move",
3312 "paddingLeft": false,
3313 "paddingRight": false
3314 },
3315 {
3316 "position": {
3317 "line": 3,
3318 "character": 16
3319 },
3320 "label": "(",
3321 "paddingLeft": false,
3322 "paddingRight": false
3323 },
3324 {
3325 "position": {
3326 "line": 3,
3327 "character": 16
3328 },
3329 "label": [
3330 {
3331 "value": "&x"
3332 }
3333 ],
3334 "paddingLeft": false,
3335 "paddingRight": false,
3336 "data": {
3337 "file_id": 0
3338 }
3339 },
3340 {
3341 "position": {
3342 "line": 3,
3343 "character": 16
3344 },
3345 "label": ")",
3346 "paddingLeft": false,
3347 "paddingRight": true
3348 },
3349 // not a correct syntax, but checks that same symbols at the same place
3350 // are not deduplicated
3351 {
3352 "position": {
3353 "line": 3,
3354 "character": 16
3355 },
3356 "label": ")",
3357 "paddingLeft": false,
3358 "paddingRight": true
3359 },
3360 ]))
3361 .unwrap(),
3362 ))
3363 },
3364 );
3365 })),
3366 ..FakeLspAdapter::default()
3367 },
3368 );
3369
3370 let buffer = project
3371 .update(cx, |project, cx| {
3372 project.open_local_buffer(path!("/a/main.rs"), cx)
3373 })
3374 .await
3375 .unwrap();
3376 let editor =
3377 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3378
3379 cx.executor().run_until_parked();
3380 editor
3381 .update(cx, |editor, window, cx| {
3382 editor.change_selections(None, window, cx, |s| {
3383 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3384 })
3385 })
3386 .unwrap();
3387 cx.executor().run_until_parked();
3388 editor
3389 .update(cx, |editor, _window, cx| {
3390 let expected_hints = vec![
3391 "move".to_string(),
3392 "(".to_string(),
3393 "&x".to_string(),
3394 ") ".to_string(),
3395 ") ".to_string(),
3396 ];
3397 assert_eq!(
3398 expected_hints,
3399 cached_hint_labels(editor),
3400 "Editor inlay hints should repeat server's order when placed at the same spot"
3401 );
3402 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3403 })
3404 .unwrap();
3405 }
3406
3407 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
3408 cx.update(|cx| {
3409 let settings_store = SettingsStore::test(cx);
3410 cx.set_global(settings_store);
3411 theme::init(theme::LoadThemes::JustBase, cx);
3412 release_channel::init(SemanticVersion::default(), cx);
3413 client::init_settings(cx);
3414 language::init(cx);
3415 Project::init_settings(cx);
3416 workspace::init_settings(cx);
3417 crate::init(cx);
3418 });
3419
3420 update_test_language_settings(cx, f);
3421 }
3422
3423 async fn prepare_test_objects(
3424 cx: &mut TestAppContext,
3425 initialize: impl 'static + Send + Fn(&mut FakeLanguageServer, &'static str) + Send + Sync,
3426 ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
3427 let fs = FakeFs::new(cx.background_executor.clone());
3428 fs.insert_tree(
3429 path!("/a"),
3430 json!({
3431 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
3432 "other.rs": "// Test file",
3433 }),
3434 )
3435 .await;
3436
3437 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3438 let file_path = path!("/a/main.rs");
3439
3440 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3441 language_registry.add(rust_lang());
3442 let mut fake_servers = language_registry.register_fake_lsp(
3443 "Rust",
3444 FakeLspAdapter {
3445 capabilities: lsp::ServerCapabilities {
3446 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3447 ..Default::default()
3448 },
3449 initializer: Some(Box::new(move |server| initialize(server, file_path))),
3450 ..Default::default()
3451 },
3452 );
3453
3454 let buffer = project
3455 .update(cx, |project, cx| {
3456 project.open_local_buffer(path!("/a/main.rs"), cx)
3457 })
3458 .await
3459 .unwrap();
3460 let editor =
3461 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3462
3463 editor
3464 .update(cx, |editor, _, cx| {
3465 assert!(cached_hint_labels(editor).is_empty());
3466 assert!(visible_hint_labels(editor, cx).is_empty());
3467 })
3468 .unwrap();
3469
3470 cx.executor().run_until_parked();
3471 let fake_server = fake_servers.next().await.unwrap();
3472 (file_path, editor, fake_server)
3473 }
3474
3475 // 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.
3476 // Ensure a stable order for testing.
3477 fn sorted_cached_hint_labels(editor: &Editor) -> Vec<String> {
3478 let mut labels = cached_hint_labels(editor);
3479 labels.sort();
3480 labels
3481 }
3482
3483 pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
3484 let mut labels = Vec::new();
3485 for excerpt_hints in editor.inlay_hint_cache().hints.values() {
3486 let excerpt_hints = excerpt_hints.read();
3487 for id in &excerpt_hints.ordered_hints {
3488 let hint = &excerpt_hints.hints_by_id[id];
3489 let mut label = hint.text();
3490 if hint.padding_left {
3491 label.insert(0, ' ');
3492 }
3493 if hint.padding_right {
3494 label.push_str(" ");
3495 }
3496 labels.push(label);
3497 }
3498 }
3499
3500 labels
3501 }
3502
3503 pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
3504 editor
3505 .visible_inlay_hints(cx)
3506 .into_iter()
3507 .map(|hint| hint.text.to_string())
3508 .collect()
3509 }
3510}