feat(lsp): enable compiling a project
This commit is contained in:
parent
b55726c90f
commit
38bcbaf701
|
@ -0,0 +1,21 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
pub 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cast_notification<N>(notification: lsp_server::Notification) -> Result<N::Params, Error>
|
||||||
|
where
|
||||||
|
N: lsp_types::notification::Notification,
|
||||||
|
N::Params: serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
let params = notification.extract::<N::Params>(N::METHOD)?;
|
||||||
|
|
||||||
|
Ok(params)
|
||||||
|
}
|
|
@ -1,12 +1,17 @@
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use aiken_project::{config::Config, paths};
|
||||||
use lsp_server::Connection;
|
use lsp_server::Connection;
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
OneOf, SaveOptions, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
|
OneOf, SaveOptions, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
|
||||||
TextDocumentSyncOptions, TextDocumentSyncSaveOptions,
|
TextDocumentSyncOptions, TextDocumentSyncSaveOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod cast;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod line_numbers;
|
mod line_numbers;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
use error::Error;
|
use error::Error;
|
||||||
|
|
||||||
|
@ -15,6 +20,18 @@ use crate::server::Server;
|
||||||
pub fn start() -> Result<(), Error> {
|
pub fn start() -> Result<(), Error> {
|
||||||
tracing::info!("Aiken language server starting");
|
tracing::info!("Aiken language server starting");
|
||||||
|
|
||||||
|
let root = env::current_dir()?;
|
||||||
|
|
||||||
|
let config = if paths::project_config().exists() {
|
||||||
|
tracing::info!("Aiken project detected");
|
||||||
|
|
||||||
|
Some(Config::load(&root).expect("failed to load aiken.toml"))
|
||||||
|
} else {
|
||||||
|
tracing::info!("Aiken project config not found");
|
||||||
|
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Create the transport. Includes the stdio (stdin and stdout) versions but this could
|
// Create the transport. Includes the stdio (stdin and stdout) versions but this could
|
||||||
// also be implemented to use sockets or HTTP.
|
// also be implemented to use sockets or HTTP.
|
||||||
let (connection, io_threads) = Connection::stdio();
|
let (connection, io_threads) = Connection::stdio();
|
||||||
|
@ -25,7 +42,7 @@ pub fn start() -> Result<(), Error> {
|
||||||
let initialization_params = connection.initialize(server_capabilities)?;
|
let initialization_params = connection.initialize(server_capabilities)?;
|
||||||
let initialize_params = serde_json::from_value(initialization_params)?;
|
let initialize_params = serde_json::from_value(initialization_params)?;
|
||||||
|
|
||||||
let mut server = Server::new(initialize_params, None);
|
let mut server = Server::new(initialize_params, config, root);
|
||||||
|
|
||||||
server.listen(connection)?;
|
server.listen(connection)?;
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,44 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
error::Error,
|
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use aiken_lang::{ast::ModuleKind, parser};
|
use aiken_lang::{ast::ModuleKind, parser};
|
||||||
use aiken_project::{config, error::Error as ProjectError};
|
use aiken_project::{
|
||||||
|
config,
|
||||||
|
error::{Error as ProjectError, GetSource},
|
||||||
|
};
|
||||||
use lsp_server::{Connection, Message};
|
use lsp_server::{Connection, Message};
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
notification::{
|
notification::{
|
||||||
DidChangeTextDocument, DidSaveTextDocument, Notification, PublishDiagnostics, ShowMessage,
|
DidChangeTextDocument, DidSaveTextDocument, Notification, Progress, PublishDiagnostics,
|
||||||
|
ShowMessage,
|
||||||
},
|
},
|
||||||
request::{Formatting, Request},
|
request::{Formatting, Request, WorkDoneProgressCreate},
|
||||||
DocumentFormattingParams, InitializeParams, TextEdit,
|
DocumentFormattingParams, InitializeParams, TextEdit,
|
||||||
};
|
};
|
||||||
use miette::Diagnostic;
|
use miette::Diagnostic;
|
||||||
|
|
||||||
use crate::{error::Error as ServerError, line_numbers::LineNumbers};
|
use crate::{
|
||||||
|
cast::{cast_notification, cast_request},
|
||||||
|
error::Error as ServerError,
|
||||||
|
line_numbers::LineNumbers,
|
||||||
|
utils::{
|
||||||
|
path_to_uri, text_edit_replace, COMPILING_PROGRESS_TOKEN, CREATE_COMPILING_PROGRESS_TOKEN,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::lsp_project::LspProject;
|
||||||
|
|
||||||
|
mod lsp_project;
|
||||||
|
pub mod telemetry;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
|
// Project root directory
|
||||||
|
root: PathBuf,
|
||||||
|
|
||||||
config: Option<config::Config>,
|
config: Option<config::Config>,
|
||||||
|
|
||||||
/// Files that have been edited in memory
|
/// Files that have been edited in memory
|
||||||
|
@ -39,114 +57,114 @@ pub struct Server {
|
||||||
/// to the client. These are likely locationless Aiken diagnostics, as LSP
|
/// to the client. These are likely locationless Aiken diagnostics, as LSP
|
||||||
/// diagnostics always need a location.
|
/// diagnostics always need a location.
|
||||||
stored_messages: Vec<lsp_types::ShowMessageParams>,
|
stored_messages: Vec<lsp_types::ShowMessageParams>,
|
||||||
|
|
||||||
|
/// An instance of a LspProject
|
||||||
|
compiler: Option<LspProject>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
pub fn new(initialize_params: InitializeParams, config: Option<config::Config>) -> Self {
|
/// Clear all diagnostics that have been previously published to the client
|
||||||
Self {
|
fn clear_all_diagnostics(&mut self, connection: &Connection) -> Result<(), ServerError> {
|
||||||
config,
|
for file in self.published_diagnostics.drain() {
|
||||||
edited: HashMap::new(),
|
let params = lsp_types::PublishDiagnosticsParams {
|
||||||
initialize_params,
|
uri: file,
|
||||||
published_diagnostics: HashSet::new(),
|
diagnostics: vec![],
|
||||||
stored_diagnostics: HashMap::new(),
|
version: None,
|
||||||
stored_messages: Vec::new(),
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn listen(&mut self, connection: Connection) -> Result<(), ServerError> {
|
let notification = lsp_server::Notification {
|
||||||
self.publish_stored_diagnostics(&connection)?;
|
method: PublishDiagnostics::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(params)?,
|
||||||
|
};
|
||||||
|
|
||||||
for msg in &connection.receiver {
|
connection
|
||||||
tracing::debug!("Got message: {:#?}", msg);
|
.sender
|
||||||
|
.send(lsp_server::Message::Notification(notification))?;
|
||||||
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_request(
|
/// Compile the project if we are in one. Otherwise do nothing.
|
||||||
&mut self,
|
fn compile(&mut self, connection: &Connection) -> Result<(), ServerError> {
|
||||||
request: lsp_server::Request,
|
self.notify_client_of_compilation_start(connection)?;
|
||||||
) -> Result<lsp_server::Response, ServerError> {
|
|
||||||
let id = request.id.clone();
|
|
||||||
|
|
||||||
match request.method.as_str() {
|
eprintln!("{:?}", self.compiler.is_some());
|
||||||
Formatting::METHOD => {
|
if let Some(compiler) = self.compiler.as_mut() {
|
||||||
let params = cast_request::<Formatting>(request)?;
|
let result = compiler.compile();
|
||||||
|
|
||||||
let result = self.format(params);
|
for warning in compiler.project.warnings() {
|
||||||
|
self.process_diagnostic(warning)?;
|
||||||
|
}
|
||||||
|
|
||||||
match result {
|
if let Err(errs) = result {
|
||||||
Ok(text_edit) => {
|
for err in errs {
|
||||||
let result = serde_json::to_value(text_edit)?;
|
self.process_diagnostic(err)?;
|
||||||
|
|
||||||
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(),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.notify_client_of_compilation_end(connection)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_compilation_progress_token(
|
||||||
|
&mut self,
|
||||||
|
connection: &lsp_server::Connection,
|
||||||
|
) -> Result<(), ServerError> {
|
||||||
|
let params = lsp_types::WorkDoneProgressCreateParams {
|
||||||
|
token: lsp_types::NumberOrString::String(COMPILING_PROGRESS_TOKEN.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = lsp_server::Request {
|
||||||
|
id: CREATE_COMPILING_PROGRESS_TOKEN.to_string().into(),
|
||||||
|
method: WorkDoneProgressCreate::METHOD.into(),
|
||||||
|
params: serde_json::to_value(¶ms)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
connection
|
||||||
|
.sender
|
||||||
|
.send(lsp_server::Message::Request(request))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_new_compiler(&mut self) {
|
||||||
|
if let Some(config) = self.config.as_ref() {
|
||||||
|
let compiler = LspProject::new(config.clone(), self.root.clone(), telemetry::Lsp);
|
||||||
|
|
||||||
|
self.compiler = Some(compiler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format(
|
||||||
|
&mut self,
|
||||||
|
params: DocumentFormattingParams,
|
||||||
|
) -> Result<Vec<TextEdit>, Vec<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).map_err(ProjectError::from)?;
|
||||||
|
|
||||||
|
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 handle_notification(
|
fn handle_notification(
|
||||||
|
@ -178,96 +196,123 @@ impl Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format(&mut self, params: DocumentFormattingParams) -> Result<Vec<TextEdit>, ProjectError> {
|
fn handle_request(
|
||||||
let path = params.text_document.uri.path();
|
&mut self,
|
||||||
let mut new_text = String::new();
|
request: lsp_server::Request,
|
||||||
|
) -> Result<lsp_server::Response, ServerError> {
|
||||||
|
let id = request.id.clone();
|
||||||
|
|
||||||
match self.edited.get(path) {
|
match request.method.as_str() {
|
||||||
Some(src) => {
|
Formatting::METHOD => {
|
||||||
let (module, extra) = parser::module(src, ModuleKind::Lib).map_err(|errs| {
|
let params = cast_request::<Formatting>(request)?;
|
||||||
aiken_project::error::Error::from_parse_errors(errs, Path::new(path), src)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
aiken_lang::format::pretty(&mut new_text, module, extra, src);
|
let result = self.format(params);
|
||||||
}
|
|
||||||
None => {
|
match result {
|
||||||
let src = fs::read_to_string(path)?;
|
Ok(text_edit) => {
|
||||||
|
let result = serde_json::to_value(text_edit)?;
|
||||||
let (module, extra) = parser::module(&src, ModuleKind::Lib).map_err(|errs| {
|
|
||||||
aiken_project::error::Error::from_parse_errors(errs, Path::new(path), &src)
|
Ok(lsp_server::Response {
|
||||||
})?;
|
id,
|
||||||
|
error: None,
|
||||||
aiken_lang::format::pretty(&mut new_text, module, extra, &src);
|
result: Some(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(errors) => {
|
||||||
|
for error in errors {
|
||||||
|
self.process_diagnostic(error)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(lsp_server::Response {
|
||||||
|
id,
|
||||||
|
error: None,
|
||||||
|
result: Some(serde_json::json!(null)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
unsupported => Err(ServerError::UnsupportedLspRequest {
|
||||||
|
request: unsupported.to_string(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(vec![text_edit_replace(new_text)])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish all stored diagnostics to the client.
|
pub fn listen(&mut self, connection: Connection) -> Result<(), ServerError> {
|
||||||
/// Any previously publish diagnostics are cleared before the new set are
|
self.create_compilation_progress_token(&connection)?;
|
||||||
/// published to the client.
|
self.start_watching_aiken_toml(&connection)?;
|
||||||
fn publish_stored_diagnostics(&mut self, connection: &Connection) -> Result<(), ServerError> {
|
|
||||||
self.clear_all_diagnostics(connection)?;
|
|
||||||
|
|
||||||
for (path, diagnostics) in self.stored_diagnostics.drain() {
|
// Compile the project once so we have all the state and any initial errors
|
||||||
let uri = path_to_uri(path)?;
|
self.compile(&connection)?;
|
||||||
|
self.publish_stored_diagnostics(&connection)?;
|
||||||
|
|
||||||
// Record that we have published diagnostics to this file so we can
|
for msg in &connection.receiver {
|
||||||
// clear it later when they are outdated.
|
tracing::debug!("Got message: {:#?}", msg);
|
||||||
self.published_diagnostics.insert(uri.clone());
|
|
||||||
|
|
||||||
// Publish the diagnostics
|
match msg {
|
||||||
let params = lsp_types::PublishDiagnosticsParams {
|
Message::Request(req) => {
|
||||||
uri,
|
if connection.handle_shutdown(&req)? {
|
||||||
diagnostics,
|
return Ok(());
|
||||||
version: None,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let notification = lsp_server::Notification {
|
tracing::debug!("Get request: {:#?}", req);
|
||||||
method: PublishDiagnostics::METHOD.to_string(),
|
|
||||||
params: serde_json::to_value(params)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
connection
|
let response = self.handle_request(req)?;
|
||||||
.sender
|
|
||||||
.send(lsp_server::Message::Notification(notification))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for message in self.stored_messages.drain(..) {
|
self.publish_stored_diagnostics(&connection)?;
|
||||||
let notification = lsp_server::Notification {
|
|
||||||
method: ShowMessage::METHOD.to_string(),
|
|
||||||
params: serde_json::to_value(message)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
connection
|
connection.sender.send(Message::Response(response))?;
|
||||||
.sender
|
}
|
||||||
.send(lsp_server::Message::Notification(notification))?;
|
Message::Response(_) => (),
|
||||||
|
Message::Notification(notification) => {
|
||||||
|
self.handle_notification(&connection, notification)?
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all diagnostics that have been previously published to the client
|
pub fn new(
|
||||||
fn clear_all_diagnostics(&mut self, connection: &Connection) -> Result<(), ServerError> {
|
initialize_params: InitializeParams,
|
||||||
for file in self.published_diagnostics.drain() {
|
config: Option<config::Config>,
|
||||||
let params = lsp_types::PublishDiagnosticsParams {
|
root: PathBuf,
|
||||||
uri: file,
|
) -> Self {
|
||||||
diagnostics: vec![],
|
let mut server = Server {
|
||||||
version: None,
|
root,
|
||||||
};
|
config,
|
||||||
|
edited: HashMap::new(),
|
||||||
|
initialize_params,
|
||||||
|
published_diagnostics: HashSet::new(),
|
||||||
|
stored_diagnostics: HashMap::new(),
|
||||||
|
stored_messages: Vec::new(),
|
||||||
|
compiler: None,
|
||||||
|
};
|
||||||
|
|
||||||
let notification = lsp_server::Notification {
|
server.create_new_compiler();
|
||||||
method: PublishDiagnostics::METHOD.to_string(),
|
|
||||||
params: serde_json::to_value(params)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
connection
|
server
|
||||||
.sender
|
}
|
||||||
.send(lsp_server::Message::Notification(notification))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
fn notify_client_of_compilation_end(&self, connection: &Connection) -> Result<(), ServerError> {
|
||||||
|
self.send_work_done_notification(
|
||||||
|
connection,
|
||||||
|
lsp_types::WorkDoneProgress::End(lsp_types::WorkDoneProgressEnd { message: None }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_client_of_compilation_start(
|
||||||
|
&self,
|
||||||
|
connection: &Connection,
|
||||||
|
) -> Result<(), ServerError> {
|
||||||
|
self.send_work_done_notification(
|
||||||
|
connection,
|
||||||
|
lsp_types::WorkDoneProgress::Begin(lsp_types::WorkDoneProgressBegin {
|
||||||
|
title: "Compiling Aiken".into(),
|
||||||
|
cancellable: Some(false),
|
||||||
|
message: None,
|
||||||
|
percentage: None,
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert Aiken diagnostics into 1 or more LSP diagnostics and store them
|
/// Convert Aiken diagnostics into 1 or more LSP diagnostics and store them
|
||||||
|
@ -277,7 +322,10 @@ impl Server {
|
||||||
/// If the Aiken diagnostic cannot be converted to LSP diagnostic (due to it
|
/// 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
|
/// not having a location) it is stored as a message suitable for use with
|
||||||
/// the `showMessage` notification instead.
|
/// the `showMessage` notification instead.
|
||||||
fn process_diagnostic(&mut self, error: ProjectError) -> Result<(), ServerError> {
|
fn process_diagnostic<E>(&mut self, error: E) -> Result<(), ServerError>
|
||||||
|
where
|
||||||
|
E: Diagnostic + GetSource,
|
||||||
|
{
|
||||||
let (severity, typ) = match error.severity() {
|
let (severity, typ) = match error.severity() {
|
||||||
Some(severity) => match severity {
|
Some(severity) => match severity {
|
||||||
miette::Severity::Error => (
|
miette::Severity::Error => (
|
||||||
|
@ -369,56 +417,126 @@ impl Server {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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(())
|
||||||
|
}
|
||||||
|
|
||||||
fn push_diagnostic(&mut self, path: PathBuf, diagnostic: lsp_types::Diagnostic) {
|
fn push_diagnostic(&mut self, path: PathBuf, diagnostic: lsp_types::Diagnostic) {
|
||||||
self.stored_diagnostics
|
self.stored_diagnostics
|
||||||
.entry(path)
|
.entry(path)
|
||||||
.or_default()
|
.or_default()
|
||||||
.push(diagnostic);
|
.push(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn cast_request<R>(request: lsp_server::Request) -> Result<R::Params, ServerError>
|
fn send_work_done_notification(
|
||||||
where
|
&self,
|
||||||
R: lsp_types::request::Request,
|
connection: &Connection,
|
||||||
R::Params: serde::de::DeserializeOwned,
|
work_done: lsp_types::WorkDoneProgress,
|
||||||
{
|
) -> Result<(), ServerError> {
|
||||||
let (_, params) = request.extract(R::METHOD)?;
|
tracing::info!("sending {:?}", work_done);
|
||||||
|
|
||||||
Ok(params)
|
let params = lsp_types::ProgressParams {
|
||||||
}
|
token: lsp_types::NumberOrString::String(COMPILING_PROGRESS_TOKEN.to_string()),
|
||||||
|
value: lsp_types::ProgressParamsValue::WorkDone(work_done),
|
||||||
|
};
|
||||||
|
|
||||||
fn cast_notification<N>(notification: lsp_server::Notification) -> Result<N::Params, ServerError>
|
let notification = lsp_server::Notification {
|
||||||
where
|
method: Progress::METHOD.into(),
|
||||||
N: lsp_types::notification::Notification,
|
params: serde_json::to_value(¶ms)?,
|
||||||
N::Params: serde::de::DeserializeOwned,
|
};
|
||||||
{
|
|
||||||
let params = notification.extract::<N::Params>(N::METHOD)?;
|
|
||||||
|
|
||||||
Ok(params)
|
connection
|
||||||
}
|
.sender
|
||||||
|
.send(lsp_server::Message::Notification(notification))?;
|
||||||
|
|
||||||
fn text_edit_replace(new_text: String) -> TextEdit {
|
Ok(())
|
||||||
TextEdit {
|
}
|
||||||
range: lsp_types::Range {
|
|
||||||
start: lsp_types::Position {
|
fn start_watching_aiken_toml(&mut self, connection: &Connection) -> Result<(), ServerError> {
|
||||||
line: 0,
|
let supports_watch_files = self
|
||||||
character: 0,
|
.initialize_params
|
||||||
},
|
.capabilities
|
||||||
end: lsp_types::Position {
|
.workspace
|
||||||
line: u32::MAX,
|
.as_ref()
|
||||||
character: 0,
|
.and_then(|w| w.did_change_watched_files)
|
||||||
},
|
.map(|wf| wf.dynamic_registration == Some(true))
|
||||||
},
|
.unwrap_or(false);
|
||||||
new_text,
|
|
||||||
|
if !supports_watch_files {
|
||||||
|
tracing::warn!("lsp_client_cannot_watch_gleam_toml");
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register gleam.toml as a watched file so we get a notification when
|
||||||
|
// it changes and thus know that we need to rebuild the entire project.
|
||||||
|
let register_options =
|
||||||
|
serde_json::value::to_value(lsp_types::DidChangeWatchedFilesRegistrationOptions {
|
||||||
|
watchers: vec![lsp_types::FileSystemWatcher {
|
||||||
|
glob_pattern: "aiken.toml".into(),
|
||||||
|
kind: Some(lsp_types::WatchKind::Change),
|
||||||
|
}],
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let watch_config = lsp_types::Registration {
|
||||||
|
id: "watch-aiken-toml".into(),
|
||||||
|
method: "workspace/didChangeWatchedFiles".into(),
|
||||||
|
register_options: Some(register_options),
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = lsp_server::Request {
|
||||||
|
id: 1.into(),
|
||||||
|
method: "client/registerCapability".into(),
|
||||||
|
params: serde_json::value::to_value(lsp_types::RegistrationParams {
|
||||||
|
registrations: vec![watch_config],
|
||||||
|
})
|
||||||
|
.expect("client/registerCapability to json"),
|
||||||
|
};
|
||||||
|
|
||||||
|
connection
|
||||||
|
.sender
|
||||||
|
.send(lsp_server::Message::Request(request))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
use aiken_lang::ast::Tracing;
|
||||||
|
use aiken_project::{config::Config, error::Error as ProjectError, module::CheckedModule, Project};
|
||||||
|
|
||||||
|
use crate::line_numbers::LineNumbers;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SourceInfo {
|
||||||
|
/// The path to the source file from within the project root
|
||||||
|
pub path: String,
|
||||||
|
/// Useful for converting from Aiken's byte index offsets to the LSP line
|
||||||
|
/// and column number positions.
|
||||||
|
pub line_numbers: LineNumbers,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LspProject {
|
||||||
|
pub project: Project<super::telemetry::Lsp>,
|
||||||
|
pub modules: HashMap<String, CheckedModule>,
|
||||||
|
pub sources: HashMap<String, SourceInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LspProject {
|
||||||
|
pub fn new(config: Config, root: PathBuf, telemetry: super::telemetry::Lsp) -> Self {
|
||||||
|
Self {
|
||||||
|
project: Project::new_with_config(config, root, telemetry),
|
||||||
|
modules: HashMap::new(),
|
||||||
|
sources: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compile(&mut self) -> Result<(), Vec<ProjectError>> {
|
||||||
|
let checkpoint = self.project.checkpoint();
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.project
|
||||||
|
.check(true, None, false, false, Tracing::NoTraces);
|
||||||
|
|
||||||
|
self.project.restore(checkpoint);
|
||||||
|
|
||||||
|
result?;
|
||||||
|
|
||||||
|
let modules = self.project.modules();
|
||||||
|
|
||||||
|
for module in modules.into_iter() {
|
||||||
|
let path = module
|
||||||
|
.input_path
|
||||||
|
.canonicalize()
|
||||||
|
.expect("Canonicalize")
|
||||||
|
.as_os_str()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let line_numbers = LineNumbers::new(&module.code);
|
||||||
|
|
||||||
|
let source = SourceInfo { path, line_numbers };
|
||||||
|
|
||||||
|
self.sources.insert(module.name.to_string(), source);
|
||||||
|
self.modules.insert(module.name.to_string(), module);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
use aiken_project::telemetry::EventListener;
|
||||||
|
|
||||||
|
pub struct Lsp;
|
||||||
|
|
||||||
|
impl EventListener for Lsp {}
|
||||||
|
|
||||||
|
impl Lsp {}
|
|
@ -0,0 +1,34 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use lsp_types::TextEdit;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
pub const COMPILING_PROGRESS_TOKEN: &str = "compiling-gleam";
|
||||||
|
pub const CREATE_COMPILING_PROGRESS_TOKEN: &str = "create-compiling-progress-token";
|
||||||
|
|
||||||
|
pub 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_to_uri(path: PathBuf) -> Result<lsp_types::Url, Error> {
|
||||||
|
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)
|
||||||
|
}
|
|
@ -151,37 +151,42 @@ impl Error {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Error::List(errors)
|
errors
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn append(self, next: Self) -> Self {
|
impl Debug for Error {
|
||||||
match (self, next) {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
(Error::List(mut errors), Error::List(mut next_errors)) => {
|
let miette_handler = MietteHandlerOpts::new()
|
||||||
errors.append(&mut next_errors);
|
// For better support of terminal themes use the ANSI coloring
|
||||||
|
.rgb_colors(RgbColors::Never)
|
||||||
|
// If ansi support is disabled in the config disable the eye-candy
|
||||||
|
.color(true)
|
||||||
|
.unicode(true)
|
||||||
|
.terminal_links(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
Error::List(errors)
|
// Ignore error to prevent format! panics. This can happen if span points at some
|
||||||
}
|
// inaccessible location, for example by calling `report_error()` with wrong working set.
|
||||||
(Error::List(mut errors), rest) => {
|
let _ = miette_handler.debug(self, f);
|
||||||
errors.push(rest);
|
|
||||||
|
|
||||||
Error::List(errors)
|
Ok(())
|
||||||
}
|
|
||||||
(rest, Error::List(mut next_errors)) => {
|
|
||||||
let mut errors = vec![rest];
|
|
||||||
|
|
||||||
errors.append(&mut next_errors);
|
|
||||||
|
|
||||||
Error::List(errors)
|
|
||||||
}
|
|
||||||
(error, next_error) => Error::List(vec![error, next_error]),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
impl From<Error> for Vec<Error> {
|
||||||
matches!(self, Error::List(errors) if errors.is_empty())
|
fn from(value: Error) -> Self {
|
||||||
|
vec![value]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> Option<PathBuf> {
|
pub trait GetSource {
|
||||||
|
fn path(&self) -> Option<PathBuf>;
|
||||||
|
fn src(&self) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetSource for Error {
|
||||||
|
fn path(&self) -> Option<PathBuf> {
|
||||||
match self {
|
match self {
|
||||||
Error::DuplicateModule { second, .. } => Some(second.to_path_buf()),
|
Error::DuplicateModule { second, .. } => Some(second.to_path_buf()),
|
||||||
Error::FileIo { .. } => None,
|
Error::FileIo { .. } => None,
|
||||||
|
@ -234,25 +239,6 @@ impl Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Error {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let miette_handler = MietteHandlerOpts::new()
|
|
||||||
// For better support of terminal themes use the ANSI coloring
|
|
||||||
.rgb_colors(RgbColors::Never)
|
|
||||||
// If ansi support is disabled in the config disable the eye-candy
|
|
||||||
.color(true)
|
|
||||||
.unicode(true)
|
|
||||||
.terminal_links(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Ignore error to prevent format! panics. This can happen if span points at some
|
|
||||||
// inaccessible location, for example by calling `report_error()` with wrong working set.
|
|
||||||
let _ = miette_handler.debug(self, f);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Diagnostic for Error {
|
impl Diagnostic for Error {
|
||||||
fn severity(&self) -> Option<miette::Severity> {
|
fn severity(&self) -> Option<miette::Severity> {
|
||||||
Some(miette::Severity::Error)
|
Some(miette::Severity::Error)
|
||||||
|
@ -493,6 +479,24 @@ pub enum Warning {
|
||||||
DependencyAlreadyExists { name: PackageName },
|
DependencyAlreadyExists { name: PackageName },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GetSource for Warning {
|
||||||
|
fn path(&self) -> Option<PathBuf> {
|
||||||
|
match self {
|
||||||
|
Warning::NoValidators => None,
|
||||||
|
Warning::Type { path, .. } => Some(path.clone()),
|
||||||
|
Warning::DependencyAlreadyExists { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
Warning::NoValidators => None,
|
||||||
|
Warning::Type { src, .. } => Some(src.clone()),
|
||||||
|
Warning::DependencyAlreadyExists { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Diagnostic for Warning {
|
impl Diagnostic for Warning {
|
||||||
fn severity(&self) -> Option<miette::Severity> {
|
fn severity(&self) -> Option<miette::Severity> {
|
||||||
Some(miette::Severity::Warning)
|
Some(miette::Severity::Warning)
|
||||||
|
|
|
@ -56,6 +56,11 @@ pub struct Source {
|
||||||
pub kind: ModuleKind,
|
pub kind: ModuleKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Checkpoint {
|
||||||
|
module_types: HashMap<String, TypeInfo>,
|
||||||
|
defined_modules: HashMap<String, PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Project<T>
|
pub struct Project<T>
|
||||||
where
|
where
|
||||||
T: EventListener,
|
T: EventListener,
|
||||||
|
|
Loading…
Reference in New Issue