Skip to content

Commit

Permalink
WIP: Add create_function assist
Browse files Browse the repository at this point in the history
  • Loading branch information
TimoFreiberg committed Mar 27, 2020
1 parent f9cf864 commit cc516e6
Show file tree
Hide file tree
Showing 2 changed files with 315 additions and 0 deletions.
313 changes: 313 additions & 0 deletions crates/ra_assists/src/handlers/create_function.rs
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!();
}
}
",
)
}
}
2 changes: 2 additions & 0 deletions crates/ra_assists/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ mod handlers {
mod apply_demorgan;
mod auto_import;
mod change_visibility;
mod create_function;
mod early_return;
mod fill_match_arms;
mod flip_binexpr;
Expand Down Expand Up @@ -134,6 +135,7 @@ mod handlers {
apply_demorgan::apply_demorgan,
auto_import::auto_import,
change_visibility::change_visibility,
create_function::create_function,
early_return::convert_to_guarded_return,
fill_match_arms::fill_match_arms,
flip_binexpr::flip_binexpr,
Expand Down

0 comments on commit cc516e6

Please sign in to comment.