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