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}