diff --git a/crates/ide-assists/src/handlers/inline_as_closure.rs b/crates/ide-assists/src/handlers/inline_as_closure.rs new file mode 100644 index 000000000000..d7e6a2a30525 --- /dev/null +++ b/crates/ide-assists/src/handlers/inline_as_closure.rs @@ -0,0 +1,398 @@ +use ast::make; +use hir::{EditionedFileId, PathResolution, Semantics}; +use ide_db::{ + RootDatabase, + defs::Definition, + path_transform::PathTransform, + search::{FileReference, FileReferenceNode}, +}; +use syntax::{ + AstNode, + ast::{self, HasGenericArgs, edit::IndentLevel, edit_in_place::Indent}, + ted, +}; + +use crate::{ + AssistId, + assist_context::{AssistContext, Assists}, +}; + +// Assist: inline_as_closure +// +// Inline non calling function as closure. +// +// ``` +// fn foo() { println!("Hello, World!"); } +// fn main() { +// let _ = foo$0; +// } +// ``` +// -> +// ``` +// fn foo() { println!("Hello, World!"); } +// fn main() { +// let _ = || { println!("Hello, World!"); }; +// } +// ``` +pub(crate) fn inline_as_closure(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + let path: ast::PathExpr = ctx.find_node_at_offset()?; + let indent_level = path.indent_level(); + + if ast::CallExpr::can_cast(path.syntax().parent()?.kind()) { + cov_mark::hit!(inline_as_closure_call_expr); + return None; + } + + let sema = &ctx.sema; + let PathResolution::Def(hir::ModuleDef::Function(func)) = sema.resolve_path(&path.path()?)? + else { + return None; + }; + + let source = sema.source(func)?; + let param_list = source.value.param_list()?; + let body = source.value.body()?.clone_for_update(); + + rename_usages(func, &body, &path, ctx.file_id(), sema)?; + + let target = path.syntax().text_range(); + acc.add( + AssistId::refactor_inline("inline_as_closure"), + format!("Inline `{path}` to closure"), + target, + |builder| { + let mut edit = builder.make_editor(path.syntax()); + + let param = process_params(param_list); + + let closure = make::expr_closure(param, process_body(body, indent_level)); + edit.replace(path.syntax(), closure.syntax().clone_for_update()); + + builder.add_file_edits(ctx.vfs_file_id(), edit); + }, + ) +} + +fn process_params(param_list: ast::ParamList) -> impl Iterator { + let this = make::untyped_param(make::path_pat(make::path_from_text("this"))); + let wildcard = make::wildcard_pat(); + + param_list.self_param().map(|_| this).into_iter().chain( + param_list + .params() + .map(move |param| param.pat().unwrap_or_else(|| wildcard.clone().into())) + .map(make::untyped_param), + ) +} + +fn rename_usages( + func: hir::Function, + body: &ast::BlockExpr, + path: &ast::PathExpr, + file_id: EditionedFileId, + sema: &Semantics<'_, RootDatabase>, +) -> Option<()> { + if func.self_param(sema.db).is_some() { + let self_param = func.assoc_fn_params(sema.db)[0].as_local(sema.db)?; + let this = make::name_ref("this"); + + Definition::Local(self_param) + .usages(sema) + .all() + .references + .remove(&file_id) + .unwrap_or_default() + .into_iter() + .filter_map(|FileReference { name, range, .. }| match name { + FileReferenceNode::NameRef(_) => Some(body.syntax().covering_element(range)), + _ => None, + }) + .for_each(|usage| ted::replace(usage, this.syntax().clone_for_update())); + } + + if let Some(generic_arg_list) = path.path()?.segment()?.generic_arg_list() { + if let Some((target, source)) = sema.scope(path.syntax()).zip(sema.scope(body.syntax())) { + PathTransform::function_call(&target, &source, func, generic_arg_list) + .apply(body.syntax()); + } + } + + Some(()) +} + +fn process_body(body: ast::BlockExpr, indent_level: IndentLevel) -> ast::Expr { + match body.tail_expr() { + Some(tail_expr) if body.statements().next().is_none() => { + tail_expr.reindent_to(indent_level); + tail_expr + } + _ => { + body.reindent_to(indent_level); + body.into() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{check_assist, check_assist_not_applicable}; + + #[test] + fn inline_as_closure_basic() { + check_assist( + inline_as_closure, + r#" +fn foo() { println!("Hello, World!"); } +fn main() { + let _ = foo$0; +} + "#, + r#" +fn foo() { println!("Hello, World!"); } +fn main() { + let _ = || { println!("Hello, World!"); }; +} + "#, + ); + } + + #[test] + fn inline_as_closure_tail_expr() { + check_assist( + inline_as_closure, + r#" +fn foo() -> i32 { 2 } +fn main() { + let _ = foo$0; +} + "#, + r#" +fn foo() -> i32 { 2 } +fn main() { + let _ = || 2; +} + "#, + ); + } + + #[test] + fn inline_as_closure_params() { + check_assist( + inline_as_closure, + r#" +fn foo(x: i32) -> i32 { x+2 } +fn main() { + let _ = foo$0; +} + "#, + r#" +fn foo(x: i32) -> i32 { x+2 } +fn main() { + let _ = |x| x+2; +} + "#, + ); + } + + #[test] + fn inline_as_closure_self_params() { + check_assist( + inline_as_closure, + r#" +struct Foo(i32); +impl Foo { + fn foo(&self, x: i32) -> i32 { x+self.0 } +} +fn main() { + let _ = Foo::foo$0; +} + "#, + r#" +struct Foo(i32); +impl Foo { + fn foo(&self, x: i32) -> i32 { x+self.0 } +} +fn main() { + let _ = |this, x| x+this.0; +} + "#, + ); + } + + #[test] + fn inline_as_closure_with_indent() { + check_assist( + inline_as_closure, + r#" +fn foo() -> i32 { + println!("foo"); + 2 +} +fn main() { + { + let _ = foo$0; + } +} + "#, + r#" +fn foo() -> i32 { + println!("foo"); + 2 +} +fn main() { + { + let _ = || { + println!("foo"); + 2 + }; + } +} + "#, + ); + + check_assist( + inline_as_closure, + r#" +mod a { + pub mod b { + pub fn foo() -> i32 { + println!("foo"); + 2 + } + } +} +use a::b::foo; +fn main() { + { + let _ = foo$0; + } +} + "#, + r#" +mod a { + pub mod b { + pub fn foo() -> i32 { + println!("foo"); + 2 + } + } +} +use a::b::foo; +fn main() { + { + let _ = || { + println!("foo"); + 2 + }; + } +} + "#, + ); + + check_assist( + inline_as_closure, + r#" +mod a { + pub mod b { + pub fn foo() -> i32 { + { + 2 + } + } + } +} +fn main() { + { + let _ = a::b::foo$0; + } +} + "#, + r#" +mod a { + pub mod b { + pub fn foo() -> i32 { + { + 2 + } + } + } +} +fn main() { + { + let _ = || { + 2 + }; + } +} + "#, + ); + } + + #[test] + fn inline_as_closure_generic_args() { + check_assist( + inline_as_closure, + r#" +fn foo() -> i32 { N } +fn main() { + let _ = foo$0; +} + "#, + r#" +fn foo() -> i32 { N } +fn main() { + let _ = || N; +} + "#, + ); + + check_assist( + inline_as_closure, + r#" +fn foo() -> usize { N } +fn main() { + let _ = foo::<2>$0; +} + "#, + r#" +fn foo() -> usize { N } +fn main() { + let _ = || 2; +} + "#, + ); + + check_assist( + inline_as_closure, + r#" +fn bar() -> usize { N } +fn foo() -> usize { bar::() } +fn main() { + let _ = foo::$0; +} + "#, + r#" +fn bar() -> usize { N } +fn foo() -> usize { bar::() } +fn main() { + let _ = || bar::(); +} + "#, + ); + } + + #[test] + fn inline_as_closure_not_applicate_call() { + cov_mark::check!(inline_as_closure_call_expr); + check_assist_not_applicable( + inline_as_closure, + r#" +fn foo() -> i32 { 2 } +fn main() { + let _ = foo$0(); +} + "#, + ); + } +} diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index c2604432032d..e7cb588b04d5 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -173,6 +173,7 @@ mod handlers { mod generate_mut_trait_impl; mod generate_new; mod generate_trait_from_impl; + mod inline_as_closure; mod inline_call; mod inline_const_as_literal; mod inline_local_variable; @@ -312,6 +313,7 @@ mod handlers { inline_macro::inline_macro, inline_type_alias::inline_type_alias_uses, inline_type_alias::inline_type_alias, + inline_as_closure::inline_as_closure, into_to_qualified_from::into_to_qualified_from, introduce_named_lifetime::introduce_named_lifetime, introduce_named_type_parameter::introduce_named_type_parameter, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 72f7195cbd77..8019b6995ce8 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -2069,6 +2069,25 @@ impl ${1:_} for Ctx {$0} ) } +#[test] +fn doctest_inline_as_closure() { + check_doc_test( + "inline_as_closure", + r#####" +fn foo() { println!("Hello, World!"); } +fn main() { + let _ = foo$0; +} +"#####, + r#####" +fn foo() { println!("Hello, World!"); } +fn main() { + let _ = || { println!("Hello, World!"); }; +} +"#####, + ) +} + #[test] fn doctest_inline_call() { check_doc_test( diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs index 955aadaa25d3..663d5a49a20b 100644 --- a/crates/syntax/src/ast/make.rs +++ b/crates/syntax/src/ast/make.rs @@ -985,6 +985,10 @@ pub fn unnamed_param(ty: ast::Type) -> ast::Param { ast_from_text(&format!("fn f({ty}) {{ }}")) } +pub fn untyped_param(pat: ast::Pat) -> ast::Param { + ast_from_text(&format!("fn f({pat}) {{ }}")) +} + pub fn param(pat: ast::Pat, ty: ast::Type) -> ast::Param { ast_from_text(&format!("fn f({pat}: {ty}) {{ }}")) }