Fix racy inlay hints queries (#41816)

Follow-up of https://github.com/zed-industries/zed/pull/40183

Release Notes:

- (Preview only) Fixed inlay hints duplicating when multiple editors are
open for the same buffer

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>
This commit is contained in:
Kirill Bulatov
2025-11-03 15:54:53 +02:00
parent 713903fe76
commit af2d462bf7
9 changed files with 447 additions and 229 deletions

View File

@@ -37,6 +37,7 @@ use std::{
Arc,
atomic::{self, AtomicBool, AtomicUsize},
},
time::Duration,
};
use text::Point;
use util::{path, rel_path::rel_path, uri};
@@ -1813,14 +1814,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
settings.project.all_languages.defaults.inlay_hints =
Some(InlayHintSettingsContent {
enabled: Some(true),
show_value_hints: Some(true),
edit_debounce_ms: Some(0),
scroll_debounce_ms: Some(0),
show_type_hints: Some(true),
show_parameter_hints: Some(false),
show_other_hints: Some(true),
show_background: Some(false),
toggle_on_modifiers_press: None,
..InlayHintSettingsContent::default()
})
});
});
@@ -1830,15 +1824,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.inlay_hints =
Some(InlayHintSettingsContent {
show_value_hints: Some(true),
enabled: Some(true),
edit_debounce_ms: Some(0),
scroll_debounce_ms: Some(0),
show_type_hints: Some(true),
show_parameter_hints: Some(false),
show_other_hints: Some(true),
show_background: Some(false),
toggle_on_modifiers_press: None,
..InlayHintSettingsContent::default()
})
});
});
@@ -1931,6 +1918,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
});
let fake_language_server = fake_language_servers.next().await.unwrap();
let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
executor.advance_clock(Duration::from_millis(100));
executor.run_until_parked();
let initial_edit = edits_made.load(atomic::Ordering::Acquire);
@@ -1951,6 +1939,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.downcast::<Editor>()
.unwrap();
executor.advance_clock(Duration::from_millis(100));
executor.run_until_parked();
editor_b.update(cx_b, |editor, cx| {
assert_eq!(
@@ -1969,6 +1958,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
});
cx_b.focus(&editor_b);
executor.advance_clock(Duration::from_secs(1));
executor.run_until_parked();
editor_a.update(cx_a, |editor, cx| {
assert_eq!(
@@ -1992,6 +1982,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
});
cx_a.focus(&editor_a);
executor.advance_clock(Duration::from_secs(1));
executor.run_until_parked();
editor_a.update(cx_a, |editor, cx| {
assert_eq!(
@@ -2013,6 +2004,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.into_response()
.expect("inlay refresh request failed");
executor.advance_clock(Duration::from_secs(1));
executor.run_until_parked();
editor_a.update(cx_a, |editor, cx| {
assert_eq!(

View File

@@ -1833,9 +1833,15 @@ impl Editor {
project::Event::RefreshCodeLens => {
// we always query lens with actions, without storing them, always refreshing them
}
project::Event::RefreshInlayHints(server_id) => {
project::Event::RefreshInlayHints {
server_id,
request_id,
} => {
editor.refresh_inlay_hints(
InlayHintRefreshReason::RefreshRequested(*server_id),
InlayHintRefreshReason::RefreshRequested {
server_id: *server_id,
request_id: *request_id,
},
cx,
);
}

View File

@@ -1,5 +1,4 @@
use std::{
collections::hash_map,
ops::{ControlFlow, Range},
time::Duration,
};
@@ -49,8 +48,8 @@ pub struct LspInlayHintData {
allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
invalidate_debounce: Option<Duration>,
append_debounce: Option<Duration>,
hint_refresh_tasks: HashMap<BufferId, HashMap<Vec<Range<BufferRow>>, Vec<Task<()>>>>,
hint_chunk_fetched: HashMap<BufferId, (Global, HashSet<Range<BufferRow>>)>,
hint_refresh_tasks: HashMap<BufferId, Vec<Task<()>>>,
hint_chunk_fetching: HashMap<BufferId, (Global, HashSet<Range<BufferRow>>)>,
invalidate_hints_for_buffers: HashSet<BufferId>,
pub added_hints: HashMap<InlayId, Option<InlayHintKind>>,
}
@@ -63,7 +62,7 @@ impl LspInlayHintData {
enabled_in_settings: settings.enabled,
hint_refresh_tasks: HashMap::default(),
added_hints: HashMap::default(),
hint_chunk_fetched: HashMap::default(),
hint_chunk_fetching: HashMap::default(),
invalidate_hints_for_buffers: HashSet::default(),
invalidate_debounce: debounce_value(settings.edit_debounce_ms),
append_debounce: debounce_value(settings.scroll_debounce_ms),
@@ -99,9 +98,8 @@ impl LspInlayHintData {
pub fn clear(&mut self) {
self.hint_refresh_tasks.clear();
self.hint_chunk_fetched.clear();
self.hint_chunk_fetching.clear();
self.added_hints.clear();
self.invalidate_hints_for_buffers.clear();
}
/// Checks inlay hint settings for enabled hint kinds and general enabled state.
@@ -199,7 +197,7 @@ impl LspInlayHintData {
) {
for buffer_id in removed_buffer_ids {
self.hint_refresh_tasks.remove(buffer_id);
self.hint_chunk_fetched.remove(buffer_id);
self.hint_chunk_fetching.remove(buffer_id);
}
}
}
@@ -211,7 +209,10 @@ pub enum InlayHintRefreshReason {
SettingsChange(InlayHintSettings),
NewLinesShown,
BufferEdited(BufferId),
RefreshRequested(LanguageServerId),
RefreshRequested {
server_id: LanguageServerId,
request_id: Option<usize>,
},
ExcerptsRemoved(Vec<ExcerptId>),
}
@@ -296,7 +297,7 @@ impl Editor {
| InlayHintRefreshReason::Toggle(_)
| InlayHintRefreshReason::SettingsChange(_) => true,
InlayHintRefreshReason::NewLinesShown
| InlayHintRefreshReason::RefreshRequested(_)
| InlayHintRefreshReason::RefreshRequested { .. }
| InlayHintRefreshReason::ExcerptsRemoved(_) => false,
InlayHintRefreshReason::BufferEdited(buffer_id) => {
let Some(affected_language) = self
@@ -370,48 +371,45 @@ impl Editor {
let Some(buffer) = multi_buffer.read(cx).buffer(buffer_id) else {
continue;
};
let fetched_tasks = inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default();
let (fetched_for_version, fetched_chunks) = inlay_hints
.hint_chunk_fetching
.entry(buffer_id)
.or_default();
if visible_excerpts
.buffer_version
.changed_since(&fetched_tasks.0)
.changed_since(fetched_for_version)
{
fetched_tasks.1.clear();
fetched_tasks.0 = visible_excerpts.buffer_version.clone();
*fetched_for_version = visible_excerpts.buffer_version.clone();
fetched_chunks.clear();
inlay_hints.hint_refresh_tasks.remove(&buffer_id);
}
let applicable_chunks =
semantics_provider.applicable_inlay_chunks(&buffer, &visible_excerpts.ranges, cx);
let known_chunks = if ignore_previous_fetches {
None
} else {
Some((fetched_for_version.clone(), fetched_chunks.clone()))
};
match inlay_hints
let mut applicable_chunks =
semantics_provider.applicable_inlay_chunks(&buffer, &visible_excerpts.ranges, cx);
applicable_chunks.retain(|chunk| fetched_chunks.insert(chunk.clone()));
if applicable_chunks.is_empty() && !ignore_previous_fetches {
continue;
}
inlay_hints
.hint_refresh_tasks
.entry(buffer_id)
.or_default()
.entry(applicable_chunks)
{
hash_map::Entry::Occupied(mut o) => {
if invalidate_cache.should_invalidate() || ignore_previous_fetches {
o.get_mut().push(spawn_editor_hints_refresh(
buffer_id,
invalidate_cache,
ignore_previous_fetches,
debounce,
visible_excerpts,
cx,
));
}
}
hash_map::Entry::Vacant(v) => {
v.insert(Vec::new()).push(spawn_editor_hints_refresh(
buffer_id,
invalidate_cache,
ignore_previous_fetches,
debounce,
visible_excerpts,
cx,
));
}
}
.push(spawn_editor_hints_refresh(
buffer_id,
invalidate_cache,
debounce,
visible_excerpts,
known_chunks,
applicable_chunks,
cx,
));
}
}
@@ -506,9 +504,13 @@ impl Editor {
}
InlayHintRefreshReason::NewLinesShown => InvalidationStrategy::None,
InlayHintRefreshReason::BufferEdited(_) => InvalidationStrategy::BufferEdited,
InlayHintRefreshReason::RefreshRequested(server_id) => {
InvalidationStrategy::RefreshRequested(*server_id)
}
InlayHintRefreshReason::RefreshRequested {
server_id,
request_id,
} => InvalidationStrategy::RefreshRequested {
server_id: *server_id,
request_id: *request_id,
},
};
match &mut self.inlay_hints {
@@ -718,44 +720,29 @@ impl Editor {
fn inlay_hints_for_buffer(
&mut self,
invalidate_cache: InvalidationStrategy,
ignore_previous_fetches: bool,
buffer_excerpts: VisibleExcerpts,
known_chunks: Option<(Global, HashSet<Range<BufferRow>>)>,
cx: &mut Context<Self>,
) -> Option<Vec<Task<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>>> {
let semantics_provider = self.semantics_provider()?;
let inlay_hints = self.inlay_hints.as_mut()?;
let buffer_id = buffer_excerpts.buffer.read(cx).remote_id();
let new_hint_tasks = semantics_provider
.inlay_hints(
invalidate_cache,
buffer_excerpts.buffer,
buffer_excerpts.ranges,
inlay_hints
.hint_chunk_fetched
.get(&buffer_id)
.filter(|_| !ignore_previous_fetches && !invalidate_cache.should_invalidate())
.cloned(),
known_chunks,
cx,
)
.unwrap_or_default();
let (known_version, known_chunks) =
inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default();
if buffer_excerpts.buffer_version.changed_since(known_version) {
known_chunks.clear();
*known_version = buffer_excerpts.buffer_version;
}
let mut hint_tasks = Vec::new();
let mut hint_tasks = None;
for (row_range, new_hints_task) in new_hint_tasks {
let inserted = known_chunks.insert(row_range.clone());
if inserted || ignore_previous_fetches || invalidate_cache.should_invalidate() {
hint_tasks.push(cx.spawn(async move |_, _| (row_range, new_hints_task.await)));
}
hint_tasks
.get_or_insert_with(Vec::new)
.push(cx.spawn(async move |_, _| (row_range, new_hints_task.await)));
}
Some(hint_tasks)
hint_tasks
}
fn apply_fetched_hints(
@@ -793,20 +780,28 @@ impl Editor {
let excerpts = self.buffer.read(cx).excerpt_ids();
let hints_to_insert = new_hints
.into_iter()
.filter_map(|(chunk_range, hints_result)| match hints_result {
Ok(new_hints) => Some(new_hints),
Err(e) => {
log::error!(
"Failed to query inlays for buffer row range {chunk_range:?}, {e:#}"
);
if let Some((for_version, chunks_fetched)) =
inlay_hints.hint_chunk_fetched.get_mut(&buffer_id)
{
if for_version == &query_version {
chunks_fetched.remove(&chunk_range);
.filter_map(|(chunk_range, hints_result)| {
let chunks_fetched = inlay_hints.hint_chunk_fetching.get_mut(&buffer_id);
match hints_result {
Ok(new_hints) => {
if new_hints.is_empty() {
if let Some((_, chunks_fetched)) = chunks_fetched {
chunks_fetched.remove(&chunk_range);
}
}
Some(new_hints)
}
Err(e) => {
log::error!(
"Failed to query inlays for buffer row range {chunk_range:?}, {e:#}"
);
if let Some((for_version, chunks_fetched)) = chunks_fetched {
if for_version == &query_version {
chunks_fetched.remove(&chunk_range);
}
}
None
}
None
}
})
.flat_map(|hints| hints.into_values())
@@ -856,9 +851,10 @@ struct VisibleExcerpts {
fn spawn_editor_hints_refresh(
buffer_id: BufferId,
invalidate_cache: InvalidationStrategy,
ignore_previous_fetches: bool,
debounce: Option<Duration>,
buffer_excerpts: VisibleExcerpts,
known_chunks: Option<(Global, HashSet<Range<BufferRow>>)>,
applicable_chunks: Vec<Range<BufferRow>>,
cx: &mut Context<'_, Editor>,
) -> Task<()> {
cx.spawn(async move |editor, cx| {
@@ -869,12 +865,7 @@ fn spawn_editor_hints_refresh(
let query_version = buffer_excerpts.buffer_version.clone();
let Some(hint_tasks) = editor
.update(cx, |editor, cx| {
editor.inlay_hints_for_buffer(
invalidate_cache,
ignore_previous_fetches,
buffer_excerpts,
cx,
)
editor.inlay_hints_for_buffer(invalidate_cache, buffer_excerpts, known_chunks, cx)
})
.ok()
else {
@@ -882,6 +873,19 @@ fn spawn_editor_hints_refresh(
};
let hint_tasks = hint_tasks.unwrap_or_default();
if hint_tasks.is_empty() {
editor
.update(cx, |editor, _| {
if let Some((_, hint_chunk_fetching)) = editor
.inlay_hints
.as_mut()
.and_then(|inlay_hints| inlay_hints.hint_chunk_fetching.get_mut(&buffer_id))
{
for applicable_chunks in &applicable_chunks {
hint_chunk_fetching.remove(applicable_chunks);
}
}
})
.ok();
return;
}
let new_hints = join_all(hint_tasks).await;
@@ -1102,7 +1106,10 @@ pub mod tests {
editor
.update(cx, |editor, _window, cx| {
editor.refresh_inlay_hints(
InlayHintRefreshReason::RefreshRequested(fake_server.server.server_id()),
InlayHintRefreshReason::RefreshRequested {
server_id: fake_server.server.server_id(),
request_id: Some(1),
},
cx,
);
})
@@ -1958,15 +1965,8 @@ pub mod tests {
async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
show_value_hints: Some(true),
enabled: Some(true),
edit_debounce_ms: Some(0),
scroll_debounce_ms: Some(0),
show_type_hints: Some(true),
show_parameter_hints: Some(true),
show_other_hints: Some(true),
show_background: Some(false),
toggle_on_modifiers_press: None,
..InlayHintSettingsContent::default()
})
});
@@ -2044,6 +2044,7 @@ pub mod tests {
cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
cx.executor().run_until_parked();
let _fake_server = fake_servers.next().await.unwrap();
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
let ranges = lsp_request_ranges
@@ -2129,6 +2130,7 @@ pub mod tests {
);
})
.unwrap();
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
editor.update(cx, |_, _, _| {
let ranges = lsp_request_ranges
@@ -2145,6 +2147,7 @@ pub mod tests {
editor.handle_input("++++more text++++", window, cx);
})
.unwrap();
cx.executor().advance_clock(Duration::from_secs(1));
cx.executor().run_until_parked();
editor.update(cx, |editor, _window, cx| {
let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
@@ -3887,7 +3890,10 @@ let c = 3;"#
editor
.update(cx, |editor, _, cx| {
editor.refresh_inlay_hints(
InlayHintRefreshReason::RefreshRequested(fake_server.server.server_id()),
InlayHintRefreshReason::RefreshRequested {
server_id: fake_server.server.server_id(),
request_id: Some(1),
},
cx,
);
})
@@ -4022,7 +4028,7 @@ let c = 3;"#
let mut all_fetched_hints = Vec::new();
for buffer in editor.buffer.read(cx).all_buffers() {
lsp_store.update(cx, |lsp_store, cx| {
let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
let hints = lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
let mut label = hint.text().to_string();
if hint.padding_left {

View File

@@ -804,23 +804,32 @@ impl LocalLspStore {
language_server
.on_request::<lsp::request::InlayHintRefreshRequest, _, _>({
let lsp_store = lsp_store.clone();
let request_id = Arc::new(AtomicUsize::new(0));
move |(), cx| {
let this = lsp_store.clone();
let lsp_store = lsp_store.clone();
let request_id = request_id.clone();
let mut cx = cx.clone();
async move {
this.update(&mut cx, |lsp_store, cx| {
cx.emit(LspStoreEvent::RefreshInlayHints(server_id));
lsp_store
.downstream_client
.as_ref()
.map(|(client, project_id)| {
client.send(proto::RefreshInlayHints {
project_id: *project_id,
server_id: server_id.to_proto(),
lsp_store
.update(&mut cx, |lsp_store, cx| {
let request_id =
Some(request_id.fetch_add(1, atomic::Ordering::AcqRel));
cx.emit(LspStoreEvent::RefreshInlayHints {
server_id,
request_id,
});
lsp_store
.downstream_client
.as_ref()
.map(|(client, project_id)| {
client.send(proto::RefreshInlayHints {
project_id: *project_id,
server_id: server_id.to_proto(),
request_id: request_id.map(|id| id as u64),
})
})
})
})?
.transpose()?;
})?
.transpose()?;
Ok(())
}
}
@@ -3610,7 +3619,10 @@ pub enum LspStoreEvent {
new_language: Option<Arc<Language>>,
},
Notification(String),
RefreshInlayHints(LanguageServerId),
RefreshInlayHints {
server_id: LanguageServerId,
request_id: Option<usize>,
},
RefreshCodeLens,
DiagnosticsUpdated {
server_id: LanguageServerId,
@@ -6583,14 +6595,22 @@ impl LspStore {
cx: &mut Context<Self>,
) -> HashMap<Range<BufferRow>, Task<Result<CacheInlayHints>>> {
let buffer_snapshot = buffer.read(cx).snapshot();
let for_server = if let InvalidationStrategy::RefreshRequested(server_id) = invalidate {
let next_hint_id = self.next_hint_id.clone();
let lsp_data = self.latest_lsp_data(&buffer, cx);
let mut lsp_refresh_requested = false;
let for_server = if let InvalidationStrategy::RefreshRequested {
server_id,
request_id,
} = invalidate
{
let invalidated = lsp_data
.inlay_hints
.invalidate_for_server_refresh(server_id, request_id);
lsp_refresh_requested = invalidated;
Some(server_id)
} else {
None
};
let invalidate_cache = invalidate.should_invalidate();
let next_hint_id = self.next_hint_id.clone();
let lsp_data = self.latest_lsp_data(&buffer, cx);
let existing_inlay_hints = &mut lsp_data.inlay_hints;
let known_chunks = known_chunks
.filter(|(known_version, _)| !lsp_data.buffer_version.changed_since(known_version))
@@ -6598,8 +6618,8 @@ impl LspStore {
.unwrap_or_default();
let mut hint_fetch_tasks = Vec::new();
let mut cached_inlay_hints = HashMap::default();
let mut ranges_to_query = Vec::new();
let mut cached_inlay_hints = None;
let mut ranges_to_query = None;
let applicable_chunks = existing_inlay_hints
.applicable_chunks(ranges.as_slice())
.filter(|chunk| !known_chunks.contains(&(chunk.start..chunk.end)))
@@ -6614,12 +6634,12 @@ impl LspStore {
match (
existing_inlay_hints
.cached_hints(&row_chunk)
.filter(|_| !invalidate_cache)
.filter(|_| !lsp_refresh_requested)
.cloned(),
existing_inlay_hints
.fetched_hints(&row_chunk)
.as_ref()
.filter(|_| !invalidate_cache)
.filter(|_| !lsp_refresh_requested)
.cloned(),
) {
(None, None) => {
@@ -6628,19 +6648,18 @@ impl LspStore {
} else {
Point::new(row_chunk.end, 0)
};
ranges_to_query.push((
ranges_to_query.get_or_insert_with(Vec::new).push((
row_chunk,
buffer_snapshot.anchor_before(Point::new(row_chunk.start, 0))
..buffer_snapshot.anchor_after(end),
));
}
(None, Some(fetched_hints)) => {
hint_fetch_tasks.push((row_chunk, fetched_hints.clone()))
}
(None, Some(fetched_hints)) => hint_fetch_tasks.push((row_chunk, fetched_hints)),
(Some(cached_hints), None) => {
for (server_id, cached_hints) in cached_hints {
if for_server.is_none_or(|for_server| for_server == server_id) {
cached_inlay_hints
.get_or_insert_with(HashMap::default)
.entry(row_chunk.start..row_chunk.end)
.or_insert_with(HashMap::default)
.entry(server_id)
@@ -6650,10 +6669,11 @@ impl LspStore {
}
}
(Some(cached_hints), Some(fetched_hints)) => {
hint_fetch_tasks.push((row_chunk, fetched_hints.clone()));
hint_fetch_tasks.push((row_chunk, fetched_hints));
for (server_id, cached_hints) in cached_hints {
if for_server.is_none_or(|for_server| for_server == server_id) {
cached_inlay_hints
.get_or_insert_with(HashMap::default)
.entry(row_chunk.start..row_chunk.end)
.or_insert_with(HashMap::default)
.entry(server_id)
@@ -6665,18 +6685,18 @@ impl LspStore {
}
}
let cached_chunk_data = cached_inlay_hints
.into_iter()
.map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints))))
.collect();
if hint_fetch_tasks.is_empty() && ranges_to_query.is_empty() {
cached_chunk_data
if hint_fetch_tasks.is_empty()
&& ranges_to_query
.as_ref()
.is_none_or(|ranges| ranges.is_empty())
&& let Some(cached_inlay_hints) = cached_inlay_hints
{
cached_inlay_hints
.into_iter()
.map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints))))
.collect()
} else {
if invalidate_cache {
lsp_data.inlay_hints.clear();
}
for (chunk, range_to_query) in ranges_to_query {
for (chunk, range_to_query) in ranges_to_query.into_iter().flatten() {
let next_hint_id = next_hint_id.clone();
let buffer = buffer.clone();
let new_inlay_hints = cx
@@ -6692,31 +6712,38 @@ impl LspStore {
let update_cache = !lsp_data
.buffer_version
.changed_since(&buffer.read(cx).version());
new_hints_by_server
.into_iter()
.map(|(server_id, new_hints)| {
let new_hints = new_hints
.into_iter()
.map(|new_hint| {
(
InlayId::Hint(next_hint_id.fetch_add(
1,
atomic::Ordering::AcqRel,
)),
new_hint,
)
})
.collect::<Vec<_>>();
if update_cache {
lsp_data.inlay_hints.insert_new_hints(
chunk,
server_id,
new_hints.clone(),
);
}
(server_id, new_hints)
})
.collect()
if new_hints_by_server.is_empty() {
if update_cache {
lsp_data.inlay_hints.invalidate_for_chunk(chunk);
}
HashMap::default()
} else {
new_hints_by_server
.into_iter()
.map(|(server_id, new_hints)| {
let new_hints = new_hints
.into_iter()
.map(|new_hint| {
(
InlayId::Hint(next_hint_id.fetch_add(
1,
atomic::Ordering::AcqRel,
)),
new_hint,
)
})
.collect::<Vec<_>>();
if update_cache {
lsp_data.inlay_hints.insert_new_hints(
chunk,
server_id,
new_hints.clone(),
);
}
(server_id, new_hints)
})
.collect()
}
})
})
.map_err(Arc::new)
@@ -6728,22 +6755,25 @@ impl LspStore {
hint_fetch_tasks.push((chunk, new_inlay_hints));
}
let mut combined_data = cached_chunk_data;
combined_data.extend(hint_fetch_tasks.into_iter().map(|(chunk, hints_fetch)| {
(
chunk.start..chunk.end,
cx.spawn(async move |_, _| {
hints_fetch.await.map_err(|e| {
if e.error_code() != ErrorCode::Internal {
anyhow!(e.error_code())
} else {
anyhow!("{e:#}")
}
})
}),
)
}));
combined_data
cached_inlay_hints
.unwrap_or_default()
.into_iter()
.map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints))))
.chain(hint_fetch_tasks.into_iter().map(|(chunk, hints_fetch)| {
(
chunk.start..chunk.end,
cx.spawn(async move |_, _| {
hints_fetch.await.map_err(|e| {
if e.error_code() != ErrorCode::Internal {
anyhow!(e.error_code())
} else {
anyhow!("{e:#}")
}
})
}),
)
}))
.collect()
}
}
@@ -9542,7 +9572,10 @@ impl LspStore {
if let Some(work) = status.pending_work.remove(&token)
&& !work.is_disk_based_diagnostics_progress
{
cx.emit(LspStoreEvent::RefreshInlayHints(language_server_id));
cx.emit(LspStoreEvent::RefreshInlayHints {
server_id: language_server_id,
request_id: None,
});
}
cx.notify();
}
@@ -9679,9 +9712,10 @@ impl LspStore {
mut cx: AsyncApp,
) -> Result<proto::Ack> {
lsp_store.update(&mut cx, |_, cx| {
cx.emit(LspStoreEvent::RefreshInlayHints(
LanguageServerId::from_proto(envelope.payload.server_id),
));
cx.emit(LspStoreEvent::RefreshInlayHints {
server_id: LanguageServerId::from_proto(envelope.payload.server_id),
request_id: envelope.payload.request_id.map(|id| id as usize),
});
})?;
Ok(proto::Ack {})
}
@@ -10900,7 +10934,6 @@ impl LspStore {
language_server.name(),
Some(key.worktree_id),
));
cx.emit(LspStoreEvent::RefreshInlayHints(server_id));
let server_capabilities = language_server.capabilities();
if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() {

View File

@@ -19,7 +19,10 @@ pub enum InvalidationStrategy {
/// 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(LanguageServerId),
RefreshRequested {
server_id: LanguageServerId,
request_id: Option<usize>,
},
/// 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,
@@ -36,7 +39,7 @@ impl InvalidationStrategy {
pub fn should_invalidate(&self) -> bool {
matches!(
self,
InvalidationStrategy::RefreshRequested(_) | InvalidationStrategy::BufferEdited
InvalidationStrategy::RefreshRequested { .. } | InvalidationStrategy::BufferEdited
)
}
}
@@ -47,6 +50,7 @@ pub struct BufferInlayHints {
hints_by_chunks: Vec<Option<CacheInlayHints>>,
fetches_by_chunks: Vec<Option<CacheInlayHintsTask>>,
hints_by_id: HashMap<InlayId, HintForId>,
latest_invalidation_requests: HashMap<LanguageServerId, Option<usize>>,
pub(super) hint_resolves: HashMap<InlayId, Shared<Task<()>>>,
}
@@ -104,6 +108,7 @@ impl BufferInlayHints {
Self {
hints_by_chunks: vec![None; buffer_chunks.len()],
fetches_by_chunks: vec![None; buffer_chunks.len()],
latest_invalidation_requests: HashMap::default(),
hints_by_id: HashMap::default(),
hint_resolves: HashMap::default(),
snapshot,
@@ -176,6 +181,7 @@ impl BufferInlayHints {
self.fetches_by_chunks = vec![None; self.buffer_chunks.len()];
self.hints_by_id.clear();
self.hint_resolves.clear();
self.latest_invalidation_requests.clear();
}
pub fn insert_new_hints(
@@ -222,4 +228,48 @@ impl BufferInlayHints {
pub fn buffer_chunks_len(&self) -> usize {
self.buffer_chunks.len()
}
pub(crate) fn invalidate_for_server_refresh(
&mut self,
for_server: LanguageServerId,
request_id: Option<usize>,
) -> bool {
match self.latest_invalidation_requests.entry(for_server) {
hash_map::Entry::Occupied(mut o) => {
if request_id > *o.get() {
o.insert(request_id);
} else {
return false;
}
}
hash_map::Entry::Vacant(v) => {
v.insert(request_id);
}
}
for (chunk_id, chunk_data) in self.hints_by_chunks.iter_mut().enumerate() {
if let Some(removed_hints) = chunk_data
.as_mut()
.and_then(|chunk_data| chunk_data.remove(&for_server))
{
for (id, _) in removed_hints {
self.hints_by_id.remove(&id);
self.hint_resolves.remove(&id);
}
self.fetches_by_chunks[chunk_id] = None;
}
}
true
}
pub(crate) fn invalidate_for_chunk(&mut self, chunk: BufferChunk) {
self.fetches_by_chunks[chunk.id] = None;
if let Some(hints_by_server) = self.hints_by_chunks[chunk.id].take() {
for (hint_id, _) in hints_by_server.into_values().flatten() {
self.hints_by_id.remove(&hint_id);
self.hint_resolves.remove(&hint_id);
}
}
}
}

View File

@@ -337,7 +337,10 @@ pub enum Event {
HostReshared,
Reshared,
Rejoined,
RefreshInlayHints(LanguageServerId),
RefreshInlayHints {
server_id: LanguageServerId,
request_id: Option<usize>,
},
RefreshCodeLens,
RevealInProjectPanel(ProjectEntryId),
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
@@ -3074,9 +3077,13 @@ impl Project {
return;
};
}
LspStoreEvent::RefreshInlayHints(server_id) => {
cx.emit(Event::RefreshInlayHints(*server_id))
}
LspStoreEvent::RefreshInlayHints {
server_id,
request_id,
} => cx.emit(Event::RefreshInlayHints {
server_id: *server_id,
request_id: *request_id,
}),
LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens),
LspStoreEvent::LanguageServerPrompt(prompt) => {
cx.emit(Event::LanguageServerPrompt(prompt.clone()))

View File

@@ -1815,10 +1815,6 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
fake_server
.start_progress(format!("{}/0", progress_token))
.await;
assert_eq!(
events.next().await.unwrap(),
Event::RefreshInlayHints(fake_server.server.server_id())
);
assert_eq!(
events.next().await.unwrap(),
Event::DiskBasedDiagnosticsStarted {
@@ -1957,10 +1953,6 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
Some(worktree_id)
)
);
assert_eq!(
events.next().await.unwrap(),
Event::RefreshInlayHints(fake_server.server.server_id())
);
fake_server.start_progress(progress_token).await;
assert_eq!(
events.next().await.unwrap(),

View File

@@ -466,6 +466,7 @@ message ResolveInlayHintResponse {
message RefreshInlayHints {
uint64 project_id = 1;
uint64 server_id = 2;
optional uint64 request_id = 3;
}
message CodeLens {

View File

@@ -2338,7 +2338,15 @@ pub fn perform_project_search(
#[cfg(test)]
pub mod tests {
use std::{ops::Deref as _, sync::Arc, time::Duration};
use std::{
ops::Deref as _,
path::PathBuf,
sync::{
Arc,
atomic::{self, AtomicUsize},
},
time::Duration,
};
use super::*;
use editor::{DisplayPoint, display_map::DisplayRow};
@@ -4239,6 +4247,8 @@ pub mod tests {
)
.await;
let requests_count = Arc::new(AtomicUsize::new(0));
let closure_requests_count = requests_count.clone();
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
let language = rust_lang();
@@ -4250,21 +4260,26 @@ pub mod tests {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
initializer: Some(Box::new(|fake_server| {
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
move |_, _| async move {
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, 17),
label: lsp::InlayHintLabel::String(": i32".to_owned()),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
},
);
initializer: Some(Box::new(move |fake_server| {
let requests_count = closure_requests_count.clone();
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>({
move |_, _| {
let requests_count = requests_count.clone();
async move {
requests_count.fetch_add(1, atomic::Ordering::Release);
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, 17),
label: lsp::InlayHintLabel::String(": i32".to_owned()),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
}
}
});
})),
..FakeLspAdapter::default()
},
@@ -4278,7 +4293,7 @@ pub mod tests {
});
perform_search(search_view, "let ", cx);
let _fake_server = fake_servers.next().await.unwrap();
let fake_server = fake_servers.next().await.unwrap();
cx.executor().advance_clock(Duration::from_secs(1));
cx.executor().run_until_parked();
search_view
@@ -4291,11 +4306,127 @@ pub mod tests {
);
})
.unwrap();
assert_eq!(
requests_count.load(atomic::Ordering::Acquire),
1,
"New hints should have been queried",
);
// Can do the 2nd search without any panics
perform_search(search_view, "let ", cx);
cx.executor().advance_clock(Duration::from_secs(1));
cx.executor().run_until_parked();
search_view
.update(cx, |search_view, _, cx| {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nfn main() { let a: i32 = 2; }\n"
);
})
.unwrap();
assert_eq!(
requests_count.load(atomic::Ordering::Acquire),
2,
"We did drop the previous buffer when cleared the old project search results, hence another query was made",
);
let singleton_editor = window
.update(cx, |workspace, window, cx| {
workspace.open_abs_path(
PathBuf::from(path!("/dir/main.rs")),
workspace::OpenOptions::default(),
window,
cx,
)
})
.unwrap()
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
singleton_editor.update(cx, |editor, cx| {
assert_eq!(
editor.display_text(cx),
"fn main() { let a: i32 = 2; }\n",
"Newly opened editor should have the correct text with hints",
);
});
assert_eq!(
requests_count.load(atomic::Ordering::Acquire),
2,
"Opening the same buffer again should reuse the cached hints",
);
window
.update(cx, |_, window, cx| {
singleton_editor.update(cx, |editor, cx| {
editor.handle_input("test", window, cx);
});
})
.unwrap();
cx.executor().advance_clock(Duration::from_secs(1));
cx.executor().run_until_parked();
singleton_editor.update(cx, |editor, cx| {
assert_eq!(
editor.display_text(cx),
"testfn main() { l: i32et a = 2; }\n",
"Newly opened editor should have the correct text with hints",
);
});
assert_eq!(
requests_count.load(atomic::Ordering::Acquire),
3,
"We have edited the buffer and should send a new request",
);
window
.update(cx, |_, window, cx| {
singleton_editor.update(cx, |editor, cx| {
editor.undo(&editor::actions::Undo, window, cx);
});
})
.unwrap();
cx.executor().advance_clock(Duration::from_secs(1));
cx.executor().run_until_parked();
assert_eq!(
requests_count.load(atomic::Ordering::Acquire),
4,
"We have edited the buffer again and should send a new request again",
);
singleton_editor.update(cx, |editor, cx| {
assert_eq!(
editor.display_text(cx),
"fn main() { let a: i32 = 2; }\n",
"Newly opened editor should have the correct text with hints",
);
});
project.update(cx, |_, cx| {
cx.emit(project::Event::RefreshInlayHints {
server_id: fake_server.server.server_id(),
request_id: Some(1),
});
});
cx.executor().advance_clock(Duration::from_secs(1));
cx.executor().run_until_parked();
assert_eq!(
requests_count.load(atomic::Ordering::Acquire),
5,
"After a simulated server refresh request, we should have sent another request",
);
perform_search(search_view, "let ", cx);
cx.executor().advance_clock(Duration::from_secs(1));
cx.executor().run_until_parked();
assert_eq!(
requests_count.load(atomic::Ordering::Acquire),
5,
"New project search should reuse the cached hints",
);
search_view
.update(cx, |search_view, _, cx| {
assert_eq!(