Skip to content

Commit

Permalink
Impl file formatting via external command
Browse files Browse the repository at this point in the history
Fixes #12
  • Loading branch information
oxalica committed Sep 26, 2022
1 parent 764c2be commit f535de5
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 17 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Super fast incremental analysis! Scans `all-packages.nix` in less than 0.1s and

See [release notes][releases] for change log between tagged unstable versions.

See [`doc/features.md`](doc/features.md) for an incomplete list of notable features currently
See [`docs/features.md`](docs/features.md) for an incomplete list of notable features currently
implemented or planned.

[releases]: https://github.com/oxalica/nil/releases
Expand Down
1 change: 1 addition & 0 deletions crates/nil/src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub(crate) fn server_capabilities() -> ServerCapabilities {
)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
document_symbol_provider: Some(OneOf::Left(true)),
document_formatting_provider: Some(OneOf::Left(true)),
..Default::default()
}
}
80 changes: 75 additions & 5 deletions crates/nil/src/handler.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use crate::{convert, Result, StateSnapshot};
use ide::FileRange;
use lsp_types::{
CompletionParams, CompletionResponse, Diagnostic, DocumentSymbolParams, DocumentSymbolResponse,
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams, Location,
PrepareRenameResponse, ReferenceParams, RenameParams, SelectionRange, SelectionRangeParams,
SemanticTokens, SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
SemanticTokensResult, TextDocumentPositionParams, Url, WorkspaceEdit,
CompletionParams, CompletionResponse, Diagnostic, DocumentFormattingParams,
DocumentSymbolParams, DocumentSymbolResponse, GotoDefinitionParams, GotoDefinitionResponse,
Hover, HoverParams, Location, Position, PrepareRenameResponse, Range, ReferenceParams,
RenameParams, SelectionRange, SelectionRangeParams, SemanticTokens, SemanticTokensParams,
SemanticTokensRangeParams, SemanticTokensRangeResult, SemanticTokensResult,
TextDocumentPositionParams, TextEdit, Url, WorkspaceEdit,
};
use std::process;
use std::sync::Arc;
use text_size::TextRange;

const MAX_DIAGNOSTICS_CNT: usize = 128;
Expand Down Expand Up @@ -174,3 +177,70 @@ pub(crate) fn document_symbol(
let syms = convert::to_document_symbols(&line_map, syms);
Ok(Some(DocumentSymbolResponse::Nested(syms)))
}

// FIXME: This is sync now.
pub(crate) fn formatting(
snap: StateSnapshot,
params: DocumentFormattingParams,
) -> Result<Option<Vec<TextEdit>>> {
fn run_with_stdin(
cmd: &[String],
stdin_data: impl AsRef<[u8]> + Send + 'static,
) -> Result<String> {
let mut child = process::Command::new(&cmd[0])
.args(&cmd[1..])
.stdin(process::Stdio::piped())
.stdout(process::Stdio::piped())
.stderr(process::Stdio::piped())
.spawn()?;
let mut stdin = child.stdin.take().unwrap();
std::thread::spawn(move || {
let _ = std::io::copy(&mut stdin_data.as_ref(), &mut stdin);
});
let output = child.wait_with_output()?;
if !output.status.success() {
return Err(format!(
"Process exited with {}.\n{}",
output.status,
String::from_utf8_lossy(&output.stderr)
)
.into());
}
let stdout = String::from_utf8(output.stdout)?;
Ok(stdout)
}

let cmd = match &snap.config.formatting_command {
Some(cmd) => cmd,
None => return Ok(None),
};

let (file_content, line_map) = {
let vfs = snap.vfs();
let file = convert::from_file(&vfs, &params.text_document)?;
(vfs.content_for_file(file), vfs.line_map_for_file(file))
};

let new_content = run_with_stdin(cmd, <Arc<[u8]>>::from(file_content.clone()))
.map_err(|err| format!("Failed to run formatter: {err}"))?;

if new_content == *file_content {
return Ok(None);
}

// Replace the whole file.
let last_line = line_map.last_line();
Ok(Some(vec![TextEdit {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: last_line,
character: line_map.end_col_for_line(last_line),
},
},
new_text: new_content,
}]))
}
31 changes: 26 additions & 5 deletions crates/nil/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const CONFIG_KEY: &str = "nil";
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Config {
pub(crate) diagnostics_ignored: HashSet<String>,
pub(crate) formatting_command: Option<Vec<String>>,
}

type ReqHandler = fn(&mut State, Response);
Expand Down Expand Up @@ -124,6 +125,7 @@ impl State {
.on::<req::SemanticTokensRangeRequest>(handler::semantic_token_range)
.on::<req::HoverRequest>(handler::hover)
.on::<req::DocumentSymbolRequest>(handler::document_symbol)
.on::<req::Formatting>(handler::formatting)
.finish();
}

