inlay_hint_cache.rs

  1use std::{collections::hash_map, ops::Range, sync::Arc};
  2
  3use collections::HashMap;
  4use futures::future::Shared;
  5use gpui::{App, Entity, Task};
  6use language::{Buffer, BufferRow, BufferSnapshot};
  7use lsp::LanguageServerId;
  8use text::OffsetRangeExt;
  9
 10use crate::{InlayHint, InlayId};
 11
 12pub type CacheInlayHints = HashMap<LanguageServerId, Vec<(InlayId, InlayHint)>>;
 13pub type CacheInlayHintsTask = Shared<Task<Result<CacheInlayHints, Arc<anyhow::Error>>>>;
 14
 15/// 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.
 16#[derive(Debug, Clone, Copy)]
 17pub enum InvalidationStrategy {
 18    /// Language servers reset hints via <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_inlayHint_refresh">request</a>.
 19    /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation.
 20    ///
 21    /// 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.
 22    RefreshRequested {
 23        server_id: LanguageServerId,
 24        request_id: Option<usize>,
 25    },
 26    /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place.
 27    /// 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.
 28    BufferEdited,
 29    /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position.
 30    /// No invalidation should be done at all, all new hints are added to the cache.
 31    ///
 32    /// A special case is the editor toggles and settings change:
 33    /// in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other) and toggling hints.
 34    /// 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.
 35    None,
 36}
 37
 38impl InvalidationStrategy {
 39    pub fn should_invalidate(&self) -> bool {
 40        matches!(
 41            self,
 42            InvalidationStrategy::RefreshRequested { .. } | InvalidationStrategy::BufferEdited
 43        )
 44    }
 45}
 46
 47pub struct BufferInlayHints {
 48    snapshot: BufferSnapshot,
 49    buffer_chunks: Vec<BufferChunk>,
 50    hints_by_chunks: Vec<Option<CacheInlayHints>>,
 51    fetches_by_chunks: Vec<Option<CacheInlayHintsTask>>,
 52    hints_by_id: HashMap<InlayId, HintForId>,
 53    latest_invalidation_requests: HashMap<LanguageServerId, Option<usize>>,
 54    pub(super) hint_resolves: HashMap<InlayId, Shared<Task<()>>>,
 55}
 56
 57#[derive(Debug, Clone, Copy)]
 58struct HintForId {
 59    chunk_id: usize,
 60    server_id: LanguageServerId,
 61    position: usize,
 62}
 63
 64/// An range of rows, exclusive as [`lsp::Range`] and
 65/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range>
 66/// denote.
 67///
 68/// Represents an area in a text editor, adjacent to other ones.
 69/// Together, chunks form entire document at a particular version [`clock::Global`].
 70/// Each chunk is queried for inlays as `(start_row, 0)..(end_exclusive, 0)` via
 71/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHintParams>
 72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 73pub struct BufferChunk {
 74    pub id: usize,
 75    pub start: BufferRow,
 76    pub end: BufferRow,
 77}
 78
 79impl std::fmt::Debug for BufferInlayHints {
 80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 81        f.debug_struct("BufferInlayHints")
 82            .field("buffer_chunks", &self.buffer_chunks)
 83            .field("hints_by_chunks", &self.hints_by_chunks)
 84            .field("fetches_by_chunks", &self.fetches_by_chunks)
 85            .field("hints_by_id", &self.hints_by_id)
 86            .finish_non_exhaustive()
 87    }
 88}
 89
 90const MAX_ROWS_IN_A_CHUNK: u32 = 50;
 91
 92impl BufferInlayHints {
 93    pub fn new(buffer: &Entity<Buffer>, cx: &mut App) -> Self {
 94        let buffer = buffer.read(cx);
 95        let snapshot = buffer.snapshot();
 96        let buffer_point_range = (0..buffer.len()).to_point(&snapshot);
 97        let last_row = buffer_point_range.end.row;
 98        let buffer_chunks = (buffer_point_range.start.row..=last_row)
 99            .step_by(MAX_ROWS_IN_A_CHUNK as usize)
100            .enumerate()
101            .map(|(id, chunk_start)| BufferChunk {
102                id,
103                start: chunk_start,
104                end: (chunk_start + MAX_ROWS_IN_A_CHUNK).min(last_row),
105            })
106            .collect::<Vec<_>>();
107
108        Self {
109            hints_by_chunks: vec![None; buffer_chunks.len()],
110            fetches_by_chunks: vec![None; buffer_chunks.len()],
111            latest_invalidation_requests: HashMap::default(),
112            hints_by_id: HashMap::default(),
113            hint_resolves: HashMap::default(),
114            snapshot,
115            buffer_chunks,
116        }
117    }
118
119    pub fn applicable_chunks(
120        &self,
121        ranges: &[Range<text::Anchor>],
122    ) -> impl Iterator<Item = BufferChunk> {
123        let row_ranges = ranges
124            .iter()
125            .map(|range| range.to_point(&self.snapshot))
126            .map(|point_range| point_range.start.row..=point_range.end.row)
127            .collect::<Vec<_>>();
128        self.buffer_chunks
129            .iter()
130            .filter(move |chunk| -> bool {
131                // Be lenient and yield multiple chunks if they "touch" the exclusive part of the range.
132                // This will result in LSP hints [re-]queried for more ranges, but also more hints already visible when scrolling around.
133                let chunk_range = chunk.start..=chunk.end;
134                row_ranges.iter().any(|row_range| {
135                    chunk_range.contains(&row_range.start())
136                        || chunk_range.contains(&row_range.end())
137                })
138            })
139            .copied()
140    }
141
142    pub fn cached_hints(&mut self, chunk: &BufferChunk) -> Option<&CacheInlayHints> {
143        self.hints_by_chunks[chunk.id].as_ref()
144    }
145
146    pub fn fetched_hints(&mut self, chunk: &BufferChunk) -> &mut Option<CacheInlayHintsTask> {
147        &mut self.fetches_by_chunks[chunk.id]
148    }
149
150    #[cfg(any(test, feature = "test-support"))]
151    pub fn all_cached_hints(&self) -> Vec<InlayHint> {
152        self.hints_by_chunks
153            .iter()
154            .filter_map(|hints| hints.as_ref())
155            .flat_map(|hints| hints.values().cloned())
156            .flatten()
157            .map(|(_, hint)| hint)
158            .collect()
159    }
160
161    #[cfg(any(test, feature = "test-support"))]
162    pub fn all_fetched_hints(&self) -> Vec<CacheInlayHintsTask> {
163        self.fetches_by_chunks
164            .iter()
165            .filter_map(|fetches| fetches.clone())
166            .collect()
167    }
168
169    pub fn remove_server_data(&mut self, for_server: LanguageServerId) {
170        for (chunk_index, hints) in self.hints_by_chunks.iter_mut().enumerate() {
171            if let Some(hints) = hints {
172                if hints.remove(&for_server).is_some() {
173                    self.fetches_by_chunks[chunk_index] = None;
174                }
175            }
176        }
177    }
178
179    pub fn clear(&mut self) {
180        self.hints_by_chunks = vec![None; self.buffer_chunks.len()];
181        self.fetches_by_chunks = vec![None; self.buffer_chunks.len()];
182        self.hints_by_id.clear();
183        self.hint_resolves.clear();
184        self.latest_invalidation_requests.clear();
185    }
186
187    pub fn insert_new_hints(
188        &mut self,
189        chunk: BufferChunk,
190        server_id: LanguageServerId,
191        new_hints: Vec<(InlayId, InlayHint)>,
192    ) {
193        let existing_hints = self.hints_by_chunks[chunk.id]
194            .get_or_insert_default()
195            .entry(server_id)
196            .or_insert_with(Vec::new);
197        let existing_count = existing_hints.len();
198        existing_hints.extend(new_hints.into_iter().enumerate().filter_map(
199            |(i, (id, new_hint))| {
200                let new_hint_for_id = HintForId {
201                    chunk_id: chunk.id,
202                    server_id,
203                    position: existing_count + i,
204                };
205                if let hash_map::Entry::Vacant(vacant_entry) = self.hints_by_id.entry(id) {
206                    vacant_entry.insert(new_hint_for_id);
207                    Some((id, new_hint))
208                } else {
209                    None
210                }
211            },
212        ));
213        *self.fetched_hints(&chunk) = None;
214    }
215
216    pub fn hint_for_id(&mut self, id: InlayId) -> Option<&mut InlayHint> {
217        let hint_for_id = self.hints_by_id.get(&id)?;
218        let (hint_id, hint) = self
219            .hints_by_chunks
220            .get_mut(hint_for_id.chunk_id)?
221            .as_mut()?
222            .get_mut(&hint_for_id.server_id)?
223            .get_mut(hint_for_id.position)?;
224        debug_assert_eq!(*hint_id, id, "Invalid pointer {hint_for_id:?}");
225        Some(hint)
226    }
227
228    pub fn buffer_chunks_len(&self) -> usize {
229        self.buffer_chunks.len()
230    }
231
232    pub(crate) fn invalidate_for_server_refresh(
233        &mut self,
234        for_server: LanguageServerId,
235        request_id: Option<usize>,
236    ) -> bool {
237        match self.latest_invalidation_requests.entry(for_server) {
238            hash_map::Entry::Occupied(mut o) => {
239                if request_id > *o.get() {
240                    o.insert(request_id);
241                } else {
242                    return false;
243                }
244            }
245            hash_map::Entry::Vacant(v) => {
246                v.insert(request_id);
247            }
248        }
249
250        for (chunk_id, chunk_data) in self.hints_by_chunks.iter_mut().enumerate() {
251            if let Some(removed_hints) = chunk_data
252                .as_mut()
253                .and_then(|chunk_data| chunk_data.remove(&for_server))
254            {
255                for (id, _) in removed_hints {
256                    self.hints_by_id.remove(&id);
257                    self.hint_resolves.remove(&id);
258                }
259                self.fetches_by_chunks[chunk_id] = None;
260            }
261        }
262
263        true
264    }
265
266    pub(crate) fn invalidate_for_chunk(&mut self, chunk: BufferChunk) {
267        self.fetches_by_chunks[chunk.id] = None;
268        if let Some(hints_by_server) = self.hints_by_chunks[chunk.id].take() {
269            for (hint_id, _) in hints_by_server.into_values().flatten() {
270                self.hints_by_id.remove(&hint_id);
271                self.hint_resolves.remove(&hint_id);
272            }
273        }
274    }
275}