From e90a2105372ee5287f56911a918a6b1503601a4b Mon Sep 17 00:00:00 2001 From: rvcas Date: Thu, 10 Nov 2022 01:03:41 -0500 Subject: [PATCH] feat: add a basic lsp --- Cargo.lock | 188 ++++++++++++++++++++++++++++++++++++-- crates/cli/Cargo.toml | 11 ++- crates/cli/src/cmd/lsp.rs | 13 +++ crates/cli/src/cmd/mod.rs | 1 + crates/cli/src/main.rs | 6 +- crates/lsp/Cargo.toml | 22 +++++ crates/lsp/src/lib.rs | 152 ++++++++++++++++++++++++++++++ 7 files changed, 377 insertions(+), 16 deletions(-) create mode 100644 crates/cli/src/cmd/lsp.rs create mode 100644 crates/lsp/Cargo.toml create mode 100644 crates/lsp/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index cb2987fb..6127e70d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,7 @@ name = "aiken" version = "0.0.24" dependencies = [ "aiken-lang", + "aiken-lsp", "aiken-project", "anyhow", "clap", @@ -80,6 +81,22 @@ dependencies = [ "vec1", ] +[[package]] +name = "aiken-lsp" +version = "0.0.0" +dependencies = [ + "aiken-lang", + "aiken-project", + "crossbeam-channel", + "lsp-server", + "lsp-types", + "miette", + "serde", + "serde_json", + "thiserror", + "tracing", +] + [[package]] name = "aiken-project" version = "0.0.24" @@ -274,6 +291,16 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.12" @@ -347,6 +374,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + [[package]] name = "getrandom" version = "0.2.7" @@ -413,6 +449,16 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "ignore" version = "0.4.18" @@ -492,6 +538,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lsp-server" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70570c1c29cf6654029b8fe201a5507c153f0d85be6f234d471d756bc36775a" +dependencies = [ + "crossbeam-channel", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "lsp-types" +version = "0.93.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be6e9c7e2d18f651974370d7aff703f9513e0df6e464fd795660edc77e6ca51" +dependencies = [ + "bitflags", + "serde", + "serde_json", + "serde_repr", + "url", +] + [[package]] name = "memchr" version = "2.5.0" @@ -500,9 +571,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "miette" -version = "5.3.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28d6092d7e94a90bb9ea8e6c26c99d5d112d49dda2afdb4f7ea8cf09e1a5a6d" +checksum = "7a24c4b4063c21e037dffb4de388ee85e400bff299803aba9513d9c52de8116b" dependencies = [ "atty", "backtrace", @@ -520,9 +591,9 @@ dependencies = [ [[package]] name = "miette-derive" -version = "5.3.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2485ed7d1fe80704928e3eb86387439609bd0c6bb96db8208daa364cfd1e09" +checksum = "827d18edee5d43dc309eb0ac565f2b8e2fdc89b986b2d929e924a0f6e7f23835" dependencies = [ "proc-macro2", "quote", @@ -743,6 +814,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9b0efd3ba03c3a409d44d60425f279ec442bcf0b9e63ff4e410da31c8b0f69f" +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + [[package]] name = "petgraph" version = "0.6.2" @@ -753,6 +830,12 @@ dependencies = [ "indexmap", ] +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -978,18 +1061,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", @@ -998,9 +1081,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" dependencies = [ "indexmap", "itoa", @@ -1008,6 +1091,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "smawk" version = "0.3.1" @@ -1160,6 +1254,21 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "toml" version = "0.5.9" @@ -1169,12 +1278,50 @@ dependencies = [ "serde", ] +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + [[package]] name = "typed-arena" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + [[package]] name = "unicode-ident" version = "1.0.4" @@ -1191,6 +1338,15 @@ dependencies = [ "regex", ] +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.10.0" @@ -1227,6 +1383,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "vec1" version = "1.8.0" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 509106c8..bd6335f7 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -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" } diff --git a/crates/cli/src/cmd/lsp.rs b/crates/cli/src/cmd/lsp.rs new file mode 100644 index 00000000..3b92eb30 --- /dev/null +++ b/crates/cli/src/cmd/lsp.rs @@ -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() +} diff --git a/crates/cli/src/cmd/mod.rs b/crates/cli/src/cmd/mod.rs index 43c895bc..3c79d162 100644 --- a/crates/cli/src/cmd/mod.rs +++ b/crates/cli/src/cmd/mod.rs @@ -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; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 11bee196..d624ce6d 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -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), } diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml new file mode 100644 index 00000000..8c5082a8 --- /dev/null +++ b/crates/lsp/Cargo.toml @@ -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 "] + + +[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" diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs new file mode 100644 index 00000000..b566375b --- /dev/null +++ b/crates/lsp/src/lib.rs @@ -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), + #[error(transparent)] + #[diagnostic(code(aiken::lsp::send))] + Send(#[from] SendError), +} + +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 { + let id = request.id.clone(); + + match request.method.as_str() { + Formatting::METHOD => { + let params = cast_request::(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(request: lsp_server::Request) -> Result +where + R: lsp_types::request::Request, + R::Params: serde::de::DeserializeOwned, +{ + let (_, params) = request.extract(R::METHOD)?; + + Ok(params) +} + +fn format(params: DocumentFormattingParams) -> Result, 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, + } +}