Skip to content

Commit 3ce6e67

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

File tree

3 files changed

+390
-0
lines changed

3 files changed

+390
-0
lines changed
Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
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 hir::ModuleDef;
9+
use ra_fmt::leading_indent;
10+
11+
// Assist: add_function
12+
//
13+
// Adds a stub function with a signature matching the function under the cursor.
14+
//
15+
// ```
16+
// fn foo() {
17+
// bar<|>("", baz());
18+
// }
19+
//
20+
// ```
21+
// ->
22+
// ```
23+
// fn foo() {
24+
// bar("", baz());
25+
// }
26+
//
27+
// fn bar(arg_1: &str, baz: Baz) {
28+
// todo!();
29+
// }
30+
//
31+
// ```
32+
pub(crate) fn add_function(ctx: AssistCtx) -> Option<Assist> {
33+
let path: ast::Path = ctx.find_node_at_offset()?;
34+
35+
if ctx.sema.resolve_path(&path).is_some() {
36+
// The function call already resolves, no need to add a function
37+
return None;
38+
}
39+
40+
if path.qualifier().is_some() {
41+
return None;
42+
}
43+
44+
let call: ast::CallExpr = ctx.find_node_at_offset()?;
45+
46+
if unresolved_path_is_not_call_expr(&path, &call)? {
47+
return None;
48+
}
49+
50+
let function_builder = FunctionBuilder::from_call(&ctx, &call)?;
51+
52+
ctx.add_assist(AssistId("add_function"), "Add function", |edit| {
53+
edit.target(call.syntax().text_range());
54+
55+
let function_template = function_builder.render();
56+
edit.set_cursor(function_template.cursor_offset);
57+
edit.insert(function_template.insert_offset, function_template.fn_text);
58+
})
59+
}
60+
61+
/// Checks whether the path of the called function in `call_expr` equals `path`.
62+
///
63+
/// If it doesn't, the cursor is on an unresolved path of a non-function item in
64+
/// `call_expr` and we don't want to generate a function with that item's name.
65+
fn unresolved_path_is_not_call_expr(path: &ast::Path, call_expr: &ast::CallExpr) -> Option<bool> {
66+
let call_path = call_expr.syntax().descendants().find_map(ast::Path::cast)?;
67+
Some(call_path != *path)
68+
}
69+
70+
struct FunctionTemplate {
71+
insert_offset: TextUnit,
72+
cursor_offset: TextUnit,
73+
fn_text: String,
74+
}
75+
76+
struct FunctionBuilder {
77+
start_offset: TextUnit,
78+
fn_name: String,
79+
fn_generics: String,
80+
fn_args: String,
81+
indent: String,
82+
}
83+
84+
impl FunctionBuilder {
85+
fn from_call(ctx: &AssistCtx, call: &ast::CallExpr) -> Option<Self> {
86+
let (start, indent) = next_space_for_fn(&call)?;
87+
let fn_name = fn_name(&call)?;
88+
let fn_generics = fn_generics(&call)?;
89+
let fn_args = fn_args(ctx, &call)?;
90+
let indent = if let Some(i) = &indent { i.to_string() } else { String::new() };
91+
Some(Self { start_offset: start, fn_name, fn_generics, fn_args, indent })
92+
}
93+
fn render(&self) -> FunctionTemplate {
94+
let mut fn_buf = String::with_capacity(128);
95+
fn_buf.push_str("\n\n");
96+
fn_buf.push_str(&self.indent);
97+
fn_buf.push_str("fn ");
98+
fn_buf.push_str(&self.fn_name);
99+
fn_buf.push_str(&self.fn_generics);
100+
fn_buf.push_str(&self.fn_args);
101+
fn_buf.push_str(" {\n");
102+
fn_buf.push_str(&self.indent);
103+
fn_buf.push_str(" ");
104+
105+
// We take the offset here to put the cursor in front of the `todo` body
106+
let offset = TextUnit::of_str(&fn_buf);
107+
108+
fn_buf.push_str("todo!()\n");
109+
fn_buf.push_str(&self.indent);
110+
fn_buf.push_str("}");
111+
112+
let cursor_pos = self.start_offset + offset;
113+
FunctionTemplate {
114+
fn_text: fn_buf,
115+
cursor_offset: cursor_pos,
116+
insert_offset: self.start_offset,
117+
}
118+
}
119+
}
120+
121+
impl FunctionTemplate {}
122+
123+
fn fn_name(call: &CallExpr) -> Option<String> {
124+
Some(call.expr()?.syntax().to_string())
125+
}
126+
127+
fn fn_generics(_call: &CallExpr) -> Option<String> {
128+
// TODO
129+
Some("".into())
130+
}
131+
132+
fn fn_args(ctx: &AssistCtx, _call: &CallExpr) -> Option<String> {
133+
let arg_list = _call.syntax().descendants().find_map(ast::ArgList::cast)?;
134+
for arg in arg_list.args() {
135+
match &arg {
136+
Expr::CallExpr(call_expr) => {
137+
arg_for_call(ctx, call_expr);
138+
}
139+
_ => {}
140+
}
141+
dbg!(&arg);
142+
}
143+
Some("()".into())
144+
}
145+
146+
fn arg_for_call(ctx: &AssistCtx, call_expr: &CallExpr) -> Option<String> {
147+
let path = call_expr.syntax().descendants().find_map(ast::Path::cast)?;
148+
let type_string = match ctx.sema.resolve_path(&path)? {
149+
hir::PathResolution::Def(ModuleDef::Function(f)) => {
150+
let _ret_type = dbg!(f.ret_type(ctx.sema.db));
151+
// TODO: find out how to print this
152+
String::new()
153+
}
154+
_ => return None,
155+
};
156+
157+
Some(format!("arg: {}", type_string))
158+
}
159+
160+
/// Returns the position inside the current mod or file
161+
/// directly after the current block
162+
/// We want to write the generated function directly after
163+
/// fns, impls or macro calls, but inside mods
164+
fn next_space_for_fn(expr: &CallExpr) -> Option<(TextUnit, Option<SmolStr>)> {
165+
let mut ancestors = expr.syntax().ancestors().peekable();
166+
let mut last_ancestor: Option<SyntaxNode> = None;
167+
while let Some(next_ancestor) = ancestors.next() {
168+
match next_ancestor.kind() {
169+
SyntaxKind::SOURCE_FILE => {
170+
break;
171+
}
172+
SyntaxKind::ITEM_LIST => {
173+
if ancestors.peek().map(|a| a.kind()) == Some(SyntaxKind::MODULE) {
174+
break;
175+
}
176+
}
177+
_ => {}
178+
}
179+
last_ancestor = Some(next_ancestor);
180+
}
181+
last_ancestor.map(|a| (a.text_range().end(), leading_indent(&a)))
182+
}
183+
184+
#[cfg(test)]
185+
mod tests {
186+
use crate::helpers::{check_assist, check_assist_not_applicable};
187+
188+
use super::*;
189+
190+
#[test]
191+
fn add_function_with_no_args() {
192+
check_assist(
193+
add_function,
194+
r"
195+
fn foo() {
196+
bar<|>();
197+
}
198+
",
199+
r"
200+
fn foo() {
201+
bar();
202+
}
203+
204+
fn bar() {
205+
<|>todo!()
206+
}
207+
",
208+
)
209+
}
210+
211+
#[test]
212+
fn add_function_from_method() {
213+
// This ensures that the function is correctly generated
214+
// in the next outer mod or file
215+
check_assist(
216+
add_function,
217+
r"
218+
impl Foo {
219+
fn foo() {
220+
bar<|>();
221+
}
222+
}
223+
",
224+
r"
225+
impl Foo {
226+
fn foo() {
227+
bar();
228+
}
229+
}
230+
231+
fn bar() {
232+
<|>todo!()
233+
}
234+
",
235+
)
236+
}
237+
238+
#[test]
239+
fn add_function_directly_after_current_block() {
240+
// The new fn should not be created at the end of the file or module
241+
check_assist(
242+
add_function,
243+
r"
244+
fn foo1() {
245+
bar<|>();
246+
}
247+
248+
fn foo2() {}
249+
",
250+
r"
251+
fn foo1() {
252+
bar();
253+
}
254+
255+
fn bar() {
256+
<|>todo!()
257+
}
258+
259+
fn foo2() {}
260+
",
261+
)
262+
}
263+
264+
#[test]
265+
fn add_function_with_no_args_in_same_module() {
266+
check_assist(
267+
add_function,
268+
r"
269+
mod baz {
270+
fn foo() {
271+
bar<|>();
272+
}
273+
}
274+
",
275+
r"
276+
mod baz {
277+
fn foo() {
278+
bar();
279+
}
280+
281+
fn bar() {
282+
<|>todo!()
283+
}
284+
}
285+
",
286+
)
287+
}
288+
289+
#[test]
290+
fn add_function_with_function_call_arg() {
291+
check_assist(
292+
add_function,
293+
r"
294+
fn baz() -> Baz { todo!() }
295+
fn foo() {
296+
bar<|>(baz());
297+
}
298+
",
299+
r"
300+
fn baz() -> Baz { todo!() }
301+
fn foo() {
302+
bar(baz());
303+
}
304+
305+
fn bar(baz: Baz) {
306+
<|>todo!()
307+
}
308+
",
309+
)
310+
}
311+
312+
#[test]
313+
fn add_function_not_applicable_if_function_already_exists() {
314+
check_assist_not_applicable(
315+
add_function,
316+
r"
317+
fn foo() {
318+
bar<|>();
319+
}
320+
321+
fn bar() {}
322+
",
323+
)
324+
}
325+
326+
#[test]
327+
fn add_function_not_applicable_if_unresolved_variable_in_call_is_selected() {
328+
check_assist_not_applicable(
329+
// bar is resolved, but baz isn't.
330+
// The assist is only active if the cursor is on an unresolved path,
331+
// but the assist should only be offered if the path is a function call.
332+
add_function,
333+
r"
334+
fn foo() {
335+
bar(b<|>az);
336+
}
337+
338+
fn bar(baz: ()) {}
339+
",
340+
)
341+
}
342+
343+
#[test]
344+
fn add_function_not_applicable_if_function_path_not_singleton() {
345+
// In the future this assist could be extended to generate functions
346+
// if the path is in the same crate (or even the same workspace).
347+
// For the beginning, I think this is fine.
348+
check_assist_not_applicable(
349+
add_function,
350+
r"
351+
fn foo() {
352+
other_crate::bar<|>();
353+
}
354+
",
355+
)
356+
}
357+
358+
#[test]
359+
#[ignore]
360+
fn create_method_with_no_args() {
361+
check_assist(
362+
add_function,
363+
r"
364+
struct Foo;
365+
impl Foo {
366+
fn foo(&self) {
367+
self.bar()<|>;
368+
}
369+
}
370+
",
371+
r"
372+
struct Foo;
373+
impl Foo {
374+
fn foo(&self) {
375+
self.bar();
376+
}
377+
fn bar(&self) {
378+
todo!();
379+
}
380+
}
381+
",
382+
)
383+
}
384+
}

0 commit comments

Comments
 (0)