Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 3973d1a

Browse files
mathew-hornerjplatte
authored andcommitted
Add assist to convert nested function to closure
1 parent 42d671f commit 3973d1a

File tree

3 files changed

+213
-1
lines changed

3 files changed

+213
-1
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
use ide_db::assists::{AssistId, AssistKind};
2+
use syntax::ast::{self, HasGenericParams, HasName};
3+
use syntax::{AstNode, SyntaxKind};
4+
5+
use crate::assist_context::{AssistContext, Assists};
6+
7+
// Assist: convert_nested_function_to_closure
8+
//
9+
// Converts a function that is defined within the body of another function into a closure.
10+
//
11+
// ```
12+
// fn main() {
13+
// fn fo$0o(label: &str, number: u64) {
14+
// println!("{}: {}", label, number);
15+
// }
16+
//
17+
// foo("Bar", 100);
18+
// }
19+
// ```
20+
// ->
21+
// ```
22+
// fn main() {
23+
// let foo = |label: &str, number: u64| {
24+
// println!("{}: {}", label, number);
25+
// };
26+
//
27+
// foo("Bar", 100);
28+
// }
29+
// ```
30+
pub(crate) fn convert_nested_function_to_closure(
31+
acc: &mut Assists,
32+
ctx: &AssistContext<'_>,
33+
) -> Option<()> {
34+
let name = ctx.find_node_at_offset::<ast::Name>()?;
35+
let function = name.syntax().parent().and_then(ast::Fn::cast)?;
36+
37+
if !is_nested_function(&function) || is_generic(&function) {
38+
return None;
39+
}
40+
41+
let target = function.syntax().text_range();
42+
let body = function.body()?;
43+
let name = function.name()?;
44+
let params = function.param_list()?;
45+
46+
acc.add(
47+
AssistId("convert_nested_function_to_closure", AssistKind::RefactorRewrite),
48+
"Convert nested function to closure",
49+
target,
50+
|edit| {
51+
let has_semicolon = has_semicolon(&function);
52+
let params_text = params.syntax().text().to_string();
53+
let params_text_trimmed =
54+
params_text.strip_prefix("(").and_then(|p| p.strip_suffix(")"));
55+
56+
if let Some(closure_params) = params_text_trimmed {
57+
let body = body.to_string();
58+
let body = if has_semicolon { body } else { format!("{};", body) };
59+
edit.replace(target, format!("let {} = |{}| {}", name, closure_params, body));
60+
}
61+
},
62+
)
63+
}
64+
65+
/// Returns whether the given function is nested within the body of another function.
66+
fn is_nested_function(function: &ast::Fn) -> bool {
67+
function
68+
.syntax()
69+
.parent()
70+
.map(|p| p.ancestors().any(|a| a.kind() == SyntaxKind::FN))
71+
.unwrap_or(false)
72+
}
73+
74+
/// Returns whether the given nested function has generic parameters.
75+
fn is_generic(function: &ast::Fn) -> bool {
76+
function.generic_param_list().is_some()
77+
}
78+
79+
/// Returns whether the given nested function has a trailing semicolon.
80+
fn has_semicolon(function: &ast::Fn) -> bool {
81+
function
82+
.syntax()
83+
.next_sibling_or_token()
84+
.map(|t| t.kind() == SyntaxKind::SEMICOLON)
85+
.unwrap_or(false)
86+
}
87+
88+
#[cfg(test)]
89+
mod tests {
90+
use crate::tests::{check_assist, check_assist_not_applicable};
91+
92+
use super::convert_nested_function_to_closure;
93+
94+
#[test]
95+
fn convert_nested_function_to_closure_works() {
96+
check_assist(
97+
convert_nested_function_to_closure,
98+
r#"
99+
fn main() {
100+
fn $0foo(a: u64, b: u64) -> u64 {
101+
2 * (a + b)
102+
}
103+
104+
_ = foo(3, 4);
105+
}
106+
"#,
107+
r#"
108+
fn main() {
109+
let foo = |a: u64, b: u64| {
110+
2 * (a + b)
111+
};
112+
113+
_ = foo(3, 4);
114+
}
115+
"#,
116+
);
117+
}
118+
119+
#[test]
120+
fn convert_nested_function_to_closure_works_with_existing_semicolon() {
121+
check_assist(
122+
convert_nested_function_to_closure,
123+
r#"
124+
fn main() {
125+
fn foo$0(a: u64, b: u64) -> u64 {
126+
2 * (a + b)
127+
};
128+
129+
_ = foo(3, 4);
130+
}
131+
"#,
132+
r#"
133+
fn main() {
134+
let foo = |a: u64, b: u64| {
135+
2 * (a + b)
136+
};
137+
138+
_ = foo(3, 4);
139+
}
140+
"#,
141+
);
142+
}
143+
144+
#[test]
145+
fn convert_nested_function_to_closure_does_not_work_on_top_level_function() {
146+
check_assist_not_applicable(
147+
convert_nested_function_to_closure,
148+
r#"
149+
fn ma$0in() {}
150+
"#,
151+
);
152+
}
153+
154+
#[test]
155+
fn convert_nested_function_to_closure_does_not_work_when_cursor_off_name() {
156+
check_assist_not_applicable(
157+
convert_nested_function_to_closure,
158+
r#"
159+
fn main() {
160+
fn foo(a: u64, $0b: u64) -> u64 {
161+
2 * (a + b)
162+
};
163+
164+
_ = foo(3, 4);
165+
}
166+
"#,
167+
);
168+
}
169+
170+
#[test]
171+
fn convert_nested_function_to_closure_does_not_work_if_function_has_generic_params() {
172+
check_assist_not_applicable(
173+
convert_nested_function_to_closure,
174+
r#"
175+
fn main() {
176+
fn fo$0o<S: Into<String>>(s: S) -> String {
177+
s.into()
178+
};
179+
180+
_ = foo("hello");
181+
}
182+
"#,
183+
);
184+
}
185+
}

