From f3fb59d7077801a3a68d2d03eef17d59c2925ae8 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Wed, 31 Oct 2018 20:59:17 +0300 Subject: Move completion to ra_analysis While we should handle completion for isolated file, it's better achieved by using empty Analysis, rather than working only with &File: we need memoization for type inference even inside a single file. --- crates/ra_analysis/src/completion.rs | 622 ++++++++++++++++++++- crates/ra_analysis/src/descriptors/module/scope.rs | 6 +- crates/ra_analysis/src/imp.rs | 10 +- crates/ra_analysis/src/lib.rs | 9 +- crates/ra_analysis/src/mock_analysis.rs | 71 +++ 5 files changed, 705 insertions(+), 13 deletions(-) create mode 100644 crates/ra_analysis/src/mock_analysis.rs (limited to 'crates/ra_analysis/src') diff --git a/crates/ra_analysis/src/completion.rs b/crates/ra_analysis/src/completion.rs index 869ab5afb..340ae3f66 100644 --- a/crates/ra_analysis/src/completion.rs +++ b/crates/ra_analysis/src/completion.rs @@ -1,7 +1,10 @@ -use ra_editor::{CompletionItem, find_node_at_offset}; +use rustc_hash::{FxHashMap, FxHashSet}; +use ra_editor::{find_node_at_offset}; use ra_syntax::{ - AtomEdit, File, TextUnit, AstNode, - ast, + AtomEdit, File, TextUnit, AstNode, SyntaxNodeRef, + algo::visit::{visitor, visitor_ctx, Visitor, VisitorCtx}, + ast::{self, AstChildren, LoopBodyOwner, ModuleItemOwner}, + SyntaxKind::*, }; use crate::{ @@ -9,9 +12,21 @@ use crate::{ input::FilesDatabase, db::{self, SyntaxDatabase}, descriptors::DescriptorDatabase, - descriptors::module::{ModuleTree, ModuleId}, + descriptors::function::FnScopes, + descriptors::module::{ModuleTree, ModuleId, ModuleScope}, }; + +#[derive(Debug)] +pub struct CompletionItem { + /// What user sees in pop-up + pub label: String, + /// What string is used for filtering, defaults to label + pub lookup: Option, + /// What is inserted, defaults to label + pub snippet: Option, +} + pub(crate) fn resolve_based_completion(db: &db::RootDatabase, file_id: FileId, offset: TextUnit) -> Cancelable>> { let source_root_id = db.file_source_root(file_id); let file = db.file_syntax(file_id); @@ -73,3 +88,602 @@ fn crate_path(name_ref: ast::NameRef) -> Option> { res.reverse(); Some(res) } + + +pub(crate) fn scope_completion( + db: &db::RootDatabase, + file_id: FileId, + offset: TextUnit, +) -> Option> { + let original_file = db.file_syntax(file_id); + // Insert a fake ident to get a valid parse tree + let file = { + let edit = AtomEdit::insert(offset, "intellijRulezz".to_string()); + original_file.reparse(&edit) + }; + let mut has_completions = false; + let mut res = Vec::new(); + if let Some(name_ref) = find_node_at_offset::(file.syntax(), offset) { + has_completions = true; + complete_name_ref(&file, name_ref, &mut res); + // special case, `trait T { fn foo(i_am_a_name_ref) {} }` + if is_node::(name_ref.syntax()) { + param_completions(name_ref.syntax(), &mut res); + } + 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(ROOT) | Some(ITEM_LIST) => complete_mod_item_snippets(&mut res), + _ => (), + } + } + if let Some(name) = find_node_at_offset::(file.syntax(), offset) { + if is_node::(name.syntax()) { + has_completions = true; + param_completions(name.syntax(), &mut res); + } + } + if has_completions { + Some(res) + } else { + None + } +} + +fn complete_module_items( + file: &File, + items: AstChildren, + this_item: Option, + acc: &mut Vec, +) { + let scope = ModuleScope::from_items(items); + acc.extend( + scope + .entries() + .iter() + .filter(|entry| { + let syntax = entry.ptr().resolve(file); + Some(syntax.borrowed()) != this_item.map(|it| it.syntax()) + }) + .map(|entry| CompletionItem { + label: entry.name().to_string(), + lookup: None, + snippet: None, + }), + ); +} + +fn complete_name_ref( + file: &File, + name_ref: ast::NameRef, + acc: &mut Vec, +) { + if !is_node::(name_ref.syntax()) { + return; + } + let mut visited_fn = false; + for node in name_ref.syntax().ancestors() { + if let Some(items) = visitor() + .visit::(|it| Some(it.items())) + .visit::(|it| Some(it.item_list()?.items())) + .accept(node) + { + if let Some(items) = items { + complete_module_items(file, items, Some(name_ref), acc); + } + break; + } else if !visited_fn { + if let Some(fn_def) = ast::FnDef::cast(node) { + visited_fn = true; + complete_expr_keywords(&file, fn_def, name_ref, acc); + complete_expr_snippets(acc); + let scopes = FnScopes::new(fn_def); + complete_fn(name_ref, &scopes, acc); + } + } + } +} + +fn param_completions(ctx: SyntaxNodeRef, acc: &mut Vec) { + let mut params = FxHashMap::default(); + for node in ctx.ancestors() { + let _ = visitor_ctx(&mut params) + .visit::(process) + .visit::(process) + .accept(node); + } + params + .into_iter() + .filter_map(|(label, (count, param))| { + let lookup = param.pat()?.syntax().text().to_string(); + if count < 2 { + None + } else { + Some((label, lookup)) + } + }) + .for_each(|(label, lookup)| { + acc.push(CompletionItem { + label, + lookup: Some(lookup), + snippet: None, + }) + }); + + fn process<'a, N: ast::FnDefOwner<'a>>( + node: N, + params: &mut FxHashMap)>, + ) { + node.functions() + .filter_map(|it| it.param_list()) + .flat_map(|it| it.params()) + .for_each(|param| { + let text = param.syntax().text().to_string(); + params.entry(text).or_insert((0, param)).0 += 1; + }) + } +} + +fn is_node<'a, N: AstNode<'a>>(node: SyntaxNodeRef<'a>) -> bool { + match node.ancestors().filter_map(N::cast).next() { + None => false, + Some(n) => n.syntax().range() == node.range(), + } +} + +fn complete_expr_keywords( + file: &File, + fn_def: ast::FnDef, + name_ref: ast::NameRef, + acc: &mut Vec, +) { + acc.push(keyword("if", "if $0 {}")); + acc.push(keyword("match", "match $0 {}")); + acc.push(keyword("while", "while $0 {}")); + acc.push(keyword("loop", "loop {$0}")); + + if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { + if let Some(if_expr) = find_node_at_offset::(file.syntax(), off) { + if if_expr.syntax().range().end() < name_ref.syntax().range().start() { + acc.push(keyword("else", "else {$0}")); + acc.push(keyword("else if", "else if $0 {}")); + } + } + } + if is_in_loop_body(name_ref) { + acc.push(keyword("continue", "continue")); + acc.push(keyword("break", "break")); + } + acc.extend(complete_return(fn_def, name_ref)); +} + +fn is_in_loop_body(name_ref: ast::NameRef) -> bool { + for node in name_ref.syntax().ancestors() { + if node.kind() == FN_DEF || node.kind() == LAMBDA_EXPR { + break; + } + let loop_body = visitor() + .visit::(LoopBodyOwner::loop_body) + .visit::(LoopBodyOwner::loop_body) + .visit::(LoopBodyOwner::loop_body) + .accept(node); + if let Some(Some(body)) = loop_body { + if name_ref.syntax().range().is_subrange(&body.syntax().range()) { + return true; + } + } + } + false +} + +fn complete_return(fn_def: ast::FnDef, name_ref: ast::NameRef) -> Option { + // let is_last_in_block = name_ref.syntax().ancestors().filter_map(ast::Expr::cast) + // .next() + // .and_then(|it| it.syntax().parent()) + // .and_then(ast::Block::cast) + // .is_some(); + + // if is_last_in_block { + // return None; + // } + + let is_stmt = match name_ref + .syntax() + .ancestors() + .filter_map(ast::ExprStmt::cast) + .next() + { + None => false, + Some(expr_stmt) => expr_stmt.syntax().range() == name_ref.syntax().range(), + }; + let snip = match (is_stmt, fn_def.ret_type().is_some()) { + (true, true) => "return $0;", + (true, false) => "return;", + (false, true) => "return $0", + (false, false) => "return", + }; + Some(keyword("return", snip)) +} + +fn keyword(kw: &str, snip: &str) -> CompletionItem { + CompletionItem { + label: kw.to_string(), + lookup: None, + snippet: Some(snip.to_string()), + } +} + +fn complete_expr_snippets(acc: &mut Vec) { + acc.push(CompletionItem { + label: "pd".to_string(), + lookup: None, + snippet: Some("eprintln!(\"$0 = {:?}\", $0);".to_string()), + }); + acc.push(CompletionItem { + label: "ppd".to_string(), + lookup: None, + snippet: Some("eprintln!(\"$0 = {:#?}\", $0);".to_string()), + }); +} + +fn complete_mod_item_snippets(acc: &mut Vec) { + acc.push(CompletionItem { + label: "tfn".to_string(), + lookup: None, + snippet: Some("#[test]\nfn $1() {\n $0\n}".to_string()), + }); + acc.push(CompletionItem { + label: "pub(crate)".to_string(), + lookup: None, + snippet: Some("pub(crate) $0".to_string()), + }) +} + +fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Vec) { + let mut shadowed = FxHashSet::default(); + acc.extend( + scopes + .scope_chain(name_ref.syntax()) + .flat_map(|scope| scopes.entries(scope).iter()) + .filter(|entry| shadowed.insert(entry.name())) + .map(|entry| CompletionItem { + label: entry.name().to_string(), + lookup: None, + snippet: None, + }), + ); + if scopes.self_param.is_some() { + acc.push(CompletionItem { + label: "self".to_string(), + lookup: None, + snippet: None, + }) + } +} + +#[cfg(test)] +mod tests { + use test_utils::{assert_eq_dbg, extract_offset}; + + use crate::FileId; + use crate::mock_analysis::MockAnalysis; + + use super::*; + + fn check_scope_completion(code: &str, expected_completions: &str) { + let (off, code) = extract_offset(&code); + let analysis = MockAnalysis::with_files(&[("/main.rs", &code)]).analysis(); + let file_id = FileId(1); + let completions = scope_completion(&analysis.imp.db, file_id, off) + .unwrap() + .into_iter() + .filter(|c| c.snippet.is_none()) + .collect::>(); + assert_eq_dbg(expected_completions, &completions); + } + + fn check_snippet_completion(code: &str, expected_completions: &str) { + let (off, code) = extract_offset(&code); + let analysis = MockAnalysis::with_files(&[("/main.rs", &code)]).analysis(); + let file_id = FileId(1); + let completions = scope_completion(&analysis.imp.db, file_id, off) + .unwrap() + .into_iter() + .filter(|c| c.snippet.is_some()) + .collect::>(); + assert_eq_dbg(expected_completions, &completions); + } + + #[test] + fn test_completion_let_scope() { + check_scope_completion( + r" + fn quux(x: i32) { + let y = 92; + 1 + <|>; + let z = (); + } + ", + r#"[CompletionItem { label: "y", lookup: None, snippet: None }, + CompletionItem { label: "x", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#, + ); + } + + #[test] + fn test_completion_if_let_scope() { + check_scope_completion( + r" + fn quux() { + if let Some(x) = foo() { + let y = 92; + }; + if let Some(a) = bar() { + let b = 62; + 1 + <|> + } + } + ", + r#"[CompletionItem { label: "b", lookup: None, snippet: None }, + CompletionItem { label: "a", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#, + ); + } + + #[test] + fn test_completion_for_scope() { + check_scope_completion( + r" + fn quux() { + for x in &[1, 2, 3] { + <|> + } + } + ", + r#"[CompletionItem { label: "x", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#, + ); + } + + #[test] + fn test_completion_mod_scope() { + check_scope_completion( + r" + struct Foo; + enum Baz {} + fn quux() { + <|> + } + ", + r#"[CompletionItem { label: "Foo", lookup: None, snippet: None }, + CompletionItem { label: "Baz", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#, + ); + } + + #[test] + fn test_completion_mod_scope_no_self_use() { + check_scope_completion( + r" + use foo<|>; + ", + r#"[]"#, + ); + } + + #[test] + fn test_completion_mod_scope_nested() { + check_scope_completion( + r" + struct Foo; + mod m { + struct Bar; + fn quux() { <|> } + } + ", + r#"[CompletionItem { label: "Bar", lookup: None, snippet: None }, + CompletionItem { label: "quux", lookup: None, snippet: None }]"#, + ); + } + + #[test] + fn test_complete_type() { + check_scope_completion( + r" + struct Foo; + fn x() -> <|> + ", + r#"[CompletionItem { label: "Foo", lookup: None, snippet: None }, + CompletionItem { label: "x", lookup: None, snippet: None }]"#, + ) + } + + #[test] + fn test_complete_shadowing() { + check_scope_completion( + r" + fn foo() -> { + let bar = 92; + { + let bar = 62; + <|> + } + } + ", + r#"[CompletionItem { label: "bar", lookup: None, snippet: None }, + CompletionItem { label: "foo", lookup: None, snippet: None }]"#, + ) + } + + #[test] + fn test_complete_self() { + check_scope_completion( + r" + impl S { fn foo(&self) { <|> } } + ", + r#"[CompletionItem { label: "self", lookup: None, snippet: None }]"#, + ) + } + + #[test] + fn test_completion_kewords() { + check_snippet_completion(r" + fn quux() { + <|> + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + } + + #[test] + fn test_completion_else() { + check_snippet_completion(r" + fn quux() { + if true { + () + } <|> + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "else", lookup: None, snippet: Some("else {$0}") }, + CompletionItem { label: "else if", lookup: None, snippet: Some("else if $0 {}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + } + + #[test] + fn test_completion_return_value() { + check_snippet_completion(r" + fn quux() -> i32 { + <|> + 92 + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0;") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + check_snippet_completion(r" + fn quux() { + <|> + 92 + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return;") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + } + + #[test] + fn test_completion_return_no_stmt() { + check_snippet_completion(r" + fn quux() -> i32 { + match () { + () => <|> + } + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + } + + #[test] + fn test_continue_break_completion() { + check_snippet_completion(r" + fn quux() -> i32 { + loop { <|> } + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "continue", lookup: None, snippet: Some("continue") }, + CompletionItem { label: "break", lookup: None, snippet: Some("break") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + check_snippet_completion(r" + fn quux() -> i32 { + loop { || { <|> } } + } + ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, + CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, + CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, + CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, + CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }, + CompletionItem { label: "pd", lookup: None, snippet: Some("eprintln!(\"$0 = {:?}\", $0);") }, + CompletionItem { label: "ppd", lookup: None, snippet: Some("eprintln!(\"$0 = {:#?}\", $0);") }]"#); + } + + #[test] + fn test_param_completion_last_param() { + check_scope_completion(r" + fn foo(file_id: FileId) {} + fn bar(file_id: FileId) {} + fn baz(file<|>) {} + ", r#"[CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); + } + + #[test] + fn test_param_completion_nth_param() { + check_scope_completion(r" + fn foo(file_id: FileId) {} + fn bar(file_id: FileId) {} + fn baz(file<|>, x: i32) {} + ", r#"[CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); + } + + #[test] + fn test_param_completion_trait_param() { + check_scope_completion(r" + pub(crate) trait SourceRoot { + pub fn contains(&self, file_id: FileId) -> bool; + pub fn module_map(&self) -> &ModuleMap; + pub fn lines(&self, file_id: FileId) -> &LineIndex; + pub fn syntax(&self, file<|>) + } + ", r#"[CompletionItem { label: "self", lookup: None, snippet: None }, + CompletionItem { label: "SourceRoot", lookup: None, snippet: None }, + CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); + } + + #[test] + fn test_item_snippets() { + // check_snippet_completion(r" + // <|> + // ", + // r##"[CompletionItem { label: "tfn", lookup: None, snippet: Some("#[test]\nfn $1() {\n $0\n}") }]"##, + // ); + check_snippet_completion(r" + #[cfg(test)] + mod tests { + <|> + } + ", + r##"[CompletionItem { label: "tfn", lookup: None, snippet: Some("#[test]\nfn $1() {\n $0\n}") }, + CompletionItem { label: "pub(crate)", lookup: None, snippet: Some("pub(crate) $0") }]"##, + ); + } +} diff --git a/crates/ra_analysis/src/descriptors/module/scope.rs b/crates/ra_analysis/src/descriptors/module/scope.rs index 0f8f325ab..846b8b44f 100644 --- a/crates/ra_analysis/src/descriptors/module/scope.rs +++ b/crates/ra_analysis/src/descriptors/module/scope.rs @@ -30,8 +30,12 @@ enum EntryKind { impl ModuleScope { pub fn new(file: &File) -> ModuleScope { + ModuleScope::from_items(file.ast().items()) + } + + pub fn from_items<'a>(items: impl Iterator>) -> ModuleScope { let mut entries = Vec::new(); - for item in file.ast().items() { + for item in items { let entry = match item { ast::ModuleItem::StructDef(item) => Entry::new(item), ast::ModuleItem::EnumDef(item) => Entry::new(item), diff --git a/crates/ra_analysis/src/imp.rs b/crates/ra_analysis/src/imp.rs index 6473a1dbc..38d4b6a23 100644 --- a/crates/ra_analysis/src/imp.rs +++ b/crates/ra_analysis/src/imp.rs @@ -3,7 +3,7 @@ use std::{ sync::Arc, }; -use ra_editor::{self, find_node_at_offset, FileSymbol, LineIndex, LocalEdit, CompletionItem}; +use ra_editor::{self, find_node_at_offset, FileSymbol, LineIndex, LocalEdit}; use ra_syntax::{ ast::{self, ArgListOwner, Expr, NameOwner}, AstNode, File, SmolStr, @@ -26,6 +26,7 @@ use crate::{ module::{ModuleTree, Problem}, function::{FnDescriptor, FnId}, }, + completion::{scope_completion, resolve_based_completion, CompletionItem}, symbol_index::SymbolIndex, syntax_ptr::SyntaxPtrDatabase, CrateGraph, CrateId, Diagnostic, FileId, FileResolver, FileSystemEdit, Position, @@ -179,7 +180,7 @@ impl AnalysisHostImpl { #[derive(Debug)] pub(crate) struct AnalysisImpl { - db: db::RootDatabase, + pub(crate) db: db::RootDatabase, } impl AnalysisImpl { @@ -249,12 +250,11 @@ impl AnalysisImpl { pub fn completions(&self, file_id: FileId, offset: TextUnit) -> Cancelable>> { let mut res = Vec::new(); let mut has_completions = false; - let file = self.file_syntax(file_id); - if let Some(scope_based) = ra_editor::scope_completion(&file, offset) { + if let Some(scope_based) = scope_completion(&self.db, file_id, offset) { res.extend(scope_based); has_completions = true; } - if let Some(scope_based) = crate::completion::resolve_based_completion(&self.db, file_id, offset)? { + if let Some(scope_based) = resolve_based_completion(&self.db, file_id, offset)? { res.extend(scope_based); has_completions = true; } diff --git a/crates/ra_analysis/src/lib.rs b/crates/ra_analysis/src/lib.rs index a77c9a5fa..776010281 100644 --- a/crates/ra_analysis/src/lib.rs +++ b/crates/ra_analysis/src/lib.rs @@ -13,6 +13,7 @@ mod imp; mod symbol_index; mod completion; mod syntax_ptr; +mod mock_analysis; use std::{ fmt, @@ -30,10 +31,12 @@ use crate::{ pub use crate::{ descriptors::function::FnDescriptor, - input::{FileId, FileResolver, CrateGraph, CrateId} + completion::CompletionItem, + input::{FileId, FileResolver, CrateGraph, CrateId}, + mock_analysis::MockAnalysis, }; pub use ra_editor::{ - CompletionItem, FileSymbol, Fold, FoldKind, HighlightedRange, LineIndex, Runnable, + FileSymbol, Fold, FoldKind, HighlightedRange, LineIndex, Runnable, RunnableKind, StructureNode, }; @@ -197,7 +200,7 @@ impl Query { #[derive(Debug)] pub struct Analysis { - imp: AnalysisImpl, + pub(crate) imp: AnalysisImpl, } impl Analysis { diff --git a/crates/ra_analysis/src/mock_analysis.rs b/crates/ra_analysis/src/mock_analysis.rs new file mode 100644 index 000000000..1c1dbee7c --- /dev/null +++ b/crates/ra_analysis/src/mock_analysis.rs @@ -0,0 +1,71 @@ + +use std::sync::Arc; + +use relative_path::{RelativePath, RelativePathBuf}; + +use crate::{ + AnalysisChange, Analysis, AnalysisHost, FileId, FileResolver, +}; + +/// Mock analysis is used in test to bootstrap an AnalysisHost/Analysis +/// from a set of in-memory files. +#[derive(Debug, Default)] +pub struct MockAnalysis { + files: Vec<(String, String)>, +} + +impl MockAnalysis { + pub fn new() -> MockAnalysis { + MockAnalysis::default() + } + pub fn with_files(files: &[(&str, &str)]) -> MockAnalysis { + let files = files.iter() + .map(|it| (it.0.to_string(), it.1.to_string())) + .collect(); + MockAnalysis { files } + } + pub fn analysis_host(self) -> AnalysisHost { + let mut host = AnalysisHost::new(); + let mut file_map = Vec::new(); + let mut change = AnalysisChange::new(); + for (id, (path, contents)) in self.files.into_iter().enumerate() { + let file_id = FileId((id + 1) as u32); + assert!(path.starts_with('/')); + let path = RelativePathBuf::from_path(&path[1..]).unwrap(); + change.add_file(file_id, contents); + file_map.push((file_id, path)); + } + change.set_file_resolver(Arc::new(FileMap(file_map))); + host.apply_change(change); + host + } + pub fn analysis(self) -> Analysis { + self.analysis_host().analysis() + } +} + +#[derive(Debug)] +struct FileMap(Vec<(FileId, RelativePathBuf)>); + +impl FileMap { + fn iter<'a>(&'a self) -> impl Iterator + 'a { + self.0 + .iter() + .map(|(id, path)| (*id, path.as_relative_path())) + } + + fn path(&self, id: FileId) -> &RelativePath { + self.iter().find(|&(it, _)| it == id).unwrap().1 + } +} + +impl FileResolver for FileMap { + fn file_stem(&self, id: FileId) -> String { + self.path(id).file_stem().unwrap().to_string() + } + fn resolve(&self, id: FileId, rel: &RelativePath) -> Option { + let path = self.path(id).join(rel).normalize(); + let id = self.iter().find(|&(_, p)| path == p)?.0; + Some(id) + } +} -- cgit v1.2.3