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