1
1
mirror of https://github.com/oxalica/nil.git synced 2024-11-27 00:06:26 +03:00

Impl completion triggered . and ?

This commit is contained in:
oxalica 2022-11-07 12:58:01 +08:00
parent 2997e14a66
commit 72db1ef2bc
4 changed files with 135 additions and 9 deletions

View File

@ -4,8 +4,9 @@ use builtin::{BuiltinKind, ALL_BUILTINS};
use either::Either::{Left, Right};
use rowan::ast::AstNode;
use smol_str::SmolStr;
use syntax::ast::Attr;
use syntax::semantic::AttrKind;
use syntax::{ast, best_token_at_offset, match_ast, SyntaxKind, TextRange, T};
use syntax::{ast, best_token_at_offset, match_ast, SyntaxKind, SyntaxNode, TextRange, T};
#[rustfmt::skip]
const EXPR_POS_KEYWORDS: &[&str] = &[
@ -74,10 +75,17 @@ impl TryFrom<NameKind> for CompletionItemKind {
pub(crate) fn completions(
db: &dyn TyDatabase,
FilePos { file_id, pos }: FilePos,
fpos @ FilePos { file_id, pos }: FilePos,
trigger_char: Option<char>,
) -> Option<Vec<CompletionItem>> {
let parse = db.parse(file_id);
if let Some(items) =
trigger_char.and_then(|ch| complete_trigger(db, fpos, parse.syntax_node(), ch))
{
return Some(items);
}
let tok = best_token_at_offset(&parse.syntax_node(), pos)?;
let source_range = match tok.kind() {
T![.] => TextRange::empty(pos),
@ -101,6 +109,46 @@ pub(crate) fn completions(
}
}
fn complete_trigger(
db: &dyn TyDatabase,
FilePos { file_id, pos }: FilePos,
root_node: SyntaxNode,
trigger_char: char,
) -> Option<Vec<CompletionItem>> {
if !matches!(trigger_char, '.' | '?') {
return None;
}
let trigger_tok = root_node.token_at_offset(pos).left_biased()?;
let source_range = TextRange::empty(trigger_tok.text_range().end());
let path_node = match_ast! {
match (trigger_tok.parent()?) {
// `foo.bar.|` or `foo?bar.|`
ast::Attrpath(n) => n,
// `foo.|`
ast::Select(n) => n.attrpath()?,
// `foo?|`
ast::HasAttr(n) => n.attrpath()?,
_ => return None,
}
};
// There may be triva between `.` and NAME, due to parser lookahead.
// We skips them to locate the correct node, while keeping `source_range` at the cursor.
// `{ a.| = 42; }`
// ^ \ NAME before here
// DOT is here
let name_node = match path_node
.attrs()
.find(|attr| source_range.start() <= attr.syntax().text_range().start())?
{
Attr::Name(name) => name,
_ => return None,
};
complete_attrpath(db, file_id, source_range, name_node)
}
fn complete_expr(
db: &dyn TyDatabase,
file_id: FileId,
@ -348,15 +396,15 @@ mod tests {
#[track_caller]
fn check_no(fixture: &str, label: &str) {
let (db, f) = TestDB::from_fixture(fixture).unwrap();
if let Some(compes) = super::completions(&db, f[0]) {
if let Some(compes) = super::completions(&db, f[0], None) {
assert_eq!(compes.iter().find(|item| item.label == label), None);
}
}
#[track_caller]
fn check(fixture: &str, label: &str, expect: Expect) {
fn check_trigger(fixture: &str, trigger_char: Option<char>, label: &str, expect: Expect) {
let (db, f) = TestDB::from_fixture(fixture).unwrap();
let compes = super::completions(&db, f[0]).expect("No completion");
let compes = super::completions(&db, f[0], trigger_char).expect("No completion");
let item = compes
.iter()
.find(|item| item.label == label)
@ -370,6 +418,11 @@ mod tests {
expect.assert_eq(&got);
}
#[track_caller]
fn check(fixture: &str, label: &str, expect: Expect) {
check_trigger(fixture, None, label, expect);
}
#[test]
fn keyword() {
check("l$0", "let", expect!["(Keyword) let"]);
@ -457,6 +510,28 @@ mod tests {
);
}
#[test]
fn trigger_select_known_field() {
check_trigger(
"{ foo.bar = 1; }.$0",
Some('.'),
"foo",
expect!["(Field) { foo.bar = 1; }.foo"],
);
check_trigger(
"{ foo.bar = 1; }.$0.bar",
Some('.'),
"foo",
expect!["(Field) { foo.bar = 1; }.foo.bar"],
);
check_trigger(
"{ foo.bar = 1; }.foo.$0",
Some('.'),
"bar",
expect!["(Field) { foo.bar = 1; }.foo.bar"],
);
}
#[test]
fn has_known_field() {
check(
@ -476,6 +551,28 @@ mod tests {
);
}
#[test]
fn trigger_has_known_field() {
check_trigger(
"{ foo.bar = 1; }?$0",
Some('?'),
"foo",
expect!["(Field) { foo.bar = 1; }?foo"],
);
check_trigger(
"{ foo.bar = 1; }?foo.$0",
Some('.'),
"bar",
expect!["(Field) { foo.bar = 1; }?foo.bar"],
);
check_trigger(
"{ foo.bar = 1; }?$0.bar",
Some('.'),
"foo",
expect!["(Field) { foo.bar = 1; }?foo.bar"],
);
}
#[test]
fn define_known_field_let() {
check(
@ -495,6 +592,28 @@ mod tests {
);
}
#[test]
fn trigger_define_known_field_let() {
check_trigger(
"let a.$0 = 1; in a.foo.bar",
Some('.'),
"foo",
expect!["(Field) let a.foo = 1; in a.foo.bar"],
);
check_trigger(
"let a.$0.bar = 1; in a.foo.bar",
Some('.'),
"foo",
expect!["(Field) let a.foo.bar = 1; in a.foo.bar"],
);
check_trigger(
"let a.foo.$0 = 1; in a.foo.bar",
Some('.'),
"bar",
expect!["(Field) let a.foo.bar = 1; in a.foo.bar"],
);
}
#[test]
fn define_known_field_attrset() {
check(

View File

@ -116,8 +116,12 @@ impl Analysis {
self.with_db(|db| goto_definition::goto_definition(db, pos))
}
pub fn completions(&self, pos: FilePos) -> Cancellable<Option<Vec<CompletionItem>>> {
self.with_db(|db| completion::completions(db, pos))
pub fn completions(
&self,
pos: FilePos,
trigger_char: Option<char>,
) -> Cancellable<Option<Vec<CompletionItem>>> {
self.with_db(|db| completion::completions(db, pos, trigger_char))
}
pub fn references(&self, pos: FilePos) -> Cancellable<Option<Vec<FileRange>>> {

View File

@ -20,7 +20,7 @@ pub(crate) fn server_capabilities() -> ServerCapabilities {
)),
definition_provider: Some(OneOf::Left(true)),
completion_provider: Some(CompletionOptions {
trigger_characters: Some(vec![".".into()]),
trigger_characters: Some(vec![".".into(), "?".into()]),
..Default::default()
}),
references_provider: Some(OneOf::Left(true)),

View File

@ -80,7 +80,10 @@ pub(crate) fn completion(
params: CompletionParams,
) -> Result<Option<CompletionResponse>> {
let (line_map, fpos) = convert::from_file_pos(&snap.vfs(), &params.text_document_position)?;
let items = match snap.analysis.completions(fpos)? {
let trigger_char = params
.context
.and_then(|ctx| ctx.trigger_character?.chars().next());
let items = match snap.analysis.completions(fpos, trigger_char)? {
None => return Ok(None),
Some(items) => items,
};