Add add_new assist

Adds a new assist to autogenerate a new fn based on the selected struct,
excluding tuple structs and unions. The fn will inherit the same
visibility as the struct and the assist will attempt to reuse any
existing impl blocks that exist at the same level of struct.
This commit is contained in:
Wesley Norris 2019-11-09 10:56:36 -05:00
parent 9d786ea221
commit cbc6f94573
4 changed files with 424 additions and 0 deletions

View file

@ -0,0 +1,379 @@
use format_buf::format;
use hir::{db::HirDatabase, FromSource};
use join_to_string::join;
use ra_syntax::{
ast::{
self, AstNode, NameOwner, StructKind, TypeAscriptionOwner, TypeParamsOwner, VisibilityOwner,
},
TextUnit, T,
};
use std::fmt::Write;
use crate::{Assist, AssistCtx, AssistId};
// Assist: add_new
//
// Adds a new inherent impl for a type.
//
// ```
// struct Ctx<T: Clone> {
// data: T,<|>
// }
// ```
// ->
// ```
// struct Ctx<T: Clone> {
// data: T,
// }
//
// impl<T: Clone> Ctx<T> {
// fn new(data: T) -> Self { Self { data } }
// }
//
// ```
pub(crate) fn add_new(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
let strukt = ctx.find_node_at_offset::<ast::StructDef>()?;
// We want to only apply this to non-union structs with named fields
let field_list = match (strukt.kind(), strukt.is_union()) {
(StructKind::Named(named), false) => named,
_ => return None,
};
// Return early if we've found an existing new fn
let impl_block = find_struct_impl(&ctx, &strukt)?;
ctx.add_assist(AssistId("add_new"), "add new fn", |edit| {
edit.target(strukt.syntax().text_range());
let mut buf = String::with_capacity(512);
if impl_block.is_some() {
buf.push('\n');
}
let vis = strukt.visibility().map(|v| format!("{} ", v.syntax()));
let vis = vis.as_ref().map(String::as_str).unwrap_or("");
write!(&mut buf, " {}fn new(", vis).unwrap();
join(field_list.fields().map(|f| {
format!(
"{}: {}",
f.name().unwrap().syntax().text(),
f.ascribed_type().unwrap().syntax().text()
)
}))
.separator(", ")
.to_buf(&mut buf);
buf.push_str(") -> Self { Self {");
join(field_list.fields().map(|f| f.name().unwrap().syntax().text()))
.separator(", ")
.surround_with(" ", " ")
.to_buf(&mut buf);
buf.push_str("} }");
let (start_offset, end_offset) = if let Some(impl_block) = impl_block {
buf.push('\n');
let start = impl_block
.syntax()
.descendants_with_tokens()
.find(|t| t.kind() == T!['{'])
.unwrap()
.text_range()
.end();
(start, TextUnit::from_usize(1))
} else {
buf = generate_impl_text(&strukt, &buf);
let start = strukt.syntax().text_range().end();
(start, TextUnit::from_usize(3))
};
edit.set_cursor(start_offset + TextUnit::of_str(&buf) - end_offset);
edit.insert(start_offset, buf);
})
}
// Generates the surrounding `impl Type { <code> }` including type and lifetime
// parameters
fn generate_impl_text(strukt: &ast::StructDef, code: &str) -> String {
let type_params = strukt.type_param_list();
let mut buf = String::with_capacity(code.len());
buf.push_str("\n\nimpl");
if let Some(type_params) = &type_params {
format!(buf, "{}", type_params.syntax());
}
buf.push_str(" ");
buf.push_str(strukt.name().unwrap().text().as_str());
if let Some(type_params) = type_params {
let lifetime_params = type_params
.lifetime_params()
.filter_map(|it| it.lifetime_token())
.map(|it| it.text().clone());
let type_params =
type_params.type_params().filter_map(|it| it.name()).map(|it| it.text().clone());
join(lifetime_params.chain(type_params)).surround_with("<", ">").to_buf(&mut buf);
}
format!(&mut buf, " {{\n{}\n}}\n", code);
buf
}
// Uses a syntax-driven approach to find any impl blocks for the struct that
// exist within the module/file
//
// Returns `None` if we've found an existing `new` fn
//
// FIXME: change the new fn checking to a more semantic approach when that's more
// viable (e.g. we process proc macros, etc)
fn find_struct_impl(
ctx: &AssistCtx<impl HirDatabase>,
strukt: &ast::StructDef,
) -> Option<Option<ast::ImplBlock>> {
let db = ctx.db;
let module = strukt.syntax().ancestors().find(|node| {
ast::Module::can_cast(node.kind()) || ast::SourceFile::can_cast(node.kind())
})?;
let struct_ty = {
let src = hir::Source { file_id: ctx.frange.file_id.into(), ast: strukt.clone() };
hir::Struct::from_source(db, src).unwrap().ty(db)
};
let mut found_new_fn = false;
let block = module.descendants().filter_map(ast::ImplBlock::cast).find(|impl_blk| {
if found_new_fn {
return false;
}
let src = hir::Source { file_id: ctx.frange.file_id.into(), ast: impl_blk.clone() };
let blk = hir::ImplBlock::from_source(db, src).unwrap();
let same_ty = blk.target_ty(db) == struct_ty;
let not_trait_impl = blk.target_trait(db).is_none();
found_new_fn = has_new_fn(impl_blk);
same_ty && not_trait_impl
});
if found_new_fn {
None
} else {
Some(block)
}
}
fn has_new_fn(imp: &ast::ImplBlock) -> bool {
if let Some(il) = imp.item_list() {
for item in il.impl_items() {
if let ast::ImplItem::FnDef(f) = item {
if f.name().unwrap().text().eq_ignore_ascii_case("new") {
return true;
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target};
#[test]
#[rustfmt::skip]
fn test_add_new() {
// Check output of generation
check_assist(
add_new,
"struct Foo {<|>}",
"struct Foo {}
impl Foo {
fn new() -> Self { Self { } }<|>
}
",
);
check_assist(
add_new,
"struct Foo<T: Clone> {<|>}",
"struct Foo<T: Clone> {}
impl<T: Clone> Foo<T> {
fn new() -> Self { Self { } }<|>
}
",
);
check_assist(
add_new,
"struct Foo<'a, T: Foo<'a>> {<|>}",
"struct Foo<'a, T: Foo<'a>> {}
impl<'a, T: Foo<'a>> Foo<'a, T> {
fn new() -> Self { Self { } }<|>
}
",
);
check_assist(
add_new,
"struct Foo { baz: String <|>}",
"struct Foo { baz: String }
impl Foo {
fn new(baz: String) -> Self { Self { baz } }<|>
}
",
);
check_assist(
add_new,
"struct Foo { baz: String, qux: Vec<i32> <|>}",
"struct Foo { baz: String, qux: Vec<i32> }
impl Foo {
fn new(baz: String, qux: Vec<i32>) -> Self { Self { baz, qux } }<|>
}
",
);
// Check that visibility modifiers don't get brought in for fields
check_assist(
add_new,
"struct Foo { pub baz: String, pub qux: Vec<i32> <|>}",
"struct Foo { pub baz: String, pub qux: Vec<i32> }
impl Foo {
fn new(baz: String, qux: Vec<i32>) -> Self { Self { baz, qux } }<|>
}
",
);
// Check that it reuses existing impls
check_assist(
add_new,
"struct Foo {<|>}
impl Foo {}
",
"struct Foo {}
impl Foo {
fn new() -> Self { Self { } }<|>
}
",
);
check_assist(
add_new,
"struct Foo {<|>}
impl Foo {
fn qux(&self) {}
}
",
"struct Foo {}
impl Foo {
fn new() -> Self { Self { } }<|>
fn qux(&self) {}
}
",
);
check_assist(
add_new,
"struct Foo {<|>}
impl Foo {
fn qux(&self) {}
fn baz() -> i32 {
5
}
}
",
"struct Foo {}
impl Foo {
fn new() -> Self { Self { } }<|>
fn qux(&self) {}
fn baz() -> i32 {
5
}
}
",
);
// Check visibility of new fn based on struct
check_assist(
add_new,
"pub struct Foo {<|>}",
"pub struct Foo {}
impl Foo {
pub fn new() -> Self { Self { } }<|>
}
",
);
check_assist(
add_new,
"pub(crate) struct Foo {<|>}",
"pub(crate) struct Foo {}
impl Foo {
pub(crate) fn new() -> Self { Self { } }<|>
}
",
);
}
#[test]
fn add_new_not_applicable_if_fn_exists() {
check_assist_not_applicable(
add_new,
"
struct Foo {<|>}
impl Foo {
fn new() -> Self {
Self
}
}",
);
check_assist_not_applicable(
add_new,
"
struct Foo {<|>}
impl Foo {
fn New() -> Self {
Self
}
}",
);
}
#[test]
fn add_new_target() {
check_assist_target(
add_new,
"
struct SomeThingIrrelevant;
/// Has a lifetime parameter
struct Foo<'a, T: Foo<'a>> {<|>}
struct EvenMoreIrrelevant;
",
"/// Has a lifetime parameter
struct Foo<'a, T: Foo<'a>> {}",
);
}
}

View file

@ -156,6 +156,28 @@ fn process(map: HashMap<String, String>) {}
)
}
#[test]
fn doctest_add_new() {
check(
"add_new",
r#####"
struct Ctx<T: Clone> {
data: T,<|>
}
"#####,
r#####"
struct Ctx<T: Clone> {
data: T,
}
impl<T: Clone> Ctx<T> {
fn new(data: T) -> Self { Self { data } }
}
"#####,
)
}
#[test]
fn doctest_apply_demorgan() {
check(

View file

@ -95,6 +95,7 @@ mod assists {
mod add_derive;
mod add_explicit_type;
mod add_impl;
mod add_new;
mod apply_demorgan;
mod flip_comma;
mod flip_binexpr;
@ -119,6 +120,7 @@ mod assists {
add_derive::add_derive,
add_explicit_type::add_explicit_type,
add_impl::add_impl,
add_new::add_new,
apply_demorgan::apply_demorgan,
change_visibility::change_visibility,
fill_match_arms::fill_match_arms,

View file

@ -150,6 +150,27 @@ use std::collections::HashMap;
fn process(map: HashMap<String, String>) {}
```
## `add_new`
Adds a new inherent impl for a type.
```rust
// BEFORE
struct Ctx<T: Clone> {
data: T,┃
}
// AFTER
struct Ctx<T: Clone> {
data: T,
}
impl<T: Clone> Ctx<T> {
fn new(data: T) -> Self { Self { data } }
}
```
## `apply_demorgan`
Apply [De Morgan's law](https://en.wikipedia.org/wiki/De_Morgan%27s_laws).