Skip to content

Commit

Permalink
Enhance completion
Browse files Browse the repository at this point in the history
  • Loading branch information
oxalica committed Aug 8, 2022
1 parent 9ef631b commit 11a690b
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 18 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions lsp/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/def/liveness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
}
}

Expand Down
154 changes: 138 additions & 16 deletions src/ide/completion.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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
}

0 comments on commit 11a690b

Please sign in to comment.