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