From 5b573deb20b15451788dd2861e9fc6e69ed0472e Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 8 Jan 2019 22:33:36 +0300 Subject: fix usages after rename --- crates/ra_ide_api/src/completion/complete_dot.rs | 121 ++++++++ .../ra_ide_api/src/completion/complete_fn_param.rs | 102 +++++++ .../ra_ide_api/src/completion/complete_keyword.rs | 339 +++++++++++++++++++++ crates/ra_ide_api/src/completion/complete_path.rs | 128 ++++++++ crates/ra_ide_api/src/completion/complete_scope.rs | 192 ++++++++++++ .../ra_ide_api/src/completion/complete_snippet.rs | 73 +++++ .../src/completion/completion_context.rs | 205 +++++++++++++ .../ra_ide_api/src/completion/completion_item.rs | 244 +++++++++++++++ 8 files changed, 1404 insertions(+) create mode 100644 crates/ra_ide_api/src/completion/complete_dot.rs create mode 100644 crates/ra_ide_api/src/completion/complete_fn_param.rs create mode 100644 crates/ra_ide_api/src/completion/complete_keyword.rs create mode 100644 crates/ra_ide_api/src/completion/complete_path.rs create mode 100644 crates/ra_ide_api/src/completion/complete_scope.rs create mode 100644 crates/ra_ide_api/src/completion/complete_snippet.rs create mode 100644 crates/ra_ide_api/src/completion/completion_context.rs create mode 100644 crates/ra_ide_api/src/completion/completion_item.rs (limited to 'crates/ra_ide_api/src/completion') diff --git a/crates/ra_ide_api/src/completion/complete_dot.rs b/crates/ra_ide_api/src/completion/complete_dot.rs new file mode 100644 index 000000000..5d4e60dc5 --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_dot.rs @@ -0,0 +1,121 @@ +use hir::{Ty, Def}; + +use crate::Cancelable; +use crate::completion::{CompletionContext, Completions, CompletionKind, CompletionItem, CompletionItemKind}; + +/// Complete dot accesses, i.e. fields or methods (currently only fields). +pub(super) fn complete_dot(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { + let (function, receiver) = match (&ctx.function, ctx.dot_receiver) { + (Some(function), Some(receiver)) => (function, receiver), + _ => return Ok(()), + }; + let infer_result = function.infer(ctx.db)?; + let syntax_mapping = function.body_syntax_mapping(ctx.db)?; + let expr = match syntax_mapping.node_expr(receiver) { + Some(expr) => expr, + None => return Ok(()), + }; + let receiver_ty = infer_result[expr].clone(); + if !ctx.is_method_call { + complete_fields(acc, ctx, receiver_ty)?; + } + Ok(()) +} + +fn complete_fields(acc: &mut Completions, ctx: &CompletionContext, receiver: Ty) -> Cancelable<()> { + for receiver in receiver.autoderef(ctx.db) { + match receiver { + Ty::Adt { def_id, .. } => { + match def_id.resolve(ctx.db)? { + Def::Struct(s) => { + let variant_data = s.variant_data(ctx.db)?; + for field in variant_data.fields() { + CompletionItem::new( + CompletionKind::Reference, + field.name().to_string(), + ) + .kind(CompletionItemKind::Field) + .add_to(acc); + } + } + // TODO unions + _ => {} + } + } + Ty::Tuple(fields) => { + for (i, _ty) in fields.iter().enumerate() { + CompletionItem::new(CompletionKind::Reference, i.to_string()) + .kind(CompletionItemKind::Field) + .add_to(acc); + } + } + _ => {} + }; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::completion::*; + + fn check_ref_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Reference); + } + + #[test] + fn test_struct_field_completion() { + check_ref_completion( + r" + struct A { the_field: u32 } + fn foo(a: A) { + a.<|> + } + ", + r#"the_field"#, + ); + } + + #[test] + fn test_struct_field_completion_self() { + check_ref_completion( + r" + struct A { the_field: u32 } + impl A { + fn foo(self) { + self.<|> + } + } + ", + r#"the_field"#, + ); + } + + #[test] + fn test_struct_field_completion_autoderef() { + check_ref_completion( + r" + struct A { the_field: u32 } + impl A { + fn foo(&self) { + self.<|> + } + } + ", + r#"the_field"#, + ); + } + + #[test] + fn test_no_struct_field_completion_for_method_call() { + check_ref_completion( + r" + struct A { the_field: u32 } + fn foo(a: A) { + a.<|>() + } + ", + r#""#, + ); + } +} diff --git a/crates/ra_ide_api/src/completion/complete_fn_param.rs b/crates/ra_ide_api/src/completion/complete_fn_param.rs new file mode 100644 index 000000000..c1739e47e --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_fn_param.rs @@ -0,0 +1,102 @@ +use ra_syntax::{ + algo::visit::{visitor_ctx, VisitorCtx}, + ast, + AstNode, +}; +use rustc_hash::FxHashMap; + +use crate::completion::{CompletionContext, Completions, CompletionKind, CompletionItem}; + +/// Complete repeated parametes, both name and type. For example, if all +/// functions in a file have a `spam: &mut Spam` parameter, a completion with +/// `spam: &mut Spam` insert text/label and `spam` lookup string will be +/// suggested. +pub(super) fn complete_fn_param(acc: &mut Completions, ctx: &CompletionContext) { + if !ctx.is_param { + return; + } + + let mut params = FxHashMap::default(); + for node in ctx.leaf.ancestors() { + let _ = visitor_ctx(&mut params) + .visit::(process) + .visit::(process) + .accept(node); + } + params + .into_iter() + .filter_map(|(label, (count, param))| { + let lookup = param.pat()?.syntax().text().to_string(); + if count < 2 { + None + } else { + Some((label, lookup)) + } + }) + .for_each(|(label, lookup)| { + CompletionItem::new(CompletionKind::Magic, label) + .lookup_by(lookup) + .add_to(acc) + }); + + fn process<'a, N: ast::FnDefOwner>( + node: &'a N, + params: &mut FxHashMap, + ) { + node.functions() + .filter_map(|it| it.param_list()) + .flat_map(|it| it.params()) + .for_each(|param| { + let text = param.syntax().text().to_string(); + params.entry(text).or_insert((0, param)).0 += 1; + }) + } +} + +#[cfg(test)] +mod tests { + use crate::completion::*; + + fn check_magic_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Magic); + } + + #[test] + fn test_param_completion_last_param() { + check_magic_completion( + r" + fn foo(file_id: FileId) {} + fn bar(file_id: FileId) {} + fn baz(file<|>) {} + ", + r#"file_id "file_id: FileId""#, + ); + } + + #[test] + fn test_param_completion_nth_param() { + check_magic_completion( + r" + fn foo(file_id: FileId) {} + fn bar(file_id: FileId) {} + fn baz(file<|>, x: i32) {} + ", + r#"file_id "file_id: FileId""#, + ); + } + + #[test] + fn test_param_completion_trait_param() { + check_magic_completion( + r" + pub(crate) trait SourceRoot { + pub fn contains(&self, file_id: FileId) -> bool; + pub fn module_map(&self) -> &ModuleMap; + pub fn lines(&self, file_id: FileId) -> &LineIndex; + pub fn syntax(&self, file<|>) + } + ", + r#"file_id "file_id: FileId""#, + ); + } +} diff --git a/crates/ra_ide_api/src/completion/complete_keyword.rs b/crates/ra_ide_api/src/completion/complete_keyword.rs new file mode 100644 index 000000000..d350f06ce --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_keyword.rs @@ -0,0 +1,339 @@ +use ra_syntax::{ + algo::visit::{visitor, Visitor}, + AstNode, + ast::{self, LoopBodyOwner}, + SyntaxKind::*, SyntaxNode, +}; + +use crate::completion::{CompletionContext, CompletionItem, Completions, CompletionKind, CompletionItemKind}; + +pub(super) fn complete_use_tree_keyword(acc: &mut Completions, ctx: &CompletionContext) { + // complete keyword "crate" in use stmt + match (ctx.use_item_syntax.as_ref(), ctx.path_prefix.as_ref()) { + (Some(_), None) => { + CompletionItem::new(CompletionKind::Keyword, "crate") + .kind(CompletionItemKind::Keyword) + .lookup_by("crate") + .snippet("crate::") + .add_to(acc); + CompletionItem::new(CompletionKind::Keyword, "self") + .kind(CompletionItemKind::Keyword) + .lookup_by("self") + .add_to(acc); + CompletionItem::new(CompletionKind::Keyword, "super") + .kind(CompletionItemKind::Keyword) + .lookup_by("super") + .add_to(acc); + } + (Some(_), Some(_)) => { + CompletionItem::new(CompletionKind::Keyword, "self") + .kind(CompletionItemKind::Keyword) + .lookup_by("self") + .add_to(acc); + CompletionItem::new(CompletionKind::Keyword, "super") + .kind(CompletionItemKind::Keyword) + .lookup_by("super") + .add_to(acc); + } + _ => {} + } +} + +fn keyword(kw: &str, snippet: &str) -> CompletionItem { + CompletionItem::new(CompletionKind::Keyword, kw) + .kind(CompletionItemKind::Keyword) + .snippet(snippet) + .build() +} + +pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &CompletionContext) { + if !ctx.is_trivial_path { + return; + } + + let fn_def = match ctx.function_syntax { + Some(it) => it, + None => return, + }; + acc.add(keyword("if", "if $0 {}")); + acc.add(keyword("match", "match $0 {}")); + acc.add(keyword("while", "while $0 {}")); + acc.add(keyword("loop", "loop {$0}")); + + if ctx.after_if { + acc.add(keyword("else", "else {$0}")); + acc.add(keyword("else if", "else if $0 {}")); + } + if is_in_loop_body(ctx.leaf) { + if ctx.can_be_stmt { + acc.add(keyword("continue", "continue;")); + acc.add(keyword("break", "break;")); + } else { + acc.add(keyword("continue", "continue")); + acc.add(keyword("break", "break")); + } + } + acc.add_all(complete_return(fn_def, ctx.can_be_stmt)); +} + +fn is_in_loop_body(leaf: &SyntaxNode) -> bool { + for node in leaf.ancestors() { + if node.kind() == FN_DEF || node.kind() == LAMBDA_EXPR { + break; + } + let loop_body = visitor() + .visit::(LoopBodyOwner::loop_body) + .visit::(LoopBodyOwner::loop_body) + .visit::(LoopBodyOwner::loop_body) + .accept(node); + if let Some(Some(body)) = loop_body { + if leaf.range().is_subrange(&body.syntax().range()) { + return true; + } + } + } + false +} + +fn complete_return(fn_def: &ast::FnDef, can_be_stmt: bool) -> Option { + let snip = match (can_be_stmt, fn_def.ret_type().is_some()) { + (true, true) => "return $0;", + (true, false) => "return;", + (false, true) => "return $0", + (false, false) => "return", + }; + Some(keyword("return", snip)) +} + +#[cfg(test)] +mod tests { + use crate::completion::{CompletionKind, check_completion}; + fn check_keyword_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Keyword); + } + + #[test] + fn completes_keywords_in_use_stmt() { + check_keyword_completion( + r" + use <|> + ", + r#" + crate "crate" "crate::" + self "self" + super "super" + "#, + ); + + check_keyword_completion( + r" + use a::<|> + ", + r#" + self "self" + super "super" + "#, + ); + + check_keyword_completion( + r" + use a::{b, <|>} + ", + r#" + self "self" + super "super" + "#, + ); + } + + #[test] + fn completes_various_keywords_in_function() { + check_keyword_completion( + r" + fn quux() { + <|> + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return;" + "#, + ); + } + + #[test] + fn completes_else_after_if() { + 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 dont_add_semi_after_return_if_not_a_statement() { + 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 last_return_in_block_has_semi() { + check_keyword_completion( + r" + fn quux() -> i32 { + if condition { + <|> + } + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return $0;" + "#, + ); + check_keyword_completion( + r" + fn quux() -> i32 { + if condition { + <|> + } + let x = 92; + x + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return $0;" + "#, + ); + } + + #[test] + fn completes_break_and_continue_in_loops() { + 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;" + "#, + ); + // No completion: lambda isolates control flow + 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 no_semi_after_break_continue_in_expr() { + check_keyword_completion( + r" + fn f() { + loop { + match () { + () => br<|> + } + } + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + continue "continue" + break "break" + return "return" + "#, + ) + } +} diff --git a/crates/ra_ide_api/src/completion/complete_path.rs b/crates/ra_ide_api/src/completion/complete_path.rs new file mode 100644 index 000000000..4723a65a6 --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_path.rs @@ -0,0 +1,128 @@ +use crate::{ + Cancelable, + completion::{CompletionItem, CompletionItemKind, Completions, CompletionKind, CompletionContext}, +}; + +pub(super) fn complete_path(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { + let (path, module) = match (&ctx.path_prefix, &ctx.module) { + (Some(path), Some(module)) => (path.clone(), module), + _ => return Ok(()), + }; + let def_id = match module.resolve_path(ctx.db, &path)?.take_types() { + Some(it) => it, + None => return Ok(()), + }; + match def_id.resolve(ctx.db)? { + hir::Def::Module(module) => { + let module_scope = module.scope(ctx.db)?; + module_scope.entries().for_each(|(name, res)| { + CompletionItem::new(CompletionKind::Reference, name.to_string()) + .from_resolution(ctx, res) + .add_to(acc) + }); + } + hir::Def::Enum(e) => e + .variants(ctx.db)? + .into_iter() + .for_each(|(name, _variant)| { + CompletionItem::new(CompletionKind::Reference, name.to_string()) + .kind(CompletionItemKind::EnumVariant) + .add_to(acc) + }), + _ => return Ok(()), + }; + Ok(()) +} + +#[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); + } + + #[test] + fn completes_use_item_starting_with_self() { + check_reference_completion( + r" + use self::m::<|>; + + mod m { + struct Bar; + } + ", + "Bar", + ); + } + + #[test] + fn completes_use_item_starting_with_crate() { + check_reference_completion( + " + //- /lib.rs + mod foo; + struct Spam; + //- /foo.rs + use crate::Sp<|> + ", + "Spam;foo", + ); + } + + #[test] + fn completes_nested_use_tree() { + check_reference_completion( + " + //- /lib.rs + mod foo; + struct Spam; + //- /foo.rs + use crate::{Sp<|>}; + ", + "Spam;foo", + ); + } + + #[test] + fn completes_deeply_nested_use_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 completes_enum_variant() { + check_reference_completion( + " + //- /lib.rs + enum E { Foo, Bar(i32) } + fn foo() { let _ = E::<|> } + ", + "Foo;Bar", + ); + } + + #[test] + fn dont_render_function_parens_in_use_item() { + check_reference_completion( + " + //- /lib.rs + mod m { pub fn foo() {} } + use crate::m::f<|>; + ", + "foo", + ) + } +} diff --git a/crates/ra_ide_api/src/completion/complete_scope.rs b/crates/ra_ide_api/src/completion/complete_scope.rs new file mode 100644 index 000000000..ee9052d3d --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_scope.rs @@ -0,0 +1,192 @@ +use rustc_hash::FxHashSet; +use ra_syntax::TextUnit; + +use crate::{ + Cancelable, + completion::{CompletionItem, CompletionItemKind, Completions, CompletionKind, CompletionContext}, +}; + +pub(super) fn complete_scope(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { + if !ctx.is_trivial_path { + return Ok(()); + } + let module = match &ctx.module { + Some(it) => it, + None => return Ok(()), + }; + if let Some(function) = &ctx.function { + let scopes = function.scopes(ctx.db)?; + complete_fn(acc, &scopes, ctx.offset); + } + + let module_scope = module.scope(ctx.db)?; + let (file_id, _) = module.defenition_source(ctx.db)?; + module_scope + .entries() + .filter(|(_name, res)| { + // Don't expose this item + // FIXME: this penetrates through all kinds of abstractions, + // we need to figura out the way to do it less ugly. + match res.import { + None => true, + Some(import) => { + let range = import.range(ctx.db, file_id); + !range.is_subrange(&ctx.leaf.range()) + } + } + }) + .for_each(|(name, res)| { + CompletionItem::new(CompletionKind::Reference, name.to_string()) + .from_resolution(ctx, res) + .add_to(acc) + }); + Ok(()) +} + +fn complete_fn(acc: &mut Completions, scopes: &hir::ScopesWithSyntaxMapping, offset: TextUnit) { + let mut shadowed = FxHashSet::default(); + scopes + .scope_chain_for_offset(offset) + .flat_map(|scope| scopes.scopes.entries(scope).iter()) + .filter(|entry| shadowed.insert(entry.name())) + .for_each(|entry| { + CompletionItem::new(CompletionKind::Reference, entry.name().to_string()) + .kind(CompletionItemKind::Binding) + .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); + } + + #[test] + fn completes_bindings_from_let() { + check_reference_completion( + r" + fn quux(x: i32) { + let y = 92; + 1 + <|>; + let z = (); + } + ", + r#"y;x;quux "quux($0)""#, + ); + } + + #[test] + fn completes_bindings_from_if_let() { + check_reference_completion( + r" + fn quux() { + if let Some(x) = foo() { + let y = 92; + }; + if let Some(a) = bar() { + let b = 62; + 1 + <|> + } + } + ", + r#"b;a;quux "quux()$0""#, + ); + } + + #[test] + fn completes_bindings_from_for() { + check_reference_completion( + r" + fn quux() { + for x in &[1, 2, 3] { + <|> + } + } + ", + r#"x;quux "quux()$0""#, + ); + } + + #[test] + fn completes_module_items() { + check_reference_completion( + r" + struct Foo; + enum Baz {} + fn quux() { + <|> + } + ", + r#"quux "quux()$0";Foo;Baz"#, + ); + } + + #[test] + fn completes_module_items_in_nested_modules() { + check_reference_completion( + r" + struct Foo; + mod m { + struct Bar; + fn quux() { <|> } + } + ", + r#"quux "quux()$0";Bar"#, + ); + } + + #[test] + fn completes_return_type() { + check_reference_completion( + r" + struct Foo; + fn x() -> <|> + ", + r#"Foo;x "x()$0""#, + ) + } + + #[test] + fn dont_show_both_completions_for_shadowing() { + check_reference_completion( + r" + fn foo() -> { + let bar = 92; + { + let bar = 62; + <|> + } + } + ", + r#"bar;foo "foo()$0""#, + ) + } + + #[test] + fn completes_self_in_methods() { + check_reference_completion(r"impl S { fn foo(&self) { <|> } }", "self") + } + + #[test] + fn inserts_parens_for_function_calls() { + check_reference_completion( + r" + fn no_args() {} + fn main() { no_<|> } + ", + r#"no_args "no_args()$0" + main "main()$0""#, + ); + check_reference_completion( + r" + fn with_args(x: i32, y: String) {} + fn main() { with_<|> } + ", + r#"main "main()$0" + with_args "with_args($0)""#, + ); + } +} diff --git a/crates/ra_ide_api/src/completion/complete_snippet.rs b/crates/ra_ide_api/src/completion/complete_snippet.rs new file mode 100644 index 000000000..a495751dd --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_snippet.rs @@ -0,0 +1,73 @@ +use crate::completion::{CompletionItem, Completions, CompletionKind, CompletionItemKind, CompletionContext, completion_item::Builder}; + +fn snippet(label: &str, snippet: &str) -> Builder { + CompletionItem::new(CompletionKind::Snippet, label) + .snippet(snippet) + .kind(CompletionItemKind::Snippet) +} + +pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &CompletionContext) { + if !(ctx.is_trivial_path && ctx.function_syntax.is_some()) { + return; + } + snippet("pd", "eprintln!(\"$0 = {:?}\", $0);").add_to(acc); + snippet("ppd", "eprintln!(\"$0 = {:#?}\", $0);").add_to(acc); +} + +pub(super) fn complete_item_snippet(acc: &mut Completions, ctx: &CompletionContext) { + if !ctx.is_new_item { + return; + } + snippet( + "Test function", + "\ +#[test] +fn ${1:feature}() { + $0 +}", + ) + .lookup_by("tfn") + .add_to(acc); + + snippet("pub(crate)", "pub(crate) $0").add_to(acc); +} + +#[cfg(test)] +mod tests { + use crate::completion::{CompletionKind, check_completion}; + fn check_snippet_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Snippet); + } + + #[test] + fn completes_snippets_in_expressions() { + check_snippet_completion( + r"fn foo(x: i32) { <|> }", + r##" + pd "eprintln!(\"$0 = {:?}\", $0);" + ppd "eprintln!(\"$0 = {:#?}\", $0);" + "##, + ); + } + + #[test] + fn completes_snippets_in_items() { + // 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" + "##, + ); + } +} diff --git a/crates/ra_ide_api/src/completion/completion_context.rs b/crates/ra_ide_api/src/completion/completion_context.rs new file mode 100644 index 000000000..01786bb69 --- /dev/null +++ b/crates/ra_ide_api/src/completion/completion_context.rs @@ -0,0 +1,205 @@ +use ra_text_edit::AtomTextEdit; +use ra_syntax::{ + AstNode, SyntaxNode, SourceFile, TextUnit, TextRange, + ast, + algo::{find_leaf_at_offset, find_covering_node, find_node_at_offset}, + SyntaxKind::*, +}; +use hir::source_binder; + +use crate::{db, FilePosition, Cancelable}; + +/// `CompletionContext` is created early during completion to figure out, where +/// exactly is the cursor, syntax-wise. +#[derive(Debug)] +pub(super) struct CompletionContext<'a> { + pub(super) db: &'a db::RootDatabase, + pub(super) offset: TextUnit, + pub(super) leaf: &'a SyntaxNode, + pub(super) module: Option, + pub(super) function: Option, + pub(super) function_syntax: Option<&'a ast::FnDef>, + pub(super) use_item_syntax: Option<&'a ast::UseItem>, + pub(super) is_param: bool, + /// A single-indent path, like `foo`. + pub(super) is_trivial_path: bool, + /// If not a trivial, path, the prefix (qualifier). + pub(super) path_prefix: Option, + pub(super) after_if: bool, + /// `true` if we are a statement or a last expr in the block. + pub(super) can_be_stmt: bool, + /// Something is typed at the "top" level, in module or impl/trait. + pub(super) is_new_item: bool, + /// The receiver if this is a field or method access, i.e. writing something.<|> + pub(super) dot_receiver: Option<&'a ast::Expr>, + /// If this is a method call in particular, i.e. the () are already there. + pub(super) is_method_call: bool, +} + +impl<'a> CompletionContext<'a> { + pub(super) fn new( + db: &'a db::RootDatabase, + original_file: &'a SourceFile, + position: FilePosition, + ) -> Cancelable>> { + let module = source_binder::module_from_position(db, position)?; + let leaf = + ctry!(find_leaf_at_offset(original_file.syntax(), position.offset).left_biased()); + let mut ctx = CompletionContext { + db, + leaf, + offset: position.offset, + module, + function: None, + function_syntax: None, + use_item_syntax: None, + is_param: false, + is_trivial_path: false, + path_prefix: None, + after_if: false, + can_be_stmt: false, + is_new_item: false, + dot_receiver: None, + is_method_call: false, + }; + ctx.fill(original_file, position.offset); + Ok(Some(ctx)) + } + + fn fill(&mut self, original_file: &'a SourceFile, offset: TextUnit) { + // Insert a fake ident to get a valid parse tree. We will use this file + // to determine context, though the original_file will be used for + // actual completion. + let file = { + let edit = AtomTextEdit::insert(offset, "intellijRulezz".to_string()); + original_file.reparse(&edit) + }; + + // First, let's try to complete a reference to some declaration. + if let Some(name_ref) = find_node_at_offset::(file.syntax(), offset) { + // Special case, `trait T { fn foo(i_am_a_name_ref) {} }`. + // See RFC#1685. + if is_node::(name_ref.syntax()) { + self.is_param = true; + return; + } + self.classify_name_ref(original_file, name_ref); + } + + // Otherwise, see if this is a declaration. We can use heuristics to + // suggest declaration names, see `CompletionKind::Magic`. + if let Some(name) = find_node_at_offset::(file.syntax(), offset) { + if is_node::(name.syntax()) { + self.is_param = true; + return; + } + } + } + fn classify_name_ref(&mut self, original_file: &'a SourceFile, name_ref: &ast::NameRef) { + let name_range = name_ref.syntax().range(); + let top_node = name_ref + .syntax() + .ancestors() + .take_while(|it| it.range() == name_range) + .last() + .unwrap(); + + match top_node.parent().map(|it| it.kind()) { + Some(SOURCE_FILE) | Some(ITEM_LIST) => { + self.is_new_item = true; + return; + } + _ => (), + } + + self.use_item_syntax = self.leaf.ancestors().find_map(ast::UseItem::cast); + + self.function_syntax = self + .leaf + .ancestors() + .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) + .find_map(ast::FnDef::cast); + match (&self.module, self.function_syntax) { + (Some(module), Some(fn_def)) => { + let function = source_binder::function_from_module(self.db, module, fn_def); + self.function = Some(function); + } + _ => (), + } + + let parent = match name_ref.syntax().parent() { + Some(it) => it, + None => return, + }; + if let Some(segment) = ast::PathSegment::cast(parent) { + let path = segment.parent_path(); + if let Some(mut path) = hir::Path::from_ast(path) { + if !path.is_ident() { + path.segments.pop().unwrap(); + self.path_prefix = Some(path); + return; + } + } + if path.qualifier().is_none() { + self.is_trivial_path = true; + + // Find either enclosing expr statement (thing with `;`) or a + // block. If block, check that we are the last expr. + self.can_be_stmt = name_ref + .syntax() + .ancestors() + .find_map(|node| { + if let Some(stmt) = ast::ExprStmt::cast(node) { + return Some(stmt.syntax().range() == name_ref.syntax().range()); + } + if let Some(block) = ast::Block::cast(node) { + return Some( + block.expr().map(|e| e.syntax().range()) + == Some(name_ref.syntax().range()), + ); + } + None + }) + .unwrap_or(false); + + if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { + if let Some(if_expr) = + find_node_at_offset::(original_file.syntax(), off) + { + if if_expr.syntax().range().end() < name_ref.syntax().range().start() { + self.after_if = true; + } + } + } + } + } + if let Some(field_expr) = ast::FieldExpr::cast(parent) { + // The receiver comes before the point of insertion of the fake + // ident, so it should have the same range in the non-modified file + self.dot_receiver = field_expr + .expr() + .map(|e| e.syntax().range()) + .and_then(|r| find_node_with_range(original_file.syntax(), r)); + } + if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) { + // As above + self.dot_receiver = method_call_expr + .expr() + .map(|e| e.syntax().range()) + .and_then(|r| find_node_with_range(original_file.syntax(), r)); + self.is_method_call = true; + } + } +} + +fn find_node_with_range(syntax: &SyntaxNode, range: TextRange) -> Option<&N> { + let node = find_covering_node(syntax, range); + node.ancestors().find_map(N::cast) +} + +fn is_node(node: &SyntaxNode) -> bool { + match node.ancestors().filter_map(N::cast).next() { + None => false, + Some(n) => n.syntax().range() == node.range(), + } +} diff --git a/crates/ra_ide_api/src/completion/completion_item.rs b/crates/ra_ide_api/src/completion/completion_item.rs new file mode 100644 index 000000000..a25b87bee --- /dev/null +++ b/crates/ra_ide_api/src/completion/completion_item.rs @@ -0,0 +1,244 @@ +use hir::PerNs; + +use crate::completion::CompletionContext; + +/// `CompletionItem` describes a single completion variant in the editor pop-up. +/// It is basically a POD with various properties. To construct a +/// `CompletionItem`, use `new` method and the `Builder` struct. +#[derive(Debug)] +pub struct CompletionItem { + /// Used only internally in tests, to check only specific kind of + /// completion. + completion_kind: CompletionKind, + label: String, + lookup: Option, + snippet: Option, + kind: Option, +} + +pub enum InsertText { + PlainText { text: String }, + Snippet { text: String }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompletionItemKind { + Snippet, + Keyword, + Module, + Function, + Struct, + Enum, + EnumVariant, + Binding, + Field, +} + +#[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, +} + +impl CompletionItem { + pub(crate) fn new(completion_kind: CompletionKind, label: impl Into) -> Builder { + let label = label.into(); + Builder { + completion_kind, + label, + lookup: None, + snippet: None, + kind: None, + } + } + /// What user sees in pop-up in the UI. + pub fn label(&self) -> &str { + &self.label + } + /// What string is used for filtering. + pub fn lookup(&self) -> &str { + self.lookup + .as_ref() + .map(|it| it.as_str()) + .unwrap_or(self.label()) + } + /// What is inserted. + pub fn insert_text(&self) -> InsertText { + match &self.snippet { + None => InsertText::PlainText { + text: self.label.clone(), + }, + Some(it) => InsertText::Snippet { text: it.clone() }, + } + } + + pub fn kind(&self) -> Option { + self.kind + } +} + +/// A helper to make `CompletionItem`s. +#[must_use] +pub(crate) struct Builder { + completion_kind: CompletionKind, + label: String, + lookup: Option, + snippet: Option, + kind: Option, +} + +impl Builder { + pub(crate) fn add_to(self, acc: &mut Completions) { + acc.add(self.build()) + } + + pub(crate) fn build(self) -> CompletionItem { + CompletionItem { + label: self.label, + lookup: self.lookup, + snippet: self.snippet, + kind: self.kind, + completion_kind: self.completion_kind, + } + } + pub(crate) fn lookup_by(mut self, lookup: impl Into) -> Builder { + self.lookup = Some(lookup.into()); + self + } + pub(crate) fn snippet(mut self, snippet: impl Into) -> Builder { + self.snippet = Some(snippet.into()); + self + } + pub(crate) fn kind(mut self, kind: CompletionItemKind) -> Builder { + self.kind = Some(kind); + self + } + pub(super) fn from_resolution( + mut self, + ctx: &CompletionContext, + resolution: &hir::Resolution, + ) -> Builder { + let resolved = resolution.def_id.and_then(|d| d.resolve(ctx.db).ok()); + let kind = match resolved { + PerNs { + types: Some(hir::Def::Module(..)), + .. + } => CompletionItemKind::Module, + PerNs { + types: Some(hir::Def::Struct(..)), + .. + } => CompletionItemKind::Struct, + PerNs { + types: Some(hir::Def::Enum(..)), + .. + } => CompletionItemKind::Enum, + PerNs { + values: Some(hir::Def::Function(function)), + .. + } => return self.from_function(ctx, function), + _ => return self, + }; + self.kind = Some(kind); + self + } + + fn from_function(mut self, ctx: &CompletionContext, function: hir::Function) -> Builder { + // If not an import, add parenthesis automatically. + if ctx.use_item_syntax.is_none() { + if function.signature(ctx.db).args().is_empty() { + self.snippet = Some(format!("{}()$0", self.label)); + } else { + self.snippet = Some(format!("{}($0)", self.label)); + } + } + self.kind = Some(CompletionItemKind::Function); + self + } +} + +impl Into for Builder { + fn into(self) -> CompletionItem { + self.build() + } +} + +/// Represents an in-progress set of completions being built. +#[derive(Debug, Default)] +pub(crate) struct Completions { + buf: Vec, +} + +impl Completions { + pub(crate) fn add(&mut self, item: impl Into) { + self.buf.push(item.into()) + } + pub(crate) fn add_all(&mut self, items: I) + where + I: IntoIterator, + I::Item: Into, + { + 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.completion_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> for Completions { + fn into(self) -> Vec { + self.buf + } +} -- cgit v1.2.3