Skip to content

Commit

Permalink
Merge pull request #1216 from tweag/lsp/formatting
Browse files Browse the repository at this point in the history
Add support for formatting capabilities to the LSP
  • Loading branch information
ebresafegaga authored Apr 13, 2023
2 parents 80edc42 + fa3dfad commit 526f95a
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 6 deletions.
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

0 comments on commit 526f95a

Please # to comment.