Zed did not advertise document highlight dynamic registration support or handle `textDocument/documentHighlight` registrations. Dynamically registered providers therefore fell through to the unhandled-capability path, while servers with a fallback had to advertise a static provider for every attached document. Advertise dynamic registration support and track document highlight registrations and unregistrations alongside the other selector-aware dynamic text document capabilities. Refresh editors whenever the effective document highlight registration state changes, including selector-only changes that leave the merged provider unchanged, so runtime registrations take effect immediately and unregistrations clear stale or in-flight highlights. Add coverage for the client capability, registration lifecycle with multiple selectors sharing identical provider options, and editor refresh behavior when an applicable selector is added or removed. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed’s UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --------- Co-authored-by: GPT-5.6 Sol <noreply@openai.com> Co-authored-by: Kirill Bulatov <kirill@zed.dev>
1293 lines
57 KiB
Rust
1293 lines
57 KiB
Rust
//! Handling of `client/registerCapability` and `client/unregisterCapability` requests,
|
|
//! which let language servers toggle their capabilities at runtime.
|
|
//!
|
|
//! Multiple registrations of the same capability may coexist: the most recent one is
|
|
//! "active" and mirrored into the server's [`lsp::ServerCapabilities`], while the rest
|
|
//! (including the statically declared capability, if any) are kept to be restored when
|
|
//! the active registration is unregistered.
|
|
|
|
use anyhow::{Context as _, Result};
|
|
use collections::{HashMap, HashSet, IndexMap};
|
|
use gpui::{Context, SharedString};
|
|
use lsp::{
|
|
DiagnosticServerCapabilities, LanguageServer, LanguageServerId, OneOf,
|
|
TextDocumentSyncSaveOptions,
|
|
};
|
|
|
|
use crate::lsp_store::{
|
|
LanguageServerState, LspStore, RenamePathsWatchedForServer,
|
|
completion_trigger_characters_for_buffer, lsp_workspace_diagnostics_refresh,
|
|
};
|
|
|
|
#[derive(Debug)]
|
|
pub(super) enum RegistrationSource {
|
|
Static,
|
|
Dynamic(String),
|
|
}
|
|
|
|
impl RegistrationSource {
|
|
pub(super) fn registration_id(&self) -> Option<&str> {
|
|
match self {
|
|
RegistrationSource::Static => None,
|
|
RegistrationSource::Dynamic(id) => Some(id),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) type CapabilityRegistrations<T> = Vec<(RegistrationSource, T)>;
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct CapabilityRegistrationChange {
|
|
active_capability_changed: bool,
|
|
text_document_registration_changed: bool,
|
|
registration_changed: bool,
|
|
}
|
|
|
|
impl CapabilityRegistrationChange {
|
|
fn applicability_may_have_changed(self) -> bool {
|
|
self.active_capability_changed || self.text_document_registration_changed
|
|
}
|
|
|
|
fn ordered_capability_may_have_changed(self) -> bool {
|
|
self.active_capability_changed || self.registration_changed
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub(super) struct DynamicTextDocumentRegistration {
|
|
pub(super) document_selector: Option<lsp::DocumentSelector>,
|
|
pub(super) server_capabilities: lsp::ServerCapabilities,
|
|
}
|
|
|
|
pub(super) type TextDocumentRegistrations =
|
|
HashMap<String, IndexMap<String, DynamicTextDocumentRegistration>>;
|
|
|
|
#[derive(Default, Debug)]
|
|
pub(super) struct DynamicRegistrations {
|
|
pub(super) did_change_watched_files: HashSet<String>,
|
|
pub(super) text_documents: TextDocumentRegistrations,
|
|
workspace_folders: CapabilityRegistrations<lsp::WorkspaceFoldersServerCapabilities>,
|
|
workspace_symbol: CapabilityRegistrations<OneOf<bool, lsp::WorkspaceSymbolOptions>>,
|
|
file_operations: CapabilityRegistrations<lsp::WorkspaceFileOperationsServerCapabilities>,
|
|
execute_command: CapabilityRegistrations<lsp::ExecuteCommandOptions>,
|
|
}
|
|
|
|
impl LspStore {
|
|
fn register_dynamic_text_document_capability<T: Clone + PartialEq>(
|
|
&mut self,
|
|
server: &LanguageServer,
|
|
method: &str,
|
|
registration_id: String,
|
|
options: T,
|
|
document_selector: Option<lsp::DocumentSelector>,
|
|
cx: &mut Context<Self>,
|
|
capability_of: impl Fn(&mut lsp::ServerCapabilities) -> &mut Option<T>,
|
|
) -> anyhow::Result<CapabilityRegistrationChange> {
|
|
let server_id = server.server_id();
|
|
let local = self
|
|
.as_local_mut()
|
|
.context("Expected LSP Store to be local")?;
|
|
let mut server_capabilities = lsp::ServerCapabilities::default();
|
|
*capability_of(&mut server_capabilities) = Some(options);
|
|
let new_registration = DynamicTextDocumentRegistration {
|
|
document_selector,
|
|
server_capabilities,
|
|
};
|
|
let registrations = local
|
|
.language_server_dynamic_registrations
|
|
.entry(server_id)
|
|
.or_default()
|
|
.text_documents
|
|
.entry(method.to_owned())
|
|
.or_default();
|
|
// Guests track registrations by id, so notify on any id-visible change; caches only
|
|
// consume the (selector, options) value set, so refresh flags compare sets.
|
|
let previous_registration = registrations.get(®istration_id);
|
|
let registration_changed = previous_registration != Some(&new_registration);
|
|
let applicable_set_changed = match previous_registration {
|
|
Some(previous_registration) if previous_registration == &new_registration => false,
|
|
Some(previous_registration) => {
|
|
let mut other_registrations = registrations
|
|
.iter()
|
|
.filter(|(id, _)| **id != registration_id)
|
|
.map(|(_, registration)| registration);
|
|
let previous_value_remains = other_registrations
|
|
.clone()
|
|
.any(|registration| registration == previous_registration);
|
|
let new_value_exists =
|
|
other_registrations.any(|registration| registration == &new_registration);
|
|
!previous_value_remains || !new_value_exists
|
|
}
|
|
None => !registrations
|
|
.values()
|
|
.any(|registration| registration == &new_registration),
|
|
};
|
|
if previous_registration.is_some() {
|
|
log::warn!(
|
|
"Received a duplicate {method} registration with ID {registration_id}, replacing the previous one"
|
|
);
|
|
}
|
|
registrations.insert(registration_id, new_registration);
|
|
let new_active_envelope = registrations
|
|
.last()
|
|
.map(|(_, registration)| registration.server_capabilities.clone());
|
|
let new_active = match new_active_envelope {
|
|
Some(mut envelope) => capability_of(&mut envelope).take(),
|
|
None => local
|
|
.initial_server_capabilities
|
|
.get(&server_id)
|
|
.cloned()
|
|
.and_then(|mut capabilities| capability_of(&mut capabilities).take()),
|
|
};
|
|
let mut current_capabilities = server.capabilities();
|
|
let previous_active = capability_of(&mut current_capabilities).take();
|
|
let active_changed = new_active != previous_active;
|
|
if active_changed {
|
|
server.update_capabilities(|capabilities| *capability_of(capabilities) = new_active);
|
|
}
|
|
if active_changed || registration_changed {
|
|
self.notify_server_capabilities_updated(server, cx);
|
|
}
|
|
Ok(CapabilityRegistrationChange {
|
|
active_capability_changed: active_changed,
|
|
text_document_registration_changed: applicable_set_changed,
|
|
registration_changed,
|
|
})
|
|
}
|
|
|
|
fn unregister_dynamic_text_document_capability<T: Clone + PartialEq>(
|
|
&mut self,
|
|
server: &LanguageServer,
|
|
unregistration: &lsp::Unregistration,
|
|
cx: &mut Context<Self>,
|
|
capability_of: impl Fn(&mut lsp::ServerCapabilities) -> &mut Option<T>,
|
|
) -> anyhow::Result<Option<CapabilityRegistrationChange>> {
|
|
let server_id = server.server_id();
|
|
let local = self
|
|
.as_local_mut()
|
|
.context("Expected LSP Store to be local")?;
|
|
let Some(registrations) = local
|
|
.language_server_dynamic_registrations
|
|
.get_mut(&server_id)
|
|
.and_then(|dynamic_registrations| {
|
|
dynamic_registrations
|
|
.text_documents
|
|
.get_mut(&unregistration.method)
|
|
})
|
|
else {
|
|
log::warn!(
|
|
"Attempted to unregister non-existent {} registration with ID {}",
|
|
unregistration.method,
|
|
unregistration.id
|
|
);
|
|
return Ok(None);
|
|
};
|
|
let Some(removed) = registrations.shift_remove(&unregistration.id) else {
|
|
log::warn!(
|
|
"Attempted to unregister non-existent {} registration with ID {}",
|
|
unregistration.method,
|
|
unregistration.id
|
|
);
|
|
return Ok(None);
|
|
};
|
|
let applicable_set_changed = !registrations
|
|
.values()
|
|
.any(|remaining| remaining == &removed);
|
|
let new_active_envelope = registrations
|
|
.last()
|
|
.map(|(_, registration)| registration.server_capabilities.clone());
|
|
let new_active = match new_active_envelope {
|
|
Some(mut envelope) => capability_of(&mut envelope).take(),
|
|
None => local
|
|
.initial_server_capabilities
|
|
.get(&server_id)
|
|
.cloned()
|
|
.and_then(|mut capabilities| capability_of(&mut capabilities).take()),
|
|
};
|
|
let mut current_capabilities = server.capabilities();
|
|
let previous_active = capability_of(&mut current_capabilities).take();
|
|
let active_changed = new_active != previous_active;
|
|
if active_changed {
|
|
server.update_capabilities(|capabilities| *capability_of(capabilities) = new_active);
|
|
}
|
|
self.notify_server_capabilities_updated(server, cx);
|
|
Ok(Some(CapabilityRegistrationChange {
|
|
active_capability_changed: active_changed,
|
|
text_document_registration_changed: applicable_set_changed,
|
|
registration_changed: true,
|
|
}))
|
|
}
|
|
|
|
/// Returns `true` when the registration changed the server's active capability value:
|
|
/// duplicate-ID replacements of non-active registrations and (re-)registrations with
|
|
/// options identical to the active ones do not.
|
|
fn register_dynamic_capability<T: Clone + PartialEq>(
|
|
&mut self,
|
|
server: &LanguageServer,
|
|
method: &str,
|
|
registration_id: String,
|
|
options: T,
|
|
cx: &mut Context<Self>,
|
|
registrations_of: impl FnOnce(&mut DynamicRegistrations) -> &mut CapabilityRegistrations<T>,
|
|
capability_of: impl Fn(&mut lsp::ServerCapabilities) -> &mut Option<T>,
|
|
) -> anyhow::Result<bool> {
|
|
let server_id = server.server_id();
|
|
let local = self
|
|
.as_local_mut()
|
|
.context("Expected LSP Store to be local")?;
|
|
let dynamic_registrations = local
|
|
.language_server_dynamic_registrations
|
|
.entry(server_id)
|
|
.or_default();
|
|
let registrations = registrations_of(dynamic_registrations);
|
|
if registrations.is_empty() {
|
|
let mut initial_capabilities = server.capabilities();
|
|
if let Some(static_options) = capability_of(&mut initial_capabilities).take() {
|
|
registrations.push((RegistrationSource::Static, static_options));
|
|
}
|
|
}
|
|
let previously_active = registrations.last().map(|(_, options)| options.clone());
|
|
if let Some(index) = registrations
|
|
.iter()
|
|
.position(|(source, _)| source.registration_id() == Some(registration_id.as_str()))
|
|
{
|
|
log::warn!(
|
|
"Received a duplicate {method} registration with ID {registration_id}, replacing the previous one"
|
|
);
|
|
registrations[index].1 = options;
|
|
} else {
|
|
registrations.push((RegistrationSource::Dynamic(registration_id), options));
|
|
}
|
|
let active = registrations.last().map(|(_, options)| options.clone());
|
|
let active_changed = active != previously_active;
|
|
if active_changed {
|
|
server.update_capabilities(|capabilities| *capability_of(capabilities) = active);
|
|
self.notify_server_capabilities_updated(server, cx);
|
|
}
|
|
Ok(active_changed)
|
|
}
|
|
|
|
fn unregister_dynamic_capability<T: Clone + PartialEq>(
|
|
&mut self,
|
|
server: &LanguageServer,
|
|
unregistration: &lsp::Unregistration,
|
|
cx: &mut Context<Self>,
|
|
registrations_of: impl FnOnce(&mut DynamicRegistrations) -> &mut CapabilityRegistrations<T>,
|
|
capability_of: impl FnOnce(&mut lsp::ServerCapabilities) -> &mut Option<T>,
|
|
) -> anyhow::Result<Option<bool>> {
|
|
let server_id = server.server_id();
|
|
let local = self
|
|
.as_local_mut()
|
|
.context("Expected LSP Store to be local")?;
|
|
let Some(dynamic_registrations) = local
|
|
.language_server_dynamic_registrations
|
|
.get_mut(&server_id)
|
|
else {
|
|
log::warn!(
|
|
"Attempted to unregister non-existent {} registration with ID {}",
|
|
unregistration.method,
|
|
unregistration.id
|
|
);
|
|
return Ok(None);
|
|
};
|
|
let registrations = registrations_of(dynamic_registrations);
|
|
let Some(index) = registrations
|
|
.iter()
|
|
.position(|(source, _)| source.registration_id() == Some(unregistration.id.as_str()))
|
|
else {
|
|
log::warn!(
|
|
"Attempted to unregister non-existent {} registration with ID {}",
|
|
unregistration.method,
|
|
unregistration.id
|
|
);
|
|
return Ok(None);
|
|
};
|
|
let removed_active = index + 1 == registrations.len();
|
|
let (_, removed_options) = registrations.remove(index);
|
|
let restored = registrations.last().map(|(_, options)| options.clone());
|
|
if !removed_active || restored.as_ref() == Some(&removed_options) {
|
|
return Ok(Some(false));
|
|
}
|
|
server.update_capabilities(|capabilities| *capability_of(capabilities) = restored);
|
|
self.notify_server_capabilities_updated(server, cx);
|
|
Ok(Some(true))
|
|
}
|
|
|
|
fn update_paths_watched_for_rename(&mut self, server: &LanguageServer) {
|
|
let Some(local) = self.as_local_mut() else {
|
|
return;
|
|
};
|
|
let watcher = server
|
|
.capabilities()
|
|
.workspace
|
|
.and_then(|workspace| workspace.file_operations)
|
|
.and_then(|file_operations| {
|
|
let did_rename_caps = file_operations.did_rename.as_ref();
|
|
let will_rename_caps = file_operations.will_rename.as_ref();
|
|
did_rename_caps.or(will_rename_caps)?;
|
|
Some(
|
|
RenamePathsWatchedForServer::default()
|
|
.with_did_rename_patterns(did_rename_caps)
|
|
.with_will_rename_patterns(will_rename_caps),
|
|
)
|
|
});
|
|
match watcher {
|
|
Some(watcher) => {
|
|
local
|
|
.language_server_paths_watched_for_rename
|
|
.insert(server.server_id(), watcher);
|
|
}
|
|
None => {
|
|
local
|
|
.language_server_paths_watched_for_rename
|
|
.remove(&server.server_id());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn apply_completion_triggers(&self, server_id: LanguageServerId, cx: &mut Context<Self>) {
|
|
let Some(local) = self.as_local() else {
|
|
return;
|
|
};
|
|
let Some(LanguageServerState::Running { adapter, .. }) =
|
|
local.language_servers.get(&server_id)
|
|
else {
|
|
return;
|
|
};
|
|
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 {
|
|
handle.update(cx, |buffer, cx| {
|
|
let triggers =
|
|
completion_trigger_characters_for_buffer(local, server_id, adapter, buffer);
|
|
buffer.set_completion_triggers(server_id, triggers, cx);
|
|
});
|
|
}
|
|
}
|
|
|
|
pub(super) fn register_server_capabilities(
|
|
&mut self,
|
|
server_id: LanguageServerId,
|
|
params: lsp::RegistrationParams,
|
|
cx: &mut Context<Self>,
|
|
) -> anyhow::Result<()> {
|
|
let server = self
|
|
.language_server_for_id(server_id)
|
|
.with_context(|| format!("no server {server_id} found"))?;
|
|
for reg in params.registrations {
|
|
match reg.method.as_str() {
|
|
"workspace/didChangeWatchedFiles" => {
|
|
if let Some(options) = reg.register_options {
|
|
let notify = if let Some(local_lsp_store) = self.as_local_mut() {
|
|
let caps = serde_json::from_value(options)?;
|
|
local_lsp_store
|
|
.on_lsp_did_change_watched_files(server_id, ®.id, caps, cx);
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
if notify {
|
|
self.notify_server_capabilities_updated(&server, cx);
|
|
}
|
|
}
|
|
}
|
|
"workspace/didChangeConfiguration" => {
|
|
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
|
|
}
|
|
"workspace/didChangeWorkspaceFolders" => {
|
|
// In this case register options is an empty object, we can ignore it
|
|
let caps = lsp::WorkspaceFoldersServerCapabilities {
|
|
supported: Some(true),
|
|
change_notifications: Some(OneOf::Right(reg.id.clone())),
|
|
};
|
|
self.register_dynamic_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
caps,
|
|
cx,
|
|
|registrations| &mut registrations.workspace_folders,
|
|
|capabilities| {
|
|
&mut capabilities
|
|
.workspace
|
|
.get_or_insert_default()
|
|
.workspace_folders
|
|
},
|
|
)?;
|
|
}
|
|
"workspace/symbol" => {
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
self.register_dynamic_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
options,
|
|
cx,
|
|
|registrations| &mut registrations.workspace_symbol,
|
|
|capabilities| &mut capabilities.workspace_symbol_provider,
|
|
)?;
|
|
}
|
|
"workspace/fileOperations" => {
|
|
if let Some(options) = reg.register_options {
|
|
let caps = serde_json::from_value(options)?;
|
|
if self.register_dynamic_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
caps,
|
|
cx,
|
|
|registrations| &mut registrations.file_operations,
|
|
|capabilities| {
|
|
&mut capabilities
|
|
.workspace
|
|
.get_or_insert_default()
|
|
.file_operations
|
|
},
|
|
)? {
|
|
self.update_paths_watched_for_rename(&server);
|
|
}
|
|
}
|
|
}
|
|
"workspace/executeCommand" => {
|
|
if let Some(options) = reg.register_options {
|
|
let options = serde_json::from_value(options)?;
|
|
self.register_dynamic_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
options,
|
|
cx,
|
|
|registrations| &mut registrations.execute_command,
|
|
|capabilities| &mut capabilities.execute_command_provider,
|
|
)?;
|
|
}
|
|
}
|
|
"textDocument/rangeFormatting" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
options,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_range_formatting_provider,
|
|
)?;
|
|
}
|
|
"textDocument/onTypeFormatting" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
if let Some(options) = reg
|
|
.register_options
|
|
.map(serde_json::from_value)
|
|
.transpose()?
|
|
{
|
|
self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
options,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_on_type_formatting_provider,
|
|
)?;
|
|
}
|
|
}
|
|
"textDocument/formatting" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
options,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_formatting_provider,
|
|
)?;
|
|
}
|
|
"textDocument/rename" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
options,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.rename_provider,
|
|
)?;
|
|
}
|
|
"textDocument/inlayHint" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
if self
|
|
.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
options,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.inlay_hint_provider,
|
|
)?
|
|
.applicability_may_have_changed()
|
|
{
|
|
self.refresh_inlay_hints(server_id, cx);
|
|
}
|
|
}
|
|
"textDocument/documentSymbol" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
if self
|
|
.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
options,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_symbol_provider,
|
|
)?
|
|
.applicability_may_have_changed()
|
|
{
|
|
self.refresh_document_symbols(Some(server_id), cx);
|
|
}
|
|
}
|
|
"textDocument/codeAction" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
let provider = match options {
|
|
OneOf::Left(value) => lsp::CodeActionProviderCapability::Simple(value),
|
|
OneOf::Right(caps) => caps,
|
|
};
|
|
self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
provider,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.code_action_provider,
|
|
)?;
|
|
}
|
|
"textDocument/prepareCallHierarchy" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
let provider = match options {
|
|
OneOf::Left(value) => lsp::CallHierarchyServerCapability::Simple(value),
|
|
OneOf::Right(options) => {
|
|
lsp::CallHierarchyServerCapability::Options(options)
|
|
}
|
|
};
|
|
self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
provider,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.call_hierarchy_provider,
|
|
)?;
|
|
}
|
|
"textDocument/definition" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
options,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.definition_provider,
|
|
)?;
|
|
}
|
|
"textDocument/documentHighlight" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
if self
|
|
.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
options,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_highlight_provider,
|
|
)?
|
|
.applicability_may_have_changed()
|
|
{
|
|
self.refresh_document_highlights(Some(server_id), cx);
|
|
}
|
|
}
|
|
"textDocument/completion" => {
|
|
if let Some(registration_options) = reg
|
|
.register_options
|
|
.map(serde_json::from_value::<lsp::CompletionRegistrationOptions>)
|
|
.transpose()?
|
|
{
|
|
self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
registration_options.completion_options,
|
|
registration_options
|
|
.text_document_registration_options
|
|
.document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.completion_provider,
|
|
)?;
|
|
self.apply_completion_triggers(server_id, cx);
|
|
}
|
|
}
|
|
"textDocument/hover" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
let provider = match options {
|
|
OneOf::Left(value) => lsp::HoverProviderCapability::Simple(value),
|
|
OneOf::Right(caps) => caps,
|
|
};
|
|
self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
provider,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.hover_provider,
|
|
)?;
|
|
}
|
|
"textDocument/signatureHelp" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
if let Some(caps) = reg
|
|
.register_options
|
|
.map(serde_json::from_value)
|
|
.transpose()?
|
|
{
|
|
self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
caps,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.signature_help_provider,
|
|
)?;
|
|
}
|
|
}
|
|
"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 = take_text_document_sync_options(capabilities);
|
|
sync_options.change = Some(sync_kind);
|
|
capabilities.text_document_sync =
|
|
Some(lsp::TextDocumentSyncCapability::Options(sync_options));
|
|
});
|
|
self.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 = 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));
|
|
});
|
|
self.notify_server_capabilities_updated(&server, cx);
|
|
}
|
|
}
|
|
"textDocument/codeLens" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
if let Some(options) = reg
|
|
.register_options
|
|
.map(serde_json::from_value)
|
|
.transpose()?
|
|
{
|
|
if self
|
|
.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
options,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.code_lens_provider,
|
|
)?
|
|
.applicability_may_have_changed()
|
|
{
|
|
self.refresh_code_lens(Some(server_id), cx);
|
|
}
|
|
}
|
|
}
|
|
"textDocument/diagnostic" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
if let Some(caps) = reg
|
|
.register_options
|
|
.map(serde_json::from_value::<DiagnosticServerCapabilities>)
|
|
.transpose()?
|
|
{
|
|
let registration_id = reg.id.clone();
|
|
self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
caps.clone(),
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.diagnostic_provider,
|
|
)?;
|
|
|
|
let supports_workspace_diagnostics =
|
|
|capabilities: &DiagnosticServerCapabilities| match capabilities {
|
|
DiagnosticServerCapabilities::Options(diagnostic_options) => {
|
|
diagnostic_options.workspace_diagnostics
|
|
}
|
|
DiagnosticServerCapabilities::RegistrationOptions(
|
|
diagnostic_registration_options,
|
|
) => {
|
|
diagnostic_registration_options
|
|
.diagnostic_options
|
|
.workspace_diagnostics
|
|
}
|
|
};
|
|
|
|
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")?;
|
|
if let LanguageServerState::Running {
|
|
workspace_diagnostics_refresh_tasks,
|
|
..
|
|
} = state
|
|
{
|
|
workspace_diagnostics_refresh_tasks
|
|
.remove(&Some(registration_id.clone()));
|
|
if supports_workspace_diagnostics(&caps)
|
|
&& let Some(task) = lsp_workspace_diagnostics_refresh(
|
|
Some(registration_id.clone()),
|
|
caps,
|
|
server.clone(),
|
|
cx,
|
|
)
|
|
{
|
|
workspace_diagnostics_refresh_tasks
|
|
.insert(Some(registration_id), task);
|
|
}
|
|
}
|
|
|
|
let _ = self.pull_document_diagnostics_for_server(server_id, None, cx);
|
|
}
|
|
}
|
|
"textDocument/documentColor" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
let provider = match options {
|
|
OneOf::Left(value) => lsp::ColorProviderCapability::Simple(value),
|
|
OneOf::Right(caps) => caps,
|
|
};
|
|
if self
|
|
.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
provider,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.color_provider,
|
|
)?
|
|
.applicability_may_have_changed()
|
|
{
|
|
self.refresh_document_colors(Some(server_id), cx);
|
|
}
|
|
}
|
|
"textDocument/foldingRange" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
let options = parse_register_capabilities(reg.register_options)?;
|
|
let provider = match options {
|
|
OneOf::Left(value) => lsp::FoldingRangeProviderCapability::Simple(value),
|
|
OneOf::Right(caps) => caps,
|
|
};
|
|
if self
|
|
.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
provider,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.folding_range_provider,
|
|
)?
|
|
.applicability_may_have_changed()
|
|
{
|
|
self.refresh_folding_ranges(Some(server_id), cx);
|
|
}
|
|
}
|
|
"textDocument/documentLink" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
if let Some(caps) = reg
|
|
.register_options
|
|
.map(serde_json::from_value)
|
|
.transpose()?
|
|
{
|
|
if self
|
|
.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
caps,
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_link_provider,
|
|
)?
|
|
.applicability_may_have_changed()
|
|
{
|
|
self.refresh_document_links(Some(server_id), cx);
|
|
}
|
|
}
|
|
}
|
|
"textDocument/semanticTokens" => {
|
|
let document_selector =
|
|
parse_text_document_registration(reg.register_options.as_ref())?;
|
|
if let Some(caps) = reg
|
|
.register_options
|
|
.map(serde_json::from_value::<lsp::SemanticTokensRegistrationOptions>)
|
|
.transpose()?
|
|
{
|
|
if self.register_dynamic_text_document_capability(
|
|
&server,
|
|
®.method,
|
|
reg.id,
|
|
lsp::SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(caps),
|
|
document_selector,
|
|
cx,
|
|
|capabilities| &mut capabilities.semantic_tokens_provider,
|
|
)?
|
|
.ordered_capability_may_have_changed()
|
|
{
|
|
// Re-query already-open buffers, which would otherwise keep
|
|
// tree-sitter-only highlighting until edited.
|
|
self.refresh_semantic_tokens(server_id, cx);
|
|
}
|
|
}
|
|
}
|
|
_ => log::warn!("unhandled capability registration: {reg:?}"),
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn unregister_server_capabilities(
|
|
&mut self,
|
|
server_id: LanguageServerId,
|
|
params: lsp::UnregistrationParams,
|
|
cx: &mut Context<Self>,
|
|
) -> anyhow::Result<()> {
|
|
let server = self
|
|
.language_server_for_id(server_id)
|
|
.with_context(|| format!("no server {server_id} found"))?;
|
|
for unreg in params.unregisterations.iter() {
|
|
match unreg.method.as_str() {
|
|
"workspace/didChangeWatchedFiles" => {
|
|
let notify = if let Some(local_lsp_store) = self.as_local_mut() {
|
|
local_lsp_store
|
|
.on_lsp_unregister_did_change_watched_files(server_id, &unreg.id, cx);
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
if notify {
|
|
self.notify_server_capabilities_updated(&server, cx);
|
|
}
|
|
}
|
|
"workspace/didChangeConfiguration" => {
|
|
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
|
|
}
|
|
"workspace/didChangeWorkspaceFolders" => {
|
|
self.unregister_dynamic_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|registrations| &mut registrations.workspace_folders,
|
|
|capabilities| {
|
|
&mut capabilities
|
|
.workspace
|
|
.get_or_insert_default()
|
|
.workspace_folders
|
|
},
|
|
)?;
|
|
}
|
|
"workspace/symbol" => {
|
|
self.unregister_dynamic_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|registrations| &mut registrations.workspace_symbol,
|
|
|capabilities| &mut capabilities.workspace_symbol_provider,
|
|
)?;
|
|
}
|
|
"workspace/fileOperations" => {
|
|
if self.unregister_dynamic_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|registrations| &mut registrations.file_operations,
|
|
|capabilities| {
|
|
&mut capabilities
|
|
.workspace
|
|
.get_or_insert_default()
|
|
.file_operations
|
|
},
|
|
)? == Some(true)
|
|
{
|
|
self.update_paths_watched_for_rename(&server);
|
|
}
|
|
}
|
|
"workspace/executeCommand" => {
|
|
self.unregister_dynamic_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|registrations| &mut registrations.execute_command,
|
|
|capabilities| &mut capabilities.execute_command_provider,
|
|
)?;
|
|
}
|
|
"textDocument/rangeFormatting" => {
|
|
self.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_range_formatting_provider,
|
|
)?;
|
|
}
|
|
"textDocument/onTypeFormatting" => {
|
|
self.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_on_type_formatting_provider,
|
|
)?;
|
|
}
|
|
"textDocument/formatting" => {
|
|
self.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_formatting_provider,
|
|
)?;
|
|
}
|
|
"textDocument/rename" => {
|
|
self.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.rename_provider,
|
|
)?;
|
|
}
|
|
"textDocument/codeAction" => {
|
|
self.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.code_action_provider,
|
|
)?;
|
|
}
|
|
"textDocument/prepareCallHierarchy" => {
|
|
self.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.call_hierarchy_provider,
|
|
)?;
|
|
}
|
|
"textDocument/definition" => {
|
|
self.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.definition_provider,
|
|
)?;
|
|
}
|
|
"textDocument/documentHighlight" => {
|
|
if self
|
|
.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_highlight_provider,
|
|
)?
|
|
.is_some_and(CapabilityRegistrationChange::applicability_may_have_changed)
|
|
{
|
|
self.refresh_document_highlights(Some(server_id), cx);
|
|
}
|
|
}
|
|
"textDocument/completion" => {
|
|
if self
|
|
.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.completion_provider,
|
|
)?
|
|
.is_some()
|
|
{
|
|
self.apply_completion_triggers(server_id, cx);
|
|
}
|
|
}
|
|
"textDocument/hover" => {
|
|
self.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.hover_provider,
|
|
)?;
|
|
}
|
|
"textDocument/signatureHelp" => {
|
|
self.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.signature_help_provider,
|
|
)?;
|
|
}
|
|
"textDocument/semanticTokens" => {
|
|
if self
|
|
.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.semantic_tokens_provider,
|
|
)?
|
|
.is_some_and(
|
|
CapabilityRegistrationChange::ordered_capability_may_have_changed,
|
|
)
|
|
{
|
|
self.refresh_semantic_tokens(server_id, cx);
|
|
}
|
|
}
|
|
"textDocument/didChange" => {
|
|
server.update_capabilities(|capabilities| {
|
|
let mut sync_options = take_text_document_sync_options(capabilities);
|
|
sync_options.change = None;
|
|
capabilities.text_document_sync =
|
|
Some(lsp::TextDocumentSyncCapability::Options(sync_options));
|
|
});
|
|
self.notify_server_capabilities_updated(&server, cx);
|
|
}
|
|
"textDocument/didSave" => {
|
|
server.update_capabilities(|capabilities| {
|
|
let mut sync_options = take_text_document_sync_options(capabilities);
|
|
sync_options.save = None;
|
|
capabilities.text_document_sync =
|
|
Some(lsp::TextDocumentSyncCapability::Options(sync_options));
|
|
});
|
|
self.notify_server_capabilities_updated(&server, cx);
|
|
}
|
|
"textDocument/inlayHint" => {
|
|
if self
|
|
.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.inlay_hint_provider,
|
|
)?
|
|
.is_some_and(CapabilityRegistrationChange::applicability_may_have_changed)
|
|
{
|
|
self.refresh_inlay_hints(server_id, cx);
|
|
}
|
|
}
|
|
"textDocument/documentSymbol" => {
|
|
if self
|
|
.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_symbol_provider,
|
|
)?
|
|
.is_some_and(CapabilityRegistrationChange::applicability_may_have_changed)
|
|
{
|
|
self.refresh_document_symbols(Some(server_id), cx);
|
|
}
|
|
}
|
|
"textDocument/codeLens" => {
|
|
if self
|
|
.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.code_lens_provider,
|
|
)?
|
|
.is_some_and(CapabilityRegistrationChange::applicability_may_have_changed)
|
|
{
|
|
self.refresh_code_lens(Some(server_id), cx);
|
|
}
|
|
}
|
|
"textDocument/diagnostic" => {
|
|
if self
|
|
.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.diagnostic_provider,
|
|
)?
|
|
.is_some()
|
|
{
|
|
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")?;
|
|
if let LanguageServerState::Running {
|
|
workspace_diagnostics_refresh_tasks,
|
|
..
|
|
} = state
|
|
{
|
|
workspace_diagnostics_refresh_tasks.remove(&Some(unreg.id.clone()));
|
|
}
|
|
|
|
self.clear_unregistered_diagnostics(
|
|
server_id,
|
|
SharedString::from(unreg.id.clone()),
|
|
cx,
|
|
)?;
|
|
}
|
|
}
|
|
"textDocument/documentColor" => {
|
|
if self
|
|
.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.color_provider,
|
|
)?
|
|
.is_some_and(CapabilityRegistrationChange::applicability_may_have_changed)
|
|
{
|
|
self.refresh_document_colors(Some(server_id), cx);
|
|
}
|
|
}
|
|
"textDocument/foldingRange" => {
|
|
if self
|
|
.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.folding_range_provider,
|
|
)?
|
|
.is_some_and(CapabilityRegistrationChange::applicability_may_have_changed)
|
|
{
|
|
self.refresh_folding_ranges(Some(server_id), cx);
|
|
}
|
|
}
|
|
"textDocument/documentLink" => {
|
|
if self
|
|
.unregister_dynamic_text_document_capability(
|
|
&server,
|
|
unreg,
|
|
cx,
|
|
|capabilities| &mut capabilities.document_link_provider,
|
|
)?
|
|
.is_some_and(CapabilityRegistrationChange::applicability_may_have_changed)
|
|
{
|
|
self.refresh_document_links(Some(server_id), cx);
|
|
}
|
|
}
|
|
_ => log::warn!("unhandled capability unregistration: {unreg:?}"),
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn parse_text_document_registration(
|
|
register_options: Option<&serde_json::Value>,
|
|
) -> Result<Option<lsp::DocumentSelector>> {
|
|
Ok(register_options
|
|
.cloned()
|
|
.map(serde_json::from_value::<lsp::TextDocumentRegistrationOptions>)
|
|
.transpose()?
|
|
.and_then(|options| options.document_selector))
|
|
}
|
|
|
|
// Registration with registerOptions as null, should fallback to true.
|
|
// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133
|
|
fn parse_register_capabilities<T: serde::de::DeserializeOwned>(
|
|
register_options: Option<serde_json::Value>,
|
|
) -> Result<OneOf<bool, T>> {
|
|
Ok(match register_options {
|
|
Some(options) => OneOf::Right(serde_json::from_value::<T>(options)?),
|
|
None => OneOf::Left(true),
|
|
})
|
|
}
|
|
|
|
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(),
|
|
}
|
|
}
|