From c2bf174e9c3f994d83e7e72b6e15c9b26c5b31a2 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 21 Dec 2018 20:25:29 +0300 Subject: Start splitting completion into components --- crates/ra_analysis/src/completion.rs | 225 ++++++++++----------- .../src/completion/complete_fn_param.rs | 107 ++++++++++ .../src/completion/complete_keywords.rs | 206 +++++++++++++++++++ .../src/completion/reference_completion.rs | 225 +-------------------- 4 files changed, 424 insertions(+), 339 deletions(-) create mode 100644 crates/ra_analysis/src/completion/complete_fn_param.rs create mode 100644 crates/ra_analysis/src/completion/complete_keywords.rs (limited to 'crates/ra_analysis/src') diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs index ae1280256..39066d51f 100644 --- a/crates/ra_analysis/src/completion.rs +++ b/crates/ra_analysis/src/completion.rs @@ -1,18 +1,23 @@ mod completion_item; mod reference_completion; +mod complete_fn_param; +mod complete_keywords; + use ra_editor::find_node_at_offset; use ra_text_edit::AtomTextEdit; use ra_syntax::{ - algo::visit::{visitor_ctx, VisitorCtx}, + algo::{ + find_leaf_at_offset, + }, ast, AstNode, SyntaxNodeRef, SourceFileNode, TextUnit, + SyntaxKind::*, }; use ra_db::SyntaxDatabase; -use rustc_hash::{FxHashMap}; use hir::source_binder; use crate::{ @@ -29,99 +34,133 @@ pub(crate) fn completions( ) -> 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 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); - } + let ctx = ctry!(SyntaxContext::new(&original_file, position.offset)); + complete_fn_param::complete_fn_param(&mut acc, &ctx); + complete_keywords::complete_expr_keyword(&mut acc, &ctx); + Ok(Some(acc)) } /// `SyntaxContext` is created early during completion to figure out, where /// exactly is the cursor, syntax-wise. #[derive(Debug)] -pub(super) enum SyntaxContext<'a> { - ParameterName(SyntaxNodeRef<'a>), - Other, +pub(super) struct SyntaxContext<'a> { + leaf: SyntaxNodeRef<'a>, + enclosing_fn: Option>, + is_param: bool, + /// a single-indent path, like `foo`. + is_trivial_path: bool, + after_if: bool, + is_stmt: bool, } -impl SyntaxContext { - pub(super) fn new(original_file: &SourceFileNode, offset: TextUnit) -> SyntaxContext { +impl SyntaxContext<'_> { + pub(super) fn new(original_file: &SourceFileNode, offset: TextUnit) -> Option { + let leaf = find_leaf_at_offset(original_file.syntax(), offset).left_biased()?; + let mut ctx = SyntaxContext { + leaf, + enclosing_fn: None, + is_param: false, + is_trivial_path: false, + after_if: false, + is_stmt: false, + }; + ctx.fill(original_file, offset); + 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()) { - if let Some(node) = find_leaf_at_offset(original_file, offset).left_biased() { - return SyntaxContext::ParameterName(node); - } + self.is_param = true; + return; } } - - SyntaxContext::Other } -} - -/// 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)) + 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) => return Some(NameRefKind::BareIdentInMod), + // _ => (), + // } + 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(path) = Path::from_ast(path) { + // if !path.is_ident() { + // return Some(NameRefKind::Path(path)); + // } + // } + 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; + } + } + } } - }) - .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; - }) + } } } @@ -143,51 +182,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 000000000..d05a5e3cf --- /dev/null +++ b/crates/ra_analysis/src/completion/complete_fn_param.rs @@ -0,0 +1,107 @@ +use ra_syntax::{ + algo::{ + visit::{visitor_ctx, VisitorCtx} + }, + ast, + AstNode, +}; +use rustc_hash::{FxHashMap}; + +use crate::{ + completion::{SyntaxContext, 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: &SyntaxContext) { + 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_keywords.rs b/crates/ra_analysis/src/completion/complete_keywords.rs new file mode 100644 index 000000000..d0a6ec19e --- /dev/null +++ b/crates/ra_analysis/src/completion/complete_keywords.rs @@ -0,0 +1,206 @@ +use ra_syntax::{ + algo::visit::{visitor, Visitor}, + AstNode, + ast::{self, LoopBodyOwner}, + SyntaxKind::*, SyntaxNodeRef, +}; + +use crate::{ + completion::{SyntaxContext, CompletionItem, Completions, CompletionKind::*}, +}; + +pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &SyntaxContext) { + 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/reference_completion.rs b/crates/ra_analysis/src/completion/reference_completion.rs index c2ac95453..15ff4c5dd 100644 --- a/crates/ra_analysis/src/completion/reference_completion.rs +++ b/crates/ra_analysis/src/completion/reference_completion.rs @@ -1,9 +1,7 @@ use rustc_hash::{FxHashSet}; -use ra_editor::find_node_at_offset; use ra_syntax::{ - algo::visit::{visitor, Visitor}, SourceFileNode, AstNode, - ast::{self, LoopBodyOwner}, + ast, SyntaxKind::*, }; use hir::{ @@ -21,7 +19,7 @@ pub(super) fn completions( acc: &mut Completions, db: &RootDatabase, module: &hir::Module, - file: &SourceFileNode, + _file: &SourceFileNode, name_ref: ast::NameRef, ) -> Cancelable<()> { let kind = match classify_name_ref(name_ref) { @@ -34,7 +32,7 @@ pub(super) fn completions( 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_keywords(&file, fn_def, name_ref, acc); complete_expr_snippets(acc); } @@ -182,91 +180,6 @@ fn ${1:feature}() { .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);") @@ -286,10 +199,6 @@ mod tests { 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); } @@ -470,134 +379,6 @@ mod tests { ); } - #[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( -- cgit v1.2.3