"proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)",
]
-[[package]]
-name = "ra_analysis"
-version = "0.1.0"
-dependencies = [
- "fst 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "ra_db 0.1.0",
- "ra_hir 0.1.0",
- "ra_ide_api_light 0.1.0",
- "ra_syntax 0.1.0",
- "ra_text_edit 0.1.0",
- "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "relative-path 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "salsa 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "test_utils 0.1.0",
- "unicase 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
[[package]]
name = "ra_arena"
version = "0.1.0"
"test_utils 0.1.0",
]
+[[package]]
+name = "ra_ide_api"
+version = "0.1.0"
+dependencies = [
+ "fst 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ra_db 0.1.0",
+ "ra_hir 0.1.0",
+ "ra_ide_api_light 0.1.0",
+ "ra_syntax 0.1.0",
+ "ra_text_edit 0.1.0",
+ "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "relative-path 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "salsa 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "test_utils 0.1.0",
+ "unicase 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "ra_ide_api_light"
version = "0.1.0"
"languageserver-types 0.53.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "ra_analysis 0.1.0",
+ "ra_ide_api 0.1.0",
"ra_syntax 0.1.0",
"ra_text_edit 0.1.0",
"ra_vfs 0.1.0",
--- /dev/null
+[package]
+edition = "2018"
+name = "ra_ide_api"
+version = "0.1.0"
+authors = ["Aleksey Kladov <aleksey.kladov@gmail.com>"]
+
+[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" }
--- /dev/null
+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<Option<CallInfo>> {
+ 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<FnCallNode<'a>> {
+ if let Some(expr) = find_node_at_offset::<ast::CallExpr>(syntax, offset) {
+ return Some(FnCallNode::CallExpr(expr));
+ }
+ if let Some(expr) = find_node_at_offset::<ast::MethodCallExpr>(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<Self> {
+ 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<String> {
+ 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::<String>::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<E>
+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()
+ )
+ );
+ }
+
+}
--- /dev/null
+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<Option<Completions>> {
+ 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);
+}
--- /dev/null
+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#""#,
+ );
+ }
+}
--- /dev/null
+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::<ast::SourceFile, _>(process)
+ .visit::<ast::ItemList, _>(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<String, (u32, &'a ast::Param)>,
+ ) {
+ 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""#,
+ );
+ }
+}
--- /dev/null
+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::<ast::ForExpr, _>(LoopBodyOwner::loop_body)
+ .visit::<ast::WhileExpr, _>(LoopBodyOwner::loop_body)
+ .visit::<ast::LoopExpr, _>(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<CompletionItem> {
+ 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"
+ "#,
+ )
+ }
+}
--- /dev/null
+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",
+ )
+ }
+}
--- /dev/null
+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)""#,
+ );
+ }
+}
--- /dev/null
+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"
+ "##,
+ );
+ }
+}
--- /dev/null
+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<hir::Module>,
+ pub(super) function: Option<hir::Function>,
+ 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<hir::Path>,
+ 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<Option<CompletionContext<'a>>> {
+ 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::<ast::NameRef>(file.syntax(), offset) {
+ // Special case, `trait T { fn foo(i_am_a_name_ref) {} }`.
+ // See RFC#1685.
+ if is_node::<ast::Param>(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::<ast::Name>(file.syntax(), offset) {
+ if is_node::<ast::Param>(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::<ast::IfExpr>(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<N: AstNode>(syntax: &SyntaxNode, range: TextRange) -> Option<&N> {
+ let node = find_covering_node(syntax, range);
+ node.ancestors().find_map(N::cast)
+}
+
+fn is_node<N: AstNode>(node: &SyntaxNode) -> bool {
+ match node.ancestors().filter_map(N::cast).next() {
+ None => false,
+ Some(n) => n.syntax().range() == node.range(),
+ }
+}
--- /dev/null
+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<String>,
+ snippet: Option<String>,
+ kind: Option<CompletionItemKind>,
+}
+
+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<String>) -> 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<CompletionItemKind> {
+ self.kind
+ }
+}
+
+/// A helper to make `CompletionItem`s.
+#[must_use]
+pub(crate) struct Builder {
+ completion_kind: CompletionKind,
+ label: String,
+ lookup: Option<String>,
+ snippet: Option<String>,
+ kind: Option<CompletionItemKind>,
+}
+
+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<String>) -> Builder {
+ self.lookup = Some(lookup.into());
+ self
+ }
+ pub(crate) fn snippet(mut self, snippet: impl Into<String>) -> 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<CompletionItem> 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<CompletionItem>,
+}
+
+impl Completions {
+ pub(crate) fn add(&mut self, item: impl Into<CompletionItem>) {
+ self.buf.push(item.into())
+ }
+ pub(crate) fn add_all<I>(&mut self, items: I)
+ where
+ I: IntoIterator,
+ I::Item: Into<CompletionItem>,
+ {
+ 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<Vec<CompletionItem>> for Completions {
+ fn into(self) -> Vec<CompletionItem> {
+ self.buf
+ }
+}
--- /dev/null
+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<RootDatabase>,
+ id_maps: Arc<IdMaps>,
+}
+
+#[derive(Default)]
+struct IdMaps {
+ defs: LocationIntener<hir::DefLoc, hir::DefId>,
+ macros: LocationIntener<hir::MacroCallLoc, hir::MacroCallId>,
+}
+
+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<RootDatabase> {
+ &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<RootDatabase> {
+ salsa::Snapshot::new(RootDatabase {
+ runtime: self.runtime.snapshot(self),
+ id_maps: self.id_maps.clone(),
+ })
+ }
+}
+
+impl BaseDatabase for RootDatabase {}
+
+impl AsRef<LocationIntener<hir::DefLoc, hir::DefId>> for RootDatabase {
+ fn as_ref(&self) -> &LocationIntener<hir::DefLoc, hir::DefId> {
+ &self.id_maps.defs
+ }
+}
+
+impl AsRef<LocationIntener<hir::MacroCallLoc, hir::MacroCallId>> for RootDatabase {
+ fn as_ref(&self) -> &LocationIntener<hir::MacroCallLoc, hir::MacroCallId> {
+ &self.id_maps.macros
+ }
+}
+
+salsa::query_group! {
+ pub(crate) trait LineIndexDatabase: ra_db::FilesDatabase + BaseDatabase {
+ fn line_index(file_id: FileId) -> Arc<LineIndex> {
+ type LineIndexQuery;
+ }
+ }
+}
+
+fn line_index(db: &impl ra_db::FilesDatabase, file_id: FileId) -> Arc<LineIndex> {
+ 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;
+ }
+ }
+}
--- /dev/null
+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<TextRange> {
+ 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);
+ }
+}
--- /dev/null
+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<Option<Vec<NavigationTarget>>> {
+ let file = db.source_file(position.file_id);
+ let syntax = file.syntax();
+ if let Some(name_ref) = find_node_at_offset::<ast::NameRef>(syntax, position.offset) {
+ return Ok(Some(reference_defenition(db, position.file_id, name_ref)?));
+ }
+ if let Some(name) = find_node_at_offset::<ast::Name>(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<Vec<NavigationTarget>> {
+ 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<Option<Vec<NavigationTarget>>> {
+ 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,
+ );
+ }
+}
--- /dev/null
+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<Option<RangeInfo<String>>> {
+ 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::<ast::NameRef>(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<Option<String>> {
+ 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<Option<String>> {
+ 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<TreePtr<SyntaxNode>> {
+ 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<String> {
+ let node = self.node(db)?;
+ fn doc_comments<N: ast::DocCommentsOwner>(node: &N) -> Option<String> {
+ let comments = node.doc_comment_text();
+ if comments.is_empty() {
+ None
+ } else {
+ Some(comments)
+ }
+ }
+
+ visitor()
+ .visit(doc_comments::<ast::FnDef>)
+ .visit(doc_comments::<ast::StructDef>)
+ .visit(doc_comments::<ast::EnumDef>)
+ .visit(doc_comments::<ast::TraitDef>)
+ .visit(doc_comments::<ast::Module>)
+ .visit(doc_comments::<ast::TypeDef>)
+ .visit(doc_comments::<ast::ConstDef>)
+ .visit(doc_comments::<ast::StaticDef>)
+ .accept(&node)?
+ }
+
+ /// Get a description of this node.
+ ///
+ /// e.g. `struct Name`, `enum Name`, `fn Name`
+ fn description(&self, db: &RootDatabase) -> Option<String> {
+ // 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);
+ }
+
+}
--- /dev/null
+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<Vec<NavigationTarget>> {
+ 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<Vec<CrateId>> {
+ 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<Vec<(FileId, TextRange)>> {
+ 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::<Vec<_>>();
+ 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<Option<(&'a ast::BindPat, hir::Function)>> {
+ let syntax = source_file.syntax();
+ if let Some(binding) = find_node_at_offset::<ast::BindPat>(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::<ast::NameRef>(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::<ast::BindPat>(
+ syntax,
+ resolved.range().end()
+ ));
+ Ok(Some((binding, descr)))
+ }
+ }
+
+ pub(crate) fn diagnostics(&self, file_id: FileId) -> Cancelable<Vec<Diagnostic>> {
+ 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::<Vec<_>>();
+ 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<SourceChange> {
+ 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<Vec<SourceFileEdit>> {
+ 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::<Vec<_>>();
+ Ok(res)
+ }
+ pub(crate) fn index_resolve(&self, name_ref: &ast::NameRef) -> Cancelable<Vec<FileSymbol>> {
+ 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 }),
+ }
+ }
+}
--- /dev/null
+//! 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<SourceRootId, RootChange>,
+ files_changed: Vec<(FileId, Arc<String>)>,
+ libraries_added: Vec<LibraryData>,
+ crate_graph: Option<CrateGraph>,
+}
+
+#[derive(Default)]
+struct RootChange {
+ added: Vec<AddFile>,
+ removed: Vec<RemoveFile>,
+}
+
+#[derive(Debug)]
+struct AddFile {
+ file_id: FileId,
+ path: RelativePathBuf,
+ text: Arc<String>,
+}
+
+#[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<String>,
+ ) {
+ 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<String>) {
+ 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<SourceFileEdit>,
+ pub file_system_edits: Vec<FileSystemEdit>,
+ pub cursor_position: Option<FilePosition>,
+}
+
+#[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<SourceChange>,
+ 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<LocalSyntaxPtr>,
+}
+
+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<T> {
+ pub range: TextRange,
+ pub info: T,
+}
+
+impl<T> RangeInfo<T> {
+ fn new(range: TextRange, info: T) -> RangeInfo<T> {
+ RangeInfo { range, info }
+ }
+}
+
+#[derive(Debug)]
+pub struct CallInfo {
+ pub label: String,
+ pub doc: Option<String>,
+ pub parameters: Vec<String>,
+ pub active_parameter: Option<usize>,
+}
+
+/// `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<db::RootDatabase>,
+}
+
+impl Analysis {
+ /// Gets the text of the source file.
+ pub fn file_text(&self, file_id: FileId) -> Arc<String> {
+ self.db.file_text(file_id)
+ }
+ /// Gets the syntax tree of the file.
+ pub fn file_syntax(&self, file_id: FileId) -> TreePtr<SourceFile> {
+ 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<LineIndex> {
+ 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<TextUnit> {
+ 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<SourceChange> {
+ 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<SourceChange> {
+ 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<SourceChange> {
+ 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<StructureNode> {
+ 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<Fold> {
+ 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<Vec<NavigationTarget>> {
+ 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<Option<Vec<NavigationTarget>>> {
+ goto_defenition::goto_defenition(&*self.db, position)
+ }
+ /// Finds all usages of the reference at point.
+ pub fn find_all_refs(&self, position: FilePosition) -> Cancelable<Vec<(FileId, TextRange)>> {
+ self.db.find_all_refs(position)
+ }
+ /// Returns a short text descrbing element at position.
+ pub fn hover(&self, position: FilePosition) -> Cancelable<Option<RangeInfo<String>>> {
+ hover::hover(&*self.db, position)
+ }
+ /// Computes parameter information for the given call expression.
+ pub fn call_info(&self, position: FilePosition) -> Cancelable<Option<CallInfo>> {
+ 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<Vec<NavigationTarget>> {
+ self.db.parent_module(position)
+ }
+ /// Returns crates this file belongs too.
+ pub fn crate_for(&self, file_id: FileId) -> Cancelable<Vec<CrateId>> {
+ self.db.crate_for(file_id)
+ }
+ /// Returns the root file of the given crate.
+ pub fn crate_root(&self, crate_id: CrateId) -> Cancelable<FileId> {
+ 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<Vec<Runnable>> {
+ runnables::runnables(&*self.db, file_id)
+ }
+ /// Computes syntax highlighting for the given file.
+ pub fn highlight(&self, file_id: FileId) -> Cancelable<Vec<HighlightedRange>> {
+ syntax_highlighting::highlight(&*self.db, file_id)
+ }
+ /// Computes completions at the given position.
+ pub fn completions(&self, position: FilePosition) -> Cancelable<Option<Vec<CompletionItem>>> {
+ 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<Vec<SourceChange>> {
+ Ok(self.db.assists(frange))
+ }
+ /// Computes the set of diagnostics for the given file.
+ pub fn diagnostics(&self, file_id: FileId) -> Cancelable<Vec<Diagnostic>> {
+ self.db.diagnostics(file_id)
+ }
+ /// Computes the type of the expression at the given position.
+ pub fn type_of(&self, frange: FileRange) -> Cancelable<Option<String>> {
+ 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<Vec<SourceFileEdit>> {
+ 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<String>)>,
+ ) -> 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<T: Send>() {}
+ is_send::<Analysis>();
+}
--- /dev/null
+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)
+}
--- /dev/null
+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<Vec<Runnable>> {
+ 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<Runnable> {
+ 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<Runnable> {
+ 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<Runnable> {
+ 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 },
+ })
+}
--- /dev/null
+//! 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<Arc<SymbolIndex>> {
+ type FileSymbolsQuery;
+ }
+ fn library_symbols(id: SourceRootId) -> Arc<SymbolIndex> {
+ type LibrarySymbolsQuery;
+ storage input;
+ }
+ }
+}
+
+fn file_symbols(db: &impl SymbolsDatabase, file_id: FileId) -> Cancelable<Arc<SymbolIndex>> {
+ 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::<Vec<_>>();
+
+ 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<Vec<FileSymbol>> {
+ /// Need to wrap Snapshot to provide `Clone` impl for `map_with`
+ struct Snap(salsa::Snapshot<RootDatabase>);
+ impl Clone for Snap {
+ fn clone(&self) -> Snap {
+ Snap(self.0.snapshot())
+ }
+ }
+
+ let buf: Vec<Arc<SymbolIndex>> = 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<FileSymbol>,
+ 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<H: Hasher>(&self, hasher: &mut H) {
+ self.symbols.hash(hasher)
+ }
+}
+
+impl SymbolIndex {
+ fn new(mut symbols: Vec<FileSymbol>) -> 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<Item = (FileId, TreePtr<SourceFile>)>,
+ ) -> 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::<Vec<_>>()
+ })
+ .collect::<Vec<_>>();
+ SymbolIndex::new(symbols)
+ }
+}
+
+impl Query {
+ pub(crate) fn search(self, indices: &[Arc<SymbolIndex>]) -> Vec<FileSymbol> {
+ 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<N: NameOwner>(node: &N) -> Option<(SmolStr, LocalSyntaxPtr)> {
+ let name = node.name()?.text().clone();
+ let ptr = LocalSyntaxPtr::new(node.syntax());
+ Some((name, ptr))
+ }
+ visitor()
+ .visit(decl::<ast::FnDef>)
+ .visit(decl::<ast::StructDef>)
+ .visit(decl::<ast::EnumDef>)
+ .visit(decl::<ast::TraitDef>)
+ .visit(decl::<ast::Module>)
+ .visit(decl::<ast::TypeDef>)
+ .visit(decl::<ast::ConstDef>)
+ .visit(decl::<ast::StaticDef>)
+ .accept(node)?
+}
--- /dev/null
+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<Vec<HighlightedRange>> {
+ 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,
+ )
+ }
+}
--- /dev/null
+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<FileId> = 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()));
+}
--- /dev/null
+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)
+}
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" }
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
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;
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};
}
fn to_diagnostic_severity(severity: Severity) -> DiagnosticSeverity {
- use ra_analysis::Severity::*;
+ use ra_ide_api::Severity::*;
match severity {
Error => DiagnosticSeverity::Error,
-use ra_analysis::FileId;
+use ra_ide_api::FileId;
use rustc_hash::FxHashSet;
pub struct Subscriptions {
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
};
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},