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