Repository: rust-analyzer/rust-analyzer
Pull request #3998 (2020-04-16 21:58:50)

Make add_function generate functions in other modules via qualified path
Additional feature for #3639 

- [x] Add tests for paths with more segments
- [x] Make generating the function in another file work
- [x] Add `pub` or `pub(crate)` to the generated function if it's generated in a different module
- [x] Make the assist jump to the edited file
- [x] Enable file support in the `check_assist` helper


Patch:
diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs
index c3e6532999..2791632576 100644
--- a/crates/ra_assists/src/assist_ctx.rs
+++ b/crates/ra_assists/src/assist_ctx.rs
@@ -10,7 +10,7 @@ use ra_syntax::{
 };
 use ra_text_edit::TextEditBuilder;
 
-use crate::{AssistAction, AssistId, AssistLabel, GroupLabel, ResolvedAssist};
+use crate::{AssistAction, AssistFile, AssistId, AssistLabel, GroupLabel, ResolvedAssist};
 use algo::SyntaxRewriter;
 
 #[derive(Clone, Debug)]
@@ -180,6 +180,7 @@ pub(crate) struct ActionBuilder {
     edit: TextEditBuilder,
     cursor_position: Option<TextUnit>,
     target: Option<TextRange>,
+    file: AssistFile,
 }
 
 impl ActionBuilder {
@@ -241,11 +242,16 @@ impl ActionBuilder {
         algo::diff(&node, &new).into_text_edit(&mut self.edit)
     }
 
+    pub(crate) fn set_file(&mut self, assist_file: AssistFile) {
+        self.file = assist_file
+    }
+
     fn build(self) -> AssistAction {
         AssistAction {
             edit: self.edit.finish(),
             cursor_position: self.cursor_position,
             target: self.target,
+            file: self.file,
         }
     }
 }
diff --git a/crates/ra_assists/src/handlers/add_function.rs b/crates/ra_assists/src/handlers/add_function.rs
index ad4ab66ede..f185cffdb7 100644
--- a/crates/ra_assists/src/handlers/add_function.rs
+++ b/crates/ra_assists/src/handlers/add_function.rs
@@ -3,8 +3,8 @@ use ra_syntax::{
     SyntaxKind, SyntaxNode, TextUnit,
 };
 
-use crate::{Assist, AssistCtx, AssistId};
-use ast::{edit::IndentLevel, ArgListOwner, CallExpr, Expr};
+use crate::{Assist, AssistCtx, AssistFile, AssistId};
+use ast::{edit::IndentLevel, ArgListOwner, ModuleItemOwner};
 use hir::HirDisplay;
 use rustc_hash::{FxHashMap, FxHashSet};
 
@@ -16,7 +16,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
 // struct Baz;
 // fn baz() -> Baz { Baz }
 // fn foo() {
-//      bar<|>("", baz());
+//     bar<|>("", baz());
 // }
 //
 // ```
@@ -25,7 +25,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
 // struct Baz;
 // fn baz() -> Baz { Baz }
 // fn foo() {
-//      bar("", baz());
+//     bar("", baz());
 // }
 //
 // fn bar(arg: &str, baz: Baz) {
