From a7f88e70c0e2b44195651de728859df3e5b4fb84 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Sat, 13 Jun 2026 13:30:44 +0900 Subject: [PATCH] Honor dynamic LSP document selectors Previously, Zed ignored the `documentSelector` of a dynamically registered `textDocument` capability and treated every such capability as global to all buffers opened in the server. As a result, Zed could route a request to a server for a document outside the registration's selector -- for example, a URI whose language matches the selector but whose scheme the server never registered for (and is thus unknown to it). I do not think Zed exhibits this in practice today, but it is possible in principle and violates the LSP specification. Track `textDocument` dynamic registrations by method and registration id so Zed can keep each registration's `documentSelector` instead of treating dynamically registered capabilities as global for every open buffer. Use the stored selectors when routing local LSP requests, including single-server requests, multi-server requests, completion-specific routing, capability checks, and range-formatting availability. Static initialize-time capabilities continue to apply to every buffer attached to the server, while dynamically registered `textDocument` features now require a matching selector. The initial matching support covers language and URI scheme, with pattern matching intentionally left fail-open. Update completion trigger management to derive triggers from static capabilities plus matching dynamic completion registrations, and recalculate triggers when completion registrations change. Also make pull diagnostic requests require an advertised diagnostic provider so selector-aware routing can correctly gate dynamic diagnostic registrations. Add an integration test covering dynamic completion registrations whose selectors do and do not match the current file buffer, asserting both request routing and completion trigger updates. --- crates/project/src/lsp_command.rs | 7 +- crates/project/src/lsp_store.rs | 711 ++++++++++++++++-- .../tests/integration/project_tests.rs | 145 ++++ 3 files changed, 787 insertions(+), 76 deletions(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 2f05aba49b5f30..ed481171d91b76 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -4431,8 +4431,11 @@ impl LspCommand for GetDocumentDiagnostics { "Get diagnostics" } - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true + fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { + capabilities + .server_capabilities + .diagnostic_provider + .is_some() } fn to_lsp( diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 454c4d18d87ba1..bc874e62313e83 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -287,10 +287,23 @@ pub struct DocumentDiagnostics { version: Option, } +#[derive(Clone, Debug, Default)] +struct DynamicTextDocumentRegistration { + document_selector: Option, + completion_options: Option, +} + #[derive(Default, Debug)] struct DynamicRegistrations { did_change_watched_files: HashSet, diagnostics: HashMap, DiagnosticServerCapabilities>, + text_documents: HashMap>, +} + +#[derive(Clone, Debug)] +struct DocumentSelectorContext { + language_id: String, + scheme: &'static str, } pub struct LocalLspStore { @@ -311,6 +324,7 @@ pub struct LocalLspStore { language_server_paths_watched_for_rename: HashMap, language_server_dynamic_registrations: HashMap, + initial_server_capabilities: HashMap, supplementary_language_servers: HashMap)>, prettier_store: Entity, @@ -2654,35 +2668,33 @@ impl LocalLspStore { self.lsp_tree .get(path, language.name(), language.manifest(), &delegate, cx) { - let server = self - .language_servers - .get(&server_id) - .and_then(|server_state| { - if let LanguageServerState::Running { server, .. } = server_state { - Some(server.clone()) - } else { - None - } - }); - let server = match server { - Some(server) => server, + let server_and_adapter = + self.language_servers + .get(&server_id) + .and_then(|server_state| { + if let LanguageServerState::Running { + server, adapter, .. + } = server_state + { + Some((server.clone(), adapter.clone())) + } else { + None + } + }); + let (server, adapter) = match server_and_adapter { + Some(server_and_adapter) => server_and_adapter, None => continue, }; buffer_handle.update(cx, |buffer, cx| { buffer.set_completion_triggers( server.server_id(), - server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider - .trigger_characters - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), + completion_trigger_characters_for_buffer( + self, + server.server_id(), + &adapter, + buffer, + ), cx, ); }); @@ -2948,17 +2960,12 @@ impl LocalLspStore { buffer_handle.update(cx, |buffer, cx| { buffer.set_completion_triggers( server.server_id(), - server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider - .trigger_characters - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), + completion_trigger_characters_for_buffer( + self, + server.server_id(), + &adapter, + buffer, + ), cx, ); }); @@ -4358,6 +4365,7 @@ impl LspStore { language_server_watched_paths: Default::default(), language_server_paths_watched_for_rename: Default::default(), language_server_dynamic_registrations: Default::default(), + initial_server_capabilities: Default::default(), buffers_being_formatted: Default::default(), buffers_to_refresh_hash_set: HashSet::default(), buffers_to_refresh_queue: VecDeque::new(), @@ -5122,14 +5130,10 @@ impl LspStore { where R: LspCommand, { - self.check_if_capable_for_proto_request( + self.check_if_any_relevant_text_document_server_matches( buffer, - |capabilities| { - request.check_capabilities(AdapterServerCapabilities { - server_capabilities: capabilities.clone(), - code_action_kinds: None, - }) - }, + ::METHOD, + |_, capabilities| request.check_capabilities(capabilities), cx, ) } @@ -5201,6 +5205,75 @@ impl LspStore { self.check_if_any_relevant_server_matches(buffer, |_, capabilities| check(capabilities), cx) } + fn check_if_any_relevant_text_document_server_matches( + &self, + buffer: &Entity, + method: &str, + mut check: F, + cx: &App, + ) -> bool + where + F: FnMut(&lsp::LanguageServerName, AdapterServerCapabilities) -> bool, + { + if let Some(local) = self.as_local() { + let buffer = buffer.read(cx); + return local + .buffers_opened_in_servers + .get(&buffer.remote_id()) + .into_iter() + .flatten() + .filter_map(|server_id| match local.language_servers.get(server_id)? { + LanguageServerState::Running { + adapter, server, .. + } => Some((adapter, server)), + _ => None, + }) + .any(|(adapter, server)| { + let current_capabilities = server.adapter_server_capabilities(); + let code_action_kinds = current_capabilities.code_action_kinds.clone(); + if !check(&server.name(), current_capabilities) { + return false; + } + + text_document_registration_allows_buffer( + local, + server.server_id(), + method, + buffer, + adapter, + || { + local + .initial_server_capabilities + .get(&server.server_id()) + .is_some_and(|capabilities| { + check( + &server.name(), + AdapterServerCapabilities { + server_capabilities: capabilities.clone(), + code_action_kinds, + }, + ) + }) + }, + ) + }); + } + + self.check_if_any_relevant_server_matches( + buffer, + |server_status, capabilities| { + check( + &server_status.name, + AdapterServerCapabilities { + server_capabilities: capabilities.clone(), + code_action_kinds: None, + }, + ) + }, + cx, + ) + } + pub fn supports_range_formatting(&self, buffer: &Entity, cx: &App) -> bool { let settings = LanguageSettings::for_buffer(buffer.read(cx), cx); settings.formatter.as_ref().iter().any(|formatter| { @@ -5208,28 +5281,41 @@ impl LspStore { Formatter::None => false, Formatter::Auto => { settings.prettier.allowed - || self.check_if_capable_for_proto_request( + || self.check_if_any_relevant_text_document_server_matches( buffer, - server_capabilities_support_range_formatting, + "textDocument/rangeFormatting", + |_, capabilities| { + server_capabilities_support_range_formatting( + &capabilities.server_capabilities, + ) + }, cx, ) } Formatter::Prettier => true, Formatter::External { .. } => false, Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current) => { - self.check_if_capable_for_proto_request( + self.check_if_any_relevant_text_document_server_matches( buffer, - server_capabilities_support_range_formatting, + "textDocument/rangeFormatting", + |_, capabilities| { + server_capabilities_support_range_formatting( + &capabilities.server_capabilities, + ) + }, cx, ) } Formatter::LanguageServer( settings::LanguageServerFormatterSpecifier::Specific { name }, - ) => self.check_if_any_relevant_server_matches( + ) => self.check_if_any_relevant_text_document_server_matches( buffer, - |server_status, capabilities| { - server_status.name.0.as_ref() == name - && server_capabilities_support_range_formatting(capabilities) + "textDocument/rangeFormatting", + |server_name, capabilities| { + server_name.0.as_ref() == name + && server_capabilities_support_range_formatting( + &capabilities.server_capabilities, + ) }, cx, ), @@ -5290,18 +5376,22 @@ impl LspStore { LanguageServerToQuery::FirstCapable => self.as_local().and_then(|local| { local .language_servers_for_buffer(buffer, cx) - .find(|(_, server)| { - request.check_capabilities(server.adapter_server_capabilities()) + .find(|(adapter, server)| { + lsp_command_allowed_for_buffer(local, &request, buffer, adapter, server) }) .map(|(_, server)| server.clone()) }), - LanguageServerToQuery::Other(id) => self - .language_server_for_local_buffer(buffer, id, cx) - .and_then(|(_, server)| { - request - .check_capabilities(server.adapter_server_capabilities()) - .then(|| Arc::clone(server)) - }), + LanguageServerToQuery::Other(id) => self.as_local().and_then(|local| { + local + .language_servers_for_buffer(buffer, cx) + .find(|(adapter, server)| { + server.server_id() == id + && lsp_command_allowed_for_buffer( + local, &request, buffer, adapter, server, + ) + }) + .map(|(_, server)| Arc::clone(server)) + }), }) else { return Task::ready(Ok(Default::default())); }; @@ -6600,10 +6690,17 @@ impl LspStore { return Task::ready(Ok(Vec::new())); } + let request = GetCompletions { + position, + context: context.clone(), + server_id: None, + }; let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| { local .language_servers_for_buffer(buffer, cx) - .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .filter(|(adapter, server)| { + lsp_command_allowed_for_buffer(local, &request, buffer, adapter, server) + }) .filter(|(adapter, _)| { scope .as_ref() @@ -9196,6 +9293,9 @@ impl LspStore { let server_ids = buffer.update(cx, |buffer, cx| { local .language_servers_for_buffer(buffer, cx) + .filter(|(adapter, server)| { + lsp_command_allowed_for_buffer(local, &request, buffer, adapter, server) + }) .filter(|(adapter, _)| { scope .as_ref() @@ -11884,10 +11984,14 @@ impl LspStore { // indicating that the server is up and running and ready let workspace_folders = workspace_folders.lock().clone(); language_server.set_workspace_folders(workspace_folders); + let server_capabilities = language_server.capabilities(); + local + .initial_server_capabilities + .insert(server_id, server_capabilities.clone()); - let workspace_diagnostics_refresh_tasks = language_server - .capabilities() + let workspace_diagnostics_refresh_tasks = server_capabilities .diagnostic_provider + .clone() .and_then(|provider| { local .language_server_dynamic_registrations @@ -11957,7 +12061,6 @@ impl LspStore { Some(key.worktree_id), )); - let server_capabilities = language_server.capabilities(); if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { downstream_client .send(proto::StartLanguageServer { @@ -12600,6 +12703,10 @@ impl LspStore { local .workspace_pull_diagnostics_result_ids .remove(&for_server); + local + .language_server_dynamic_registrations + .remove(&for_server); + local.initial_server_capabilities.remove(&for_server); for buffer_servers in local.buffers_opened_in_servers.values_mut() { buffer_servers.remove(&for_server); } @@ -12947,13 +13054,30 @@ impl LspStore { } } "textDocument/rangeFormatting" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); let options = parse_register_capabilities(reg)?; server.update_capabilities(|capabilities| { capabilities.document_range_formatting_provider = Some(options); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } "textDocument/onTypeFormatting" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); if let Some(options) = reg .register_options .map(serde_json::from_value) @@ -12962,38 +13086,103 @@ impl LspStore { server.update_capabilities(|capabilities| { capabilities.document_on_type_formatting_provider = Some(options); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } } "textDocument/formatting" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); let options = parse_register_capabilities(reg)?; server.update_capabilities(|capabilities| { capabilities.document_formatting_provider = Some(options); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } "textDocument/rename" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); let options = parse_register_capabilities(reg)?; server.update_capabilities(|capabilities| { capabilities.rename_provider = Some(options); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } "textDocument/inlayHint" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); let options = parse_register_capabilities(reg)?; server.update_capabilities(|capabilities| { capabilities.inlay_hint_provider = Some(options); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } "textDocument/documentSymbol" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); let options = parse_register_capabilities(reg)?; server.update_capabilities(|capabilities| { capabilities.document_symbol_provider = Some(options); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } "textDocument/codeAction" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); let options = parse_register_capabilities(reg)?; let provider = match options { OneOf::Left(value) => lsp::CodeActionProviderCapability::Simple(value), @@ -13002,25 +13191,67 @@ impl LspStore { server.update_capabilities(|capabilities| { capabilities.code_action_provider = Some(provider); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } "textDocument/definition" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); let options = parse_register_capabilities(reg)?; server.update_capabilities(|capabilities| { capabilities.definition_provider = Some(options); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } "textDocument/completion" => { + let registration_id = reg.id.clone(); + let method = reg.method.clone(); if let Some(caps) = reg .register_options - .map(serde_json::from_value::) + .map(serde_json::from_value::) .transpose()? { + let registration = DynamicTextDocumentRegistration { + document_selector: caps + .text_document_registration_options + .document_selector + .clone(), + completion_options: Some(caps.completion_options.clone()), + }; server.update_capabilities(|capabilities| { - capabilities.completion_provider = Some(caps.clone()); + capabilities.completion_provider = + Some(caps.completion_options.clone()); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } + if let Some(local) = self.as_local() { let mut buffers_with_language_server = Vec::new(); for handle in self.buffer_store.read(cx).buffers() { @@ -13034,13 +13265,18 @@ impl LspStore { buffers_with_language_server.push(handle); } } - let triggers = caps - .trigger_characters - .unwrap_or_default() - .into_iter() - .collect::>(); for handle in buffers_with_language_server { - let triggers = triggers.clone(); + let triggers = handle.update(cx, |buffer, cx| { + local + .language_servers_for_buffer(buffer, cx) + .find(|(_, server)| server.server_id() == server_id) + .map(|(adapter, _)| { + completion_trigger_characters_for_buffer( + local, server_id, adapter, buffer, + ) + }) + .unwrap_or_default() + }); let _ = handle.update(cx, move |buffer, cx| { buffer.set_completion_triggers(server_id, triggers, cx); }); @@ -13050,6 +13286,10 @@ impl LspStore { } } "textDocument/hover" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); let options = parse_register_capabilities(reg)?; let provider = match options { OneOf::Left(value) => lsp::HoverProviderCapability::Simple(value), @@ -13058,9 +13298,22 @@ impl LspStore { server.update_capabilities(|capabilities| { capabilities.hover_provider = Some(provider); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } "textDocument/signatureHelp" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); if let Some(caps) = reg .register_options .map(serde_json::from_value) @@ -13069,6 +13322,15 @@ impl LspStore { server.update_capabilities(|capabilities| { capabilities.signature_help_provider = Some(caps); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } } @@ -13119,6 +13381,10 @@ impl LspStore { } } "textDocument/codeLens" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); if let Some(caps) = reg .register_options .map(serde_json::from_value) @@ -13127,10 +13393,21 @@ impl LspStore { server.update_capabilities(|capabilities| { capabilities.code_lens_provider = Some(caps); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } } "textDocument/diagnostic" => { + let text_document_registration = + parse_text_document_registration(reg.register_options.as_ref())?; if let Some(caps) = reg .register_options .map(serde_json::from_value::) @@ -13139,16 +13416,19 @@ impl LspStore { let local = self .as_local_mut() .context("Expected LSP Store to be local")?; - let state = local - .language_servers - .get_mut(&server_id) - .context("Could not obtain Language Servers state")?; local .language_server_dynamic_registrations .entry(server_id) .or_default() .diagnostics .insert(Some(reg.id.clone()), caps.clone()); + register_text_document_dynamic_registration( + local, + server_id, + ®.method, + reg.id.clone(), + text_document_registration, + ); let supports_workspace_diagnostics = |capabilities: &DiagnosticServerCapabilities| match capabilities { @@ -13165,6 +13445,10 @@ impl LspStore { }; if supports_workspace_diagnostics(&caps) { + let state = local + .language_servers + .get_mut(&server_id) + .context("Could not obtain Language Servers state")?; if let LanguageServerState::Running { workspace_diagnostics_refresh_tasks, .. @@ -13190,6 +13474,10 @@ impl LspStore { } } "textDocument/documentColor" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); let options = parse_register_capabilities(reg)?; let provider = match options { OneOf::Left(value) => lsp::ColorProviderCapability::Simple(value), @@ -13198,9 +13486,22 @@ impl LspStore { server.update_capabilities(|capabilities| { capabilities.color_provider = Some(provider); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } "textDocument/foldingRange" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); let options = parse_register_capabilities(reg)?; let provider = match options { OneOf::Left(value) => lsp::FoldingRangeProviderCapability::Simple(value), @@ -13209,9 +13510,22 @@ impl LspStore { server.update_capabilities(|capabilities| { capabilities.folding_range_provider = Some(provider); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } "textDocument/documentLink" => { + let registration = + parse_text_document_registration(reg.register_options.as_ref())?; + let registration_id = reg.id.clone(); + let method = reg.method.clone(); if let Some(caps) = reg .register_options .map(serde_json::from_value) @@ -13220,6 +13534,15 @@ impl LspStore { server.update_capabilities(|capabilities| { capabilities.document_link_provider = Some(caps); }); + if let Some(local) = self.as_local_mut() { + register_text_document_dynamic_registration( + local, + server_id, + &method, + registration_id, + registration, + ); + } notify_server_capabilities_updated(&server, cx); } } @@ -13240,6 +13563,17 @@ impl LspStore { .language_server_for_id(server_id) .with_context(|| format!("no server {server_id} found"))?; for unreg in params.unregisterations.iter() { + if unreg.method.starts_with("textDocument/") + && let Some(local) = self.as_local_mut() + { + unregister_text_document_dynamic_registration( + local, + server_id, + &unreg.method, + &unreg.id, + ); + } + match unreg.method.as_str() { "workspace/didChangeWatchedFiles" => { let notify = if let Some(local_lsp_store) = self.as_local_mut() { @@ -13330,6 +13664,36 @@ impl LspStore { server.update_capabilities(|capabilities| { capabilities.completion_provider = None; }); + if let Some(local) = self.as_local() { + let mut buffers_with_language_server = Vec::new(); + for handle in self.buffer_store.read(cx).buffers() { + let buffer_id = handle.read(cx).remote_id(); + if local + .buffers_opened_in_servers + .get(&buffer_id) + .filter(|servers| servers.contains(&server_id)) + .is_some() + { + buffers_with_language_server.push(handle); + } + } + for handle in buffers_with_language_server { + let triggers = handle.update(cx, |buffer, cx| { + local + .language_servers_for_buffer(buffer, cx) + .find(|(_, server)| server.server_id() == server_id) + .map(|(adapter, _)| { + completion_trigger_characters_for_buffer( + local, server_id, adapter, buffer, + ) + }) + .unwrap_or_default() + }); + let _ = handle.update(cx, move |buffer, cx| { + buffer.set_completion_triggers(server_id, triggers, cx); + }); + } + } notify_server_capabilities_updated(&server, cx); } "textDocument/hover" => { @@ -13816,6 +14180,205 @@ fn parse_register_capabilities( }) } +fn register_text_document_dynamic_registration( + local: &mut LocalLspStore, + server_id: LanguageServerId, + method: &str, + registration_id: String, + registration: DynamicTextDocumentRegistration, +) { + local + .language_server_dynamic_registrations + .entry(server_id) + .or_default() + .text_documents + .entry(method.to_string()) + .or_default() + .insert(registration_id, registration); +} + +fn unregister_text_document_dynamic_registration( + local: &mut LocalLspStore, + server_id: LanguageServerId, + method: &str, + registration_id: &str, +) { + let Some(registrations) = local + .language_server_dynamic_registrations + .get_mut(&server_id) + .and_then(|registrations| registrations.text_documents.get_mut(method)) + else { + return; + }; + + registrations.remove(registration_id); +} + +fn parse_text_document_registration( + register_options: Option<&Value>, +) -> Result { + let document_selector = register_options + .cloned() + .map(serde_json::from_value::) + .transpose()? + .and_then(|options| options.document_selector); + + Ok(DynamicTextDocumentRegistration { + document_selector, + completion_options: None, + }) +} + +fn document_selector_context_for_buffer( + buffer: &Buffer, + adapter: &CachedLspAdapter, +) -> Option { + let language = buffer.language()?; + Some(DocumentSelectorContext { + language_id: adapter.language_id(&language.name()), + scheme: "file", + }) +} + +fn document_selector_matches( + document_selector: Option<&lsp::DocumentSelector>, + context: &DocumentSelectorContext, +) -> bool { + let Some(document_selector) = document_selector else { + return true; + }; + + document_selector.iter().any(|filter| { + if let Some(language) = &filter.language + && language != &context.language_id + { + return false; + } + + if let Some(scheme) = &filter.scheme + && !scheme.eq_ignore_ascii_case(context.scheme) + { + return false; + } + + true + }) +} + +fn text_document_registration_allows_buffer( + local: &LocalLspStore, + server_id: LanguageServerId, + method: &str, + buffer: &Buffer, + adapter: &CachedLspAdapter, + statically_supported: impl FnOnce() -> bool, +) -> bool { + // Check for a dynamic registration before evaluating `statically_supported()`: + // it clones the full `ServerCapabilities`, and a method without any dynamic + // registration has no selector to honor anyway. + let Some(registrations) = local + .language_server_dynamic_registrations + .get(&server_id) + .and_then(|registrations| registrations.text_documents.get(method)) + else { + return true; + }; + + if statically_supported() { + return true; + } + + let Some(context) = document_selector_context_for_buffer(buffer, adapter) else { + return true; + }; + + registrations.values().any(|registration| { + document_selector_matches(registration.document_selector.as_ref(), &context) + }) +} + +fn lsp_command_allowed_for_buffer( + local: &LocalLspStore, + request: &R, + buffer: &Buffer, + adapter: &CachedLspAdapter, + server: &LanguageServer, +) -> bool +where + R: LspCommand, +{ + let current_capabilities = server.adapter_server_capabilities(); + let code_action_kinds = current_capabilities.code_action_kinds.clone(); + if !request.check_capabilities(current_capabilities) { + return false; + } + + let method = ::METHOD; + text_document_registration_allows_buffer( + local, + server.server_id(), + method, + buffer, + adapter, + || { + local + .initial_server_capabilities + .get(&server.server_id()) + .is_some_and(|capabilities| { + request.check_capabilities(AdapterServerCapabilities { + server_capabilities: capabilities.clone(), + code_action_kinds, + }) + }) + }, + ) +} + +fn completion_trigger_characters_for_buffer( + local: &LocalLspStore, + server_id: LanguageServerId, + adapter: &CachedLspAdapter, + buffer: &Buffer, +) -> BTreeSet { + let mut triggers = BTreeSet::new(); + + // Static capabilities advertised at initialize-time apply to every buffer + // attached to the server. + if let Some(provider) = local + .initial_server_capabilities + .get(&server_id) + .and_then(|capabilities| capabilities.completion_provider.as_ref()) + && let Some(characters) = &provider.trigger_characters + { + triggers.extend(characters.iter().cloned()); + } + + // Dynamic completion registrations only contribute their triggers when their + // selector matches this buffer. + if let Some(context) = document_selector_context_for_buffer(buffer, adapter) + && let Some(registrations) = local + .language_server_dynamic_registrations + .get(&server_id) + .and_then(|registrations| registrations.text_documents.get("textDocument/completion")) + { + for registration in registrations.values() { + if !document_selector_matches(registration.document_selector.as_ref(), &context) { + continue; + } + + if let Some(characters) = registration + .completion_options + .as_ref() + .and_then(|options| options.trigger_characters.as_ref()) + { + triggers.extend(characters.iter().cloned()); + } + } + } + + triggers +} + fn server_capabilities_support_range_formatting(capabilities: &lsp::ServerCapabilities) -> bool { matches!( capabilities.document_range_formatting_provider.as_ref(), diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index b6112e97389112..59d658809d7076 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -4926,6 +4926,151 @@ async fn test_completions_with_text_edit(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_dynamic_completion_registration_honors_document_selector( + cx: &mut gpui::TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/dir"), json!({ "a.rs": "let value = foo" })) + .await; + + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_language_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + text_document_sync: Some(lsp::TextDocumentSyncCapability::Options( + lsp::TextDocumentSyncOptions { + open_close: Some(true), + change: Some(lsp::TextDocumentSyncKind::FULL), + ..Default::default() + }, + )), + ..Default::default() + }, + ..Default::default() + }, + ); + + let (buffer, _handle) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx) + }) + .await + .unwrap(); + + let fake_server = fake_language_servers.next().await.unwrap(); + cx.executor().run_until_parked(); + + fake_server + .request::( + lsp::RegistrationParams { + registrations: vec![lsp::Registration { + id: "untitled-completion".to_string(), + method: "textDocument/completion".to_string(), + register_options: serde_json::to_value(lsp::CompletionRegistrationOptions { + text_document_registration_options: lsp::TextDocumentRegistrationOptions { + document_selector: Some(vec![lsp::DocumentFilter { + language: Some("rust".to_string()), + scheme: Some("untitled".to_string()), + pattern: None, + }]), + }, + completion_options: lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }, + }) + .ok(), + }], + }, + DEFAULT_LSP_REQUEST_TIMEOUT, + ) + .await + .into_response() + .unwrap(); + cx.executor().run_until_parked(); + + assert!(buffer.read_with(cx, |buffer, _| buffer.completion_triggers().is_empty())); + + let completion_request_count = Arc::new(atomic::AtomicUsize::new(0)); + let _completion_requests = fake_server.set_request_handler::({ + let completion_request_count = completion_request_count.clone(); + move |_, _| { + completion_request_count.fetch_add(1, atomic::Ordering::SeqCst); + async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "matched".into(), + ..Default::default() + }, + ]))) + } + } + }); + + let completions = project + .update(cx, |project, cx| { + project.completions(&buffer, 15, DEFAULT_COMPLETION_CONTEXT, cx) + }) + .await + .unwrap(); + assert!(completions.is_empty()); + assert_eq!(completion_request_count.load(atomic::Ordering::SeqCst), 0); + + fake_server + .request::( + lsp::RegistrationParams { + registrations: vec![lsp::Registration { + id: "file-completion".to_string(), + method: "textDocument/completion".to_string(), + register_options: serde_json::to_value(lsp::CompletionRegistrationOptions { + text_document_registration_options: lsp::TextDocumentRegistrationOptions { + document_selector: Some(vec![lsp::DocumentFilter { + language: Some("rust".to_string()), + scheme: Some("file".to_string()), + pattern: None, + }]), + }, + completion_options: lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }, + }) + .ok(), + }], + }, + DEFAULT_LSP_REQUEST_TIMEOUT, + ) + .await + .into_response() + .unwrap(); + cx.executor().run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.completion_triggers().clone()), + BTreeSet::from([":".to_string()]) + ); + + let completions = project + .update(cx, |project, cx| { + project.completions(&buffer, 15, DEFAULT_COMPLETION_CONTEXT, cx) + }) + .await + .unwrap() + .into_iter() + .flat_map(|response| response.completions) + .collect::>(); + + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].new_text, "matched"); + assert_eq!(completion_request_count.load(atomic::Ordering::SeqCst), 1); +} + #[gpui::test] async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) { init_test(cx);