docs: documenting sync-lsp crate (#1155)

This commit is contained in:
Myriad-Dreamin 2025-01-11 18:09:45 +08:00 committed by GitHub
parent 0e2874a6c9
commit 8e918cb132
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 149 additions and 46 deletions

View file

@ -13,21 +13,19 @@ rust-version.workspace = true
[dependencies]
parking_lot.workspace = true
log.workspace = true
futures.workspace = true
anyhow.workspace = true
tokio.workspace = true
tokio-util.workspace = true
clap = { workspace = true, optional = true }
crossbeam-channel.workspace = true
futures.workspace = true
log.workspace = true
lsp-server.workspace = true
lsp-types.workspace = true
parking_lot.workspace = true
reflexo.workspace = true
serde.workspace = true
serde_json.workspace = true
clap = { workspace = true, optional = true }
tokio.workspace = true
tokio-util.workspace = true
# [lints]
# workspace = true
[lints]
workspace = true

View file

@ -1,3 +1,5 @@
//! A synchronous language server implementation.
use core::fmt;
use std::any::Any;
use std::path::Path;
@ -19,25 +21,27 @@ pub mod transport;
type Event = Box<dyn Any + Send>;
/// The sender of the language server.
#[derive(Debug, Clone)]
pub struct ConnectionTx {
/// The sender of the events.
pub event: crossbeam_channel::Sender<Event>,
/// The sender of the LSP messages.
pub lsp: crossbeam_channel::Sender<Message>,
}
/// The sender of the language server.
#[derive(Debug, Clone)]
pub struct ConnectionRx {
/// The receiver of the events.
pub event: crossbeam_channel::Receiver<Event>,
/// The receiver of the LSP messages.
pub lsp: crossbeam_channel::Receiver<Message>,
}
pub enum EventOrMessage {
Evt(Event),
Msg(Message),
}
impl ConnectionRx {
pub fn recv(&self) -> Result<EventOrMessage, RecvError> {
/// Receives a message or an event.
fn recv(&self) -> Result<EventOrMessage, RecvError> {
crossbeam_channel::select_biased! {
recv(self.lsp) -> msg => msg.map(EventOrMessage::Msg),
recv(self.event) -> event => event.map(EventOrMessage::Evt),
@ -45,12 +49,21 @@ impl ConnectionRx {
}
}
/// This is a helper enum to handle both events and messages.
enum EventOrMessage {
Evt(Event),
Msg(Message),
}
/// Connection is just a pair of channels of LSP messages.
pub struct Connection {
/// The senders of the connection.
pub sender: ConnectionTx,
/// The receivers of the connection.
pub receiver: ConnectionRx,
}
/// The common message type for the language server.
pub type Message = lsp_server::Message;
/// The common error type for the language server.
@ -109,6 +122,7 @@ pub struct TypedLspClient<S> {
}
impl<S> TypedLspClient<S> {
/// Converts the client to an untyped client.
pub fn to_untyped(self) -> LspClient {
self.client
}
@ -207,6 +221,7 @@ impl LspClient {
self.req_queue.lock().incoming.has_pending()
}
/// Prints states of the request queue and panics.
pub fn begin_panic(&self) {
self.req_queue.lock().begin_panic();
}
@ -353,39 +368,55 @@ type MayInitBoxHandler<A, S, T> =
Box<dyn for<'a> Fn(ServiceState<'a, A, S>, &LspClient, T) -> anyhow::Result<()>>;
type EventMap<A, S> = HashMap<core::any::TypeId, MayInitBoxHandler<A, S, Event>>;
/// A trait that initializes the language server.
pub trait Initializer {
/// The type of the initialization request.
type I: for<'de> serde::Deserialize<'de>;
/// The type of the service.
type S;
/// Handles the initialization request.
/// If the behind protocol is the standard LSP, the request is
/// `InitializeParams`.
fn initialize(self, req: Self::I) -> (Self::S, AnySchedulableResponse);
}
/// The builder pattern for the language server.
pub struct LspBuilder<Args: Initializer> {
/// The extra initialization arguments.
pub args: Args,
/// The client surface for the implementing language server.
pub client: LspClient,
/// The event handlers.
pub events: EventMap<Args, Args::S>,
pub exec_cmds: ExecuteCmdMap<Args::S>,
pub notify_cmds: NotifyCmdMap<Args::S>,
pub regular_cmds: RegularCmdMap<Args::S>,
pub resource_routes: ResourceMap<Args::S>,
/// The command handlers.
pub command_handlers: ExecuteCmdMap<Args::S>,
/// The notification handlers.
pub notif_handlers: NotifyCmdMap<Args::S>,
/// The request handlers.
pub req_handlers: RegularCmdMap<Args::S>,
/// The resource handlers.
pub resource_handlers: ResourceMap<Args::S>,
}
impl<Args: Initializer> LspBuilder<Args>
where
Args::S: 'static,
{
/// Creates a new language server builder.
pub fn new(args: Args, client: LspClient) -> Self {
Self {
args,
client,
events: EventMap::new(),
exec_cmds: ExecuteCmdMap::new(),
notify_cmds: NotifyCmdMap::new(),
regular_cmds: RegularCmdMap::new(),
resource_routes: ResourceMap::new(),
command_handlers: ExecuteCmdMap::new(),
notif_handlers: NotifyCmdMap::new(),
req_handlers: RegularCmdMap::new(),
resource_handlers: ResourceMap::new(),
}
}
/// Registers an event handler.
pub fn with_event<T: std::any::Any>(
mut self,
ins: &T,
@ -398,65 +429,74 @@ where
self
}
/// Registers an raw event handler.
pub fn with_command_(
mut self,
cmd: &'static str,
handler: RawHandler<Args::S, Vec<JsonValue>>,
) -> Self {
self.exec_cmds.insert(cmd, raw_to_boxed(handler));
self.command_handlers.insert(cmd, raw_to_boxed(handler));
self
}
/// Registers an async command handler.
pub fn with_command<R: Serialize + 'static>(
mut self,
cmd: &'static str,
handler: AsyncHandler<Args::S, Vec<JsonValue>, R>,
) -> Self {
self.exec_cmds.insert(
self.command_handlers.insert(
cmd,
Box::new(move |s, client, req_id, req| client.schedule(req_id, handler(s, req))),
);
self
}
/// Registers an untyped notification handler.
pub fn with_notification_<R: Notif>(
mut self,
handler: PureHandler<Args::S, JsonValue>,
) -> Self {
self.notify_cmds.insert(R::METHOD, Box::new(handler));
self.notif_handlers.insert(R::METHOD, Box::new(handler));
self
}
/// Registers a typed notification handler.
pub fn with_notification<R: Notif>(mut self, handler: PureHandler<Args::S, R::Params>) -> Self {
self.notify_cmds.insert(
self.notif_handlers.insert(
R::METHOD,
Box::new(move |s, req| handler(s, from_json(req)?)),
);
self
}
/// Registers a raw request handler that handlers a kind of untyped lsp
/// request.
pub fn with_raw_request<R: Req>(mut self, handler: RawHandler<Args::S, JsonValue>) -> Self {
self.regular_cmds.insert(R::METHOD, raw_to_boxed(handler));
self.req_handlers.insert(R::METHOD, raw_to_boxed(handler));
self
}
// todo: unsafe typed
/// Registers an raw request handler that handlers a kind of typed lsp
/// request.
pub fn with_request_<R: Req>(
mut self,
handler: fn(&mut Args::S, RequestId, R::Params) -> ScheduledResult,
) -> Self {
self.regular_cmds.insert(
self.req_handlers.insert(
R::METHOD,
Box::new(move |s, _client, req_id, req| handler(s, req_id, from_json(req)?)),
);
self
}
/// Registers a typed request handler.
pub fn with_request<R: Req>(
mut self,
handler: AsyncHandler<Args::S, R::Params, R::Result>,
) -> Self {
self.regular_cmds.insert(
self.req_handlers.insert(
R::METHOD,
Box::new(move |s, client, req_id, req| {
client.schedule(req_id, handler(s, from_json(req)?))
@ -465,46 +505,53 @@ where
self
}
/// Registers a raw resource handler.
pub fn with_resource_(
mut self,
path: ImmutPath,
handler: RawHandler<Args::S, Vec<JsonValue>>,
) -> Self {
self.resource_routes.insert(path, raw_to_boxed(handler));
self.resource_handlers.insert(path, raw_to_boxed(handler));
self
}
/// Registers an async resource handler.
pub fn with_resource(
mut self,
path: &'static str,
handler: fn(&mut Args::S, Vec<JsonValue>) -> AnySchedulableResponse,
) -> Self {
self.resource_routes.insert(
self.resource_handlers.insert(
Path::new(path).into(),
Box::new(move |s, client, req_id, req| client.schedule(req_id, handler(s, req))),
);
self
}
/// Builds the language server driver.
pub fn build(self) -> LspDriver<Args> {
LspDriver {
state: State::Uninitialized(Some(Box::new(self.args))),
events: self.events,
client: self.client,
commands: self.exec_cmds,
notifications: self.notify_cmds,
requests: self.regular_cmds,
resources: self.resource_routes,
commands: self.command_handlers,
notifications: self.notif_handlers,
requests: self.req_handlers,
resources: self.resource_handlers,
}
}
}
/// An enum to represent the state of the language server.
pub enum ServiceState<'a, A, S> {
/// The service is uninitialized.
Uninitialized(Option<&'a mut A>),
/// The service is initializing.
Ready(&'a mut S),
}
impl<A, S> ServiceState<'_, A, S> {
/// Converts the state to an option holding the ready service.
pub fn ready(&mut self) -> Option<&mut S> {
match self {
ServiceState::Ready(s) => Some(s),
@ -537,6 +584,7 @@ impl<Args, S> State<Args, S> {
}
}
/// The language server driver.
pub struct LspDriver<Args: Initializer> {
/// State to synchronize with the client.
state: State<Args, Args::S>,
@ -557,14 +605,17 @@ pub struct LspDriver<Args: Initializer> {
}
impl<Args: Initializer> LspDriver<Args> {
/// Gets the state of the language server.
pub fn state(&self) -> Option<&Args::S> {
self.state.opt()
}
/// Gets the mutable state of the language server.
pub fn state_mut(&mut self) -> Option<&mut Args::S> {
self.state.opt_mut()
}
/// Makes the language server ready.
pub fn ready(&mut self, params: Args::I) -> AnySchedulableResponse {
let args = match &mut self.state {
State::Uninitialized(args) => args,
@ -583,6 +634,13 @@ impl<Args: Initializer> LspDriver<Args>
where
Args::S: 'static,
{
/// Starts the language server on the given connection.
///
/// If `is_replay` is true, the server will wait for all pending requests to
/// finish before exiting. This is useful for testing the language server.
///
/// See [`transport::MirrorArgs`] for information about the record-replay
/// feature.
pub fn start(&mut self, inbox: ConnectionRx, is_replay: bool) -> anyhow::Result<()> {
let res = self.start_(inbox);
@ -611,6 +669,7 @@ where
res
}
/// Starts the language server on the given connection.
pub fn start_(&mut self, inbox: ConnectionRx) -> anyhow::Result<()> {
use EventOrMessage::*;
// todo: follow what rust analyzer does
@ -855,30 +914,33 @@ fn resp_err(code: ErrorCode, msg: impl fmt::Display) -> ResponseError {
}
}
/// Creates an invalid params error.
pub fn invalid_params(msg: impl fmt::Display) -> ResponseError {
resp_err(ErrorCode::InvalidParams, msg)
}
/// Creates an internal error.
pub fn internal_error(msg: impl fmt::Display) -> ResponseError {
resp_err(ErrorCode::InternalError, msg)
}
/// Creates a not initialized error.
pub fn not_initialized() -> ResponseError {
resp_err(ErrorCode::ServerNotInitialized, "not initialized yet")
}
/// Creates a method not found error.
pub fn method_not_found() -> ResponseError {
resp_err(ErrorCode::MethodNotFound, "method not found")
}
/// Creates an invalid request error.
pub fn invalid_request(msg: impl fmt::Display) -> ResponseError {
resp_err(ErrorCode::InvalidRequest, msg)
}
pub fn result_to_response<T: Serialize>(
id: RequestId,
result: Result<T, ResponseError>,
) -> Response {
/// Converts an [`LspResult`] to a response.
pub fn result_to_response<T: Serialize>(id: RequestId, result: LspResult<T>) -> Response {
match result.and_then(|t| serde_json::to_value(t).map_err(internal_error)) {
Ok(resp) => Response::new_ok(id, resp),
Err(e) => Response::new_err(id, e.code, e.message),

View file

@ -1,3 +1,7 @@
//! The request-response queue for the LSP server.
//!
//! This is stolen from the lsp-server crate for customization.
use core::fmt;
use std::collections::HashMap;
@ -7,7 +11,9 @@ use lsp_server::{ErrorCode, Request, RequestId, Response, ResponseError};
/// Manages the set of pending requests, both incoming and outgoing.
pub struct ReqQueue<I, O> {
/// The incoming requests.
pub incoming: Incoming<I>,
/// The outgoing requests.
pub outgoing: Outgoing<O>,
}
@ -32,6 +38,7 @@ impl<I, O> fmt::Debug for ReqQueue<I, O> {
}
impl<I, O> ReqQueue<I, O> {
/// Prints states of the request queue and panics.
pub fn begin_panic(&self) {
let keys = self.incoming.pending.keys().cloned().collect::<Vec<_>>();
log::error!("incoming pending: {keys:?}");
@ -42,11 +49,15 @@ impl<I, O> ReqQueue<I, O> {
}
}
/// The incoming request queue.
#[derive(Debug)]
pub struct Incoming<I> {
pending: HashMap<RequestId, I>,
}
/// The outgoing request queue.
///
/// It holds the next request ID and the pending requests.
#[derive(Debug)]
pub struct Outgoing<O> {
next_id: i32,
@ -54,14 +65,24 @@ pub struct Outgoing<O> {
}
impl<I> Incoming<I> {
/// Registers a request with the given ID and data.
pub fn register(&mut self, id: RequestId, data: I) {
self.pending.insert(id, data);
}
/// Checks if there are *any* pending requests.
///
/// This is useful for testing language server.
pub fn has_pending(&self) -> bool {
!self.pending.is_empty()
}
/// Checks if a request with the given ID is completed.
pub fn is_completed(&self, id: &RequestId) -> bool {
!self.pending.contains_key(id)
}
/// Cancels a request with the given ID.
pub fn cancel(&mut self, id: RequestId) -> Option<Response> {
let _data = self.complete(id.clone())?;
let error = ResponseError {
@ -76,16 +97,14 @@ impl<I> Incoming<I> {
})
}
/// Completes a request with the given ID.
pub fn complete(&mut self, id: RequestId) -> Option<I> {
self.pending.remove(&id)
}
pub fn is_completed(&self, id: &RequestId) -> bool {
!self.pending.contains_key(id)
}
}
impl<O> Outgoing<O> {
/// Registers a request with the given method, params, and data.
pub fn register<P: Serialize>(&mut self, method: String, params: P, data: O) -> Request {
let id = RequestId::from(self.next_id);
self.pending.insert(id.clone(), data);
@ -93,6 +112,7 @@ impl<O> Outgoing<O> {
Request::new(id, method, params)
}
/// Completes a request with the given ID.
pub fn complete(&mut self, id: RequestId) -> Option<O> {
self.pending.remove(&id)
}

View file

@ -1,3 +1,5 @@
//! Transport layer for LSP messages.
use std::{
io::{self, BufRead, Read, Write},
thread,
@ -7,6 +9,27 @@ use crossbeam_channel::{bounded, Receiver, Sender};
use crate::{Connection, ConnectionRx, ConnectionTx, Message};
/// Convenience cli arguments for setting up a transport with an optional mirror
/// or replay file.
///
/// The `mirror` argument will write the stdin to the file.
/// The `replay` argument will read the file as input.
///
/// # Example
///
/// The example below shows the typical usage of the `MirrorArgs` struct.
/// It records an LSP session and replays it to compare the output.
///
/// If the language server has stable output, the replayed output should be the
/// same.
///
/// ```bash
/// $ my-lsp --mirror /tmp/mirror.log > responses.txt
/// $ ls /tmp
/// mirror.log
/// $ my-lsp --replay /tmp/mirror.log > responses-replayed.txt
/// $ diff responses.txt responses-replayed.txt
/// ```
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "clap", derive(clap::Parser))]
pub struct MirrorArgs {

View file

@ -267,7 +267,7 @@ impl LanguageState {
provider.args.add_commands(
&Some("tinymist.getResources")
.iter()
.chain(provider.exec_cmds.keys())
.chain(provider.command_handlers.keys())
.map(ToString::to_string)
.collect::<Vec<_>>(),
);