crates/ide-assists/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ mod handlers {
122122
mod convert_iter_for_each_to_for;
123123
mod convert_let_else_to_match;
124124
mod convert_match_to_let_else;
125+
mod convert_nested_function_to_closure;
125126
mod convert_tuple_struct_to_named_struct;
126127
mod convert_named_struct_to_tuple_struct;
127128
mod convert_to_guarded_return;
@@ -228,8 +229,9 @@ mod handlers {
228229
convert_iter_for_each_to_for::convert_iter_for_each_to_for,
229230
convert_iter_for_each_to_for::convert_for_loop_with_for_each,
230231
convert_let_else_to_match::convert_let_else_to_match,
231-
convert_named_struct_to_tuple_struct::convert_named_struct_to_tuple_struct,
232232
convert_match_to_let_else::convert_match_to_let_else,
233+
convert_named_struct_to_tuple_struct::convert_named_struct_to_tuple_struct,
234+
convert_nested_function_to_closure::convert_nested_function_to_closure,
233235
convert_to_guarded_return::convert_to_guarded_return,
234236
convert_tuple_struct_to_named_struct::convert_tuple_struct_to_named_struct,
235237
convert_two_arm_bool_match_to_matches_macro::convert_two_arm_bool_match_to_matches_macro,

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,31 @@ impl Point {
494494
)
495495
}
496496

497+
#[test]
498+
fn doctest_convert_nested_function_to_closure() {
499+
check_doc_test(
500+
"convert_nested_function_to_closure",
501+
r#####"
502+
fn main() {
503+
fn fo$0o(label: &str, number: u64) {
504+
println!("{}: {}", label, number);
505+
}
506+
507+
foo("Bar", 100);
508+
}
509+
"#####,
510+
r#####"
511+
fn main() {
512+
let foo = |label: &str, number: u64| {
513+
println!("{}: {}", label, number);
514+
};
515+
516+
foo("Bar", 100);
517+
}
518+
"#####,
519+
)
520+
}
521+
497522
#[test]
498523
fn doctest_convert_to_guarded_return() {
499524
check_doc_test(

0 commit comments

Comments
 (0)