Expand Down Expand Up @@ -222,22 +224,41 @@ impl State {
fn update_config(&mut self, mut v: serde_json::Value) {
let mut updated_diagnostics = false;
let mut config = Config::clone(&self.config);
let mut errors = Vec::new();
if let Some(v) = v.pointer_mut("/diagnostics/ignored") {
match serde_json::from_value(v.take()) {
Ok(v) => {
config.diagnostics_ignored = v;
updated_diagnostics = true;
}
Err(e) => {
self.show_message(
MessageType::ERROR,
format!("Invalid value of setting `diagnostics.ignored`: {e}"),
);
errors.push(format!("Invalid value of `diagnostics.ignored`: {e}"));
}
}
}
if let Some(v) = v.pointer_mut("/formatting/command") {
match serde_json::from_value::<Option<Vec<String>>>(v.take()) {
Ok(Some(v)) if v.is_empty() => {
errors.push("`formatting.command` must not be an empty list".into());
}
Ok(v) => {
config.formatting_command = v;
}
Err(e) => {
errors.push(format!("Invalid value of `formatting.command`: {e}"));
}
}
}
tracing::debug!("Updated config, errors: {errors:?}, config: {config:?}");
self.config = Arc::new(config);
tracing::debug!("Updated config: {:?}", self.config);

if !errors.is_empty() {
let msg = ["Failed to apply some settings:"]
.into_iter()
.chain(errors.iter().flat_map(|s| ["\n- ", s]))
.collect::<String>();
self.show_message(MessageType::ERROR, msg);
}

// Refresh all diagnostics since the filter may be changed.
if updated_diagnostics {
Expand Down
8 changes: 6 additions & 2 deletions crates/nil/src/vfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,12 @@ impl Vfs {
change
}

pub fn line_map_for_file(&self, file_id: FileId) -> Arc<LineMap> {
self.files[file_id.0 as usize].1.clone()
pub fn content_for_file(&self, file: FileId) -> Arc<str> {
self.files[file.0 as usize].0.clone()
}

pub fn line_map_for_file(&self, file: FileId) -> Arc<LineMap> {
self.files[file.0 as usize].1.clone()
}
}

Expand Down
2 changes: 2 additions & 0 deletions dev/vim-coc.nix
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,14 @@ let
'';

cocSetting = {
"coc.preferences.formatOnSaveFiletypes" = [ "nix" ];
languageserver.nix = {
command = "nil";
filetypes = [ "nix" ];
rootPatterns = [ "flake.nix" ];
settings.nil = {
testSetting = 42;
formatting.command = [ "nixpkgs-fmt" ];
};
};
semanticTokens.filetypes = [ "nix" ];
Expand Down
31 changes: 27 additions & 4 deletions doc/features.md → docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,41 @@ This incomplete list tracks noteble features currently implemented or planned.

[`coc.nvim`] doesn't enable semantic highlighting by default.
You need to manually enable it in settings.
```json
```jsonc
// coc-settings.json
{
"semanticTokens": { "filetypes": ["nix"] }
}
```

[`coc.nvim`]: https://github.com/neoclide/coc.nvim


- [x] Hover text. `textDocument/hover`.
- [x] Show kind of names.
- [x] Documentation for builtin names.
- [x] File symbols with hierarchy (aka. outline). `textDocument/documentSymbol`

- [x] File formatting.
- [x] Whole file formatting.
- [ ] Range formatting.
- [ ] On-type formatting.
- [x] External formatter.
- Currently, an external formatter must be configured via LSP setting
`formatting.command` to enable this functionality.
It accepts `null` for disabled, or an non-empty array for the formatting command,
eg. `["nixpkgs-fmt"]` for [nixpkgs-fmt].
The command must read Nix code from stdin and print the formatted code to stdout.

[nixpkgs-fmt]: https://github.com/nix-community/nixpkgs-fmt

You might need to set other editor settings to enable format-on-save.
Like, for [`coc.nvim`],
```jsonc
// coc-settings.json
{
"coc.preferences.formatOnSaveFiletypes": ["nix"]
}
```

- [ ] Cross-file analysis.
- [ ] Multi-threaded.

[`coc.nvim`]: https://github.com/neoclide/coc.nvim
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
gdb
jq
pre-commit
nixpkgs-fmt
(import ./dev/neovim-lsp.nix { inherit pkgs; })
(import ./dev/vim-coc.nix { inherit pkgs; })
] ++ lib.optionals (lib.meta.availableOn stdenv.hostPlatform vscodium) [
Expand Down

0 comments on commit f535de5

Please sign in to comment.