/// 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./// Has nothing to do with other inlays, e.g. copilot suggestions — those are stored elsewhere./// On every update, cache may query for more inlay hints and update inlays on the screen.////// 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./// 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.////// 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.use std::{ cmp, ops::{ControlFlow, Range}, sync::Arc, time::Duration,}; use crate::{ Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot, display_map::Inlay,};use anyhow::Context as _;use clock::Global;use futures::future;use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window};use language::{Buffer, BufferSnapshot, language_settings::InlayHintKind};use parking_lot::RwLock;use project::{InlayHint, ResolveState}; use collections::{HashMap, HashSet, hash_map};use language::language_settings::InlayHintSettings;use smol::lock::Semaphore;use sum_tree::Bias;use text::{BufferId, ToOffset, ToPoint};use util::{ResultExt, post_inc}; pub struct InlayHintCache { hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>, allowed_hint_kinds: HashSet<Option<InlayHintKind>>, version: usize, pub(super) enabled: bool, modifiers_override: bool, enabled_in_settings: bool, update_tasks: HashMap<ExcerptId, TasksForRanges>, refresh_task: Task<()>, invalidate_debounce: Option<Duration>, append_debounce: Option<Duration>, lsp_request_limiter: Arc<Semaphore>,} #[derive(Debug)]struct TasksForRanges { tasks: Vec<Task<()>>, sorted_ranges: Vec<Range<language::Anchor>>,} #[derive(Debug)]struct CachedExcerptHints { version: usize, buffer_version: Global, buffer_id: BufferId, ordered_hints: Vec<InlayId>, hints_by_id: HashMap<InlayId, InlayHint>,} /// 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.#[derive(Debug, Clone, Copy)]pub(super) enum InvalidationStrategy { /// 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. /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation. /// /// 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. RefreshRequested, /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place. /// 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. BufferEdited, /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position. /// No invalidation should be done at all, all new hints are added to the cache. /// /// 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). /// 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. None,} /// A splice to send into the `inlay_map` for updating the visible inlays on the screen./// "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./// 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./// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.#[derive(Debug, Default)]pub(super) struct InlaySplice { pub to_remove: Vec<InlayId>, pub to_insert: Vec<Inlay>,} #[derive(Debug)]struct ExcerptHintsUpdate { excerpt_id: ExcerptId, remove_from_visible: HashSet<InlayId>, remove_from_cache: HashSet<InlayId>, add_to_cache: Vec<InlayHint>,} #[derive(Debug, Clone, Copy)]struct ExcerptQuery { buffer_id: BufferId, excerpt_id: ExcerptId, cache_version: usize, invalidate: InvalidationStrategy, reason: &'static str,} impl InvalidationStrategy { fn should_invalidate(&self) -> bool { matches!( self, InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited ) }} impl TasksForRanges { fn new(query_ranges: QueryRanges, task: Task<()>) -> Self { Self { tasks: vec![task], sorted_ranges: query_ranges.into_sorted_query_ranges(), } } fn update_cached_tasks( &mut self, buffer_snapshot: &BufferSnapshot, query_ranges: QueryRanges, invalidate: InvalidationStrategy, spawn_task: impl FnOnce(QueryRanges) -> Task<()>, ) { let query_ranges = if invalidate.should_invalidate() { self.tasks.clear(); self.sorted_ranges = query_ranges.clone().into_sorted_query_ranges(); query_ranges } else { let mut non_cached_query_ranges = query_ranges; non_cached_query_ranges.before_visible = non_cached_query_ranges .before_visible .into_iter() .flat_map(|query_range| { self.remove_cached_ranges_from_query(buffer_snapshot, query_range) }) .collect(); non_cached_query_ranges.visible = non_cached_query_ranges .visible .into_iter() .flat_map(|query_range| { self.remove_cached_ranges_from_query(buffer_snapshot, query_range) }) .collect(); non_cached_query_ranges.after_visible = non_cached_query_ranges .after_visible .into_iter() .flat_map(|query_range| { self.remove_cached_ranges_from_query(buffer_snapshot, query_range) }) .collect(); non_cached_query_ranges }; if !query_ranges.is_empty() { self.tasks.push(spawn_task(query_ranges)); } } fn remove_cached_ranges_from_query( &mut self, buffer_snapshot: &BufferSnapshot, query_range: Range<language::Anchor>, ) -> Vec<Range<language::Anchor>> { let mut ranges_to_query = Vec::new(); let mut latest_cached_range = None::<&mut Range<language::Anchor>>; for cached_range in self .sorted_ranges .iter_mut() .skip_while(|cached_range| { cached_range .end .cmp(&query_range.start, buffer_snapshot) .is_lt() }) .take_while(|cached_range| { cached_range .start .cmp(&query_range.end, buffer_snapshot) .is_le() }) { match latest_cached_range { Some(latest_cached_range) => { if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset { ranges_to_query.push(latest_cached_range.end..cached_range.start); cached_range.start = latest_cached_range.end; } } None => { if query_range .start .cmp(&cached_range.start, buffer_snapshot) .is_lt() { ranges_to_query.push(query_range.start..cached_range.start); cached_range.start = query_range.start; } } } latest_cached_range = Some(cached_range); } match latest_cached_range { Some(latest_cached_range) => { if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset { ranges_to_query.push(latest_cached_range.end..query_range.end); latest_cached_range.end = query_range.end; } } None => { ranges_to_query.push(query_range.clone()); self.sorted_ranges.push(query_range); self.sorted_ranges .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); } } ranges_to_query } fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range<language::Anchor>) { self.sorted_ranges = self .sorted_ranges .drain(..) .filter_map(|mut cached_range| { if cached_range.start.cmp(&range.end, buffer).is_gt() || cached_range.end.cmp(&range.start, buffer).is_lt() { Some(vec![cached_range]) } else if cached_range.start.cmp(&range.start, buffer).is_ge() && cached_range.end.cmp(&range.end, buffer).is_le() { None } else if range.start.cmp(&cached_range.start, buffer).is_ge() && range.end.cmp(&cached_range.end, buffer).is_le() { Some(vec![ cached_range.start..range.start, range.end..cached_range.end, ]) } else if cached_range.start.cmp(&range.start, buffer).is_ge() { cached_range.start = range.end; Some(vec![cached_range]) } else { cached_range.end = range.start; Some(vec![cached_range]) } }) .flatten() .collect(); }} impl InlayHintCache { pub(super) fn new(inlay_hint_settings: InlayHintSettings) -> Self { Self { allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(), enabled: inlay_hint_settings.enabled, modifiers_override: false, enabled_in_settings: inlay_hint_settings.enabled, hints: HashMap::default(), update_tasks: HashMap::default(), refresh_task: Task::ready(()), invalidate_debounce: debounce_value(inlay_hint_settings.edit_debounce_ms), append_debounce: debounce_value(inlay_hint_settings.scroll_debounce_ms), version: 0, lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)), } } /// Checks inlay hint settings for enabled hint kinds and general enabled state. /// Generates corresponding inlay_map splice updates on settings changes. /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries. pub(super) fn update_settings( &mut self, multi_buffer: &Entity<MultiBuffer>, new_hint_settings: InlayHintSettings, visible_hints: Vec<Inlay>, cx: &mut Context<Editor>, ) -> ControlFlow<Option<InlaySplice>> { let old_enabled = self.enabled; // If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay // hint visibility changes when other settings change (such as theme). // // Another option might be to store whether the user has manually toggled inlay hint // visibility, and prefer this. This could lead to confusion as it means inlay hint // visibility would not change when updating the setting if they were ever toggled. if new_hint_settings.enabled != self.enabled_in_settings { self.enabled = new_hint_settings.enabled; self.enabled_in_settings = new_hint_settings.enabled; self.modifiers_override = false; }; self.invalidate_debounce = debounce_value(new_hint_settings.edit_debounce_ms); self.append_debounce = debounce_value(new_hint_settings.scroll_debounce_ms); let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds(); match (old_enabled, self.enabled) { (false, false) => { self.allowed_hint_kinds = new_allowed_hint_kinds; ControlFlow::Break(None) } (true, true) => { if new_allowed_hint_kinds == self.allowed_hint_kinds { ControlFlow::Break(None) } else { let new_splice = self.new_allowed_hint_kinds_splice( multi_buffer, &visible_hints, &new_allowed_hint_kinds, cx, ); if new_splice.is_some() { self.version += 1; self.allowed_hint_kinds = new_allowed_hint_kinds; } ControlFlow::Break(new_splice) } } (true, false) => { self.modifiers_override = false; self.allowed_hint_kinds = new_allowed_hint_kinds; if self.hints.is_empty() { ControlFlow::Break(None) } else { self.clear(); ControlFlow::Break(Some(InlaySplice { to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(), to_insert: Vec::new(), })) } } (false, true) => { self.modifiers_override = false; self.allowed_hint_kinds = new_allowed_hint_kinds; ControlFlow::Continue(()) } } } pub(super) fn modifiers_override(&mut self, new_override: bool) -> Option<bool> { if self.modifiers_override == new_override { return None; } self.modifiers_override = new_override; if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override) { self.clear(); Some(false) } else { Some(true) } } pub(super) fn toggle(&mut self, enabled: bool) -> bool { if self.enabled == enabled { return false; } self.enabled = enabled; self.modifiers_override = false; if !enabled { self.clear(); } true } /// If needed, queries LSP for new inlay hints, using the invalidation strategy given. /// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first, /// followed by the delayed queries of the same range above and below the visible one. /// This way, subsequent refresh invocations are less likely to trigger LSP queries for the invisible ranges. pub(super) fn spawn_hint_refresh( &mut self, reason_description: &'static str, excerpts_to_query: HashMap<ExcerptId, (Entity<Buffer>, Global, Range<usize>)>, invalidate: InvalidationStrategy, ignore_debounce: bool, cx: &mut Context<Editor>, ) -> Option<InlaySplice> { if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override) { return None; } let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { self.update_tasks .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); self.hints.retain(|cached_excerpt, cached_hints| { let retain = excerpts_to_query.contains_key(cached_excerpt); if !retain { invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied()); } retain }); } if excerpts_to_query.is_empty() && invalidated_hints.is_empty() { return None; } let cache_version = self.version + 1; let debounce_duration = if ignore_debounce { None } else if invalidate.should_invalidate() { self.invalidate_debounce } else { self.append_debounce }; self.refresh_task = cx.spawn(async move |editor, cx| { if let Some(debounce_duration) = debounce_duration { cx.background_executor().timer(debounce_duration).await; } editor .update(cx, |editor, cx| { spawn_new_update_tasks( editor, reason_description, excerpts_to_query, invalidate, cache_version, cx, ) }) .ok(); }); if invalidated_hints.is_empty() { None } else { Some(InlaySplice { to_remove: invalidated_hints, to_insert: Vec::new(), }) } } fn new_allowed_hint_kinds_splice( &self, multi_buffer: &Entity<MultiBuffer>, visible_hints: &[Inlay], new_kinds: &HashSet<Option<InlayHintKind>>, cx: &mut Context<Editor>, ) -> Option<InlaySplice> { let old_kinds = &self.allowed_hint_kinds; if new_kinds == old_kinds { return None; } let mut to_remove = Vec::new(); let mut to_insert = Vec::new(); let mut shown_hints_to_remove = visible_hints.iter().fold( HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(), |mut current_hints, inlay| { current_hints .entry(inlay.position.excerpt_id) .or_default() .push((inlay.position, inlay.id)); current_hints }, ); let multi_buffer = multi_buffer.read(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx); for (excerpt_id, excerpt_cached_hints) in &self.hints { let shown_excerpt_hints_to_remove = shown_hints_to_remove.entry(*excerpt_id).or_default(); let excerpt_cached_hints = excerpt_cached_hints.read(); let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable(); shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { let Some(buffer) = shown_anchor .buffer_id .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false; }; let buffer_snapshot = buffer.read(cx).snapshot(); loop { match excerpt_cache.peek() { Some(&cached_hint_id) => { let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; if cached_hint_id == shown_hint_id { excerpt_cache.next(); return !new_kinds.contains(&cached_hint.kind); } match cached_hint .position .cmp(&shown_anchor.text_anchor, &buffer_snapshot) { cmp::Ordering::Less | cmp::Ordering::Equal => { if !old_kinds.contains(&cached_hint.kind) && new_kinds.contains(&cached_hint.kind)Lines 1–500 of 3543. Load more
/// 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. /// Has nothing to do with other inlays, e.g. copilot suggestions — those are stored elsewhere. /// On every update, cache may query for more inlay hints and update inlays on the screen. /// /// 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. /// 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. /// /// 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. use std::{ cmp, ops::{ControlFlow, Range}, sync::Arc, time::Duration, }; use crate::{ Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot, display_map::Inlay, }; use anyhow::Context as _; use clock::Global; use futures::future; use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window}; use language::{Buffer, BufferSnapshot, language_settings::InlayHintKind}; use parking_lot::RwLock; use project::{InlayHint, ResolveState}; use collections::{HashMap, HashSet, hash_map}; use language::language_settings::InlayHintSettings; use smol::lock::Semaphore; use sum_tree::Bias; use text::{BufferId, ToOffset, ToPoint}; use util::{ResultExt, post_inc}; pub struct InlayHintCache { hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>, allowed_hint_kinds: HashSet<Option<InlayHintKind>>, version: usize, pub(super) enabled: bool, modifiers_override: bool, enabled_in_settings: bool, update_tasks: HashMap<ExcerptId, TasksForRanges>, refresh_task: Task<()>, invalidate_debounce: Option<Duration>, append_debounce: Option<Duration>, lsp_request_limiter: Arc<Semaphore>, } #[derive(Debug)] struct TasksForRanges { tasks: Vec<Task<()>>, sorted_ranges: Vec<Range<language::Anchor>>, } #[derive(Debug)] struct CachedExcerptHints { version: usize, buffer_version: Global, buffer_id: BufferId, ordered_hints: Vec<InlayId>, hints_by_id: HashMap<InlayId, InlayHint>, } /// 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. #[derive(Debug, Clone, Copy)] pub(super) enum InvalidationStrategy { /// 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. /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation. /// /// 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. RefreshRequested, /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place. /// 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. BufferEdited, /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position. /// No invalidation should be done at all, all new hints are added to the cache. /// /// 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). /// 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. None, } /// A splice to send into the `inlay_map` for updating the visible inlays on the screen. /// "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. /// 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. /// Splice is picked to help avoid extra hint flickering and "jumps" on the screen. #[derive(Debug, Default)] pub(super) struct InlaySplice { pub to_remove: Vec<InlayId>, pub to_insert: Vec<Inlay>, } #[derive(Debug)] struct ExcerptHintsUpdate { excerpt_id: ExcerptId, remove_from_visible: HashSet<InlayId>, remove_from_cache: HashSet<InlayId>, add_to_cache: Vec<InlayHint>, } #[derive(Debug, Clone, Copy)] struct ExcerptQuery { buffer_id: BufferId, excerpt_id: ExcerptId, cache_version: usize, invalidate: InvalidationStrategy, reason: &'static str, } impl InvalidationStrategy { fn should_invalidate(&self) -> bool { matches!( self, InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited ) } } impl TasksForRanges { fn new(query_ranges: QueryRanges, task: Task<()>) -> Self { Self { tasks: vec![task], sorted_ranges: query_ranges.into_sorted_query_ranges(), } } fn update_cached_tasks( &mut self, buffer_snapshot: &BufferSnapshot, query_ranges: QueryRanges, invalidate: InvalidationStrategy, spawn_task: impl FnOnce(QueryRanges) -> Task<()>, ) { let query_ranges = if invalidate.should_invalidate() { self.tasks.clear(); self.sorted_ranges = query_ranges.clone().into_sorted_query_ranges(); query_ranges } else { let mut non_cached_query_ranges = query_ranges; non_cached_query_ranges.before_visible = non_cached_query_ranges .before_visible .into_iter() .flat_map(|query_range| { self.remove_cached_ranges_from_query(buffer_snapshot, query_range) }) .collect(); non_cached_query_ranges.visible = non_cached_query_ranges .visible .into_iter() .flat_map(|query_range| { self.remove_cached_ranges_from_query(buffer_snapshot, query_range) }) .collect(); non_cached_query_ranges.after_visible = non_cached_query_ranges .after_visible .into_iter() .flat_map(|query_range| { self.remove_cached_ranges_from_query(buffer_snapshot, query_range) }) .collect(); non_cached_query_ranges }; if !query_ranges.is_empty() { self.tasks.push(spawn_task(query_ranges)); } } fn remove_cached_ranges_from_query( &mut self, buffer_snapshot: &BufferSnapshot, query_range: Range<language::Anchor>, ) -> Vec<Range<language::Anchor>> { let mut ranges_to_query = Vec::new(); let mut latest_cached_range = None::<&mut Range<language::Anchor>>; for cached_range in self .sorted_ranges .iter_mut() .skip_while(|cached_range| { cached_range .end .cmp(&query_range.start, buffer_snapshot) .is_lt() }) .take_while(|cached_range| { cached_range .start .cmp(&query_range.end, buffer_snapshot) .is_le() }) { match latest_cached_range { Some(latest_cached_range) => { if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset { ranges_to_query.push(latest_cached_range.end..cached_range.start); cached_range.start = latest_cached_range.end; } } None => { if query_range .start .cmp(&cached_range.start, buffer_snapshot) .is_lt() { ranges_to_query.push(query_range.start..cached_range.start); cached_range.start = query_range.start; } } } latest_cached_range = Some(cached_range); } match latest_cached_range { Some(latest_cached_range) => { if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset { ranges_to_query.push(latest_cached_range.end..query_range.end); latest_cached_range.end = query_range.end; } } None => { ranges_to_query.push(query_range.clone()); self.sorted_ranges.push(query_range); self.sorted_ranges .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); } } ranges_to_query } fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range<language::Anchor>) { self.sorted_ranges = self .sorted_ranges .drain(..) .filter_map(|mut cached_range| { if cached_range.start.cmp(&range.end, buffer).is_gt() || cached_range.end.cmp(&range.start, buffer).is_lt() { Some(vec![cached_range]) } else if cached_range.start.cmp(&range.start, buffer).is_ge() && cached_range.end.cmp(&range.end, buffer).is_le() { None } else if range.start.cmp(&cached_range.start, buffer).is_ge() && range.end.cmp(&cached_range.end, buffer).is_le() { Some(vec![ cached_range.start..range.start, range.end..cached_range.end, ]) } else if cached_range.start.cmp(&range.start, buffer).is_ge() { cached_range.start = range.end; Some(vec![cached_range]) } else { cached_range.end = range.start; Some(vec![cached_range]) } }) .flatten() .collect(); } } impl InlayHintCache { pub(super) fn new(inlay_hint_settings: InlayHintSettings) -> Self { Self { allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(), enabled: inlay_hint_settings.enabled, modifiers_override: false, enabled_in_settings: inlay_hint_settings.enabled, hints: HashMap::default(), update_tasks: HashMap::default(), refresh_task: Task::ready(()), invalidate_debounce: debounce_value(inlay_hint_settings.edit_debounce_ms), append_debounce: debounce_value(inlay_hint_settings.scroll_debounce_ms), version: 0, lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)), } } /// Checks inlay hint settings for enabled hint kinds and general enabled state. /// Generates corresponding inlay_map splice updates on settings changes. /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries. pub(super) fn update_settings( &mut self, multi_buffer: &Entity<MultiBuffer>, new_hint_settings: InlayHintSettings, visible_hints: Vec<Inlay>, cx: &mut Context<Editor>, ) -> ControlFlow<Option<InlaySplice>> { let old_enabled = self.enabled; // If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay // hint visibility changes when other settings change (such as theme). // // Another option might be to store whether the user has manually toggled inlay hint // visibility, and prefer this. This could lead to confusion as it means inlay hint // visibility would not change when updating the setting if they were ever toggled. if new_hint_settings.enabled != self.enabled_in_settings { self.enabled = new_hint_settings.enabled; self.enabled_in_settings = new_hint_settings.enabled; self.modifiers_override = false; }; self.invalidate_debounce = debounce_value(new_hint_settings.edit_debounce_ms); self.append_debounce = debounce_value(new_hint_settings.scroll_debounce_ms); let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds(); match (old_enabled, self.enabled) { (false, false) => { self.allowed_hint_kinds = new_allowed_hint_kinds; ControlFlow::Break(None) } (true, true) => { if new_allowed_hint_kinds == self.allowed_hint_kinds { ControlFlow::Break(None) } else { let new_splice = self.new_allowed_hint_kinds_splice( multi_buffer, &visible_hints, &new_allowed_hint_kinds, cx, ); if new_splice.is_some() { self.version += 1; self.allowed_hint_kinds = new_allowed_hint_kinds; } ControlFlow::Break(new_splice) } } (true, false) => { self.modifiers_override = false; self.allowed_hint_kinds = new_allowed_hint_kinds; if self.hints.is_empty() { ControlFlow::Break(None) } else { self.clear(); ControlFlow::Break(Some(InlaySplice { to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(), to_insert: Vec::new(), })) } } (false, true) => { self.modifiers_override = false; self.allowed_hint_kinds = new_allowed_hint_kinds; ControlFlow::Continue(()) } } } pub(super) fn modifiers_override(&mut self, new_override: bool) -> Option<bool> { if self.modifiers_override == new_override { return None; } self.modifiers_override = new_override; if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override) { self.clear(); Some(false) } else { Some(true) } } pub(super) fn toggle(&mut self, enabled: bool) -> bool { if self.enabled == enabled { return false; } self.enabled = enabled; self.modifiers_override = false; if !enabled { self.clear(); } true } /// If needed, queries LSP for new inlay hints, using the invalidation strategy given. /// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first, /// followed by the delayed queries of the same range above and below the visible one. /// This way, subsequent refresh invocations are less likely to trigger LSP queries for the invisible ranges. pub(super) fn spawn_hint_refresh( &mut self, reason_description: &'static str, excerpts_to_query: HashMap<ExcerptId, (Entity<Buffer>, Global, Range<usize>)>, invalidate: InvalidationStrategy, ignore_debounce: bool, cx: &mut Context<Editor>, ) -> Option<InlaySplice> { if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override) { return None; } let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { self.update_tasks .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); self.hints.retain(|cached_excerpt, cached_hints| { let retain = excerpts_to_query.contains_key(cached_excerpt); if !retain { invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied()); } retain }); } if excerpts_to_query.is_empty() && invalidated_hints.is_empty() { return None; } let cache_version = self.version + 1; let debounce_duration = if ignore_debounce { None } else if invalidate.should_invalidate() { self.invalidate_debounce } else { self.append_debounce }; self.refresh_task = cx.spawn(async move |editor, cx| { if let Some(debounce_duration) = debounce_duration { cx.background_executor().timer(debounce_duration).await; } editor .update(cx, |editor, cx| { spawn_new_update_tasks( editor, reason_description, excerpts_to_query, invalidate, cache_version, cx, ) }) .ok(); }); if invalidated_hints.is_empty() { None } else { Some(InlaySplice { to_remove: invalidated_hints, to_insert: Vec::new(), }) } } fn new_allowed_hint_kinds_splice( &self, multi_buffer: &Entity<MultiBuffer>, visible_hints: &[Inlay], new_kinds: &HashSet<Option<InlayHintKind>>, cx: &mut Context<Editor>, ) -> Option<InlaySplice> { let old_kinds = &self.allowed_hint_kinds; if new_kinds == old_kinds { return None; } let mut to_remove = Vec::new(); let mut to_insert = Vec::new(); let mut shown_hints_to_remove = visible_hints.iter().fold( HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(), |mut current_hints, inlay| { current_hints .entry(inlay.position.excerpt_id) .or_default() .push((inlay.position, inlay.id)); current_hints }, ); let multi_buffer = multi_buffer.read(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx); for (excerpt_id, excerpt_cached_hints) in &self.hints { let shown_excerpt_hints_to_remove = shown_hints_to_remove.entry(*excerpt_id).or_default(); let excerpt_cached_hints = excerpt_cached_hints.read(); let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable(); shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { let Some(buffer) = shown_anchor .buffer_id .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false; }; let buffer_snapshot = buffer.read(cx).snapshot(); loop { match excerpt_cache.peek() { Some(&cached_hint_id) => { let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; if cached_hint_id == shown_hint_id { excerpt_cache.next(); return !new_kinds.contains(&cached_hint.kind); } match cached_hint .position .cmp(&shown_anchor.text_anchor, &buffer_snapshot) { cmp::Ordering::Less | cmp::Ordering::Equal => { if !old_kinds.contains(&cached_hint.kind) && new_kinds.contains(&cached_hint.kind)