diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs index a11e98ac0be..2d61a3aef53 100644 --- a/crates/ra_analysis/src/completion.rs +++ b/crates/ra_analysis/src/completion.rs @@ -1,114 +1,52 @@ mod completion_item; -mod reference_completion; +mod completion_context; + +mod complete_fn_param; +mod complete_keyword; +mod complete_snippet; +mod complete_path; +mod complete_scope; -use ra_editor::find_node_at_offset; -use ra_text_edit::AtomTextEdit; -use ra_syntax::{ - algo::visit::{visitor_ctx, VisitorCtx}, - ast, - AstNode, - SyntaxNodeRef, -}; use ra_db::SyntaxDatabase; -use rustc_hash::{FxHashMap}; -use hir::source_binder; use crate::{ db, Cancelable, FilePosition, - completion::completion_item::{Completions, CompletionKind}, + completion::{ + completion_item::{Completions, CompletionKind}, + completion_context::CompletionContext, + }, }; pub use crate::completion::completion_item::{CompletionItem, InsertText}; +/// Main entry point for copmletion. We run comletion as a two-phase process. +/// +/// First, we look at the position and collect a so-called `CompletionContext. +/// This is a somewhat messy process, because, during completion, syntax tree is +/// incomplete and can look readlly weired. +/// +/// Once the context is collected, we run a series of completion routines whihc +/// look at the context and produce completion items. pub(crate) fn completions( db: &db::RootDatabase, position: FilePosition, ) -> Cancelable> { let original_file = db.source_file(position.file_id); - // Insert a fake ident to get a valid parse tree - let file = { - let edit = AtomTextEdit::insert(position.offset, "intellijRulezz".to_string()); - original_file.reparse(&edit) - }; - - let module = ctry!(source_binder::module_from_position(db, position)?); + let ctx = ctry!(CompletionContext::new(db, &original_file, position)?); let mut acc = Completions::default(); - let mut has_completions = false; - // First, let's try to complete a reference to some declaration. - if let Some(name_ref) = find_node_at_offset::(file.syntax(), position.offset) { - has_completions = true; - reference_completion::completions(&mut acc, db, &module, &file, name_ref)?; - // special case, `trait T { fn foo(i_am_a_name_ref) {} }` - if is_node::(name_ref.syntax()) { - param_completions(&mut acc, name_ref.syntax()); - } - } - // Otherwise, if this is a declaration, use heuristics to suggest a name. - if let Some(name) = find_node_at_offset::(file.syntax(), position.offset) { - if is_node::(name.syntax()) { - has_completions = true; - param_completions(&mut acc, name.syntax()); - } - } - if !has_completions { - return Ok(None); - } + complete_fn_param::complete_fn_param(&mut acc, &ctx); + complete_keyword::complete_expr_keyword(&mut acc, &ctx); + complete_snippet::complete_expr_snippet(&mut acc, &ctx); + complete_snippet::complete_item_snippet(&mut acc, &ctx); + complete_path::complete_path(&mut acc, &ctx)?; + complete_scope::complete_scope(&mut acc, &ctx)?; + Ok(Some(acc)) } -/// 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. -fn param_completions(acc: &mut Completions, ctx: SyntaxNodeRef) { - let mut params = FxHashMap::default(); - for node in ctx.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(label) - .lookup_by(lookup) - .kind(CompletionKind::Magic) - .add_to(acc) - }); - - fn process<'a, N: ast::FnDefOwner<'a>>( - node: 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; - }) - } -} - -fn is_node<'a, N: AstNode<'a>>(node: SyntaxNodeRef<'a>) -> bool { - match node.ancestors().filter_map(N::cast).next() { - None => false, - Some(n) => n.syntax().range() == node.range(), - } -} - #[cfg(test)] fn check_completion(code: &str, expected_completions: &str, kind: CompletionKind) { use crate::mock_analysis::{single_file_with_position, analysis_and_position}; @@ -120,51 +58,3 @@ fn check_completion(code: &str, expected_completions: &str, kind: CompletionKind let completions = completions(&analysis.imp.db, position).unwrap().unwrap(); completions.assert_match(expected_completions, kind); } - -#[cfg(test)] -mod tests { - use super::*; - - 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_analysis/src/completion/complete_fn_param.rs b/crates/ra_analysis/src/completion/complete_fn_param.rs new file mode 100644 index 00000000000..6a6213e67bf --- /dev/null +++ b/crates/ra_analysis/src/completion/complete_fn_param.rs @@ -0,0 +1,103 @@ +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(label) + .lookup_by(lookup) + .kind(CompletionKind::Magic) + .add_to(acc) + }); + + fn process<'a, N: ast::FnDefOwner<'a>>( + node: 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_analysis/src/completion/complete_keyword.rs b/crates/ra_analysis/src/completion/complete_keyword.rs new file mode 100644 index 00000000000..dead15bb6f0 --- /dev/null +++ b/crates/ra_analysis/src/completion/complete_keyword.rs @@ -0,0 +1,204 @@ +use ra_syntax::{ + algo::visit::{visitor, Visitor}, + AstNode, + ast::{self, LoopBodyOwner}, + SyntaxKind::*, SyntaxNodeRef, +}; + +use crate::completion::{CompletionContext, CompletionItem, Completions, CompletionKind::*}; + +pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &CompletionContext) { + if !ctx.is_trivial_path { + return; + } + let fn_def = match ctx.enclosing_fn { + 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) { + acc.add(keyword("continue", "continue")); + acc.add(keyword("break", "break")); + } + acc.add_all(complete_return(fn_def, ctx.is_stmt)); +} + +fn is_in_loop_body(leaf: SyntaxNodeRef) -> 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, is_stmt: bool) -> Option { + let snip = match (is_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)) +} + +fn keyword(kw: &str, snippet: &str) -> CompletionItem { + CompletionItem::new(kw) + .kind(Keyword) + .snippet(snippet) + .build() +} + +#[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 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" + "#, + ); + } +} diff --git a/crates/ra_analysis/src/completion/complete_path.rs b/crates/ra_analysis/src/completion/complete_path.rs new file mode 100644 index 00000000000..5fc24af7209 --- /dev/null +++ b/crates/ra_analysis/src/completion/complete_path.rs @@ -0,0 +1,95 @@ +use crate::{ + Cancelable, + completion::{CompletionItem, 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)? { + Some(it) => it, + None => return Ok(()), + }; + let target_module = match def_id.resolve(ctx.db)? { + hir::Def::Module(it) => it, + _ => return Ok(()), + }; + let module_scope = target_module.scope(ctx.db)?; + module_scope.entries().for_each(|(name, _res)| { + CompletionItem::new(name.to_string()) + .kind(Reference) + .add_to(acc) + }); + 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", + ); + } +} diff --git a/crates/ra_analysis/src/completion/complete_scope.rs b/crates/ra_analysis/src/completion/complete_scope.rs new file mode 100644 index 00000000000..d07c0e46d97 --- /dev/null +++ b/crates/ra_analysis/src/completion/complete_scope.rs @@ -0,0 +1,171 @@ +use rustc_hash::FxHashSet; +use ra_syntax::TextUnit; + +use crate::{ + Cancelable, + completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}, +}; + +pub(super) fn complete_scope(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { + if !ctx.is_trivial_path { + return Ok(()); + } + if let Some(fn_def) = ctx.enclosing_fn { + let scopes = hir::FnScopes::new(fn_def); + complete_fn(acc, &scopes, ctx.offset); + } + + if let Some(module) = &ctx.module { + let module_scope = module.scope(ctx.db)?; + module_scope + .entries() + .filter(|(_name, res)| { + // Don't expose this item + match res.import { + None => true, + Some(import) => { + let range = import.range(ctx.db, module.source().file_id()); + !range.is_subrange(&ctx.leaf.range()) + } + } + }) + .for_each(|(name, _res)| { + CompletionItem::new(name.to_string()) + .kind(Reference) + .add_to(acc) + }); + } + + Ok(()) +} + +fn complete_fn(acc: &mut Completions, scopes: &hir::FnScopes, offset: TextUnit) { + let mut shadowed = FxHashSet::default(); + scopes + .scope_chain_for_offset(offset) + .flat_map(|scope| scopes.entries(scope).iter()) + .filter(|entry| shadowed.insert(entry.name())) + .for_each(|entry| { + CompletionItem::new(entry.name().to_string()) + .kind(Reference) + .add_to(acc) + }); + if scopes.self_param.is_some() { + CompletionItem::new("self").kind(Reference).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 = (); + } + ", + "y;x;quux", + ); + } + + #[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 + <|> + } + } + ", + "b;a;quux", + ); + } + + #[test] + fn completes_bindings_from_for() { + check_reference_completion( + r" + fn quux() { + for x in &[1, 2, 3] { + <|> + } + } + ", + "x;quux", + ); + } + + #[test] + fn completes_module_items() { + check_reference_completion( + r" + struct Foo; + enum Baz {} + fn quux() { + <|> + } + ", + "quux;Foo;Baz", + ); + } + + #[test] + fn completes_module_items_in_nested_modules() { + check_reference_completion( + r" + struct Foo; + mod m { + struct Bar; + fn quux() { <|> } + } + ", + "quux;Bar", + ); + } + + #[test] + fn completes_return_type() { + check_reference_completion( + r" + struct Foo; + fn x() -> <|> + ", + "Foo;x", + ) + } + + #[test] + fn dont_show_to_completions_for_shadowing() { + check_reference_completion( + r" + fn foo() -> { + let bar = 92; + { + let bar = 62; + <|> + } + } + ", + "bar;foo", + ) + } + + #[test] + fn completes_self_in_methods() { + check_reference_completion(r"impl S { fn foo(&self) { <|> } }", "self") + } +} diff --git a/crates/ra_analysis/src/completion/complete_snippet.rs b/crates/ra_analysis/src/completion/complete_snippet.rs new file mode 100644 index 00000000000..ccd68832b4a --- /dev/null +++ b/crates/ra_analysis/src/completion/complete_snippet.rs @@ -0,0 +1,76 @@ +use crate::completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}; + +pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &CompletionContext) { + if !(ctx.is_trivial_path && ctx.enclosing_fn.is_some()) { + return; + } + CompletionItem::new("pd") + .snippet("eprintln!(\"$0 = {:?}\", $0);") + .kind(Snippet) + .add_to(acc); + CompletionItem::new("ppd") + .snippet("eprintln!(\"$0 = {:#?}\", $0);") + .kind(Snippet) + .add_to(acc); +} + +pub(super) fn complete_item_snippet(acc: &mut Completions, ctx: &CompletionContext) { + if !ctx.is_new_item { + return; + } + CompletionItem::new("Test function") + .lookup_by("tfn") + .snippet( + "\ +#[test] +fn ${1:feature}() { + $0 +}", + ) + .kind(Snippet) + .add_to(acc); + CompletionItem::new("pub(crate)") + .snippet("pub(crate) $0") + .kind(Snippet) + .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_analysis/src/completion/completion_context.rs b/crates/ra_analysis/src/completion/completion_context.rs new file mode 100644 index 00000000000..064fbc6f7be --- /dev/null +++ b/crates/ra_analysis/src/completion/completion_context.rs @@ -0,0 +1,156 @@ +use ra_editor::find_node_at_offset; +use ra_text_edit::AtomTextEdit; +use ra_syntax::{ + algo::find_leaf_at_offset, + ast, + AstNode, + SyntaxNodeRef, + SourceFileNode, + TextUnit, + 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: SyntaxNodeRef<'a>, + pub(super) module: Option, + pub(super) enclosing_fn: Option>, + 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, + pub(super) is_stmt: bool, + /// Something is typed at the "top" level, in module or impl/trait. + pub(super) is_new_item: bool, +} + +impl<'a> CompletionContext<'a> { + pub(super) fn new( + db: &'a db::RootDatabase, + original_file: &'a SourceFileNode, + 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, + enclosing_fn: None, + is_param: false, + is_trivial_path: false, + path_prefix: None, + after_if: false, + is_stmt: false, + is_new_item: false, + }; + ctx.fill(original_file, position.offset); + Ok(Some(ctx)) + } + + fn fill(&mut self, original_file: &SourceFileNode, 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(&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, file: &SourceFileNode, 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; + } + _ => (), + } + + 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; + self.enclosing_fn = self + .leaf + .ancestors() + .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) + .find_map(ast::FnDef::cast); + + self.is_stmt = match name_ref + .syntax() + .ancestors() + .filter_map(ast::ExprStmt::cast) + .next() + { + None => false, + Some(expr_stmt) => expr_stmt.syntax().range() == name_ref.syntax().range(), + }; + + if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { + if let Some(if_expr) = find_node_at_offset::(file.syntax(), off) { + if if_expr.syntax().range().end() < name_ref.syntax().range().start() { + self.after_if = true; + } + } + } + } + } + } +} + +fn is_node<'a, N: AstNode<'a>>(node: SyntaxNodeRef<'a>) -> bool { + match node.ancestors().filter_map(N::cast).next() { + None => false, + Some(n) => n.syntax().range() == node.range(), + } +} diff --git a/crates/ra_analysis/src/completion/reference_completion.rs b/crates/ra_analysis/src/completion/reference_completion.rs deleted file mode 100644 index c2ac9545376..00000000000 --- a/crates/ra_analysis/src/completion/reference_completion.rs +++ /dev/null @@ -1,633 +0,0 @@ -use rustc_hash::{FxHashSet}; -use ra_editor::find_node_at_offset; -use ra_syntax::{ - algo::visit::{visitor, Visitor}, - SourceFileNode, AstNode, - ast::{self, LoopBodyOwner}, - SyntaxKind::*, -}; -use hir::{ - self, - FnScopes, Def, Path -}; - -use crate::{ - db::RootDatabase, - completion::{CompletionItem, Completions, CompletionKind::*}, - Cancelable -}; - -pub(super) fn completions( - acc: &mut Completions, - db: &RootDatabase, - module: &hir::Module, - file: &SourceFileNode, - name_ref: ast::NameRef, -) -> Cancelable<()> { - let kind = match classify_name_ref(name_ref) { - Some(it) => it, - None => return Ok(()), - }; - - match kind { - NameRefKind::LocalRef { enclosing_fn } => { - if let Some(fn_def) = enclosing_fn { - let scopes = FnScopes::new(fn_def); - complete_fn(name_ref, &scopes, acc); - complete_expr_keywords(&file, fn_def, name_ref, acc); - complete_expr_snippets(acc); - } - - let module_scope = module.scope(db)?; - module_scope - .entries() - .filter(|(_name, res)| { - // Don't expose this item - match res.import { - None => true, - Some(import) => { - let range = import.range(db, module.source().file_id()); - !range.is_subrange(&name_ref.syntax().range()) - } - } - }) - .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 => { - 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) => complete_mod_item_snippets(acc), - _ => (), - } - } - } - Ok(()) -} - -enum NameRefKind<'a> { - /// NameRef is a part of single-segment path, for example, a refernece to a - /// local variable. - LocalRef { - enclosing_fn: Option>, - }, - /// NameRef is the last segment in some path - Path(Path), - /// NameRef is bare identifier at the module's root. - /// Used for keyword completion - BareIdentInMod, -} - -fn classify_name_ref(name_ref: ast::NameRef) -> Option { - 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) => return Some(NameRefKind::BareIdentInMod), - _ => (), - } - - let parent = name_ref.syntax().parent()?; - if let Some(segment) = ast::PathSegment::cast(parent) { - let path = segment.parent_path(); - if let Some(path) = Path::from_ast(path) { - if !path.is_ident() { - return Some(NameRefKind::Path(path)); - } - } - if path.qualifier().is_none() { - let enclosing_fn = name_ref - .syntax() - .ancestors() - .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) - .find_map(ast::FnDef::cast); - return Some(NameRefKind::LocalRef { enclosing_fn }); - } - } - None -} - -fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Completions) { - let mut shadowed = FxHashSet::default(); - scopes - .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()) - .kind(Reference) - .add_to(acc) - }); - if scopes.self_param.is_some() { - CompletionItem::new("self").kind(Reference).add_to(acc); - } -} - -fn complete_path( - acc: &mut Completions, - db: &RootDatabase, - module: &hir::Module, - mut path: Path, -) -> Cancelable<()> { - if path.segments.is_empty() { - return Ok(()); - } - path.segments.pop(); - let def_id = match module.resolve_path(db, path)? { - None => return Ok(()), - Some(it) => it, - }; - let target_module = match def_id.resolve(db)? { - Def::Module(it) => it, - _ => return Ok(()), - }; - let module_scope = target_module.scope(db)?; - module_scope.entries().for_each(|(name, _res)| { - CompletionItem::new(name.to_string()) - .kind(Reference) - .add_to(acc) - }); - Ok(()) -} - -fn complete_mod_item_snippets(acc: &mut Completions) { - CompletionItem::new("Test function") - .lookup_by("tfn") - .snippet( - "\ -#[test] -fn ${1:feature}() { - $0 -}", - ) - .kind(Snippet) - .add_to(acc); - CompletionItem::new("pub(crate)") - .snippet("pub(crate) $0") - .kind(Snippet) - .add_to(acc); -} - -fn complete_expr_keywords( - file: &SourceFileNode, - fn_def: ast::FnDef, - name_ref: ast::NameRef, - acc: &mut Completions, -) { - 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 let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { - if let Some(if_expr) = find_node_at_offset::(file.syntax(), off) { - if if_expr.syntax().range().end() < name_ref.syntax().range().start() { - acc.add(keyword("else", "else {$0}")); - acc.add(keyword("else if", "else if $0 {}")); - } - } - } - if is_in_loop_body(name_ref) { - acc.add(keyword("continue", "continue")); - acc.add(keyword("break", "break")); - } - acc.add_all(complete_return(fn_def, name_ref)); -} - -fn is_in_loop_body(name_ref: ast::NameRef) -> bool { - for node in name_ref.syntax().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 name_ref - .syntax() - .range() - .is_subrange(&body.syntax().range()) - { - return true; - } - } - } - false -} - -fn complete_return(fn_def: ast::FnDef, name_ref: ast::NameRef) -> Option { - // let is_last_in_block = name_ref.syntax().ancestors().filter_map(ast::Expr::cast) - // .next() - // .and_then(|it| it.syntax().parent()) - // .and_then(ast::Block::cast) - // .is_some(); - - // if is_last_in_block { - // return None; - // } - - let is_stmt = match name_ref - .syntax() - .ancestors() - .filter_map(ast::ExprStmt::cast) - .next() - { - None => false, - Some(expr_stmt) => expr_stmt.syntax().range() == name_ref.syntax().range(), - }; - let snip = match (is_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)) -} - -fn keyword(kw: &str, snippet: &str) -> CompletionItem { - 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 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_hir/src/function/scope.rs b/crates/ra_hir/src/function/scope.rs index 86345329166..9f1aa1ef2d8 100644 --- a/crates/ra_hir/src/function/scope.rs +++ b/crates/ra_hir/src/function/scope.rs @@ -1,7 +1,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use ra_syntax::{ - AstNode, SmolStr, SyntaxNodeRef, TextRange, + AstNode, SmolStr, SyntaxNodeRef, TextUnit, TextRange, algo::generate, ast::{self, ArgListOwner, LoopBodyOwner, NameOwner}, }; @@ -57,6 +57,48 @@ impl FnScopes { self.scopes[scope].parent }) } + pub fn scope_chain_for_offset<'a>( + &'a self, + offset: TextUnit, + ) -> impl Iterator + 'a { + let scope = self + .scope_for + .iter() + // find containin scope + .min_by_key(|(ptr, _scope)| { + ( + !(ptr.range().start() <= offset && offset <= ptr.range().end()), + ptr.range().len(), + ) + }) + .map(|(ptr, scope)| self.adjust(*ptr, *scope, offset)); + + generate(scope, move |&scope| self.scopes[scope].parent) + } + // XXX: during completion, cursor might be outside of any particular + // expression. Try to figure out the correct scope... + fn adjust(&self, ptr: LocalSyntaxPtr, original_scope: ScopeId, offset: TextUnit) -> ScopeId { + let r = ptr.range(); + let child_scopes = self + .scope_for + .iter() + .map(|(ptr, scope)| (ptr.range(), scope)) + .filter(|(range, _)| range.start() <= offset && range.is_subrange(&r) && *range != r); + + child_scopes + .max_by(|(r1, _), (r2, _)| { + if r2.is_subrange(&r1) { + std::cmp::Ordering::Greater + } else if r1.is_subrange(&r2) { + std::cmp::Ordering::Less + } else { + r1.start().cmp(&r2.start()) + } + }) + .map(|(ptr, scope)| *scope) + .unwrap_or(original_scope) + } + pub fn resolve_local_name<'a>(&'a self, name_ref: ast::NameRef) -> Option<&'a ScopeEntry> { let mut shadowed = FxHashSet::default(); let ret = self @@ -144,6 +186,8 @@ impl ScopeEntry { } fn compute_block_scopes(block: ast::Block, scopes: &mut FnScopes, mut scope: ScopeId) { + // A hack for completion :( + scopes.set_scope(block.syntax(), scope); for stmt in block.statements() { match stmt { ast::Stmt::LetStmt(stmt) => { @@ -165,6 +209,7 @@ fn compute_block_scopes(block: ast::Block, scopes: &mut FnScopes, mut scope: Sco } } if let Some(expr) = block.expr() { + eprintln!("{:?}", expr); scopes.set_scope(expr.syntax(), scope); compute_expr_scopes(expr, scopes, scope); }