This commit is contained in:
Smit Barmase 2025-08-26 12:48:48 -07:00 committed by GitHub
commit 9a7945fbf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 157 additions and 94 deletions

View file

@ -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<TextDocumentSyncKind>;
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<bool>;
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,
})
}
}

View file

@ -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<str>,
binary: LanguageServerBinary,
capabilities: RwLock<ServerCapabilities>,
static_capabilities: RwLock<ServerCapabilities>,
dynamic_capabilities: RwLock<DynamicCapabilities>,
/// 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<Vec<CodeActionKind>>,
}
#[derive(Debug, Default, Clone)]
pub struct DynamicCapabilities {
pub text_document_sync_did_change:
Option<HashMap<String, TextDocumentChangeRegistrationOptions>>,
pub text_document_sync_did_save: Option<HashMap<String, TextDocumentSaveRegistrationOptions>>,
}
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::<notification::Initialized>(&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<Cap: EffectiveCapability>(&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 {

View file

@ -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::<lsp::cap::DidChangeTextDocument>();
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::<lsp::cap::DidSaveTextDocument>()
{
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::<lsp::TextDocumentSyncKind>)
.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::<Option<bool>>)
.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<CodeLensTask> {
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<bool> {
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.