From baaa44dfc19ec140effe75cf4700382b3fc2da4d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Jul 2026 18:23:19 +0300 Subject: [PATCH] Do not drop other servers' data on semantic token refresh --- crates/project/src/lsp_store/code_lens.rs | 2 +- .../project/src/lsp_store/document_colors.rs | 2 +- .../project/src/lsp_store/document_links.rs | 2 +- .../project/src/lsp_store/document_symbols.rs | 2 +- .../project/src/lsp_store/folding_ranges.rs | 2 +- .../project/src/lsp_store/semantic_tokens.rs | 31 +- .../tests/integration/dynamic_registration.rs | 875 ++++++++++++------ 7 files changed, 648 insertions(+), 268 deletions(-) diff --git a/crates/project/src/lsp_store/code_lens.rs b/crates/project/src/lsp_store/code_lens.rs index 36982102779..52c35e13e68 100644 --- a/crates/project/src/lsp_store/code_lens.rs +++ b/crates/project/src/lsp_store/code_lens.rs @@ -71,9 +71,9 @@ impl CodeLensData { self.lens.clear(); self.fetched_servers.clear(); self.resolving.clear(); + self.update = None; } } - self.update = None; } } diff --git a/crates/project/src/lsp_store/document_colors.rs b/crates/project/src/lsp_store/document_colors.rs index 70497361f22..e3465eba447 100644 --- a/crates/project/src/lsp_store/document_colors.rs +++ b/crates/project/src/lsp_store/document_colors.rs @@ -56,9 +56,9 @@ impl DocumentColorData { None => { self.colors.clear(); self.fetched_servers.clear(); + self.colors_update = None; } } - self.colors_update = None; } } diff --git a/crates/project/src/lsp_store/document_links.rs b/crates/project/src/lsp_store/document_links.rs index 22eca9cae0e..323b37604dc 100644 --- a/crates/project/src/lsp_store/document_links.rs +++ b/crates/project/src/lsp_store/document_links.rs @@ -66,9 +66,9 @@ impl DocumentLinksData { self.links.clear(); self.fetched_servers.clear(); self.link_resolves.clear(); + self.links_update = None; } } - self.links_update = None; } } diff --git a/crates/project/src/lsp_store/document_symbols.rs b/crates/project/src/lsp_store/document_symbols.rs index 20088da3211..adda847c47e 100644 --- a/crates/project/src/lsp_store/document_symbols.rs +++ b/crates/project/src/lsp_store/document_symbols.rs @@ -46,9 +46,9 @@ impl DocumentSymbolsData { None => { self.symbols.clear(); self.fetched_servers.clear(); + self.symbols_update = None; } } - self.symbols_update = None; } } diff --git a/crates/project/src/lsp_store/folding_ranges.rs b/crates/project/src/lsp_store/folding_ranges.rs index 72e7998e810..e1be0c3d57a 100644 --- a/crates/project/src/lsp_store/folding_ranges.rs +++ b/crates/project/src/lsp_store/folding_ranges.rs @@ -51,9 +51,9 @@ impl FoldingRangeData { None => { self.ranges.clear(); self.fetched_servers.clear(); + self.ranges_update = None; } } - self.ranges_update = None; } } diff --git a/crates/project/src/lsp_store/semantic_tokens.rs b/crates/project/src/lsp_store/semantic_tokens.rs index 6b5c40253ba..675ab29f76d 100644 --- a/crates/project/src/lsp_store/semantic_tokens.rs +++ b/crates/project/src/lsp_store/semantic_tokens.rs @@ -91,7 +91,8 @@ impl LspStore { let current_servers = self.relevant_server_ids_for_capability_check(&buffer, cx); let latest_lsp_data = self.latest_lsp_data(&buffer, cx); let semantic_tokens_data = latest_lsp_data.semantic_tokens.get_or_insert_default(); - let refreshed_servers = std::mem::take(&mut semantic_tokens_data.pending_refreshes); + let mut refreshed_servers = std::mem::take(&mut semantic_tokens_data.pending_refreshes); + refreshed_servers.retain(|server_id| current_servers.contains(server_id)); if !refreshed_servers.is_empty() { semantic_tokens_data.update = None; semantic_tokens_data.generation += 1; @@ -190,17 +191,39 @@ impl LspStore { .await, ) } else { - lsp_store.update(cx, |lsp_store, cx| { + let remaining_tokens = lsp_store.update(cx, |lsp_store, cx| { + let mut remaining_tokens = None; if let Some(current_lsp_data) = lsp_store.current_lsp_data(buffer.read(cx).remote_id()) && current_lsp_data.buffer_version == version_queried_for && let Some(semantic_tokens) = current_lsp_data.semantic_tokens.as_mut() && semantic_tokens.generation == query_generation { - semantic_tokens.evict_all(); + if for_server.is_none() || semantic_tokens.raw_tokens.servers.is_empty() + { + semantic_tokens.evict_all(); + } else { + // A targeted fetch that sent no requests (e.g. the server + // lost the capability or does not serve this buffer) must + // not drop the other servers' cached tokens. + let buffer_snapshot = + buffer.read_with(cx, |buffer, _| buffer.snapshot()); + remaining_tokens = + Some((semantic_tokens.raw_tokens.clone(), buffer_snapshot)); + } } + remaining_tokens })?; - None + match remaining_tokens { + Some((raw_tokens, buffer_snapshot)) => Some( + cx.background_spawn(raw_to_buffer_semantic_tokens( + raw_tokens, + buffer_snapshot.text.clone(), + )) + .await, + ), + None => None, + } }; Ok(BufferSemanticTokens { tokens: res }) }) diff --git a/crates/project/tests/integration/dynamic_registration.rs b/crates/project/tests/integration/dynamic_registration.rs index 0dcfd708d41..9600b2cc4cf 100644 --- a/crates/project/tests/integration/dynamic_registration.rs +++ b/crates/project/tests/integration/dynamic_registration.rs @@ -1,6 +1,8 @@ //! Tests for dynamic (un)registration of language server capabilities, handled in //! `project::lsp_store::dynamic_registration`. +use std::collections::BTreeMap; + use pretty_assertions::assert_eq; use super::*; @@ -942,12 +944,225 @@ async fn test_multi_registration_diagnostics(cx: &mut gpui::TestAppContext) { ); } +/// One large scenario for all per-server-refreshable LSP data kinds (document colors, +/// links, folding ranges, symbols, code lens, semantic tokens, inlay hints), asserting +/// at the data level that per-server refreshes never disturb other servers: +/// neither the second server on the same buffer, nor a server of an unrelated language. +#[gpui::test] +async fn test_per_server_refreshes_keep_other_servers_data(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/the-root"), + json!({ "a.rs": "fn main() {\n}\n", "b.md": "# title\ntext\n" }), + ) + .await; + let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + language_registry.add(markdown_lang()); + + let mut static_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + name: "static-server", + capabilities: all_lsp_data_server_capabilities(), + ..FakeLspAdapter::default() + }, + ); + let mut dynamic_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + name: "dynamic-server", + ..FakeLspAdapter::default() + }, + ); + let mut markdown_servers = language_registry.register_fake_lsp( + "Markdown", + FakeLspAdapter { + name: "markdown-server", + capabilities: all_lsp_data_server_capabilities(), + ..FakeLspAdapter::default() + }, + ); + cx.executor().run_until_parked(); + + let (rust_buffer, _rust_lsp_handle) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx) + }) + .await + .unwrap(); + let (markdown_buffer, _markdown_lsp_handle) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/the-root/b.md"), cx) + }) + .await + .unwrap(); + let static_server = static_servers.next().await.unwrap(); + let dynamic_server = dynamic_servers.next().await.unwrap(); + let markdown_server = markdown_servers.next().await.unwrap(); + let static_server_id = static_server.server.server_id(); + let dynamic_server_id = dynamic_server.server.server_id(); + let markdown_server_id = markdown_server.server.server_id(); + let static_counts = serve_lsp_data(&static_server, 1); + let dynamic_counts = serve_lsp_data(&dynamic_server, 2); + let markdown_counts = serve_lsp_data(&markdown_server, 3); + let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx); + cx.executor().run_until_parked(); + + let only_static = lsp_data_labels(&[(static_server_id, 1)]); + let both_rust_servers = lsp_data_labels(&[(static_server_id, 1), (dynamic_server_id, 2)]); + let only_markdown = lsp_data_labels(&[(markdown_server_id, 3)]); + let dynamic_refresh = InvalidationStrategy::RefreshRequested { + server_id: dynamic_server_id, + }; + let all_dynamic_refresh_events = vec![ + format!("code_lens({dynamic_server_id})"), + format!("document_colors({dynamic_server_id})"), + format!("document_links({dynamic_server_id})"), + format!("document_symbols({dynamic_server_id})"), + format!("folding_ranges({dynamic_server_id})"), + format!("inlay_hints({dynamic_server_id})"), + format!("semantic_tokens({dynamic_server_id})"), + ]; + + assert_eq!( + fetch_all_lsp_data(&project, &rust_buffer, InvalidationStrategy::None, cx).await, + only_static, + "expected only the statically capable server's data after the initial fetch", + ); + assert_eq!( + fetch_all_lsp_data(&project, &markdown_buffer, InvalidationStrategy::None, cx).await, + only_markdown, + "expected the markdown server's data after the initial fetch", + ); + assert_eq!(refresh_events.lock().as_slice(), &[] as &[String]); + + register_all_lsp_data_capabilities(&dynamic_server).await; + cx.executor().run_until_parked(); + assert_eq!( + sorted(refresh_events.lock().drain(..)), + all_dynamic_refresh_events, + "expected every dynamic registration to trigger the corresponding per-server refresh", + ); + assert_eq!( + fetch_all_lsp_data(&project, &rust_buffer, dynamic_refresh, cx).await, + both_rust_servers, + "expected both servers' data after the dynamic registrations", + ); + assert_eq!( + fetch_all_lsp_data(&project, &markdown_buffer, InvalidationStrategy::None, cx).await, + only_markdown, + "expected the registrations of another language's server to not disturb markdown data", + ); + + dynamic_server + .request::((), DEFAULT_LSP_REQUEST_TIMEOUT) + .await + .into_response() + .unwrap(); + dynamic_server + .request::((), DEFAULT_LSP_REQUEST_TIMEOUT) + .await + .into_response() + .unwrap(); + dynamic_server + .request::((), DEFAULT_LSP_REQUEST_TIMEOUT) + .await + .into_response() + .unwrap(); + cx.executor().run_until_parked(); + assert_eq!( + sorted(refresh_events.lock().drain(..)), + vec![ + format!("code_lens({dynamic_server_id})"), + format!("inlay_hints({dynamic_server_id})"), + format!("semantic_tokens({dynamic_server_id})"), + ], + "expected the server-sent refresh requests to trigger per-server refreshes", + ); + assert_eq!( + fetch_all_lsp_data(&project, &rust_buffer, dynamic_refresh, cx).await, + both_rust_servers, + "expected the per-server refresh requests to keep the other server's data", + ); + assert_eq!( + fetch_all_lsp_data(&project, &markdown_buffer, InvalidationStrategy::None, cx).await, + only_markdown, + "expected another language server's refresh requests to not disturb markdown data", + ); + + unregister_all_lsp_data_capabilities(&dynamic_server).await; + cx.executor().run_until_parked(); + assert_eq!( + sorted(refresh_events.lock().drain(..)), + all_dynamic_refresh_events, + "expected every unregistration to trigger the corresponding per-server refresh", + ); + assert_eq!( + fetch_all_lsp_data(&project, &rust_buffer, dynamic_refresh, cx).await, + only_static, + "expected the unregistrations to drop only the dynamic server's data", + ); + assert_eq!( + fetch_all_lsp_data(&project, &markdown_buffer, InvalidationStrategy::None, cx).await, + only_markdown, + "expected the unregistrations of another language's server to not disturb markdown data", + ); + + assert_eq!( + *static_counts.lock(), + BTreeMap::from_iter([ + ("code_lens", 1), + ("colors", 1), + ("document_symbols", 1), + ("folding_ranges", 1), + ("inlay_hints", 1), + ("links", 1), + ("semantic_tokens", 1), + ]), + "expected the static server to never be re-queried by the other server's changes", + ); + assert_eq!( + *dynamic_counts.lock(), + BTreeMap::from_iter([ + ("code_lens", 2), + ("colors", 1), + ("document_symbols", 1), + ("folding_ranges", 1), + ("inlay_hints", 2), + ("links", 1), + ("semantic_tokens", 2), + ]), + "expected the dynamic server to be re-queried only after registering and for the kinds it requested refreshes for", + ); + assert_eq!( + *markdown_counts.lock(), + BTreeMap::from_iter([ + ("code_lens", 1), + ("colors", 1), + ("document_symbols", 1), + ("folding_ranges", 1), + ("inlay_hints", 1), + ("links", 1), + ("semantic_tokens", 1), + ]), + "expected the markdown server to never be re-queried by the rust servers' changes", + ); +} + +/// Unlike [`test_per_server_refreshes_keep_other_servers_data`], focuses on the +/// dynamic capability changes as the refresh trigger: how they combine data from +/// static and dynamic capabilities (e.g. completion trigger characters), and which +/// servers get [re-]queried after the capability changes. #[gpui::test] async fn test_dynamic_registration_refreshes_lsp_data(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/the-root"), json!({ "a.rs": "fn main() {}" })) + fs.insert_tree(path!("/the-root"), json!({ "a.rs": "fn main() {\n}\n" })) .await; let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); @@ -958,30 +1173,11 @@ async fn test_dynamic_registration_refreshes_lsp_data(cx: &mut gpui::TestAppCont FakeLspAdapter { name: "static-server", capabilities: lsp::ServerCapabilities { - color_provider: Some(lsp::ColorProviderCapability::Simple(true)), - document_link_provider: Some(lsp::DocumentLinkOptions { - resolve_provider: None, - work_done_progress_options: lsp::WorkDoneProgressOptions::default(), - }), - folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)), - document_symbol_provider: Some(lsp::OneOf::Left(true)), - code_lens_provider: Some(lsp::CodeLensOptions { - resolve_provider: None, - }), - semantic_tokens_provider: Some( - lsp::SemanticTokensServerCapabilities::SemanticTokensOptions( - lsp::SemanticTokensOptions { - full: Some(lsp::SemanticTokensFullOptions::Bool(true)), - ..lsp::SemanticTokensOptions::default() - }, - ), - ), - inlay_hint_provider: Some(lsp::OneOf::Left(true)), completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string()]), ..lsp::CompletionOptions::default() }), - ..lsp::ServerCapabilities::default() + ..all_lsp_data_server_capabilities() }, ..FakeLspAdapter::default() }, @@ -1004,96 +1200,48 @@ async fn test_dynamic_registration_refreshes_lsp_data(cx: &mut gpui::TestAppCont let static_server = static_servers.next().await.unwrap(); let dynamic_server = dynamic_servers.next().await.unwrap(); let dynamic_server_id = dynamic_server.server.server_id(); - cx.executor().run_until_parked(); - - let static_counters = count_lsp_requests(&static_server); - let dynamic_counters = count_lsp_requests(&dynamic_server); + let static_counts = serve_lsp_data(&static_server, 1); + let dynamic_counts = serve_lsp_data(&dynamic_server, 2); let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx); let buffer_triggers = |cx: &mut gpui::TestAppContext| { buffer.read_with(cx, |buffer, _| buffer.completion_triggers().clone()) }; + cx.executor().run_until_parked(); - fetch_lsp_data(&project, &buffer, cx).await; + let all_kinds_queried_once = BTreeMap::from_iter([ + ("code_lens", 1), + ("colors", 1), + ("document_symbols", 1), + ("folding_ranges", 1), + ("inlay_hints", 1), + ("links", 1), + ("semantic_tokens", 1), + ]); + let all_dynamic_refresh_events = vec![ + format!("code_lens({dynamic_server_id})"), + format!("document_colors({dynamic_server_id})"), + format!("document_links({dynamic_server_id})"), + format!("document_symbols({dynamic_server_id})"), + format!("folding_ranges({dynamic_server_id})"), + format!("inlay_hints({dynamic_server_id})"), + format!("semantic_tokens({dynamic_server_id})"), + ]; + + fetch_all_lsp_data(&project, &buffer, InvalidationStrategy::None, cx).await; assert_eq!( - static_counters.snapshot(), - LspRequestCounts { - colors: 1, - links: 1, - folding_ranges: 1, - document_symbols: 1, - code_lens: 1, - semantic_tokens: 1, - }, + *static_counts.lock(), + all_kinds_queried_once, "expected the initial fetch to query the statically capable server", ); assert_eq!( - dynamic_counters.snapshot(), - LspRequestCounts::default(), + *dynamic_counts.lock(), + BTreeMap::new(), "expected the initial fetch to skip the server without capabilities", ); assert_eq!(buffer_triggers(cx), BTreeSet::from([".".to_string()])); assert_eq!(refresh_events.lock().as_slice(), &[] as &[String]); - register_capability( - &dynamic_server, - "textDocument/documentColor", - "colors", - None, - ) - .await; - register_capability( - &dynamic_server, - "textDocument/documentLink", - "links", - serde_json::to_value(lsp::DocumentLinkOptions { - resolve_provider: None, - work_done_progress_options: lsp::WorkDoneProgressOptions::default(), - }) - .ok(), - ) - .await; - register_capability( - &dynamic_server, - "textDocument/foldingRange", - "folding", - None, - ) - .await; - register_capability( - &dynamic_server, - "textDocument/documentSymbol", - "symbols", - None, - ) - .await; - register_capability( - &dynamic_server, - "textDocument/codeLens", - "code-lens", - serde_json::to_value(lsp::CodeLensOptions { - resolve_provider: None, - }) - .ok(), - ) - .await; - register_capability( - &dynamic_server, - "textDocument/semanticTokens", - "tokens", - serde_json::to_value(lsp::SemanticTokensRegistrationOptions { - text_document_registration_options: lsp::TextDocumentRegistrationOptions { - document_selector: None, - }, - semantic_tokens_options: lsp::SemanticTokensOptions { - full: Some(lsp::SemanticTokensFullOptions::Bool(true)), - ..lsp::SemanticTokensOptions::default() - }, - static_registration_options: lsp::StaticRegistrationOptions::default(), - }) - .ok(), - ) - .await; - register_capability(&dynamic_server, "textDocument/inlayHint", "hints", None).await; + register_all_lsp_data_capabilities(&dynamic_server).await; register_capability( &dynamic_server, "textDocument/completion", @@ -1109,15 +1257,7 @@ async fn test_dynamic_registration_refreshes_lsp_data(cx: &mut gpui::TestAppCont assert_eq!( sorted(refresh_events.lock().drain(..)), - vec![ - format!("code_lens({dynamic_server_id})"), - format!("document_colors({dynamic_server_id})"), - format!("document_links({dynamic_server_id})"), - format!("document_symbols({dynamic_server_id})"), - format!("folding_ranges({dynamic_server_id})"), - format!("inlay_hints({dynamic_server_id})"), - format!("semantic_tokens({dynamic_server_id})"), - ], + all_dynamic_refresh_events, "expected every dynamic registration to trigger the corresponding per-server refresh", ); assert_eq!( @@ -1126,53 +1266,33 @@ async fn test_dynamic_registration_refreshes_lsp_data(cx: &mut gpui::TestAppCont "expected trigger characters from both servers to be combined", ); - fetch_lsp_data(&project, &buffer, cx).await; - assert_eq!( - dynamic_counters.snapshot(), - LspRequestCounts { - colors: 1, - links: 1, - folding_ranges: 1, - document_symbols: 1, - code_lens: 1, - semantic_tokens: 1, + fetch_all_lsp_data( + &project, + &buffer, + InvalidationStrategy::RefreshRequested { + server_id: dynamic_server_id, }, + cx, + ) + .await; + assert_eq!( + *dynamic_counts.lock(), + all_kinds_queried_once, "expected the newly registered server to be queried after the refreshes", ); assert_eq!( - static_counters.snapshot(), - LspRequestCounts { - colors: 1, - links: 1, - folding_ranges: 1, - document_symbols: 1, - code_lens: 1, - semantic_tokens: 1, - }, + *static_counts.lock(), + all_kinds_queried_once, "expected per-server refreshes to leave the static server's cached data untouched", ); - unregister_capabilities(&dynamic_server, "textDocument/documentColor", &["colors"]).await; - unregister_capabilities(&dynamic_server, "textDocument/documentLink", &["links"]).await; - unregister_capabilities(&dynamic_server, "textDocument/foldingRange", &["folding"]).await; - unregister_capabilities(&dynamic_server, "textDocument/documentSymbol", &["symbols"]).await; - unregister_capabilities(&dynamic_server, "textDocument/codeLens", &["code-lens"]).await; - unregister_capabilities(&dynamic_server, "textDocument/semanticTokens", &["tokens"]).await; - unregister_capabilities(&dynamic_server, "textDocument/inlayHint", &["hints"]).await; + unregister_all_lsp_data_capabilities(&dynamic_server).await; unregister_capabilities(&dynamic_server, "textDocument/completion", &["completions"]).await; cx.executor().run_until_parked(); assert_eq!( sorted(refresh_events.lock().drain(..)), - vec![ - format!("code_lens({dynamic_server_id})"), - format!("document_colors({dynamic_server_id})"), - format!("document_links({dynamic_server_id})"), - format!("document_symbols({dynamic_server_id})"), - format!("folding_ranges({dynamic_server_id})"), - format!("inlay_hints({dynamic_server_id})"), - format!("semantic_tokens({dynamic_server_id})"), - ], + all_dynamic_refresh_events, "expected every unregistration to trigger the corresponding per-server refresh", ); assert_eq!( @@ -1181,29 +1301,23 @@ async fn test_dynamic_registration_refreshes_lsp_data(cx: &mut gpui::TestAppCont "expected only the static server's trigger characters to remain", ); - fetch_lsp_data(&project, &buffer, cx).await; - assert_eq!( - dynamic_counters.snapshot(), - LspRequestCounts { - colors: 1, - links: 1, - folding_ranges: 1, - document_symbols: 1, - code_lens: 1, - semantic_tokens: 1, + fetch_all_lsp_data( + &project, + &buffer, + InvalidationStrategy::RefreshRequested { + server_id: dynamic_server_id, }, + cx, + ) + .await; + assert_eq!( + *dynamic_counts.lock(), + all_kinds_queried_once, "expected the unregistered server to not be queried anymore", ); assert_eq!( - static_counters.snapshot(), - LspRequestCounts { - colors: 1, - links: 1, - folding_ranges: 1, - document_symbols: 1, - code_lens: 1, - semantic_tokens: 1, - }, + *static_counts.lock(), + all_kinds_queried_once, "expected the static server to never be re-queried by another server's capability changes", ); } @@ -1717,90 +1831,6 @@ fn server_capabilities( }) } -#[derive(Default)] -struct LspRequestCounters { - colors: Arc, - links: Arc, - folding_ranges: Arc, - document_symbols: Arc, - code_lens: Arc, - semantic_tokens: Arc, -} - -#[derive(Debug, Default, PartialEq)] -struct LspRequestCounts { - colors: usize, - links: usize, - folding_ranges: usize, - document_symbols: usize, - code_lens: usize, - semantic_tokens: usize, -} - -impl LspRequestCounters { - fn snapshot(&self) -> LspRequestCounts { - LspRequestCounts { - colors: self.colors.load(atomic::Ordering::Acquire), - links: self.links.load(atomic::Ordering::Acquire), - folding_ranges: self.folding_ranges.load(atomic::Ordering::Acquire), - document_symbols: self.document_symbols.load(atomic::Ordering::Acquire), - code_lens: self.code_lens.load(atomic::Ordering::Acquire), - semantic_tokens: self.semantic_tokens.load(atomic::Ordering::Acquire), - } - } -} - -fn count_lsp_requests(fake_server: &lsp::FakeLanguageServer) -> LspRequestCounters { - let counters = LspRequestCounters::default(); - fake_server.set_request_handler::({ - let count = counters.colors.clone(); - move |_, _| { - count.fetch_add(1, atomic::Ordering::Release); - async move { Ok(Vec::new()) } - } - }); - fake_server.set_request_handler::({ - let count = counters.links.clone(); - move |_, _| { - count.fetch_add(1, atomic::Ordering::Release); - async move { Ok(None) } - } - }); - fake_server.set_request_handler::({ - let count = counters.folding_ranges.clone(); - move |_, _| { - count.fetch_add(1, atomic::Ordering::Release); - async move { Ok(None) } - } - }); - fake_server.set_request_handler::({ - let count = counters.document_symbols.clone(); - move |_, _| { - count.fetch_add(1, atomic::Ordering::Release); - async move { Ok(None) } - } - }); - fake_server.set_request_handler::({ - let count = counters.code_lens.clone(); - move |_, _| { - count.fetch_add(1, atomic::Ordering::Release); - async move { Ok(None) } - } - }); - fake_server.set_request_handler::({ - let count = counters.semantic_tokens.clone(); - move |_, _| { - count.fetch_add(1, atomic::Ordering::Release); - async move { - Ok(Some(lsp::SemanticTokensResult::Tokens( - lsp::SemanticTokens::default(), - ))) - } - } - }); - counters -} - fn observe_refresh_events( project: &Entity, cx: &mut gpui::TestAppContext, @@ -1841,12 +1871,284 @@ fn observe_refresh_events( (events, subscription) } -async fn fetch_lsp_data( +fn sorted(events: impl IntoIterator) -> Vec { + let mut events = events.into_iter().collect::>(); + events.sort(); + events +} + +fn all_lsp_data_server_capabilities() -> lsp::ServerCapabilities { + lsp::ServerCapabilities { + color_provider: Some(lsp::ColorProviderCapability::Simple(true)), + document_link_provider: Some(lsp::DocumentLinkOptions { + resolve_provider: None, + work_done_progress_options: lsp::WorkDoneProgressOptions::default(), + }), + folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)), + document_symbol_provider: Some(lsp::OneOf::Left(true)), + code_lens_provider: Some(lsp::CodeLensOptions { + resolve_provider: None, + }), + semantic_tokens_provider: Some( + lsp::SemanticTokensServerCapabilities::SemanticTokensOptions( + lsp::SemanticTokensOptions { + legend: lsp::SemanticTokensLegend { + token_types: vec!["keyword".into()], + token_modifiers: Vec::new(), + }, + full: Some(lsp::SemanticTokensFullOptions::Bool(true)), + ..lsp::SemanticTokensOptions::default() + }, + ), + ), + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + } +} + +async fn register_all_lsp_data_capabilities(fake_server: &lsp::FakeLanguageServer) { + register_capability(fake_server, "textDocument/documentColor", "colors", None).await; + register_capability( + fake_server, + "textDocument/documentLink", + "links", + serde_json::to_value(lsp::DocumentLinkOptions { + resolve_provider: None, + work_done_progress_options: lsp::WorkDoneProgressOptions::default(), + }) + .ok(), + ) + .await; + register_capability(fake_server, "textDocument/foldingRange", "folding", None).await; + register_capability(fake_server, "textDocument/documentSymbol", "symbols", None).await; + register_capability( + fake_server, + "textDocument/codeLens", + "code-lens", + serde_json::to_value(lsp::CodeLensOptions { + resolve_provider: None, + }) + .ok(), + ) + .await; + register_capability( + fake_server, + "textDocument/semanticTokens", + "tokens", + serde_json::to_value(lsp::SemanticTokensRegistrationOptions { + text_document_registration_options: lsp::TextDocumentRegistrationOptions { + document_selector: None, + }, + semantic_tokens_options: lsp::SemanticTokensOptions { + legend: lsp::SemanticTokensLegend { + token_types: vec!["keyword".into()], + token_modifiers: Vec::new(), + }, + full: Some(lsp::SemanticTokensFullOptions::Bool(true)), + ..lsp::SemanticTokensOptions::default() + }, + static_registration_options: lsp::StaticRegistrationOptions::default(), + }) + .ok(), + ) + .await; + register_capability(fake_server, "textDocument/inlayHint", "hints", None).await; +} + +async fn unregister_all_lsp_data_capabilities(fake_server: &lsp::FakeLanguageServer) { + unregister_capabilities(fake_server, "textDocument/documentColor", &["colors"]).await; + unregister_capabilities(fake_server, "textDocument/documentLink", &["links"]).await; + unregister_capabilities(fake_server, "textDocument/foldingRange", &["folding"]).await; + unregister_capabilities(fake_server, "textDocument/documentSymbol", &["symbols"]).await; + unregister_capabilities(fake_server, "textDocument/codeLens", &["code-lens"]).await; + unregister_capabilities(fake_server, "textDocument/semanticTokens", &["tokens"]).await; + unregister_capabilities(fake_server, "textDocument/inlayHint", &["hints"]).await; +} + +type LspRequestCounts = Arc>>; + +/// Serves distinct data, derived from `index`, for every LSP data kind, and counts the +/// requests served per kind. [`lsp_data_labels`] builds the expected fetch results for +/// the same `index`. +fn serve_lsp_data(fake_server: &lsp::FakeLanguageServer, index: u8) -> LspRequestCounts { + let counts = LspRequestCounts::default(); + fake_server.set_request_handler::({ + let counts = counts.clone(); + move |_, _| { + *counts.lock().entry("colors").or_default() += 1; + async move { + Ok(vec![lsp::ColorInformation { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)), + color: lsp::Color { + red: 0.0, + green: 0.0, + blue: 0.0, + alpha: f32::from(index), + }, + }]) + } + } + }); + fake_server.set_request_handler::({ + let counts = counts.clone(); + move |_, _| { + *counts.lock().entry("links").or_default() += 1; + async move { + Ok(Some(vec![lsp::DocumentLink { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)), + target: Some(format!("https://server-{index}.example/").parse().unwrap()), + tooltip: None, + data: None, + }])) + } + } + }); + fake_server.set_request_handler::({ + let counts = counts.clone(); + move |_, _| { + *counts.lock().entry("folding_ranges").or_default() += 1; + async move { + Ok(Some(vec![lsp::FoldingRange { + start_line: 0, + start_character: None, + end_line: 1, + end_character: None, + kind: None, + collapsed_text: Some(format!("fold-{index}")), + }])) + } + } + }); + fake_server.set_request_handler::({ + let counts = counts.clone(); + move |_, _| { + *counts.lock().entry("document_symbols").or_default() += 1; + async move { + #[allow(deprecated)] + Ok(Some(lsp::DocumentSymbolResponse::Nested(vec![ + lsp::DocumentSymbol { + name: format!("symbol-{index}"), + detail: None, + kind: lsp::SymbolKind::FUNCTION, + tags: None, + deprecated: None, + range: lsp::Range::default(), + selection_range: lsp::Range::default(), + children: None, + }, + ]))) + } + } + }); + fake_server.set_request_handler::({ + let counts = counts.clone(); + move |_, _| { + *counts.lock().entry("code_lens").or_default() += 1; + async move { + Ok(Some(vec![lsp::CodeLens { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)), + command: Some(lsp::Command { + title: format!("lens-{index}"), + command: "test.command".to_string(), + arguments: None, + }), + data: None, + }])) + } + } + }); + fake_server.set_request_handler::({ + let counts = counts.clone(); + move |_, _| { + *counts.lock().entry("semantic_tokens").or_default() += 1; + async move { + Ok(Some(lsp::SemanticTokensResult::Tokens( + lsp::SemanticTokens { + result_id: None, + data: semantic_token_test_data(index), + }, + ))) + } + } + }); + fake_server.set_request_handler::({ + let counts = counts.clone(); + move |_, _| { + *counts.lock().entry("inlay_hints").or_default() += 1; + async move { + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, 0), + label: lsp::InlayHintLabel::String(format!("hint-{index}")), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + } + }); + counts +} + +fn semantic_token_test_data(index: u8) -> Vec { + vec![0, u32::from(index - 1) * 4, 1, 0, 0] +} + +#[derive(Debug, Default, PartialEq)] +struct LspDataLabels { + colors: Vec, + links: Vec, + folding_ranges: Vec, + document_symbols: Vec, + code_lens: Vec, + semantic_tokens: Vec<(LanguageServerId, Vec)>, + inlay_hints: Vec<(LanguageServerId, Vec)>, +} + +impl LspDataLabels { + fn sort(&mut self) { + self.colors.sort(); + self.links.sort(); + self.folding_ranges.sort(); + self.document_symbols.sort(); + self.code_lens.sort(); + self.semantic_tokens.sort(); + self.inlay_hints.sort(); + } +} + +/// The expected [`fetch_all_lsp_data`] results for servers set up with [`serve_lsp_data`]. +fn lsp_data_labels(servers: &[(LanguageServerId, u8)]) -> LspDataLabels { + let mut labels = LspDataLabels::default(); + for &(server_id, index) in servers { + labels.colors.push(format!("color-{index}")); + labels + .links + .push(format!("https://server-{index}.example/")); + labels.folding_ranges.push(format!("fold-{index}")); + labels.document_symbols.push(format!("symbol-{index}")); + labels.code_lens.push(format!("lens-{index}")); + labels + .semantic_tokens + .push((server_id, semantic_token_test_data(index))); + labels + .inlay_hints + .push((server_id, vec![format!("hint-{index}")])); + } + labels.sort(); + labels +} + +async fn fetch_all_lsp_data( project: &Entity, buffer: &Entity, + inlay_hints_invalidate: InvalidationStrategy, cx: &mut gpui::TestAppContext, -) { +) -> LspDataLabels { let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); let colors = lsp_store.update(cx, |lsp_store, cx| { lsp_store.document_colors(buffer.clone(), cx) }); @@ -1863,18 +2165,73 @@ async fn fetch_lsp_data( let semantic_tokens = lsp_store.update(cx, |lsp_store, cx| { lsp_store.semantic_tokens(buffer.clone(), cx) }); - if let Some(colors) = colors { - colors.await.unwrap(); - } - links.await; - folding_ranges.await; - document_symbols.await; - code_lens.await.unwrap(); - semantic_tokens.await.unwrap(); -} + let inlay_hint_tasks = lsp_store.update(cx, |lsp_store, cx| { + let query_range = + buffer.read(cx).anchor_before(0)..buffer.read(cx).anchor_after(buffer.read(cx).len()); + lsp_store.inlay_hints( + inlay_hints_invalidate, + buffer.clone(), + vec![query_range], + None, + cx, + ) + }); -fn sorted(events: impl IntoIterator) -> Vec { - let mut events = events.into_iter().collect::>(); - events.sort(); - events + let mut labels = LspDataLabels::default(); + if let Some(colors) = colors { + labels.colors.extend( + colors + .await + .unwrap() + .colors + .into_iter() + .map(|color| format!("color-{}", color.color.alpha as u8)), + ); + } + labels.links.extend( + links + .await + .unwrap_or_default() + .values() + .flat_map(|server_links| server_links.values()) + .filter_map(|link| link.target.clone()) + .map(|target| target.to_string()), + ); + labels.folding_ranges.extend( + folding_ranges + .await + .into_iter() + .filter_map(|folding_range| folding_range.collapsed_text) + .map(|collapsed_text| collapsed_text.to_string()), + ); + labels.document_symbols.extend( + document_symbols + .await + .into_iter() + .map(|symbol| symbol.text.to_string()), + ); + labels.code_lens.extend( + code_lens + .await + .unwrap() + .unwrap_or_default() + .values() + .map(|action| action.lsp_action.title().to_string()), + ); + semantic_tokens.await.unwrap(); + labels.semantic_tokens = + lsp_store.read_with(cx, |lsp_store, _| lsp_store.semantic_token_data(buffer_id)); + for (_, hints_task) in inlay_hint_tasks { + for (server_id, server_hints) in hints_task.await.unwrap() { + let hint_labels = server_hints + .into_iter() + .map(|(_, hint)| hint.text().to_string()) + .collect::>(); + if !hint_labels.is_empty() { + labels.inlay_hints.push((server_id, hint_labels)); + } + } + } + labels.sort(); + labels }