use std::collections::HashMap;
use std::future::Future;
use std::sync::{Arc, Mutex};
use bytes::Bytes;
use futures_util::future::{Either, select};
use lsp_types::{
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWorkspaceFoldersParams,
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams,
InitializeParams, InitializedParams, OneOf, ServerInfo, SetTraceParams, TextDocumentSyncKind,
TextDocumentSyncOptions, TextDocumentSyncSaveOptions, WillSaveTextDocumentParams,
WorkDoneProgressCancelParams, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
};
use serde::Serialize;
use tokio::sync::mpsc::UnboundedReceiver;
use tokio_util::sync::CancellationToken;
use tracing::{Instrument, Span, debug, info_span, warn};
use crate::builder::{
ConfigureInitialize, InitializeRegistrar, OnExit, OnInitialize, OnInitialized,
ProtocolNotification, Registrations, Server,
};
use crate::capability::GeneratedCapabilities;
use crate::client::{Client, OutboundQueue, OutboundRegistry};
use crate::codec::{decode_params, decode_value, encode_body};
use crate::context::Context;
use crate::documents::Documents;
use crate::error::Error;
use crate::file_provider::SharedFileProvider;
use crate::progress::{ProgressCancel, ProgressRegistry};
use crate::raw::{JsonRpcError, RawMessage, RequestId};
use crate::runtime::{Runtime, TaskHandle, TaskSend, default_runtime};
use crate::service::{IncomingCall, ServiceResult, UserLayer, UserService, build_service_stack};
use crate::transport::{Transport, TransportError, TransportReader, TransportWriter};
use crate::workspace::Workspace;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct WireInitializeResult {
capabilities: GeneratedCapabilities,
#[serde(skip_serializing_if = "Option::is_none")]
server_info: Option<ServerInfo>,
}
use crate::{LspError, Result};
fn validate_sync_changes(
kind: TextDocumentSyncKind,
changes: &[lsp_types::TextDocumentContentChangeEvent],
) -> std::result::Result<(), LspError> {
if kind == TextDocumentSyncKind::INCREMENTAL {
return Ok(());
}
if kind == TextDocumentSyncKind::FULL {
return if changes.iter().all(|change| change.range.is_none()) {
Ok(())
} else {
Err(LspError::invalid_request(
"range changes require incremental document synchronization",
))
};
}
Err(LspError::invalid_request(
"document changes are disabled by the configured synchronization kind",
))
}
fn decode_initialized_params(raw: &Bytes) -> std::result::Result<InitializedParams, LspError> {
match serde_json::from_slice::<serde_json::Value>(raw) {
Ok(serde_json::Value::Null) => Ok(InitializedParams {}),
Ok(serde_json::Value::Object(map)) if map.is_empty() => Ok(InitializedParams {}),
_ => Err(LspError::invalid_params(
"initialized params must be an empty object",
)),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Exit { code: i32 },
TransportClosed,
WriterFailed,
InitializeFailed,
}
impl Outcome {
pub fn code(&self) -> i32 {
match self {
Self::Exit { code } => *code,
Self::TransportClosed | Self::WriterFailed | Self::InitializeFailed => 1,
}
}
}
#[derive(Debug)]
enum CloseCause {
Exit { code: i32 },
ReaderEof,
ReaderFailed(TransportError),
WriterFailed,
InitializeFailed,
}
impl CloseCause {
fn into_result(self) -> Result<Outcome> {
match self {
Self::Exit { code } => Ok(Outcome::Exit { code }),
Self::ReaderEof => Ok(Outcome::TransportClosed),
Self::ReaderFailed(error) => Err(Error::Transport(error)),
Self::WriterFailed => Ok(Outcome::WriterFailed),
Self::InitializeFailed => Ok(Outcome::InitializeFailed),
}
}
}
#[derive(Clone)]
struct CloseSignal {
inner: Arc<CloseInner>,
}
struct CloseInner {
cause: Mutex<Option<CloseCause>>,
requested: CancellationToken,
}
impl CloseSignal {
fn new() -> Self {
Self {
inner: Arc::new(CloseInner {
cause: Mutex::new(None),
requested: CancellationToken::new(),
}),
}
}
fn request(&self, cause: CloseCause) {
{
let mut recorded = self.inner.cause.lock().unwrap();
if recorded.is_none() {
*recorded = Some(cause);
}
}
self.inner.requested.cancel();
}
fn requested(&self) -> CancellationToken {
self.inner.requested.clone()
}
fn take_cause(&self) -> Option<CloseCause> {
self.inner.cause.lock().unwrap().take()
}
}
pub(crate) async fn run<S, T>(server: Server<S>, transport: T) -> Result<Outcome>
where
S: Send + Sync + 'static,
T: Transport,
{
let (reader, writer) = transport.split();
let (out_tx, out_rx) = OutboundQueue::new(server.outbound_warning_threshold);
let client = Client::new(out_tx.clone(), OutboundRegistry::default());
let close = CloseSignal::new();
let runtime = default_runtime();
let send_task = runtime.spawn(send_loop(writer, out_rx, client.clone(), close.clone()));
ProtocolEngine::new(server, runtime, out_tx, client, close, send_task)
.serve(reader)
.await
}
struct TaskGroup<R> {
runtime: R,
handles: Vec<TaskHandle>,
}
impl<R: Runtime> TaskGroup<R> {
fn new(runtime: R) -> Self {
Self {
runtime,
handles: Vec::new(),
}
}
fn spawn<F>(&mut self, future: F)
where
F: Future<Output = ()> + TaskSend + 'static,
{
self.handles.push(self.runtime.spawn(future));
}
async fn reap_finished(&mut self) {
let mut running = Vec::with_capacity(self.handles.len());
for handle in std::mem::take(&mut self.handles) {
if handle.is_finished() {
handle.join().await;
} else {
running.push(handle);
}
}
self.handles = running;
}
async fn abort_and_join(&mut self) {
for handle in &self.handles {
handle.abort();
}
self.join_all().await;
}
async fn join_all(&mut self) {
for handle in std::mem::take(&mut self.handles) {
handle.join().await;
}
}
}
#[derive(Clone)]
struct Reservation {
id: RequestId,
generation: u64,
}
struct InboundEntry {
generation: u64,
cancellation: Option<CancellationToken>,
}
#[derive(Default)]
struct InboundInner {
entries: HashMap<RequestId, InboundEntry>,
next_generation: u64,
}
#[derive(Clone, Default)]
struct InboundRegistry {
inner: Arc<Mutex<InboundInner>>,
}
impl InboundRegistry {
fn reserve(
&self,
id: RequestId,
cancellation: Option<CancellationToken>,
) -> Option<Reservation> {
let mut inner = self.inner.lock().unwrap();
if inner.entries.contains_key(&id) {
return None;
}
let generation = inner.next_generation;
inner.next_generation += 1;
inner.entries.insert(
id.clone(),
InboundEntry {
generation,
cancellation,
},
);
Some(Reservation { id, generation })
}
fn complete(
&self,
out_tx: &OutboundQueue,
reservation: Reservation,
result: std::result::Result<Bytes, LspError>,
) {
let claimed = {
let mut inner = self.inner.lock().unwrap();
match inner.entries.get(&reservation.id) {
Some(entry) if entry.generation == reservation.generation => {
inner.entries.remove(&reservation.id).is_some()
}
_ => false,
}
};
if claimed {
enqueue_encoded(out_tx, reservation.id, result);
}
}
fn complete_cancellation(&self, out_tx: &OutboundQueue, id: &RequestId) {
let token = {
let mut inner = self.inner.lock().unwrap();
match inner.entries.get(id) {
Some(entry) if entry.cancellation.is_some() => inner
.entries
.remove(id)
.and_then(|entry| entry.cancellation),
_ => None,
}
};
if let Some(token) = token {
token.cancel();
enqueue_encoded(out_tx, id.clone(), Err(LspError::RequestCancelled));
}
}
fn cancel_all_with_response(&self, out_tx: &OutboundQueue) {
let entries = std::mem::take(&mut self.inner.lock().unwrap().entries);
for (id, entry) in entries {
if let Some(cancellation) = entry.cancellation {
cancellation.cancel();
}
enqueue_encoded(out_tx, id, Err(LspError::RequestCancelled));
}
}
fn close_all(&self) {
let entries = std::mem::take(&mut self.inner.lock().unwrap().entries);
for cancellation in entries.into_values().filter_map(|entry| entry.cancellation) {
cancellation.cancel();
}
}
}
#[derive(serde::Deserialize)]
struct CancelParams {
id: RequestId,
}
enum BuiltInGate {
RunHook,
SkipHook,
}
fn gate_progress_cancel(registry: &ProgressRegistry, raw_params: &Bytes) -> BuiltInGate {
let params = match decode_params::<WorkDoneProgressCancelParams>(raw_params) {
Ok(params) => params,
Err(error) => {
debug!(%error, "ignoring malformed window/workDoneProgress/cancel");
return BuiltInGate::SkipHook;
}
};
match registry.cancel(¶ms.token) {
ProgressCancel::Cancelled => {}
ProgressCancel::NotCancellable => debug!(
token = ?params.token,
"ignoring work-done progress cancel for a non-cancellable token"
),
ProgressCancel::NotActive => debug!(
token = ?params.token,
"ignoring work-done progress cancel for an unknown or ended token"
),
}
BuiltInGate::RunHook
}
async fn send_loop<W: TransportWriter>(
mut writer: W,
mut out_rx: UnboundedReceiver<RawMessage>,
client: Client,
close: CloseSignal,
) {
let outbound_closing = client.outbound_closing();
loop {
let msg = tokio::select! {
biased;
msg = out_rx.recv() => msg,
() = outbound_closing.cancelled() => {
out_rx.close();
break;
}
};
let Some(msg) = msg else {
client.close_outbound();
break;
};
let sent = writer.send(msg).await;
client.record_done();
if let Err(e) = sent {
warn!(error = %e, "send_loop: transport write failed");
close.request(CloseCause::WriterFailed);
return;
}
}
while let Some(msg) = out_rx.recv().await {
let sent = writer.send(msg).await;
client.record_done();
if let Err(e) = sent {
warn!(error = %e, "send_loop: transport write failed while draining");
close.request(CloseCause::WriterFailed);
return;
}
}
if let Err(e) = writer.shutdown().await {
warn!(error = %e, "send_loop: transport shutdown failed");
close.request(CloseCause::WriterFailed);
}
}
struct Pending<S> {
registrations: Registrations<S>,
file_provider: SharedFileProvider,
configure_initialize: Option<ConfigureInitialize<S>>,
on_initialize: Option<OnInitialize<S>>,
on_initialized: Option<OnInitialized<S>>,
on_exit: Option<OnExit<S>>,
layers: Vec<UserLayer<S>>,
concurrency_limit: usize,
}
enum Lifecycle<S> {
Uninitialized(Box<Pending<S>>),
Initializing,
Running(UserService<S>),
ShuttingDown,
Exited,
}
struct ProtocolEngine<S, R> {
state: Arc<S>,
documents: Documents,
workspace: Option<Workspace>,
document_sync: TextDocumentSyncOptions,
lifecycle: Lifecycle<S>,
on_initialized: Option<OnInitialized<S>>,
on_exit: Option<OnExit<S>>,
inbound: InboundRegistry,
tasks: TaskGroup<R>,
out_tx: OutboundQueue,
client: Client,
session: CancellationToken,
close: CloseSignal,
send_task: Option<TaskHandle>,
}
impl<S, R> ProtocolEngine<S, R>
where
S: Send + Sync + 'static,
R: Runtime,
{
fn new(
server: Server<S>,
runtime: R,
out_tx: OutboundQueue,
client: Client,
close: CloseSignal,
send_task: TaskHandle,
) -> Self {
Self {
state: server.state,
documents: Documents::new(),
workspace: None,
document_sync: TextDocumentSyncOptions::default(),
lifecycle: Lifecycle::Uninitialized(Box::new(Pending {
registrations: server.registrations,
file_provider: server.file_provider,
configure_initialize: server.configure_initialize,
on_initialize: server.on_initialize,
on_initialized: server.on_initialized,
on_exit: server.on_exit,
layers: server.layers,
concurrency_limit: server.concurrency_limit,
})),
on_initialized: None,
on_exit: None,
inbound: InboundRegistry::default(),
tasks: TaskGroup::new(runtime),
out_tx,
client,
session: CancellationToken::new(),
close,
send_task: Some(send_task),
}
}
async fn serve<Rd>(mut self, mut reader: Rd) -> Result<Outcome>
where
Rd: TransportReader,
{
let requested = self.close.requested();
loop {
self.tasks.reap_finished().await;
let msg = tokio::select! {
biased;
() = requested.cancelled() => break,
msg = reader.recv() => msg,
};
match msg {
Ok(msg) => match self.dispatch(msg).await {
Flow::Continue => {}
Flow::Close(cause) => {
self.close.request(cause);
break;
}
},
Err(TransportError::Closed) => {
warn!("transport closed by peer before exit notification");
self.close.request(CloseCause::ReaderEof);
break;
}
Err(error) => {
self.close.request(CloseCause::ReaderFailed(error));
break;
}
}
}
self.close().await;
self.close
.take_cause()
.expect("every path out of the read-loop records its close cause")
.into_result()
}
async fn dispatch(&mut self, msg: RawMessage) -> Flow {
match msg {
RawMessage::Request { id, method, params } => {
let span = info_span!("request", method = %method, id = ?id);
let cancellation = (method != "initialize").then(|| self.session.child_token());
let Some(reservation) = self.inbound.reserve(id.clone(), cancellation.clone())
else {
enqueue_error(
&self.out_tx,
id,
LspError::invalid_request("duplicate request id"),
);
return Flow::Continue;
};
if method != "initialize"
&& matches!(
self.lifecycle,
Lifecycle::Uninitialized(_) | Lifecycle::Initializing
)
{
self.inbound.complete(
&self.out_tx,
reservation,
Err(LspError::ServerNotInitialized),
);
return Flow::Continue;
}
if matches!(self.lifecycle, Lifecycle::ShuttingDown | Lifecycle::Exited) {
self.inbound.complete(
&self.out_tx,
reservation,
Err(LspError::invalid_request("invalid request")),
);
return Flow::Continue;
}
match method.as_ref() {
"initialize" => return self.initialize(&span, reservation, params).await,
"shutdown" => {
self.inbound.complete(
&self.out_tx,
reservation,
encode_body(&serde_json::Value::Null),
);
self.inbound.cancel_all_with_response(&self.out_tx);
self.lifecycle = Lifecycle::ShuttingDown;
}
_other => {
let service = match &self.lifecycle {
Lifecycle::Running(service) => Arc::clone(service),
_ => {
self.inbound.complete(
&self.out_tx,
reservation,
Err(LspError::ServerNotInitialized),
);
return Flow::Continue;
}
};
let params = match decode_value(¶ms) {
Ok(params) => params,
Err(error) => {
self.inbound.complete(&self.out_tx, reservation, Err(error));
return Flow::Continue;
}
};
self.spawn_service_request(
service,
span,
reservation,
method.into_owned(),
params,
cancellation.expect("non-initialize requests are cancellable"),
);
}
}
}
RawMessage::Notification { method, params } => match method.as_ref() {
"exit" => {
if matches!(
self.lifecycle,
Lifecycle::Running(_) | Lifecycle::ShuttingDown
) && let Some(hook) = self.on_exit.take()
{
let span = info_span!("notification", method = "exit");
let ctx = Context::for_notification(
span,
self.client.clone(),
self.established_workspace(),
);
hook(Arc::clone(&self.state), ctx).await;
}
let code = match self.lifecycle {
Lifecycle::ShuttingDown => 0,
_ => 1,
};
return Flow::Close(CloseCause::Exit { code });
}
"$/cancelRequest" => {
let bytes: &[u8] = if params.is_empty() { b"{}" } else { ¶ms };
match serde_json::from_slice::<CancelParams>(bytes) {
Ok(cancel) => self.inbound.complete_cancellation(&self.out_tx, &cancel.id),
Err(error) => {
debug!(%error, "ignoring malformed $/cancelRequest");
}
}
}
"initialized" => {
let Lifecycle::Running(_) = &self.lifecycle else {
debug!("initialized notification outside the running state ignored");
return Flow::Continue;
};
let params = match decode_initialized_params(¶ms) {
Ok(params) => params,
Err(error) => {
warn!(%error, "dropping initialized notification with malformed params");
return Flow::Continue;
}
};
let Some(hook) = self.on_initialized.take() else {
return Flow::Continue;
};
let span = info_span!("notification", method = "initialized");
let ctx = Context::for_notification(
span,
self.client.clone(),
self.established_workspace(),
);
hook(Arc::clone(&self.state), ctx, params).await;
}
other => {
let Lifecycle::Running(service) = &self.lifecycle else {
debug!(method = other, "notification outside running state ignored");
return Flow::Continue;
};
let service = Arc::clone(service);
if let Some(built_in) = ProtocolNotification::from_method(other) {
if !self.accepts_protocol_notification(built_in) {
debug!(
method = other,
"document-sync notification disabled and ignored"
);
return Flow::Continue;
}
match self.process_protocol_notification(built_in, ¶ms) {
Ok(BuiltInGate::RunHook) => {}
Ok(BuiltInGate::SkipHook) => return Flow::Continue,
Err(error) => {
warn!(method = other, %error, "protocol validation skipped its hook");
return Flow::Continue;
}
}
}
let params = match decode_value(¶ms) {
Ok(params) => params,
Err(error) => {
debug!(method = other, %error, "notification params ignored");
return Flow::Continue;
}
};
self.dispatch_notification(service, other, params).await;
}
},
RawMessage::Response { id, result } => {
let id_num = match &id {
RequestId::Number(n) if *n > 0 => Some(*n as u32),
_ => None,
};
let delivered =
id_num.is_some_and(|n| self.client.outbound_registry().complete(n, result));
if !delivered {
debug!(?id, "ignoring response with unknown or non-numeric id");
}
}
RawMessage::ProtocolError { error } => {
let _ = self.out_tx.send(RawMessage::ProtocolError { error });
}
}
Flow::Continue
}
async fn dispatch_notification(
&mut self,
service: UserService<S>,
method: &str,
params: serde_json::Value,
) {
let span = info_span!("notification", method = %method);
let ctx =
Context::for_notification(span, self.client.clone(), self.established_workspace());
let result = service
.call(IncomingCall::notification(
method.to_string(),
params,
ctx,
Arc::clone(&self.state),
))
.await;
if !matches!(result, ServiceResult::NoResponse) {
warn!("notification service attempted to produce a response");
}
}
fn process_protocol_notification(
&self,
built_in: ProtocolNotification,
raw_params: &Bytes,
) -> std::result::Result<BuiltInGate, LspError> {
match built_in {
ProtocolNotification::Open => {
let params: DidOpenTextDocumentParams = decode_params(raw_params)?;
self.documents.open(params.text_document);
}
ProtocolNotification::Change => {
let params: DidChangeTextDocumentParams = decode_params(raw_params)?;
validate_sync_changes(
self.document_sync
.change
.unwrap_or(TextDocumentSyncKind::INCREMENTAL),
¶ms.content_changes,
)?;
self.documents.apply_changes(
¶ms.text_document.uri,
params.text_document.version,
params.content_changes,
)?;
}
ProtocolNotification::Close => {
let params: DidCloseTextDocumentParams = decode_params(raw_params)?;
if self.documents.close(¶ms.text_document.uri).is_none() {
debug!(
uri = ?params.text_document.uri,
"closing a document that was not open"
);
}
}
ProtocolNotification::WillSave => {
let _: WillSaveTextDocumentParams = decode_params(raw_params)?;
}
ProtocolNotification::Save => {
let params: DidSaveTextDocumentParams = decode_params(raw_params)?;
if matches!(
&self.document_sync.save,
Some(TextDocumentSyncSaveOptions::SaveOptions(options))
if options.include_text == Some(true)
) && params.text.is_none()
{
return Err(LspError::invalid_request(
"didSave text is required by textDocumentSync.save.includeText",
));
}
}
ProtocolNotification::WorkspaceFolders => {
let params: DidChangeWorkspaceFoldersParams = decode_params(raw_params)?;
self.established_workspace().apply_folder_change(params);
}
ProtocolNotification::Configuration => {
let params: DidChangeConfigurationParams = decode_params(raw_params)?;
self.established_workspace()
.set_configuration(params.settings);
}
ProtocolNotification::Trace => {
let params: SetTraceParams = decode_params(raw_params)?;
self.established_workspace().set_trace(params.value);
}
ProtocolNotification::ProgressCancel => {
return Ok(gate_progress_cancel(
self.client.progress_registry(),
raw_params,
));
}
}
Ok(BuiltInGate::RunHook)
}
fn accepts_protocol_notification(&self, built_in: ProtocolNotification) -> bool {
match built_in {
ProtocolNotification::WorkspaceFolders
| ProtocolNotification::Configuration
| ProtocolNotification::Trace
| ProtocolNotification::ProgressCancel => true,
_ if self.document_sync.change == Some(TextDocumentSyncKind::NONE) => false,
ProtocolNotification::Open | ProtocolNotification::Close => {
self.document_sync.open_close == Some(true)
}
ProtocolNotification::Change => matches!(
self.document_sync.change,
Some(TextDocumentSyncKind::FULL | TextDocumentSyncKind::INCREMENTAL)
),
ProtocolNotification::WillSave => self.document_sync.will_save == Some(true),
ProtocolNotification::Save => matches!(
self.document_sync.save,
Some(TextDocumentSyncSaveOptions::Supported(true))
| Some(TextDocumentSyncSaveOptions::SaveOptions(_))
),
}
}
async fn initialize(&mut self, span: &Span, reservation: Reservation, params: Bytes) -> Flow {
if !matches!(self.lifecycle, Lifecycle::Uninitialized(_)) {
self.inbound.complete(
&self.out_tx,
reservation,
Err(LspError::ServerError {
code: -32600,
message: "server already initialized".into(),
data: None,
}),
);
return Flow::Continue;
}
let params = match decode_params::<InitializeParams>(¶ms) {
Ok(params) => params,
Err(err) => {
self.inbound.complete(&self.out_tx, reservation, Err(err));
return Flow::Continue;
}
};
let pending = match std::mem::replace(&mut self.lifecycle, Lifecycle::Initializing) {
Lifecycle::Uninitialized(pending) => *pending,
_ => unreachable!("initialize runs only while uninitialized"),
};
let Pending {
registrations,
file_provider,
configure_initialize,
on_initialize,
on_initialized,
on_exit,
layers,
concurrency_limit,
} = pending;
let mut registrar = InitializeRegistrar::new(registrations);
let committed = match configure_initialize {
Some(callback) => callback(¶ms, &mut registrar),
None => Ok(()),
}
.and_then(|()| registrar.commit().map_err(LspError::internal));
let registrations = match committed {
Ok(registrations) => registrations,
Err(_err) => {
self.inbound.complete(
&self.out_tx,
reservation,
Err(LspError::internal("initialization failed")),
);
return Flow::Close(CloseCause::InitializeFailed);
}
};
let router = Arc::new(registrations.freeze());
self.on_initialized = on_initialized;
self.on_exit = on_exit;
let established = Workspace::from_params_with_provider(
¶ms,
self.documents.clone(),
file_provider,
self.client.shared_trace(),
);
self.workspace = Some(established.clone());
let position_encoding = self.documents.negotiate_position_encoding(¶ms);
let mut capabilities = router.generated_capabilities();
let standard_capabilities = capabilities.standard_mut();
standard_capabilities.position_encoding = Some(position_encoding);
let document_sync = router.document_sync();
self.document_sync = document_sync.options;
standard_capabilities.text_document_sync = Some(document_sync.capability);
let file_operations = standard_capabilities
.workspace
.take()
.and_then(|workspace| workspace.file_operations);
standard_capabilities.workspace = Some(WorkspaceServerCapabilities {
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
supported: Some(true),
change_notifications: Some(OneOf::Left(true)),
}),
file_operations,
});
let server_info = match on_initialize {
Some(hook) => {
let ctx = Context::for_request(
reservation.id.clone(),
span.clone(),
self.client.clone(),
established,
);
match hook(
Arc::clone(&self.state),
ctx,
params,
self.session.child_token(),
)
.instrument(span.clone())
.await
{
Ok(server_info) => server_info,
Err(err) => {
self.inbound.complete(&self.out_tx, reservation, Err(err));
return Flow::Close(CloseCause::InitializeFailed);
}
}
}
None => None,
};
self.inbound.complete(
&self.out_tx,
reservation,
encode_body(&WireInitializeResult {
capabilities,
server_info,
}),
);
self.lifecycle = Lifecycle::Running(build_service_stack(router, layers, concurrency_limit));
Flow::Continue
}
fn established_workspace(&self) -> Workspace {
self.workspace.clone().expect(
"user dispatch runs only after the initialize transaction establishes the workspace",
)
}
fn spawn_service_request(
&mut self,
service: UserService<S>,
span: Span,
reservation: Reservation,
method: String,
params: serde_json::Value,
cancellation: CancellationToken,
) {
let state = Arc::clone(&self.state);
let workspace = self.established_workspace();
let out_tx = self.out_tx.clone();
let client = self.client.clone();
let inbound = self.inbound.clone();
self.tasks.spawn(async move {
let id = reservation.id.clone();
let ctx = Context::for_request(id.clone(), span, client, workspace)
.with_cancellation(cancellation.clone());
let call = IncomingCall::request(method, id, params, ctx, state);
let result = match select(
Box::pin(service.call(call)),
Box::pin(cancellation.cancelled()),
)
.await
{
Either::Left((result, _)) => result,
Either::Right(((), _)) => ServiceResult::Error(LspError::RequestCancelled),
};
let result = match result {
ServiceResult::Response(value) => encode_body(&value),
ServiceResult::Error(error) => Err(error),
ServiceResult::NoResponse => {
Err(LspError::internal("request service returned no response"))
}
};
inbound.complete(&out_tx, reservation, result);
});
}
async fn close(&mut self) {
if matches!(self.lifecycle, Lifecycle::Exited) {
return;
}
self.lifecycle = Lifecycle::Exited;
self.client.close_connection();
self.session.cancel();
self.client.outbound_registry().close_all();
self.inbound.close_all();
self.client.progress_registry().clear();
self.tasks.abort_and_join().await;
self.client.close_outbound();
if let Some(send_task) = self.send_task.take() {
send_task.join().await;
}
}
}
impl<S, R> Drop for ProtocolEngine<S, R> {
fn drop(&mut self) {
for handle in self.tasks.handles.iter().chain(self.send_task.iter()) {
handle.abort();
}
}
}
enum Flow {
Continue,
Close(CloseCause),
}
fn enqueue_encoded(
out_tx: &OutboundQueue,
id: RequestId,
result: std::result::Result<Bytes, LspError>,
) {
let response = match result {
Ok(bytes) => RawMessage::Response {
id,
result: Ok(bytes),
},
Err(err) => error_response(id, &err),
};
let _ = out_tx.send(response);
}
fn error_response(id: RequestId, err: &LspError) -> RawMessage {
RawMessage::Response {
id,
result: Err(JsonRpcError {
code: err.code(),
message: err.message(),
data: err.data().cloned(),
}),
}
}
fn enqueue_error(out_tx: &OutboundQueue, id: RequestId, err: LspError) {
let _ = out_tx.send(error_response(id, &err));
}
#[cfg(test)]
mod tests {
use lsp_types::ProgressToken;
use tracing_subscriber::layer::SubscriberExt;
use super::*;
fn gated_cancel(
registry: &ProgressRegistry,
raw_params: &'static [u8],
) -> (BuiltInGate, crate::test_util::EventCapture) {
let events = crate::test_util::EventCapture::new();
let subscriber = tracing_subscriber::registry().with(events.clone());
let gate = tracing::subscriber::with_default(subscriber, || {
gate_progress_cancel(registry, &Bytes::from_static(raw_params))
});
(gate, events)
}
#[test]
fn a_matching_cancellable_token_fires_and_opens_the_hook_gate() {
let registry = ProgressRegistry::default();
let cancellation = CancellationToken::new();
registry.register(ProgressToken::Number(1), true, cancellation.clone());
let (gate, events) = gated_cancel(®istry, br#"{"token": 1}"#);
assert!(
matches!(gate, BuiltInGate::RunHook),
"a successful decode lets the hook run"
);
assert!(cancellation.is_cancelled());
assert!(
registry.is_active(&ProgressToken::Number(1)),
"cancellation never ends the progress: the token stays registered"
);
assert!(
events.messages().is_empty(),
"a matched cancel logs nothing, got {:?}",
events.messages()
);
}
#[test]
fn non_cancellable_and_inactive_tokens_log_at_debug_and_keep_the_gate_open() {
let registry = ProgressRegistry::default();
let plain = CancellationToken::new();
registry.register(ProgressToken::Number(1), false, plain.clone());
let (gate, events) = gated_cancel(®istry, br#"{"token": 1}"#);
assert!(matches!(gate, BuiltInGate::RunHook));
assert!(!plain.is_cancelled(), "a non-cancellable token never fires");
assert!(
events.contains_at(tracing::Level::DEBUG, "non-cancellable token"),
"got {:?}",
events.messages()
);
let (gate, events) = gated_cancel(®istry, br#"{"token": 99}"#);
assert!(matches!(gate, BuiltInGate::RunHook));
assert!(
events.contains_at(tracing::Level::DEBUG, "unknown or ended token"),
"got {:?}",
events.messages()
);
assert!(
registry.is_active(&ProgressToken::Number(1)),
"an unknown token leaves the registry untouched"
);
}
#[test]
fn malformed_cancel_params_log_at_debug_and_close_the_hook_gate() {
let registry = ProgressRegistry::default();
let cancellation = CancellationToken::new();
registry.register(ProgressToken::Number(1), true, cancellation.clone());
for raw in [
br#"{"token": true}"#.as_slice(),
br#"{}"#.as_slice(),
b"not json".as_slice(),
] {
let (gate, events) = gated_cancel(®istry, raw);
assert!(
matches!(gate, BuiltInGate::SkipHook),
"malformed params {raw:?} skip the hook"
);
assert!(
events.contains_at(
tracing::Level::DEBUG,
"malformed window/workDoneProgress/cancel"
),
"got {:?}",
events.messages()
);
}
assert!(
!cancellation.is_cancelled(),
"malformed params never cancel anything"
);
}
fn content_change(with_range: bool) -> lsp_types::TextDocumentContentChangeEvent {
lsp_types::TextDocumentContentChangeEvent {
range: with_range.then(|| lsp_types::Range {
start: lsp_types::Position::new(0, 0),
end: lsp_types::Position::new(0, 1),
}),
range_length: None,
text: "replacement".to_string(),
}
}
#[test]
fn sync_kind_validation_accepts_only_compatible_change_shapes() {
assert!(
validate_sync_changes(TextDocumentSyncKind::FULL, &[content_change(false)]).is_ok()
);
assert!(
validate_sync_changes(TextDocumentSyncKind::FULL, &[content_change(true)]).is_err()
);
assert!(
validate_sync_changes(
TextDocumentSyncKind::INCREMENTAL,
&[content_change(true), content_change(false)],
)
.is_ok()
);
assert!(
validate_sync_changes(TextDocumentSyncKind::NONE, &[content_change(false)]).is_err()
);
}
#[test]
fn the_first_requester_records_the_cause_and_later_ones_do_not_replace_it() {
let close = CloseSignal::new();
assert!(!close.requested().is_cancelled());
close.request(CloseCause::WriterFailed);
close.request(CloseCause::Exit { code: 0 });
close.request(CloseCause::ReaderEof);
assert!(
close.requested().is_cancelled(),
"requesting close wakes the read-loop"
);
assert!(
matches!(close.take_cause(), Some(CloseCause::WriterFailed)),
"the first cause requested is the one reported"
);
assert!(
close.take_cause().is_none(),
"the cause is taken once, by the read-loop that ran the close"
);
}
#[test]
fn every_cause_maps_to_one_outcome_or_a_transport_error() {
assert_eq!(
CloseCause::Exit { code: 0 }.into_result().unwrap(),
Outcome::Exit { code: 0 }
);
assert_eq!(
CloseCause::ReaderEof.into_result().unwrap(),
Outcome::TransportClosed
);
assert_eq!(
CloseCause::WriterFailed.into_result().unwrap(),
Outcome::WriterFailed
);
assert_eq!(
CloseCause::InitializeFailed.into_result().unwrap(),
Outcome::InitializeFailed
);
assert!(matches!(
CloseCause::ReaderFailed(TransportError::Malformed("bad".into())).into_result(),
Err(Error::Transport(_))
));
}
#[test]
fn a_stale_reservation_cannot_claim_a_reused_request_id() {
let (out_tx, mut out_rx) = OutboundQueue::new(crate::DEFAULT_OUTBOUND_WARNING_THRESHOLD);
let registry = InboundRegistry::default();
let id = RequestId::Number(2);
let first = registry
.reserve(id.clone(), Some(CancellationToken::new()))
.expect("the id is free");
assert!(
registry
.reserve(id.clone(), Some(CancellationToken::new()))
.is_none(),
"an in-flight id is not reserved twice"
);
registry.complete_cancellation(&out_tx, &id);
let second = registry
.reserve(id.clone(), Some(CancellationToken::new()))
.expect("the id is free once the first request is answered");
registry.complete(&out_tx, first, encode_body(&"race"));
registry.complete(&out_tx, second, encode_body(&"reused"));
assert_eq!(
out_rx.try_recv().unwrap().id(),
Some(&id),
"the cancellation answers the first request"
);
let answer = out_rx.try_recv().expect("the second request is answered");
match answer {
RawMessage::Response {
result: Ok(body), ..
} => assert_eq!(
serde_json::from_slice::<String>(&body).unwrap(),
"reused",
"the second request gets its own result, not the stale one"
),
other => panic!("expected a success response, got {other:?}"),
}
assert!(
out_rx.try_recv().is_err(),
"the stale reservation enqueued nothing"
);
}
#[test]
fn only_a_shutdown_exit_reports_code_zero() {
assert_eq!(Outcome::Exit { code: 0 }.code(), 0);
assert_eq!(Outcome::Exit { code: 1 }.code(), 1);
assert_eq!(Outcome::TransportClosed.code(), 1);
assert_eq!(Outcome::WriterFailed.code(), 1);
assert_eq!(Outcome::InitializeFailed.code(), 1);
}
enum TestOutboundNotification {}
impl lsp_types::notification::Notification for TestOutboundNotification {
type Params = serde_json::Value;
const METHOD: &'static str = "test/outbound-notification";
}
enum TestOutboundRequest {}
impl lsp_types::request::Request for TestOutboundRequest {
type Params = serde_json::Value;
type Result = String;
const METHOD: &'static str = "test/outbound-request";
}
fn send_loop_message(tag: u8) -> RawMessage {
RawMessage::Notification {
method: "test/send-loop".into(),
params: Bytes::from(vec![tag]),
}
}
struct ScriptedWriter {
outbox: Arc<Mutex<Vec<RawMessage>>>,
fail_on_send: Option<usize>,
sends: usize,
}
impl TransportWriter for ScriptedWriter {
async fn send(&mut self, msg: RawMessage) -> std::result::Result<(), TransportError> {
self.sends += 1;
if self.fail_on_send == Some(self.sends) {
return Err(TransportError::Closed);
}
self.outbox.lock().unwrap().push(msg);
Ok(())
}
async fn shutdown(self) -> std::result::Result<(), TransportError> {
Ok(())
}
}
#[tokio::test]
async fn requests_responses_and_notifications_share_one_depth_counter() {
let (queue, _rx) = OutboundQueue::new(crate::DEFAULT_OUTBOUND_WARNING_THRESHOLD);
let client = Client::new(queue.clone(), OutboundRegistry::default());
client
.notify::<TestOutboundNotification>(serde_json::json!({}))
.unwrap();
enqueue_encoded(
&queue,
RequestId::Number(1),
Ok(Bytes::from_static(b"null")),
);
let pending = client.request::<TestOutboundRequest>(serde_json::json!({}));
futures_util::pin_mut!(pending);
assert!(
futures_util::poll!(pending.as_mut()).is_pending(),
"the request awaits the peer's response"
);
assert_eq!(queue.depth(), 3);
client
.outbound_registry()
.complete(1, Ok(Bytes::from_static(b"\"pong\"")));
let answer = pending.await;
assert_eq!(answer.unwrap(), "pong");
assert_eq!(
queue.depth(),
3,
"a notification, a response, and a request all increment the one counter"
);
}
#[tokio::test]
async fn the_send_loop_decrements_each_attempted_send_including_the_failed_one() {
let (queue, rx) = OutboundQueue::new(16);
let client = Client::new(queue.clone(), OutboundRegistry::default());
for tag in 0..3 {
queue.send(send_loop_message(tag)).unwrap();
}
assert_eq!(queue.depth(), 3);
let outbox = Arc::new(Mutex::new(Vec::new()));
let writer = ScriptedWriter {
outbox: outbox.clone(),
fail_on_send: Some(2),
sends: 0,
};
let close = CloseSignal::new();
send_loop(writer, rx, client, close.clone()).await;
assert_eq!(
queue.depth(),
1,
"the succeeded and the terminally failed send are both decremented"
);
assert_eq!(
outbox.lock().unwrap().len(),
1,
"only the first send landed on the transport"
);
assert!(
matches!(close.take_cause(), Some(CloseCause::WriterFailed)),
"the writer reports its terminal failure"
);
}
#[tokio::test]
async fn draining_after_close_decrements_every_message_in_order() {
let (queue, rx) = OutboundQueue::new(2);
let client = Client::new(queue.clone(), OutboundRegistry::default());
for tag in 0..3 {
queue.send(send_loop_message(tag)).unwrap();
}
client.close_outbound();
let outbox = Arc::new(Mutex::new(Vec::new()));
let writer = ScriptedWriter {
outbox: outbox.clone(),
fail_on_send: None,
sends: 0,
};
let close = CloseSignal::new();
send_loop(writer, rx, client, close.clone()).await;
assert_eq!(
queue.depth(),
0,
"draining decrements every queued message, above and below the threshold"
);
let tags: Vec<u8> = outbox
.lock()
.unwrap()
.iter()
.map(|msg| match msg {
RawMessage::Notification { params, .. } => params[0],
other => panic!("expected notifications, got {other:?}"),
})
.collect();
assert_eq!(
tags,
vec![0, 1, 2],
"draining never drops or reorders a message"
);
assert!(
close.take_cause().is_none(),
"clean draining is not a writer failure"
);
}
}