diff --git a/lsp/README.md b/lsp/README.md index ab6d316e76..d9924a834c 100644 --- a/lsp/README.md +++ b/lsp/README.md @@ -166,4 +166,4 @@ edit `coc-settings.json`) and add: ### Emacs -TODO +Follow the instructions on the the `nickel-mode` [repo](https://github.com/nickel-lang/nickel-mode). diff --git a/lsp/nls/README.md b/lsp/nls/README.md index 34b113b9e4..3b251403a6 100644 --- a/lsp/nls/README.md +++ b/lsp/nls/README.md @@ -8,4 +8,49 @@ editor. NLS is a stand-alone binary. Once built, you must then configure you code editor to use it for Nickel source files. This document covers building NLS and using -it in VSCode and (Neo)Vim. +it in VSCode, (Neo)Vim and Emacs. + +## Formatting Capabilities + +Formatting in `nls` is currently based on [topiary](https://github.com/tweag/topiary). + +To use it successfully, you need ensure you follow these steps: + +1. Have the `topiary` binary installed in your `PATH`. See [here](https://github.com/tweag/topiary#installing). +2. Have the [topiary](https://github.com/tweag/topiary) repo cloned locally. +3. Set the environment variable `TOPIARY_REPO_DIR` to point to the local copy +of the repo. +4. And finally, an environment variable `TOPIARY_LANGUAGE_DIR` to point to `$TOPIARY_REPO_DIR/languages`. + +Steps 2-4 are necessary because, for now, `topiary` cannot be used outside its +repo directory. + +## Alternatives + +I think making a user fetch the `topiary` and set those environment variables, +just to have the formatting capability in `nls` is a bit too much. I can think +of the following alternatives, but I don't know if they are ideal. + +### Keep a cache of the topiary repo + +Keep a cache of the `topiary` repo. Fetch the repo from GitHub if it is not +available in the local cache or not up to date. + +* Pros + * Automatic updates of the repo (and hence the formatting rules for Nickel) +* Cons + * We still have to set environment variables at runtime + * `nls` has to download a potentially large repo + +### Embedded Nickel formatting rules as a string in the `nls` binary + + Since `topiary` just needs a single `nickel.scm` to be able to format nickel + files we could just point `TOPIARY_LANGUAGE_DIR` to `/language` + , and put the embedded file in that directory. + +* Pros: + * It's just a single file, so it will be small + * No need to download/fetch anything +* Cons: + * We still have to set environment variables at runtime + * We have to ensure the formatting rules are up to date with `topiary` diff --git a/lsp/nls/src/requests/formatting.rs b/lsp/nls/src/requests/formatting.rs new file mode 100644 index 0000000000..b240fab7aa --- /dev/null +++ b/lsp/nls/src/requests/formatting.rs @@ -0,0 +1,72 @@ +use std::process; + +use lsp_server::{ErrorCode, RequestId, Response, ResponseError}; +use lsp_types::{DocumentFormattingParams, Position, Range, TextEdit}; + +use crate::server::{self, Server}; + +/// Handle the LSP formatting request from a client using an external binary as a formatter. +/// If this succeds, it sends a reponse to the server and returns `Ok(..)`, otherwise, +/// it only returns an `Err(..)`. +pub fn handle_format_document( + params: DocumentFormattingParams, + id: RequestId, + server: &mut Server, +) -> Result<(), ResponseError> { + let document_id = params.text_document.uri.to_file_path().unwrap(); + let file_id = server.cache.id_of(document_id).unwrap(); + let text = server.cache.files().source(file_id).clone(); + let document_length = text.lines().count() as u32; + let last_line_length = text.lines().rev().next().unwrap().len() as u32; + + let formatting_command = server::FORMATTING_COMMAND; + let Ok(mut topiary) = process::Command::new(formatting_command[0]) + .args(&formatting_command[1..]) + .stdin(process::Stdio::piped()) + .stdout(process::Stdio::piped()) + .stderr(process::Stdio::piped()) + .spawn() else { + let message = "Executing topiary failed"; + return Err(ResponseError { + code: ErrorCode::InternalError as i32, + message: String::from(message), + data: None, + }); + }; + + let mut stdin = topiary.stdin.take().unwrap(); + + std::thread::spawn(move || { + let mut text_bytes = text.as_bytes(); + std::io::copy(&mut text_bytes, &mut stdin).unwrap(); + }); + + let output = topiary.wait_with_output().unwrap(); + + if !output.status.success() { + let error = String::from_utf8_lossy(&output.stderr); + return Err(ResponseError { + code: ErrorCode::InternalError as i32, + message: error.to_string(), + data: None, + }); + } + + let new_text = String::from_utf8(output.stdout).unwrap(); + + let result = Some(vec![TextEdit { + range: Range { + start: Position { + line: 0, + character: 0, + }, + end: Position { + line: document_length - 1, + character: last_line_length, + }, + }, + new_text, + }]); + server.reply(Response::new_ok(id, result)); + Ok(()) +} diff --git a/lsp/nls/src/requests/mod.rs b/lsp/nls/src/requests/mod.rs index 66c441c46a..e9598ad947 100644 --- a/lsp/nls/src/requests/mod.rs +++ b/lsp/nls/src/requests/mod.rs @@ -1,4 +1,5 @@ pub mod completion; +pub mod formatting; pub mod goto; pub mod hover; pub mod symbols; diff --git a/lsp/nls/src/server.rs b/lsp/nls/src/server.rs index d3e72c1e61..7e7a612190 100644 --- a/lsp/nls/src/server.rs +++ b/lsp/nls/src/server.rs @@ -11,9 +11,10 @@ use lsp_types::{ notification::{DidChangeTextDocument, DidOpenTextDocument}, request::{Request as RequestTrait, *}, CompletionOptions, CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, - DocumentSymbolParams, GotoDefinitionParams, HoverOptions, HoverParams, HoverProviderCapability, - OneOf, ReferenceParams, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, - TextDocumentSyncOptions, WorkDoneProgressOptions, + DocumentFormattingParams, DocumentSymbolParams, GotoDefinitionParams, HoverOptions, + HoverParams, HoverProviderCapability, OneOf, ReferenceParams, ServerCapabilities, + TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, + WorkDoneProgressOptions, }; use nickel_lang::{ @@ -26,11 +27,12 @@ use nickel_lang::{stdlib, typecheck::Context}; use crate::{ cache::CacheExt, linearization::{completed::Completed, Environment, ItemId}, - requests::{completion, goto, hover, symbols}, + requests::{completion, formatting, goto, hover, symbols}, trace::Trace, }; pub const DOT_COMPL_TRIGGER: &str = "."; +pub const FORMATTING_COMMAND: [&str; 3] = ["topiary", "--language", "nickel"]; pub struct Server { pub connection: Connection, @@ -62,6 +64,7 @@ impl Server { ..Default::default() }), document_symbol_provider: Some(OneOf::Left(true)), + document_formatting_provider: Some(OneOf::Left(true)), ..ServerCapabilities::default() } } @@ -235,6 +238,12 @@ impl Server { symbols::handle_document_symbols(params, req.id.clone(), self) } + Formatting::METHOD => { + debug!("handle formatting"); + let params: DocumentFormattingParams = serde_json::from_value(req.params).unwrap(); + formatting::handle_format_document(params, req.id.clone(), self) + } + _ => Ok(()), };