use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
#[cfg(test)]
use lsp_types::ServerCapabilities;
use lsp_types::notification::Notification;
use lsp_types::request::Request;
use lsp_types::{
InitializeParams, InitializedParams, ServerInfo, TextDocumentSyncCapability,
TextDocumentSyncKind, TextDocumentSyncOptions, TextDocumentSyncSaveOptions,
};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use tokio_util::sync::CancellationToken;
use tracing::warn;
use crate::FileProvider;
use crate::capability::{CapabilityBuilder, GeneratedCapabilities};
use crate::codec::erase_value;
use crate::context::Context;
use crate::error::{BuildError, LspError};
use crate::features::{FeatureSpec, NotificationFeatureSpec};
use crate::file_provider::{SharedFileProvider, erase};
use crate::runtime::{TaskFuture, TaskSend};
use crate::service::{Layer, UserLayer};
const RESERVED_METHODS: &[&str] = &[
"initialize",
"shutdown",
"exit",
"initialized",
"$/cancelRequest",
];
const EXECUTE_COMMAND_METHOD: &str = "workspace/executeCommand";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ProtocolNotification {
Open,
Change,
Close,
WillSave,
Save,
WorkspaceFolders,
Configuration,
Trace,
ProgressCancel,
}
impl ProtocolNotification {
const OPEN_METHOD: &'static str = "textDocument/didOpen";
const CHANGE_METHOD: &'static str = "textDocument/didChange";
const CLOSE_METHOD: &'static str = "textDocument/didClose";
const WILL_SAVE_METHOD: &'static str = "textDocument/willSave";
const SAVE_METHOD: &'static str = "textDocument/didSave";
pub(crate) fn from_method(method: &str) -> Option<Self> {
match method {
Self::OPEN_METHOD => Some(Self::Open),
Self::CHANGE_METHOD => Some(Self::Change),
Self::CLOSE_METHOD => Some(Self::Close),
Self::WILL_SAVE_METHOD => Some(Self::WillSave),
Self::SAVE_METHOD => Some(Self::Save),
"workspace/didChangeWorkspaceFolders" => Some(Self::WorkspaceFolders),
"workspace/didChangeConfiguration" => Some(Self::Configuration),
"$/setTrace" => Some(Self::Trace),
"window/workDoneProgress/cancel" => Some(Self::ProgressCancel),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct DocumentSyncSettings {
pub(crate) capability: TextDocumentSyncCapability,
pub(crate) options: TextDocumentSyncOptions,
}
type HandlerFuture = Pin<Box<dyn TaskFuture<Result<Value, LspError>>>>;
type NotificationFuture = Pin<Box<dyn TaskFuture<()>>>;
#[cfg(not(target_arch = "wasm32"))]
#[doc(hidden)]
pub trait SharedHandler<Args, Output>: TaskSend + Sync {
fn invoke(&self, args: Args) -> Output;
}
#[cfg(target_arch = "wasm32")]
#[doc(hidden)]
pub trait SharedHandler<Args, Output>: TaskSend {
fn invoke(&self, args: Args) -> Output;
}
macro_rules! impl_shared_handler {
($($arg:ident),+) => {
#[cfg(not(target_arch = "wasm32"))]
impl<F, Output, $($arg),+> SharedHandler<($($arg,)+), Output> for F
where
F: Fn($($arg),+) -> Output + TaskSend + Sync,
{
#[allow(non_snake_case)]
fn invoke(&self, ($($arg,)+): ($($arg,)+)) -> Output {
self($($arg),+)
}
}
#[cfg(target_arch = "wasm32")]
impl<F, Output, $($arg),+> SharedHandler<($($arg,)+), Output> for F
where
F: Fn($($arg),+) -> Output + TaskSend,
{
#[allow(non_snake_case)]
fn invoke(&self, ($($arg,)+): ($($arg,)+)) -> Output {
self($($arg),+)
}
}
};
}
impl_shared_handler!(A, B);
impl_shared_handler!(A, B, C);
impl_shared_handler!(A, B, C, D);
pub(crate) trait ConfigureInitializeCallback<S>: TaskSend {
fn invoke(
self: Box<Self>,
params: &InitializeParams,
registrar: &mut InitializeRegistrar<S>,
) -> Result<(), LspError>;
}
impl<S, F> ConfigureInitializeCallback<S> for F
where
F: FnOnce(&InitializeParams, &mut InitializeRegistrar<S>) -> Result<(), LspError> + TaskSend,
{
fn invoke(
self: Box<Self>,
params: &InitializeParams,
registrar: &mut InitializeRegistrar<S>,
) -> Result<(), LspError> {
self(params, registrar)
}
}
pub(crate) type ErasedRequestHandler<S> =
Box<dyn SharedHandler<(Arc<S>, Context, Value, CancellationToken), HandlerFuture>>;
pub(crate) type ErasedNotificationHandler<S> =
Box<dyn SharedHandler<(Arc<S>, Context, Value), NotificationFuture>>;
pub(crate) type ErasedCommandHandler<S> =
Box<dyn SharedHandler<(Arc<S>, Context, Vec<Value>, CancellationToken), HandlerFuture>>;
pub(crate) type ConfigureInitialize<S> = Box<dyn ConfigureInitializeCallback<S>>;
type OnInitializeFuture = Pin<Box<dyn TaskFuture<Result<Option<ServerInfo>, LspError>>>>;
pub(crate) type OnInitialize<S> = Box<
dyn SharedHandler<(Arc<S>, Context, InitializeParams, CancellationToken), OnInitializeFuture>,
>;
pub(crate) type OnInitialized<S> =
Box<dyn SharedHandler<(Arc<S>, Context, InitializedParams), NotificationFuture>>;
type OnShutdownFuture = Pin<Box<dyn TaskFuture<Result<(), LspError>>>>;
pub(crate) type OnShutdown<S> =
Box<dyn SharedHandler<(Arc<S>, Context, (), CancellationToken), OnShutdownFuture>>;
pub(crate) type OnExit<S> = Box<dyn SharedHandler<(Arc<S>, Context), NotificationFuture>>;
fn erase_request<S, R, H, Fut>(handler: H) -> ErasedRequestHandler<S>
where
S: Send + Sync + 'static,
R: Request,
H: Fn(Arc<S>, Context, R::Params, CancellationToken) -> Fut
+ SharedHandler<(Arc<S>, Context, R::Params, CancellationToken), Fut>
+ 'static,
Fut: Future<Output = Result<R::Result, LspError>> + TaskSend + 'static,
{
let handler = Arc::new(handler);
Box::new(move |state, ctx, params, ct| -> HandlerFuture {
let handler = Arc::clone(&handler);
Box::pin(async move {
let parsed: R::Params =
serde_json::from_value(params).map_err(LspError::invalid_params)?;
let result = handler.invoke((state, ctx, parsed, ct)).await?;
erase_value(result)
})
})
}
fn erase_notification<S, N, H, Fut>(handler: H) -> ErasedNotificationHandler<S>
where
S: Send + Sync + 'static,
N: Notification,
H: Fn(Arc<S>, Context, N::Params) -> Fut
+ SharedHandler<(Arc<S>, Context, N::Params), Fut>
+ 'static,
Fut: Future<Output = ()> + TaskSend + 'static,
{
let handler = Arc::new(handler);
Box::new(move |state, ctx, params| -> NotificationFuture {
let handler = Arc::clone(&handler);
Box::pin(async move {
let parsed: N::Params = match serde_json::from_value(params) {
Ok(parsed) => parsed,
Err(error) => {
warn!(
method = N::METHOD,
%error,
"dropping notification with malformed params"
);
return;
}
};
handler.invoke((state, ctx, parsed)).await;
})
})
}
pub(crate) struct Registrations<S> {
requests: HashMap<String, ErasedRequestHandler<S>>,
notifications: HashMap<String, ErasedNotificationHandler<S>>,
built_in_hooks: HashMap<String, ErasedNotificationHandler<S>>,
commands: HashMap<String, ErasedCommandHandler<S>>,
capabilities: CapabilityBuilder,
document_sync: Option<TextDocumentSyncOptions>,
}
impl<S: Send + Sync + 'static> Registrations<S> {
fn new() -> Self {
Self {
requests: HashMap::new(),
notifications: HashMap::new(),
built_in_hooks: HashMap::new(),
commands: HashMap::new(),
capabilities: CapabilityBuilder::default(),
document_sync: None,
}
}
fn add_feature<F, H, Fut>(&mut self, spec: F, handler: H) -> Result<(), BuildError>
where
F: FeatureSpec,
H: Fn(Arc<S>, Context, <F::Marker as Request>::Params, CancellationToken) -> Fut
+ SharedHandler<
(
Arc<S>,
Context,
<F::Marker as Request>::Params,
CancellationToken,
),
Fut,
> + 'static,
Fut: Future<Output = Result<<F::Marker as Request>::Result, LspError>> + TaskSend + 'static,
{
let method = <F::Marker as Request>::METHOD.to_string();
let erased = erase_request::<S, F::Marker, H, Fut>(handler);
self.insert_request(method, erased)?;
spec.contribute(&mut self.capabilities)
}
fn add_request<R, H, Fut>(&mut self, handler: H) -> Result<(), BuildError>
where
R: Request,
H: Fn(Arc<S>, Context, R::Params, CancellationToken) -> Fut
+ SharedHandler<(Arc<S>, Context, R::Params, CancellationToken), Fut>
+ 'static,
Fut: Future<Output = Result<R::Result, LspError>> + TaskSend + 'static,
{
let method = R::METHOD.to_string();
let erased = erase_request::<S, R, H, Fut>(handler);
self.insert_request(method, erased)
}
fn insert_request(
&mut self,
method: String,
erased: ErasedRequestHandler<S>,
) -> Result<(), BuildError> {
if RESERVED_METHODS.contains(&method.as_str()) {
return Err(BuildError::ReservedMethod(method));
}
if self.requests.insert(method.clone(), erased).is_some() {
return Err(BuildError::DuplicateMethod(method));
}
Ok(())
}
fn add_notification<N, H, Fut>(&mut self, handler: H) -> Result<(), BuildError>
where
N: Notification,
H: Fn(Arc<S>, Context, N::Params) -> Fut
+ SharedHandler<(Arc<S>, Context, N::Params), Fut>
+ 'static,
Fut: Future<Output = ()> + TaskSend + 'static,
{
let method = N::METHOD.to_string();
let erased = erase_notification::<S, N, H, Fut>(handler);
self.insert_notification(method, erased)
}
fn add_feature_notification<F, H, Fut>(&mut self, spec: F, handler: H) -> Result<(), BuildError>
where
F: NotificationFeatureSpec,
H: Fn(Arc<S>, Context, <F::Marker as Notification>::Params) -> Fut
+ SharedHandler<(Arc<S>, Context, <F::Marker as Notification>::Params), Fut>
+ 'static,
Fut: Future<Output = ()> + TaskSend + 'static,
{
let method = <F::Marker as Notification>::METHOD.to_string();
let erased = erase_notification::<S, F::Marker, H, Fut>(handler);
self.insert_notification(method, erased)?;
spec.contribute(&mut self.capabilities)
}
fn insert_notification(
&mut self,
method: String,
erased: ErasedNotificationHandler<S>,
) -> Result<(), BuildError> {
if RESERVED_METHODS.contains(&method.as_str()) {
return Err(BuildError::ReservedMethod(method));
}
let table = if ProtocolNotification::from_method(&method).is_some() {
&mut self.built_in_hooks
} else {
&mut self.notifications
};
if table.insert(method.clone(), erased).is_some() {
return Err(BuildError::DuplicateMethod(method));
}
Ok(())
}
fn add_command<Args, Output, H, Fut>(
&mut self,
name: String,
handler: H,
) -> Result<(), BuildError>
where
Args: DeserializeOwned + TaskSend + 'static,
Output: Serialize + 'static,
H: Fn(Arc<S>, Context, Args, CancellationToken) -> Fut
+ SharedHandler<(Arc<S>, Context, Args, CancellationToken), Fut>
+ 'static,
Fut: Future<Output = Result<Output, LspError>> + TaskSend + 'static,
{
if name.is_empty() {
return Err(BuildError::EmptyCommandName);
}
let handler = Arc::new(handler);
let erased: ErasedCommandHandler<S> =
Box::new(move |state, ctx, arguments, ct| -> HandlerFuture {
let handler = Arc::clone(&handler);
Box::pin(async move {
let args: Args = serde_json::from_value(Value::Array(arguments))
.map_err(LspError::invalid_params)?;
let result = handler.invoke((state, ctx, args, ct)).await?;
erase_value(result)
})
});
if self.commands.insert(name.clone(), erased).is_some() {
return Err(BuildError::DuplicateCommand(name));
}
self.capabilities.add_command(name);
Ok(())
}
fn validate(&self) -> Result<(), BuildError> {
self.capabilities.validate()?;
self.document_sync_settings()?;
if !self.commands.is_empty() && self.requests.contains_key(EXECUTE_COMMAND_METHOD) {
return Err(BuildError::ExecuteCommandConflict);
}
Ok(())
}
fn document_sync_settings(&self) -> Result<DocumentSyncSettings, BuildError> {
let save_hook = self
.built_in_hooks
.contains_key(ProtocolNotification::SAVE_METHOD);
let will_save_hook = self
.built_in_hooks
.contains_key(ProtocolNotification::WILL_SAVE_METHOD);
let wait_until = self.capabilities.has_will_save_wait_until();
if let Some(explicit) = &self.document_sync {
let save_disabled = matches!(
explicit.save,
Some(TextDocumentSyncSaveOptions::Supported(false))
);
if save_hook && save_disabled {
return Err(BuildError::ConflictingCapability {
field: "textDocumentSync.save",
});
}
if will_save_hook && explicit.will_save == Some(false) {
return Err(BuildError::ConflictingCapability {
field: "textDocumentSync.willSave",
});
}
if wait_until && explicit.will_save_wait_until == Some(false) {
return Err(BuildError::ConflictingCapability {
field: "textDocumentSync.willSaveWaitUntil",
});
}
if explicit.change == Some(TextDocumentSyncKind::NONE) {
let field = if save_hook {
Some("textDocumentSync.save")
} else if will_save_hook {
Some("textDocumentSync.willSave")
} else if wait_until {
Some("textDocumentSync.willSaveWaitUntil")
} else {
None
};
if let Some(field) = field {
return Err(BuildError::ConflictingCapability { field });
}
}
}
let mut options = self.document_sync.clone().unwrap_or_default();
options.open_close.get_or_insert(true);
options
.change
.get_or_insert(TextDocumentSyncKind::INCREMENTAL);
if save_hook && options.save.is_none() {
options.save = Some(true.into());
}
if will_save_hook && options.will_save.is_none() {
options.will_save = Some(true);
}
if wait_until && options.will_save_wait_until.is_none() {
options.will_save_wait_until = Some(true);
}
if options.change == Some(TextDocumentSyncKind::NONE) {
options.open_close = Some(false);
options.will_save = Some(false);
options.will_save_wait_until = Some(false);
options.save = Some(false.into());
return Ok(DocumentSyncSettings {
capability: TextDocumentSyncCapability::Kind(TextDocumentSyncKind::NONE),
options,
});
}
let capability =
if self.document_sync.is_none() && !save_hook && !will_save_hook && !wait_until {
TextDocumentSyncCapability::Kind(TextDocumentSyncKind::INCREMENTAL)
} else {
TextDocumentSyncCapability::Options(options.clone())
};
Ok(DocumentSyncSettings {
capability,
options,
})
}
pub(crate) fn freeze(self) -> Router<S> {
let document_sync = self
.document_sync_settings()
.expect("registrations are validated before freeze");
Router {
requests: self.requests,
notifications: self.notifications,
built_in_hooks: self.built_in_hooks,
commands: self.commands,
capabilities: self.capabilities.finish_generated(),
document_sync,
}
}
}
pub(crate) struct Router<S> {
requests: HashMap<String, ErasedRequestHandler<S>>,
notifications: HashMap<String, ErasedNotificationHandler<S>>,
built_in_hooks: HashMap<String, ErasedNotificationHandler<S>>,
commands: HashMap<String, ErasedCommandHandler<S>>,
capabilities: GeneratedCapabilities,
document_sync: DocumentSyncSettings,
}
impl<S> Router<S> {
pub(crate) fn request(&self, method: &str) -> Option<&ErasedRequestHandler<S>> {
self.requests.get(method)
}
pub(crate) fn notification(&self, method: &str) -> Option<&ErasedNotificationHandler<S>> {
self.notifications.get(method)
}
pub(crate) fn built_in_hook(&self, method: &str) -> Option<&ErasedNotificationHandler<S>> {
self.built_in_hooks.get(method)
}
pub(crate) fn command(&self, name: &str) -> Option<&ErasedCommandHandler<S>> {
self.commands.get(name)
}
pub(crate) fn has_commands(&self) -> bool {
!self.commands.is_empty()
}
#[cfg(test)]
pub(crate) fn capabilities(&self) -> ServerCapabilities {
self.capabilities.standard.clone()
}
pub(crate) fn generated_capabilities(&self) -> GeneratedCapabilities {
self.capabilities.clone()
}
pub(crate) fn document_sync(&self) -> DocumentSyncSettings {
self.document_sync.clone()
}
}
pub struct ServerBuilder<S> {
state: Arc<S>,
file_provider: SharedFileProvider,
registrations: Registrations<S>,
configure_initialize: Option<ConfigureInitialize<S>>,
on_initialize: Option<OnInitialize<S>>,
on_initialized: Option<OnInitialized<S>>,
on_shutdown: Option<OnShutdown<S>>,
on_exit: Option<OnExit<S>>,
layers: Vec<UserLayer<S>>,
resource_policy: crate::ResourcePolicy,
error: Option<BuildError>,
}
impl<S: Send + Sync + 'static> ServerBuilder<S> {
fn new(state: S) -> Self {
Self {
state: Arc::new(state),
file_provider: crate::file_provider::default_file_provider(),
registrations: Registrations::new(),
configure_initialize: None,
on_initialize: None,
on_initialized: None,
on_shutdown: None,
on_exit: None,
layers: Vec::new(),
resource_policy: crate::ResourcePolicy::default(),
error: None,
}
}
pub fn text_document_sync(mut self, options: TextDocumentSyncOptions) -> Self {
self.registrations.document_sync = Some(options);
self
}
pub fn file_provider<P: FileProvider>(mut self, provider: P) -> Self {
self.file_provider = erase(provider);
self
}
pub fn feature<F, H, Fut>(mut self, spec: F, handler: H) -> Self
where
F: FeatureSpec,
H: Fn(Arc<S>, Context, <F::Marker as Request>::Params, CancellationToken) -> Fut
+ SharedHandler<
(
Arc<S>,
Context,
<F::Marker as Request>::Params,
CancellationToken,
),
Fut,
> + 'static,
Fut: Future<Output = Result<<F::Marker as Request>::Result, LspError>> + TaskSend + 'static,
{
if let Err(err) = self.registrations.add_feature(spec, handler) {
self.record(err);
}
self
}
pub fn request<R, H, Fut>(mut self, handler: H) -> Self
where
R: Request,
H: Fn(Arc<S>, Context, R::Params, CancellationToken) -> Fut
+ SharedHandler<(Arc<S>, Context, R::Params, CancellationToken), Fut>
+ 'static,
Fut: Future<Output = Result<R::Result, LspError>> + TaskSend + 'static,
{
if let Err(err) = self.registrations.add_request::<R, H, Fut>(handler) {
self.record(err);
}
self
}
pub fn notification<N, H, Fut>(mut self, handler: H) -> Self
where
N: Notification,
H: Fn(Arc<S>, Context, N::Params) -> Fut
+ SharedHandler<(Arc<S>, Context, N::Params), Fut>
+ 'static,
Fut: Future<Output = ()> + TaskSend + 'static,
{
if let Err(err) = self.registrations.add_notification::<N, H, Fut>(handler) {
self.record(err);
}
self
}
pub fn feature_notification<F, H, Fut>(mut self, spec: F, handler: H) -> Self
where
F: NotificationFeatureSpec,
H: Fn(Arc<S>, Context, <F::Marker as Notification>::Params) -> Fut
+ SharedHandler<(Arc<S>, Context, <F::Marker as Notification>::Params), Fut>
+ 'static,
Fut: Future<Output = ()> + TaskSend + 'static,
{
if let Err(err) = self.registrations.add_feature_notification(spec, handler) {
self.record(err);
}
self
}
pub fn command<Args, Output, H, Fut>(mut self, name: impl Into<String>, handler: H) -> Self
where
Args: DeserializeOwned + TaskSend + 'static,
Output: Serialize + 'static,
H: Fn(Arc<S>, Context, Args, CancellationToken) -> Fut
+ SharedHandler<(Arc<S>, Context, Args, CancellationToken), Fut>
+ 'static,
Fut: Future<Output = Result<Output, LspError>> + TaskSend + 'static,
{
if let Err(err) = self
.registrations
.add_command::<Args, Output, H, Fut>(name.into(), handler)
{
self.record(err);
}
self
}
pub fn configure_initialize<F>(mut self, callback: F) -> Self
where
F: FnOnce(&InitializeParams, &mut InitializeRegistrar<S>) -> Result<(), LspError>
+ TaskSend
+ 'static,
{
if self.configure_initialize.is_some() {
self.record(BuildError::DuplicateConfigureInitialize);
} else {
self.configure_initialize = Some(Box::new(callback));
}
self
}
pub fn on_initialize<H, Fut>(mut self, hook: H) -> Self
where
H: Fn(Arc<S>, Context, InitializeParams, CancellationToken) -> Fut
+ SharedHandler<(Arc<S>, Context, InitializeParams, CancellationToken), Fut>
+ 'static,
Fut: Future<Output = Result<Option<ServerInfo>, LspError>> + TaskSend + 'static,
{
if self.on_initialize.is_some() {
self.record(BuildError::DuplicateLifecycleHook("on_initialize"));
} else {
self.on_initialize = Some(Box::new(
move |state, ctx, params, ct| -> OnInitializeFuture {
Box::pin(hook.invoke((state, ctx, params, ct)))
},
));
}
self
}
pub fn on_initialized<H, Fut>(mut self, hook: H) -> Self
where
H: Fn(Arc<S>, Context, InitializedParams) -> Fut
+ SharedHandler<(Arc<S>, Context, InitializedParams), Fut>
+ 'static,
Fut: Future<Output = ()> + TaskSend + 'static,
{
if self.on_initialized.is_some() {
self.record(BuildError::DuplicateLifecycleHook("on_initialized"));
} else {
self.on_initialized = Some(Box::new(move |state, ctx, params| -> NotificationFuture {
Box::pin(hook.invoke((state, ctx, params)))
}));
}
self
}
pub fn on_shutdown<H, Fut>(mut self, hook: H) -> Self
where
H: Fn(Arc<S>, Context, (), CancellationToken) -> Fut
+ SharedHandler<(Arc<S>, Context, (), CancellationToken), Fut>
+ 'static,
Fut: Future<Output = Result<(), LspError>> + TaskSend + 'static,
{
if self.on_shutdown.is_some() {
self.record(BuildError::DuplicateLifecycleHook("on_shutdown"));
} else {
self.on_shutdown = Some(Box::new(
move |state, ctx, params, ct| -> OnShutdownFuture {
Box::pin(hook.invoke((state, ctx, params, ct)))
},
));
}
self
}
pub fn on_exit<H, Fut>(mut self, hook: H) -> Self
where
H: Fn(Arc<S>, Context) -> Fut + SharedHandler<(Arc<S>, Context), Fut> + 'static,
Fut: Future<Output = ()> + TaskSend + 'static,
{
if self.on_exit.is_some() {
self.record(BuildError::DuplicateLifecycleHook("on_exit"));
} else {
self.on_exit = Some(Box::new(move |state, ctx| -> NotificationFuture {
Box::pin(hook.invoke((state, ctx)))
}));
}
self
}
pub fn layer<L>(mut self, layer: L) -> Self
where
L: Layer<S>,
{
self.layers.push(Arc::new(layer));
self
}
pub fn concurrency_limit(mut self, limit: usize) -> Self {
if limit == 0 {
self.record(BuildError::InvalidConcurrencyLimit);
} else {
self.resource_policy.max_inbound_requests = limit;
}
self
}
pub fn outbound_warning_threshold(mut self, threshold: usize) -> Self {
if threshold == 0 {
self.record(BuildError::InvalidOutboundWarningThreshold);
} else {
self.resource_policy.max_outbound_messages = threshold;
}
self
}
pub fn resource_policy(mut self, policy: crate::ResourcePolicy) -> Self {
self.resource_policy = policy;
self
}
pub fn build(mut self) -> Result<Server<S>, BuildError> {
if let Err(err) = self.resource_policy.validate() {
self.record(err);
}
if let Err(err) = self.registrations.validate() {
self.record(err);
}
if let Some(error) = self.error {
return Err(error);
}
Ok(Server {
state: self.state,
file_provider: self.file_provider,
registrations: self.registrations,
configure_initialize: self.configure_initialize,
on_initialize: self.on_initialize,
on_initialized: self.on_initialized,
on_shutdown: self.on_shutdown,
on_exit: self.on_exit,
layers: self.layers,
resource_policy: self.resource_policy,
})
}
fn record(&mut self, error: BuildError) {
if self.error.is_none() {
self.error = Some(error);
}
}
}
pub struct InitializeRegistrar<S> {
registrations: Registrations<S>,
error: Option<BuildError>,
}
impl<S: Send + Sync + 'static> InitializeRegistrar<S> {
pub(crate) fn new(registrations: Registrations<S>) -> Self {
Self {
registrations,
error: None,
}
}
pub fn feature<F, H, Fut>(&mut self, spec: F, handler: H) -> &mut Self
where
F: FeatureSpec,
H: Fn(Arc<S>, Context, <F::Marker as Request>::Params, CancellationToken) -> Fut
+ SharedHandler<
(
Arc<S>,
Context,
<F::Marker as Request>::Params,
CancellationToken,
),
Fut,
> + 'static,
Fut: Future<Output = Result<<F::Marker as Request>::Result, LspError>> + TaskSend + 'static,
{
self.try_register(|r| r.add_feature(spec, handler))
}
pub fn request<R, H, Fut>(&mut self, handler: H) -> &mut Self
where
R: Request,
H: Fn(Arc<S>, Context, R::Params, CancellationToken) -> Fut
+ SharedHandler<(Arc<S>, Context, R::Params, CancellationToken), Fut>
+ 'static,
Fut: Future<Output = Result<R::Result, LspError>> + TaskSend + 'static,
{
self.try_register(|r| r.add_request::<R, H, Fut>(handler))
}
pub fn notification<N, H, Fut>(&mut self, handler: H) -> &mut Self
where
N: Notification,
H: Fn(Arc<S>, Context, N::Params) -> Fut
+ SharedHandler<(Arc<S>, Context, N::Params), Fut>
+ 'static,
Fut: Future<Output = ()> + TaskSend + 'static,
{
self.try_register(|r| r.add_notification::<N, H, Fut>(handler))
}
pub fn feature_notification<F, H, Fut>(&mut self, spec: F, handler: H) -> &mut Self
where
F: NotificationFeatureSpec,
H: Fn(Arc<S>, Context, <F::Marker as Notification>::Params) -> Fut
+ SharedHandler<(Arc<S>, Context, <F::Marker as Notification>::Params), Fut>
+ 'static,
Fut: Future<Output = ()> + TaskSend + 'static,
{
self.try_register(|r| r.add_feature_notification(spec, handler))
}
pub fn command<Args, Output, H, Fut>(
&mut self,
name: impl Into<String>,
handler: H,
) -> &mut Self
where
Args: DeserializeOwned + TaskSend + 'static,
Output: Serialize + 'static,
H: Fn(Arc<S>, Context, Args, CancellationToken) -> Fut
+ SharedHandler<(Arc<S>, Context, Args, CancellationToken), Fut>
+ 'static,
Fut: Future<Output = Result<Output, LspError>> + TaskSend + 'static,
{
self.try_register(|r| r.add_command::<Args, Output, H, Fut>(name.into(), handler))
}
fn try_register(
&mut self,
op: impl FnOnce(&mut Registrations<S>) -> Result<(), BuildError>,
) -> &mut Self {
if self.error.is_none()
&& let Err(err) = op(&mut self.registrations)
{
self.error = Some(err);
}
self
}
pub(crate) fn commit(self) -> Result<Registrations<S>, BuildError> {
if let Some(error) = self.error {
return Err(error);
}
self.registrations.validate()?;
Ok(self.registrations)
}
}
pub struct Server<S> {
pub(crate) state: Arc<S>,
pub(crate) file_provider: SharedFileProvider,
pub(crate) registrations: Registrations<S>,
pub(crate) configure_initialize: Option<ConfigureInitialize<S>>,
pub(crate) on_initialize: Option<OnInitialize<S>>,
pub(crate) on_initialized: Option<OnInitialized<S>>,
pub(crate) on_shutdown: Option<OnShutdown<S>>,
pub(crate) on_exit: Option<OnExit<S>>,
pub(crate) layers: Vec<UserLayer<S>>,
pub(crate) resource_policy: crate::ResourcePolicy,
}
impl<S: Send + Sync + 'static> Server<S> {
pub fn builder(state: S) -> ServerBuilder<S> {
ServerBuilder::new(state)
}
#[cfg(test)]
pub(crate) fn into_router(self) -> Router<S> {
self.registrations.freeze()
}
#[cfg(any(feature = "runtime-tokio", target_arch = "wasm32"))]
pub async fn serve<T>(self, transport: T) -> crate::Result<crate::Outcome>
where
T: crate::Transport,
{
crate::engine::run(self, transport).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use lsp_types::request::{ExecuteCommand, HoverRequest, Shutdown};
use lsp_types::{CompletionOptions, HoverProviderCapability};
struct DummyState;
async fn ok_hover(
_state: Arc<DummyState>,
_ctx: Context,
_params: lsp_types::HoverParams,
_ct: CancellationToken,
) -> Result<Option<lsp_types::Hover>, LspError> {
Ok(None)
}
async fn ok_completion(
_state: Arc<DummyState>,
_ctx: Context,
_params: lsp_types::CompletionParams,
_ct: CancellationToken,
) -> Result<Option<lsp_types::CompletionResponse>, LspError> {
Ok(None)
}
async fn ok_resolve(
_state: Arc<DummyState>,
_ctx: Context,
item: lsp_types::CompletionItem,
_ct: CancellationToken,
) -> Result<lsp_types::CompletionItem, LspError> {
Ok(item)
}
async fn noop_command(
_state: Arc<DummyState>,
_ctx: Context,
_args: Vec<String>,
_ct: CancellationToken,
) -> Result<(), LspError> {
Ok(())
}
async fn noop_notification(_state: Arc<DummyState>, _ctx: Context, _params: ()) {}
#[test]
fn duplicate_request_method_is_a_build_error() {
let err = Server::builder(DummyState)
.request::<HoverRequest, _, _>(ok_hover)
.request::<HoverRequest, _, _>(ok_hover)
.build()
.err()
.expect("second registration for the same method must fail");
assert_eq!(
err,
BuildError::DuplicateMethod("textDocument/hover".to_string())
);
}
#[test]
fn registering_a_reserved_method_is_a_build_error() {
async fn shutdown_handler(
_state: Arc<DummyState>,
_ctx: Context,
_params: (),
_ct: CancellationToken,
) -> Result<(), LspError> {
Ok(())
}
let err = Server::builder(DummyState)
.request::<Shutdown, _, _>(shutdown_handler)
.build()
.err()
.expect("shutdown is framework-reserved");
assert_eq!(err, BuildError::ReservedMethod("shutdown".to_string()));
}
#[test]
fn a_single_registration_builds_and_advertises_no_extra_capabilities() {
let server = Server::builder(DummyState)
.request::<HoverRequest, _, _>(ok_hover)
.build()
.expect("a lone custom request builds");
let router = server.into_router();
assert!(router.request("textDocument/hover").is_some());
assert!(router.request("nope").is_none());
assert_eq!(
router.capabilities(),
ServerCapabilities::default(),
"custom requests must not contribute capabilities"
);
}
#[test]
fn a_reserved_notification_method_is_a_build_error() {
let err = Server::builder(DummyState)
.notification::<lsp_types::notification::Exit, _, _>(noop_notification)
.build()
.err()
.expect("exit is framework-reserved");
assert_eq!(err, BuildError::ReservedMethod("exit".to_string()));
}
#[test]
fn a_duplicate_notification_method_is_a_build_error() {
let err = Server::builder(DummyState)
.notification::<lsp_types::notification::DidChangeConfiguration, _, _>(
|_s, _c, _p: lsp_types::DidChangeConfigurationParams| async {},
)
.notification::<lsp_types::notification::DidChangeConfiguration, _, _>(
|_s, _c, _p: lsp_types::DidChangeConfigurationParams| async {},
)
.build()
.err()
.expect("a repeated notification method must fail");
assert_eq!(
err,
BuildError::DuplicateMethod("workspace/didChangeConfiguration".to_string())
);
}
#[test]
fn workspace_mutation_hooks_contribute_no_catalog_capabilities() {
let server = Server::builder(DummyState)
.notification::<lsp_types::notification::DidChangeConfiguration, _, _>(
|_s, _c, _p: lsp_types::DidChangeConfigurationParams| async {},
)
.build()
.expect("a lone notification builds");
let router = server.into_router();
assert!(
router
.built_in_hook("workspace/didChangeConfiguration")
.is_some()
);
assert!(
router
.notification("workspace/didChangeConfiguration")
.is_none()
);
assert_eq!(router.capabilities(), ServerCapabilities::default());
}
#[test]
fn a_document_sync_registration_records_a_hook_not_a_route() {
let server = Server::builder(DummyState)
.notification::<lsp_types::notification::DidOpenTextDocument, _, _>(
|_s, _c, _p: lsp_types::DidOpenTextDocumentParams| async {},
)
.notification::<lsp_types::notification::DidSaveTextDocument, _, _>(
|_s, _c, _p: lsp_types::DidSaveTextDocumentParams| async {},
)
.build()
.expect("one hook and one ordinary notification build");
let router = server.into_router();
assert!(
router.built_in_hook("textDocument/didOpen").is_some(),
"a built-in document notification records a post-validation hook"
);
assert!(
router.notification("textDocument/didOpen").is_none(),
"the hook is not a Router route, so it cannot shadow the built-in"
);
assert!(router.notification("textDocument/didSave").is_none());
assert!(
router.built_in_hook("textDocument/didSave").is_some(),
"didSave is protocol-validated before its typed hook runs"
);
}
#[test]
fn a_progress_cancel_registration_records_a_hook_not_a_route() {
let server = Server::builder(DummyState)
.notification::<lsp_types::notification::WorkDoneProgressCancel, _, _>(
|_s, _c, _p: lsp_types::WorkDoneProgressCancelParams| async {},
)
.build()
.expect("a lone progress-cancel hook builds");
let router = server.into_router();
assert!(
router
.built_in_hook("window/workDoneProgress/cancel")
.is_some(),
"the progress-cancel built-in records a post-validation hook"
);
assert!(
router
.notification("window/workDoneProgress/cancel")
.is_none(),
"the hook is not a Router route, so it cannot replace the built-in"
);
assert_eq!(
router.capabilities(),
ServerCapabilities::default(),
"a progress-cancel hook contributes no capabilities"
);
}
#[test]
fn a_duplicate_document_hook_is_a_build_error() {
let err = Server::builder(DummyState)
.notification::<lsp_types::notification::DidChangeTextDocument, _, _>(
|_s, _c, _p: lsp_types::DidChangeTextDocumentParams| async {},
)
.notification::<lsp_types::notification::DidChangeTextDocument, _, _>(
|_s, _c, _p: lsp_types::DidChangeTextDocumentParams| async {},
)
.build()
.err()
.expect("a built-in notification takes at most one hook");
assert_eq!(
err,
BuildError::DuplicateMethod("textDocument/didChange".to_string())
);
}
#[test]
fn document_hooks_contribute_no_capabilities() {
let without_hook = Server::builder(DummyState)
.build()
.expect("an empty server builds")
.into_router()
.capabilities();
let with_hook = Server::builder(DummyState)
.notification::<lsp_types::notification::DidCloseTextDocument, _, _>(
|_s, _c, _p: lsp_types::DidCloseTextDocumentParams| async {},
)
.build()
.expect("a lone document hook builds")
.into_router()
.capabilities();
assert_eq!(
with_hook, without_hook,
"observing a built-in advertises nothing the built-in did not"
);
}
#[test]
fn an_empty_command_name_is_a_build_error() {
let err = Server::builder(DummyState)
.command::<Vec<String>, (), _, _>("", noop_command)
.build()
.err()
.expect("an empty command name must fail");
assert_eq!(err, BuildError::EmptyCommandName);
}
#[test]
fn a_duplicate_command_name_is_a_build_error() {
let err = Server::builder(DummyState)
.command::<Vec<String>, (), _, _>("my.cmd", noop_command)
.command::<Vec<String>, (), _, _>("my.cmd", noop_command)
.build()
.err()
.expect("a repeated command name must fail");
assert_eq!(err, BuildError::DuplicateCommand("my.cmd".to_string()));
}
#[test]
fn commands_alongside_an_explicit_execute_command_handler_conflict() {
async fn raw_execute(
_state: Arc<DummyState>,
_ctx: Context,
_params: lsp_types::ExecuteCommandParams,
_ct: CancellationToken,
) -> Result<Option<serde_json::Value>, LspError> {
Ok(None)
}
let err = Server::builder(DummyState)
.command::<Vec<String>, (), _, _>("my.cmd", noop_command)
.request::<ExecuteCommand, _, _>(raw_execute)
.build()
.err()
.expect("a command and a raw execute-command handler cannot coexist");
assert_eq!(err, BuildError::ExecuteCommandConflict);
}
#[test]
fn registered_commands_contribute_one_execute_command_capability() {
let server = Server::builder(DummyState)
.command::<Vec<String>, (), _, _>("b.cmd", noop_command)
.command::<Vec<String>, (), _, _>("a.cmd", noop_command)
.build()
.expect("commands build");
let provider = server
.into_router()
.capabilities()
.execute_command_provider
.expect("commands advertise an execute-command capability");
assert_eq!(
provider.commands,
vec!["b.cmd".to_string(), "a.cmd".to_string()],
"command names merge into one de-duplicated, registration-order list"
);
}
#[test]
fn hover_feature_sets_only_hover_provider() {
let server = Server::builder(DummyState)
.feature(crate::features::hover(), ok_hover)
.build()
.expect("hover builds");
let router = server.into_router();
let caps = router.capabilities();
assert_eq!(
caps.hover_provider,
Some(HoverProviderCapability::Simple(true))
);
assert_eq!(caps.completion_provider, None);
assert!(router.request("textDocument/hover").is_some());
}
#[test]
fn hover_and_completion_merge_independent_of_order() {
let options = CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..CompletionOptions::default()
};
let hover_first = Server::builder(DummyState)
.feature(crate::features::hover(), ok_hover)
.feature(crate::features::completion(options.clone()), ok_completion)
.build()
.expect("hover then completion builds")
.into_router()
.capabilities();
let completion_first = Server::builder(DummyState)
.feature(crate::features::completion(options.clone()), ok_completion)
.feature(crate::features::hover(), ok_hover)
.build()
.expect("completion then hover builds")
.into_router()
.capabilities();
assert_eq!(
hover_first, completion_first,
"capability merge is independent of registration order"
);
assert_eq!(
hover_first.completion_provider,
Some(options),
"completion advertises the supplied options"
);
}
#[test]
fn a_duplicate_feature_is_a_build_error_not_last_write_wins() {
let err = Server::builder(DummyState)
.feature(crate::features::hover(), ok_hover)
.feature(crate::features::hover(), ok_hover)
.build()
.err()
.expect("registering hover twice must fail");
assert_eq!(
err,
BuildError::DuplicateMethod("textDocument/hover".to_string())
);
}
#[test]
fn completion_and_resolve_merge_into_one_capability_independent_of_order() {
let options = || CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..CompletionOptions::default()
};
let base_first = Server::builder(DummyState)
.feature(crate::features::completion(options()), ok_completion)
.feature(crate::features::completion_resolve(), ok_resolve)
.build()
.expect("completion then resolve builds")
.into_router();
let resolve_first = Server::builder(DummyState)
.feature(crate::features::completion_resolve(), ok_resolve)
.feature(crate::features::completion(options()), ok_completion)
.build()
.expect("resolve then completion builds")
.into_router();
assert_eq!(
base_first.capabilities(),
resolve_first.capabilities(),
"the family merge is independent of registration order"
);
let merged = base_first
.capabilities()
.completion_provider
.expect("the family emits one completionProvider capability");
assert_eq!(merged.resolve_provider, Some(true));
assert_eq!(merged.trigger_characters, Some(vec![".".to_string()]));
assert!(base_first.request("textDocument/completion").is_some());
assert!(base_first.request("completionItem/resolve").is_some());
}
#[test]
fn completion_resolve_without_completion_is_a_build_error() {
let err = Server::builder(DummyState)
.feature(crate::features::completion_resolve(), ok_resolve)
.build()
.err()
.expect("resolve without its base feature must fail");
assert_eq!(
err,
BuildError::ConflictingCapability {
field: "completionProvider"
}
);
}
#[test]
fn unequal_resolve_contributions_within_the_family_fail() {
let err = Server::builder(DummyState)
.feature(
crate::features::completion(CompletionOptions {
resolve_provider: Some(false),
..CompletionOptions::default()
}),
ok_completion,
)
.feature(crate::features::completion_resolve(), ok_resolve)
.build()
.err()
.expect("a base that denies resolve and a resolve registration clash");
assert_eq!(
err,
BuildError::ConflictingCapability {
field: "completionProvider"
},
"capability construction never resolves a clash by last-write-wins"
);
}
async fn noop_on_initialize(
_state: Arc<DummyState>,
_ctx: Context,
_params: lsp_types::InitializeParams,
_ct: CancellationToken,
) -> Result<Option<lsp_types::ServerInfo>, LspError> {
Ok(None)
}
#[test]
fn duplicate_configure_initialize_is_a_build_error() {
let err = Server::builder(DummyState)
.configure_initialize(|_params, _registrar| Ok(()))
.configure_initialize(|_params, _registrar| Ok(()))
.build()
.err()
.expect("supplying configure_initialize twice must fail");
assert_eq!(err, BuildError::DuplicateConfigureInitialize);
}
#[test]
fn duplicate_on_initialize_is_a_build_error() {
let err = Server::builder(DummyState)
.on_initialize(noop_on_initialize)
.on_initialize(noop_on_initialize)
.build()
.err()
.expect("supplying on_initialize twice must fail");
assert_eq!(err, BuildError::DuplicateLifecycleHook("on_initialize"));
}
async fn noop_on_initialized(
_state: Arc<DummyState>,
_ctx: Context,
_params: lsp_types::InitializedParams,
) {
}
async fn noop_on_shutdown(
_state: Arc<DummyState>,
_ctx: Context,
_params: (),
_ct: CancellationToken,
) -> Result<(), LspError> {
Ok(())
}
async fn noop_on_exit(_state: Arc<DummyState>, _ctx: Context) {}
#[test]
fn duplicate_on_initialized_is_a_build_error() {
let err = Server::builder(DummyState)
.on_initialized(noop_on_initialized)
.on_initialized(noop_on_initialized)
.build()
.err()
.expect("supplying on_initialized twice must fail");
assert_eq!(err, BuildError::DuplicateLifecycleHook("on_initialized"));
}
#[test]
fn duplicate_on_shutdown_is_a_build_error() {
let err = Server::builder(DummyState)
.on_shutdown(noop_on_shutdown)
.on_shutdown(noop_on_shutdown)
.build()
.err()
.expect("supplying on_shutdown twice must fail");
assert_eq!(err, BuildError::DuplicateLifecycleHook("on_shutdown"));
}
#[test]
fn duplicate_on_exit_is_a_build_error() {
let err = Server::builder(DummyState)
.on_exit(noop_on_exit)
.on_exit(noop_on_exit)
.build()
.err()
.expect("supplying on_exit twice must fail");
assert_eq!(err, BuildError::DuplicateLifecycleHook("on_exit"));
}
#[test]
fn lifecycle_hooks_contribute_no_catalog_capabilities() {
let server = Server::builder(DummyState)
.on_initialized(noop_on_initialized)
.on_shutdown(noop_on_shutdown)
.on_exit(noop_on_exit)
.build()
.expect("a server with only lifecycle hooks builds");
let router = server.into_router();
assert!(
router.notification("initialized").is_none(),
"initialized is not a Router route; it is a reserved lifecycle notification"
);
assert_eq!(
router.capabilities(),
ServerCapabilities::default(),
"lifecycle hooks contribute nothing to the capability catalog"
);
}
#[test]
fn initialized_is_a_reserved_notification_method() {
let err = Server::builder(DummyState)
.notification::<lsp_types::notification::Initialized, _, _>(
|_s, _c, _p: lsp_types::InitializedParams| async {},
)
.build()
.err()
.expect("initialized is framework-reserved");
assert_eq!(err, BuildError::ReservedMethod("initialized".to_string()));
}
async fn ok_workspace_symbol(
_state: Arc<DummyState>,
_ctx: Context,
_params: lsp_types::WorkspaceSymbolParams,
_ct: CancellationToken,
) -> Result<Option<lsp_types::WorkspaceSymbolResponse>, LspError> {
Ok(None)
}
async fn ok_symbol_resolve(
_state: Arc<DummyState>,
_ctx: Context,
symbol: lsp_types::WorkspaceSymbol,
_ct: CancellationToken,
) -> Result<lsp_types::WorkspaceSymbol, LspError> {
Ok(symbol)
}
async fn ok_will_rename(
_state: Arc<DummyState>,
_ctx: Context,
_params: lsp_types::RenameFilesParams,
_ct: CancellationToken,
) -> Result<Option<lsp_types::WorkspaceEdit>, LspError> {
Ok(None)
}
async fn noop_rename_files(
_state: Arc<DummyState>,
_ctx: Context,
_params: lsp_types::RenameFilesParams,
) {
}
fn rename_filters() -> lsp_types::FileOperationRegistrationOptions {
lsp_types::FileOperationRegistrationOptions {
filters: vec![lsp_types::FileOperationFilter {
scheme: Some("file".to_string()),
pattern: lsp_types::FileOperationPattern {
glob: "**/*.rs".to_string(),
matches: Some(lsp_types::FileOperationPatternKind::File),
options: None,
},
}],
}
}
fn workspace_symbol_options() -> lsp_types::WorkspaceSymbolOptions {
lsp_types::WorkspaceSymbolOptions {
work_done_progress_options: Default::default(),
resolve_provider: None,
}
}
#[test]
fn workspace_symbol_and_resolve_merge_into_one_capability_independent_of_order() {
let base_first = Server::builder(DummyState)
.feature(
crate::features::workspace_symbol(workspace_symbol_options()),
ok_workspace_symbol,
)
.feature(
crate::features::workspace_symbol_resolve(),
ok_symbol_resolve,
)
.build()
.expect("workspace symbol then resolve builds")
.into_router();
let resolve_first = Server::builder(DummyState)
.feature(
crate::features::workspace_symbol_resolve(),
ok_symbol_resolve,
)
.feature(
crate::features::workspace_symbol(workspace_symbol_options()),
ok_workspace_symbol,
)
.build()
.expect("resolve then workspace symbol builds")
.into_router();
assert_eq!(
base_first.capabilities(),
resolve_first.capabilities(),
"the family merge is independent of registration order"
);
let merged = base_first
.capabilities()
.workspace_symbol_provider
.expect("the family emits one workspaceSymbolProvider capability");
let lsp_types::OneOf::Right(options) = merged else {
panic!("the family advertises full options, not a bare boolean");
};
assert_eq!(options.resolve_provider, Some(true));
assert!(base_first.request("workspace/symbol").is_some());
assert!(base_first.request("workspaceSymbol/resolve").is_some());
}
#[test]
fn workspace_symbol_resolve_without_workspace_symbol_is_a_build_error() {
let err = Server::builder(DummyState)
.feature(
crate::features::workspace_symbol_resolve(),
ok_symbol_resolve,
)
.build()
.err()
.expect("resolve without its base feature must fail");
assert_eq!(
err,
BuildError::ConflictingCapability {
field: "workspaceSymbolProvider"
}
);
}
#[test]
fn file_operation_features_share_one_family_capability() {
let server = Server::builder(DummyState)
.feature(
crate::features::will_rename_files(rename_filters()),
ok_will_rename,
)
.feature_notification(
crate::features::did_rename_files(rename_filters()),
noop_rename_files,
)
.build()
.expect("identical will/did filters merge");
let router = server.into_router();
assert!(router.request("workspace/willRenameFiles").is_some());
assert!(router.notification("workspace/didRenameFiles").is_some());
let file_operations = router
.capabilities()
.workspace
.expect("the family advertises the workspace object")
.file_operations
.expect("the family advertises a fileOperations capability");
let expected = Some(rename_filters());
assert_eq!(file_operations.will_rename, expected.clone());
assert_eq!(file_operations.did_rename, expected);
assert_eq!(file_operations.will_create, None);
}
#[test]
fn disagreeing_file_operation_filters_are_a_build_error() {
let mut other = rename_filters();
other.filters[0].pattern.glob = "**/*.toml".to_string();
let err = Server::builder(DummyState)
.feature(
crate::features::will_rename_files(rename_filters()),
ok_will_rename,
)
.feature_notification(crate::features::did_rename_files(other), noop_rename_files)
.build()
.err()
.expect("differing filters within one family must fail");
assert_eq!(
err,
BuildError::ConflictingCapability {
field: "workspace.fileOperations.rename"
}
);
}
#[test]
fn a_duplicate_notification_feature_is_a_build_error() {
let err = Server::builder(DummyState)
.feature_notification(
crate::features::did_rename_files(rename_filters()),
noop_rename_files,
)
.feature_notification(
crate::features::did_rename_files(rename_filters()),
noop_rename_files,
)
.build()
.err()
.expect("registering the same notification feature twice must fail");
assert_eq!(
err,
BuildError::DuplicateMethod("workspace/didRenameFiles".to_string())
);
}
#[test]
fn watched_files_feature_registers_a_route_and_contributes_no_capability() {
async fn noop_watched(
_state: Arc<DummyState>,
_ctx: Context,
_params: lsp_types::DidChangeWatchedFilesParams,
) {
}
let server = Server::builder(DummyState)
.feature_notification(crate::features::did_change_watched_files(), noop_watched)
.build()
.expect("the watched-files feature builds");
let router = server.into_router();
assert!(
router
.notification("workspace/didChangeWatchedFiles")
.is_some(),
"watched files is an ordinary route: the framework owns no mutation for it"
);
assert_eq!(
router.capabilities(),
ServerCapabilities::default(),
"LSP 3.17 has no watched-files server capability, so none is advertised"
);
}
#[test]
fn the_outbound_warning_threshold_defaults_to_1024() {
let server = Server::builder(DummyState)
.build()
.expect("the default threshold builds");
assert_eq!(server.resource_policy.max_outbound_messages, 1024);
assert_eq!(
server.resource_policy.max_outbound_messages,
crate::DEFAULT_OUTBOUND_WARNING_THRESHOLD
);
}
#[test]
fn the_outbound_warning_threshold_accepts_positive_values() {
let server = Server::builder(DummyState)
.outbound_warning_threshold(7)
.build()
.expect("a positive threshold builds");
assert_eq!(server.resource_policy.max_outbound_messages, 7);
}
#[test]
fn a_zero_outbound_warning_threshold_is_a_build_error() {
let err = Server::builder(DummyState)
.outbound_warning_threshold(0)
.build()
.err()
.expect("a zero threshold must fail the build");
assert_eq!(err, BuildError::InvalidOutboundWarningThreshold);
assert_eq!(
err.to_string(),
"outbound warning threshold must be greater than zero"
);
}
}