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