@@ -38,21 +38,30 @@ pub(crate) fn add_function(ctx: AssistCtx) -> Option<Assist> {
     let call = path_expr.syntax().parent().and_then(ast::CallExpr::cast)?;
     let path = path_expr.path()?;
 
-    if path.qualifier().is_some() {
-        return None;
-    }
-
     if ctx.sema.resolve_path(&path).is_some() {
         // The function call already resolves, no need to add a function
         return None;
     }
 
-    let function_builder = FunctionBuilder::from_call(&ctx, &call)?;
+    let target_module = if let Some(qualifier) = path.qualifier() {
+        if let Some(hir::PathResolution::Def(hir::ModuleDef::Module(module))) =
+            ctx.sema.resolve_path(&qualifier)
+        {
+            Some(module.definition_source(ctx.sema.db))
+        } else {
+            return None;
+        }
+    } else {
+        None
+    };
+
+    let function_builder = FunctionBuilder::from_call(&ctx, &call, &path, target_module)?;
 
     ctx.add_assist(AssistId("add_function"), "Add function", |edit| {
         edit.target(call.syntax().text_range());
 
         if let Some(function_template) = function_builder.render() {
+            edit.set_file(function_template.file);
             edit.set_cursor(function_template.cursor_offset);
             edit.insert(function_template.insert_offset, function_template.fn_def.to_string());
         }
@@ -63,29 +72,67 @@ struct FunctionTemplate {
     insert_offset: TextUnit,
     cursor_offset: TextUnit,
     fn_def: ast::SourceFile,
+    file: AssistFile,
 }
 
 struct FunctionBuilder {
-    append_fn_at: SyntaxNode,
+    target: GeneratedFunctionTarget,
     fn_name: ast::Name,
     type_params: Option<ast::TypeParamList>,
     params: ast::ParamList,
+    file: AssistFile,
+    needs_pub: bool,
 }
 
 impl FunctionBuilder {
-    fn from_call(ctx: &AssistCtx, call: &ast::CallExpr) -> Option<Self> {
-        let append_fn_at = next_space_for_fn(&call)?;
-        let fn_name = fn_name(&call)?;
+    /// Prepares a generated function that matches `call` in `generate_in`
+    /// (or as close to `call` as possible, if `generate_in` is `None`)
+    fn from_call(
+        ctx: &AssistCtx,
+        call: &ast::CallExpr,
+        path: &ast::Path,
+        target_module: Option<hir::InFile<hir::ModuleSource>>,
+    ) -> Option<Self> {
+        let needs_pub = target_module.is_some();
+        let mut file = AssistFile::default();
+        let target = if let Some(target_module) = target_module {
+            let (in_file, target) = next_space_for_fn_in_module(ctx.sema.db, target_module)?;
+            file = in_file;
+            target
+        } else {
+            next_space_for_fn_after_call_site(&call)?
+        };
+        let fn_name = fn_name(&path)?;
         let (type_params, params) = fn_args(ctx, &call)?;
-        Some(Self { append_fn_at, fn_name, type_params, params })
+        Some(Self { target, fn_name, type_params, params, file, needs_pub })
     }
+
     fn render(self) -> Option<FunctionTemplate> {
         let placeholder_expr = ast::make::expr_todo();
         let fn_body = ast::make::block_expr(vec![], Some(placeholder_expr));
-        let fn_def = ast::make::fn_def(self.fn_name, self.type_params, self.params, fn_body);
-        let fn_def = ast::make::add_newlines(2, fn_def);
-        let fn_def = IndentLevel::from_node(&self.append_fn_at).increase_indent(fn_def);
-        let insert_offset = self.append_fn_at.text_range().end();
+        let mut fn_def = ast::make::fn_def(self.fn_name, self.type_params, self.params, fn_body);
+        if self.needs_pub {
+            fn_def = ast::make::add_pub_crate_modifier(fn_def);
+        }
+
+        let (fn_def, insert_offset) = match self.target {
+            GeneratedFunctionTarget::BehindItem(it) => {
+                let with_leading_blank_line = ast::make::add_leading_newlines(2, fn_def);
+                let indented = IndentLevel::from_node(&it).increase_indent(with_leading_blank_line);
+                (indented, it.text_range().end())
+            }
+            GeneratedFunctionTarget::InEmptyItemList(it) => {
+                let indent_once = IndentLevel(1);
+                let indent = IndentLevel::from_node(it.syntax());
+
+                let fn_def = ast::make::add_leading_newlines(1, fn_def);
+                let fn_def = indent_once.increase_indent(fn_def);
+                let fn_def = ast::make::add_trailing_newlines(1, fn_def);
+                let fn_def = indent.increase_indent(fn_def);
+                (fn_def, it.syntax().text_range().start() + TextUnit::from_usize(1))
+            }
+        };
+
         let cursor_offset_from_fn_start = fn_def
             .syntax()
             .descendants()
@@ -94,19 +141,24 @@ impl FunctionBuilder {
             .text_range()
             .start();
         let cursor_offset = insert_offset + cursor_offset_from_fn_start;
-        Some(FunctionTemplate { insert_offset, cursor_offset, fn_def })
+        Some(FunctionTemplate { insert_offset, cursor_offset, fn_def, file: self.file })
     }
 }
 
-fn fn_name(call: &CallExpr) -> Option<ast::Name> {
-    let name = call.expr()?.syntax().to_string();
+enum GeneratedFunctionTarget {
+    BehindItem(SyntaxNode),
+    InEmptyItemList(ast::ItemList),
+}
+
+fn fn_name(call: &ast::Path) -> Option<ast::Name> {
+    let name = call.segment()?.syntax().to_string();
     Some(ast::make::name(&name))
 }
 
 /// Computes the type variables and arguments required for the generated function
 fn fn_args(
     ctx: &AssistCtx,
-    call: &CallExpr,
+    call: &ast::CallExpr,
 ) -> Option<(Option<ast::TypeParamList>, ast::ParamList)> {
     let mut arg_names = Vec::new();
     let mut arg_types = Vec::new();
@@ -158,9 +210,9 @@ fn deduplicate_arg_names(arg_names: &mut Vec<String>) {
     }
 }
 
-fn fn_arg_name(fn_arg: &Expr) -> Option<String> {
+fn fn_arg_name(fn_arg: &ast::Expr) -> Option<String> {
     match fn_arg {
-        Expr::CastExpr(cast_expr) => fn_arg_name(&cast_expr.expr()?),
+        ast::Expr::CastExpr(cast_expr) => fn_arg_name(&cast_expr.expr()?),
         _ => Some(
             fn_arg
                 .syntax()
@@ -172,7 +224,7 @@ fn fn_arg_name(fn_arg: &Expr) -> Option<String> {
     }
 }
 
-fn fn_arg_type(ctx: &AssistCtx, fn_arg: &Expr) -> Option<String> {
+fn fn_arg_type(ctx: &AssistCtx, fn_arg: &ast::Expr) -> Option<String> {
     let ty = ctx.sema.type_of_expr(fn_arg)?;
     if ty.is_unknown() {
         return None;
@@ -184,7 +236,7 @@ fn fn_arg_type(ctx: &AssistCtx, fn_arg: &Expr) -> Option<String> {
 /// directly after the current block
 /// We want to write the generated function directly after
 /// fns, impls or macro calls, but inside mods
-fn next_space_for_fn(expr: &CallExpr) -> Option<SyntaxNode> {
+fn next_space_for_fn_after_call_site(expr: &ast::CallExpr) -> Option<GeneratedFunctionTarget> {
     let mut ancestors = expr.syntax().ancestors().peekable();
     let mut last_ancestor: Option<SyntaxNode> = None;
     while let Some(next_ancestor) = ancestors.next() {
@@ -201,7 +253,32 @@ fn next_space_for_fn(expr: &CallExpr) -> Option<SyntaxNode> {
         }
         last_ancestor = Some(next_ancestor);
     }
-    last_ancestor
+    last_ancestor.map(GeneratedFunctionTarget::BehindItem)
+}
+
+fn next_space_for_fn_in_module(
+    db: &dyn hir::db::AstDatabase,
+    module: hir::InFile<hir::ModuleSource>,
+) -> Option<(AssistFile, GeneratedFunctionTarget)> {
+    let file = module.file_id.original_file(db);
+    let assist_file = AssistFile::TargetFile(file);
+    let assist_item = match module.value {
+        hir::ModuleSource::SourceFile(it) => {
+            if let Some(last_item) = it.items().last() {
+                GeneratedFunctionTarget::BehindItem(last_item.syntax().clone())
+            } else {
+                GeneratedFunctionTarget::BehindItem(it.syntax().clone())
+            }
+        }
+        hir::ModuleSource::Module(it) => {
+            if let Some(last_item) = it.item_list().and_then(|it| it.items().last()) {
+                GeneratedFunctionTarget::BehindItem(last_item.syntax().clone())
+            } else {
+                GeneratedFunctionTarget::InEmptyItemList(it.item_list()?)
+            }
+        }
+    };
+    Some((assist_file, assist_item))
 }
 
 #[cfg(test)]
@@ -713,6 +790,111 @@ fn bar(baz_1: Baz, baz_2: Baz, arg_1: &str, arg_2: &str) {
         )
     }
 
+    #[test]
+    fn add_function_in_module() {
+        check_assist(
+            add_function,
+            r"
+mod bar {}
+
+fn foo() {
+    bar::my_fn<|>()
+}
+",
+            r"
+mod bar {
+    pub(crate) fn my_fn() {
+        <|>todo!()
+    }
+}
+
+fn foo() {
+    bar::my_fn()
+}
+",
+        )
+    }
+
+    #[test]
+    fn add_function_in_module_containing_other_items() {
+        check_assist(
+            add_function,
+            r"
+mod bar {
+    fn something_else() {}
+}
+
+fn foo() {
+    bar::my_fn<|>()
+}
+",
+            r"
+mod bar {
+    fn something_else() {}
+
+    pub(crate) fn my_fn() {
+        <|>todo!()
+    }
+}
+
+fn foo() {
+    bar::my_fn()
+}
+",
+        )
+    }
+
+    #[test]
+    fn add_function_in_nested_module() {
+        check_assist(
+            add_function,
+            r"
+mod bar {
+    mod baz {}
+}
+
+fn foo() {
+    bar::baz::my_fn<|>()
+}
+",
+            r"
+mod bar {
+    mod baz {
+        pub(crate) fn my_fn() {
+            <|>todo!()
+        }
+    }
+}
+
+fn foo() {
+    bar::baz::my_fn()
+}
+",
+        )
+    }
+
+    #[test]
+    fn add_function_in_another_file() {
+        check_assist(
+            add_function,
+            r"
+//- /main.rs
+mod foo;
+
+fn main() {
+    foo::bar<|>()
+}
+//- /foo.rs
+",
+            r"
+
+
+pub(crate) fn bar() {
+    <|>todo!()
+}",
+        )
+    }
+
     #[test]
     fn add_function_not_applicable_if_function_already_exists() {
         check_assist_not_applicable(
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs
index a00136da1c..ccc95735f7 100644
--- a/crates/ra_assists/src/lib.rs
+++ b/crates/ra_assists/src/lib.rs
@@ -17,7 +17,7 @@ mod doc_tests;
 pub mod utils;
 pub mod ast_transform;
 
-use ra_db::FileRange;
+use ra_db::{FileId, FileRange};
 use ra_ide_db::RootDatabase;
 use ra_syntax::{TextRange, TextUnit};
 use ra_text_edit::TextEdit;
@@ -54,6 +54,7 @@ pub struct AssistAction {
     pub cursor_position: Option<TextUnit>,
     // FIXME: This belongs to `AssistLabel`
     pub target: Option<TextRange>,
+    pub file: AssistFile,
 }
 
 #[derive(Debug, Clone)]
@@ -63,6 +64,18 @@ pub struct ResolvedAssist {
     pub action: AssistAction,
 }
 
+#[derive(Debug, Clone, Copy)]
+pub enum AssistFile {
+    CurrentFile,
+    TargetFile(FileId),
+}
+
+impl Default for AssistFile {
+    fn default() -> Self {
+        Self::CurrentFile
+    }
+}
+
 /// Return all the assists applicable at the given position.
 ///
 /// Assists are returned in the "unresolved" state, that is only labels are
@@ -184,7 +197,7 @@ mod helpers {
     use ra_ide_db::{symbol_index::SymbolsDatabase, RootDatabase};
     use test_utils::{add_cursor, assert_eq_text, extract_range_or_offset, RangeOrOffset};
 
-    use crate::{AssistCtx, AssistHandler};
+    use crate::{AssistCtx, AssistFile, AssistHandler};
     use hir::Semantics;
 
     pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) {
@@ -246,7 +259,13 @@ mod helpers {
             (Some(assist), ExpectedResult::After(after)) => {
                 let action = assist.0[0].action.clone().unwrap();
 
-                let mut actual = action.edit.apply(&text_without_caret);
+                let assisted_file_text = if let AssistFile::TargetFile(file_id) = action.file {
+                    db.file_text(file_id).as_ref().to_owned()
+                } else {
+                    text_without_caret
+                };
+
+                let mut actual = action.edit.apply(&assisted_file_text);
                 match action.cursor_position {
                     None => {
                         if let RangeOrOffset::Offset(before_cursor_pos) = range_or_offset {
diff --git a/crates/ra_ide/src/assists.rs b/crates/ra_ide/src/assists.rs
index 40d56a4f7b..2b5d11681a 100644
--- a/crates/ra_ide/src/assists.rs
+++ b/crates/ra_ide/src/assists.rs
@@ -37,6 +37,10 @@ fn action_to_edit(
     file_id: FileId,
     assist_label: &AssistLabel,
 ) -> SourceChange {
+    let file_id = match action.file {
+        ra_assists::AssistFile::TargetFile(it) => it,
+        _ => file_id,
+    };
     let file_edit = SourceFileEdit { file_id, edit: action.edit };
     SourceChange::source_file_edit(assist_label.label.clone(), file_edit)
         .with_cursor_opt(action.cursor_position.map(|offset| FilePosition { offset, file_id }))
diff --git a/crates/ra_syntax/src/ast/make.rs b/crates/ra_syntax/src/ast/make.rs
index 0f4a50be47..ee0f5cc406 100644
--- a/crates/ra_syntax/src/ast/make.rs
+++ b/crates/ra_syntax/src/ast/make.rs
@@ -293,11 +293,20 @@ pub fn fn_def(
     ast_from_text(&format!("fn {}{}{} {}", fn_name, type_params, params, body))
 }
 
-pub fn add_newlines(amount_of_newlines: usize, t: impl AstNode) -> ast::SourceFile {
+pub fn add_leading_newlines(amount_of_newlines: usize, t: impl AstNode) -> ast::SourceFile {
     let newlines = "\n".repeat(amount_of_newlines);
     ast_from_text(&format!("{}{}", newlines, t.syntax()))
 }
 
+pub fn add_trailing_newlines(amount_of_newlines: usize, t: impl AstNode) -> ast::SourceFile {
+    let newlines = "\n".repeat(amount_of_newlines);
+    ast_from_text(&format!("{}{}", t.syntax(), newlines))
+}
+
+pub fn add_pub_crate_modifier(fn_def: ast::FnDef) -> ast::FnDef {
+    ast_from_text(&format!("pub(crate) {}", fn_def))
+}
+
 fn ast_from_text<N: AstNode>(text: &str) -> N {
     let parse = SourceFile::parse(text);
     let node = parse.tree().syntax().descendants().find_map(N::cast).unwrap();
diff --git a/docs/user/assists.md b/docs/user/assists.md
index 6483ba4f3e..6c69436223 100644
--- a/docs/user/assists.md
+++ b/docs/user/assists.md
@@ -65,7 +65,7 @@ Adds a stub function with a signature matching the function under the cursor.
 struct Baz;
 fn baz() -> Baz { Baz }
 fn foo() {
-     bar┃("", baz());
+    bar┃("", baz());
 }
 
 
@@ -73,7 +73,7 @@ fn foo() {
 struct Baz;
 fn baz() -> Baz { Baz }
 fn foo() {
-     bar("", baz());
+    bar("", baz());
 }
 
 fn bar(arg: &str, baz: Baz) {
diff --git a/editors/code/src/source_change.ts b/editors/code/src/source_change.ts
index 399a150c65..af8f1df511 100644
--- a/editors/code/src/source_change.ts
+++ b/editors/code/src/source_change.ts
@@ -37,11 +37,13 @@ export async function applySourceChange(ctx: Ctx, change: ra.SourceChange) {
             toReveal.position,
         );
         const editor = vscode.window.activeTextEditor;
-        if (!editor || editor.document.uri.toString() !== uri.toString()) {
+        if (!editor || !editor.selection.isEmpty) {
             return;
         }
-        if (!editor.selection.isEmpty) {
-            return;
+
+        if (editor.document.uri !== uri) {
+            const doc = await vscode.workspace.openTextDocument(uri);
+            await vscode.window.showTextDocument(doc);
         }
         editor.selection = new vscode.Selection(position, position);
         editor.revealRange(


Test patch:
diff --git a/crates/ra_assists/src/doc_tests/generated.rs b/crates/ra_assists/src/doc_tests/generated.rs
index b39e60870e..e4fa9ee366 100644
--- a/crates/ra_assists/src/doc_tests/generated.rs
+++ b/crates/ra_assists/src/doc_tests/generated.rs
@@ -66,7 +66,7 @@ fn doctest_add_function() {
 struct Baz;
 fn baz() -> Baz { Baz }
 fn foo() {
-     bar<|>("", baz());
+    bar<|>("", baz());
 }
 
 "#####,
@@ -74,7 +74,7 @@ fn foo() {
 struct Baz;
 fn baz() -> Baz { Baz }
 fn foo() {
-     bar("", baz());
+    bar("", baz());
 }
 
 fn bar(arg: &str, baz: Baz) {