chore: make folder names match crate name
This commit is contained in:
23
crates/aiken-lsp/Cargo.toml
Normal file
23
crates/aiken-lsp/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "aiken-lsp"
|
||||
version = "0.0.26"
|
||||
edition = "2021"
|
||||
description = "Cardano smart contract language and toolchain"
|
||||
repository = "https://github.com/aiken-lang/aiken"
|
||||
homepage = "https://github.com/aiken-lang/aiken"
|
||||
license = "Apache-2.0"
|
||||
authors = ["Lucas Rosa <x@rvcas.dev>"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
aiken-lang = { path = '../aiken-lang', version = "0.0.26" }
|
||||
aiken-project = { path = '../aiken-project', version = "0.0.26" }
|
||||
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"
|
||||
url = "2.3.1"
|
||||
32
crates/aiken-lsp/src/error.rs
Normal file
32
crates/aiken-lsp/src/error.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::io;
|
||||
|
||||
use crossbeam_channel::SendError;
|
||||
use lsp_server::{ExtractError, Message};
|
||||
|
||||
#[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::cast_notification))]
|
||||
CastNotification(#[from] ExtractError<lsp_server::Notification>),
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(aiken::lsp::send))]
|
||||
Send(#[from] SendError<Message>),
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(aiken::lsp::send))]
|
||||
PathToUri(#[from] url::ParseError),
|
||||
}
|
||||
56
crates/aiken-lsp/src/lib.rs
Normal file
56
crates/aiken-lsp/src/lib.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use lsp_server::Connection;
|
||||
use lsp_types::{
|
||||
OneOf, SaveOptions, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
|
||||
TextDocumentSyncOptions, TextDocumentSyncSaveOptions,
|
||||
};
|
||||
|
||||
pub mod error;
|
||||
mod line_numbers;
|
||||
pub mod server;
|
||||
|
||||
use error::Error;
|
||||
|
||||
use crate::server::Server;
|
||||
|
||||
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(&capabilities())?;
|
||||
|
||||
let initialization_params = connection.initialize(server_capabilities)?;
|
||||
let initialize_params = serde_json::from_value(initialization_params)?;
|
||||
|
||||
let mut server = Server::new(initialize_params, None);
|
||||
|
||||
server.listen(connection)?;
|
||||
|
||||
io_threads.join()?;
|
||||
|
||||
tracing::info!("Aiken language server shutting down");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn capabilities() -> ServerCapabilities {
|
||||
ServerCapabilities {
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||
TextDocumentSyncOptions {
|
||||
open_close: None,
|
||||
change: Some(TextDocumentSyncKind::FULL),
|
||||
will_save: None,
|
||||
will_save_wait_until: None,
|
||||
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
|
||||
include_text: Some(false),
|
||||
})),
|
||||
},
|
||||
)),
|
||||
// definition_provider: Some(OneOf::Left(true)),
|
||||
document_formatting_provider: Some(OneOf::Left(true)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
48
crates/aiken-lsp/src/line_numbers.rs
Normal file
48
crates/aiken-lsp/src/line_numbers.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct LineNumbers {
|
||||
line_starts: Vec<usize>,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl LineNumbers {
|
||||
pub fn new(src: &str) -> Self {
|
||||
Self {
|
||||
length: src.len(),
|
||||
line_starts: std::iter::once(0)
|
||||
.chain(src.match_indices('\n').map(|(i, _)| i + 1))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the line number for a byte index
|
||||
pub fn line_number(&self, byte_index: usize) -> usize {
|
||||
self.line_starts
|
||||
.binary_search(&byte_index)
|
||||
.unwrap_or_else(|next_line| next_line - 1)
|
||||
+ 1
|
||||
}
|
||||
|
||||
// TODO: handle unicode characters that may be more than 1 byte in width
|
||||
pub fn line_and_column_number(&self, byte_index: usize) -> LineColumn {
|
||||
let line = self.line_number(byte_index);
|
||||
let column = byte_index - self.line_starts.get(line - 1).copied().unwrap_or_default() + 1;
|
||||
LineColumn { line, column }
|
||||
}
|
||||
|
||||
// TODO: handle unicode characters that may be more than 1 byte in width
|
||||
/// 0 indexed line and character to byte index
|
||||
#[allow(dead_code)]
|
||||
pub fn byte_index(&self, line: usize, character: usize) -> usize {
|
||||
match self.line_starts.get(line) {
|
||||
Some(line_index) => *line_index + character,
|
||||
None => self.length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LineColumn {
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
424
crates/aiken-lsp/src/server.rs
Normal file
424
crates/aiken-lsp/src/server.rs
Normal file
@@ -0,0 +1,424 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use aiken_lang::{ast::ModuleKind, parser};
|
||||
use aiken_project::{config, error::Error as ProjectError};
|
||||
use lsp_server::{Connection, Message};
|
||||
use lsp_types::{
|
||||
notification::{
|
||||
DidChangeTextDocument, DidSaveTextDocument, Notification, PublishDiagnostics, ShowMessage,
|
||||
},
|
||||
request::{Formatting, Request},
|
||||
DocumentFormattingParams, InitializeParams, TextEdit,
|
||||
};
|
||||
use miette::Diagnostic;
|
||||
|
||||
use crate::{error::Error as ServerError, line_numbers::LineNumbers};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Server {
|
||||
config: Option<config::Config>,
|
||||
|
||||
/// Files that have been edited in memory
|
||||
edited: HashMap<String, String>,
|
||||
|
||||
initialize_params: InitializeParams,
|
||||
|
||||
/// Files for which there are active diagnostics
|
||||
published_diagnostics: HashSet<lsp_types::Url>,
|
||||
|
||||
/// Diagnostics that have been emitted by the compiler but not yet published
|
||||
/// to the client
|
||||
stored_diagnostics: HashMap<PathBuf, Vec<lsp_types::Diagnostic>>,
|
||||
|
||||
/// Diagnostics that have been emitted by the compiler but not yet published
|
||||
/// to the client. These are likely locationless Aiken diagnostics, as LSP
|
||||
/// diagnostics always need a location.
|
||||
stored_messages: Vec<lsp_types::ShowMessageParams>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(initialize_params: InitializeParams, config: Option<config::Config>) -> Self {
|
||||
Self {
|
||||
config,
|
||||
edited: HashMap::new(),
|
||||
initialize_params,
|
||||
published_diagnostics: HashSet::new(),
|
||||
stored_diagnostics: HashMap::new(),
|
||||
stored_messages: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen(&mut self, connection: Connection) -> Result<(), ServerError> {
|
||||
self.publish_stored_diagnostics(&connection)?;
|
||||
|
||||
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 = self.handle_request(req)?;
|
||||
|
||||
self.publish_stored_diagnostics(&connection)?;
|
||||
|
||||
connection.sender.send(Message::Response(response))?;
|
||||
}
|
||||
Message::Response(_) => todo!(),
|
||||
Message::Notification(notification) => {
|
||||
self.handle_notification(&connection, notification)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
&mut self,
|
||||
request: lsp_server::Request,
|
||||
) -> Result<lsp_server::Response, ServerError> {
|
||||
let id = request.id.clone();
|
||||
|
||||
match request.method.as_str() {
|
||||
Formatting::METHOD => {
|
||||
let params = cast_request::<Formatting>(request)?;
|
||||
|
||||
let result = self.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(err) => match err {
|
||||
ProjectError::List(errors) => {
|
||||
for error in errors {
|
||||
if error.source_code().is_some() {
|
||||
self.process_diagnostic(error)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(lsp_server::Response {
|
||||
id,
|
||||
error: None,
|
||||
result: Some(serde_json::json!(null)),
|
||||
})
|
||||
}
|
||||
error => {
|
||||
if error.source_code().is_some() {
|
||||
self.process_diagnostic(error)?;
|
||||
|
||||
Ok(lsp_server::Response {
|
||||
id,
|
||||
error: None,
|
||||
result: Some(serde_json::json!(null)),
|
||||
})
|
||||
} else {
|
||||
Ok(lsp_server::Response {
|
||||
id,
|
||||
error: Some(lsp_server::ResponseError {
|
||||
code: 1, // We should assign a code to each error.
|
||||
message: format!("{:?}", error),
|
||||
data: None,
|
||||
}),
|
||||
result: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
unsupported => Err(ServerError::UnsupportedLspRequest {
|
||||
request: unsupported.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_notification(
|
||||
&mut self,
|
||||
_connection: &lsp_server::Connection,
|
||||
notification: lsp_server::Notification,
|
||||
) -> Result<(), ServerError> {
|
||||
match notification.method.as_str() {
|
||||
DidSaveTextDocument::METHOD => {
|
||||
let params = cast_notification::<DidSaveTextDocument>(notification)?;
|
||||
|
||||
self.edited.remove(params.text_document.uri.path());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
DidChangeTextDocument::METHOD => {
|
||||
let params = cast_notification::<DidChangeTextDocument>(notification)?;
|
||||
|
||||
// A file has changed in the editor so store a copy of the new content in memory
|
||||
let path = params.text_document.uri.path().to_string();
|
||||
|
||||
if let Some(changes) = params.content_changes.into_iter().next() {
|
||||
self.edited.insert(path, changes.text);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn format(&mut self, params: DocumentFormattingParams) -> Result<Vec<TextEdit>, ProjectError> {
|
||||
let path = params.text_document.uri.path();
|
||||
let mut new_text = String::new();
|
||||
|
||||
match self.edited.get(path) {
|
||||
Some(src) => {
|
||||
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);
|
||||
}
|
||||
None => {
|
||||
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)])
|
||||
}
|
||||
|
||||
/// Publish all stored diagnostics to the client.
|
||||
/// Any previously publish diagnostics are cleared before the new set are
|
||||
/// published to the client.
|
||||
fn publish_stored_diagnostics(&mut self, connection: &Connection) -> Result<(), ServerError> {
|
||||
self.clear_all_diagnostics(connection)?;
|
||||
|
||||
for (path, diagnostics) in self.stored_diagnostics.drain() {
|
||||
let uri = path_to_uri(path)?;
|
||||
|
||||
// Record that we have published diagnostics to this file so we can
|
||||
// clear it later when they are outdated.
|
||||
self.published_diagnostics.insert(uri.clone());
|
||||
|
||||
// Publish the diagnostics
|
||||
let params = lsp_types::PublishDiagnosticsParams {
|
||||
uri,
|
||||
diagnostics,
|
||||
version: None,
|
||||
};
|
||||
|
||||
let notification = lsp_server::Notification {
|
||||
method: PublishDiagnostics::METHOD.to_string(),
|
||||
params: serde_json::to_value(params)?,
|
||||
};
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send(lsp_server::Message::Notification(notification))?;
|
||||
}
|
||||
|
||||
for message in self.stored_messages.drain(..) {
|
||||
let notification = lsp_server::Notification {
|
||||
method: ShowMessage::METHOD.to_string(),
|
||||
params: serde_json::to_value(message)?,
|
||||
};
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send(lsp_server::Message::Notification(notification))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear all diagnostics that have been previously published to the client
|
||||
fn clear_all_diagnostics(&mut self, connection: &Connection) -> Result<(), ServerError> {
|
||||
for file in self.published_diagnostics.drain() {
|
||||
let params = lsp_types::PublishDiagnosticsParams {
|
||||
uri: file,
|
||||
diagnostics: vec![],
|
||||
version: None,
|
||||
};
|
||||
|
||||
let notification = lsp_server::Notification {
|
||||
method: PublishDiagnostics::METHOD.to_string(),
|
||||
params: serde_json::to_value(params)?,
|
||||
};
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send(lsp_server::Message::Notification(notification))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert Aiken diagnostics into 1 or more LSP diagnostics and store them
|
||||
/// so that they can later be published to the client with
|
||||
/// `publish_stored_diagnostics`
|
||||
///
|
||||
/// If the Aiken diagnostic cannot be converted to LSP diagnostic (due to it
|
||||
/// not having a location) it is stored as a message suitable for use with
|
||||
/// the `showMessage` notification instead.
|
||||
fn process_diagnostic(&mut self, error: ProjectError) -> Result<(), ServerError> {
|
||||
let (severity, typ) = match error.severity() {
|
||||
Some(severity) => match severity {
|
||||
miette::Severity::Error => (
|
||||
lsp_types::DiagnosticSeverity::ERROR,
|
||||
lsp_types::MessageType::ERROR,
|
||||
),
|
||||
miette::Severity::Warning => (
|
||||
lsp_types::DiagnosticSeverity::WARNING,
|
||||
lsp_types::MessageType::WARNING,
|
||||
),
|
||||
miette::Severity::Advice => (
|
||||
lsp_types::DiagnosticSeverity::HINT,
|
||||
lsp_types::MessageType::INFO,
|
||||
),
|
||||
},
|
||||
None => (
|
||||
lsp_types::DiagnosticSeverity::ERROR,
|
||||
lsp_types::MessageType::ERROR,
|
||||
),
|
||||
};
|
||||
|
||||
let mut text = match error.source() {
|
||||
Some(err) => err.to_string(),
|
||||
None => error.to_string(),
|
||||
};
|
||||
|
||||
if let (Some(mut labels), Some(path), Some(src)) =
|
||||
(error.labels(), error.path(), error.src())
|
||||
{
|
||||
if let Some(labeled_span) = labels.next() {
|
||||
if let Some(label) = labeled_span.label() {
|
||||
text.push_str("\n\n");
|
||||
text.push_str(label);
|
||||
|
||||
if !label.ends_with(['.', '?']) {
|
||||
text.push('.');
|
||||
}
|
||||
}
|
||||
|
||||
let line_numbers = LineNumbers::new(&src);
|
||||
|
||||
let start = line_numbers.line_and_column_number(labeled_span.inner().offset());
|
||||
let end = line_numbers.line_and_column_number(
|
||||
labeled_span.inner().offset() + labeled_span.inner().len(),
|
||||
);
|
||||
|
||||
let lsp_diagnostic = lsp_types::Diagnostic {
|
||||
range: lsp_types::Range::new(
|
||||
lsp_types::Position {
|
||||
line: start.line as u32 - 1,
|
||||
character: start.column as u32 - 1,
|
||||
},
|
||||
lsp_types::Position {
|
||||
line: end.line as u32 - 1,
|
||||
character: end.column as u32 - 1,
|
||||
},
|
||||
),
|
||||
severity: Some(severity),
|
||||
code: error
|
||||
.code()
|
||||
.map(|c| lsp_types::NumberOrString::String(c.to_string())),
|
||||
code_description: None,
|
||||
source: None,
|
||||
message: text.clone(),
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
};
|
||||
|
||||
let path = path.canonicalize()?;
|
||||
|
||||
self.push_diagnostic(path.clone(), lsp_diagnostic.clone());
|
||||
|
||||
if let Some(hint) = error.help() {
|
||||
let lsp_hint = lsp_types::Diagnostic {
|
||||
severity: Some(lsp_types::DiagnosticSeverity::HINT),
|
||||
message: hint.to_string(),
|
||||
..lsp_diagnostic
|
||||
};
|
||||
|
||||
self.push_diagnostic(path, lsp_hint);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.stored_messages
|
||||
.push(lsp_types::ShowMessageParams { typ, message: text })
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_diagnostic(&mut self, path: PathBuf, diagnostic: lsp_types::Diagnostic) {
|
||||
self.stored_diagnostics
|
||||
.entry(path)
|
||||
.or_default()
|
||||
.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
fn cast_request<R>(request: lsp_server::Request) -> Result<R::Params, ServerError>
|
||||
where
|
||||
R: lsp_types::request::Request,
|
||||
R::Params: serde::de::DeserializeOwned,
|
||||
{
|
||||
let (_, params) = request.extract(R::METHOD)?;
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn cast_notification<N>(notification: lsp_server::Notification) -> Result<N::Params, ServerError>
|
||||
where
|
||||
N: lsp_types::notification::Notification,
|
||||
N::Params: serde::de::DeserializeOwned,
|
||||
{
|
||||
let params = notification.extract::<N::Params>(N::METHOD)?;
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_uri(path: PathBuf) -> Result<lsp_types::Url, ServerError> {
|
||||
let mut file: String = "file://".into();
|
||||
|
||||
file.push_str(&path.as_os_str().to_string_lossy());
|
||||
|
||||
let uri = lsp_types::Url::parse(&file)?;
|
||||
|
||||
Ok(uri)
|
||||
}
|
||||
Reference in New Issue
Block a user