Skip to content

Commit cc516e6

Browse files
committed
WIP: Add create_function assist
1 parent f9cf864 commit cc516e6

File tree

2 files changed

+315
-0
lines changed

2 files changed

+315
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
use ra_syntax::{
2+
ast::{self, AstNode},
3+
SmolStr, SyntaxKind, SyntaxNode, TextUnit,
4+
};
5+
6+
use crate::{Assist, AssistCtx, AssistId};
7+
use ast::{CallExpr, Expr};
8+
use ra_fmt::leading_indent;
9+
10+
// Assist: create_function
11+
//
12+
// Creates a stub function with a signature matching the function under the cursor.
13+
//
14+
// ```
15+
// fn foo() {
16+
// bar<|>("", baz());
17+
// }
18+
//
19+
// ```
20+
// ->
21+
// ```
22+
// fn foo() {
23+
// bar("", baz());
24+
// }
25+
//
26+
// fn bar(arg_1: &str, baz: Baz) {
27+
// todo!();
28+
// }
29+
//
30+
// ```
31+
pub(crate) fn create_function(ctx: AssistCtx) -> Option<Assist> {
32+
let path: ast::Path = ctx.find_node_at_offset()?;
33+
34+
if ctx.sema.resolve_path(&path).is_some() {
35+
// The function call already resolves, no need to create a function
36+
return None;
37+
}
38+
39+
if path.qualifier().is_some() {
40+
return None;
41+
}
42+
43+
let call: ast::CallExpr = ctx.find_node_at_offset()?;
44+
45+
let (generated_fn, cursor_pos, text_start) = generate_fn(&ctx, &call)?;
46+
47+
ctx.add_assist(AssistId("create_function"), "Create function", |edit| {
48+
edit.target(call.syntax().text_range());
49+
50+
edit.set_cursor(cursor_pos);
51+
edit.insert(text_start, generated_fn);
52+
})
53+
}
54+
55+
/// Generates a function definition that will allow `call` to compile.
56+
/// The function name must match.
57+
/// The arguments must have valid, nonconflicting names.
58+
/// The arguments' types must match those being passed to `call`.
59+
/// TODO generics?
60+
fn generate_fn(ctx: &AssistCtx, call: &CallExpr) -> Option<(String, TextUnit, TextUnit)> {
61+
let (start, indent) = next_space_for_fn(&call)?;
62+
let indent = if let Some(i) = &indent { i.as_str() } else { "" };
63+
let mut fn_buf = String::with_capacity(128);
64+
65+
fn_buf.push_str("\n\n");
66+
fn_buf.push_str(indent);
67+
fn_buf.push_str("fn ");
68+
69+
let fn_name = fn_name(&call)?;
70+
fn_buf.push_str(&fn_name);
71+
72+
let fn_generics = fn_generics(&call)?;
73+
fn_buf.push_str(&fn_generics);
74+
75+
let fn_args = fn_args()?;
76+
fn_buf.push_str(&fn_args);
77+
78+
fn_buf.push_str(" {\n");
79+
fn_buf.push_str(indent);
80+
fn_buf.push_str(" ");
81+
82+
// We take the offset here to put the cursor in front of the `todo` body
83+
let offset = TextUnit::of_str(&fn_buf);
84+
85+
fn_buf.push_str("todo!()\n");
86+
fn_buf.push_str(indent);
87+
fn_buf.push_str("}");
88+
89+
let cursor_pos = start + offset;
90+
Some((fn_buf, cursor_pos, start))
91+
}
92+
93+
fn fn_name(call: &CallExpr) -> Option<String> {
94+
Some(call.expr()?.syntax().to_string())
95+
}
96+
97+
fn fn_generics(_call: &CallExpr) -> Option<String> {
98+
// TODO
99+
Some("".into())
100+
}
101+
102+
fn fn_args() -> Option<String> {
103+
Some("()".into())
104+
}
105+
106+
/// Returns the position inside the current mod or file
107+
/// directly after the current block
108+
/// We want to write the generated function directly after
109+
/// fns, impls or macro calls, but inside mods
110+
fn next_space_for_fn(expr: &CallExpr) -> Option<(TextUnit, Option<SmolStr>)> {
111+
let mut ancestors = expr.syntax().ancestors().peekable();
112+
let mut last_ancestor: Option<SyntaxNode> = None;
113+
while let Some(next_ancestor) = ancestors.next() {
114+
match next_ancestor.kind() {
115+
SyntaxKind::SOURCE_FILE => {
116+
break;
117+
}
118+
SyntaxKind::ITEM_LIST => {
119+
if ancestors.peek().map(|a| a.kind()) == Some(SyntaxKind::MODULE) {
120+
break;
121+
}
122+
}
123+
_ => {}
124+
}
125+
last_ancestor = Some(next_ancestor);
126+
}
127+
last_ancestor.map(|a| (a.text_range().end(), leading_indent(&a)))
128+
}
129+
130+
#[cfg(test)]
131+
mod tests {
132+
use crate::helpers::{check_assist, check_assist_not_applicable};
133+
134+
use super::*;
135+
136+
#[test]
137+
fn create_function_with_no_args() {
138+
check_assist(
139+
create_function,
140+
r"
141+
fn foo() {
142+
bar<|>();
143+
}
144+
",
145+
r"
146+
fn foo() {
147+
bar();
148+
}
149+
150+
fn bar() {
151+
<|>todo!()
152+
}
153+
",
154+
)
155+
}
156+
157+
#[test]
158+
fn create_function_from_method() {
159+
// This ensures that the function is correctly generated
160+
// in the next outer mod or file
161+
check_assist(
162+
create_function,
163+
r"
164+
impl Foo {
165+
fn foo() {
166+
bar<|>();
167+
}
168+
}
169+
",
170+
r"
171+
impl Foo {
172+
fn foo() {
173+
bar();
174+
}
175+
}
176+
177+
fn bar() {
178+
<|>todo!()
179+
}
180+
",
181+
)
182+
}
183+
184+
#[test]
185+
fn create_function_directly_after_current_block() {
186+
// The new fn should not be created at the end of the file or module
187+
check_assist(
188+
create_function,
189+
r"
190+
fn foo1() {
191+
bar<|>();
192+
}
193+
194+
fn foo2() {}
195+
",
196+
r"
197+
fn foo1() {
198+
bar();
199+
}
200+
201+
fn bar() {
202+
<|>todo!()
203+
}
204+
205+
fn foo2() {}
206+
",
207+
)
208+
}
209+
210+
#[test]
211+
fn create_function_with_no_args_in_same_module() {
212+
check_assist(
213+
create_function,
214+
r"
215+
mod baz {
216+
fn foo() {
217+
bar<|>();
218+
}
219+
}
220+
",
221+
r"
222+
mod baz {
223+
fn foo() {
224+
bar();
225+
}
226+
227+
fn bar() {
228+
<|>todo!()
229+
}
230+
}
231+
",
232+
)
233+
}
234+
235+
#[test]
236+
fn create_function_with_function_call_arg() {
237+
check_assist(
238+
create_function,
239+
r"
240+
fn baz() -> Baz { todo!() }
241+
fn foo() {
242+
bar<|>(baz());
243+
}
244+
",
245+
r"
246+
fn baz() -> Baz { todo!() }
247+
fn foo() {
248+
bar(baz());
249+
}
250+
251+
fn bar(baz: Baz) {
252+
<|>todo!()
253+
}
254+
",
255+
)
256+
}
257+
258+
#[test]
259+
fn create_function_not_applicable_if_function_already_exists() {
260+
check_assist_not_applicable(
261+
create_function,
262+
r"
263+
fn foo() {
264+
bar<|>();
265+
}
266+
267+
fn bar() {}
268+
",
269+
)
270+
}
271+
272+
#[test]
273+
fn create_function_not_applicable_if_function_path_not_singleton() {
274+
// In the future this assist could be extended to generate functions
275+
// if the path is in the same crate (or even the same workspace).
276+
// For the beginning, I think this is fine.
277+
check_assist_not_applicable(
278+
create_function,
279+
r"
280+
fn foo() {
281+
other_crate::bar<|>();
282+
}
283+
",
284+
)
285+
}
286+
287+
#[test]
288+
#[ignore]
289+
fn create_method_with_no_args() {
290+
check_assist(
291+
create_function,
292+
r"
293+
struct Foo;
294+
impl Foo {
295+
fn foo(&self) {
296+
self.bar()<|>;
297+
}
298+
}
299+
",
300+
r"
301+
struct Foo;
302+
impl Foo {
303+
fn foo(&self) {
304+
self.bar();
305+
}
306+
fn bar(&self) {
307+
todo!();
308+
}
309+
}
310+
",
311+
)
312+
}
313+
}

crates/ra_assists/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ mod handlers {
102102
mod apply_demorgan;
103103
mod auto_import;
104104
mod change_visibility;
105+
mod create_function;
105106
mod early_return;
106107
mod fill_match_arms;
107108
mod flip_binexpr;
@@ -134,6 +135,7 @@ mod handlers {
134135
apply_demorgan::apply_demorgan,
135136
auto_import::auto_import,
136137
change_visibility::change_visibility,
138+
create_function::create_function,
137139
early_return::convert_to_guarded_return,
138140
fill_match_arms::fill_match_arms,
139141
flip_binexpr::flip_binexpr,

0 commit comments

Comments
 (0)