feat: add a basic lsp
This commit is contained in:
@@ -14,12 +14,13 @@ clap = { version = "3.1.14", features = ["derive"] }
|
||||
hex = "0.4.3"
|
||||
ignore = "0.4.18"
|
||||
miette = { version = "5.3.0", features = ["fancy"] }
|
||||
pallas-addresses = "0.14.0-alpha.3"
|
||||
pallas-codec = "0.14.0-alpha.3"
|
||||
pallas-crypto = "0.14.0-alpha.3"
|
||||
pallas-primitives = "0.14.0-alpha.3"
|
||||
pallas-traverse = "0.14.0-alpha.3"
|
||||
pallas-addresses = "0.14.0"
|
||||
pallas-codec = "0.14.0"
|
||||
pallas-crypto = "0.14.0"
|
||||
pallas-primitives = "0.14.0"
|
||||
pallas-traverse = "0.14.0"
|
||||
|
||||
aiken-lang = { path = "../lang", version = "0.0.24" }
|
||||
aiken-lsp = { path = "../lsp", version = "0.0.0" }
|
||||
aiken-project = { path = '../project', version = "0.0.24" }
|
||||
uplc = { path = '../uplc', version = "0.0.24" }
|
||||
|
||||
13
crates/cli/src/cmd/lsp.rs
Normal file
13
crates/cli/src/cmd/lsp.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use miette::IntoDiagnostic;
|
||||
|
||||
#[derive(clap::Args)]
|
||||
/// Start the Aiken language server
|
||||
pub struct Args {
|
||||
/// Run on stdio
|
||||
#[clap(long)]
|
||||
stdio: bool,
|
||||
}
|
||||
|
||||
pub fn exec(_args: Args) -> miette::Result<()> {
|
||||
aiken_lsp::start().into_diagnostic()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod build;
|
||||
pub mod check;
|
||||
pub mod fmt;
|
||||
pub mod lsp;
|
||||
pub mod new;
|
||||
pub mod tx;
|
||||
pub mod uplc;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use aiken::cmd::{build, check, fmt, new, tx, uplc};
|
||||
use aiken::cmd::{build, check, fmt, lsp, new, tx, uplc};
|
||||
use clap::Parser;
|
||||
|
||||
/// Aiken: a smart-contract language and toolchain for Cardano
|
||||
@@ -12,6 +12,9 @@ pub enum Cmd {
|
||||
Build(build::Args),
|
||||
Check(check::Args),
|
||||
|
||||
#[clap(hide = true)]
|
||||
Lsp(lsp::Args),
|
||||
|
||||
#[clap(subcommand)]
|
||||
Tx(tx::Cmd),
|
||||
|
||||
@@ -32,6 +35,7 @@ fn main() -> miette::Result<()> {
|
||||
Cmd::Fmt(args) => fmt::exec(args),
|
||||
Cmd::Build(args) => build::exec(args),
|
||||
Cmd::Check(args) => check::exec(args),
|
||||
Cmd::Lsp(args) => lsp::exec(args),
|
||||
Cmd::Tx(sub_cmd) => tx::exec(sub_cmd),
|
||||
Cmd::Uplc(sub_cmd) => uplc::exec(sub_cmd),
|
||||
}
|
||||
|
||||
22
crates/lsp/Cargo.toml
Normal file
22
crates/lsp/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "aiken-lsp"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
description = "Cardano smart contract language and toolchain"
|
||||
repository = "https://github.com/txpipe/aiken"
|
||||
homepage = "https://github.com/txpipe/aiken"
|
||||
license = "Apache-2.0"
|
||||
authors = ["Lucas Rosa <x@rvcas.dev>"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
aiken-lang = { path = '../lang', version = "0.0.24" }
|
||||
aiken-project = { path = '../project', version = "0.0.24" }
|
||||
crossbeam-channel = "0.5.6"
|
||||
lsp-server = "0.6.0"
|
||||
lsp-types = "0.93.2"
|
||||
miette = "5.4.1"
|
||||
serde = "1.0.147"
|
||||
serde_json = "1.0.87"
|
||||
thiserror = "1.0.37"
|
||||
tracing = "0.1.37"
|
||||
152
crates/lsp/src/lib.rs
Normal file
152
crates/lsp/src/lib.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use std::{fs, io, path::Path};
|
||||
|
||||
use aiken_lang::{ast::ModuleKind, parser};
|
||||
use crossbeam_channel::SendError;
|
||||
use lsp_server::{Connection, ExtractError, Message};
|
||||
use lsp_types::{
|
||||
request::{Formatting, Request},
|
||||
DocumentFormattingParams, InitializeParams, OneOf, ServerCapabilities, TextEdit,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(aiken::lsp::server_capabilities))]
|
||||
ServerCapabilities(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(aiken::lsp::server_init))]
|
||||
ServerInit(#[from] lsp_server::ProtocolError),
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(aiken::lsp::io))]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Unsupported LSP request: {request}")]
|
||||
#[diagnostic(code(aiken::lsp::unsupported_lsp_request))]
|
||||
UnsupportedLspRequest { request: String },
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(aiken::lsp::cast_request))]
|
||||
CastRequest(#[from] ExtractError<lsp_server::Request>),
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(aiken::lsp::send))]
|
||||
Send(#[from] SendError<Message>),
|
||||
}
|
||||
|
||||
pub fn start() -> Result<(), Error> {
|
||||
tracing::info!("Aiken language server starting");
|
||||
|
||||
// Create the transport. Includes the stdio (stdin and stdout) versions but this could
|
||||
// also be implemented to use sockets or HTTP.
|
||||
let (connection, io_threads) = Connection::stdio();
|
||||
|
||||
// Run the server and wait for the two threads to end (typically by trigger LSP Exit event).
|
||||
let server_capabilities = serde_json::to_value(&ServerCapabilities {
|
||||
definition_provider: Some(OneOf::Left(true)),
|
||||
document_formatting_provider: Some(OneOf::Left(true)),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
let initialization_params = connection.initialize(server_capabilities)?;
|
||||
let initialization_params = serde_json::from_value(initialization_params)?;
|
||||
|
||||
main_loop(connection, initialization_params)?;
|
||||
|
||||
io_threads.join()?;
|
||||
|
||||
tracing::info!("Aiken language server shutting down");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main_loop(connection: Connection, _params: InitializeParams) -> Result<(), Error> {
|
||||
for msg in &connection.receiver {
|
||||
tracing::debug!("Got message: {:#?}", msg);
|
||||
|
||||
match msg {
|
||||
Message::Request(req) => {
|
||||
if connection.handle_shutdown(&req)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::debug!("Get request: {:#?}", req);
|
||||
|
||||
let response = handle_request(req)?;
|
||||
|
||||
connection.sender.send(Message::Response(response))?;
|
||||
}
|
||||
Message::Response(_) => todo!(),
|
||||
Message::Notification(_) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_request(request: lsp_server::Request) -> Result<lsp_server::Response, Error> {
|
||||
let id = request.id.clone();
|
||||
|
||||
match request.method.as_str() {
|
||||
Formatting::METHOD => {
|
||||
let params = cast_request::<Formatting>(request)?;
|
||||
|
||||
let result = format(params);
|
||||
|
||||
match result {
|
||||
Ok(text_edit) => {
|
||||
let result = serde_json::to_value(text_edit)?;
|
||||
|
||||
Ok(lsp_server::Response {
|
||||
id,
|
||||
error: None,
|
||||
result: Some(result),
|
||||
})
|
||||
}
|
||||
Err(_) => {
|
||||
todo!("transform project errors in lsp diagnostic")
|
||||
}
|
||||
}
|
||||
}
|
||||
unsupported => Err(Error::UnsupportedLspRequest {
|
||||
request: unsupported.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn cast_request<R>(request: lsp_server::Request) -> Result<R::Params, Error>
|
||||
where
|
||||
R: lsp_types::request::Request,
|
||||
R::Params: serde::de::DeserializeOwned,
|
||||
{
|
||||
let (_, params) = request.extract(R::METHOD)?;
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn format(params: DocumentFormattingParams) -> Result<Vec<TextEdit>, aiken_project::error::Error> {
|
||||
let path = params.text_document.uri.path();
|
||||
let mut new_text = String::new();
|
||||
|
||||
let src = fs::read_to_string(path)?;
|
||||
|
||||
let (module, extra) = parser::module(&src, ModuleKind::Lib).map_err(|errs| {
|
||||
aiken_project::error::Error::from_parse_errors(errs, Path::new(path), &src)
|
||||
})?;
|
||||
|
||||
aiken_lang::format::pretty(&mut new_text, module, extra, &src);
|
||||
|
||||
Ok(vec![text_edit_replace(new_text)])
|
||||
}
|
||||
|
||||
fn text_edit_replace(new_text: String) -> TextEdit {
|
||||
TextEdit {
|
||||
range: lsp_types::Range {
|
||||
start: lsp_types::Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: lsp_types::Position {
|
||||
line: u32::MAX,
|
||||
character: 0,
|
||||
},
|
||||
},
|
||||
new_text,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user