From 12810b93c5141b9ae31f4af17dcc61b0166314b0 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 21 Dec 2018 19:10:07 +0300 Subject: [PATCH 1/8] wip --- crates/ra_analysis/src/completion.rs | 33 +++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs index a11e98ac0be..ae12802564e 100644 --- a/crates/ra_analysis/src/completion.rs +++ b/crates/ra_analysis/src/completion.rs @@ -8,6 +8,8 @@ use ra_syntax::{ ast, AstNode, SyntaxNodeRef, + SourceFileNode, + TextUnit, }; use ra_db::SyntaxDatabase; use rustc_hash::{FxHashMap}; @@ -27,11 +29,6 @@ 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(); @@ -59,6 +56,32 @@ pub(crate) fn completions( 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, +} + +impl SyntaxContext { + pub(super) fn new(original_file: &SourceFileNode, offset: TextUnit) -> SyntaxContext { + let file = { + let edit = AtomTextEdit::insert(offset, "intellijRulezz".to_string()); + original_file.reparse(&edit) + }; + 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); + } + } + } + + 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 From c2bf174e9c3f994d83e7e72b6e15c9b26c5b31a2 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 21 Dec 2018 20:25:29 +0300 Subject: [PATCH 2/8] Start splitting completion into components --- crates/ra_analysis/src/completion.rs | 227 +++++++++--------- .../src/completion/complete_fn_param.rs | 107 +++++++++ .../src/completion/complete_keywords.rs | 206 ++++++++++++++++ .../src/completion/reference_completion.rs | 225 +---------------- 4 files changed, 425 insertions(+), 340 deletions(-) create mode 100644 crates/ra_analysis/src/completion/complete_fn_param.rs create mode 100644 crates/ra_analysis/src/completion/complete_keywords.rs diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs index ae12802564e..39066d51f69 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; + } + } + } + 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; + } + } } } } - - 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)) - } - }) - .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 00000000000..d05a5e3cf5c --- /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 00000000000..d0a6ec19e3c --- /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 c2ac9545376..15ff4c5dd43 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( From cbe67339df2bbcb17e12ad74e8b8cd53baffb9f7 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 21 Dec 2018 20:55:00 +0300 Subject: [PATCH 3/8] more completion components --- crates/ra_analysis/src/completion.rs | 39 +++++---- ...mplete_keywords.rs => complete_keyword.rs} | 0 .../src/completion/complete_snippet.rs | 78 ++++++++++++++++++ .../src/completion/reference_completion.rs | 82 +------------------ 4 files changed, 104 insertions(+), 95 deletions(-) rename crates/ra_analysis/src/completion/{complete_keywords.rs => complete_keyword.rs} (100%) create mode 100644 crates/ra_analysis/src/completion/complete_snippet.rs diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs index 39066d51f69..883b3e8510d 100644 --- a/crates/ra_analysis/src/completion.rs +++ b/crates/ra_analysis/src/completion.rs @@ -2,7 +2,8 @@ mod completion_item; mod reference_completion; mod complete_fn_param; -mod complete_keywords; +mod complete_keyword; +mod complete_snippet; use ra_editor::find_node_at_offset; use ra_text_edit::AtomTextEdit; @@ -49,7 +50,9 @@ pub(crate) fn completions( 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); + complete_keyword::complete_expr_keyword(&mut acc, &ctx); + complete_snippet::complete_expr_snippet(&mut acc, &ctx); + complete_snippet::complete_item_snippet(&mut acc, &ctx); Ok(Some(acc)) } @@ -61,10 +64,12 @@ pub(super) struct SyntaxContext<'a> { leaf: SyntaxNodeRef<'a>, enclosing_fn: Option>, is_param: bool, - /// a single-indent path, like `foo`. + /// A single-indent path, like `foo`. is_trivial_path: bool, after_if: bool, is_stmt: bool, + /// Something is typed at the "top" level, in module or impl/trait. + is_new_item: bool, } impl SyntaxContext<'_> { @@ -77,6 +82,7 @@ impl SyntaxContext<'_> { is_trivial_path: false, after_if: false, is_stmt: false, + is_new_item: false, }; ctx.fill(original_file, offset); Some(ctx) @@ -112,17 +118,22 @@ impl SyntaxContext<'_> { } } 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 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, diff --git a/crates/ra_analysis/src/completion/complete_keywords.rs b/crates/ra_analysis/src/completion/complete_keyword.rs similarity index 100% rename from crates/ra_analysis/src/completion/complete_keywords.rs rename to crates/ra_analysis/src/completion/complete_keyword.rs 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..5d6cc5dc9a7 --- /dev/null +++ b/crates/ra_analysis/src/completion/complete_snippet.rs @@ -0,0 +1,78 @@ +use crate::{ + completion::{CompletionItem, Completions, CompletionKind::*, SyntaxContext}, +}; + +pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &SyntaxContext) { + 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: &SyntaxContext) { + 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/reference_completion.rs b/crates/ra_analysis/src/completion/reference_completion.rs index 15ff4c5dd43..46d3819277f 100644 --- a/crates/ra_analysis/src/completion/reference_completion.rs +++ b/crates/ra_analysis/src/completion/reference_completion.rs @@ -32,8 +32,6 @@ 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_snippets(acc); } let module_scope = module.scope(db)?; @@ -56,19 +54,7 @@ pub(super) fn completions( }); } 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), - _ => (), - } - } + NameRefKind::BareIdentInMod => (), } Ok(()) } @@ -162,35 +148,6 @@ fn complete_path( 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_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}; @@ -199,10 +156,6 @@ mod tests { check_completion(code, expected_completions, CompletionKind::Reference); } - 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( @@ -378,37 +331,4 @@ mod tests { "Spam", ); } - - #[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" - "##, - ); - } - } From 2136e75c0bce090d104bb5b5006e48e42fb22a0a Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 21 Dec 2018 23:04:56 +0300 Subject: [PATCH 4/8] move path completion to a separate component --- crates/ra_analysis/src/completion.rs | 43 ++++++--- .../src/completion/complete_path.rs | 95 +++++++++++++++++++ .../src/completion/reference_completion.rs | 33 +------ 3 files changed, 127 insertions(+), 44 deletions(-) create mode 100644 crates/ra_analysis/src/completion/complete_path.rs diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs index 883b3e8510d..d91304bc28b 100644 --- a/crates/ra_analysis/src/completion.rs +++ b/crates/ra_analysis/src/completion.rs @@ -4,13 +4,12 @@ mod reference_completion; mod complete_fn_param; mod complete_keyword; mod complete_snippet; +mod complete_path; use ra_editor::find_node_at_offset; use ra_text_edit::AtomTextEdit; use ra_syntax::{ - algo::{ - find_leaf_at_offset, - }, + algo::find_leaf_at_offset, ast, AstNode, SyntaxNodeRef, @@ -48,11 +47,12 @@ pub(crate) fn completions( reference_completion::completions(&mut acc, db, &module, &file, name_ref)?; } - let ctx = ctry!(SyntaxContext::new(&original_file, position.offset)); + let ctx = ctry!(SyntaxContext::new(db, &original_file, position)?); 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)?; Ok(Some(acc)) } @@ -61,31 +61,44 @@ pub(crate) fn completions( /// exactly is the cursor, syntax-wise. #[derive(Debug)] pub(super) struct SyntaxContext<'a> { + db: &'a db::RootDatabase, leaf: SyntaxNodeRef<'a>, + module: Option, enclosing_fn: Option>, is_param: bool, /// A single-indent path, like `foo`. is_trivial_path: bool, + /// If not a trivial, path, the prefix (qualifier). + path_prefix: Option, after_if: bool, is_stmt: bool, /// Something is typed at the "top" level, in module or impl/trait. is_new_item: bool, } -impl SyntaxContext<'_> { - pub(super) fn new(original_file: &SourceFileNode, offset: TextUnit) -> Option { - let leaf = find_leaf_at_offset(original_file.syntax(), offset).left_biased()?; +impl<'a> SyntaxContext<'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 = SyntaxContext { + db, leaf, + 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, offset); - Some(ctx) + ctx.fill(original_file, position.offset); + Ok(Some(ctx)) } fn fill(&mut self, original_file: &SourceFileNode, offset: TextUnit) { @@ -140,11 +153,13 @@ impl SyntaxContext<'_> { }; 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 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 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..d04503e46a0 --- /dev/null +++ b/crates/ra_analysis/src/completion/complete_path.rs @@ -0,0 +1,95 @@ +use crate::{ + completion::{CompletionItem, Completions, CompletionKind::*, SyntaxContext}, + Cancelable, +}; + +pub(super) fn complete_path(acc: &mut Completions, ctx: &SyntaxContext) -> 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)? { + None => return Ok(()), + Some(it) => it, + }; + 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/reference_completion.rs b/crates/ra_analysis/src/completion/reference_completion.rs index 46d3819277f..459ed8f6f76 100644 --- a/crates/ra_analysis/src/completion/reference_completion.rs +++ b/crates/ra_analysis/src/completion/reference_completion.rs @@ -1,4 +1,4 @@ -use rustc_hash::{FxHashSet}; +use rustc_hash::FxHashSet; use ra_syntax::{ SourceFileNode, AstNode, ast, @@ -6,7 +6,7 @@ use ra_syntax::{ }; use hir::{ self, - FnScopes, Def, Path + FnScopes, Path }; use crate::{ @@ -53,7 +53,7 @@ pub(super) fn completions( .add_to(acc) }); } - NameRefKind::Path(path) => complete_path(acc, db, module, path)?, + NameRefKind::Path(_) => (), NameRefKind::BareIdentInMod => (), } Ok(()) @@ -121,33 +121,6 @@ fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Completions) } } -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(()) -} - #[cfg(test)] mod tests { use crate::completion::{CompletionKind, check_completion}; From ccca5aae43a27c94180bb099bcc09bb6c29c2ea3 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sat, 22 Dec 2018 00:52:02 +0300 Subject: [PATCH 5/8] scope-based copmletions on original file --- crates/ra_analysis/src/completion.rs | 18 +- .../src/completion/complete_path.rs | 2 +- .../src/completion/complete_scope.rs | 171 ++++++++++ .../src/completion/reference_completion.rs | 307 ------------------ crates/ra_hir/src/function/scope.rs | 47 ++- 5 files changed, 223 insertions(+), 322 deletions(-) create mode 100644 crates/ra_analysis/src/completion/complete_scope.rs delete mode 100644 crates/ra_analysis/src/completion/reference_completion.rs diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs index d91304bc28b..93edcc4c267 100644 --- a/crates/ra_analysis/src/completion.rs +++ b/crates/ra_analysis/src/completion.rs @@ -1,10 +1,10 @@ mod completion_item; -mod reference_completion; 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; @@ -33,26 +33,16 @@ pub(crate) fn completions( 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!(SyntaxContext::new(db, &original_file, position)?); let mut acc = Completions::default(); - // First, let's try to complete a reference to some declaration. - if let Some(name_ref) = find_node_at_offset::(file.syntax(), position.offset) { - reference_completion::completions(&mut acc, db, &module, &file, name_ref)?; - } - - let ctx = ctry!(SyntaxContext::new(db, &original_file, position)?); 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)) } @@ -62,6 +52,7 @@ pub(crate) fn completions( #[derive(Debug)] pub(super) struct SyntaxContext<'a> { db: &'a db::RootDatabase, + offset: TextUnit, leaf: SyntaxNodeRef<'a>, module: Option, enclosing_fn: Option>, @@ -88,6 +79,7 @@ impl<'a> SyntaxContext<'a> { let mut ctx = SyntaxContext { db, leaf, + offset: position.offset, module, enclosing_fn: None, is_param: false, diff --git a/crates/ra_analysis/src/completion/complete_path.rs b/crates/ra_analysis/src/completion/complete_path.rs index d04503e46a0..8374ec34612 100644 --- a/crates/ra_analysis/src/completion/complete_path.rs +++ b/crates/ra_analysis/src/completion/complete_path.rs @@ -9,8 +9,8 @@ pub(super) fn complete_path(acc: &mut Completions, ctx: &SyntaxContext) -> Cance _ => return Ok(()), }; let def_id = match module.resolve_path(ctx.db, path)? { - None => return Ok(()), Some(it) => it, + None => return Ok(()), }; let target_module = match def_id.resolve(ctx.db)? { hir::Def::Module(it) => it, 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..4ffd630165c --- /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::{ + completion::{CompletionItem, Completions, CompletionKind::*, SyntaxContext}, + Cancelable +}; + +pub(super) fn complete_scope(acc: &mut Completions, ctx: &SyntaxContext) -> 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 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_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") + } +} 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 459ed8f6f76..00000000000 --- a/crates/ra_analysis/src/completion/reference_completion.rs +++ /dev/null @@ -1,307 +0,0 @@ -use rustc_hash::FxHashSet; -use ra_syntax::{ - SourceFileNode, AstNode, - ast, - SyntaxKind::*, -}; -use hir::{ - self, - FnScopes, 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); - } - - 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(_) => (), - NameRefKind::BareIdentInMod => (), - } - 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); - } -} - -#[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 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", - ); - } -} 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); } From 200cc0a1e355cbe63dfea844a31b90ea13d42ad5 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sat, 22 Dec 2018 00:55:20 +0300 Subject: [PATCH 6/8] rename tests --- .../ra_analysis/src/completion/complete_scope.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/ra_analysis/src/completion/complete_scope.rs b/crates/ra_analysis/src/completion/complete_scope.rs index 4ffd630165c..ddaf13b884a 100644 --- a/crates/ra_analysis/src/completion/complete_scope.rs +++ b/crates/ra_analysis/src/completion/complete_scope.rs @@ -64,7 +64,7 @@ mod tests { } #[test] - fn test_completion_let_scope() { + fn completes_bindings_from_let() { check_reference_completion( r" fn quux(x: i32) { @@ -78,7 +78,7 @@ mod tests { } #[test] - fn test_completion_if_let_scope() { + fn completes_bindings_from_if_let() { check_reference_completion( r" fn quux() { @@ -96,7 +96,7 @@ mod tests { } #[test] - fn test_completion_for_scope() { + fn completes_bindings_from_for() { check_reference_completion( r" fn quux() { @@ -110,7 +110,7 @@ mod tests { } #[test] - fn test_completion_mod_scope() { + fn completes_module_items() { check_reference_completion( r" struct Foo; @@ -124,7 +124,7 @@ mod tests { } #[test] - fn test_completion_mod_scope_nested() { + fn completes_module_items_in_nested_modules() { check_reference_completion( r" struct Foo; @@ -138,7 +138,7 @@ mod tests { } #[test] - fn test_complete_type() { + fn completes_return_type() { check_reference_completion( r" struct Foo; @@ -149,7 +149,7 @@ mod tests { } #[test] - fn test_complete_shadowing() { + fn dont_show_to_completions_for_shadowing() { check_reference_completion( r" fn foo() -> { @@ -165,7 +165,7 @@ mod tests { } #[test] - fn test_complete_self() { + fn completes_self_in_methods() { check_reference_completion(r"impl S { fn foo(&self) { <|> } }", "self") } } From a8e04a702827c454f5336c82262b0963df7fe484 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sat, 22 Dec 2018 01:01:40 +0300 Subject: [PATCH 7/8] docs --- crates/ra_analysis/src/completion.rs | 170 ++---------------- .../src/completion/complete_fn_param.rs | 4 +- .../src/completion/complete_keyword.rs | 4 +- .../src/completion/complete_path.rs | 4 +- .../src/completion/complete_scope.rs | 4 +- .../src/completion/complete_snippet.rs | 6 +- .../src/completion/completion_context.rs | 156 ++++++++++++++++ 7 files changed, 181 insertions(+), 167 deletions(-) create mode 100644 crates/ra_analysis/src/completion/completion_context.rs diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs index 93edcc4c267..2d61a3aef53 100644 --- a/crates/ra_analysis/src/completion.rs +++ b/crates/ra_analysis/src/completion.rs @@ -1,4 +1,5 @@ mod completion_item; +mod completion_context; mod complete_fn_param; mod complete_keyword; @@ -6,34 +7,33 @@ 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::find_leaf_at_offset, - ast, - AstNode, - SyntaxNodeRef, - SourceFileNode, - TextUnit, - SyntaxKind::*, -}; use ra_db::SyntaxDatabase; -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); - let ctx = ctry!(SyntaxContext::new(db, &original_file, position)?); + let ctx = ctry!(CompletionContext::new(db, &original_file, position)?); let mut acc = Completions::default(); @@ -47,148 +47,6 @@ pub(crate) fn completions( Ok(Some(acc)) } -/// `SyntaxContext` is created early during completion to figure out, where -/// exactly is the cursor, syntax-wise. -#[derive(Debug)] -pub(super) struct SyntaxContext<'a> { - db: &'a db::RootDatabase, - offset: TextUnit, - leaf: SyntaxNodeRef<'a>, - module: Option, - enclosing_fn: Option>, - is_param: bool, - /// A single-indent path, like `foo`. - is_trivial_path: bool, - /// If not a trivial, path, the prefix (qualifier). - path_prefix: Option, - after_if: bool, - is_stmt: bool, - /// Something is typed at the "top" level, in module or impl/trait. - is_new_item: bool, -} - -impl<'a> SyntaxContext<'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 = SyntaxContext { - 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(), - } -} - #[cfg(test)] fn check_completion(code: &str, expected_completions: &str, kind: CompletionKind) { use crate::mock_analysis::{single_file_with_position, analysis_and_position}; diff --git a/crates/ra_analysis/src/completion/complete_fn_param.rs b/crates/ra_analysis/src/completion/complete_fn_param.rs index d05a5e3cf5c..3ec507fdf54 100644 --- a/crates/ra_analysis/src/completion/complete_fn_param.rs +++ b/crates/ra_analysis/src/completion/complete_fn_param.rs @@ -8,14 +8,14 @@ use ra_syntax::{ use rustc_hash::{FxHashMap}; use crate::{ - completion::{SyntaxContext, Completions, CompletionKind, CompletionItem}, + 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: &SyntaxContext) { +pub(super) fn complete_fn_param(acc: &mut Completions, ctx: &CompletionContext) { if !ctx.is_param { return; } diff --git a/crates/ra_analysis/src/completion/complete_keyword.rs b/crates/ra_analysis/src/completion/complete_keyword.rs index d0a6ec19e3c..2ee36430ec6 100644 --- a/crates/ra_analysis/src/completion/complete_keyword.rs +++ b/crates/ra_analysis/src/completion/complete_keyword.rs @@ -6,10 +6,10 @@ use ra_syntax::{ }; use crate::{ - completion::{SyntaxContext, CompletionItem, Completions, CompletionKind::*}, + completion::{CompletionContext, CompletionItem, Completions, CompletionKind::*}, }; -pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &SyntaxContext) { +pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &CompletionContext) { if !ctx.is_trivial_path { return; } diff --git a/crates/ra_analysis/src/completion/complete_path.rs b/crates/ra_analysis/src/completion/complete_path.rs index 8374ec34612..41e439b1bab 100644 --- a/crates/ra_analysis/src/completion/complete_path.rs +++ b/crates/ra_analysis/src/completion/complete_path.rs @@ -1,9 +1,9 @@ use crate::{ - completion::{CompletionItem, Completions, CompletionKind::*, SyntaxContext}, + completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}, Cancelable, }; -pub(super) fn complete_path(acc: &mut Completions, ctx: &SyntaxContext) -> Cancelable<()> { +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(()), diff --git a/crates/ra_analysis/src/completion/complete_scope.rs b/crates/ra_analysis/src/completion/complete_scope.rs index ddaf13b884a..c1ab19d5bce 100644 --- a/crates/ra_analysis/src/completion/complete_scope.rs +++ b/crates/ra_analysis/src/completion/complete_scope.rs @@ -2,11 +2,11 @@ use rustc_hash::FxHashSet; use ra_syntax::TextUnit; use crate::{ - completion::{CompletionItem, Completions, CompletionKind::*, SyntaxContext}, + completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}, Cancelable }; -pub(super) fn complete_scope(acc: &mut Completions, ctx: &SyntaxContext) -> Cancelable<()> { +pub(super) fn complete_scope(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { if !ctx.is_trivial_path { return Ok(()); } diff --git a/crates/ra_analysis/src/completion/complete_snippet.rs b/crates/ra_analysis/src/completion/complete_snippet.rs index 5d6cc5dc9a7..6816ae6959d 100644 --- a/crates/ra_analysis/src/completion/complete_snippet.rs +++ b/crates/ra_analysis/src/completion/complete_snippet.rs @@ -1,8 +1,8 @@ use crate::{ - completion::{CompletionItem, Completions, CompletionKind::*, SyntaxContext}, + completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}, }; -pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &SyntaxContext) { +pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &CompletionContext) { if !(ctx.is_trivial_path && ctx.enclosing_fn.is_some()) { return; } @@ -16,7 +16,7 @@ pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &SyntaxContext) .add_to(acc); } -pub(super) fn complete_item_snippet(acc: &mut Completions, ctx: &SyntaxContext) { +pub(super) fn complete_item_snippet(acc: &mut Completions, ctx: &CompletionContext) { if !ctx.is_new_item { return; } 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(), + } +} From 2ae87ffc9afe67945e2ad655c3577b589ff640ab Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sat, 22 Dec 2018 01:02:43 +0300 Subject: [PATCH 8/8] cleanup --- crates/ra_analysis/src/completion/complete_fn_param.rs | 10 +++------- crates/ra_analysis/src/completion/complete_keyword.rs | 4 +--- crates/ra_analysis/src/completion/complete_path.rs | 2 +- crates/ra_analysis/src/completion/complete_scope.rs | 2 +- crates/ra_analysis/src/completion/complete_snippet.rs | 4 +--- 5 files changed, 7 insertions(+), 15 deletions(-) diff --git a/crates/ra_analysis/src/completion/complete_fn_param.rs b/crates/ra_analysis/src/completion/complete_fn_param.rs index 3ec507fdf54..6a6213e67bf 100644 --- a/crates/ra_analysis/src/completion/complete_fn_param.rs +++ b/crates/ra_analysis/src/completion/complete_fn_param.rs @@ -1,15 +1,11 @@ use ra_syntax::{ - algo::{ - visit::{visitor_ctx, VisitorCtx} - }, + algo::visit::{visitor_ctx, VisitorCtx}, ast, AstNode, }; -use rustc_hash::{FxHashMap}; +use rustc_hash::FxHashMap; -use crate::{ - completion::{CompletionContext, Completions, CompletionKind, CompletionItem}, -}; +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 diff --git a/crates/ra_analysis/src/completion/complete_keyword.rs b/crates/ra_analysis/src/completion/complete_keyword.rs index 2ee36430ec6..dead15bb6f0 100644 --- a/crates/ra_analysis/src/completion/complete_keyword.rs +++ b/crates/ra_analysis/src/completion/complete_keyword.rs @@ -5,9 +5,7 @@ use ra_syntax::{ SyntaxKind::*, SyntaxNodeRef, }; -use crate::{ - completion::{CompletionContext, CompletionItem, Completions, CompletionKind::*}, -}; +use crate::completion::{CompletionContext, CompletionItem, Completions, CompletionKind::*}; pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &CompletionContext) { if !ctx.is_trivial_path { diff --git a/crates/ra_analysis/src/completion/complete_path.rs b/crates/ra_analysis/src/completion/complete_path.rs index 41e439b1bab..5fc24af7209 100644 --- a/crates/ra_analysis/src/completion/complete_path.rs +++ b/crates/ra_analysis/src/completion/complete_path.rs @@ -1,6 +1,6 @@ use crate::{ - completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}, Cancelable, + completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}, }; pub(super) fn complete_path(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { diff --git a/crates/ra_analysis/src/completion/complete_scope.rs b/crates/ra_analysis/src/completion/complete_scope.rs index c1ab19d5bce..d07c0e46d97 100644 --- a/crates/ra_analysis/src/completion/complete_scope.rs +++ b/crates/ra_analysis/src/completion/complete_scope.rs @@ -2,8 +2,8 @@ use rustc_hash::FxHashSet; use ra_syntax::TextUnit; use crate::{ + Cancelable, completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}, - Cancelable }; pub(super) fn complete_scope(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { diff --git a/crates/ra_analysis/src/completion/complete_snippet.rs b/crates/ra_analysis/src/completion/complete_snippet.rs index 6816ae6959d..ccd68832b4a 100644 --- a/crates/ra_analysis/src/completion/complete_snippet.rs +++ b/crates/ra_analysis/src/completion/complete_snippet.rs @@ -1,6 +1,4 @@ -use crate::{ - completion::{CompletionItem, Completions, CompletionKind::*, CompletionContext}, -}; +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()) {