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