#![doc = include_str!("../README.md")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/solar/main/assets/logo.png",
html_favicon_url = "https://raw.githubusercontent.com/paradigmxyz/solar/main/assets/favicon.ico"
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use crate::global_state::GlobalState;
use async_lsp::{
ClientSocket, client_monitor::ClientProcessMonitorLayer, concurrency::ConcurrencyLayer,
router::Router, server::LifecycleLayer, tracing::TracingLayer,
};
use lsp_types::{notification as notif, request as req};
use serde_json as _;
use solar_config::LspArgs;
use std::ops::ControlFlow;
use tower::ServiceBuilder;
mod config;
mod global_state;
mod handlers;
mod inlay_hints;
mod proto;
mod serde;
mod symbols;
mod utils;
mod vfs;
mod workspace;
#[cfg(test)]
mod test_support;
pub(crate) type NotifyResult = ControlFlow<async_lsp::Result<()>>;
fn new_router(client: ClientSocket) -> Router<GlobalState> {
let this = GlobalState::new(client);
let mut router = Router::new(this);
router
.request::<req::Initialize, _>(GlobalState::on_initialize)
.notification::<notif::Initialized>(GlobalState::on_initialized)
.request::<req::Shutdown, _>(|_, _| std::future::ready(Ok(())))
.notification::<notif::Exit>(|_, _| ControlFlow::Break(Ok(())));
router
.request::<req::DocumentSymbolRequest, _>(handlers::document_symbol)
.request::<req::WorkspaceSymbolRequest, _>(handlers::workspace_symbol)
.request::<req::GotoDefinition, _>(handlers::goto_definition)
.request::<req::GotoDeclaration, _>(handlers::goto_declaration)
.request::<req::References, _>(handlers::references)
.request::<req::InlayHintRequest, _>(handlers::inlay_hints)
.request::<req::Completion, _>(handlers::completion);
router
.notification::<notif::DidChangeWorkspaceFolders>(handlers::did_change_workspace_folders)
.notification::<notif::DidChangeWatchedFiles>(handlers::did_change_watched_files);
router
.notification::<notif::DidOpenTextDocument>(handlers::did_open_text_document)
.notification::<notif::DidCloseTextDocument>(handlers::did_close_text_document)
.notification::<notif::DidChangeTextDocument>(handlers::did_change_text_document)
.notification::<notif::DidChangeConfiguration>(handlers::did_change_configuration);
router
}
pub async fn run_server_stdio(_args: LspArgs) -> async_lsp::Result<()> {
#[cfg(unix)]
let (stdin, stdout) =
(async_lsp::stdio::PipeStdin::lock_tokio()?, async_lsp::stdio::PipeStdout::lock_tokio()?);
#[cfg(not(unix))]
let (stdin, stdout) = (
tokio_util::compat::TokioAsyncReadCompatExt::compat(tokio::io::stdin()),
tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(tokio::io::stdout()),
);
let (eloop, _) = async_lsp::MainLoop::new_server(|client| {
ServiceBuilder::new()
.layer(TracingLayer::default())
.layer(LifecycleLayer::default())
.layer(ConcurrencyLayer::new(2.try_into().unwrap()))
.layer(ClientProcessMonitorLayer::new(client.clone()))
.service(new_router(client))
});
eloop.run_buffered(stdin, stdout).await
}
#[cfg(test)]
mod tests {
use super::*;
use async_lsp::{AnyNotification, LanguageServer, LspService, router::Router};
use lsp_types::{
DidChangeWatchedFilesClientCapabilities, DidChangeWatchedFilesParams, FileChangeType,
FileEvent, InitializeParams, InitializedParams, WorkspaceClientCapabilities,
notification as notif, notification::Notification, request,
};
use std::ops::ControlFlow;
use tokio::sync::oneshot;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
#[tokio::test(flavor = "current_thread")]
async fn router_handles_watched_file_changes() {
let mut router = new_router(ClientSocket::new_closed());
let params = DidChangeWatchedFilesParams {
changes: vec![FileEvent::new(
lsp_types::Url::parse("file:///workspace/src/Test.sol").unwrap(),
FileChangeType::CHANGED,
)],
};
let notification = serde_json::from_value::<AnyNotification>(serde_json::json!({
"method": notif::DidChangeWatchedFiles::METHOD,
"params": params,
}))
.unwrap();
assert!(matches!(router.notify(notification), ControlFlow::Continue(())));
}
#[tokio::test(flavor = "current_thread")]
async fn initialized_registers_watched_files_when_client_supports_dynamic_registration() {
let (server_main, _client) = async_lsp::MainLoop::new_server(new_router);
let (registration_tx, registration_rx) = oneshot::channel();
let (client_main, mut server) = async_lsp::MainLoop::new_client(|_| {
let mut router = Router::new(Some(registration_tx));
router.request::<request::RegisterCapability, _>(|state, params| {
state.take().unwrap().send(params).unwrap();
async move { Ok(()) }
});
router.notification::<notif::LogMessage>(|_, _| ControlFlow::Continue(()));
router
});
let (server_stream, client_stream) = tokio::io::duplex(64 << 10);
let (server_rx, server_tx) = tokio::io::split(server_stream);
let (server_rx, server_tx) = (server_rx.compat(), server_tx.compat_write());
let server_main =
tokio::spawn(async move { server_main.run_buffered(server_rx, server_tx).await });
let (client_rx, client_tx) = tokio::io::split(client_stream);
let (client_rx, client_tx) = (client_rx.compat(), client_tx.compat_write());
let client_main =
tokio::spawn(async move { client_main.run_buffered(client_rx, client_tx).await });
let mut params = InitializeParams::default();
params.capabilities.workspace = Some(WorkspaceClientCapabilities {
did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities {
dynamic_registration: Some(true),
..Default::default()
}),
..Default::default()
});
server.initialize(params).await.unwrap();
server.initialized(InitializedParams {}).unwrap();
let registrations =
tokio::time::timeout(std::time::Duration::from_secs(1), registration_rx)
.await
.unwrap()
.unwrap();
let [registration] = registrations.registrations.try_into().unwrap();
assert_eq!(registration.method, notif::DidChangeWatchedFiles::METHOD);
server.shutdown(()).await.unwrap();
server.exit(()).unwrap();
assert!(server_main.await.unwrap().is_ok());
assert!(matches!(client_main.await.unwrap(), Err(async_lsp::Error::Eof)));
}
}