diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs index c3e65329999c..279163257654 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, target: Option, + 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/doc_tests/generated.rs b/crates/ra_assists/src/doc_tests/generated.rs index b39e60870e5d..e4fa9ee366e4 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) { diff --git a/crates/ra_assists/src/handlers/add_function.rs b/crates/ra_assists/src/handlers/add_function.rs index ad4ab66edef0..f185cffdb7c3 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 { 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, params: ast::ParamList, + file: AssistFile, + needs_pub: bool, } impl FunctionBuilder { - fn from_call(ctx: &AssistCtx, call: &ast::CallExpr) -> Option { - 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>, + ) -> Option { + 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 { 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 { - let name = call.expr()?.syntax().to_string(); +enum GeneratedFunctionTarget { + BehindItem(SyntaxNode), + InEmptyItemList(ast::ItemList), +} + +fn fn_name(call: &ast::Path) -> Option { + 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::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) { } } -fn fn_arg_name(fn_arg: &Expr) -> Option { +fn fn_arg_name(fn_arg: &ast::Expr) -> Option { 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 { } } -fn fn_arg_type(ctx: &AssistCtx, fn_arg: &Expr) -> Option { +fn fn_arg_type(ctx: &AssistCtx, fn_arg: &ast::Expr) -> Option { 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 { /// 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 { +fn next_space_for_fn_after_call_site(expr: &ast::CallExpr) -> Option { let mut ancestors = expr.syntax().ancestors().peekable(); let mut last_ancestor: Option = None; while let Some(next_ancestor) = ancestors.next() { @@ -201,7 +253,32 @@ fn next_space_for_fn(expr: &CallExpr) -> Option { } 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, +) -> 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 a00136da1c14..ccc95735f789 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, // FIXME: This belongs to `AssistLabel` pub target: Option, + 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 40d56a4f7b53..2b5d11681af7 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 0f4a50be4764..ee0f5cc406a4 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(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 6483ba4f3e35..6c6943622301 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 399a150c6544..af8f1df51126 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(