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 .excerpts_for_inlay_hints_query(None, cx)
960 .remove(&query.excerpt_id)
961 {
962 Some((_, _, current_visible_range)) => {
963 let visible_offset_length = current_visible_range.len();
964 let double_visible_range = current_visible_range
965 .start
966 .saturating_sub(visible_offset_length)
967 ..current_visible_range
968 .end
969 .saturating_add(visible_offset_length)
970 .min(buffer_snapshot.len());
971 !double_visible_range
972 .contains(&fetch_range.start.to_offset(&buffer_snapshot))
973 && !double_visible_range
974 .contains(&fetch_range.end.to_offset(&buffer_snapshot))
975 }
976 None => true,
977 };
978 if query_not_around_visible_range {
979 log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
980 if let Some(task_ranges) = editor
981 .inlay_hint_cache
982 .update_tasks
983 .get_mut(&query.excerpt_id)
984 {
985 task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
986 }
987 return None;
988 }
989 }
990
991 let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?;
992
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| {
2515 editor.excerpts_for_inlay_hints_query(None, cx)
2516 })
2517 .unwrap();
2518 assert_eq!(
2519 ranges.len(),
2520 1,
2521 "Single buffer should produce a single excerpt with visible range"
2522 );
2523 let (_, (excerpt_buffer, _, excerpt_visible_range)) = ranges.into_iter().next().unwrap();
2524 excerpt_buffer.read_with(cx, |buffer, _| {
2525 let snapshot = buffer.snapshot();
2526 let start = buffer
2527 .anchor_before(excerpt_visible_range.start)
2528 .to_point(&snapshot);
2529 let end = buffer
2530 .anchor_after(excerpt_visible_range.end)
2531 .to_point(&snapshot);
2532 start..end
2533 })
2534 }
2535
2536 #[gpui::test]
2537 async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
2538 init_test(cx, |settings| {
2539 settings.defaults.inlay_hints = Some(InlayHintSettings {
2540 show_value_hints: true,
2541 enabled: true,
2542 edit_debounce_ms: 0,
2543 scroll_debounce_ms: 0,
2544 show_type_hints: true,
2545 show_parameter_hints: true,
2546 show_other_hints: true,
2547 show_background: false,
2548 toggle_on_modifiers_press: None,
2549 })
2550 });
2551
2552 let fs = FakeFs::new(cx.background_executor.clone());
2553 fs.insert_tree(
2554 path!("/a"),
2555 json!({
2556 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2557 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2558 }),
2559 )
2560 .await;
2561
2562 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2563
2564 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2565 let language = rust_lang();
2566 language_registry.add(language);
2567 let mut fake_servers = language_registry.register_fake_lsp(
2568 "Rust",
2569 FakeLspAdapter {
2570 capabilities: lsp::ServerCapabilities {
2571 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2572 ..Default::default()
2573 },
2574 ..Default::default()
2575 },
2576 );
2577
2578 let (buffer_1, _handle1) = project
2579 .update(cx, |project, cx| {
2580 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2581 })
2582 .await
2583 .unwrap();
2584 let (buffer_2, _handle2) = project
2585 .update(cx, |project, cx| {
2586 project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2587 })
2588 .await
2589 .unwrap();
2590 let multibuffer = cx.new(|cx| {
2591 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2592 multibuffer.push_excerpts(
2593 buffer_1.clone(),
2594 [
2595 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
2596 ExcerptRange::new(Point::new(4, 0)..Point::new(11, 0)),
2597 ExcerptRange::new(Point::new(22, 0)..Point::new(33, 0)),
2598 ExcerptRange::new(Point::new(44, 0)..Point::new(55, 0)),
2599 ExcerptRange::new(Point::new(56, 0)..Point::new(66, 0)),
2600 ExcerptRange::new(Point::new(67, 0)..Point::new(77, 0)),
2601 ],
2602 cx,
2603 );
2604 multibuffer.push_excerpts(
2605 buffer_2.clone(),
2606 [
2607 ExcerptRange::new(Point::new(0, 1)..Point::new(2, 1)),
2608 ExcerptRange::new(Point::new(4, 1)..Point::new(11, 1)),
2609 ExcerptRange::new(Point::new(22, 1)..Point::new(33, 1)),
2610 ExcerptRange::new(Point::new(44, 1)..Point::new(55, 1)),
2611 ExcerptRange::new(Point::new(56, 1)..Point::new(66, 1)),
2612 ExcerptRange::new(Point::new(67, 1)..Point::new(77, 1)),
2613 ],
2614 cx,
2615 );
2616 multibuffer
2617 });
2618
2619 cx.executor().run_until_parked();
2620 let editor = cx.add_window(|window, cx| {
2621 Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2622 });
2623
2624 let editor_edited = Arc::new(AtomicBool::new(false));
2625 let fake_server = fake_servers.next().await.unwrap();
2626 let closure_editor_edited = Arc::clone(&editor_edited);
2627 fake_server
2628 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2629 let task_editor_edited = Arc::clone(&closure_editor_edited);
2630 async move {
2631 let hint_text = if params.text_document.uri
2632 == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
2633 {
2634 "main hint"
2635 } else if params.text_document.uri
2636 == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
2637 {
2638 "other hint"
2639 } else {
2640 panic!("unexpected uri: {:?}", params.text_document.uri);
2641 };
2642
2643 // one hint per excerpt
2644 let positions = [
2645 lsp::Position::new(0, 2),
2646 lsp::Position::new(4, 2),
2647 lsp::Position::new(22, 2),
2648 lsp::Position::new(44, 2),
2649 lsp::Position::new(56, 2),
2650 lsp::Position::new(67, 2),
2651 ];
2652 let out_of_range_hint = lsp::InlayHint {
2653 position: lsp::Position::new(
2654 params.range.start.line + 99,
2655 params.range.start.character + 99,
2656 ),
2657 label: lsp::InlayHintLabel::String(
2658 "out of excerpt range, should be ignored".to_string(),
2659 ),
2660 kind: None,
2661 text_edits: None,
2662 tooltip: None,
2663 padding_left: None,
2664 padding_right: None,
2665 data: None,
2666 };
2667
2668 let edited = task_editor_edited.load(Ordering::Acquire);
2669 Ok(Some(
2670 std::iter::once(out_of_range_hint)
2671 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2672 lsp::InlayHint {
2673 position,
2674 label: lsp::InlayHintLabel::String(format!(
2675 "{hint_text}{E} #{i}",
2676 E = if edited { "(edited)" } else { "" },
2677 )),
2678 kind: None,
2679 text_edits: None,
2680 tooltip: None,
2681 padding_left: None,
2682 padding_right: None,
2683 data: None,
2684 }
2685 }))
2686 .collect(),
2687 ))
2688 }
2689 })
2690 .next()
2691 .await;
2692 cx.executor().run_until_parked();
2693
2694 editor
2695 .update(cx, |editor, _window, cx| {
2696 let expected_hints = vec![
2697 "main hint #0".to_string(),
2698 "main hint #1".to_string(),
2699 "main hint #2".to_string(),
2700 "main hint #3".to_string(),
2701 "main hint #4".to_string(),
2702 "main hint #5".to_string(),
2703 ];
2704 assert_eq!(
2705 expected_hints,
2706 sorted_cached_hint_labels(editor),
2707 "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
2708 );
2709 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2710 })
2711 .unwrap();
2712
2713 editor
2714 .update(cx, |editor, window, cx| {
2715 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2716 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2717 });
2718 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2719 s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
2720 });
2721 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2722 s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
2723 });
2724 })
2725 .unwrap();
2726 cx.executor().run_until_parked();
2727 editor
2728 .update(cx, |editor, _window, cx| {
2729 let expected_hints = vec![
2730 "main hint #0".to_string(),
2731 "main hint #1".to_string(),
2732 "main hint #2".to_string(),
2733 "main hint #3".to_string(),
2734 "main hint #4".to_string(),
2735 "main hint #5".to_string(),
2736 "other hint #0".to_string(),
2737 "other hint #1".to_string(),
2738 "other hint #2".to_string(),
2739 ];
2740 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2741 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
2742 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2743 })
2744 .unwrap();
2745
2746 editor
2747 .update(cx, |editor, window, cx| {
2748 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2749 s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
2750 });
2751 })
2752 .unwrap();
2753 cx.executor().advance_clock(Duration::from_millis(
2754 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2755 ));
2756 cx.executor().run_until_parked();
2757 editor
2758 .update(cx, |editor, _window, cx| {
2759 let expected_hints = vec![
2760 "main hint #0".to_string(),
2761 "main hint #1".to_string(),
2762 "main hint #2".to_string(),
2763 "main hint #3".to_string(),
2764 "main hint #4".to_string(),
2765 "main hint #5".to_string(),
2766 "other hint #0".to_string(),
2767 "other hint #1".to_string(),
2768 "other hint #2".to_string(),
2769 "other hint #3".to_string(),
2770 "other hint #4".to_string(),
2771 "other hint #5".to_string(),
2772 ];
2773 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2774 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
2775 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2776 })
2777 .unwrap();
2778
2779 editor
2780 .update(cx, |editor, window, cx| {
2781 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
2782 s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
2783 });
2784 })
2785 .unwrap();
2786 cx.executor().advance_clock(Duration::from_millis(
2787 INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
2788 ));
2789 cx.executor().run_until_parked();
2790 editor
2791 .update(cx, |editor, _window, cx| {
2792 let expected_hints = vec![
2793 "main hint #0".to_string(),
2794 "main hint #1".to_string(),
2795 "main hint #2".to_string(),
2796 "main hint #3".to_string(),
2797 "main hint #4".to_string(),
2798 "main hint #5".to_string(),
2799 "other hint #0".to_string(),
2800 "other hint #1".to_string(),
2801 "other hint #2".to_string(),
2802 "other hint #3".to_string(),
2803 "other hint #4".to_string(),
2804 "other hint #5".to_string(),
2805 ];
2806 assert_eq!(expected_hints, sorted_cached_hint_labels(editor),
2807 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
2808 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2809 })
2810 .unwrap();
2811
2812 editor_edited.store(true, Ordering::Release);
2813 editor
2814 .update(cx, |editor, window, cx| {
2815 editor.change_selections(None, window, cx, |s| {
2816 s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
2817 });
2818 editor.handle_input("++++more text++++", window, cx);
2819 })
2820 .unwrap();
2821 cx.executor().run_until_parked();
2822 editor
2823 .update(cx, |editor, _window, cx| {
2824 let expected_hints = vec![
2825 "main hint #0".to_string(),
2826 "main hint #1".to_string(),
2827 "main hint #2".to_string(),
2828 "main hint #3".to_string(),
2829 "main hint #4".to_string(),
2830 "main hint #5".to_string(),
2831 "other hint(edited) #0".to_string(),
2832 "other hint(edited) #1".to_string(),
2833 ];
2834 assert_eq!(
2835 expected_hints,
2836 sorted_cached_hint_labels(editor),
2837 "After multibuffer edit, editor gets scrolled back to the last selection; \
2838 all hints should be invalidated and required for all of its visible excerpts"
2839 );
2840 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
2841 })
2842 .unwrap();
2843 }
2844
2845 #[gpui::test]
2846 async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
2847 init_test(cx, |settings| {
2848 settings.defaults.inlay_hints = Some(InlayHintSettings {
2849 show_value_hints: true,
2850 enabled: true,
2851 edit_debounce_ms: 0,
2852 scroll_debounce_ms: 0,
2853 show_type_hints: false,
2854 show_parameter_hints: false,
2855 show_other_hints: false,
2856 show_background: false,
2857 toggle_on_modifiers_press: None,
2858 })
2859 });
2860
2861 let fs = FakeFs::new(cx.background_executor.clone());
2862 fs.insert_tree(
2863 path!("/a"),
2864 json!({
2865 "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
2866 "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
2867 }),
2868 )
2869 .await;
2870
2871 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
2872
2873 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2874 language_registry.add(rust_lang());
2875 let mut fake_servers = language_registry.register_fake_lsp(
2876 "Rust",
2877 FakeLspAdapter {
2878 capabilities: lsp::ServerCapabilities {
2879 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2880 ..Default::default()
2881 },
2882 ..Default::default()
2883 },
2884 );
2885
2886 let (buffer_1, _handle) = project
2887 .update(cx, |project, cx| {
2888 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
2889 })
2890 .await
2891 .unwrap();
2892 let (buffer_2, _handle2) = project
2893 .update(cx, |project, cx| {
2894 project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx)
2895 })
2896 .await
2897 .unwrap();
2898 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
2899 let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
2900 let buffer_1_excerpts = multibuffer.push_excerpts(
2901 buffer_1.clone(),
2902 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
2903 cx,
2904 );
2905 let buffer_2_excerpts = multibuffer.push_excerpts(
2906 buffer_2.clone(),
2907 [ExcerptRange::new(Point::new(0, 1)..Point::new(2, 1))],
2908 cx,
2909 );
2910 (buffer_1_excerpts, buffer_2_excerpts)
2911 });
2912
2913 assert!(!buffer_1_excerpts.is_empty());
2914 assert!(!buffer_2_excerpts.is_empty());
2915
2916 cx.executor().run_until_parked();
2917 let editor = cx.add_window(|window, cx| {
2918 Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
2919 });
2920 let editor_edited = Arc::new(AtomicBool::new(false));
2921 let fake_server = fake_servers.next().await.unwrap();
2922 let closure_editor_edited = Arc::clone(&editor_edited);
2923 fake_server
2924 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2925 let task_editor_edited = Arc::clone(&closure_editor_edited);
2926 async move {
2927 let hint_text = if params.text_document.uri
2928 == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
2929 {
2930 "main hint"
2931 } else if params.text_document.uri
2932 == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
2933 {
2934 "other hint"
2935 } else {
2936 panic!("unexpected uri: {:?}", params.text_document.uri);
2937 };
2938
2939 let positions = [
2940 lsp::Position::new(0, 2),
2941 lsp::Position::new(4, 2),
2942 lsp::Position::new(22, 2),
2943 lsp::Position::new(44, 2),
2944 lsp::Position::new(56, 2),
2945 lsp::Position::new(67, 2),
2946 ];
2947 let out_of_range_hint = lsp::InlayHint {
2948 position: lsp::Position::new(
2949 params.range.start.line + 99,
2950 params.range.start.character + 99,
2951 ),
2952 label: lsp::InlayHintLabel::String(
2953 "out of excerpt range, should be ignored".to_string(),
2954 ),
2955 kind: None,
2956 text_edits: None,
2957 tooltip: None,
2958 padding_left: None,
2959 padding_right: None,
2960 data: None,
2961 };
2962
2963 let edited = task_editor_edited.load(Ordering::Acquire);
2964 Ok(Some(
2965 std::iter::once(out_of_range_hint)
2966 .chain(positions.into_iter().enumerate().map(|(i, position)| {
2967 lsp::InlayHint {
2968 position,
2969 label: lsp::InlayHintLabel::String(format!(
2970 "{hint_text}{} #{i}",
2971 if edited { "(edited)" } else { "" },
2972 )),
2973 kind: None,
2974 text_edits: None,
2975 tooltip: None,
2976 padding_left: None,
2977 padding_right: None,
2978 data: None,
2979 }
2980 }))
2981 .collect(),
2982 ))
2983 }
2984 })
2985 .next()
2986 .await;
2987 cx.executor().run_until_parked();
2988 editor
2989 .update(cx, |editor, _, cx| {
2990 assert_eq!(
2991 vec!["main hint #0".to_string(), "other hint #0".to_string()],
2992 sorted_cached_hint_labels(editor),
2993 "Cache should update for both excerpts despite hints display was disabled"
2994 );
2995 assert!(
2996 visible_hint_labels(editor, cx).is_empty(),
2997 "All hints are disabled and should not be shown despite being present in the cache"
2998 );
2999 })
3000 .unwrap();
3001
3002 editor
3003 .update(cx, |editor, _, cx| {
3004 editor.buffer().update(cx, |multibuffer, cx| {
3005 multibuffer.remove_excerpts(buffer_2_excerpts, cx)
3006 })
3007 })
3008 .unwrap();
3009 cx.executor().run_until_parked();
3010 editor
3011 .update(cx, |editor, _, cx| {
3012 assert_eq!(
3013 vec!["main hint #0".to_string()],
3014 cached_hint_labels(editor),
3015 "For the removed excerpt, should clean corresponding cached hints"
3016 );
3017 assert!(
3018 visible_hint_labels(editor, cx).is_empty(),
3019 "All hints are disabled and should not be shown despite being present in the cache"
3020 );
3021 })
3022 .unwrap();
3023
3024 update_test_language_settings(cx, |settings| {
3025 settings.defaults.inlay_hints = Some(InlayHintSettings {
3026 show_value_hints: true,
3027 enabled: true,
3028 edit_debounce_ms: 0,
3029 scroll_debounce_ms: 0,
3030 show_type_hints: true,
3031 show_parameter_hints: true,
3032 show_other_hints: true,
3033 show_background: false,
3034 toggle_on_modifiers_press: None,
3035 })
3036 });
3037 cx.executor().run_until_parked();
3038 editor
3039 .update(cx, |editor, _, cx| {
3040 let expected_hints = vec!["main hint #0".to_string()];
3041 assert_eq!(
3042 expected_hints,
3043 cached_hint_labels(editor),
3044 "Hint display settings change should not change the cache"
3045 );
3046 assert_eq!(
3047 expected_hints,
3048 visible_hint_labels(editor, cx),
3049 "Settings change should make cached hints visible"
3050 );
3051 })
3052 .unwrap();
3053 }
3054
3055 #[gpui::test]
3056 async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
3057 init_test(cx, |settings| {
3058 settings.defaults.inlay_hints = Some(InlayHintSettings {
3059 show_value_hints: true,
3060 enabled: true,
3061 edit_debounce_ms: 0,
3062 scroll_debounce_ms: 0,
3063 show_type_hints: true,
3064 show_parameter_hints: true,
3065 show_other_hints: true,
3066 show_background: false,
3067 toggle_on_modifiers_press: None,
3068 })
3069 });
3070
3071 let fs = FakeFs::new(cx.background_executor.clone());
3072 fs.insert_tree(
3073 path!("/a"),
3074 json!({
3075 "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)),
3076 "other.rs": "// Test file",
3077 }),
3078 )
3079 .await;
3080
3081 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3082
3083 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3084 language_registry.add(rust_lang());
3085 language_registry.register_fake_lsp(
3086 "Rust",
3087 FakeLspAdapter {
3088 capabilities: lsp::ServerCapabilities {
3089 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3090 ..Default::default()
3091 },
3092 initializer: Some(Box::new(move |fake_server| {
3093 let lsp_request_count = Arc::new(AtomicU32::new(0));
3094 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3095 move |params, _| {
3096 let i = lsp_request_count.fetch_add(1, Ordering::Release) + 1;
3097 async move {
3098 assert_eq!(
3099 params.text_document.uri,
3100 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3101 );
3102 let query_start = params.range.start;
3103 Ok(Some(vec![lsp::InlayHint {
3104 position: query_start,
3105 label: lsp::InlayHintLabel::String(i.to_string()),
3106 kind: None,
3107 text_edits: None,
3108 tooltip: None,
3109 padding_left: None,
3110 padding_right: None,
3111 data: None,
3112 }]))
3113 }
3114 },
3115 );
3116 })),
3117 ..Default::default()
3118 },
3119 );
3120
3121 let buffer = project
3122 .update(cx, |project, cx| {
3123 project.open_local_buffer(path!("/a/main.rs"), cx)
3124 })
3125 .await
3126 .unwrap();
3127 let editor =
3128 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3129
3130 cx.executor().run_until_parked();
3131 editor
3132 .update(cx, |editor, window, cx| {
3133 editor.change_selections(None, window, cx, |s| {
3134 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3135 })
3136 })
3137 .unwrap();
3138 cx.executor().run_until_parked();
3139 editor
3140 .update(cx, |editor, _, cx| {
3141 let expected_hints = vec!["1".to_string()];
3142 assert_eq!(expected_hints, cached_hint_labels(editor));
3143 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3144 })
3145 .unwrap();
3146 }
3147
3148 #[gpui::test]
3149 async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
3150 init_test(cx, |settings| {
3151 settings.defaults.inlay_hints = Some(InlayHintSettings {
3152 show_value_hints: true,
3153 enabled: false,
3154 edit_debounce_ms: 0,
3155 scroll_debounce_ms: 0,
3156 show_type_hints: true,
3157 show_parameter_hints: true,
3158 show_other_hints: true,
3159 show_background: false,
3160 toggle_on_modifiers_press: None,
3161 })
3162 });
3163
3164 let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
3165 let lsp_request_count = Arc::new(AtomicU32::new(0));
3166 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3167 move |params, _| {
3168 let lsp_request_count = lsp_request_count.clone();
3169 async move {
3170 assert_eq!(
3171 params.text_document.uri,
3172 lsp::Url::from_file_path(file_with_hints).unwrap(),
3173 );
3174
3175 let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
3176 Ok(Some(vec![lsp::InlayHint {
3177 position: lsp::Position::new(0, i),
3178 label: lsp::InlayHintLabel::String(i.to_string()),
3179 kind: None,
3180 text_edits: None,
3181 tooltip: None,
3182 padding_left: None,
3183 padding_right: None,
3184 data: None,
3185 }]))
3186 }
3187 },
3188 );
3189 })
3190 .await;
3191
3192 editor
3193 .update(cx, |editor, window, cx| {
3194 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3195 })
3196 .unwrap();
3197
3198 cx.executor().run_until_parked();
3199 editor
3200 .update(cx, |editor, _, cx| {
3201 let expected_hints = vec!["1".to_string()];
3202 assert_eq!(
3203 expected_hints,
3204 cached_hint_labels(editor),
3205 "Should display inlays after toggle despite them disabled in settings"
3206 );
3207 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3208 })
3209 .unwrap();
3210
3211 editor
3212 .update(cx, |editor, window, cx| {
3213 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3214 })
3215 .unwrap();
3216 cx.executor().run_until_parked();
3217 editor
3218 .update(cx, |editor, _, cx| {
3219 assert!(
3220 cached_hint_labels(editor).is_empty(),
3221 "Should clear hints after 2nd toggle"
3222 );
3223 assert!(visible_hint_labels(editor, cx).is_empty());
3224 })
3225 .unwrap();
3226
3227 update_test_language_settings(cx, |settings| {
3228 settings.defaults.inlay_hints = Some(InlayHintSettings {
3229 show_value_hints: true,
3230 enabled: true,
3231 edit_debounce_ms: 0,
3232 scroll_debounce_ms: 0,
3233 show_type_hints: true,
3234 show_parameter_hints: true,
3235 show_other_hints: true,
3236 show_background: false,
3237 toggle_on_modifiers_press: None,
3238 })
3239 });
3240 cx.executor().run_until_parked();
3241 editor
3242 .update(cx, |editor, _, cx| {
3243 let expected_hints = vec!["2".to_string()];
3244 assert_eq!(
3245 expected_hints,
3246 cached_hint_labels(editor),
3247 "Should query LSP hints for the 2nd time after enabling hints in settings"
3248 );
3249 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3250 })
3251 .unwrap();
3252
3253 editor
3254 .update(cx, |editor, window, cx| {
3255 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3256 })
3257 .unwrap();
3258 cx.executor().run_until_parked();
3259 editor
3260 .update(cx, |editor, _, cx| {
3261 assert!(
3262 cached_hint_labels(editor).is_empty(),
3263 "Should clear hints after enabling in settings and a 3rd toggle"
3264 );
3265 assert!(visible_hint_labels(editor, cx).is_empty());
3266 })
3267 .unwrap();
3268
3269 editor
3270 .update(cx, |editor, window, cx| {
3271 editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx)
3272 })
3273 .unwrap();
3274 cx.executor().run_until_parked();
3275 editor.update(cx, |editor, _, cx| {
3276 let expected_hints = vec!["3".to_string()];
3277 assert_eq!(
3278 expected_hints,
3279 cached_hint_labels(editor),
3280 "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
3281 );
3282 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3283 }).unwrap();
3284 }
3285
3286 #[gpui::test]
3287 async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) {
3288 init_test(cx, |settings| {
3289 settings.defaults.inlay_hints = Some(InlayHintSettings {
3290 show_value_hints: true,
3291 enabled: true,
3292 edit_debounce_ms: 0,
3293 scroll_debounce_ms: 0,
3294 show_type_hints: true,
3295 show_parameter_hints: true,
3296 show_other_hints: true,
3297 show_background: false,
3298 toggle_on_modifiers_press: None,
3299 })
3300 });
3301
3302 let fs = FakeFs::new(cx.background_executor.clone());
3303 fs.insert_tree(
3304 path!("/a"),
3305 json!({
3306 "main.rs": "fn main() {
3307 let x = 42;
3308 std::thread::scope(|s| {
3309 s.spawn(|| {
3310 let _x = x;
3311 });
3312 });
3313 }",
3314 "other.rs": "// Test file",
3315 }),
3316 )
3317 .await;
3318
3319 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3320
3321 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3322 language_registry.add(rust_lang());
3323 language_registry.register_fake_lsp(
3324 "Rust",
3325 FakeLspAdapter {
3326 capabilities: lsp::ServerCapabilities {
3327 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3328 ..Default::default()
3329 },
3330 initializer: Some(Box::new(move |fake_server| {
3331 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
3332 move |params, _| async move {
3333 assert_eq!(
3334 params.text_document.uri,
3335 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3336 );
3337 Ok(Some(
3338 serde_json::from_value(json!([
3339 {
3340 "position": {
3341 "line": 3,
3342 "character": 16
3343 },
3344 "label": "move",
3345 "paddingLeft": false,
3346 "paddingRight": false
3347 },
3348 {
3349 "position": {
3350 "line": 3,
3351 "character": 16
3352 },
3353 "label": "(",
3354 "paddingLeft": false,
3355 "paddingRight": false
3356 },
3357 {
3358 "position": {
3359 "line": 3,
3360 "character": 16
3361 },
3362 "label": [
3363 {
3364 "value": "&x"
3365 }
3366 ],
3367 "paddingLeft": false,
3368 "paddingRight": false,
3369 "data": {
3370 "file_id": 0
3371 }
3372 },
3373 {
3374 "position": {
3375 "line": 3,
3376 "character": 16
3377 },
3378 "label": ")",
3379 "paddingLeft": false,
3380 "paddingRight": true
3381 },
3382 // not a correct syntax, but checks that same symbols at the same place
3383 // are not deduplicated
3384 {
3385 "position": {
3386 "line": 3,
3387 "character": 16
3388 },
3389 "label": ")",
3390 "paddingLeft": false,
3391 "paddingRight": true
3392 },
3393 ]))
3394 .unwrap(),
3395 ))
3396 },
3397 );
3398 })),
3399 ..FakeLspAdapter::default()
3400 },
3401 );
3402
3403 let buffer = project
3404 .update(cx, |project, cx| {
3405 project.open_local_buffer(path!("/a/main.rs"), cx)
3406 })
3407 .await
3408 .unwrap();
3409 let editor =
3410 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3411
3412 cx.executor().run_until_parked();
3413 editor
3414 .update(cx, |editor, window, cx| {
3415 editor.change_selections(None, window, cx, |s| {
3416 s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
3417 })
3418 })
3419 .unwrap();
3420 cx.executor().run_until_parked();
3421 editor
3422 .update(cx, |editor, _window, cx| {
3423 let expected_hints = vec![
3424 "move".to_string(),
3425 "(".to_string(),
3426 "&x".to_string(),
3427 ") ".to_string(),
3428 ") ".to_string(),
3429 ];
3430 assert_eq!(
3431 expected_hints,
3432 cached_hint_labels(editor),
3433 "Editor inlay hints should repeat server's order when placed at the same spot"
3434 );
3435 assert_eq!(expected_hints, visible_hint_labels(editor, cx));
3436 })
3437 .unwrap();
3438 }
3439
3440 pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
3441 cx.update(|cx| {
3442 let settings_store = SettingsStore::test(cx);
3443 cx.set_global(settings_store);
3444 theme::init(theme::LoadThemes::JustBase, cx);
3445 release_channel::init(SemanticVersion::default(), cx);
3446 client::init_settings(cx);
3447 language::init(cx);
3448 Project::init_settings(cx);
3449 workspace::init_settings(cx);
3450 crate::init(cx);
3451 });
3452
3453 update_test_language_settings(cx, f);
3454 }
3455
3456 async fn prepare_test_objects(
3457 cx: &mut TestAppContext,
3458 initialize: impl 'static + Send + Fn(&mut FakeLanguageServer, &'static str) + Send + Sync,
3459 ) -> (&'static str, WindowHandle<Editor>, FakeLanguageServer) {
3460 let fs = FakeFs::new(cx.background_executor.clone());
3461 fs.insert_tree(
3462 path!("/a"),
3463 json!({
3464 "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
3465 "other.rs": "// Test file",
3466 }),
3467 )
3468 .await;
3469
3470 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
3471 let file_path = path!("/a/main.rs");
3472
3473 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3474 language_registry.add(rust_lang());
3475 let mut fake_servers = language_registry.register_fake_lsp(
3476 "Rust",
3477 FakeLspAdapter {
3478 capabilities: lsp::ServerCapabilities {
3479 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
3480 ..Default::default()
3481 },
3482 initializer: Some(Box::new(move |server| initialize(server, file_path))),
3483 ..Default::default()
3484 },
3485 );
3486
3487 let buffer = project
3488 .update(cx, |project, cx| {
3489 project.open_local_buffer(path!("/a/main.rs"), cx)
3490 })
3491 .await
3492 .unwrap();
3493 let editor =
3494 cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
3495
3496 editor
3497 .update(cx, |editor, _, cx| {
3498 assert!(cached_hint_labels(editor).is_empty());
3499 assert!(visible_hint_labels(editor, cx).is_empty());
3500 })
3501 .unwrap();
3502
3503 cx.executor().run_until_parked();
3504 let fake_server = fake_servers.next().await.unwrap();
3505 (file_path, editor, fake_server)
3506 }
3507
3508 // 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.
3509 // Ensure a stable order for testing.
3510 fn sorted_cached_hint_labels(editor: &Editor) -> Vec<String> {
3511 let mut labels = cached_hint_labels(editor);
3512 labels.sort();
3513 labels
3514 }
3515
3516 pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
3517 let mut labels = Vec::new();
3518 for excerpt_hints in editor.inlay_hint_cache().hints.values() {
3519 let excerpt_hints = excerpt_hints.read();
3520 for id in &excerpt_hints.ordered_hints {
3521 let hint = &excerpt_hints.hints_by_id[id];
3522 let mut label = hint.text();
3523 if hint.padding_left {
3524 label.insert(0, ' ');
3525 }
3526 if hint.padding_right {
3527 label.push_str(" ");
3528 }
3529 labels.push(label);
3530 }
3531 }
3532
3533 labels
3534 }
3535
3536 pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
3537 editor
3538 .visible_inlay_hints(cx)
3539 .into_iter()
3540 .map(|hint| hint.text.to_string())
3541 .collect()
3542 }
3543}