Enhance completion

This commit is contained in:
oxalica 2022-08-09 07:25:57 +08:00
parent 9ef631bb0c
commit 11a690bb73
6 changed files with 150 additions and 18 deletions

7
Cargo.lock generated
View file

@ -78,6 +78,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c97b9233581d84b8e1e689cdd3a47b6f69770084fc246e86a7f78b0d9c1d4a5"
[[package]]
name = "either"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
[[package]]
name = "env_logger"
version = "0.9.0"
@ -286,6 +292,7 @@ dependencies = [
name = "nil"
version = "0.0.0"
dependencies = [
"either",
"expect-test",
"indexmap",
"la-arena",

View file

@ -5,6 +5,7 @@ edition = "2021"
license = "MIT OR Apache-2.0"
[dependencies]
either = "1.7.0"
indexmap = "1.9.1"
la-arena = "0.2.1"
ordered-float = "3.0.0"

View file

@ -12,7 +12,8 @@ Super fast incremental analysis! Scans `all-packages.nix` in less than 0.1s and
- [x] With expression references.
- [x] Completion. `textDocument/completion`
- [x] Builtin names.
- [x] Local bindings.
- [x] Local bindings and rec-attrset fields.
- [x] Keywords.
- [ ] Attrset fields.
- [x] Diagnostics. `textDocument/publishDiagnostics`
- Syntax errors.

View file

@ -64,6 +64,7 @@ pub(crate) fn completion(
.into_iter()
.map(|item| {
let kind = match item.kind {
CompletionItemKind::Keyword => lsp::CompletionItemKind::KEYWORD,
CompletionItemKind::Param => lsp::CompletionItemKind::VARIABLE,
CompletionItemKind::LetBinding => lsp::CompletionItemKind::VARIABLE,
CompletionItemKind::Field => lsp::CompletionItemKind::FIELD,

View file

@ -114,7 +114,7 @@ impl<'a> Traversal<'a> {
},
// Don't walk LetIn bindings unless referenced.
Expr::LetIn(_, body) => self.queue.push(*body),
e => e.walk_child_exprs(&self.module, |e| self.queue.push(e)),
e => e.walk_child_exprs(self.module, |e| self.queue.push(e)),
}
}

View file

@ -1,10 +1,26 @@
use crate::builtin::BuiltinKind;
use crate::def::{AstPtr, DefDatabase, NameDefKind};
use crate::{builtin, FileId};
use either::Either::{Left, Right};
use rowan::ast::AstNode;
use smol_str::SmolStr;
use syntax::{ast, match_ast, SyntaxKind, TextRange, TextSize, T};
#[rustfmt::skip]
const EXPR_POS_KEYWORDS: &[&str] = &[
"assert",
// "else",
"if",
// "in",
// "inherit",
"let",
"or",
"rec",
// "then",
"with",
];
const ATTR_POS_KEYWORDS: &[&str] = &["inherit"];
/// A single completion variant in the editor pop-up.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompletionItem {
@ -21,6 +37,7 @@ pub struct CompletionItem {
/// The type of the completion item.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CompletionItemKind {
Keyword,
Param,
LetBinding,
Field,
@ -63,22 +80,73 @@ pub(crate) fn completions(
_ => return None,
};
let ref_node = tok.parent_ancestors().find_map(|node| {
let node = tok.parent_ancestors().find_map(|node| {
match_ast! {
match node {
ast::Ref(n) => Some(n),
ast::Ref(n) => Some(Left(n)),
ast::Name(n) => Some(Right(n)),
_ => None,
}
}
})?;
match node {
Left(ref_node) => complete_expr(db, file_id, source_range, ref_node),
Right(name_node) => {
let path_node = ast::Attrpath::cast(name_node.syntax().parent()?)?;
let _entry_node = ast::AttrpathValue::cast(path_node.syntax().parent()?)?;
complete_attrpath_def(db, file_id, source_range, path_node, name_node)
}
}
}
fn complete_expr(
db: &dyn DefDatabase,
file_id: FileId,
source_range: TextRange,
ref_node: ast::Ref,
) -> Option<Vec<CompletionItem>> {
let module = db.module(file_id);
let source_map = db.source_map(file_id);
let expr_id = source_map.node_expr(AstPtr::new(ref_node.syntax()))?;
let scopes = db.scopes(file_id);
let scope_id = scopes.scope_by_expr(expr_id)?;
// TODO: Better sorting.
let mut items = scopes
let prefix = SmolStr::from(ref_node.token()?.text());
let mut items = Vec::new();
let mut feed = |compe: CompletionItem| {
if can_complete(&prefix, &compe.replace) {
items.push(compe);
}
};
// Keywords.
EXPR_POS_KEYWORDS
.iter()
.map(|kw| keyword_to_completion(kw, source_range))
.for_each(&mut feed);
// Contectual keywords.
if ref_node
.syntax()
.ancestors()
.find_map(ast::IfThenElse::cast)
.is_some()
{
feed(keyword_to_completion("then", source_range));
feed(keyword_to_completion("else", source_range));
}
if ref_node
.syntax()
.ancestors()
.find_map(ast::LetIn::cast)
.is_some()
{
feed(keyword_to_completion("in", source_range));
}
// Names in current scopes.
scopes
.ancestors(scope_id)
.filter_map(|scope| scope.as_name_defs())
.flatten()
@ -88,20 +156,74 @@ pub(crate) fn completions(
replace: name.clone(),
kind: module[def].kind.into(),
})
.chain(
builtin::BUILTINS
.values()
.filter(|b| !b.is_hidden)
.map(|b| CompletionItem {
label: b.name.into(),
source_range,
replace: b.name.into(),
kind: b.kind.into(),
}),
)
.collect::<Vec<_>>();
.for_each(&mut feed);
// Global builtins.
builtin::BUILTINS
.values()
.filter(|b| !b.is_hidden)
.map(|b| CompletionItem {
label: b.name.into(),
source_range,
replace: b.name.into(),
kind: b.kind.into(),
})
.for_each(&mut feed);
// TODO: Better sorting.
items.sort_by(|lhs, rhs| lhs.label.cmp(&rhs.label));
items.dedup_by(|lhs, rhs| lhs.label == rhs.label);
Some(items)
}
fn complete_attrpath_def(
_db: &dyn DefDatabase,
_file_id: FileId,
source_range: TextRange,
path_node: ast::Attrpath,
_name_node: ast::Name,
) -> Option<Vec<CompletionItem>> {
if path_node.attrs().count() >= 2 {
return None;
}
let in_let = path_node
.syntax()
.ancestors()
.find_map(ast::LetIn::cast)
.is_some();
Some(
ATTR_POS_KEYWORDS
.iter()
.copied()
.chain(in_let.then_some("in"))
.map(|kw| keyword_to_completion(kw, source_range))
.collect(),
)
}
fn keyword_to_completion(kw: &str, source_range: TextRange) -> CompletionItem {
CompletionItem {
label: kw.into(),
source_range,
replace: kw.into(),
kind: CompletionItemKind::Keyword,
}
}
// Subsequence matching.
fn can_complete(prefix: &str, replace: &str) -> bool {
let mut rest = prefix.as_bytes();
if rest.is_empty() {
return true;
}
for b in replace.bytes() {
if rest.first().unwrap() == &b {
rest = &rest[1..];
if rest.is_empty() {
return true;
}
}
}
false
}