From 9b88ec488b3f83ab718c8cb4d7dff95aff0113ed Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Wed, 7 Nov 2018 20:34:16 +0300 Subject: split completion mod --- crates/ra_analysis/src/completion.rs | 689 --------------------- crates/ra_analysis/src/completion/mod.rs | 425 +++++++++++++ .../src/completion/reference_completion.rs | 331 ++++++++++ 3 files changed, 756 insertions(+), 689 deletions(-) delete mode 100644 crates/ra_analysis/src/completion.rs create mode 100644 crates/ra_analysis/src/completion/mod.rs create mode 100644 crates/ra_analysis/src/completion/reference_completion.rs (limited to 'crates/ra_analysis/src') diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs deleted file mode 100644 index 27566a8a1..000000000 --- a/crates/ra_analysis/src/completion.rs +++ /dev/null @@ -1,689 +0,0 @@ -use ra_editor::find_node_at_offset; -use ra_syntax::{ - algo::visit::{visitor, visitor_ctx, Visitor, VisitorCtx}, - ast::{self, AstChildren, LoopBodyOwner, ModuleItemOwner}, - AstNode, AtomEdit, SourceFileNode, - SyntaxKind::*, - SyntaxNodeRef, -}; -use rustc_hash::{FxHashMap, FxHashSet}; - -use crate::{ - db::{self, SyntaxDatabase}, - descriptors::function::FnScopes, - descriptors::module::{ModuleId, ModuleScope, ModuleTree, ModuleSource}, - descriptors::DescriptorDatabase, - input::FilesDatabase, - Cancelable, FilePosition, FileId, -}; - -#[derive(Debug)] -pub struct CompletionItem { - /// What user sees in pop-up - pub label: String, - /// What string is used for filtering, defaults to label - pub lookup: Option, - /// What is inserted, defaults to label - pub snippet: Option, -} - -pub(crate) fn completions( - db: &db::RootDatabase, - position: FilePosition, -) -> Cancelable>> { - let original_file = db.file_syntax(position.file_id); - // Insert a fake ident to get a valid parse tree - let file = { - let edit = AtomEdit::insert(position.offset, "intellijRulezz".to_string()); - original_file.reparse(&edit) - }; - - let mut res = Vec::new(); - 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; - // completion from lexical scope - complete_name_ref(&file, name_ref, &mut res); - // special case, `trait T { fn foo(i_am_a_name_ref) {} }` - if is_node::(name_ref.syntax()) { - param_completions(name_ref.syntax(), &mut res); - } - // snippet completions - { - 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(&mut res), - _ => (), - } - } - complete_path(db, position.file_id, name_ref, &mut res)?; - } - - // 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(name.syntax(), &mut res); - } - } - let res = if has_completions { Some(res) } else { None }; - Ok(res) -} - -fn complete_path( - db: &db::RootDatabase, - file_id: FileId, - name_ref: ast::NameRef, - acc: &mut Vec, -) -> Cancelable<()> { - let source_root_id = db.file_source_root(file_id); - let module_tree = db.module_tree(source_root_id)?; - let module_id = match module_tree.any_module_for_source(ModuleSource::SourceFile(file_id)) { - None => return Ok(()), - Some(it) => it, - }; - let target_module_id = match find_target_module(&module_tree, module_id, name_ref) { - None => return Ok(()), - Some(it) => it, - }; - let module_scope = db.module_scope(source_root_id, target_module_id)?; - let completions = module_scope.entries().iter().map(|entry| CompletionItem { - label: entry.name().to_string(), - lookup: None, - snippet: None, - }); - acc.extend(completions); - Ok(()) -} - -fn find_target_module( - module_tree: &ModuleTree, - module_id: ModuleId, - name_ref: ast::NameRef, -) -> Option { - let mut crate_path = crate_path(name_ref)?; - - crate_path.pop(); - let mut target_module = module_id.root(&module_tree); - for name in crate_path { - target_module = target_module.child(module_tree, name.text().as_str())?; - } - Some(target_module) -} - -fn crate_path(name_ref: ast::NameRef) -> Option> { - let mut path = name_ref - .syntax() - .parent() - .and_then(ast::PathSegment::cast)? - .parent_path(); - let mut res = Vec::new(); - loop { - let segment = path.segment()?; - match segment.kind()? { - ast::PathSegmentKind::Name(name) => res.push(name), - ast::PathSegmentKind::CrateKw => break, - ast::PathSegmentKind::SelfKw | ast::PathSegmentKind::SuperKw => return None, - } - path = path.qualifier()?; - } - res.reverse(); - Some(res) -} - -fn complete_module_items( - file: &SourceFileNode, - items: AstChildren, - this_item: Option, - acc: &mut Vec, -) { - let scope = ModuleScope::new(items); // FIXME - acc.extend( - scope - .entries() - .iter() - .filter(|entry| { - let syntax = entry.ptr().resolve(file); - Some(syntax.borrowed()) != this_item.map(|it| it.syntax()) - }) - .map(|entry| CompletionItem { - label: entry.name().to_string(), - lookup: None, - snippet: None, - }), - ); -} - -fn complete_name_ref(file: &SourceFileNode, name_ref: ast::NameRef, acc: &mut Vec) { - if !is_node::(name_ref.syntax()) { - return; - } - let mut visited_fn = false; - for node in name_ref.syntax().ancestors() { - if let Some(items) = visitor() - .visit::(|it| Some(it.items())) - .visit::(|it| Some(it.item_list()?.items())) - .accept(node) - { - if let Some(items) = items { - complete_module_items(file, items, Some(name_ref), acc); - } - break; - } else if !visited_fn { - if let Some(fn_def) = ast::FnDef::cast(node) { - visited_fn = true; - complete_expr_keywords(&file, fn_def, name_ref, acc); - complete_expr_snippets(acc); - let scopes = FnScopes::new(fn_def); - complete_fn(name_ref, &scopes, acc); - } - } - } -} - -fn param_completions(ctx: SyntaxNodeRef, acc: &mut Vec) { - 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)| { - acc.push(CompletionItem { - label, - lookup: Some(lookup), - snippet: None, - }) - }); - - 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(), - } -} - -fn complete_expr_keywords( - file: &SourceFileNode, - fn_def: ast::FnDef, - name_ref: ast::NameRef, - acc: &mut Vec, -) { - acc.push(keyword("if", "if $0 {}")); - acc.push(keyword("match", "match $0 {}")); - acc.push(keyword("while", "while $0 {}")); - acc.push(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.push(keyword("else", "else {$0}")); - acc.push(keyword("else if", "else if $0 {}")); - } - } - } - if is_in_loop_body(name_ref) { - acc.push(keyword("continue", "continue")); - acc.push(keyword("break", "break")); - } - acc.extend(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, snip: &str) -> CompletionItem { - CompletionItem { - label: kw.to_string(), - lookup: None, - snippet: Some(snip.to_string()), - } -} - -fn complete_expr_snippets(acc: &mut Vec) { - acc.push(CompletionItem { - label: "pd".to_string(), - lookup: None, - snippet: Some("eprintln!(\"$0 = {:?}\", $0);".to_string()), - }); - acc.push(CompletionItem { - label: "ppd".to_string(), - lookup: None, - snippet: Some("eprintln!(\"$0 = {:#?}\", $0);".to_string()), - }); -} - -fn complete_mod_item_snippets(acc: &mut Vec) { - acc.push(CompletionItem { - label: "tfn".to_string(), - lookup: None, - snippet: Some("#[test]\nfn $1() {\n $0\n}".to_string()), - }); - acc.push(CompletionItem { - label: "pub(crate)".to_string(), - lookup: None, - snippet: Some("pub(crate) $0".to_string()), - }) -} - -fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Vec) { - let mut shadowed = FxHashSet::default(); - acc.extend( - scopes - .scope_chain(name_ref.syntax()) - .flat_map(|scope| scopes.entries(scope).iter()) - .filter(|entry| shadowed.insert(entry.name())) - .map(|entry| CompletionItem { - label: entry.name().to_string(), - lookup: None, - snippet: None, - }), - ); - if scopes.self_param.is_some() { - acc.push(CompletionItem { - label: "self".to_string(), - lookup: None, - snippet: None, - }) - } -} - -#[cfg(test)] -mod tests { - use test_utils::assert_eq_dbg; - - use crate::mock_analysis::single_file_with_position; - - use super::*; - - fn check_scope_completion(code: &str, expected_completions: &str) { - let (analysis, position) = single_file_with_position(code); - let completions = completions(&analysis.imp.db, position) - .unwrap() - .unwrap() - .into_iter() - .filter(|c| c.snippet.is_none()) - .collect::>(); - assert_eq_dbg(expected_completions, &completions); - } - - fn check_snippet_completion(code: &str, expected_completions: &str) { - let (analysis, position) = single_file_with_position(code); - let completions = completions(&analysis.imp.db, position) - .unwrap() - .unwrap() - .into_iter() - .filter(|c| c.snippet.is_some()) - .collect::>(); - assert_eq_dbg(expected_completions, &completions); - } - - #[test] - fn test_completion_let_scope() { - check_scope_completion( - r" - fn quux(x: i32) { - let y = 92; - 1 + <|>; - let z = (); - } - ", - r#"[CompletionItem { label: "y", lookup: None, snippet: None }, - CompletionItem { label: "x", lookup: None, snippet: None }, - CompletionItem { label: "quux", lookup: None, snippet: None }]"#, - ); - } - - #[test] - fn test_completion_if_let_scope() { - check_scope_completion( - r" - fn quux() { - if let Some(x) = foo() { - let y = 92; - }; - if let Some(a) = bar() { - let b = 62; - 1 + <|> - } - } - ", - r#"[CompletionItem { label: "b", lookup: None, snippet: None }, - CompletionItem { label: "a", lookup: None, snippet: None }, - CompletionItem { label: "quux", lookup: None, snippet: None }]"#, - ); - } - - #[test] - fn test_completion_for_scope() { - check_scope_completion( - r" - fn quux() { - for x in &[1, 2, 3] { - <|> - } - } - ", - r#"[CompletionItem { label: "x", lookup: None, snippet: None }, - CompletionItem { label: "quux", lookup: None, snippet: None }]"#, - ); - } - - #[test] - fn test_completion_mod_scope() { - check_scope_completion( - r" - struct Foo; - enum Baz {} - fn quux() { - <|> - } - ", - r#"[CompletionItem { label: "Foo", lookup: None, snippet: None }, - CompletionItem { label: "Baz", lookup: None, snippet: None }, - CompletionItem { label: "quux", lookup: None, snippet: None }]"#, - ); - } - - #[test] - fn test_completion_mod_scope_no_self_use() { - check_scope_completion( - r" - use foo<|>; - ", - r#"[]"#, - ); - } - - #[test] - fn test_completion_mod_scope_nested() { - check_scope_completion( - r" - struct Foo; - mod m { - struct Bar; - fn quux() { <|> } - } - ", - r#"[CompletionItem { label: "Bar", lookup: None, snippet: None }, - CompletionItem { label: "quux", lookup: None, snippet: None }]"#, - ); - } - - #[test] - fn test_complete_type() { - check_scope_completion( - r" - struct Foo; - fn x() -> <|> - ", - r#"[CompletionItem { label: "Foo", lookup: None, snippet: None }, - CompletionItem { label: "x", lookup: None, snippet: None }]"#, - ) - } - - #[test] - fn test_complete_shadowing() { - check_scope_completion( - r" - fn foo() -> { - let bar = 92; - { - let bar = 62; - <|> - } - } - ", - r#"[CompletionItem { label: "bar", lookup: None, snippet: None }, - CompletionItem { label: "foo", lookup: None, snippet: None }]"#, - ) - } - - #[test] - fn test_complete_self() { - check_scope_completion( - r" - impl S { fn foo(&self) { <|> } } - ", - r#"[CompletionItem { label: "self", lookup: None, snippet: None }]"#, - ) - } - - #[test] - fn test_completion_kewords() { - check_snippet_completion(r" - fn quux() { - <|> - } - ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, - CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, - CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, - CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, - CompletionItem { label: "return", lookup: None, snippet: Some("return") }, - CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, - CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); - } - - #[test] - fn test_completion_else() { - check_snippet_completion(r" - fn quux() { - if true { - () - } <|> - } - ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, - CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, - CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, - CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, - CompletionItem { label: "else", lookup: None, snippet: Some("else {$0}") }, - CompletionItem { label: "else if", lookup: None, snippet: Some("else if $0 {}") }, - CompletionItem { label: "return", lookup: None, snippet: Some("return") }, - CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, - CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); - } - - #[test] - fn test_completion_return_value() { - check_snippet_completion(r" - fn quux() -> i32 { - <|> - 92 - } - ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, - CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, - CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, - CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, - CompletionItem { label: "return", lookup: None, snippet: Some("return $0;") }, - CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, - CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); - check_snippet_completion(r" - fn quux() { - <|> - 92 - } - ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, - CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, - CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, - CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, - CompletionItem { label: "return", lookup: None, snippet: Some("return;") }, - CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, - CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); - } - - #[test] - fn test_completion_return_no_stmt() { - check_snippet_completion(r" - fn quux() -> i32 { - match () { - () => <|> - } - } - ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, - CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, - CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, - CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, - CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }, - CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, - CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); - } - - #[test] - fn test_continue_break_completion() { - check_snippet_completion(r" - fn quux() -> i32 { - loop { <|> } - } - ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, - CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, - CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, - CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, - CompletionItem { label: "continue", lookup: None, snippet: Some("continue") }, - CompletionItem { label: "break", lookup: None, snippet: Some("break") }, - CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }, - CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, - CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); - check_snippet_completion(r" - fn quux() -> i32 { - loop { || { <|> } } - } - ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, - CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, - CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, - CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, - CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }, - CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, - CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); - } - - #[test] - fn test_param_completion_last_param() { - check_scope_completion(r" - fn foo(file_id: FileId) {} - fn bar(file_id: FileId) {} - fn baz(file<|>) {} - ", r#"[CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); - } - - #[test] - fn test_param_completion_nth_param() { - check_scope_completion(r" - fn foo(file_id: FileId) {} - fn bar(file_id: FileId) {} - fn baz(file<|>, x: i32) {} - ", r#"[CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); - } - - #[test] - fn test_param_completion_trait_param() { - check_scope_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#"[CompletionItem { label: "self", lookup: None, snippet: None }, - CompletionItem { label: "SourceRoot", lookup: None, snippet: None }, - CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); - } - - #[test] - fn test_item_snippets() { - // check_snippet_completion(r" - // <|> - // ", - // r##"[CompletionItem { label: "tfn", lookup: None, snippet: Some("#[test]\nfn $1() {\n $0\n}") }]"##, - // ); - check_snippet_completion(r" - #[cfg(test)] - mod tests { - <|> - } - ", - r##"[CompletionItem { label: "tfn", lookup: None, snippet: Some("#[test]\nfn $1() {\n $0\n}") }, - CompletionItem { label: "pub(crate)", lookup: None, snippet: Some("pub(crate) $0") }]"##, - ); - } -} diff --git a/crates/ra_analysis/src/completion/mod.rs b/crates/ra_analysis/src/completion/mod.rs new file mode 100644 index 000000000..763533012 --- /dev/null +++ b/crates/ra_analysis/src/completion/mod.rs @@ -0,0 +1,425 @@ +mod reference_completion; + +use ra_editor::find_node_at_offset; +use ra_syntax::{ + algo::visit::{visitor_ctx, VisitorCtx}, + ast, + AstNode, AtomEdit, + SyntaxNodeRef, +}; +use rustc_hash::{FxHashMap}; + +use crate::{ + db::{self, SyntaxDatabase}, + Cancelable, FilePosition +}; + +#[derive(Debug)] +pub struct CompletionItem { + /// What user sees in pop-up + pub label: String, + /// What string is used for filtering, defaults to label + pub lookup: Option, + /// What is inserted, defaults to label + pub snippet: Option, +} + +pub(crate) fn completions( + db: &db::RootDatabase, + position: FilePosition, +) -> Cancelable>> { + let original_file = db.file_syntax(position.file_id); + // Insert a fake ident to get a valid parse tree + let file = { + let edit = AtomEdit::insert(position.offset, "intellijRulezz".to_string()); + original_file.reparse(&edit) + }; + + let mut res = Vec::new(); + 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 res, db, position.file_id, &file, name_ref)?; + // special case, `trait T { fn foo(i_am_a_name_ref) {} }` + if is_node::(name_ref.syntax()) { + param_completions(name_ref.syntax(), &mut res); + } + } + + // 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(name.syntax(), &mut res); + } + } + let res = if has_completions { Some(res) } else { None }; + Ok(res) +} + +fn param_completions(ctx: SyntaxNodeRef, acc: &mut Vec) { + 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)| { + acc.push(CompletionItem { + label, + lookup: Some(lookup), + snippet: None, + }) + }); + + 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)] +mod tests { + use test_utils::assert_eq_dbg; + + use crate::mock_analysis::single_file_with_position; + + use super::*; + + fn check_scope_completion(code: &str, expected_completions: &str) { + let (analysis, position) = single_file_with_position(code); + let completions = completions(&analysis.imp.db, position) + .unwrap() + .unwrap() + .into_iter() + .filter(|c| c.snippet.is_none()) + .collect::>(); + assert_eq_dbg(expected_completions, &completions); + } + + fn check_snippet_completion(code: &str, expected_completions: &str) { + let (analysis, position) = single_file_with_position(code); + let completions = completions(&analysis.imp.db, position) + .unwrap() + .unwrap() + .into_iter() + .filter(|c| c.snippet.is_some()) + .collect::>(); + assert_eq_dbg(expected_completions, &completions); + } + + #[test] + fn test_completion_let_scope() { + check_scope_completion( + r" + fn quux(x: i32) { + let y = 92; + 1 + <|>; + let z = (); + } + ", + r#"[CompletionItem { label: "y", lookup: None, snippet: None }, + CompletionItem { label: "x", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#, + ); + } + + #[test] + fn test_completion_if_let_scope() { + check_scope_completion( + r" + fn quux() { + if let Some(x) = foo() { + let y = 92; + }; + if let Some(a) = bar() { + let b = 62; + 1 + <|> + } + } + ", + r#"[CompletionItem { label: "b", lookup: None, snippet: None }, + CompletionItem { label: "a", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#, + ); + } + + #[test] + fn test_completion_for_scope() { + check_scope_completion( + r" + fn quux() { + for x in &[1, 2, 3] { + <|> + } + } + ", + r#"[CompletionItem { label: "x", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#, + ); + } + + #[test] + fn test_completion_mod_scope() { + check_scope_completion( + r" + struct Foo; + enum Baz {} + fn quux() { + <|> + } + ", + r#"[CompletionItem { label: "Foo", lookup: None, snippet: None }, + CompletionItem { label: "Baz", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#, + ); + } + + #[test] + fn test_completion_mod_scope_no_self_use() { + check_scope_completion( + r" + use foo<|>; + ", + r#"[]"#, + ); + } + + #[test] + fn test_completion_mod_scope_nested() { + check_scope_completion( + r" + struct Foo; + mod m { + struct Bar; + fn quux() { <|> } + } + ", + r#"[CompletionItem { label: "Bar", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#, + ); + } + + #[test] + fn test_complete_type() { + check_scope_completion( + r" + struct Foo; + fn x() -> <|> + ", + r#"[CompletionItem { label: "Foo", lookup: None, snippet: None }, + CompletionItem { label: "x", lookup: None, snippet: None }]"#, + ) + } + + #[test] + fn test_complete_shadowing() { + check_scope_completion( + r" + fn foo() -> { + let bar = 92; + { + let bar = 62; + <|> + } + } + ", + r#"[CompletionItem { label: "bar", lookup: None, snippet: None }, + CompletionItem { label: "foo", lookup: None, snippet: None }]"#, + ) + } + + #[test] + fn test_complete_self() { + check_scope_completion( + r" + impl S { fn foo(&self) { <|> } } + ", + r#"[CompletionItem { label: "self", lookup: None, snippet: None }]"#, + ) + } + + #[test] + fn test_completion_kewords() { + check_snippet_completion(r" + fn quux() { + <|> + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + } + + #[test] + fn test_completion_else() { + check_snippet_completion(r" + fn quux() { + if true { + () + } <|> + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "else", lookup: None, snippet: Some("else {$0}") }, + CompletionItem { label: "else if", lookup: None, snippet: Some("else if $0 {}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + } + + #[test] + fn test_completion_return_value() { + check_snippet_completion(r" + fn quux() -> i32 { + <|> + 92 + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0;") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + check_snippet_completion(r" + fn quux() { + <|> + 92 + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return;") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + } + + #[test] + fn test_completion_return_no_stmt() { + check_snippet_completion(r" + fn quux() -> i32 { + match () { + () => <|> + } + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + } + + #[test] + fn test_continue_break_completion() { + check_snippet_completion(r" + fn quux() -> i32 { + loop { <|> } + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "continue", lookup: None, snippet: Some("continue") }, + CompletionItem { label: "break", lookup: None, snippet: Some("break") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + check_snippet_completion(r" + fn quux() -> i32 { + loop { || { <|> } } + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + } + + #[test] + fn test_param_completion_last_param() { + check_scope_completion(r" + fn foo(file_id: FileId) {} + fn bar(file_id: FileId) {} + fn baz(file<|>) {} + ", r#"[CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); + } + + #[test] + fn test_param_completion_nth_param() { + check_scope_completion(r" + fn foo(file_id: FileId) {} + fn bar(file_id: FileId) {} + fn baz(file<|>, x: i32) {} + ", r#"[CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); + } + + #[test] + fn test_param_completion_trait_param() { + check_scope_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#"[CompletionItem { label: "self", lookup: None, snippet: None }, + CompletionItem { label: "SourceRoot", lookup: None, snippet: None }, + CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); + } + + #[test] + fn test_item_snippets() { + // check_snippet_completion(r" + // <|> + // ", + // r##"[CompletionItem { label: "tfn", lookup: None, snippet: Some("#[test]\nfn $1() {\n $0\n}") }]"##, + // ); + check_snippet_completion(r" + #[cfg(test)] + mod tests { + <|> + } + ", + r##"[CompletionItem { label: "tfn", lookup: None, snippet: Some("#[test]\nfn $1() {\n $0\n}") }, + CompletionItem { label: "pub(crate)", lookup: None, snippet: Some("pub(crate) $0") }]"##, + ); + } +} diff --git a/crates/ra_analysis/src/completion/reference_completion.rs b/crates/ra_analysis/src/completion/reference_completion.rs new file mode 100644 index 000000000..b08174968 --- /dev/null +++ b/crates/ra_analysis/src/completion/reference_completion.rs @@ -0,0 +1,331 @@ +use rustc_hash::{FxHashSet}; +use ra_editor::find_node_at_offset; +use ra_syntax::{ + algo::visit::{visitor, Visitor}, + SourceFileNode, AstNode, + ast::{self, AstChildren, ModuleItemOwner, LoopBodyOwner}, + SyntaxKind::*, +}; + +use crate::{ + db::RootDatabase, + input::FilesDatabase, + completion::CompletionItem, + descriptors::module::{ModuleId, ModuleScope, ModuleTree, ModuleSource}, + descriptors::function::FnScopes, + descriptors::DescriptorDatabase, + FileId, Cancelable +}; + +pub(super) fn completions( + acc: &mut Vec, + db: &RootDatabase, + file_id: FileId, + 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 => { + if let Some(fn_def) = complete_local_name(acc, &file, name_ref) { + complete_expr_keywords(&file, fn_def, name_ref, acc); + complete_expr_snippets(acc); + } + } + NameRefKind::CratePath(path) => complete_path(acc, db, file_id, 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, + /// NameRef is the last segment in crate:: path + CratePath(Vec>), + /// 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 path.qualifier().is_none() { + return Some(NameRefKind::LocalRef); + } + if let Some(crate_path) = crate_path(path) { + return Some(NameRefKind::CratePath(crate_path)); + } + } + None +} + +fn crate_path(mut path: ast::Path) -> Option> { + let mut res = Vec::new(); + loop { + let segment = path.segment()?; + match segment.kind()? { + ast::PathSegmentKind::Name(name) => res.push(name), + ast::PathSegmentKind::CrateKw => break, + ast::PathSegmentKind::SelfKw | ast::PathSegmentKind::SuperKw => return None, + } + path = path.qualifier()?; + } + res.reverse(); + Some(res) +} + +fn complete_local_name<'a>( + acc: &mut Vec, + file: &SourceFileNode, + name_ref: ast::NameRef<'a>, +) -> Option> { + let mut enclosing_fn = None; + for node in name_ref.syntax().ancestors() { + if let Some(items) = visitor() + .visit::(|it| Some(it.items())) + .visit::(|it| Some(it.item_list()?.items())) + .accept(node) + { + if let Some(items) = items { + complete_module_items(file, items, Some(name_ref), acc); + } + break; + } else if enclosing_fn.is_none() { + if let Some(fn_def) = ast::FnDef::cast(node) { + enclosing_fn = Some(fn_def); + let scopes = FnScopes::new(fn_def); + complete_fn(name_ref, &scopes, acc); + } + } + } + enclosing_fn +} + +fn complete_module_items( + file: &SourceFileNode, + items: AstChildren, + this_item: Option, + acc: &mut Vec, +) { + let scope = ModuleScope::new(items); // FIXME + acc.extend( + scope + .entries() + .iter() + .filter(|entry| { + let syntax = entry.ptr().resolve(file); + Some(syntax.borrowed()) != this_item.map(|it| it.syntax()) + }) + .map(|entry| CompletionItem { + label: entry.name().to_string(), + lookup: None, + snippet: None, + }), + ); +} + +fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Vec) { + let mut shadowed = FxHashSet::default(); + acc.extend( + scopes + .scope_chain(name_ref.syntax()) + .flat_map(|scope| scopes.entries(scope).iter()) + .filter(|entry| shadowed.insert(entry.name())) + .map(|entry| CompletionItem { + label: entry.name().to_string(), + lookup: None, + snippet: None, + }), + ); + if scopes.self_param.is_some() { + acc.push(CompletionItem { + label: "self".to_string(), + lookup: None, + snippet: None, + }) + } +} + +fn complete_path( + acc: &mut Vec, + db: &RootDatabase, + file_id: FileId, + crate_path: Vec, +) -> Cancelable<()> { + let source_root_id = db.file_source_root(file_id); + let module_tree = db.module_tree(source_root_id)?; + let module_id = match module_tree.any_module_for_source(ModuleSource::SourceFile(file_id)) { + None => return Ok(()), + Some(it) => it, + }; + let target_module_id = match find_target_module(&module_tree, module_id, crate_path) { + None => return Ok(()), + Some(it) => it, + }; + let module_scope = db.module_scope(source_root_id, target_module_id)?; + let completions = module_scope.entries().iter().map(|entry| CompletionItem { + label: entry.name().to_string(), + lookup: None, + snippet: None, + }); + acc.extend(completions); + Ok(()) +} + +fn find_target_module( + module_tree: &ModuleTree, + module_id: ModuleId, + mut crate_path: Vec, +) -> Option { + crate_path.pop(); + let mut target_module = module_id.root(&module_tree); + for name in crate_path { + target_module = target_module.child(module_tree, name.text().as_str())?; + } + Some(target_module) +} + +fn complete_mod_item_snippets(acc: &mut Vec) { + acc.push(CompletionItem { + label: "tfn".to_string(), + lookup: None, + snippet: Some("#[test]\nfn $1() {\n $0\n}".to_string()), + }); + acc.push(CompletionItem { + label: "pub(crate)".to_string(), + lookup: None, + snippet: Some("pub(crate) $0".to_string()), + }) +} + +fn complete_expr_keywords( + file: &SourceFileNode, + fn_def: ast::FnDef, + name_ref: ast::NameRef, + acc: &mut Vec, +) { + acc.push(keyword("if", "if $0 {}")); + acc.push(keyword("match", "match $0 {}")); + acc.push(keyword("while", "while $0 {}")); + acc.push(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.push(keyword("else", "else {$0}")); + acc.push(keyword("else if", "else if $0 {}")); + } + } + } + if is_in_loop_body(name_ref) { + acc.push(keyword("continue", "continue")); + acc.push(keyword("break", "break")); + } + acc.extend(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, snip: &str) -> CompletionItem { + CompletionItem { + label: kw.to_string(), + lookup: None, + snippet: Some(snip.to_string()), + } +} + +fn complete_expr_snippets(acc: &mut Vec) { + acc.push(CompletionItem { + label: "pd".to_string(), + lookup: None, + snippet: Some("eprintln!(\"$0 = {:?}\", $0);".to_string()), + }); + acc.push(CompletionItem { + label: "ppd".to_string(), + lookup: None, + snippet: Some("eprintln!(\"$0 = {:#?}\", $0);".to_string()), + }); +} + + -- cgit v1.2.3