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