organize completion tests better

This commit is contained in:
Aleksey Kladov 2018-12-21 18:13:21 +03:00
parent d4ef07b235
commit 45232dfa68
5 changed files with 488 additions and 393 deletions

View file

@ -6,6 +6,8 @@ pub struct CompletionItem {
label: String,
lookup: Option<String>,
snippet: Option<String>,
/// Used only internally in test, to check only specific kind of completion.
kind: CompletionKind,
}
pub enum InsertText {
@ -13,6 +15,18 @@ pub enum InsertText {
Snippet { text: String },
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum CompletionKind {
/// Parser-based keyword completion.
Keyword,
/// Your usual "complete all valid identifiers".
Reference,
/// "Secret sauce" completions.
Magic,
Snippet,
Unspecified,
}
impl CompletionItem {
pub(crate) fn new(label: impl Into<String>) -> Builder {
let label = label.into();
@ -20,6 +34,7 @@ impl CompletionItem {
label,
lookup: None,
snippet: None,
kind: CompletionKind::Unspecified,
}
}
/// What user sees in pop-up in the UI.
@ -50,28 +65,34 @@ pub(crate) struct Builder {
label: String,
lookup: Option<String>,
snippet: Option<String>,
kind: CompletionKind,
}
impl Builder {
pub fn add_to(self, acc: &mut Completions) {
pub(crate) fn add_to(self, acc: &mut Completions) {
acc.add(self.build())
}
pub fn build(self) -> CompletionItem {
pub(crate) fn build(self) -> CompletionItem {
CompletionItem {
label: self.label,
lookup: self.lookup,
snippet: self.snippet,
kind: self.kind,
}
}
pub fn lookup_by(mut self, lookup: impl Into<String>) -> Builder {
pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder {
self.lookup = Some(lookup.into());
self
}
pub fn snippet(mut self, snippet: impl Into<String>) -> Builder {
pub(crate) fn snippet(mut self, snippet: impl Into<String>) -> Builder {
self.snippet = Some(snippet.into());
self
}
pub(crate) fn kind(mut self, kind: CompletionKind) -> Builder {
self.kind = kind;
self
}
}
impl Into<CompletionItem> for Builder {
@ -97,6 +118,57 @@ impl Completions {
{
items.into_iter().for_each(|item| self.add(item.into()))
}
#[cfg(test)]
pub(crate) fn assert_match(&self, expected: &str, kind: CompletionKind) {
let expected = normalize(expected);
let actual = self.debug_render(kind);
test_utils::assert_eq_text!(expected.as_str(), actual.as_str(),);
/// Normalize the textual representation of `Completions`:
/// replace `;` with newlines, normalize whitespace
fn normalize(expected: &str) -> String {
use ra_syntax::{tokenize, TextUnit, TextRange, SyntaxKind::SEMI};
let mut res = String::new();
for line in expected.trim().lines() {
let line = line.trim();
let mut start_offset: TextUnit = 0.into();
// Yep, we use rust tokenize in completion tests :-)
for token in tokenize(line) {
let range = TextRange::offset_len(start_offset, token.len);
start_offset += token.len;
if token.kind == SEMI {
res.push('\n');
} else {
res.push_str(&line[range]);
}
}
res.push('\n');
}
res
}
}
#[cfg(test)]
fn debug_render(&self, kind: CompletionKind) -> String {
let mut res = String::new();
for c in self.buf.iter() {
if c.kind == kind {
if let Some(lookup) = &c.lookup {
res.push_str(lookup);
res.push_str(&format!(" {:?}", c.label));
} else {
res.push_str(&c.label);
}
if let Some(snippet) = &c.snippet {
res.push_str(&format!(" {:?}", snippet));
}
res.push('\n');
}
}
res
}
}
impl Into<Vec<CompletionItem>> for Completions {

View file

@ -13,7 +13,7 @@ use hir::{
use crate::{
db::RootDatabase,
completion::{CompletionItem, Completions},
completion::{CompletionItem, Completions, CompletionKind::*},
Cancelable
};
@ -51,7 +51,11 @@ pub(super) fn completions(
}
}
})
.for_each(|(name, _res)| CompletionItem::new(name.to_string()).add_to(acc));
.for_each(|(name, _res)| {
CompletionItem::new(name.to_string())
.kind(Reference)
.add_to(acc)
});
}
NameRefKind::Path(path) => complete_path(acc, db, module, path)?,
NameRefKind::BareIdentInMod => {
@ -123,9 +127,13 @@ fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Completions)
.scope_chain(name_ref.syntax())
.flat_map(|scope| scopes.entries(scope).iter())
.filter(|entry| shadowed.insert(entry.name()))
.for_each(|entry| CompletionItem::new(entry.name().to_string()).add_to(acc));
.for_each(|entry| {
CompletionItem::new(entry.name().to_string())
.kind(Reference)
.add_to(acc)
});
if scopes.self_param.is_some() {
CompletionItem::new("self").add_to(acc);
CompletionItem::new("self").kind(Reference).add_to(acc);
}
}
@ -148,9 +156,11 @@ fn complete_path(
_ => return Ok(()),
};
let module_scope = target_module.scope(db)?;
module_scope
.entries()
.for_each(|(name, _res)| CompletionItem::new(name.to_string()).add_to(acc));
module_scope.entries().for_each(|(name, _res)| {
CompletionItem::new(name.to_string())
.kind(Reference)
.add_to(acc)
});
Ok(())
}
@ -164,9 +174,11 @@ fn ${1:feature}() {
$0
}",
)
.kind(Snippet)
.add_to(acc);
CompletionItem::new("pub(crate)")
.snippet("pub(crate) $0")
.kind(Snippet)
.add_to(acc);
}
@ -249,14 +261,362 @@ fn complete_return(fn_def: ast::FnDef, name_ref: ast::NameRef) -> Option<Complet
}
fn keyword(kw: &str, snippet: &str) -> CompletionItem {
CompletionItem::new(kw).snippet(snippet).build()
CompletionItem::new(kw)
.kind(Keyword)
.snippet(snippet)
.build()
}
fn complete_expr_snippets(acc: &mut Completions) {
CompletionItem::new("pd")
.snippet("eprintln!(\"$0 = {:?}\", $0);")
.kind(Snippet)
.add_to(acc);
CompletionItem::new("ppd")
.snippet("eprintln!(\"$0 = {:#?}\", $0);")
.kind(Snippet)
.add_to(acc);
}
#[cfg(test)]
mod tests {
use crate::completion::{CompletionKind, check_completion};
fn check_reference_completion(code: &str, expected_completions: &str) {
check_completion(code, expected_completions, CompletionKind::Reference);
}
fn check_keyword_completion(code: &str, expected_completions: &str) {
check_completion(code, expected_completions, CompletionKind::Keyword);
}
fn check_snippet_completion(code: &str, expected_completions: &str) {
check_completion(code, expected_completions, CompletionKind::Snippet);
}
#[test]
fn test_completion_let_scope() {
check_reference_completion(
r"
fn quux(x: i32) {
let y = 92;
1 + <|>;
let z = ();
}
",
"y;x;quux",
);
}
#[test]
fn test_completion_if_let_scope() {
check_reference_completion(
r"
fn quux() {
if let Some(x) = foo() {
let y = 92;
};
if let Some(a) = bar() {
let b = 62;
1 + <|>
}
}
",
"b;a;quux",
);
}
#[test]
fn test_completion_for_scope() {
check_reference_completion(
r"
fn quux() {
for x in &[1, 2, 3] {
<|>
}
}
",
"x;quux",
);
}
#[test]
fn test_completion_mod_scope() {
check_reference_completion(
r"
struct Foo;
enum Baz {}
fn quux() {
<|>
}
",
"quux;Foo;Baz",
);
}
#[test]
fn test_completion_mod_scope_no_self_use() {
check_reference_completion(
r"
use foo<|>;
",
"",
);
}
#[test]
fn test_completion_self_path() {
check_reference_completion(
r"
use self::m::<|>;
mod m {
struct Bar;
}
",
"Bar",
);
}
#[test]
fn test_completion_mod_scope_nested() {
check_reference_completion(
r"
struct Foo;
mod m {
struct Bar;
fn quux() { <|> }
}
",
"quux;Bar",
);
}
#[test]
fn test_complete_type() {
check_reference_completion(
r"
struct Foo;
fn x() -> <|>
",
"Foo;x",
)
}
#[test]
fn test_complete_shadowing() {
check_reference_completion(
r"
fn foo() -> {
let bar = 92;
{
let bar = 62;
<|>
}
}
",
"bar;foo",
)
}
#[test]
fn test_complete_self() {
check_reference_completion(r"impl S { fn foo(&self) { <|> } }", "self")
}
#[test]
fn test_complete_crate_path() {
check_reference_completion(
"
//- /lib.rs
mod foo;
struct Spam;
//- /foo.rs
use crate::Sp<|>
",
"Spam;foo",
);
}
#[test]
fn test_complete_crate_path_with_braces() {
check_reference_completion(
"
//- /lib.rs
mod foo;
struct Spam;
//- /foo.rs
use crate::{Sp<|>};
",
"Spam;foo",
);
}
#[test]
fn test_complete_crate_path_in_nested_tree() {
check_reference_completion(
"
//- /lib.rs
mod foo;
pub mod bar {
pub mod baz {
pub struct Spam;
}
}
//- /foo.rs
use crate::{bar::{baz::Sp<|>}};
",
"Spam",
);
}
#[test]
fn test_completion_kewords() {
check_keyword_completion(
r"
fn quux() {
<|>
}
",
r#"
if "if $0 {}"
match "match $0 {}"
while "while $0 {}"
loop "loop {$0}"
return "return"
"#,
);
}
#[test]
fn test_completion_else() {
check_keyword_completion(
r"
fn quux() {
if true {
()
} <|>
}
",
r#"
if "if $0 {}"
match "match $0 {}"
while "while $0 {}"
loop "loop {$0}"
else "else {$0}"
else if "else if $0 {}"
return "return"
"#,
);
}
#[test]
fn test_completion_return_value() {
check_keyword_completion(
r"
fn quux() -> i32 {
<|>
92
}
",
r#"
if "if $0 {}"
match "match $0 {}"
while "while $0 {}"
loop "loop {$0}"
return "return $0;"
"#,
);
check_keyword_completion(
r"
fn quux() {
<|>
92
}
",
r#"
if "if $0 {}"
match "match $0 {}"
while "while $0 {}"
loop "loop {$0}"
return "return;"
"#,
);
}
#[test]
fn test_completion_return_no_stmt() {
check_keyword_completion(
r"
fn quux() -> i32 {
match () {
() => <|>
}
}
",
r#"
if "if $0 {}"
match "match $0 {}"
while "while $0 {}"
loop "loop {$0}"
return "return $0"
"#,
);
}
#[test]
fn test_continue_break_completion() {
check_keyword_completion(
r"
fn quux() -> i32 {
loop { <|> }
}
",
r#"
if "if $0 {}"
match "match $0 {}"
while "while $0 {}"
loop "loop {$0}"
continue "continue"
break "break"
return "return $0"
"#,
);
check_keyword_completion(
r"
fn quux() -> i32 {
loop { || { <|> } }
}
",
r#"
if "if $0 {}"
match "match $0 {}"
while "while $0 {}"
loop "loop {$0}"
return "return $0"
"#,
);
}
#[test]
fn test_item_snippets() {
// check_snippet_completion(r"
// <|>
// ",
// r##"[CompletionItem { label: "Test function", lookup: None, snippet: Some("#[test]\nfn test_${1:feature}() {\n$0\n}"##,
// );
check_snippet_completion(
r"
#[cfg(test)]
mod tests {
<|>
}
",
r##"
tfn "Test function" "#[test]\nfn ${1:feature}() {\n $0\n}"
pub(crate) "pub(crate) $0"
"##,
);
}
}