From 5b573deb20b15451788dd2861e9fc6e69ed0472e Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 8 Jan 2019 22:33:36 +0300 Subject: fix usages after rename --- crates/ra_ide_api/Cargo.toml | 23 + crates/ra_ide_api/src/call_info.rs | 451 ++++++++++++++++++ crates/ra_ide_api/src/completion.rs | 77 ++++ crates/ra_ide_api/src/completion/complete_dot.rs | 121 +++++ .../ra_ide_api/src/completion/complete_fn_param.rs | 102 +++++ .../ra_ide_api/src/completion/complete_keyword.rs | 339 ++++++++++++++ crates/ra_ide_api/src/completion/complete_path.rs | 128 ++++++ crates/ra_ide_api/src/completion/complete_scope.rs | 192 ++++++++ .../ra_ide_api/src/completion/complete_snippet.rs | 73 +++ .../src/completion/completion_context.rs | 205 +++++++++ .../ra_ide_api/src/completion/completion_item.rs | 244 ++++++++++ crates/ra_ide_api/src/db.rs | 128 ++++++ crates/ra_ide_api/src/extend_selection.rs | 56 +++ crates/ra_ide_api/src/goto_defenition.rs | 139 ++++++ crates/ra_ide_api/src/hover.rs | 257 +++++++++++ crates/ra_ide_api/src/imp.rs | 309 +++++++++++++ crates/ra_ide_api/src/lib.rs | 509 +++++++++++++++++++++ crates/ra_ide_api/src/mock_analysis.rs | 135 ++++++ crates/ra_ide_api/src/runnables.rs | 89 ++++ crates/ra_ide_api/src/symbol_index.rs | 222 +++++++++ crates/ra_ide_api/src/syntax_highlighting.rs | 92 ++++ crates/ra_ide_api/tests/test/main.rs | 249 ++++++++++ crates/ra_ide_api/tests/test/runnables.rs | 109 +++++ crates/ra_lsp_server/Cargo.toml | 2 +- crates/ra_lsp_server/src/conv.rs | 2 +- crates/ra_lsp_server/src/main_loop.rs | 2 +- crates/ra_lsp_server/src/main_loop/handlers.rs | 4 +- .../ra_lsp_server/src/main_loop/subscriptions.rs | 2 +- crates/ra_lsp_server/src/server_world.rs | 6 +- 29 files changed, 4258 insertions(+), 9 deletions(-) create mode 100644 crates/ra_ide_api/Cargo.toml create mode 100644 crates/ra_ide_api/src/call_info.rs create mode 100644 crates/ra_ide_api/src/completion.rs create mode 100644 crates/ra_ide_api/src/completion/complete_dot.rs create mode 100644 crates/ra_ide_api/src/completion/complete_fn_param.rs create mode 100644 crates/ra_ide_api/src/completion/complete_keyword.rs create mode 100644 crates/ra_ide_api/src/completion/complete_path.rs create mode 100644 crates/ra_ide_api/src/completion/complete_scope.rs create mode 100644 crates/ra_ide_api/src/completion/complete_snippet.rs create mode 100644 crates/ra_ide_api/src/completion/completion_context.rs create mode 100644 crates/ra_ide_api/src/completion/completion_item.rs create mode 100644 crates/ra_ide_api/src/db.rs create mode 100644 crates/ra_ide_api/src/extend_selection.rs create mode 100644 crates/ra_ide_api/src/goto_defenition.rs create mode 100644 crates/ra_ide_api/src/hover.rs create mode 100644 crates/ra_ide_api/src/imp.rs create mode 100644 crates/ra_ide_api/src/lib.rs create mode 100644 crates/ra_ide_api/src/mock_analysis.rs create mode 100644 crates/ra_ide_api/src/runnables.rs create mode 100644 crates/ra_ide_api/src/symbol_index.rs create mode 100644 crates/ra_ide_api/src/syntax_highlighting.rs create mode 100644 crates/ra_ide_api/tests/test/main.rs create mode 100644 crates/ra_ide_api/tests/test/runnables.rs (limited to 'crates') diff --git a/crates/ra_ide_api/Cargo.toml b/crates/ra_ide_api/Cargo.toml new file mode 100644 index 000000000..d42a664b6 --- /dev/null +++ b/crates/ra_ide_api/Cargo.toml @@ -0,0 +1,23 @@ +[package] +edition = "2018" +name = "ra_ide_api" +version = "0.1.0" +authors = ["Aleksey Kladov "] + +[dependencies] +itertools = "0.8.0" +log = "0.4.5" +relative-path = "0.4.0" +rayon = "1.0.2" +fst = "0.3.1" +salsa = "0.9.1" +rustc-hash = "1.0" +parking_lot = "0.7.0" +unicase = "2.2.0" + +ra_syntax = { path = "../ra_syntax" } +ra_ide_api_light = { path = "../ra_ide_api_light" } +ra_text_edit = { path = "../ra_text_edit" } +ra_db = { path = "../ra_db" } +hir = { path = "../ra_hir", package = "ra_hir" } +test_utils = { path = "../test_utils" } diff --git a/crates/ra_ide_api/src/call_info.rs b/crates/ra_ide_api/src/call_info.rs new file mode 100644 index 000000000..27b760780 --- /dev/null +++ b/crates/ra_ide_api/src/call_info.rs @@ -0,0 +1,451 @@ +use std::cmp::{max, min}; + +use ra_db::{SyntaxDatabase, Cancelable}; +use ra_syntax::{ + AstNode, SyntaxNode, TextUnit, TextRange, + SyntaxKind::FN_DEF, + ast::{self, ArgListOwner, DocCommentsOwner}, + algo::find_node_at_offset, +}; + +use crate::{FilePosition, CallInfo, db::RootDatabase}; + +/// Computes parameter information for the given call expression. +pub(crate) fn call_info(db: &RootDatabase, position: FilePosition) -> Cancelable> { + let file = db.source_file(position.file_id); + let syntax = file.syntax(); + + // Find the calling expression and it's NameRef + let calling_node = ctry!(FnCallNode::with_node(syntax, position.offset)); + let name_ref = ctry!(calling_node.name_ref()); + + // Resolve the function's NameRef (NOTE: this isn't entirely accurate). + let file_symbols = db.index_resolve(name_ref)?; + let symbol = ctry!(file_symbols.into_iter().find(|it| it.ptr.kind() == FN_DEF)); + let fn_file = db.source_file(symbol.file_id); + let fn_def = symbol.ptr.resolve(&fn_file); + let fn_def = ast::FnDef::cast(&fn_def).unwrap(); + let mut call_info = ctry!(CallInfo::new(fn_def)); + // If we have a calling expression let's find which argument we are on + let num_params = call_info.parameters.len(); + let has_self = fn_def.param_list().and_then(|l| l.self_param()).is_some(); + + if num_params == 1 { + if !has_self { + call_info.active_parameter = Some(0); + } + } else if num_params > 1 { + // Count how many parameters into the call we are. + // TODO: This is best effort for now and should be fixed at some point. + // It may be better to see where we are in the arg_list and then check + // where offset is in that list (or beyond). + // Revisit this after we get documentation comments in. + if let Some(ref arg_list) = calling_node.arg_list() { + let start = arg_list.syntax().range().start(); + + let range_search = TextRange::from_to(start, position.offset); + let mut commas: usize = arg_list + .syntax() + .text() + .slice(range_search) + .to_string() + .matches(',') + .count(); + + // If we have a method call eat the first param since it's just self. + if has_self { + commas += 1; + } + + call_info.active_parameter = Some(commas); + } + } + + Ok(Some(call_info)) +} + +enum FnCallNode<'a> { + CallExpr(&'a ast::CallExpr), + MethodCallExpr(&'a ast::MethodCallExpr), +} + +impl<'a> FnCallNode<'a> { + pub fn with_node(syntax: &'a SyntaxNode, offset: TextUnit) -> Option> { + if let Some(expr) = find_node_at_offset::(syntax, offset) { + return Some(FnCallNode::CallExpr(expr)); + } + if let Some(expr) = find_node_at_offset::(syntax, offset) { + return Some(FnCallNode::MethodCallExpr(expr)); + } + None + } + + pub fn name_ref(&self) -> Option<&'a ast::NameRef> { + match *self { + FnCallNode::CallExpr(call_expr) => Some(match call_expr.expr()?.kind() { + ast::ExprKind::PathExpr(path_expr) => path_expr.path()?.segment()?.name_ref()?, + _ => return None, + }), + + FnCallNode::MethodCallExpr(call_expr) => call_expr + .syntax() + .children() + .filter_map(ast::NameRef::cast) + .nth(0), + } + } + + pub fn arg_list(&self) -> Option<&'a ast::ArgList> { + match *self { + FnCallNode::CallExpr(expr) => expr.arg_list(), + FnCallNode::MethodCallExpr(expr) => expr.arg_list(), + } + } +} + +impl CallInfo { + fn new(node: &ast::FnDef) -> Option { + let mut doc = None; + + // Strip the body out for the label. + let mut label: String = if let Some(body) = node.body() { + let body_range = body.syntax().range(); + let label: String = node + .syntax() + .children() + .filter(|child| !child.range().is_subrange(&body_range)) + .map(|node| node.text().to_string()) + .collect(); + label + } else { + node.syntax().text().to_string() + }; + + if let Some((comment_range, docs)) = extract_doc_comments(node) { + let comment_range = comment_range + .checked_sub(node.syntax().range().start()) + .unwrap(); + let start = comment_range.start().to_usize(); + let end = comment_range.end().to_usize(); + + // Remove the comment from the label + label.replace_range(start..end, ""); + + // Massage markdown + let mut processed_lines = Vec::new(); + let mut in_code_block = false; + for line in docs.lines() { + if line.starts_with("```") { + in_code_block = !in_code_block; + } + + let line = if in_code_block && line.starts_with("```") && !line.contains("rust") { + "```rust".into() + } else { + line.to_string() + }; + + processed_lines.push(line); + } + + if !processed_lines.is_empty() { + doc = Some(processed_lines.join("\n")); + } + } + + Some(CallInfo { + parameters: param_list(node), + label: label.trim().to_owned(), + doc, + active_parameter: None, + }) + } +} + +fn extract_doc_comments(node: &ast::FnDef) -> Option<(TextRange, String)> { + if node.doc_comments().count() == 0 { + return None; + } + + let comment_text = node.doc_comment_text(); + + let (begin, end) = node + .doc_comments() + .map(|comment| comment.syntax().range()) + .map(|range| (range.start().to_usize(), range.end().to_usize())) + .fold((std::usize::MAX, std::usize::MIN), |acc, range| { + (min(acc.0, range.0), max(acc.1, range.1)) + }); + + let range = TextRange::from_to(TextUnit::from_usize(begin), TextUnit::from_usize(end)); + + Some((range, comment_text)) +} + +fn param_list(node: &ast::FnDef) -> Vec { + let mut res = vec![]; + if let Some(param_list) = node.param_list() { + if let Some(self_param) = param_list.self_param() { + res.push(self_param.syntax().text().to_string()) + } + + // Maybe use param.pat here? See if we can just extract the name? + //res.extend(param_list.params().map(|p| p.syntax().text().to_string())); + res.extend( + param_list + .params() + .filter_map(|p| p.pat()) + .map(|pat| pat.syntax().text().to_string()), + ); + } + res +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::mock_analysis::single_file_with_position; + + fn call_info(text: &str) -> CallInfo { + let (analysis, position) = single_file_with_position(text); + analysis.call_info(position).unwrap().unwrap() + } + + #[test] + fn test_fn_signature_two_args_first() { + let info = call_info( + r#"fn foo(x: u32, y: u32) -> u32 {x + y} +fn bar() { foo(<|>3, ); }"#, + ); + + assert_eq!(info.parameters, vec!("x".to_string(), "y".to_string())); + assert_eq!(info.active_parameter, Some(0)); + } + + #[test] + fn test_fn_signature_two_args_second() { + let info = call_info( + r#"fn foo(x: u32, y: u32) -> u32 {x + y} +fn bar() { foo(3, <|>); }"#, + ); + + assert_eq!(info.parameters, vec!("x".to_string(), "y".to_string())); + assert_eq!(info.active_parameter, Some(1)); + } + + #[test] + fn test_fn_signature_for_impl() { + let info = call_info( + r#"struct F; impl F { pub fn new() { F{}} } +fn bar() {let _ : F = F::new(<|>);}"#, + ); + + assert_eq!(info.parameters, Vec::::new()); + assert_eq!(info.active_parameter, None); + } + + #[test] + fn test_fn_signature_for_method_self() { + let info = call_info( + r#"struct F; +impl F { + pub fn new() -> F{ + F{} + } + + pub fn do_it(&self) {} +} + +fn bar() { + let f : F = F::new(); + f.do_it(<|>); +}"#, + ); + + assert_eq!(info.parameters, vec!["&self".to_string()]); + assert_eq!(info.active_parameter, None); + } + + #[test] + fn test_fn_signature_for_method_with_arg() { + let info = call_info( + r#"struct F; +impl F { + pub fn new() -> F{ + F{} + } + + pub fn do_it(&self, x: i32) {} +} + +fn bar() { + let f : F = F::new(); + f.do_it(<|>); +}"#, + ); + + assert_eq!(info.parameters, vec!["&self".to_string(), "x".to_string()]); + assert_eq!(info.active_parameter, Some(1)); + } + + #[test] + fn test_fn_signature_with_docs_simple() { + let info = call_info( + r#" +/// test +// non-doc-comment +fn foo(j: u32) -> u32 { + j +} + +fn bar() { + let _ = foo(<|>); +} +"#, + ); + + assert_eq!(info.parameters, vec!["j".to_string()]); + assert_eq!(info.active_parameter, Some(0)); + assert_eq!(info.label, "fn foo(j: u32) -> u32".to_string()); + assert_eq!(info.doc, Some("test".into())); + } + + #[test] + fn test_fn_signature_with_docs() { + let info = call_info( + r#" +/// Adds one to the number given. +/// +/// # Examples +/// +/// ``` +/// let five = 5; +/// +/// assert_eq!(6, my_crate::add_one(5)); +/// ``` +pub fn add_one(x: i32) -> i32 { + x + 1 +} + +pub fn do() { + add_one(<|> +}"#, + ); + + assert_eq!(info.parameters, vec!["x".to_string()]); + assert_eq!(info.active_parameter, Some(0)); + assert_eq!(info.label, "pub fn add_one(x: i32) -> i32".to_string()); + assert_eq!( + info.doc, + Some( + r#"Adds one to the number given. + +# Examples + +```rust +let five = 5; + +assert_eq!(6, my_crate::add_one(5)); +```"# + .into() + ) + ); + } + + #[test] + fn test_fn_signature_with_docs_impl() { + let info = call_info( + r#" +struct addr; +impl addr { + /// Adds one to the number given. + /// + /// # Examples + /// + /// ``` + /// let five = 5; + /// + /// assert_eq!(6, my_crate::add_one(5)); + /// ``` + pub fn add_one(x: i32) -> i32 { + x + 1 + } +} + +pub fn do_it() { + addr {}; + addr::add_one(<|>); +}"#, + ); + + assert_eq!(info.parameters, vec!["x".to_string()]); + assert_eq!(info.active_parameter, Some(0)); + assert_eq!(info.label, "pub fn add_one(x: i32) -> i32".to_string()); + assert_eq!( + info.doc, + Some( + r#"Adds one to the number given. + +# Examples + +```rust +let five = 5; + +assert_eq!(6, my_crate::add_one(5)); +```"# + .into() + ) + ); + } + + #[test] + fn test_fn_signature_with_docs_from_actix() { + let info = call_info( + r#" +pub trait WriteHandler +where + Self: Actor, + Self::Context: ActorContext, +{ + /// Method is called when writer emits error. + /// + /// If this method returns `ErrorAction::Continue` writer processing + /// continues otherwise stream processing stops. + fn error(&mut self, err: E, ctx: &mut Self::Context) -> Running { + Running::Stop + } + + /// Method is called when writer finishes. + /// + /// By default this method stops actor's `Context`. + fn finished(&mut self, ctx: &mut Self::Context) { + ctx.stop() + } +} + +pub fn foo() { + WriteHandler r; + r.finished(<|>); +} + +"#, + ); + + assert_eq!( + info.parameters, + vec!["&mut self".to_string(), "ctx".to_string()] + ); + assert_eq!(info.active_parameter, Some(1)); + assert_eq!( + info.doc, + Some( + r#"Method is called when writer finishes. + +By default this method stops actor's `Context`."# + .into() + ) + ); + } + +} diff --git a/crates/ra_ide_api/src/completion.rs b/crates/ra_ide_api/src/completion.rs new file mode 100644 index 000000000..ce777a771 --- /dev/null +++ b/crates/ra_ide_api/src/completion.rs @@ -0,0 +1,77 @@ +mod completion_item; +mod completion_context; + +mod complete_dot; +mod complete_fn_param; +mod complete_keyword; +mod complete_snippet; +mod complete_path; +mod complete_scope; + +use ra_db::SyntaxDatabase; + +use crate::{ + db, + Cancelable, FilePosition, + completion::{ + completion_item::{Completions, CompletionKind}, + completion_context::CompletionContext, + }, +}; + +pub use crate::completion::completion_item::{CompletionItem, InsertText, CompletionItemKind}; + +/// Main entry point for completion. We run completion 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 really weird. +/// +/// Once the context is collected, we run a series of completion routines which +/// look at the context and produce completion items. One subtelty about this +/// phase is that completion engine should not filter by the substring which is +/// already present, it should give all possible variants for the identifier at +/// the caret. In other words, for +/// +/// ```no-run +/// fn f() { +/// let foo = 92; +/// let _ = bar<|> +/// } +/// ``` +/// +/// `foo` *should* be present among the completion variants. Filtering by +/// identifier prefix/fuzzy match should be done higher in the stack, together +/// with ordering of completions (currently this is done by the client). +pub(crate) fn completions( + db: &db::RootDatabase, + position: FilePosition, +) -> Cancelable> { + let original_file = db.source_file(position.file_id); + let ctx = ctry!(CompletionContext::new(db, &original_file, position)?); + + let mut acc = Completions::default(); + + complete_fn_param::complete_fn_param(&mut acc, &ctx); + complete_keyword::complete_expr_keyword(&mut acc, &ctx); + complete_keyword::complete_use_tree_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)?; + complete_dot::complete_dot(&mut acc, &ctx)?; + + Ok(Some(acc)) +} + +#[cfg(test)] +fn check_completion(code: &str, expected_completions: &str, kind: CompletionKind) { + use crate::mock_analysis::{single_file_with_position, analysis_and_position}; + let (analysis, position) = if code.contains("//-") { + analysis_and_position(code) + } else { + single_file_with_position(code) + }; + let completions = completions(&analysis.db, position).unwrap().unwrap(); + completions.assert_match(expected_completions, kind); +} diff --git a/crates/ra_ide_api/src/completion/complete_dot.rs b/crates/ra_ide_api/src/completion/complete_dot.rs new file mode 100644 index 000000000..5d4e60dc5 --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_dot.rs @@ -0,0 +1,121 @@ +use hir::{Ty, Def}; + +use crate::Cancelable; +use crate::completion::{CompletionContext, Completions, CompletionKind, CompletionItem, CompletionItemKind}; + +/// Complete dot accesses, i.e. fields or methods (currently only fields). +pub(super) fn complete_dot(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { + let (function, receiver) = match (&ctx.function, ctx.dot_receiver) { + (Some(function), Some(receiver)) => (function, receiver), + _ => return Ok(()), + }; + let infer_result = function.infer(ctx.db)?; + let syntax_mapping = function.body_syntax_mapping(ctx.db)?; + let expr = match syntax_mapping.node_expr(receiver) { + Some(expr) => expr, + None => return Ok(()), + }; + let receiver_ty = infer_result[expr].clone(); + if !ctx.is_method_call { + complete_fields(acc, ctx, receiver_ty)?; + } + Ok(()) +} + +fn complete_fields(acc: &mut Completions, ctx: &CompletionContext, receiver: Ty) -> Cancelable<()> { + for receiver in receiver.autoderef(ctx.db) { + match receiver { + Ty::Adt { def_id, .. } => { + match def_id.resolve(ctx.db)? { + Def::Struct(s) => { + let variant_data = s.variant_data(ctx.db)?; + for field in variant_data.fields() { + CompletionItem::new( + CompletionKind::Reference, + field.name().to_string(), + ) + .kind(CompletionItemKind::Field) + .add_to(acc); + } + } + // TODO unions + _ => {} + } + } + Ty::Tuple(fields) => { + for (i, _ty) in fields.iter().enumerate() { + CompletionItem::new(CompletionKind::Reference, i.to_string()) + .kind(CompletionItemKind::Field) + .add_to(acc); + } + } + _ => {} + }; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::completion::*; + + fn check_ref_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Reference); + } + + #[test] + fn test_struct_field_completion() { + check_ref_completion( + r" + struct A { the_field: u32 } + fn foo(a: A) { + a.<|> + } + ", + r#"the_field"#, + ); + } + + #[test] + fn test_struct_field_completion_self() { + check_ref_completion( + r" + struct A { the_field: u32 } + impl A { + fn foo(self) { + self.<|> + } + } + ", + r#"the_field"#, + ); + } + + #[test] + fn test_struct_field_completion_autoderef() { + check_ref_completion( + r" + struct A { the_field: u32 } + impl A { + fn foo(&self) { + self.<|> + } + } + ", + r#"the_field"#, + ); + } + + #[test] + fn test_no_struct_field_completion_for_method_call() { + check_ref_completion( + r" + struct A { the_field: u32 } + fn foo(a: A) { + a.<|>() + } + ", + r#""#, + ); + } +} diff --git a/crates/ra_ide_api/src/completion/complete_fn_param.rs b/crates/ra_ide_api/src/completion/complete_fn_param.rs new file mode 100644 index 000000000..c1739e47e --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_fn_param.rs @@ -0,0 +1,102 @@ +use ra_syntax::{ + algo::visit::{visitor_ctx, VisitorCtx}, + ast, + AstNode, +}; +use rustc_hash::FxHashMap; + +use crate::completion::{CompletionContext, Completions, CompletionKind, CompletionItem}; + +/// Complete repeated parametes, both name and type. For example, if all +/// functions in a file have a `spam: &mut Spam` parameter, a completion with +/// `spam: &mut Spam` insert text/label and `spam` lookup string will be +/// suggested. +pub(super) fn complete_fn_param(acc: &mut Completions, ctx: &CompletionContext) { + if !ctx.is_param { + return; + } + + let mut params = FxHashMap::default(); + for node in ctx.leaf.ancestors() { + let _ = visitor_ctx(&mut params) + .visit::(process) + .visit::(process) + .accept(node); + } + params + .into_iter() + .filter_map(|(label, (count, param))| { + let lookup = param.pat()?.syntax().text().to_string(); + if count < 2 { + None + } else { + Some((label, lookup)) + } + }) + .for_each(|(label, lookup)| { + CompletionItem::new(CompletionKind::Magic, label) + .lookup_by(lookup) + .add_to(acc) + }); + + fn process<'a, N: ast::FnDefOwner>( + node: &'a 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_ide_api/src/completion/complete_keyword.rs b/crates/ra_ide_api/src/completion/complete_keyword.rs new file mode 100644 index 000000000..d350f06ce --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_keyword.rs @@ -0,0 +1,339 @@ +use ra_syntax::{ + algo::visit::{visitor, Visitor}, + AstNode, + ast::{self, LoopBodyOwner}, + SyntaxKind::*, SyntaxNode, +}; + +use crate::completion::{CompletionContext, CompletionItem, Completions, CompletionKind, CompletionItemKind}; + +pub(super) fn complete_use_tree_keyword(acc: &mut Completions, ctx: &CompletionContext) { + // complete keyword "crate" in use stmt + match (ctx.use_item_syntax.as_ref(), ctx.path_prefix.as_ref()) { + (Some(_), None) => { + CompletionItem::new(CompletionKind::Keyword, "crate") + .kind(CompletionItemKind::Keyword) + .lookup_by("crate") + .snippet("crate::") + .add_to(acc); + CompletionItem::new(CompletionKind::Keyword, "self") + .kind(CompletionItemKind::Keyword) + .lookup_by("self") + .add_to(acc); + CompletionItem::new(CompletionKind::Keyword, "super") + .kind(CompletionItemKind::Keyword) + .lookup_by("super") + .add_to(acc); + } + (Some(_), Some(_)) => { + CompletionItem::new(CompletionKind::Keyword, "self") + .kind(CompletionItemKind::Keyword) + .lookup_by("self") + .add_to(acc); + CompletionItem::new(CompletionKind::Keyword, "super") + .kind(CompletionItemKind::Keyword) + .lookup_by("super") + .add_to(acc); + } + _ => {} + } +} + +fn keyword(kw: &str, snippet: &str) -> CompletionItem { + CompletionItem::new(CompletionKind::Keyword, kw) + .kind(CompletionItemKind::Keyword) + .snippet(snippet) + .build() +} + +pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &CompletionContext) { + if !ctx.is_trivial_path { + return; + } + + let fn_def = match ctx.function_syntax { + 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) { + if ctx.can_be_stmt { + acc.add(keyword("continue", "continue;")); + acc.add(keyword("break", "break;")); + } else { + acc.add(keyword("continue", "continue")); + acc.add(keyword("break", "break")); + } + } + acc.add_all(complete_return(fn_def, ctx.can_be_stmt)); +} + +fn is_in_loop_body(leaf: &SyntaxNode) -> 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, can_be_stmt: bool) -> Option { + let snip = match (can_be_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)) +} + +#[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 completes_keywords_in_use_stmt() { + check_keyword_completion( + r" + use <|> + ", + r#" + crate "crate" "crate::" + self "self" + super "super" + "#, + ); + + check_keyword_completion( + r" + use a::<|> + ", + r#" + self "self" + super "super" + "#, + ); + + check_keyword_completion( + r" + use a::{b, <|>} + ", + r#" + self "self" + super "super" + "#, + ); + } + + #[test] + fn completes_various_keywords_in_function() { + check_keyword_completion( + r" + fn quux() { + <|> + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return;" + "#, + ); + } + + #[test] + fn completes_else_after_if() { + 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 dont_add_semi_after_return_if_not_a_statement() { + 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 last_return_in_block_has_semi() { + check_keyword_completion( + r" + fn quux() -> i32 { + if condition { + <|> + } + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return $0;" + "#, + ); + check_keyword_completion( + r" + fn quux() -> i32 { + if condition { + <|> + } + let x = 92; + x + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + return "return $0;" + "#, + ); + } + + #[test] + fn completes_break_and_continue_in_loops() { + 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;" + "#, + ); + // No completion: lambda isolates control flow + 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 no_semi_after_break_continue_in_expr() { + check_keyword_completion( + r" + fn f() { + loop { + match () { + () => br<|> + } + } + } + ", + r#" + if "if $0 {}" + match "match $0 {}" + while "while $0 {}" + loop "loop {$0}" + continue "continue" + break "break" + return "return" + "#, + ) + } +} diff --git a/crates/ra_ide_api/src/completion/complete_path.rs b/crates/ra_ide_api/src/completion/complete_path.rs new file mode 100644 index 000000000..4723a65a6 --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_path.rs @@ -0,0 +1,128 @@ +use crate::{ + Cancelable, + completion::{CompletionItem, CompletionItemKind, Completions, CompletionKind, CompletionContext}, +}; + +pub(super) fn complete_path(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { + let (path, module) = match (&ctx.path_prefix, &ctx.module) { + (Some(path), Some(module)) => (path.clone(), module), + _ => return Ok(()), + }; + let def_id = match module.resolve_path(ctx.db, &path)?.take_types() { + Some(it) => it, + None => return Ok(()), + }; + match def_id.resolve(ctx.db)? { + hir::Def::Module(module) => { + let module_scope = module.scope(ctx.db)?; + module_scope.entries().for_each(|(name, res)| { + CompletionItem::new(CompletionKind::Reference, name.to_string()) + .from_resolution(ctx, res) + .add_to(acc) + }); + } + hir::Def::Enum(e) => e + .variants(ctx.db)? + .into_iter() + .for_each(|(name, _variant)| { + CompletionItem::new(CompletionKind::Reference, name.to_string()) + .kind(CompletionItemKind::EnumVariant) + .add_to(acc) + }), + _ => return Ok(()), + }; + 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", + ); + } + + #[test] + fn completes_enum_variant() { + check_reference_completion( + " + //- /lib.rs + enum E { Foo, Bar(i32) } + fn foo() { let _ = E::<|> } + ", + "Foo;Bar", + ); + } + + #[test] + fn dont_render_function_parens_in_use_item() { + check_reference_completion( + " + //- /lib.rs + mod m { pub fn foo() {} } + use crate::m::f<|>; + ", + "foo", + ) + } +} diff --git a/crates/ra_ide_api/src/completion/complete_scope.rs b/crates/ra_ide_api/src/completion/complete_scope.rs new file mode 100644 index 000000000..ee9052d3d --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_scope.rs @@ -0,0 +1,192 @@ +use rustc_hash::FxHashSet; +use ra_syntax::TextUnit; + +use crate::{ + Cancelable, + completion::{CompletionItem, CompletionItemKind, Completions, CompletionKind, CompletionContext}, +}; + +pub(super) fn complete_scope(acc: &mut Completions, ctx: &CompletionContext) -> Cancelable<()> { + if !ctx.is_trivial_path { + return Ok(()); + } + let module = match &ctx.module { + Some(it) => it, + None => return Ok(()), + }; + if let Some(function) = &ctx.function { + let scopes = function.scopes(ctx.db)?; + complete_fn(acc, &scopes, ctx.offset); + } + + let module_scope = module.scope(ctx.db)?; + let (file_id, _) = module.defenition_source(ctx.db)?; + module_scope + .entries() + .filter(|(_name, res)| { + // Don't expose this item + // FIXME: this penetrates through all kinds of abstractions, + // we need to figura out the way to do it less ugly. + match res.import { + None => true, + Some(import) => { + let range = import.range(ctx.db, file_id); + !range.is_subrange(&ctx.leaf.range()) + } + } + }) + .for_each(|(name, res)| { + CompletionItem::new(CompletionKind::Reference, name.to_string()) + .from_resolution(ctx, res) + .add_to(acc) + }); + Ok(()) +} + +fn complete_fn(acc: &mut Completions, scopes: &hir::ScopesWithSyntaxMapping, offset: TextUnit) { + let mut shadowed = FxHashSet::default(); + scopes + .scope_chain_for_offset(offset) + .flat_map(|scope| scopes.scopes.entries(scope).iter()) + .filter(|entry| shadowed.insert(entry.name())) + .for_each(|entry| { + CompletionItem::new(CompletionKind::Reference, entry.name().to_string()) + .kind(CompletionItemKind::Binding) + .add_to(acc) + }); +} + +#[cfg(test)] +mod tests { + use crate::completion::{CompletionKind, check_completion}; + + fn check_reference_completion(code: &str, expected_completions: &str) { + check_completion(code, expected_completions, CompletionKind::Reference); + } + + #[test] + fn completes_bindings_from_let() { + check_reference_completion( + r" + fn quux(x: i32) { + let y = 92; + 1 + <|>; + let z = (); + } + ", + r#"y;x;quux "quux($0)""#, + ); + } + + #[test] + fn completes_bindings_from_if_let() { + check_reference_completion( + r" + fn quux() { + if let Some(x) = foo() { + let y = 92; + }; + if let Some(a) = bar() { + let b = 62; + 1 + <|> + } + } + ", + r#"b;a;quux "quux()$0""#, + ); + } + + #[test] + fn completes_bindings_from_for() { + check_reference_completion( + r" + fn quux() { + for x in &[1, 2, 3] { + <|> + } + } + ", + r#"x;quux "quux()$0""#, + ); + } + + #[test] + fn completes_module_items() { + check_reference_completion( + r" + struct Foo; + enum Baz {} + fn quux() { + <|> + } + ", + r#"quux "quux()$0";Foo;Baz"#, + ); + } + + #[test] + fn completes_module_items_in_nested_modules() { + check_reference_completion( + r" + struct Foo; + mod m { + struct Bar; + fn quux() { <|> } + } + ", + r#"quux "quux()$0";Bar"#, + ); + } + + #[test] + fn completes_return_type() { + check_reference_completion( + r" + struct Foo; + fn x() -> <|> + ", + r#"Foo;x "x()$0""#, + ) + } + + #[test] + fn dont_show_both_completions_for_shadowing() { + check_reference_completion( + r" + fn foo() -> { + let bar = 92; + { + let bar = 62; + <|> + } + } + ", + r#"bar;foo "foo()$0""#, + ) + } + + #[test] + fn completes_self_in_methods() { + check_reference_completion(r"impl S { fn foo(&self) { <|> } }", "self") + } + + #[test] + fn inserts_parens_for_function_calls() { + check_reference_completion( + r" + fn no_args() {} + fn main() { no_<|> } + ", + r#"no_args "no_args()$0" + main "main()$0""#, + ); + check_reference_completion( + r" + fn with_args(x: i32, y: String) {} + fn main() { with_<|> } + ", + r#"main "main()$0" + with_args "with_args($0)""#, + ); + } +} diff --git a/crates/ra_ide_api/src/completion/complete_snippet.rs b/crates/ra_ide_api/src/completion/complete_snippet.rs new file mode 100644 index 000000000..a495751dd --- /dev/null +++ b/crates/ra_ide_api/src/completion/complete_snippet.rs @@ -0,0 +1,73 @@ +use crate::completion::{CompletionItem, Completions, CompletionKind, CompletionItemKind, CompletionContext, completion_item::Builder}; + +fn snippet(label: &str, snippet: &str) -> Builder { + CompletionItem::new(CompletionKind::Snippet, label) + .snippet(snippet) + .kind(CompletionItemKind::Snippet) +} + +pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &CompletionContext) { + if !(ctx.is_trivial_path && ctx.function_syntax.is_some()) { + return; + } + snippet("pd", "eprintln!(\"$0 = {:?}\", $0);").add_to(acc); + snippet("ppd", "eprintln!(\"$0 = {:#?}\", $0);").add_to(acc); +} + +pub(super) fn complete_item_snippet(acc: &mut Completions, ctx: &CompletionContext) { + if !ctx.is_new_item { + return; + } + snippet( + "Test function", + "\ +#[test] +fn ${1:feature}() { + $0 +}", + ) + .lookup_by("tfn") + .add_to(acc); + + snippet("pub(crate)", "pub(crate) $0").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_ide_api/src/completion/completion_context.rs b/crates/ra_ide_api/src/completion/completion_context.rs new file mode 100644 index 000000000..01786bb69 --- /dev/null +++ b/crates/ra_ide_api/src/completion/completion_context.rs @@ -0,0 +1,205 @@ +use ra_text_edit::AtomTextEdit; +use ra_syntax::{ + AstNode, SyntaxNode, SourceFile, TextUnit, TextRange, + ast, + algo::{find_leaf_at_offset, find_covering_node, find_node_at_offset}, + 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: &'a SyntaxNode, + pub(super) module: Option, + pub(super) function: Option, + pub(super) function_syntax: Option<&'a ast::FnDef>, + pub(super) use_item_syntax: Option<&'a ast::UseItem>, + 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, + /// `true` if we are a statement or a last expr in the block. + pub(super) can_be_stmt: bool, + /// Something is typed at the "top" level, in module or impl/trait. + pub(super) is_new_item: bool, + /// The receiver if this is a field or method access, i.e. writing something.<|> + pub(super) dot_receiver: Option<&'a ast::Expr>, + /// If this is a method call in particular, i.e. the () are already there. + pub(super) is_method_call: bool, +} + +impl<'a> CompletionContext<'a> { + pub(super) fn new( + db: &'a db::RootDatabase, + original_file: &'a SourceFile, + 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, + function: None, + function_syntax: None, + use_item_syntax: None, + is_param: false, + is_trivial_path: false, + path_prefix: None, + after_if: false, + can_be_stmt: false, + is_new_item: false, + dot_receiver: None, + is_method_call: false, + }; + ctx.fill(original_file, position.offset); + Ok(Some(ctx)) + } + + fn fill(&mut self, original_file: &'a SourceFile, 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(original_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, original_file: &'a SourceFile, 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; + } + _ => (), + } + + self.use_item_syntax = self.leaf.ancestors().find_map(ast::UseItem::cast); + + self.function_syntax = self + .leaf + .ancestors() + .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) + .find_map(ast::FnDef::cast); + match (&self.module, self.function_syntax) { + (Some(module), Some(fn_def)) => { + let function = source_binder::function_from_module(self.db, module, fn_def); + self.function = Some(function); + } + _ => (), + } + + 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; + + // Find either enclosing expr statement (thing with `;`) or a + // block. If block, check that we are the last expr. + self.can_be_stmt = name_ref + .syntax() + .ancestors() + .find_map(|node| { + if let Some(stmt) = ast::ExprStmt::cast(node) { + return Some(stmt.syntax().range() == name_ref.syntax().range()); + } + if let Some(block) = ast::Block::cast(node) { + return Some( + block.expr().map(|e| e.syntax().range()) + == Some(name_ref.syntax().range()), + ); + } + None + }) + .unwrap_or(false); + + if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { + if let Some(if_expr) = + find_node_at_offset::(original_file.syntax(), off) + { + if if_expr.syntax().range().end() < name_ref.syntax().range().start() { + self.after_if = true; + } + } + } + } + } + if let Some(field_expr) = ast::FieldExpr::cast(parent) { + // The receiver comes before the point of insertion of the fake + // ident, so it should have the same range in the non-modified file + self.dot_receiver = field_expr + .expr() + .map(|e| e.syntax().range()) + .and_then(|r| find_node_with_range(original_file.syntax(), r)); + } + if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) { + // As above + self.dot_receiver = method_call_expr + .expr() + .map(|e| e.syntax().range()) + .and_then(|r| find_node_with_range(original_file.syntax(), r)); + self.is_method_call = true; + } + } +} + +fn find_node_with_range(syntax: &SyntaxNode, range: TextRange) -> Option<&N> { + let node = find_covering_node(syntax, range); + node.ancestors().find_map(N::cast) +} + +fn is_node(node: &SyntaxNode) -> bool { + match node.ancestors().filter_map(N::cast).next() { + None => false, + Some(n) => n.syntax().range() == node.range(), + } +} diff --git a/crates/ra_ide_api/src/completion/completion_item.rs b/crates/ra_ide_api/src/completion/completion_item.rs new file mode 100644 index 000000000..a25b87bee --- /dev/null +++ b/crates/ra_ide_api/src/completion/completion_item.rs @@ -0,0 +1,244 @@ +use hir::PerNs; + +use crate::completion::CompletionContext; + +/// `CompletionItem` describes a single completion variant in the editor pop-up. +/// It is basically a POD with various properties. To construct a +/// `CompletionItem`, use `new` method and the `Builder` struct. +#[derive(Debug)] +pub struct CompletionItem { + /// Used only internally in tests, to check only specific kind of + /// completion. + completion_kind: CompletionKind, + label: String, + lookup: Option, + snippet: Option, + kind: Option, +} + +pub enum InsertText { + PlainText { text: String }, + Snippet { text: String }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompletionItemKind { + Snippet, + Keyword, + Module, + Function, + Struct, + Enum, + EnumVariant, + Binding, + Field, +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum CompletionKind { + /// Parser-based keyword completion. + Keyword, + /// Your usual "complete all valid identifiers". + Reference, + /// "Secret sauce" completions. + Magic, + Snippet, +} + +impl CompletionItem { + pub(crate) fn new(completion_kind: CompletionKind, label: impl Into) -> Builder { + let label = label.into(); + Builder { + completion_kind, + label, + lookup: None, + snippet: None, + kind: None, + } + } + /// What user sees in pop-up in the UI. + pub fn label(&self) -> &str { + &self.label + } + /// What string is used for filtering. + pub fn lookup(&self) -> &str { + self.lookup + .as_ref() + .map(|it| it.as_str()) + .unwrap_or(self.label()) + } + /// What is inserted. + pub fn insert_text(&self) -> InsertText { + match &self.snippet { + None => InsertText::PlainText { + text: self.label.clone(), + }, + Some(it) => InsertText::Snippet { text: it.clone() }, + } + } + + pub fn kind(&self) -> Option { + self.kind + } +} + +/// A helper to make `CompletionItem`s. +#[must_use] +pub(crate) struct Builder { + completion_kind: CompletionKind, + label: String, + lookup: Option, + snippet: Option, + kind: Option, +} + +impl Builder { + pub(crate) fn add_to(self, acc: &mut Completions) { + acc.add(self.build()) + } + + pub(crate) fn build(self) -> CompletionItem { + CompletionItem { + label: self.label, + lookup: self.lookup, + snippet: self.snippet, + kind: self.kind, + completion_kind: self.completion_kind, + } + } + pub(crate) fn lookup_by(mut self, lookup: impl Into) -> Builder { + self.lookup = Some(lookup.into()); + self + } + pub(crate) fn snippet(mut self, snippet: impl Into) -> Builder { + self.snippet = Some(snippet.into()); + self + } + pub(crate) fn kind(mut self, kind: CompletionItemKind) -> Builder { + self.kind = Some(kind); + self + } + pub(super) fn from_resolution( + mut self, + ctx: &CompletionContext, + resolution: &hir::Resolution, + ) -> Builder { + let resolved = resolution.def_id.and_then(|d| d.resolve(ctx.db).ok()); + let kind = match resolved { + PerNs { + types: Some(hir::Def::Module(..)), + .. + } => CompletionItemKind::Module, + PerNs { + types: Some(hir::Def::Struct(..)), + .. + } => CompletionItemKind::Struct, + PerNs { + types: Some(hir::Def::Enum(..)), + .. + } => CompletionItemKind::Enum, + PerNs { + values: Some(hir::Def::Function(function)), + .. + } => return self.from_function(ctx, function), + _ => return self, + }; + self.kind = Some(kind); + self + } + + fn from_function(mut self, ctx: &CompletionContext, function: hir::Function) -> Builder { + // If not an import, add parenthesis automatically. + if ctx.use_item_syntax.is_none() { + if function.signature(ctx.db).args().is_empty() { + self.snippet = Some(format!("{}()$0", self.label)); + } else { + self.snippet = Some(format!("{}($0)", self.label)); + } + } + self.kind = Some(CompletionItemKind::Function); + self + } +} + +impl Into for Builder { + fn into(self) -> CompletionItem { + self.build() + } +} + +/// Represents an in-progress set of completions being built. +#[derive(Debug, Default)] +pub(crate) struct Completions { + buf: Vec, +} + +impl Completions { + pub(crate) fn add(&mut self, item: impl Into) { + self.buf.push(item.into()) + } + pub(crate) fn add_all(&mut self, items: I) + where + I: IntoIterator, + I::Item: Into, + { + items.into_iter().for_each(|item| self.add(item.into())) + } + + #[cfg(test)] + pub(crate) fn assert_match(&self, expected: &str, kind: CompletionKind) { + let expected = normalize(expected); + let actual = self.debug_render(kind); + test_utils::assert_eq_text!(expected.as_str(), actual.as_str(),); + + /// Normalize the textual representation of `Completions`: + /// replace `;` with newlines, normalize whitespace + fn normalize(expected: &str) -> String { + use ra_syntax::{tokenize, TextUnit, TextRange, SyntaxKind::SEMI}; + let mut res = String::new(); + for line in expected.trim().lines() { + let line = line.trim(); + let mut start_offset: TextUnit = 0.into(); + // Yep, we use rust tokenize in completion tests :-) + for token in tokenize(line) { + let range = TextRange::offset_len(start_offset, token.len); + start_offset += token.len; + if token.kind == SEMI { + res.push('\n'); + } else { + res.push_str(&line[range]); + } + } + + res.push('\n'); + } + res + } + } + + #[cfg(test)] + fn debug_render(&self, kind: CompletionKind) -> String { + let mut res = String::new(); + for c in self.buf.iter() { + if c.completion_kind == kind { + if let Some(lookup) = &c.lookup { + res.push_str(lookup); + res.push_str(&format!(" {:?}", c.label)); + } else { + res.push_str(&c.label); + } + if let Some(snippet) = &c.snippet { + res.push_str(&format!(" {:?}", snippet)); + } + res.push('\n'); + } + } + res + } +} + +impl Into> for Completions { + fn into(self) -> Vec { + self.buf + } +} diff --git a/crates/ra_ide_api/src/db.rs b/crates/ra_ide_api/src/db.rs new file mode 100644 index 000000000..9d46609ec --- /dev/null +++ b/crates/ra_ide_api/src/db.rs @@ -0,0 +1,128 @@ +use std::{fmt, sync::Arc}; + +use salsa::{self, Database}; +use ra_db::{LocationIntener, BaseDatabase, FileId}; + +use crate::{symbol_index, LineIndex}; + +#[derive(Debug)] +pub(crate) struct RootDatabase { + runtime: salsa::Runtime, + id_maps: Arc, +} + +#[derive(Default)] +struct IdMaps { + defs: LocationIntener, + macros: LocationIntener, +} + +impl fmt::Debug for IdMaps { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("IdMaps") + .field("n_defs", &self.defs.len()) + .finish() + } +} + +impl salsa::Database for RootDatabase { + fn salsa_runtime(&self) -> &salsa::Runtime { + &self.runtime + } +} + +impl Default for RootDatabase { + fn default() -> RootDatabase { + let mut db = RootDatabase { + runtime: salsa::Runtime::default(), + id_maps: Default::default(), + }; + db.query_mut(ra_db::CrateGraphQuery) + .set((), Default::default()); + db.query_mut(ra_db::LocalRootsQuery) + .set((), Default::default()); + db.query_mut(ra_db::LibraryRootsQuery) + .set((), Default::default()); + db + } +} + +impl salsa::ParallelDatabase for RootDatabase { + fn snapshot(&self) -> salsa::Snapshot { + salsa::Snapshot::new(RootDatabase { + runtime: self.runtime.snapshot(self), + id_maps: self.id_maps.clone(), + }) + } +} + +impl BaseDatabase for RootDatabase {} + +impl AsRef> for RootDatabase { + fn as_ref(&self) -> &LocationIntener { + &self.id_maps.defs + } +} + +impl AsRef> for RootDatabase { + fn as_ref(&self) -> &LocationIntener { + &self.id_maps.macros + } +} + +salsa::query_group! { + pub(crate) trait LineIndexDatabase: ra_db::FilesDatabase + BaseDatabase { + fn line_index(file_id: FileId) -> Arc { + type LineIndexQuery; + } + } +} + +fn line_index(db: &impl ra_db::FilesDatabase, file_id: FileId) -> Arc { + let text = db.file_text(file_id); + Arc::new(LineIndex::new(&*text)) +} + +salsa::database_storage! { + pub(crate) struct RootDatabaseStorage for RootDatabase { + impl ra_db::FilesDatabase { + fn file_text() for ra_db::FileTextQuery; + fn file_relative_path() for ra_db::FileRelativePathQuery; + fn file_source_root() for ra_db::FileSourceRootQuery; + fn source_root() for ra_db::SourceRootQuery; + fn local_roots() for ra_db::LocalRootsQuery; + fn library_roots() for ra_db::LibraryRootsQuery; + fn crate_graph() for ra_db::CrateGraphQuery; + } + impl ra_db::SyntaxDatabase { + fn source_file() for ra_db::SourceFileQuery; + } + impl LineIndexDatabase { + fn line_index() for LineIndexQuery; + } + impl symbol_index::SymbolsDatabase { + fn file_symbols() for symbol_index::FileSymbolsQuery; + fn library_symbols() for symbol_index::LibrarySymbolsQuery; + } + impl hir::db::HirDatabase { + fn hir_source_file() for hir::db::HirSourceFileQuery; + fn expand_macro_invocation() for hir::db::ExpandMacroCallQuery; + fn module_tree() for hir::db::ModuleTreeQuery; + fn fn_scopes() for hir::db::FnScopesQuery; + fn file_items() for hir::db::SourceFileItemsQuery; + fn file_item() for hir::db::FileItemQuery; + fn input_module_items() for hir::db::InputModuleItemsQuery; + fn item_map() for hir::db::ItemMapQuery; + fn submodules() for hir::db::SubmodulesQuery; + fn infer() for hir::db::InferQuery; + fn type_for_def() for hir::db::TypeForDefQuery; + fn type_for_field() for hir::db::TypeForFieldQuery; + fn struct_data() for hir::db::StructDataQuery; + fn enum_data() for hir::db::EnumDataQuery; + fn impls_in_module() for hir::db::ImplsInModuleQuery; + fn body_hir() for hir::db::BodyHirQuery; + fn body_syntax_mapping() for hir::db::BodySyntaxMappingQuery; + fn fn_signature() for hir::db::FnSignatureQuery; + } + } +} diff --git a/crates/ra_ide_api/src/extend_selection.rs b/crates/ra_ide_api/src/extend_selection.rs new file mode 100644 index 000000000..c3c809c9f --- /dev/null +++ b/crates/ra_ide_api/src/extend_selection.rs @@ -0,0 +1,56 @@ +use ra_db::SyntaxDatabase; +use ra_syntax::{ + SyntaxNode, AstNode, SourceFile, + ast, algo::find_covering_node, +}; + +use crate::{ + TextRange, FileRange, + db::RootDatabase, +}; + +pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange { + let source_file = db.source_file(frange.file_id); + if let Some(range) = extend_selection_in_macro(db, &source_file, frange) { + return range; + } + ra_ide_api_light::extend_selection(source_file.syntax(), frange.range).unwrap_or(frange.range) +} + +fn extend_selection_in_macro( + _db: &RootDatabase, + source_file: &SourceFile, + frange: FileRange, +) -> Option { + let macro_call = find_macro_call(source_file.syntax(), frange.range)?; + let (off, exp) = hir::MacroDef::ast_expand(macro_call)?; + let dst_range = exp.map_range_forward(frange.range - off)?; + let dst_range = ra_ide_api_light::extend_selection(&exp.syntax(), dst_range)?; + let src_range = exp.map_range_back(dst_range)? + off; + Some(src_range) +} + +fn find_macro_call(node: &SyntaxNode, range: TextRange) -> Option<&ast::MacroCall> { + find_covering_node(node, range) + .ancestors() + .find_map(ast::MacroCall::cast) +} + +#[cfg(test)] +mod tests { + use crate::mock_analysis::single_file_with_range; + use test_utils::assert_eq_dbg; + + #[test] + fn extend_selection_inside_macros() { + let (analysis, frange) = single_file_with_range( + " + fn main() { + ctry!(foo(|x| <|>x<|>)); + } + ", + ); + let r = analysis.extend_selection(frange); + assert_eq_dbg("[51; 56)", &r); + } +} diff --git a/crates/ra_ide_api/src/goto_defenition.rs b/crates/ra_ide_api/src/goto_defenition.rs new file mode 100644 index 000000000..fcd8d315e --- /dev/null +++ b/crates/ra_ide_api/src/goto_defenition.rs @@ -0,0 +1,139 @@ +use ra_db::{FileId, Cancelable, SyntaxDatabase}; +use ra_syntax::{ + TextRange, AstNode, ast, SyntaxKind::{NAME, MODULE}, + algo::find_node_at_offset, +}; + +use crate::{FilePosition, NavigationTarget, db::RootDatabase}; + +pub(crate) fn goto_defenition( + db: &RootDatabase, + position: FilePosition, +) -> Cancelable>> { + let file = db.source_file(position.file_id); + let syntax = file.syntax(); + if let Some(name_ref) = find_node_at_offset::(syntax, position.offset) { + return Ok(Some(reference_defenition(db, position.file_id, name_ref)?)); + } + if let Some(name) = find_node_at_offset::(syntax, position.offset) { + return name_defenition(db, position.file_id, name); + } + Ok(None) +} + +pub(crate) fn reference_defenition( + db: &RootDatabase, + file_id: FileId, + name_ref: &ast::NameRef, +) -> Cancelable> { + if let Some(fn_descr) = + hir::source_binder::function_from_child_node(db, file_id, name_ref.syntax())? + { + let scope = fn_descr.scopes(db)?; + // First try to resolve the symbol locally + if let Some(entry) = scope.resolve_local_name(name_ref) { + let nav = NavigationTarget { + file_id, + name: entry.name().to_string().into(), + range: entry.ptr().range(), + kind: NAME, + ptr: None, + }; + return Ok(vec![nav]); + }; + } + // If that fails try the index based approach. + let navs = db + .index_resolve(name_ref)? + .into_iter() + .map(NavigationTarget::from_symbol) + .collect(); + Ok(navs) +} + +fn name_defenition( + db: &RootDatabase, + file_id: FileId, + name: &ast::Name, +) -> Cancelable>> { + if let Some(module) = name.syntax().parent().and_then(ast::Module::cast) { + if module.has_semi() { + if let Some(child_module) = + hir::source_binder::module_from_declaration(db, file_id, module)? + { + let (file_id, _) = child_module.defenition_source(db)?; + let name = match child_module.name(db)? { + Some(name) => name.to_string().into(), + None => "".into(), + }; + let nav = NavigationTarget { + file_id, + name, + range: TextRange::offset_len(0.into(), 0.into()), + kind: MODULE, + ptr: None, + }; + return Ok(Some(vec![nav])); + } + } + } + Ok(None) +} + +#[cfg(test)] +mod tests { + use test_utils::assert_eq_dbg; + use crate::mock_analysis::analysis_and_position; + + #[test] + fn goto_defenition_works_in_items() { + let (analysis, pos) = analysis_and_position( + " + //- /lib.rs + struct Foo; + enum E { X(Foo<|>) } + ", + ); + + let symbols = analysis.goto_defenition(pos).unwrap().unwrap(); + assert_eq_dbg( + r#"[NavigationTarget { file_id: FileId(1), name: "Foo", + kind: STRUCT_DEF, range: [0; 11), + ptr: Some(LocalSyntaxPtr { range: [0; 11), kind: STRUCT_DEF }) }]"#, + &symbols, + ); + } + + #[test] + fn goto_defenition_works_for_module_declaration() { + let (analysis, pos) = analysis_and_position( + " + //- /lib.rs + mod <|>foo; + //- /foo.rs + // empty + ", + ); + + let symbols = analysis.goto_defenition(pos).unwrap().unwrap(); + assert_eq_dbg( + r#"[NavigationTarget { file_id: FileId(2), name: "foo", kind: MODULE, range: [0; 0), ptr: None }]"#, + &symbols, + ); + + let (analysis, pos) = analysis_and_position( + " + //- /lib.rs + mod <|>foo; + //- /foo/mod.rs + // empty + ", + ); + + let symbols = analysis.goto_defenition(pos).unwrap().unwrap(); + assert_eq_dbg( + r#"[NavigationTarget { file_id: FileId(2), name: "foo", kind: MODULE, range: [0; 0), ptr: None }]"#, + &symbols, + ); + } +} diff --git a/crates/ra_ide_api/src/hover.rs b/crates/ra_ide_api/src/hover.rs new file mode 100644 index 000000000..475524ee1 --- /dev/null +++ b/crates/ra_ide_api/src/hover.rs @@ -0,0 +1,257 @@ +use ra_db::{Cancelable, SyntaxDatabase}; +use ra_syntax::{ + AstNode, SyntaxNode, TreePtr, + ast::{self, NameOwner}, + algo::{find_covering_node, find_node_at_offset, find_leaf_at_offset, visit::{visitor, Visitor}}, +}; + +use crate::{db::RootDatabase, RangeInfo, FilePosition, FileRange, NavigationTarget}; + +pub(crate) fn hover( + db: &RootDatabase, + position: FilePosition, +) -> Cancelable>> { + let file = db.source_file(position.file_id); + let mut res = Vec::new(); + + let mut range = None; + if let Some(name_ref) = find_node_at_offset::(file.syntax(), position.offset) { + let navs = crate::goto_defenition::reference_defenition(db, position.file_id, name_ref)?; + for nav in navs { + res.extend(doc_text_for(db, nav)?) + } + if !res.is_empty() { + range = Some(name_ref.syntax().range()) + } + } + if range.is_none() { + let node = find_leaf_at_offset(file.syntax(), position.offset).find_map(|leaf| { + leaf.ancestors() + .find(|n| ast::Expr::cast(*n).is_some() || ast::Pat::cast(*n).is_some()) + }); + let node = ctry!(node); + let frange = FileRange { + file_id: position.file_id, + range: node.range(), + }; + res.extend(type_of(db, frange)?); + range = Some(node.range()); + }; + + let range = ctry!(range); + if res.is_empty() { + return Ok(None); + } + let res = RangeInfo::new(range, res.join("\n\n---\n")); + Ok(Some(res)) +} + +pub(crate) fn type_of(db: &RootDatabase, frange: FileRange) -> Cancelable> { + let file = db.source_file(frange.file_id); + let syntax = file.syntax(); + let leaf_node = find_covering_node(syntax, frange.range); + // if we picked identifier, expand to pattern/expression + let node = leaf_node + .ancestors() + .take_while(|it| it.range() == leaf_node.range()) + .find(|&it| ast::Expr::cast(it).is_some() || ast::Pat::cast(it).is_some()) + .unwrap_or(leaf_node); + let parent_fn = ctry!(node.ancestors().find_map(ast::FnDef::cast)); + let function = ctry!(hir::source_binder::function_from_source( + db, + frange.file_id, + parent_fn + )?); + let infer = function.infer(db)?; + let syntax_mapping = function.body_syntax_mapping(db)?; + if let Some(expr) = ast::Expr::cast(node).and_then(|e| syntax_mapping.node_expr(e)) { + Ok(Some(infer[expr].to_string())) + } else if let Some(pat) = ast::Pat::cast(node).and_then(|p| syntax_mapping.node_pat(p)) { + Ok(Some(infer[pat].to_string())) + } else { + Ok(None) + } +} + +// FIXME: this should not really use navigation target. Rather, approximatelly +// resovled symbol should return a `DefId`. +fn doc_text_for(db: &RootDatabase, nav: NavigationTarget) -> Cancelable> { + let result = match (nav.description(db), nav.docs(db)) { + (Some(desc), Some(docs)) => Some("```rust\n".to_string() + &*desc + "\n```\n\n" + &*docs), + (Some(desc), None) => Some("```rust\n".to_string() + &*desc + "\n```"), + (None, Some(docs)) => Some(docs), + _ => None, + }; + + Ok(result) +} + +impl NavigationTarget { + fn node(&self, db: &RootDatabase) -> Option> { + let source_file = db.source_file(self.file_id); + let source_file = source_file.syntax(); + let node = source_file + .descendants() + .find(|node| node.kind() == self.kind && node.range() == self.range)? + .to_owned(); + Some(node) + } + + fn docs(&self, db: &RootDatabase) -> Option { + let node = self.node(db)?; + fn doc_comments(node: &N) -> Option { + let comments = node.doc_comment_text(); + if comments.is_empty() { + None + } else { + Some(comments) + } + } + + visitor() + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .visit(doc_comments::) + .accept(&node)? + } + + /// Get a description of this node. + /// + /// e.g. `struct Name`, `enum Name`, `fn Name` + fn description(&self, db: &RootDatabase) -> Option { + // TODO: After type inference is done, add type information to improve the output + let node = self.node(db)?; + // TODO: Refactor to be have less repetition + visitor() + .visit(|node: &ast::FnDef| { + let mut string = "fn ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::StructDef| { + let mut string = "struct ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::EnumDef| { + let mut string = "enum ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::TraitDef| { + let mut string = "trait ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::Module| { + let mut string = "mod ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::TypeDef| { + let mut string = "type ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::ConstDef| { + let mut string = "const ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .visit(|node: &ast::StaticDef| { + let mut string = "static ".to_string(); + node.name()?.syntax().text().push_to(&mut string); + Some(string) + }) + .accept(&node)? + } +} + +#[cfg(test)] +mod tests { + use ra_syntax::TextRange; + use crate::mock_analysis::{single_file_with_position, single_file_with_range}; + + #[test] + fn hover_shows_type_of_an_expression() { + let (analysis, position) = single_file_with_position( + " + pub fn foo() -> u32 { 1 } + + fn main() { + let foo_test = foo()<|>; + } + ", + ); + let hover = analysis.hover(position).unwrap().unwrap(); + assert_eq!(hover.range, TextRange::from_to(95.into(), 100.into())); + assert_eq!(hover.info, "u32"); + } + + #[test] + fn hover_for_local_variable() { + let (analysis, position) = single_file_with_position("fn func(foo: i32) { fo<|>o; }"); + let hover = analysis.hover(position).unwrap().unwrap(); + assert_eq!(hover.info, "i32"); + } + + #[test] + fn hover_for_local_variable_pat() { + let (analysis, position) = single_file_with_position("fn func(fo<|>o: i32) {}"); + let hover = analysis.hover(position).unwrap().unwrap(); + assert_eq!(hover.info, "i32"); + } + + #[test] + fn test_type_of_for_function() { + let (analysis, range) = single_file_with_range( + " + pub fn foo() -> u32 { 1 }; + + fn main() { + let foo_test = <|>foo()<|>; + } + ", + ); + + let type_name = analysis.type_of(range).unwrap().unwrap(); + assert_eq!("u32", &type_name); + } + + // FIXME: improve type_of to make this work + #[test] + fn test_type_of_for_expr_1() { + let (analysis, range) = single_file_with_range( + " + fn main() { + let foo = <|>1 + foo_test<|>; + } + ", + ); + + let type_name = analysis.type_of(range).unwrap().unwrap(); + assert_eq!("[unknown]", &type_name); + } + + // FIXME: improve type_of to make this work + #[test] + fn test_type_of_for_expr_2() { + let (analysis, range) = single_file_with_range( + " + fn main() { + let foo: usize = 1; + let bar = <|>1 + foo_test<|>; + } + ", + ); + + let type_name = analysis.type_of(range).unwrap().unwrap(); + assert_eq!("[unknown]", &type_name); + } + +} diff --git a/crates/ra_ide_api/src/imp.rs b/crates/ra_ide_api/src/imp.rs new file mode 100644 index 000000000..7c60ab7d6 --- /dev/null +++ b/crates/ra_ide_api/src/imp.rs @@ -0,0 +1,309 @@ +use std::sync::Arc; + +use salsa::Database; + +use hir::{ + self, Problem, source_binder, +}; +use ra_db::{FilesDatabase, SourceRoot, SourceRootId, SyntaxDatabase}; +use ra_ide_api_light::{self, assists, LocalEdit, Severity}; +use ra_syntax::{ + TextRange, AstNode, SourceFile, + ast::{self, NameOwner}, + algo::find_node_at_offset, + SyntaxKind::*, +}; + +use crate::{ + AnalysisChange, + Cancelable, NavigationTarget, + CrateId, db, Diagnostic, FileId, FilePosition, FileRange, FileSystemEdit, + Query, RootChange, SourceChange, SourceFileEdit, + symbol_index::{LibrarySymbolsQuery, FileSymbol}, +}; + +impl db::RootDatabase { + pub(crate) fn apply_change(&mut self, change: AnalysisChange) { + log::info!("apply_change {:?}", change); + // self.gc_syntax_trees(); + if !change.new_roots.is_empty() { + let mut local_roots = Vec::clone(&self.local_roots()); + for (root_id, is_local) in change.new_roots { + self.query_mut(ra_db::SourceRootQuery) + .set(root_id, Default::default()); + if is_local { + local_roots.push(root_id); + } + } + self.query_mut(ra_db::LocalRootsQuery) + .set((), Arc::new(local_roots)); + } + + for (root_id, root_change) in change.roots_changed { + self.apply_root_change(root_id, root_change); + } + for (file_id, text) in change.files_changed { + self.query_mut(ra_db::FileTextQuery).set(file_id, text) + } + if !change.libraries_added.is_empty() { + let mut libraries = Vec::clone(&self.library_roots()); + for library in change.libraries_added { + libraries.push(library.root_id); + self.query_mut(ra_db::SourceRootQuery) + .set(library.root_id, Default::default()); + self.query_mut(LibrarySymbolsQuery) + .set_constant(library.root_id, Arc::new(library.symbol_index)); + self.apply_root_change(library.root_id, library.root_change); + } + self.query_mut(ra_db::LibraryRootsQuery) + .set((), Arc::new(libraries)); + } + if let Some(crate_graph) = change.crate_graph { + self.query_mut(ra_db::CrateGraphQuery) + .set((), Arc::new(crate_graph)) + } + } + + fn apply_root_change(&mut self, root_id: SourceRootId, root_change: RootChange) { + let mut source_root = SourceRoot::clone(&self.source_root(root_id)); + for add_file in root_change.added { + self.query_mut(ra_db::FileTextQuery) + .set(add_file.file_id, add_file.text); + self.query_mut(ra_db::FileRelativePathQuery) + .set(add_file.file_id, add_file.path.clone()); + self.query_mut(ra_db::FileSourceRootQuery) + .set(add_file.file_id, root_id); + source_root.files.insert(add_file.path, add_file.file_id); + } + for remove_file in root_change.removed { + self.query_mut(ra_db::FileTextQuery) + .set(remove_file.file_id, Default::default()); + source_root.files.remove(&remove_file.path); + } + self.query_mut(ra_db::SourceRootQuery) + .set(root_id, Arc::new(source_root)); + } + + #[allow(unused)] + /// Ideally, we should call this function from time to time to collect heavy + /// syntax trees. However, if we actually do that, everything is recomputed + /// for some reason. Needs investigation. + fn gc_syntax_trees(&mut self) { + self.query(ra_db::SourceFileQuery) + .sweep(salsa::SweepStrategy::default().discard_values()); + self.query(hir::db::SourceFileItemsQuery) + .sweep(salsa::SweepStrategy::default().discard_values()); + self.query(hir::db::FileItemQuery) + .sweep(salsa::SweepStrategy::default().discard_values()); + } +} + +impl db::RootDatabase { + /// This returns `Vec` because a module may be included from several places. We + /// don't handle this case yet though, so the Vec has length at most one. + pub(crate) fn parent_module( + &self, + position: FilePosition, + ) -> Cancelable> { + let module = match source_binder::module_from_position(self, position)? { + None => return Ok(Vec::new()), + Some(it) => it, + }; + let (file_id, ast_module) = match module.declaration_source(self)? { + None => return Ok(Vec::new()), + Some(it) => it, + }; + let name = ast_module.name().unwrap(); + Ok(vec![NavigationTarget { + file_id, + name: name.text().clone(), + range: name.syntax().range(), + kind: MODULE, + ptr: None, + }]) + } + /// Returns `Vec` for the same reason as `parent_module` + pub(crate) fn crate_for(&self, file_id: FileId) -> Cancelable> { + let module = match source_binder::module_from_file_id(self, file_id)? { + Some(it) => it, + None => return Ok(Vec::new()), + }; + let krate = match module.krate(self)? { + Some(it) => it, + None => return Ok(Vec::new()), + }; + Ok(vec![krate.crate_id()]) + } + pub(crate) fn find_all_refs( + &self, + position: FilePosition, + ) -> Cancelable> { + let file = self.source_file(position.file_id); + // Find the binding associated with the offset + let (binding, descr) = match find_binding(self, &file, position)? { + None => return Ok(Vec::new()), + Some(it) => it, + }; + + let mut ret = binding + .name() + .into_iter() + .map(|name| (position.file_id, name.syntax().range())) + .collect::>(); + ret.extend( + descr + .scopes(self)? + .find_all_refs(binding) + .into_iter() + .map(|ref_desc| (position.file_id, ref_desc.range)), + ); + + return Ok(ret); + + fn find_binding<'a>( + db: &db::RootDatabase, + source_file: &'a SourceFile, + position: FilePosition, + ) -> Cancelable> { + let syntax = source_file.syntax(); + if let Some(binding) = find_node_at_offset::(syntax, position.offset) { + let descr = ctry!(source_binder::function_from_child_node( + db, + position.file_id, + binding.syntax(), + )?); + return Ok(Some((binding, descr))); + }; + let name_ref = ctry!(find_node_at_offset::(syntax, position.offset)); + let descr = ctry!(source_binder::function_from_child_node( + db, + position.file_id, + name_ref.syntax(), + )?); + let scope = descr.scopes(db)?; + let resolved = ctry!(scope.resolve_local_name(name_ref)); + let resolved = resolved.ptr().resolve(source_file); + let binding = ctry!(find_node_at_offset::( + syntax, + resolved.range().end() + )); + Ok(Some((binding, descr))) + } + } + + pub(crate) fn diagnostics(&self, file_id: FileId) -> Cancelable> { + let syntax = self.source_file(file_id); + + let mut res = ra_ide_api_light::diagnostics(&syntax) + .into_iter() + .map(|d| Diagnostic { + range: d.range, + message: d.msg, + severity: d.severity, + fix: d.fix.map(|fix| SourceChange::from_local_edit(file_id, fix)), + }) + .collect::>(); + if let Some(m) = source_binder::module_from_file_id(self, file_id)? { + for (name_node, problem) in m.problems(self)? { + let source_root = self.file_source_root(file_id); + let diag = match problem { + Problem::UnresolvedModule { candidate } => { + let create_file = FileSystemEdit::CreateFile { + source_root, + path: candidate.clone(), + }; + let fix = SourceChange { + label: "create module".to_string(), + source_file_edits: Vec::new(), + file_system_edits: vec![create_file], + cursor_position: None, + }; + Diagnostic { + range: name_node.range(), + message: "unresolved module".to_string(), + severity: Severity::Error, + fix: Some(fix), + } + } + Problem::NotDirOwner { move_to, candidate } => { + let move_file = FileSystemEdit::MoveFile { + src: file_id, + dst_source_root: source_root, + dst_path: move_to.clone(), + }; + let create_file = FileSystemEdit::CreateFile { + source_root, + path: move_to.join(candidate), + }; + let fix = SourceChange { + label: "move file and create module".to_string(), + source_file_edits: Vec::new(), + file_system_edits: vec![move_file, create_file], + cursor_position: None, + }; + Diagnostic { + range: name_node.range(), + message: "can't declare module at this location".to_string(), + severity: Severity::Error, + fix: Some(fix), + } + } + }; + res.push(diag) + } + }; + Ok(res) + } + + pub(crate) fn assists(&self, frange: FileRange) -> Vec { + let file = self.source_file(frange.file_id); + assists::assists(&file, frange.range) + .into_iter() + .map(|local_edit| SourceChange::from_local_edit(frange.file_id, local_edit)) + .collect() + } + + pub(crate) fn rename( + &self, + position: FilePosition, + new_name: &str, + ) -> Cancelable> { + let res = self + .find_all_refs(position)? + .iter() + .map(|(file_id, text_range)| SourceFileEdit { + file_id: *file_id, + edit: { + let mut builder = ra_text_edit::TextEditBuilder::default(); + builder.replace(*text_range, new_name.into()); + builder.finish() + }, + }) + .collect::>(); + Ok(res) + } + pub(crate) fn index_resolve(&self, name_ref: &ast::NameRef) -> Cancelable> { + let name = name_ref.text(); + let mut query = Query::new(name.to_string()); + query.exact(); + query.limit(4); + crate::symbol_index::world_symbols(self, query) + } +} + +impl SourceChange { + pub(crate) fn from_local_edit(file_id: FileId, edit: LocalEdit) -> SourceChange { + let file_edit = SourceFileEdit { + file_id, + edit: edit.edit, + }; + SourceChange { + label: edit.label, + source_file_edits: vec![file_edit], + file_system_edits: vec![], + cursor_position: edit + .cursor_position + .map(|offset| FilePosition { offset, file_id }), + } + } +} diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs new file mode 100644 index 000000000..183e36706 --- /dev/null +++ b/crates/ra_ide_api/src/lib.rs @@ -0,0 +1,509 @@ +//! ra_analyzer crate provides "ide-centric" APIs for the rust-analyzer. What +//! powers this API are the `RootDatabase` struct, which defines a `salsa` +//! database, and the `ra_hir` crate, where majority of the analysis happens. +//! However, IDE specific bits of the analysis (most notably completion) happen +//! in this crate. +macro_rules! ctry { + ($expr:expr) => { + match $expr { + None => return Ok(None), + Some(it) => it, + } + }; +} + +mod completion; +mod db; +mod goto_defenition; +mod imp; +pub mod mock_analysis; +mod runnables; +mod symbol_index; + +mod extend_selection; +mod hover; +mod call_info; +mod syntax_highlighting; + +use std::{fmt, sync::Arc}; + +use ra_syntax::{SmolStr, SourceFile, TreePtr, SyntaxKind, TextRange, TextUnit}; +use ra_text_edit::TextEdit; +use ra_db::{SyntaxDatabase, FilesDatabase, LocalSyntaxPtr}; +use rayon::prelude::*; +use relative_path::RelativePathBuf; +use rustc_hash::FxHashMap; +use salsa::ParallelDatabase; + +use crate::{ + symbol_index::{FileSymbol, SymbolIndex}, + db::LineIndexDatabase, +}; + +pub use crate::{ + completion::{CompletionItem, CompletionItemKind, InsertText}, + runnables::{Runnable, RunnableKind}, +}; +pub use ra_ide_api_light::{ + Fold, FoldKind, HighlightedRange, Severity, StructureNode, + LineIndex, LineCol, translate_offset_with_edit, +}; +pub use ra_db::{ + Cancelable, Canceled, CrateGraph, CrateId, FileId, FilePosition, FileRange, SourceRootId +}; + +#[derive(Default)] +pub struct AnalysisChange { + new_roots: Vec<(SourceRootId, bool)>, + roots_changed: FxHashMap, + files_changed: Vec<(FileId, Arc)>, + libraries_added: Vec, + crate_graph: Option, +} + +#[derive(Default)] +struct RootChange { + added: Vec, + removed: Vec, +} + +#[derive(Debug)] +struct AddFile { + file_id: FileId, + path: RelativePathBuf, + text: Arc, +} + +#[derive(Debug)] +struct RemoveFile { + file_id: FileId, + path: RelativePathBuf, +} + +impl fmt::Debug for AnalysisChange { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let mut d = fmt.debug_struct("AnalysisChange"); + if !self.new_roots.is_empty() { + d.field("new_roots", &self.new_roots); + } + if !self.roots_changed.is_empty() { + d.field("roots_changed", &self.roots_changed); + } + if !self.files_changed.is_empty() { + d.field("files_changed", &self.files_changed.len()); + } + if !self.libraries_added.is_empty() { + d.field("libraries_added", &self.libraries_added.len()); + } + if !self.crate_graph.is_some() { + d.field("crate_graph", &self.crate_graph); + } + d.finish() + } +} + +impl fmt::Debug for RootChange { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.debug_struct("AnalysisChange") + .field("added", &self.added.len()) + .field("removed", &self.removed.len()) + .finish() + } +} + +impl AnalysisChange { + pub fn new() -> AnalysisChange { + AnalysisChange::default() + } + pub fn add_root(&mut self, root_id: SourceRootId, is_local: bool) { + self.new_roots.push((root_id, is_local)); + } + pub fn add_file( + &mut self, + root_id: SourceRootId, + file_id: FileId, + path: RelativePathBuf, + text: Arc, + ) { + let file = AddFile { + file_id, + path, + text, + }; + self.roots_changed + .entry(root_id) + .or_default() + .added + .push(file); + } + pub fn change_file(&mut self, file_id: FileId, new_text: Arc) { + self.files_changed.push((file_id, new_text)) + } + pub fn remove_file(&mut self, root_id: SourceRootId, file_id: FileId, path: RelativePathBuf) { + let file = RemoveFile { file_id, path }; + self.roots_changed + .entry(root_id) + .or_default() + .removed + .push(file); + } + pub fn add_library(&mut self, data: LibraryData) { + self.libraries_added.push(data) + } + pub fn set_crate_graph(&mut self, graph: CrateGraph) { + self.crate_graph = Some(graph); + } +} + +#[derive(Debug)] +pub struct SourceChange { + pub label: String, + pub source_file_edits: Vec, + pub file_system_edits: Vec, + pub cursor_position: Option, +} + +#[derive(Debug)] +pub struct SourceFileEdit { + pub file_id: FileId, + pub edit: TextEdit, +} + +#[derive(Debug)] +pub enum FileSystemEdit { + CreateFile { + source_root: SourceRootId, + path: RelativePathBuf, + }, + MoveFile { + src: FileId, + dst_source_root: SourceRootId, + dst_path: RelativePathBuf, + }, +} + +#[derive(Debug)] +pub struct Diagnostic { + pub message: String, + pub range: TextRange, + pub fix: Option, + pub severity: Severity, +} + +#[derive(Debug)] +pub struct Query { + query: String, + lowercased: String, + only_types: bool, + libs: bool, + exact: bool, + limit: usize, +} + +impl Query { + pub fn new(query: String) -> Query { + let lowercased = query.to_lowercase(); + Query { + query, + lowercased, + only_types: false, + libs: false, + exact: false, + limit: usize::max_value(), + } + } + pub fn only_types(&mut self) { + self.only_types = true; + } + pub fn libs(&mut self) { + self.libs = true; + } + pub fn exact(&mut self) { + self.exact = true; + } + pub fn limit(&mut self, limit: usize) { + self.limit = limit + } +} + +/// `NavigationTarget` represents and element in the editor's UI whihc you can +/// click on to navigate to a particular piece of code. +/// +/// Typically, a `NavigationTarget` corresponds to some element in the source +/// code, like a function or a struct, but this is not strictly required. +#[derive(Debug, Clone)] +pub struct NavigationTarget { + file_id: FileId, + name: SmolStr, + kind: SyntaxKind, + range: TextRange, + // Should be DefId ideally + ptr: Option, +} + +impl NavigationTarget { + fn from_symbol(symbol: FileSymbol) -> NavigationTarget { + NavigationTarget { + file_id: symbol.file_id, + name: symbol.name.clone(), + kind: symbol.ptr.kind(), + range: symbol.ptr.range(), + ptr: Some(symbol.ptr.clone()), + } + } + pub fn name(&self) -> &SmolStr { + &self.name + } + pub fn kind(&self) -> SyntaxKind { + self.kind + } + pub fn file_id(&self) -> FileId { + self.file_id + } + pub fn range(&self) -> TextRange { + self.range + } +} + +#[derive(Debug)] +pub struct RangeInfo { + pub range: TextRange, + pub info: T, +} + +impl RangeInfo { + fn new(range: TextRange, info: T) -> RangeInfo { + RangeInfo { range, info } + } +} + +#[derive(Debug)] +pub struct CallInfo { + pub label: String, + pub doc: Option, + pub parameters: Vec, + pub active_parameter: Option, +} + +/// `AnalysisHost` stores the current state of the world. +#[derive(Debug, Default)] +pub struct AnalysisHost { + db: db::RootDatabase, +} + +impl AnalysisHost { + /// Returns a snapshot of the current state, which you can query for + /// semantic information. + pub fn analysis(&self) -> Analysis { + Analysis { + db: self.db.snapshot(), + } + } + /// Applies changes to the current state of the world. If there are + /// outstanding snapshots, they will be canceled. + pub fn apply_change(&mut self, change: AnalysisChange) { + self.db.apply_change(change) + } +} + +/// Analysis is a snapshot of a world state at a moment in time. It is the main +/// entry point for asking semantic information about the world. When the world +/// state is advanced using `AnalysisHost::apply_change` method, all existing +/// `Analysis` are canceled (most method return `Err(Canceled)`). +#[derive(Debug)] +pub struct Analysis { + db: salsa::Snapshot, +} + +impl Analysis { + /// Gets the text of the source file. + pub fn file_text(&self, file_id: FileId) -> Arc { + self.db.file_text(file_id) + } + /// Gets the syntax tree of the file. + pub fn file_syntax(&self, file_id: FileId) -> TreePtr { + self.db.source_file(file_id).clone() + } + /// Gets the file's `LineIndex`: data structure to convert between absolute + /// offsets and line/column representation. + pub fn file_line_index(&self, file_id: FileId) -> Arc { + self.db.line_index(file_id) + } + /// Selects the next syntactic nodes encopasing the range. + pub fn extend_selection(&self, frange: FileRange) -> TextRange { + extend_selection::extend_selection(&self.db, frange) + } + /// Returns position of the mathcing brace (all types of braces are + /// supported). + pub fn matching_brace(&self, file: &SourceFile, offset: TextUnit) -> Option { + ra_ide_api_light::matching_brace(file, offset) + } + /// Returns a syntax tree represented as `String`, for debug purposes. + // FIXME: use a better name here. + pub fn syntax_tree(&self, file_id: FileId) -> String { + let file = self.db.source_file(file_id); + ra_ide_api_light::syntax_tree(&file) + } + /// Returns an edit to remove all newlines in the range, cleaning up minor + /// stuff like trailing commas. + pub fn join_lines(&self, frange: FileRange) -> SourceChange { + let file = self.db.source_file(frange.file_id); + SourceChange::from_local_edit( + frange.file_id, + ra_ide_api_light::join_lines(&file, frange.range), + ) + } + /// Returns an edit which should be applied when opening a new line, fixing + /// up minor stuff like continuing the comment. + pub fn on_enter(&self, position: FilePosition) -> Option { + let file = self.db.source_file(position.file_id); + let edit = ra_ide_api_light::on_enter(&file, position.offset)?; + Some(SourceChange::from_local_edit(position.file_id, edit)) + } + /// Returns an edit which should be applied after `=` was typed. Primarily, + /// this works when adding `let =`. + // FIXME: use a snippet completion instead of this hack here. + pub fn on_eq_typed(&self, position: FilePosition) -> Option { + let file = self.db.source_file(position.file_id); + let edit = ra_ide_api_light::on_eq_typed(&file, position.offset)?; + Some(SourceChange::from_local_edit(position.file_id, edit)) + } + /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. + pub fn on_dot_typed(&self, position: FilePosition) -> Option { + let file = self.db.source_file(position.file_id); + let edit = ra_ide_api_light::on_dot_typed(&file, position.offset)?; + Some(SourceChange::from_local_edit(position.file_id, edit)) + } + /// Returns a tree representation of symbols in the file. Useful to draw a + /// file outline. + pub fn file_structure(&self, file_id: FileId) -> Vec { + let file = self.db.source_file(file_id); + ra_ide_api_light::file_structure(&file) + } + /// Returns the set of folding ranges. + pub fn folding_ranges(&self, file_id: FileId) -> Vec { + let file = self.db.source_file(file_id); + ra_ide_api_light::folding_ranges(&file) + } + /// Fuzzy searches for a symbol. + pub fn symbol_search(&self, query: Query) -> Cancelable> { + let res = symbol_index::world_symbols(&*self.db, query)? + .into_iter() + .map(NavigationTarget::from_symbol) + .collect(); + Ok(res) + } + pub fn goto_defenition( + &self, + position: FilePosition, + ) -> Cancelable>> { + goto_defenition::goto_defenition(&*self.db, position) + } + /// Finds all usages of the reference at point. + pub fn find_all_refs(&self, position: FilePosition) -> Cancelable> { + self.db.find_all_refs(position) + } + /// Returns a short text descrbing element at position. + pub fn hover(&self, position: FilePosition) -> Cancelable>> { + hover::hover(&*self.db, position) + } + /// Computes parameter information for the given call expression. + pub fn call_info(&self, position: FilePosition) -> Cancelable> { + call_info::call_info(&*self.db, position) + } + /// Returns a `mod name;` declaration which created the current module. + pub fn parent_module(&self, position: FilePosition) -> Cancelable> { + self.db.parent_module(position) + } + /// Returns crates this file belongs too. + pub fn crate_for(&self, file_id: FileId) -> Cancelable> { + self.db.crate_for(file_id) + } + /// Returns the root file of the given crate. + pub fn crate_root(&self, crate_id: CrateId) -> Cancelable { + Ok(self.db.crate_graph().crate_root(crate_id)) + } + /// Returns the set of possible targets to run for the current file. + pub fn runnables(&self, file_id: FileId) -> Cancelable> { + runnables::runnables(&*self.db, file_id) + } + /// Computes syntax highlighting for the given file. + pub fn highlight(&self, file_id: FileId) -> Cancelable> { + syntax_highlighting::highlight(&*self.db, file_id) + } + /// Computes completions at the given position. + pub fn completions(&self, position: FilePosition) -> Cancelable>> { + let completions = completion::completions(&self.db, position)?; + Ok(completions.map(|it| it.into())) + } + /// Computes assists (aks code actons aka intentions) for the given + /// position. + pub fn assists(&self, frange: FileRange) -> Cancelable> { + Ok(self.db.assists(frange)) + } + /// Computes the set of diagnostics for the given file. + pub fn diagnostics(&self, file_id: FileId) -> Cancelable> { + self.db.diagnostics(file_id) + } + /// Computes the type of the expression at the given position. + pub fn type_of(&self, frange: FileRange) -> Cancelable> { + hover::type_of(&*self.db, frange) + } + /// Returns the edit required to rename reference at the position to the new + /// name. + pub fn rename( + &self, + position: FilePosition, + new_name: &str, + ) -> Cancelable> { + self.db.rename(position, new_name) + } +} + +pub struct LibraryData { + root_id: SourceRootId, + root_change: RootChange, + symbol_index: SymbolIndex, +} + +impl fmt::Debug for LibraryData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("LibraryData") + .field("root_id", &self.root_id) + .field("root_change", &self.root_change) + .field("n_symbols", &self.symbol_index.len()) + .finish() + } +} + +impl LibraryData { + pub fn prepare( + root_id: SourceRootId, + files: Vec<(FileId, RelativePathBuf, Arc)>, + ) -> LibraryData { + let symbol_index = SymbolIndex::for_files(files.par_iter().map(|(file_id, _, text)| { + let file = SourceFile::parse(text); + (*file_id, file) + })); + let mut root_change = RootChange::default(); + root_change.added = files + .into_iter() + .map(|(file_id, path, text)| AddFile { + file_id, + path, + text, + }) + .collect(); + LibraryData { + root_id, + root_change, + symbol_index, + } + } +} + +#[test] +fn analysis_is_send() { + fn is_send() {} + is_send::(); +} diff --git a/crates/ra_ide_api/src/mock_analysis.rs b/crates/ra_ide_api/src/mock_analysis.rs new file mode 100644 index 000000000..846c76cfe --- /dev/null +++ b/crates/ra_ide_api/src/mock_analysis.rs @@ -0,0 +1,135 @@ +use std::sync::Arc; + +use relative_path::RelativePathBuf; +use test_utils::{extract_offset, extract_range, parse_fixture, CURSOR_MARKER}; +use ra_db::mock::FileMap; + +use crate::{Analysis, AnalysisChange, AnalysisHost, CrateGraph, FileId, FilePosition, FileRange, SourceRootId}; + +/// 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() + } + /// Creates `MockAnalysis` using a fixture data in the following format: + /// + /// ```notrust + /// //- /main.rs + /// mod foo; + /// fn main() {} + /// + /// //- /foo.rs + /// struct Baz; + /// ``` + pub fn with_files(fixture: &str) -> MockAnalysis { + let mut res = MockAnalysis::new(); + for entry in parse_fixture(fixture) { + res.add_file(&entry.meta, &entry.text); + } + res + } + + /// Same as `with_files`, but requires that a single file contains a `<|>` marker, + /// whose position is also returned. + pub fn with_files_and_position(fixture: &str) -> (MockAnalysis, FilePosition) { + let mut position = None; + let mut res = MockAnalysis::new(); + for entry in parse_fixture(fixture) { + if entry.text.contains(CURSOR_MARKER) { + assert!( + position.is_none(), + "only one marker (<|>) per fixture is allowed" + ); + position = Some(res.add_file_with_position(&entry.meta, &entry.text)); + } else { + res.add_file(&entry.meta, &entry.text); + } + } + let position = position.expect("expected a marker (<|>)"); + (res, position) + } + + pub fn add_file(&mut self, path: &str, text: &str) -> FileId { + let file_id = FileId((self.files.len() + 1) as u32); + self.files.push((path.to_string(), text.to_string())); + file_id + } + pub fn add_file_with_position(&mut self, path: &str, text: &str) -> FilePosition { + let (offset, text) = extract_offset(text); + let file_id = FileId((self.files.len() + 1) as u32); + self.files.push((path.to_string(), text.to_string())); + FilePosition { file_id, offset } + } + pub fn add_file_with_range(&mut self, path: &str, text: &str) -> FileRange { + let (range, text) = extract_range(text); + let file_id = FileId((self.files.len() + 1) as u32); + self.files.push((path.to_string(), text.to_string())); + FileRange { file_id, range } + } + pub fn id_of(&self, path: &str) -> FileId { + let (idx, _) = self + .files + .iter() + .enumerate() + .find(|(_, (p, _text))| path == p) + .expect("no file in this mock"); + FileId(idx as u32 + 1) + } + pub fn analysis_host(self) -> AnalysisHost { + let mut host = AnalysisHost::default(); + let mut file_map = FileMap::default(); + let source_root = SourceRootId(0); + let mut change = AnalysisChange::new(); + change.add_root(source_root, true); + let mut crate_graph = CrateGraph::default(); + for (path, contents) in self.files.into_iter() { + assert!(path.starts_with('/')); + let path = RelativePathBuf::from_path(&path[1..]).unwrap(); + let file_id = file_map.add(path.clone()); + if path == "/lib.rs" || path == "/main.rs" { + crate_graph.add_crate_root(file_id); + } + change.add_file(source_root, file_id, path, Arc::new(contents)); + } + change.set_crate_graph(crate_graph); + // change.set_file_resolver(Arc::new(file_map)); + host.apply_change(change); + host + } + pub fn analysis(self) -> Analysis { + self.analysis_host().analysis() + } +} + +/// Creates analysis from a multi-file fixture, returns positions marked with <|>. +pub fn analysis_and_position(fixture: &str) -> (Analysis, FilePosition) { + let (mock, position) = MockAnalysis::with_files_and_position(fixture); + (mock.analysis(), position) +} + +/// Creates analysis for a single file. +pub fn single_file(code: &str) -> (Analysis, FileId) { + let mut mock = MockAnalysis::new(); + let file_id = mock.add_file("/main.rs", code); + (mock.analysis(), file_id) +} + +/// Creates analysis for a single file, returns position marked with <|>. +pub fn single_file_with_position(code: &str) -> (Analysis, FilePosition) { + let mut mock = MockAnalysis::new(); + let pos = mock.add_file_with_position("/main.rs", code); + (mock.analysis(), pos) +} + +/// Creates analysis for a single file, returns range marked with a pair of <|>. +pub fn single_file_with_range(code: &str) -> (Analysis, FileRange) { + let mut mock = MockAnalysis::new(); + let pos = mock.add_file_with_range("/main.rs", code); + (mock.analysis(), pos) +} diff --git a/crates/ra_ide_api/src/runnables.rs b/crates/ra_ide_api/src/runnables.rs new file mode 100644 index 000000000..98b1d2d55 --- /dev/null +++ b/crates/ra_ide_api/src/runnables.rs @@ -0,0 +1,89 @@ +use itertools::Itertools; +use ra_syntax::{ + TextRange, SyntaxNode, + ast::{self, AstNode, NameOwner, ModuleItemOwner}, +}; +use ra_db::{Cancelable, SyntaxDatabase}; + +use crate::{db::RootDatabase, FileId}; + +#[derive(Debug)] +pub struct Runnable { + pub range: TextRange, + pub kind: RunnableKind, +} + +#[derive(Debug)] +pub enum RunnableKind { + Test { name: String }, + TestMod { path: String }, + Bin, +} + +pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Cancelable> { + let source_file = db.source_file(file_id); + let res = source_file + .syntax() + .descendants() + .filter_map(|i| runnable(db, file_id, i)) + .collect(); + Ok(res) +} + +fn runnable(db: &RootDatabase, file_id: FileId, item: &SyntaxNode) -> Option { + if let Some(fn_def) = ast::FnDef::cast(item) { + runnable_fn(fn_def) + } else if let Some(m) = ast::Module::cast(item) { + runnable_mod(db, file_id, m) + } else { + None + } +} + +fn runnable_fn(fn_def: &ast::FnDef) -> Option { + let name = fn_def.name()?.text(); + let kind = if name == "main" { + RunnableKind::Bin + } else if fn_def.has_atom_attr("test") { + RunnableKind::Test { + name: name.to_string(), + } + } else { + return None; + }; + Some(Runnable { + range: fn_def.syntax().range(), + kind, + }) +} + +fn runnable_mod(db: &RootDatabase, file_id: FileId, module: &ast::Module) -> Option { + let has_test_function = module + .item_list()? + .items() + .filter_map(|it| match it.kind() { + ast::ModuleItemKind::FnDef(it) => Some(it), + _ => None, + }) + .any(|f| f.has_atom_attr("test")); + if !has_test_function { + return None; + } + let range = module.syntax().range(); + let module = + hir::source_binder::module_from_child_node(db, file_id, module.syntax()).ok()??; + + // FIXME: thread cancellation instead of `.ok`ing + let path = module + .path_to_root(db) + .ok()? + .into_iter() + .rev() + .filter_map(|it| it.name(db).ok()) + .filter_map(|it| it) + .join("::"); + Some(Runnable { + range, + kind: RunnableKind::TestMod { path }, + }) +} diff --git a/crates/ra_ide_api/src/symbol_index.rs b/crates/ra_ide_api/src/symbol_index.rs new file mode 100644 index 000000000..8dd15b40e --- /dev/null +++ b/crates/ra_ide_api/src/symbol_index.rs @@ -0,0 +1,222 @@ +//! This module handles fuzzy-searching of functions, structs and other symbols +//! by name across the whole workspace and dependencies. +//! +//! It works by building an incrementally-updated text-search index of all +//! symbols. The backbone of the index is the **awesome** `fst` crate by +//! @BurntSushi. +//! +//! In a nutshell, you give a set of strings to the `fst`, and it builds a +//! finite state machine describing this set of strtings. The strings which +//! could fuzzy-match a pattern can also be described by a finite state machine. +//! What is freakingly cool is that you can now traverse both state machines in +//! lock-step to enumerate the strings which are both in the input set and +//! fuzz-match the query. Or, more formally, given two langauges described by +//! fsts, one can build an product fst which describes the intersection of the +//! languages. +//! +//! `fst` does not support cheap updating of the index, but it supports unioning +//! of state machines. So, to account for changing source code, we build an fst +//! for each library (which is assumed to never change) and an fst for each rust +//! file in the current workspace, and run a query aginst the union of all +//! thouse fsts. +use std::{ + cmp::Ordering, + hash::{Hash, Hasher}, + sync::Arc, +}; + +use fst::{self, Streamer}; +use ra_syntax::{ + SyntaxNode, SourceFile, SmolStr, TreePtr, AstNode, + algo::{visit::{visitor, Visitor}, find_covering_node}, + SyntaxKind::{self, *}, + ast::{self, NameOwner}, +}; +use ra_db::{SourceRootId, FilesDatabase, LocalSyntaxPtr}; +use salsa::ParallelDatabase; +use rayon::prelude::*; + +use crate::{ + Cancelable, FileId, Query, + db::RootDatabase, +}; + +salsa::query_group! { + pub(crate) trait SymbolsDatabase: hir::db::HirDatabase { + fn file_symbols(file_id: FileId) -> Cancelable> { + type FileSymbolsQuery; + } + fn library_symbols(id: SourceRootId) -> Arc { + type LibrarySymbolsQuery; + storage input; + } + } +} + +fn file_symbols(db: &impl SymbolsDatabase, file_id: FileId) -> Cancelable> { + db.check_canceled()?; + let source_file = db.source_file(file_id); + let mut symbols = source_file + .syntax() + .descendants() + .filter_map(to_symbol) + .map(move |(name, ptr)| FileSymbol { name, ptr, file_id }) + .collect::>(); + + for (name, text_range) in hir::source_binder::macro_symbols(db, file_id)? { + let node = find_covering_node(source_file.syntax(), text_range); + let ptr = LocalSyntaxPtr::new(node); + symbols.push(FileSymbol { file_id, name, ptr }) + } + + Ok(Arc::new(SymbolIndex::new(symbols))) +} + +pub(crate) fn world_symbols(db: &RootDatabase, query: Query) -> Cancelable> { + /// Need to wrap Snapshot to provide `Clone` impl for `map_with` + struct Snap(salsa::Snapshot); + impl Clone for Snap { + fn clone(&self) -> Snap { + Snap(self.0.snapshot()) + } + } + + let buf: Vec> = if query.libs { + let snap = Snap(db.snapshot()); + db.library_roots() + .par_iter() + .map_with(snap, |db, &lib_id| db.0.library_symbols(lib_id)) + .collect() + } else { + let mut files = Vec::new(); + for &root in db.local_roots().iter() { + let sr = db.source_root(root); + files.extend(sr.files.values().map(|&it| it)) + } + + let snap = Snap(db.snapshot()); + files + .par_iter() + .map_with(snap, |db, &file_id| db.0.file_symbols(file_id)) + .filter_map(|it| it.ok()) + .collect() + }; + Ok(query.search(&buf)) +} + +#[derive(Default, Debug)] +pub(crate) struct SymbolIndex { + symbols: Vec, + map: fst::Map, +} + +impl PartialEq for SymbolIndex { + fn eq(&self, other: &SymbolIndex) -> bool { + self.symbols == other.symbols + } +} + +impl Eq for SymbolIndex {} + +impl Hash for SymbolIndex { + fn hash(&self, hasher: &mut H) { + self.symbols.hash(hasher) + } +} + +impl SymbolIndex { + fn new(mut symbols: Vec) -> SymbolIndex { + fn cmp(s1: &FileSymbol, s2: &FileSymbol) -> Ordering { + unicase::Ascii::new(s1.name.as_str()).cmp(&unicase::Ascii::new(s2.name.as_str())) + } + symbols.par_sort_by(cmp); + symbols.dedup_by(|s1, s2| cmp(s1, s2) == Ordering::Equal); + let names = symbols.iter().map(|it| it.name.as_str().to_lowercase()); + let map = fst::Map::from_iter(names.into_iter().zip(0u64..)).unwrap(); + SymbolIndex { symbols, map } + } + + pub(crate) fn len(&self) -> usize { + self.symbols.len() + } + + pub(crate) fn for_files( + files: impl ParallelIterator)>, + ) -> SymbolIndex { + let symbols = files + .flat_map(|(file_id, file)| { + file.syntax() + .descendants() + .filter_map(to_symbol) + .map(move |(name, ptr)| FileSymbol { name, ptr, file_id }) + .collect::>() + }) + .collect::>(); + SymbolIndex::new(symbols) + } +} + +impl Query { + pub(crate) fn search(self, indices: &[Arc]) -> Vec { + let mut op = fst::map::OpBuilder::new(); + for file_symbols in indices.iter() { + let automaton = fst::automaton::Subsequence::new(&self.lowercased); + op = op.add(file_symbols.map.search(automaton)) + } + let mut stream = op.union(); + let mut res = Vec::new(); + while let Some((_, indexed_values)) = stream.next() { + if res.len() >= self.limit { + break; + } + for indexed_value in indexed_values { + let file_symbols = &indices[indexed_value.index]; + let idx = indexed_value.value as usize; + + let symbol = &file_symbols.symbols[idx]; + if self.only_types && !is_type(symbol.ptr.kind()) { + continue; + } + if self.exact && symbol.name != self.query { + continue; + } + res.push(symbol.clone()); + } + } + res + } +} + +fn is_type(kind: SyntaxKind) -> bool { + match kind { + STRUCT_DEF | ENUM_DEF | TRAIT_DEF | TYPE_DEF => true, + _ => false, + } +} + +/// The actual data that is stored in the index. It should be as compact as +/// possible. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct FileSymbol { + pub(crate) file_id: FileId, + pub(crate) name: SmolStr, + pub(crate) ptr: LocalSyntaxPtr, +} + +fn to_symbol(node: &SyntaxNode) -> Option<(SmolStr, LocalSyntaxPtr)> { + fn decl(node: &N) -> Option<(SmolStr, LocalSyntaxPtr)> { + let name = node.name()?.text().clone(); + let ptr = LocalSyntaxPtr::new(node.syntax()); + Some((name, ptr)) + } + visitor() + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .visit(decl::) + .accept(node)? +} diff --git a/crates/ra_ide_api/src/syntax_highlighting.rs b/crates/ra_ide_api/src/syntax_highlighting.rs new file mode 100644 index 000000000..cb19e9515 --- /dev/null +++ b/crates/ra_ide_api/src/syntax_highlighting.rs @@ -0,0 +1,92 @@ +use ra_syntax::{ast, AstNode,}; +use ra_db::SyntaxDatabase; + +use crate::{ + FileId, Cancelable, HighlightedRange, + db::RootDatabase, +}; + +pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Cancelable> { + let source_file = db.source_file(file_id); + let mut res = ra_ide_api_light::highlight(source_file.syntax()); + for macro_call in source_file + .syntax() + .descendants() + .filter_map(ast::MacroCall::cast) + { + if let Some((off, exp)) = hir::MacroDef::ast_expand(macro_call) { + let mapped_ranges = ra_ide_api_light::highlight(&exp.syntax()) + .into_iter() + .filter_map(|r| { + let mapped_range = exp.map_range_back(r.range)?; + let res = HighlightedRange { + range: mapped_range + off, + tag: r.tag, + }; + Some(res) + }); + res.extend(mapped_ranges); + } + } + Ok(res) +} + +#[cfg(test)] +mod tests { + use crate::mock_analysis::single_file; + use test_utils::assert_eq_dbg; + + #[test] + fn highlights_code_inside_macros() { + let (analysis, file_id) = single_file( + " + fn main() { + ctry!({ let x = 92; x}); + vec![{ let x = 92; x}]; + } + ", + ); + let highlights = analysis.highlight(file_id).unwrap(); + assert_eq_dbg( + r#"[HighlightedRange { range: [13; 15), tag: "keyword" }, + HighlightedRange { range: [16; 20), tag: "function" }, + HighlightedRange { range: [41; 46), tag: "macro" }, + HighlightedRange { range: [49; 52), tag: "keyword" }, + HighlightedRange { range: [57; 59), tag: "literal" }, + HighlightedRange { range: [82; 86), tag: "macro" }, + HighlightedRange { range: [89; 92), tag: "keyword" }, + HighlightedRange { range: [97; 99), tag: "literal" }, + HighlightedRange { range: [49; 52), tag: "keyword" }, + HighlightedRange { range: [53; 54), tag: "function" }, + HighlightedRange { range: [57; 59), tag: "literal" }, + HighlightedRange { range: [61; 62), tag: "text" }, + HighlightedRange { range: [89; 92), tag: "keyword" }, + HighlightedRange { range: [93; 94), tag: "function" }, + HighlightedRange { range: [97; 99), tag: "literal" }, + HighlightedRange { range: [101; 102), tag: "text" }]"#, + &highlights, + ) + } + + // FIXME: this test is not really necessary: artifact of the inital hacky + // macros implementation. + #[test] + fn highlight_query_group_macro() { + let (analysis, file_id) = single_file( + " + salsa::query_group! { + pub trait HirDatabase: SyntaxDatabase {} + } + ", + ); + let highlights = analysis.highlight(file_id).unwrap(); + assert_eq_dbg( + r#"[HighlightedRange { range: [20; 32), tag: "macro" }, + HighlightedRange { range: [13; 18), tag: "text" }, + HighlightedRange { range: [51; 54), tag: "keyword" }, + HighlightedRange { range: [55; 60), tag: "keyword" }, + HighlightedRange { range: [61; 72), tag: "function" }]"#, + &highlights, + ) + } +} diff --git a/crates/ra_ide_api/tests/test/main.rs b/crates/ra_ide_api/tests/test/main.rs new file mode 100644 index 000000000..d1dc07e5b --- /dev/null +++ b/crates/ra_ide_api/tests/test/main.rs @@ -0,0 +1,249 @@ +mod runnables; + +use ra_syntax::TextRange; +use test_utils::{assert_eq_dbg, assert_eq_text}; + +use ra_ide_api::{ + mock_analysis::{analysis_and_position, single_file, single_file_with_position, MockAnalysis}, + AnalysisChange, CrateGraph, FileId, Query +}; + +#[test] +fn test_unresolved_module_diagnostic() { + let (analysis, file_id) = single_file("mod foo;"); + let diagnostics = analysis.diagnostics(file_id).unwrap(); + assert_eq_dbg( + r#"[Diagnostic { + message: "unresolved module", + range: [4; 7), + fix: Some(SourceChange { + label: "create module", + source_file_edits: [], + file_system_edits: [CreateFile { source_root: SourceRootId(0), path: "foo.rs" }], + cursor_position: None }), + severity: Error }]"#, + &diagnostics, + ); +} + +// FIXME: move this test to hir +#[test] +fn test_unresolved_module_diagnostic_no_diag_for_inline_mode() { + let (analysis, file_id) = single_file("mod foo {}"); + let diagnostics = analysis.diagnostics(file_id).unwrap(); + assert_eq_dbg(r#"[]"#, &diagnostics); +} + +#[test] +fn test_resolve_parent_module() { + let (analysis, pos) = analysis_and_position( + " + //- /lib.rs + mod foo; + //- /foo.rs + <|>// empty + ", + ); + let symbols = analysis.parent_module(pos).unwrap(); + assert_eq_dbg( + r#"[NavigationTarget { file_id: FileId(1), name: "foo", kind: MODULE, range: [4; 7), ptr: None }]"#, + &symbols, + ); +} + +#[test] +fn test_resolve_parent_module_for_inline() { + let (analysis, pos) = analysis_and_position( + " + //- /lib.rs + mod foo { + mod bar { + mod baz { <|> } + } + } + ", + ); + let symbols = analysis.parent_module(pos).unwrap(); + assert_eq_dbg( + r#"[NavigationTarget { file_id: FileId(1), name: "baz", kind: MODULE, range: [36; 39), ptr: None }]"#, + &symbols, + ); +} + +#[test] +fn test_resolve_crate_root() { + let mock = MockAnalysis::with_files( + " + //- /bar.rs + mod foo; + //- /bar/foo.rs + // emtpy <|> + ", + ); + let root_file = mock.id_of("/bar.rs"); + let mod_file = mock.id_of("/bar/foo.rs"); + let mut host = mock.analysis_host(); + assert!(host.analysis().crate_for(mod_file).unwrap().is_empty()); + + let mut crate_graph = CrateGraph::default(); + let crate_id = crate_graph.add_crate_root(root_file); + let mut change = AnalysisChange::new(); + change.set_crate_graph(crate_graph); + host.apply_change(change); + + assert_eq!(host.analysis().crate_for(mod_file).unwrap(), vec![crate_id]); +} + +fn get_all_refs(text: &str) -> Vec<(FileId, TextRange)> { + let (analysis, position) = single_file_with_position(text); + analysis.find_all_refs(position).unwrap() +} + +#[test] +fn test_find_all_refs_for_local() { + let code = r#" + fn main() { + let mut i = 1; + let j = 1; + i = i<|> + j; + + { + i = 0; + } + + i = 5; + }"#; + + let refs = get_all_refs(code); + assert_eq!(refs.len(), 5); +} + +#[test] +fn test_find_all_refs_for_param_inside() { + let code = r#" + fn foo(i : u32) -> u32 { + i<|> + }"#; + + let refs = get_all_refs(code); + assert_eq!(refs.len(), 2); +} + +#[test] +fn test_find_all_refs_for_fn_param() { + let code = r#" + fn foo(i<|> : u32) -> u32 { + i + }"#; + + let refs = get_all_refs(code); + assert_eq!(refs.len(), 2); +} +#[test] +fn test_rename_for_local() { + test_rename( + r#" + fn main() { + let mut i = 1; + let j = 1; + i = i<|> + j; + + { + i = 0; + } + + i = 5; + }"#, + "k", + r#" + fn main() { + let mut k = 1; + let j = 1; + k = k + j; + + { + k = 0; + } + + k = 5; + }"#, + ); +} + +#[test] +fn test_rename_for_param_inside() { + test_rename( + r#" + fn foo(i : u32) -> u32 { + i<|> + }"#, + "j", + r#" + fn foo(j : u32) -> u32 { + j + }"#, + ); +} + +#[test] +fn test_rename_refs_for_fn_param() { + test_rename( + r#" + fn foo(i<|> : u32) -> u32 { + i + }"#, + "new_name", + r#" + fn foo(new_name : u32) -> u32 { + new_name + }"#, + ); +} + +#[test] +fn test_rename_for_mut_param() { + test_rename( + r#" + fn foo(mut i<|> : u32) -> u32 { + i + }"#, + "new_name", + r#" + fn foo(mut new_name : u32) -> u32 { + new_name + }"#, + ); +} + +fn test_rename(text: &str, new_name: &str, expected: &str) { + let (analysis, position) = single_file_with_position(text); + let edits = analysis.rename(position, new_name).unwrap(); + let mut text_edit_bulder = ra_text_edit::TextEditBuilder::default(); + let mut file_id: Option = None; + for edit in edits { + file_id = Some(edit.file_id); + for atom in edit.edit.as_atoms() { + text_edit_bulder.replace(atom.delete, atom.insert.clone()); + } + } + let result = text_edit_bulder + .finish() + .apply(&*analysis.file_text(file_id.unwrap())); + assert_eq_text!(expected, &*result); +} + +#[test] +fn world_symbols_include_stuff_from_macros() { + let (analysis, _) = single_file( + " +salsa::query_group! { +pub trait HirDatabase: SyntaxDatabase {} +} + ", + ); + + let mut symbols = analysis.symbol_search(Query::new("Hir".into())).unwrap(); + let s = symbols.pop().unwrap(); + assert_eq!(s.name(), "HirDatabase"); + assert_eq!(s.range(), TextRange::from_to(33.into(), 44.into())); +} diff --git a/crates/ra_ide_api/tests/test/runnables.rs b/crates/ra_ide_api/tests/test/runnables.rs new file mode 100644 index 000000000..da8d5e0d5 --- /dev/null +++ b/crates/ra_ide_api/tests/test/runnables.rs @@ -0,0 +1,109 @@ +use test_utils::assert_eq_dbg; + +use ra_ide_api::mock_analysis::analysis_and_position; + +#[test] +fn test_runnables() { + let (analysis, pos) = analysis_and_position( + r#" + //- /lib.rs + <|> //empty + fn main() {} + + #[test] + fn test_foo() {} + + #[test] + #[ignore] + fn test_foo() {} + "#, + ); + let runnables = analysis.runnables(pos.file_id).unwrap(); + assert_eq_dbg( + r#"[Runnable { range: [1; 21), kind: Bin }, + Runnable { range: [22; 46), kind: Test { name: "test_foo" } }, + Runnable { range: [47; 81), kind: Test { name: "test_foo" } }]"#, + &runnables, + ) +} + +#[test] +fn test_runnables_module() { + let (analysis, pos) = analysis_and_position( + r#" + //- /lib.rs + <|> //empty + mod test_mod { + #[test] + fn test_foo1() {} + } + "#, + ); + let runnables = analysis.runnables(pos.file_id).unwrap(); + assert_eq_dbg( + r#"[Runnable { range: [1; 59), kind: TestMod { path: "test_mod" } }, + Runnable { range: [28; 57), kind: Test { name: "test_foo1" } }]"#, + &runnables, + ) +} + +#[test] +fn test_runnables_one_depth_layer_module() { + let (analysis, pos) = analysis_and_position( + r#" + //- /lib.rs + <|> //empty + mod foo { + mod test_mod { + #[test] + fn test_foo1() {} + } + } + "#, + ); + let runnables = analysis.runnables(pos.file_id).unwrap(); + assert_eq_dbg( + r#"[Runnable { range: [23; 85), kind: TestMod { path: "foo::test_mod" } }, + Runnable { range: [46; 79), kind: Test { name: "test_foo1" } }]"#, + &runnables, + ) +} + +#[test] +fn test_runnables_multiple_depth_module() { + let (analysis, pos) = analysis_and_position( + r#" + //- /lib.rs + <|> //empty + mod foo { + mod bar { + mod test_mod { + #[test] + fn test_foo1() {} + } + } + } + "#, + ); + let runnables = analysis.runnables(pos.file_id).unwrap(); + assert_eq_dbg( + r#"[Runnable { range: [41; 115), kind: TestMod { path: "foo::bar::test_mod" } }, + Runnable { range: [68; 105), kind: Test { name: "test_foo1" } }]"#, + &runnables, + ) +} + +#[test] +fn test_runnables_no_test_function_in_module() { + let (analysis, pos) = analysis_and_position( + r#" + //- /lib.rs + <|> //empty + mod test_mod { + fn foo1() {} + } + "#, + ); + let runnables = analysis.runnables(pos.file_id).unwrap(); + assert_eq_dbg(r#"[]"#, &runnables) +} diff --git a/crates/ra_lsp_server/Cargo.toml b/crates/ra_lsp_server/Cargo.toml index b9fd61105..296fae34f 100644 --- a/crates/ra_lsp_server/Cargo.toml +++ b/crates/ra_lsp_server/Cargo.toml @@ -29,7 +29,7 @@ parking_lot = "0.7.0" thread_worker = { path = "../thread_worker" } ra_syntax = { path = "../ra_syntax" } ra_text_edit = { path = "../ra_text_edit" } -ra_analysis = { path = "../ra_analysis" } +ra_ide_api = { path = "../ra_ide_api" } gen_lsp_server = { path = "../gen_lsp_server" } ra_vfs = { path = "../ra_vfs" } diff --git a/crates/ra_lsp_server/src/conv.rs b/crates/ra_lsp_server/src/conv.rs index b3f8c83cc..5c8b3c194 100644 --- a/crates/ra_lsp_server/src/conv.rs +++ b/crates/ra_lsp_server/src/conv.rs @@ -4,7 +4,7 @@ use languageserver_types::{ TextDocumentItem, TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier, WorkspaceEdit, }; -use ra_analysis::{ +use ra_ide_api::{ CompletionItem, CompletionItemKind, FileId, FilePosition, FileRange, FileSystemEdit, InsertText, NavigationTarget, SourceChange, SourceFileEdit, LineCol, LineIndex, translate_offset_with_edit diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs index 2dc1be26a..96923fac7 100644 --- a/crates/ra_lsp_server/src/main_loop.rs +++ b/crates/ra_lsp_server/src/main_loop.rs @@ -10,7 +10,7 @@ use gen_lsp_server::{ handle_shutdown, ErrorCode, RawMessage, RawNotification, RawRequest, RawResponse, }; use languageserver_types::NumberOrString; -use ra_analysis::{Canceled, FileId, LibraryData}; +use ra_ide_api::{Canceled, FileId, LibraryData}; use ra_vfs::VfsTask; use rayon; use rustc_hash::FxHashSet; diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index b7777bfc3..a653c5ada 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs @@ -8,7 +8,7 @@ use languageserver_types::{ ParameterInformation, ParameterLabel, Position, PrepareRenameResponse, Range, RenameParams, SignatureInformation, SymbolInformation, TextDocumentIdentifier, TextEdit, WorkspaceEdit, }; -use ra_analysis::{ +use ra_ide_api::{ FileId, FilePosition, FileRange, FoldKind, Query, RunnableKind, Severity, SourceChange, }; use ra_syntax::{TextUnit, AstNode}; @@ -736,7 +736,7 @@ fn highlight(world: &ServerWorld, file_id: FileId) -> Result> { } fn to_diagnostic_severity(severity: Severity) -> DiagnosticSeverity { - use ra_analysis::Severity::*; + use ra_ide_api::Severity::*; match severity { Error => DiagnosticSeverity::Error, diff --git a/crates/ra_lsp_server/src/main_loop/subscriptions.rs b/crates/ra_lsp_server/src/main_loop/subscriptions.rs index 03f41e870..a83e01557 100644 --- a/crates/ra_lsp_server/src/main_loop/subscriptions.rs +++ b/crates/ra_lsp_server/src/main_loop/subscriptions.rs @@ -1,4 +1,4 @@ -use ra_analysis::FileId; +use ra_ide_api::FileId; use rustc_hash::FxHashSet; pub struct Subscriptions { diff --git a/crates/ra_lsp_server/src/server_world.rs b/crates/ra_lsp_server/src/server_world.rs index ebf2b15cc..76c76766d 100644 --- a/crates/ra_lsp_server/src/server_world.rs +++ b/crates/ra_lsp_server/src/server_world.rs @@ -1,10 +1,10 @@ use std::{ - path::{PathBuf}, + path::PathBuf, sync::Arc, }; use languageserver_types::Url; -use ra_analysis::{ +use ra_ide_api::{ Analysis, AnalysisChange, AnalysisHost, CrateGraph, FileId, LibraryData, SourceRootId }; @@ -12,7 +12,7 @@ use ra_vfs::{Vfs, VfsChange, VfsFile, VfsRoot}; use rustc_hash::FxHashMap; use relative_path::RelativePathBuf; use parking_lot::RwLock; -use failure::{format_err}; +use failure::format_err; use crate::{ project_model::{CargoWorkspace, TargetKind}, -- cgit v1.2.3