diff --git a/Cargo.lock b/Cargo.lock index 83c1d95..a43f1c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,11 +96,12 @@ checksum = "328b822bdcba4d4e402be8d9adb6eebf269f969f8eadef977a553ff3c4fbcb58" [[package]] name = "crossbeam-channel" -version = "0.3.9" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ec7fcd21571dc78f96cc96243cab8d8f035247c3efd16c687be154c3fa9efa" +checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" dependencies = [ - "crossbeam-utils 0.6.6", + "crossbeam-utils 0.7.2", + "maybe-uninit", ] [[package]] @@ -115,10 +116,11 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.6.6" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ + "autocfg", "cfg-if 0.1.10", "lazy_static", ] @@ -341,6 +343,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "memchr" version = "2.5.0" @@ -357,14 +365,14 @@ dependencies = [ ] [[package]] -name = "nixpkgs-fmt-rnix" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38cd28a1c98361571955f6ddd71a27631671b025c49f4a5885cf4305ea82ba60" +name = "nixpkgs-fmt" +version = "1.3.0" +source = "git+https://github.com/nix-community/nixpkgs-fmt#3c4addcc0aa9a6eb9fb64d9206733110d1153a52" dependencies = [ "clap", - "crossbeam-channel 0.3.9", + "crossbeam-channel 0.4.4", "ignore", + "libc", "rnix", "rowan", "serde_json", @@ -471,12 +479,14 @@ dependencies = [ "lsp-server", "lsp-types", "maplit", - "nixpkgs-fmt-rnix", + "nixpkgs-fmt", "regex", "rnix", "serde", "serde_json", + "smol_str", "stoppable_thread", + "textedit-merge", ] [[package]] @@ -618,6 +628,12 @@ dependencies = [ "serde", ] +[[package]] +name = "textedit-merge" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9bb566d3c513aaadef8a4b2e805d75c5714e21b0a689c930365762ff0f2fc03" + [[package]] name = "textwrap" version = "0.11.0" @@ -679,9 +695,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" [[package]] name = "unicode-normalization" diff --git a/Cargo.toml b/Cargo.toml index 39d5233..07478a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,9 @@ regex = "1.5.6" rnix = "0.10.2" serde = "1.0.138" serde_json = "1.0.82" -nixpkgs-fmt-rnix = "1.2.0" +nixpkgs-fmt = { git = "https://github.com/nix-community/nixpkgs-fmt" } +smol_str = "0.1.17" +textedit-merge = "0.2.1" [dev-dependencies] stoppable_thread = "0.2.1" diff --git a/src/main.rs b/src/main.rs index 6f92189..6118f5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,7 +59,10 @@ use std::{ str::FromStr, }; -use crate::error::AppError; +use crate::{ + error::AppError, + utils::{atom_edit_to_tuple, tuple_to_text_edit}, +}; type Error = Box; @@ -210,11 +213,21 @@ impl App { self.reply(Response::new_ok(id, document_links)); } else if let Some((id, params)) = cast::(&mut req) { let changes = if let Some((ast, code, _)) = self.files.get(¶ms.text_document.uri) { - let fmt = nixpkgs_fmt::reformat_node(&ast.node()); - vec![TextEdit { - range: utils::range(&code, TextRange::up_to(ast.node().text().len())), - new_text: fmt.text().to_string(), - }] + let (spacing_edits, indent_edits) = nixpkgs_fmt::reformat_edits(&ast.node()); + let merged_edits = textedit_merge::merge( + &spacing_edits + .into_iter() + .map(atom_edit_to_tuple) + .collect::, _)>>(), + &indent_edits + .into_iter() + .map(atom_edit_to_tuple) + .collect::, _)>>(), + ); + merged_edits + .into_iter() + .map(|t| tuple_to_text_edit(t, &code)) + .collect::>() } else { Vec::new() }; diff --git a/src/tests.rs b/src/tests.rs index 84b887c..7a2a2e1 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -263,6 +263,138 @@ fn test_rename() { handle.stop().join().expect("Failed to gracefully terminate LSP worker thread!"); } +#[test] +fn test_reformat_integration() { + let urlpath = "file:///code/default.nix"; + let input = r#"{ + f = { x + , y + }: body; + + testAllTrue = expr: {inherit expr;expected=map (x: true) expr; }; +} +"#; + let (client, handle) = prepare_integration_test(input, urlpath); + + let r = Request { + id: RequestId::from(23), + method: String::from("textDocument/formatting"), + params: json!({ + "textDocument": { + "uri": "file:///code/default.nix", + }, + "options": { + // Tab size isn't respected yet + "tabSize": 37, + "insertSpaces": true + } + }) + }; + client.sender.send(r.into()).expect("Cannot send reformat request!"); + + expect_diagnostics(&client); + + let msg = recv_msg(&client); + let hover_json = coerce_response(msg).result.expect("Expected reformat response!"); + let edits = hover_json; + let expected = json!([ + { + "newText": "\n ", + "range": { + "start": { + "character": 5, + "line": 1 + }, + "end": { + "character": 6, + "line": 1 + } + } + }, + { + "newText": "\n ", + "range": { + "start": { + "character": 9, + "line": 1 + }, + "end": { + "character": 2, + "line": 2 + } + } + }, + { + "newText": "\n ", + "range": { + "start": { + "character": 5, + "line": 2 + }, + "end": { + "character": 6, + "line": 3 + } + } + }, + { + "newText": " ", + "range": { + "start": { + "character": 23, + "line": 5 + }, + "end": { + "character": 23, + "line": 5 + } + } + }, + { + "newText": " ", + "range": { + "start": { + "character": 36, + "line": 5 + }, + "end": { + "character": 36, + "line": 5 + } + } + }, + { + "newText": " ", + "range": { + "start": { + "character": 44, + "line": 5 + }, + "end": { + "character": 44, + "line": 5 + } + } + }, + { + "newText": " ", + "range": { + "start": { + "character": 45, + "line": 5 + }, + "end": { + "character": 45, + "line": 5 + } + } + } + ]); + assert_eq!(edits, expected); + + handle.stop().join().expect("Failed to gracefully terminate LSP worker thread!"); +} + #[test] fn attrs_simple() { let code = "{ x = 1; y = 2; }.x"; diff --git a/src/utils.rs b/src/utils.rs index a1651b8..1706f2e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,11 @@ use lsp_types::*; +use nixpkgs_fmt::AtomEdit; use rnix::{types::*, SyntaxNode, TextRange, TextSize, TokenAtOffset}; use std::{ collections::HashMap, convert::TryFrom, fmt::{Debug, Display, Formatter, Result}, + ops, path::PathBuf, rc::Rc, }; @@ -323,9 +325,30 @@ pub fn selection_ranges(root: &SyntaxNode, content: &str, pos: Position) -> Opti root.map(|b| *b) } +/// Convert an AtomEdit to a tuple whose first element is `atom_edit`'s delete range, and whose +/// second element is a String with the same contents as `atom_edit`'s insert value. +pub fn atom_edit_to_tuple(atom_edit: AtomEdit) -> (ops::Range, String) { + let r: std::ops::Range = atom_edit.delete.start().into()..atom_edit.delete.end().into(); + (r, atom_edit.insert.to_string()) +} + +pub fn tuple_to_text_edit(tup: (ops::Range, String), code: &str) -> TextEdit { + TextEdit { + range: range( + code, + TextRange::new( + TextSize::from(tup.0.start as u32), + TextSize::from(tup.0.end as u32), + ), + ), + new_text: tup.1, + } +} + #[cfg(test)] mod tests { use super::*; + use smol_str::SmolStr; #[test] fn test_get_offset_from_nix_expr() { @@ -467,4 +490,34 @@ mod tests { let ident_ = ident.unwrap(); assert_eq!(vec!["a"], ident_.path); } + + #[test] + fn test_atom_edit_to_tuple() { + let atom_edits = vec![ + AtomEdit { + delete: TextRange::new(TextSize::from(5), TextSize::from(9)), + insert: SmolStr::from("hello world"), + }, + AtomEdit { + delete: TextRange::new(TextSize::from(11), TextSize::from(11)), + insert: SmolStr::from("the quick brown fox"), + }, + AtomEdit { + delete: TextRange::new(TextSize::from(115698), TextSize::from(126498)), + insert: SmolStr::from("a string"), + }, + ]; + let expected = vec![ + (5..9, String::from("hello world")), + (11..11, String::from("the quick brown fox")), + (115698..126498, String::from("a string")), + ]; + assert_eq!( + atom_edits + .into_iter() + .map(atom_edit_to_tuple) + .collect::, String)>>(), + expected + ); + } }