Skip to content

Commit f690e59

Browse files
committed
Assist to convert nested function to closure.
1 parent b6e3f41 commit f690e59

File tree

3 files changed

+174
-0
lines changed

3 files changed

+174
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use hir::Semantics;
2+
use ide_db::assists::{AssistId, AssistKind};
3+
use ide_db::base_db::FileId;
4+
use ide_db::RootDatabase;
5+
use syntax::ast::{self, HasName};
6+
use syntax::{AstNode, SyntaxKind};
7+
8+
use crate::assist_context::{AssistContext, Assists};
9+
10+
// Assist: convert_nested_function_to_closure
11+
//
12+
// Converts a function that is defined within the body of another function into a closure.
13+
//
14+
// ```
15+
// fn main() {
16+
// fn foo(label: &str, $0number: u64) {
17+
// println!("{}: {}", label, number);
18+
// }
19+
//
20+
// foo("Bar", 100);
21+
// }
22+
// ```
23+
// ->
24+
// ```
25+
// fn main() {
26+
// let foo = |label: &str, number: u64| {
27+
// println!("{}: {}", label, number);
28+
// };
29+
//
30+
// foo("Bar", 100);
31+
// }
32+
// ```
33+
pub(crate) fn convert_nested_function_to_closure(
34+
acc: &mut Assists,
35+
ctx: &AssistContext<'_>,
36+
) -> Option<()> {
37+
let function = ctx.find_node_at_offset::<ast::Fn>()?;
38+
if !is_nested_function(&function) {
39+
return None;
40+
}
41+
42+
let target = function.syntax().text_range();
43+
let body = function.body()?;
44+
let name = function.name()?;
45+
let params = function.param_list()?;
46+
let has_semicolon = has_semicolon(&function, ctx.file_id(), &ctx.sema);
47+
48+
acc.add(
49+
AssistId("convert_nested_function_to_closure", AssistKind::RefactorRewrite),
50+
"Convert nested function to closure",
51+
target,
52+
|edit| {
53+
let params_text = params.syntax().text().to_string();
54+
let params_text_trimmed =
55+
params_text.strip_prefix("(").and_then(|p| p.strip_suffix(")"));
56+
57+
if let Some(closure_params) = params_text_trimmed {
58+
let body = body.to_string();
59+
let body = if has_semicolon { body } else { format!("{};", body) };
60+
edit.replace(target, format!("let {} = |{}| {}", name, closure_params, body));
61+
}
62+
},
63+
)
64+
}
65+
66+
/// Returns whether the given function is nested within the body of another function.
67+
fn is_nested_function(function: &ast::Fn) -> bool {
68+
function
69+
.syntax()
70+
.parent()
71+
.map(|p| p.ancestors().any(|a| a.kind() == SyntaxKind::FN))
72+
.unwrap_or(false)
73+
}
74+
75+
/// Returns whether the given nested function has a trailing semicolon.
76+
fn has_semicolon(function: &ast::Fn, file_id: FileId, sema: &Semantics<'_, RootDatabase>) -> bool {
77+
let source = sema.parse(file_id).syntax().text();
78+
let offset = function.syntax().text_range().end();
79+
source.char_at(offset) == Some(';')
80+
}
81+
82+
#[cfg(test)]
83+
mod tests {
84+
use crate::tests::{check_assist, check_assist_not_applicable};
85+
86+
use super::convert_nested_function_to_closure;
87+
88+
#[test]
89+
fn convert_nested_function_to_closure_works() {
90+
check_assist(
91+
convert_nested_function_to_closure,
92+
r#"
93+
fn main() {
94+
$0fn foo(a: u64, b: u64) -> u64 {
95+
2 * (a + b)
96+
}
97+
98+
_ = foo(3, 4);
99+
}
100+
"#,
101+
r#"
102+
fn main() {
103+
let foo = |a: u64, b: u64| {
104+
2 * (a + b)
105+
};
106+
107+
_ = foo(3, 4);
108+
}
109+
"#,
110+
);
111+
}
112+
113+
#[test]
114+
fn convert_nested_function_to_closure_works_with_existing_semicolon() {
115+
check_assist(
116+
convert_nested_function_to_closure,
117+
r#"
118+
fn main() {
119+
$0fn foo(a: u64, b: u64) -> u64 {
120+
2 * (a + b)
121+
};
122+
123+
_ = foo(3, 4);
124+
}
125+
"#,
126+
r#"
127+
fn main() {
128+
let foo = |a: u64, b: u64| {
129+
2 * (a + b)
130+
};
131+
132+
_ = foo(3, 4);
133+
}
134+
"#,
135+
);
136+
}
137+
138+
#[test]
139+
fn convert_nested_function_to_closure_does_not_work_on_top_level_function() {
140+
check_assist_not_applicable(
141+
convert_nested_function_to_closure,
142+
r#"
143+
fn ma$0in() {}
144+
"#,
145+
);
146+
}
147+
}

crates/ide-assists/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ mod handlers {
120120
mod convert_into_to_from;
121121
mod convert_iter_for_each_to_for;
122122
mod convert_let_else_to_match;
123+
mod convert_nested_function_to_closure;
123124
mod convert_tuple_struct_to_named_struct;
124125
mod convert_to_guarded_return;
125126
mod convert_two_arm_bool_match_to_matches_macro;
@@ -217,6 +218,7 @@ mod handlers {
217218
convert_iter_for_each_to_for::convert_iter_for_each_to_for,
218219
convert_iter_for_each_to_for::convert_for_loop_with_for_each,
219220
convert_let_else_to_match::convert_let_else_to_match,
221+
convert_nested_function_to_closure::convert_nested_function_to_closure,
220222
convert_to_guarded_return::convert_to_guarded_return,
221223
convert_tuple_struct_to_named_struct::convert_tuple_struct_to_named_struct,
222224
convert_two_arm_bool_match_to_matches_macro::convert_two_arm_bool_match_to_matches_macro,

crates/ide-assists/src/tests/generated.rs

+25
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,31 @@ fn main() {
407407
)
408408
}
409409

410+
#[test]
411+
fn doctest_convert_nested_function_to_closure() {
412+
check_doc_test(
413+
"convert_nested_function_to_closure",
414+
r#####"
415+
fn main() {
416+
fn foo(label: &str, $0number: u64) {
417+
println!("{}: {}", label, number);
418+
}
419+
420+
foo("Bar", 100);
421+
}
422+
"#####,
423+
r#####"
424+
fn main() {
425+
let foo = |label: &str, number: u64| {
426+
println!("{}: {}", label, number);
427+
};
428+
429+
foo("Bar", 100);
430+
}
431+
"#####,
432+
)
433+
}
434+
410435
#[test]
411436
fn doctest_convert_to_guarded_return() {
412437
check_doc_test(

0 commit comments

Comments
 (0)