Skip to content
This repository was archived by the owner on Jan 25, 2024. It is now read-only.

Commit fd1ad49

Browse files
Ma27jD91mZM2
andauthored
Enhance completion details for builtins if Nix 2.4 is available (#27)
A hidden feature of Nix 2.4+ (a.k.a. `nixUnstable`) is `nix __dump-builtins` which returns a JSON giving details about built in functions. This change basically does the following things: * It checks whether Nix 2.4 is available on `$PATH`[1]. In that case built-in functions are retrieved via `nix __dump-builtins`[2]. If not, the hard-coded list of builtins is used as a fallback. * If builtins can be retrieved from Nix, the following additional things are added to `CompletionItem`: * The `documentation`[3] field containing markdown documentation for the function is added. This will be shown in editors as one is used to it from e.g. doc-block comments in other languages. * A very basic parameter info is provided. E.g. for `builtins.removeAttrs` the popup shows `Lambda: set -> list -> Result`. While this is fairly basic, it's IMHO a small benefit to make sure one's not forgetting the order of parameters. Please note that this will currently display *wrong* info if a built-in is called via `lib.flip`. * If `**DEPRECATED.**` is at the beginning of `documentation`, the built-in is marked in the LSP response as `deprecated`. While this is a gross hack, it ensures that the only deprecated builtin (`builtins.toPath`) is actually marked as such. [1] Please note that this means that you can wrap `$PATH` for `rnix-lsp` to have this feature without being forced to use `nixUnstable` on your system. [2] As this is only an optional feature (and hence a loose dependency to Nix) I decided against using an FFI and thus keeping the build process rather simple. [3] https://microsoft.github.io/language-server-protocol/specification#textDocument_completion Co-authored-by: jD91mZM2 <[email protected]>
1 parent 6c12d7f commit fd1ad49

File tree

4 files changed

+110
-13
lines changed

4 files changed

+110
-13
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ log = "0.4.8"
1919
lsp-server = "0.3.1"
2020
lsp-types = { version = "0.68.1", features = ["proposed"] }
2121
nixpkgs-fmt = "1.1.0"
22+
regex = "1"
2223
rnix = "0.9.0"
2324
rowan = "0.12.6"
2425
serde = "1.0.104"

src/lookup.rs

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ use std::{
1212

1313
use lazy_static::lazy_static;
1414

15-
// FIXME use Nix bindings to dynamically extract existing builtins.
16-
// e.g. use API behind `nix __dump-builtins`.
15+
use std::{process, str};
16+
use regex;
17+
1718
lazy_static! {
1819
static ref BUILTINS: Vec<String> = vec![
1920
// `nix __dump-builtins | jq 'keys'
@@ -29,34 +30,80 @@ lazy_static! {
2930
].into_iter().map(String::from).collect::<Vec<_>>();
3031
}
3132

33+
#[derive(Debug)]
34+
pub struct LSPDetails {
35+
pub datatype: Datatype,
36+
pub var: Option<Var>,
37+
pub documentation: Option<String>,
38+
pub deprecated: bool,
39+
pub params: Option<String>,
40+
}
41+
42+
impl LSPDetails {
43+
fn builtin_fallback() -> LSPDetails {
44+
LSPDetails {
45+
datatype: Datatype::Lambda,
46+
var: None,
47+
documentation: None,
48+
deprecated: false,
49+
params: None,
50+
}
51+
}
52+
53+
fn builtin_with_doc(deprecated: bool, params: Option<String>, documentation: String) -> LSPDetails {
54+
LSPDetails {
55+
datatype: Datatype::Lambda,
56+
var: None,
57+
documentation: Some(documentation),
58+
deprecated,
59+
params,
60+
}
61+
}
62+
63+
fn from_scope(datatype: Datatype, var: Var) -> LSPDetails {
64+
LSPDetails {
65+
datatype,
66+
var: Some(var),
67+
documentation: None,
68+
deprecated: false,
69+
params: None,
70+
}
71+
}
72+
73+
pub fn render_detail(&self) -> String {
74+
match &self.params {
75+
None => self.datatype.to_string(),
76+
Some(params) => format!("{}: {} -> Result", self.datatype.to_string(), params),
77+
}
78+
}
79+
}
80+
3281
impl App {
3382
pub fn scope_for_ident(
3483
&mut self,
3584
file: Url,
3685
root: &SyntaxNode,
3786
offset: usize,
38-
) -> Option<(Ident, HashMap<String, (Datatype, Option<Var>)>, String)> {
87+
) -> Option<(Ident, HashMap<String, LSPDetails>, String)> {
88+
3989
let mut file = Rc::new(file);
4090
let info = utils::ident_at(&root, offset)?;
4191
let ident = info.ident;
4292
let mut entries = utils::scope_for(&file, ident.node().clone())?
4393
.into_iter()
44-
.map(|(x, var)| (x.to_owned(), (var.datatype, Some(var))))
94+
.map(|(x, var)| (x.to_owned(), LSPDetails::from_scope(var.datatype, var)))
4595
.collect::<HashMap<_, _>>();
4696
for var in info.path {
4797
if !entries.contains_key(&var) && var == "builtins" {
48-
entries = BUILTINS
49-
.iter()
50-
.map(|x| (x.to_owned(), (Datatype::Lambda, None)))
51-
.collect::<HashMap<_, _>>();
98+
entries = self.load_builtins();
5299
} else {
53100
let node_entry = entries.get(&var)?;
54-
if let (_, Some(var)) = node_entry {
101+
if let Some(var) = &node_entry.var {
55102
let node = var.value.clone()?;
56103
entries = self
57104
.scope_from_node(&mut file, node)?
58105
.into_iter()
59-
.map(|(x, var)| (x.to_owned(), (var.datatype, Some(var))))
106+
.map(|(x, var)| (x.to_owned(), LSPDetails::from_scope(var.datatype, var)))
60107
.collect::<HashMap<_, _>>();
61108
}
62109
}
@@ -118,4 +165,49 @@ impl App {
118165
}
119166
Some(scope)
120167
}
168+
169+
fn fallback_builtins(&self, list: Vec<String>) -> HashMap<String, LSPDetails> {
170+
list.into_iter().map(|x| (x, LSPDetails::builtin_fallback())).collect::<HashMap<_, _>>()
171+
}
172+
173+
fn load_builtins(&self) -> HashMap<String, LSPDetails> {
174+
let nixver = process::Command::new("nix").args(&["--version"]).output();
175+
176+
// `nix __dump-builtins` is only supported on `nixUnstable` a.k.a. Nix 2.4.
177+
// Thus, we have to check if this is actually available. If not, `rnix-lsp` will fall
178+
// back to a hard-coded list of builtins which is missing additional info such as documentation
179+
// or parameter names though.
180+
match nixver {
181+
Ok(out) => {
182+
match str::from_utf8(&out.stdout) {
183+
Ok(v) => {
184+
let re = regex::Regex::new(r"^nix \(Nix\) (?P<major>\d)\.(?P<minor>\d).*").unwrap();
185+
let m = re.captures(v).unwrap();
186+
let major = m.name("major").map_or(1, |m| m.as_str().parse::<u8>().unwrap());
187+
let minor = m.name("minor").map_or(1, |m| m.as_str().parse::<u8>().unwrap());
188+
if major == 2 && minor >= 4 || major > 2 {
189+
let builtins_raw = process::Command::new("nix").args(&["__dump-builtins"]).output().unwrap();
190+
let v: serde_json::Value = serde_json::from_str(str::from_utf8(&builtins_raw.stdout).unwrap()).unwrap();
191+
192+
v.as_object().unwrap()
193+
.iter().map(|(x, v)| {
194+
let doc = String::from(v["doc"].as_str().unwrap());
195+
(String::from(x), LSPDetails::builtin_with_doc(
196+
doc.starts_with("**DEPRECATED.**"),
197+
// FIXME make sure that `lib.flip` is taken into account here
198+
v["args"].as_array().map(|x| x.iter().map(|y| y.as_str().unwrap()).collect::<Vec<_>>().join(" -> ")),
199+
doc
200+
))
201+
})
202+
.collect::<HashMap<_, _>>()
203+
} else {
204+
self.fallback_builtins(BUILTINS.to_vec())
205+
}
206+
},
207+
Err(_) => self.fallback_builtins(BUILTINS.to_vec()),
208+
}
209+
},
210+
Err(_) => self.fallback_builtins(BUILTINS.to_vec()),
211+
}
212+
}
121213
}

src/main.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ impl App {
249249
let (name, scope, _) = self.scope_for_ident(params.text_document.uri, &node, offset)?;
250250

251251
let var_e = scope.get(name.as_str())?;
252-
if let (_, Some(var)) = var_e {
252+
if let Some(var) = &var_e.var {
253253
let (_definition_ast, definition_content) = self.files.get(&var.file)?;
254254
Some(Location {
255255
uri: (*var.file).clone(),
@@ -272,15 +272,18 @@ impl App {
272272
let (_, content) = self.files.get(&params.text_document.uri)?;
273273

274274
let mut completions = Vec::new();
275-
for (var, (datatype, _)) in scope {
275+
for (var, data) in scope {
276276
if var.starts_with(&name.as_str()) {
277+
let det = data.render_detail();
277278
completions.push(CompletionItem {
278279
label: var.clone(),
280+
documentation: data.documentation.map(|x| lsp_types::Documentation::String(x)),
281+
deprecated: Some(data.deprecated),
279282
text_edit: Some(TextEdit {
280283
range: utils::range(content, node.node().text_range()),
281284
new_text: var.clone(),
282285
}),
283-
detail: Some(datatype.to_string()),
286+
detail: Some(det),
284287
..CompletionItem::default()
285288
});
286289
}

0 commit comments

Comments
 (0)