diff --git a/crates/lsp/src/capabilities.rs b/crates/lsp/src/capabilities.rs new file mode 100644 index 0000000000..76ce4d739b --- /dev/null +++ b/crates/lsp/src/capabilities.rs @@ -0,0 +1,98 @@ +use lsp_types::{ + ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, + TextDocumentSyncSaveOptions, +}; + +use super::DynamicCapabilities; + +pub mod cap { + pub struct DidChangeTextDocument; + pub struct DidSaveTextDocument; +} + +pub trait EffectiveCapability { + type Value; + fn compute(static_caps: &ServerCapabilities, dynamic_caps: &DynamicCapabilities) + -> Self::Value; +} + +impl EffectiveCapability for cap::DidChangeTextDocument { + type Value = Option; + + fn compute( + static_caps: &ServerCapabilities, + dynamic_caps: &DynamicCapabilities, + ) -> Self::Value { + dynamic_caps + .text_document_sync_did_change + .as_ref() + .and_then(|id_to_sync_kind_map| { + if id_to_sync_kind_map.is_empty() { + return None; + } + let mut has_incremental = false; + for data in id_to_sync_kind_map.values() { + let sync_kind = match data.sync_kind { + 0 => Some(TextDocumentSyncKind::NONE), + 1 => Some(TextDocumentSyncKind::FULL), + 2 => Some(TextDocumentSyncKind::INCREMENTAL), + _ => None, + }; + if sync_kind == Some(TextDocumentSyncKind::FULL) { + return Some(TextDocumentSyncKind::FULL); + } + if sync_kind == Some(TextDocumentSyncKind::INCREMENTAL) { + has_incremental = true; + } + } + if has_incremental { + Some(TextDocumentSyncKind::INCREMENTAL) + } else { + None + } + }) + .or_else(|| { + static_caps + .text_document_sync + .as_ref() + .and_then(|sync| match sync { + TextDocumentSyncCapability::Kind(kind) => Some(*kind), + TextDocumentSyncCapability::Options(opts) => opts.change, + }) + }) + } +} + +impl EffectiveCapability for cap::DidSaveTextDocument { + type Value = Option; + + fn compute( + static_caps: &ServerCapabilities, + dynamic_caps: &DynamicCapabilities, + ) -> Self::Value { + dynamic_caps + .text_document_sync_did_save + .as_ref() + .and_then(|id_to_save_options_map| { + if id_to_save_options_map.is_empty() { + None + } else { + Some( + id_to_save_options_map + .values() + .any(|data| data.include_text.unwrap_or(false)), + ) + } + }) + .or_else(|| match static_caps.text_document_sync.as_ref()? { + TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { + TextDocumentSyncSaveOptions::Supported(true) => Some(false), + TextDocumentSyncSaveOptions::Supported(false) => None, + TextDocumentSyncSaveOptions::SaveOptions(save_opts) => { + Some(save_opts.include_text.unwrap_or(false)) + } + }, + TextDocumentSyncCapability::Kind(_) => None, + }) + } +} diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 942225d098..b618f7e6c0 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,5 +1,8 @@ +mod capabilities; mod input_handler; +pub use capabilities::{EffectiveCapability, cap}; + pub use lsp_types::request::*; pub use lsp_types::*; @@ -86,7 +89,8 @@ pub struct LanguageServer { name: LanguageServerName, process_name: Arc, binary: LanguageServerBinary, - capabilities: RwLock, + static_capabilities: RwLock, + dynamic_capabilities: RwLock, /// Configuration sent to the server, stored for display in the language server logs /// buffer. This is represented as the message sent to the LSP in order to avoid cloning it (can /// be large in cases like sending schemas to the json server). @@ -301,6 +305,13 @@ pub struct AdapterServerCapabilities { pub code_action_kinds: Option>, } +#[derive(Debug, Default, Clone)] +pub struct DynamicCapabilities { + pub text_document_sync_did_change: + Option>, + pub text_document_sync_did_save: Option>, +} + impl LanguageServer { /// Starts a language server process. pub fn new( @@ -484,7 +495,8 @@ impl LanguageServer { .map(|name| Arc::from(name.to_string_lossy())) .unwrap_or_default(), binary, - capabilities: Default::default(), + static_capabilities: Default::default(), + dynamic_capabilities: Default::default(), configuration, code_action_kinds, next_id: Default::default(), @@ -898,7 +910,7 @@ impl LanguageServer { if let Some(info) = response.server_info { self.process_name = info.name.into(); } - self.capabilities = RwLock::new(response.capabilities); + self.static_capabilities = RwLock::new(response.capabilities); self.configuration = configuration; self.notify::(&InitializedParams {})?; @@ -1130,7 +1142,18 @@ impl LanguageServer { /// Get the reported capabilities of the running language server. pub fn capabilities(&self) -> ServerCapabilities { - self.capabilities.read().clone() + self.static_capabilities.read().clone() + } + + pub fn update_dynamic_capabilities(&self, update: impl FnOnce(&mut DynamicCapabilities)) { + update(self.dynamic_capabilities.write().deref_mut()); + } + + /// Get effective capabilities by combining static and dynamic capabilities. + pub fn effective_capability(&self) -> Cap::Value { + let static_capabilities = self.capabilities(); + let dynamic_capabilities = self.dynamic_capabilities.read().clone(); + Cap::compute(&static_capabilities, &dynamic_capabilities) } /// Get the reported capabilities of the running language server and @@ -1143,7 +1166,7 @@ impl LanguageServer { } pub fn update_capabilities(&self, update: impl FnOnce(&mut ServerCapabilities)) { - update(self.capabilities.write().deref_mut()); + update(self.static_capabilities.write().deref_mut()); } pub fn configuration(&self) -> &Value { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index deebaedd74..232374e191 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -74,9 +74,8 @@ use lsp::{ FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture, - MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind, - TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, - WorkspaceFolder, notification::DidRenameFiles, + MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind, TextEdit, + WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -7208,14 +7207,8 @@ impl LspStore { .collect() }; - let document_sync_kind = language_server - .capabilities() - .text_document_sync - .as_ref() - .and_then(|sync| match sync { - lsp::TextDocumentSyncCapability::Kind(kind) => Some(*kind), - lsp::TextDocumentSyncCapability::Options(options) => options.change, - }); + let document_sync_kind = + language_server.effective_capability::(); let content_changes: Vec<_> = match document_sync_kind { Some(lsp::TextDocumentSyncKind::FULL) => { @@ -7276,7 +7269,9 @@ impl LspStore { let local = self.as_local()?; for server in local.language_servers_for_worktree(worktree_id) { - if let Some(include_text) = include_text(server.as_ref()) { + if let Some(include_text) = + server.effective_capability::() + { let text = if include_text { Some(buffer.read(cx).text()) } else { @@ -11834,47 +11829,27 @@ impl LspStore { } } "textDocument/didChange" => { - if let Some(sync_kind) = reg - .register_options - .and_then(|opts| opts.get("syncKind").cloned()) - .map(serde_json::from_value::) - .transpose()? - { - server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.change = Some(sync_kind); - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + if let Some(options) = reg.register_options { + let options: lsp::TextDocumentChangeRegistrationOptions = + serde_json::from_value(options)?; + server.update_dynamic_capabilities(|dyn_caps| { + let map = dyn_caps + .text_document_sync_did_change + .get_or_insert_with(HashMap::default); + map.insert(reg.id, options); }); notify_server_capabilities_updated(&server, cx); } } "textDocument/didSave" => { - if let Some(include_text) = reg - .register_options - .map(|opts| { - let transpose = opts - .get("includeText") - .cloned() - .map(serde_json::from_value::>) - .transpose(); - match transpose { - Ok(value) => Ok(value.flatten()), - Err(e) => Err(e), - } - }) - .transpose()? - { - server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.save = - Some(TextDocumentSyncSaveOptions::SaveOptions(lsp::SaveOptions { - include_text, - })); - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + if let Some(options) = reg.register_options { + let options: lsp::TextDocumentSaveRegistrationOptions = + serde_json::from_value(options)?; + server.update_dynamic_capabilities(|dyn_caps| { + let map = dyn_caps + .text_document_sync_did_save + .get_or_insert_with(HashMap::default); + map.insert(reg.id, options); }); notify_server_capabilities_updated(&server, cx); } @@ -12025,20 +12000,18 @@ impl LspStore { notify_server_capabilities_updated(&server, cx); } "textDocument/didChange" => { - server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.change = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + server.update_dynamic_capabilities(|dyn_caps| { + if let Some(map) = dyn_caps.text_document_sync_did_change.as_mut() { + map.remove(&unreg.id); + } }); notify_server_capabilities_updated(&server, cx); } "textDocument/didSave" => { - server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.save = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + server.update_dynamic_capabilities(|dyn_caps| { + if let Some(map) = dyn_caps.text_document_sync_did_save.as_mut() { + map.remove(&unreg.id); + } }); notify_server_capabilities_updated(&server, cx); } @@ -12149,20 +12122,6 @@ impl LspStore { Ok(()) } - fn take_text_document_sync_options( - capabilities: &mut lsp::ServerCapabilities, - ) -> lsp::TextDocumentSyncOptions { - match capabilities.text_document_sync.take() { - Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { - let mut sync_options = lsp::TextDocumentSyncOptions::default(); - sync_options.change = Some(sync_kind); - sync_options - } - None => lsp::TextDocumentSyncOptions::default(), - } - } - #[cfg(any(test, feature = "test-support"))] pub fn forget_code_lens_task(&mut self, buffer_id: BufferId) -> Option { let data = self.lsp_code_lens.get_mut(&buffer_id)?; @@ -13262,23 +13221,6 @@ async fn populate_labels_for_symbols( } } -fn include_text(server: &lsp::LanguageServer) -> Option { - match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { - // Server wants didSave but didn't specify includeText. - lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), - // Server doesn't want didSave at all. - lsp::TextDocumentSyncSaveOptions::Supported(false) => None, - // Server provided SaveOptions. - lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { - Some(save_options.include_text.unwrap_or(false)) - } - }, - // We do not have any save info. Kind affects didChange only. - lsp::TextDocumentSyncCapability::Kind(_) => None, - } -} - /// Completion items are displayed in a `UniformList`. /// Usually, those items are single-line strings, but in LSP responses, /// completion items `label`, `detail` and `label_details.description` may contain newlines or long spaces.