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_types::{
|
||||
OneOf, SaveOptions, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
|
||||
TextDocumentSyncOptions, TextDocumentSyncSaveOptions,
|
||||
};
|
||||
|
||||
mod cast;
|
||||
pub mod error;
|
||||
mod line_numbers;
|
||||
pub mod server;
|
||||
mod utils;
|
||||
|
||||
use error::Error;
|
||||
|
||||
|
@ -15,6 +20,18 @@ use crate::server::Server;
|
|||
pub fn start() -> Result<(), Error> {
|
||||
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
|
||||
// also be implemented to use sockets or HTTP.
|
||||
let (connection, io_threads) = Connection::stdio();
|
||||
|
@ -25,7 +42,7 @@ pub fn start() -> Result<(), Error> {
|
|||
let initialization_params = connection.initialize(server_capabilities)?;
|
||||
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)?;
|
||||
|
||||
|
|
|
@ -1,26 +1,44 @@
|
|||
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 aiken_project::{
|
||||
config,
|
||||
error::{Error as ProjectError, GetSource},
|
||||
};
|
||||
use lsp_server::{Connection, Message};
|
||||
use lsp_types::{
|
||||
notification::{
|
||||
DidChangeTextDocument, DidSaveTextDocument, Notification, PublishDiagnostics, ShowMessage,
|
||||
DidChangeTextDocument, DidSaveTextDocument, Notification, Progress, PublishDiagnostics,
|
||||
ShowMessage,
|
||||
},
|
||||
request::{Formatting, Request},
|
||||
request::{Formatting, Request, WorkDoneProgressCreate},
|
||||
DocumentFormattingParams, InitializeParams, TextEdit,
|
||||
};
|
||||
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)]
|
||||
pub struct Server {
|
||||
// Project root directory
|
||||
root: PathBuf,
|
||||
|
||||
config: Option<config::Config>,
|
||||
|
||||
/// 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
|
||||
/// diagnostics always need a location.
|
||||
stored_messages: Vec<lsp_types::ShowMessageParams>,
|
||||
|
||||
/// An instance of a LspProject
|
||||
compiler: Option<LspProject>,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
/// 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,
|
||||
};
|
||||
|
||||
pub fn listen(&mut self, connection: Connection) -> Result<(), ServerError> {
|
||||
self.publish_stored_diagnostics(&connection)?;
|
||||
let notification = lsp_server::Notification {
|
||||
method: PublishDiagnostics::METHOD.to_string(),
|
||||
params: serde_json::to_value(params)?,
|
||||
};
|
||||
|
||||
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)?
|
||||
}
|
||||
}
|
||||
connection
|
||||
.sender
|
||||
.send(lsp_server::Message::Notification(notification))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
/// Compile the project if we are in one. Otherwise do nothing.
|
||||
fn compile(&mut self, connection: &Connection) -> Result<(), ServerError> {
|
||||
self.notify_client_of_compilation_start(connection)?;
|
||||
|
||||
eprintln!("{:?}", self.compiler.is_some());
|
||||
if let Some(compiler) = self.compiler.as_mut() {
|
||||
let result = compiler.compile();
|
||||
|
||||
for warning in compiler.project.warnings() {
|
||||
self.process_diagnostic(warning)?;
|
||||
}
|
||||
|
||||
if let Err(errs) = result {
|
||||
for err in errs {
|
||||
self.process_diagnostic(err)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.notify_client_of_compilation_end(connection)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_compilation_progress_token(
|
||||
&mut self,
|
||||
request: lsp_server::Request,
|
||||
) -> Result<lsp_server::Response, ServerError> {
|
||||
let id = request.id.clone();
|
||||
connection: &lsp_server::Connection,
|
||||
) -> Result<(), ServerError> {
|
||||
let params = lsp_types::WorkDoneProgressCreateParams {
|
||||
token: lsp_types::NumberOrString::String(COMPILING_PROGRESS_TOKEN.into()),
|
||||
};
|
||||
|
||||
match request.method.as_str() {
|
||||
Formatting::METHOD => {
|
||||
let params = cast_request::<Formatting>(request)?;
|
||||
let request = lsp_server::Request {
|
||||
id: CREATE_COMPILING_PROGRESS_TOKEN.to_string().into(),
|
||||
method: WorkDoneProgressCreate::METHOD.into(),
|
||||
params: serde_json::to_value(¶ms)?,
|
||||
};
|
||||
|
||||
let result = self.format(params);
|
||||
connection
|
||||
.sender
|
||||
.send(lsp_server::Message::Request(request))?;
|
||||
|
||||
match result {
|
||||
Ok(text_edit) => {
|
||||
let result = serde_json::to_value(text_edit)?;
|
||||
|
||||
Ok(lsp_server::Response {
|
||||
id,
|
||||
error: None,
|
||||
result: Some(result),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => match err {
|
||||
ProjectError::List(errors) => {
|
||||
for error in errors {
|
||||
if error.source_code().is_some() {
|
||||
self.process_diagnostic(error)?;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(lsp_server::Response {
|
||||
id,
|
||||
error: None,
|
||||
result: Some(serde_json::json!(null)),
|
||||
})
|
||||
}
|
||||
error => {
|
||||
if error.source_code().is_some() {
|
||||
self.process_diagnostic(error)?;
|
||||
fn format(
|
||||
&mut self,
|
||||
params: DocumentFormattingParams,
|
||||
) -> Result<Vec<TextEdit>, Vec<ProjectError>> {
|
||||
let path = params.text_document.uri.path();
|
||||
let mut new_text = String::new();
|
||||
|
||||
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,
|
||||
})
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
unsupported => Err(ServerError::UnsupportedLspRequest {
|
||||
request: unsupported.to_string(),
|
||||
}),
|
||||
}
|
||||
|
||||
Ok(vec![text_edit_replace(new_text)])
|
||||
}
|
||||
|
||||
fn handle_notification(
|
||||
|
@ -178,96 +196,123 @@ impl Server {
|
|||
}
|
||||
}
|
||||
|
||||
fn format(&mut self, params: DocumentFormattingParams) -> Result<Vec<TextEdit>, ProjectError> {
|
||||
let path = params.text_document.uri.path();
|
||||
let mut new_text = String::new();
|
||||
fn handle_request(
|
||||
&mut self,
|
||||
request: lsp_server::Request,
|
||||
) -> Result<lsp_server::Response, ServerError> {
|
||||
let id = request.id.clone();
|
||||
|
||||
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)
|
||||
})?;
|
||||
match request.method.as_str() {
|
||||
Formatting::METHOD => {
|
||||
let params = cast_request::<Formatting>(request)?;
|
||||
|
||||
aiken_lang::format::pretty(&mut new_text, module, extra, src);
|
||||
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(errors) => {
|
||||
for error in errors {
|
||||
self.process_diagnostic(error)?;
|
||||
}
|
||||
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(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)])
|
||||
pub fn listen(&mut self, connection: Connection) -> Result<(), ServerError> {
|
||||
self.create_compilation_progress_token(&connection)?;
|
||||
self.start_watching_aiken_toml(&connection)?;
|
||||
|
||||
// Compile the project once so we have all the state and any initial errors
|
||||
self.compile(&connection)?;
|
||||
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(());
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
tracing::debug!("Get request: {:#?}", req);
|
||||
|
||||
for (path, diagnostics) in self.stored_diagnostics.drain() {
|
||||
let uri = path_to_uri(path)?;
|
||||
let response = self.handle_request(req)?;
|
||||
|
||||
// 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());
|
||||
self.publish_stored_diagnostics(&connection)?;
|
||||
|
||||
// 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))?;
|
||||
connection.sender.send(Message::Response(response))?;
|
||||
}
|
||||
Message::Response(_) => (),
|
||||
Message::Notification(notification) => {
|
||||
self.handle_notification(&connection, 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,
|
||||
pub fn new(
|
||||
initialize_params: InitializeParams,
|
||||
config: Option<config::Config>,
|
||||
root: PathBuf,
|
||||
) -> Self {
|
||||
let mut server = Server {
|
||||
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 {
|
||||
method: PublishDiagnostics::METHOD.to_string(),
|
||||
params: serde_json::to_value(params)?,
|
||||
};
|
||||
server.create_new_compiler();
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send(lsp_server::Message::Notification(notification))?;
|
||||
server
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -277,7 +322,10 @@ impl Server {
|
|||
/// 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> {
|
||||
fn process_diagnostic<E>(&mut self, error: E) -> Result<(), ServerError>
|
||||
where
|
||||
E: Diagnostic + GetSource,
|
||||
{
|
||||
let (severity, typ) = match error.severity() {
|
||||
Some(severity) => match severity {
|
||||
miette::Severity::Error => (
|
||||
|
@ -369,56 +417,126 @@ impl Server {
|
|||
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) {
|
||||
self.stored_diagnostics
|
||||
.entry(path)
|
||||
.or_default()
|
||||
.push(diagnostic);
|
||||
}
|
||||
|
||||
fn send_work_done_notification(
|
||||
&self,
|
||||
connection: &Connection,
|
||||
work_done: lsp_types::WorkDoneProgress,
|
||||
) -> Result<(), ServerError> {
|
||||
tracing::info!("sending {:?}", work_done);
|
||||
|
||||
let params = lsp_types::ProgressParams {
|
||||
token: lsp_types::NumberOrString::String(COMPILING_PROGRESS_TOKEN.to_string()),
|
||||
value: lsp_types::ProgressParamsValue::WorkDone(work_done),
|
||||
};
|
||||
|
||||
let notification = lsp_server::Notification {
|
||||
method: Progress::METHOD.into(),
|
||||
params: serde_json::to_value(¶ms)?,
|
||||
};
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send(lsp_server::Message::Notification(notification))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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)?;
|
||||
fn start_watching_aiken_toml(&mut self, connection: &Connection) -> Result<(), ServerError> {
|
||||
let supports_watch_files = self
|
||||
.initialize_params
|
||||
.capabilities
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|w| w.did_change_watched_files)
|
||||
.map(|wf| wf.dynamic_registration == Some(true))
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(params)
|
||||
if !supports_watch_files {
|
||||
tracing::warn!("lsp_client_cannot_watch_gleam_toml");
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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)?;
|
||||
// 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),
|
||||
}],
|
||||
})?;
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
let watch_config = lsp_types::Registration {
|
||||
id: "watch-aiken-toml".into(),
|
||||
method: "workspace/didChangeWatchedFiles".into(),
|
||||
register_options: Some(register_options),
|
||||
};
|
||||
|
||||
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,
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn append(self, next: Self) -> Self {
|
||||
match (self, next) {
|
||||
(Error::List(mut errors), Error::List(mut next_errors)) => {
|
||||
errors.append(&mut next_errors);
|
||||
|
||||
Error::List(errors)
|
||||
}
|
||||
(Error::List(mut errors), rest) => {
|
||||
errors.push(rest);
|
||||
|
||||
Error::List(errors)
|
||||
}
|
||||
(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]),
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
matches!(self, Error::List(errors) if errors.is_empty())
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<PathBuf> {
|
||||
impl From<Error> for Vec<Error> {
|
||||
fn from(value: Error) -> Self {
|
||||
vec![value]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GetSource {
|
||||
fn path(&self) -> Option<PathBuf>;
|
||||
fn src(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
impl GetSource for Error {
|
||||
fn path(&self) -> Option<PathBuf> {
|
||||
match self {
|
||||
Error::DuplicateModule { second, .. } => Some(second.to_path_buf()),
|
||||
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 {
|
||||
fn severity(&self) -> Option<miette::Severity> {
|
||||
Some(miette::Severity::Error)
|
||||
|
@ -493,6 +479,24 @@ pub enum Warning {
|
|||
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 {
|
||||
fn severity(&self) -> Option<miette::Severity> {
|
||||
Some(miette::Severity::Warning)
|
||||
|
|
|
@ -56,6 +56,11 @@ pub struct Source {
|
|||
pub kind: ModuleKind,
|
||||
}
|
||||
|
||||
pub struct Checkpoint {
|
||||
module_types: HashMap<String, TypeInfo>,
|
||||
defined_modules: HashMap<String, PathBuf>,
|
||||
}
|
||||
|
||||
pub struct Project<T>
|
||||
where
|
||||
T: EventListener,
|
||||
|
|
Loading…
Reference in New Issue