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