-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f9cf864
commit cc516e6
Showing
2 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
use ra_syntax::{ | ||
ast::{self, AstNode}, | ||
SmolStr, SyntaxKind, SyntaxNode, TextUnit, | ||
}; | ||
|
||
use crate::{Assist, AssistCtx, AssistId}; | ||
use ast::{CallExpr, Expr}; | ||
use ra_fmt::leading_indent; | ||
|
||
// Assist: create_function | ||
// | ||
// Creates a stub function with a signature matching the function under the cursor. | ||
// | ||
// ``` | ||
// fn foo() { | ||
// bar<|>("", baz()); | ||
// } | ||
// | ||
// ``` | ||
// -> | ||
// ``` | ||
// fn foo() { | ||
// bar("", baz()); | ||
// } | ||
// | ||
// fn bar(arg_1: &str, baz: Baz) { | ||
// todo!(); | ||
// } | ||
// | ||
// ``` | ||
pub(crate) fn create_function(ctx: AssistCtx) -> Option<Assist> { | ||
let path: ast::Path = ctx.find_node_at_offset()?; | ||
|
||
if ctx.sema.resolve_path(&path).is_some() { | ||
// The function call already resolves, no need to create a function | ||
return None; | ||
} | ||
|
||
if path.qualifier().is_some() { | ||
return None; | ||
} | ||
|
||
let call: ast::CallExpr = ctx.find_node_at_offset()?; | ||
|
||
let (generated_fn, cursor_pos, text_start) = generate_fn(&ctx, &call)?; | ||
|
||
ctx.add_assist(AssistId("create_function"), "Create function", |edit| { | ||
edit.target(call.syntax().text_range()); | ||
|
||
edit.set_cursor(cursor_pos); | ||
edit.insert(text_start, generated_fn); | ||
}) | ||
} | ||
|
||
/// Generates a function definition that will allow `call` to compile. | ||
/// The function name must match. | ||
/// The arguments must have valid, nonconflicting names. | ||
/// The arguments' types must match those being passed to `call`. | ||
/// TODO generics? | ||
fn generate_fn(ctx: &AssistCtx, call: &CallExpr) -> Option<(String, TextUnit, TextUnit)> { | ||
let (start, indent) = next_space_for_fn(&call)?; | ||
let indent = if let Some(i) = &indent { i.as_str() } else { "" }; | ||
let mut fn_buf = String::with_capacity(128); | ||
|
||
fn_buf.push_str("\n\n"); | ||
fn_buf.push_str(indent); | ||
fn_buf.push_str("fn "); | ||
|
||
let fn_name = fn_name(&call)?; | ||
fn_buf.push_str(&fn_name); | ||
|
||
let fn_generics = fn_generics(&call)?; | ||
fn_buf.push_str(&fn_generics); | ||
|
||
let fn_args = fn_args()?; | ||
fn_buf.push_str(&fn_args); | ||
|
||
fn_buf.push_str(" {\n"); | ||
fn_buf.push_str(indent); | ||
fn_buf.push_str(" "); | ||
|
||
// We take the offset here to put the cursor in front of the `todo` body | ||
let offset = TextUnit::of_str(&fn_buf); | ||
|
||
fn_buf.push_str("todo!()\n"); | ||
fn_buf.push_str(indent); | ||
fn_buf.push_str("}"); | ||
|
||
let cursor_pos = start + offset; | ||
Some((fn_buf, cursor_pos, start)) | ||
} | ||
|
||
fn fn_name(call: &CallExpr) -> Option<String> { | ||
Some(call.expr()?.syntax().to_string()) | ||
} | ||
|
||
fn fn_generics(_call: &CallExpr) -> Option<String> { | ||
// TODO | ||
Some("".into()) | ||
} | ||
|
||
fn fn_args() -> Option<String> { | ||
Some("()".into()) | ||
} | ||
|
||
/// Returns the position inside the current mod or file | ||
/// directly after the current block | ||
/// We want to write the generated function directly after | ||
/// fns, impls or macro calls, but inside mods | ||
fn next_space_for_fn(expr: &CallExpr) -> Option<(TextUnit, Option<SmolStr>)> { | ||
let mut ancestors = expr.syntax().ancestors().peekable(); | ||
let mut last_ancestor: Option<SyntaxNode> = None; | ||
while let Some(next_ancestor) = ancestors.next() { | ||
match next_ancestor.kind() { | ||
SyntaxKind::SOURCE_FILE => { | ||
break; | ||
} | ||
SyntaxKind::ITEM_LIST => { | ||
if ancestors.peek().map(|a| a.kind()) == Some(SyntaxKind::MODULE) { | ||
break; | ||
} | ||
} | ||
_ => {} | ||
} | ||
last_ancestor = Some(next_ancestor); | ||
} | ||
last_ancestor.map(|a| (a.text_range().end(), leading_indent(&a))) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use crate::helpers::{check_assist, check_assist_not_applicable}; | ||
|
||
use super::*; | ||
|
||
#[test] | ||
fn create_function_with_no_args() { | ||
check_assist( | ||
create_function, | ||
r" | ||
fn foo() { | ||
bar<|>(); | ||
} | ||
", | ||
r" | ||
fn foo() { | ||
bar(); | ||
} | ||
fn bar() { | ||
<|>todo!() | ||
} | ||
", | ||
) | ||
} | ||
|
||
#[test] | ||
fn create_function_from_method() { | ||
// This ensures that the function is correctly generated | ||
// in the next outer mod or file | ||
check_assist( | ||
create_function, | ||
r" | ||
impl Foo { | ||
fn foo() { | ||
bar<|>(); | ||
} | ||
} | ||
", | ||
r" | ||
impl Foo { | ||
fn foo() { | ||
bar(); | ||
} | ||
} | ||
fn bar() { | ||
<|>todo!() | ||
} | ||
", | ||
) | ||
} | ||
|
||
#[test] | ||
fn create_function_directly_after_current_block() { | ||
// The new fn should not be created at the end of the file or module | ||
check_assist( | ||
create_function, | ||
r" | ||
fn foo1() { | ||
bar<|>(); | ||
} | ||
fn foo2() {} | ||
", | ||
r" | ||
fn foo1() { | ||
bar(); | ||
} | ||
fn bar() { | ||
<|>todo!() | ||
} | ||
fn foo2() {} | ||
", | ||
) | ||
} | ||
|
||
#[test] | ||
fn create_function_with_no_args_in_same_module() { | ||
check_assist( | ||
create_function, | ||
r" | ||
mod baz { | ||
fn foo() { | ||
bar<|>(); | ||
} | ||
} | ||
", | ||
r" | ||
mod baz { | ||
fn foo() { | ||
bar(); | ||
} | ||
fn bar() { | ||
<|>todo!() | ||
} | ||
} | ||
", | ||
) | ||
} | ||
|
||
#[test] | ||
fn create_function_with_function_call_arg() { | ||
check_assist( | ||
create_function, | ||
r" | ||
fn baz() -> Baz { todo!() } | ||
fn foo() { | ||
bar<|>(baz()); | ||
} | ||
", | ||
r" | ||
fn baz() -> Baz { todo!() } | ||
fn foo() { | ||
bar(baz()); | ||
} | ||
fn bar(baz: Baz) { | ||
<|>todo!() | ||
} | ||
", | ||
) | ||
} | ||
|
||
#[test] | ||
fn create_function_not_applicable_if_function_already_exists() { | ||
check_assist_not_applicable( | ||
create_function, | ||
r" | ||
fn foo() { | ||
bar<|>(); | ||
} | ||
fn bar() {} | ||
", | ||
) | ||
} | ||
|
||
#[test] | ||
fn create_function_not_applicable_if_function_path_not_singleton() { | ||
// In the future this assist could be extended to generate functions | ||
// if the path is in the same crate (or even the same workspace). | ||
// For the beginning, I think this is fine. | ||
check_assist_not_applicable( | ||
create_function, | ||
r" | ||
fn foo() { | ||
other_crate::bar<|>(); | ||
} | ||
", | ||
) | ||
} | ||
|
||
#[test] | ||
#[ignore] | ||
fn create_method_with_no_args() { | ||
check_assist( | ||
create_function, | ||
r" | ||
struct Foo; | ||
impl Foo { | ||
fn foo(&self) { | ||
self.bar()<|>; | ||
} | ||
} | ||
", | ||
r" | ||
struct Foo; | ||
impl Foo { | ||
fn foo(&self) { | ||
self.bar(); | ||
} | ||
fn bar(&self) { | ||
todo!(); | ||
} | ||
} | ||
", | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters