diff --git a/lsp/nls/src/actions.rs b/lsp/nls/src/actions.rs new file mode 100644 index 0000000000..53817a9607 --- /dev/null +++ b/lsp/nls/src/actions.rs @@ -0,0 +1,23 @@ +use lsp_server::{RequestId, Response, ResponseError}; +use lsp_types::{CodeActionOrCommand, CodeActionParams}; + +use crate::{cache::CacheExt, server::Server}; + +pub fn handle_code_action( + params: CodeActionParams, + req: RequestId, + server: &mut Server, +) -> Result<(), ResponseError> { + let mut actions = Vec::new(); + + if server.cache.file_id(¶ms.text_document.uri)?.is_some() { + actions.push(CodeActionOrCommand::Command(lsp_types::Command { + title: "evaluate term".to_owned(), + command: "eval".to_owned(), + arguments: Some(vec![serde_json::to_value(¶ms.text_document).unwrap()]), + })); + } + + server.reply(Response::new_ok(req, Some(actions))); + Ok(()) +} diff --git a/lsp/nls/src/cache.rs b/lsp/nls/src/cache.rs index 4b966860b9..4873767556 100644 --- a/lsp/nls/src/cache.rs +++ b/lsp/nls/src/cache.rs @@ -1,5 +1,5 @@ use codespan::{ByteIndex, FileId}; -use lsp_types::TextDocumentPositionParams; +use lsp_types::{TextDocumentPositionParams, Url}; use nickel_lang_core::term::{RichTerm, Term, Traverse}; use nickel_lang_core::{ cache::{Cache, CacheError, CacheOp, EntryState, SourcePath, TermEntry}, @@ -21,6 +21,8 @@ pub trait CacheExt { fn position(&self, lsp_pos: &TextDocumentPositionParams) -> Result; + + fn file_id(&self, uri: &Url) -> Result, crate::error::Error>; } impl CacheExt for Cache { @@ -106,18 +108,21 @@ impl CacheExt for Cache { } } + fn file_id(&self, uri: &Url) -> Result, crate::error::Error> { + let path = uri + .to_file_path() + .map_err(|_| crate::error::Error::FileNotFound(uri.clone()))?; + Ok(self.id_of(&SourcePath::Path(path))) + } + fn position( &self, lsp_pos: &TextDocumentPositionParams, ) -> Result { let uri = &lsp_pos.text_document.uri; - let path = uri - .to_file_path() - .map_err(|_| crate::error::Error::FileNotFound(uri.clone()))?; let file_id = self - .id_of(&SourcePath::Path(path)) + .file_id(uri)? .ok_or_else(|| crate::error::Error::FileNotFound(uri.clone()))?; - let pos = lsp_pos.position; let idx = codespan_lsp::position_to_byte_index(self.files(), file_id, &pos).map_err(|_| { diff --git a/lsp/nls/src/command.rs b/lsp/nls/src/command.rs new file mode 100644 index 0000000000..d1e5787653 --- /dev/null +++ b/lsp/nls/src/command.rs @@ -0,0 +1,39 @@ +use lsp_server::{RequestId, Response, ResponseError}; +use lsp_types::{ExecuteCommandParams, TextDocumentIdentifier, Url}; +use nickel_lang_core::{ + error::IntoDiagnostics, + eval::{cache::CacheImpl, VirtualMachine}, +}; + +use crate::{cache::CacheExt, error::Error, server::Server}; + +pub fn handle_command( + params: ExecuteCommandParams, + req: RequestId, + server: &mut Server, +) -> Result<(), ResponseError> { + match params.command.as_str() { + "eval" => { + server.reply(Response::new_ok(req, None::<()>)); + + let doc: TextDocumentIdentifier = + serde_json::from_value(params.arguments[0].clone()).unwrap(); + eval(server, &doc.uri)?; + Ok(()) + } + _ => Err(Error::CommandNotFound(params.command).into()), + } +} + +fn eval(server: &mut Server, uri: &Url) -> Result<(), Error> { + if let Some(file_id) = server.cache.file_id(uri)? { + // TODO: avoid cloning the cache. Maybe we can have a VM with a &mut Cache? + let mut vm = VirtualMachine::<_, CacheImpl>::new(server.cache.clone(), std::io::stderr()); + let rt = vm.prepare_eval(file_id)?; + if let Err(e) = vm.eval_full(rt) { + let diags = e.into_diagnostics(server.cache.files_mut(), None); + server.issue_diagnostics(file_id, diags); + } + } + Ok(()) +} diff --git a/lsp/nls/src/error.rs b/lsp/nls/src/error.rs index 35d017f2ad..d114310d52 100644 --- a/lsp/nls/src/error.rs +++ b/lsp/nls/src/error.rs @@ -19,8 +19,22 @@ pub enum Error { #[error("Method not supported")] MethodNotFound, + #[error("Command not supported: {0}")] + CommandNotFound(String), + #[error("formatting failed for file {file}: {details}")] FormattingFailed { details: String, file: Url }, + + // Mostly we convert nickel errors into nice diagnostics, but there are a few + // places where we just don't expect them to happen, and then they go here. + #[error("unhandled nickel error: {0}")] + Nickel(String), +} + +impl From for Error { + fn from(e: nickel_lang_core::error::Error) -> Self { + Error::Nickel(format!("{:?}", e)) + } } impl From for ResponseError { @@ -30,8 +44,10 @@ impl From for ResponseError { Error::InvalidPosition { .. } => ErrorCode::InvalidParams, Error::SchemeNotSupported(_) => ErrorCode::InvalidParams, Error::InvalidPath(_) => ErrorCode::InvalidParams, + Error::CommandNotFound(_) => ErrorCode::InvalidParams, Error::MethodNotFound => ErrorCode::MethodNotFound, Error::FormattingFailed { .. } => ErrorCode::InternalError, + Error::Nickel(_) => ErrorCode::InternalError, }; ResponseError { code: code as i32, diff --git a/lsp/nls/src/main.rs b/lsp/nls/src/main.rs index fd7dc61984..aa067bedc6 100644 --- a/lsp/nls/src/main.rs +++ b/lsp/nls/src/main.rs @@ -5,8 +5,10 @@ use anyhow::Result; use log::debug; use lsp_server::Connection; +mod actions; mod analysis; mod cache; +mod command; mod diagnostic; mod error; mod field_walker; diff --git a/lsp/nls/src/server.rs b/lsp/nls/src/server.rs index 1ff65f3d76..0fa57266fc 100644 --- a/lsp/nls/src/server.rs +++ b/lsp/nls/src/server.rs @@ -11,11 +11,12 @@ use lsp_types::{ notification::Notification as _, notification::{DidChangeTextDocument, DidOpenTextDocument}, request::{Request as RequestTrait, *}, - CompletionOptions, CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, - DocumentFormattingParams, DocumentSymbolParams, GotoDefinitionParams, HoverOptions, - HoverParams, HoverProviderCapability, OneOf, PublishDiagnosticsParams, ReferenceParams, - ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, - Url, WorkDoneProgressOptions, + CodeActionParams, CompletionOptions, CompletionParams, DidChangeTextDocumentParams, + DidOpenTextDocumentParams, DocumentFormattingParams, DocumentSymbolParams, + ExecuteCommandParams, GotoDefinitionParams, HoverOptions, HoverParams, HoverProviderCapability, + OneOf, PublishDiagnosticsParams, ReferenceParams, ServerCapabilities, + TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, Url, + WorkDoneProgressOptions, }; use nickel_lang_core::{ @@ -27,8 +28,10 @@ use nickel_lang_core::{ use nickel_lang_core::{stdlib, typecheck::Context}; use crate::{ + actions, analysis::{Analysis, AnalysisRegistry}, cache::CacheExt, + command, diagnostic::DiagnosticCompat, field_walker::DefWithPath, requests::{completion, formatting, goto, hover, symbols}, @@ -72,6 +75,11 @@ impl Server { }), document_symbol_provider: Some(OneOf::Left(true)), document_formatting_provider: Some(OneOf::Left(true)), + code_action_provider: Some(lsp_types::CodeActionProviderCapability::Simple(true)), + execute_command_provider: Some(lsp_types::ExecuteCommandOptions { + commands: vec!["eval".to_owned()], + ..Default::default() + }), ..ServerCapabilities::default() } } @@ -250,6 +258,18 @@ impl Server { formatting::handle_format_document(params, req.id.clone(), self) } + CodeActionRequest::METHOD => { + debug!("code action"); + let params: CodeActionParams = serde_json::from_value(req.params).unwrap(); + actions::handle_code_action(params, req.id.clone(), self) + } + + ExecuteCommand::METHOD => { + debug!("command"); + let params: ExecuteCommandParams = serde_json::from_value(req.params).unwrap(); + command::handle_command(params, req.id.clone(), self) + } + _ => Ok(()), }; diff --git a/lsp/nls/test2.ncl b/lsp/nls/test2.ncl deleted file mode 100644 index aea6f0dac1..0000000000 --- a/lsp/nls/test2.ncl +++ /dev/null @@ -1,2 +0,0 @@ -let x : { _ : { foo : Number | default = 1 } } = {} in -x.PATH.foo