Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add support for formatting capabilities to the LSP #1216

Merged
merged 12 commits into from
Apr 13, 2023
2 changes: 1 addition & 1 deletion lsp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
47 changes: 46 additions & 1 deletion lsp/nls/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<temp-directory>/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`
72 changes: 72 additions & 0 deletions lsp/nls/src/requests/formatting.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
1 change: 1 addition & 0 deletions lsp/nls/src/requests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod completion;
pub mod formatting;
pub mod goto;
pub mod hover;
pub mod symbols;
17 changes: 13 additions & 4 deletions lsp/nls/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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,
Expand Down Expand Up @@ -62,6 +64,7 @@ impl Server {
..Default::default()
}),
document_symbol_provider: Some(OneOf::Left(true)),
document_formatting_provider: Some(OneOf::Left(true)),
..ServerCapabilities::default()
}
}
Expand Down Expand Up @@ -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(()),
};

Expand Down