Skip to content

Commit cbc6f94

Browse files
committed
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.
1 parent 9d786ea commit cbc6f94

File tree

4 files changed

+424
-0
lines changed

4 files changed

+424
-0
lines changed
+379
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
use format_buf::format;
2+
use hir::{db::HirDatabase, FromSource};
3+
use join_to_string::join;
4+
use ra_syntax::{
5+
ast::{
6+
self, AstNode, NameOwner, StructKind, TypeAscriptionOwner, TypeParamsOwner, VisibilityOwner,
7+
},
8+
TextUnit, T,
9+
};
10+
use std::fmt::Write;
11+
12+
use crate::{Assist, AssistCtx, AssistId};
13+
14+
// Assist: add_new
15+
//
16+
// Adds a new inherent impl for a type.
17+
//
18+
// ```
19+
// struct Ctx<T: Clone> {
20+
// data: T,<|>
21+
// }
22+
// ```
23+
// ->
24+
// ```
25+
// struct Ctx<T: Clone> {
26+
// data: T,
27+
// }
28+
//
29+
// impl<T: Clone> Ctx<T> {
30+
// fn new(data: T) -> Self { Self { data } }
31+
// }
32+
//
33+
// ```
34+
pub(crate) fn add_new(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
35+
let strukt = ctx.find_node_at_offset::<ast::StructDef>()?;
36+
37+
// We want to only apply this to non-union structs with named fields
38+
let field_list = match (strukt.kind(), strukt.is_union()) {
39+
(StructKind::Named(named), false) => named,
40+
_ => return None,
41+
};
42+
43+
// Return early if we've found an existing new fn
44+
let impl_block = find_struct_impl(&ctx, &strukt)?;
45+
46+
ctx.add_assist(AssistId("add_new"), "add new fn", |edit| {
47+
edit.target(strukt.syntax().text_range());
48+
49+
let mut buf = String::with_capacity(512);
50+
51+
if impl_block.is_some() {
52+
buf.push('\n');
53+
}
54+
55+
let vis = strukt.visibility().map(|v| format!("{} ", v.syntax()));
56+
let vis = vis.as_ref().map(String::as_str).unwrap_or("");
57+
write!(&mut buf, " {}fn new(", vis).unwrap();
58+
59+
join(field_list.fields().map(|f| {
60+
format!(
61+
"{}: {}",
62+
f.name().unwrap().syntax().text(),
63+
f.ascribed_type().unwrap().syntax().text()
64+
)
65+
}))
66+
.separator(", ")
67+
.to_buf(&mut buf);
68+
69+
buf.push_str(") -> Self { Self {");
70+
71+
join(field_list.fields().map(|f| f.name().unwrap().syntax().text()))
72+
.separator(", ")
73+
.surround_with(" ", " ")
74+
.to_buf(&mut buf);
75+
76+
buf.push_str("} }");
77+
78+
let (start_offset, end_offset) = if let Some(impl_block) = impl_block {
79+
buf.push('\n');
80+
let start = impl_block
81+
.syntax()
82+
.descendants_with_tokens()
83+
.find(|t| t.kind() == T!['{'])
84+
.unwrap()
85+
.text_range()
86+
.end();
87+
88+
(start, TextUnit::from_usize(1))
89+
} else {
90+
buf = generate_impl_text(&strukt, &buf);
91+
let start = strukt.syntax().text_range().end();
92+
93+
(start, TextUnit::from_usize(3))
94+
};
95+
96+
edit.set_cursor(start_offset + TextUnit::of_str(&buf) - end_offset);
97+
edit.insert(start_offset, buf);
98+
})
99+
}
100+
101+
// Generates the surrounding `impl Type { <code> }` including type and lifetime
102+
// parameters
103+
fn generate_impl_text(strukt: &ast::StructDef, code: &str) -> String {
104+
let type_params = strukt.type_param_list();
105+
let mut buf = String::with_capacity(code.len());
106+
buf.push_str("\n\nimpl");
107+
if let Some(type_params) = &type_params {
108+
format!(buf, "{}", type_params.syntax());
109+
}
110+
buf.push_str(" ");
111+
buf.push_str(strukt.name().unwrap().text().as_str());
112+
if let Some(type_params) = type_params {
113+
let lifetime_params = type_params
114+
.lifetime_params()
115+
.filter_map(|it| it.lifetime_token())
116+
.map(|it| it.text().clone());
117+
let type_params =
118+
type_params.type_params().filter_map(|it| it.name()).map(|it| it.text().clone());
119+
join(lifetime_params.chain(type_params)).surround_with("<", ">").to_buf(&mut buf);
120+
}
121+
122+
format!(&mut buf, " {{\n{}\n}}\n", code);
123+
124+
buf
125+
}
126+
127+
// Uses a syntax-driven approach to find any impl blocks for the struct that
128+
// exist within the module/file
129+
//
130+
// Returns `None` if we've found an existing `new` fn
131+
//
132+
// FIXME: change the new fn checking to a more semantic approach when that's more
133+
// viable (e.g. we process proc macros, etc)
134+
fn find_struct_impl(
135+
ctx: &AssistCtx<impl HirDatabase>,
136+
strukt: &ast::StructDef,
137+
) -> Option<Option<ast::ImplBlock>> {
138+
let db = ctx.db;
139+
let module = strukt.syntax().ancestors().find(|node| {
140+
ast::Module::can_cast(node.kind()) || ast::SourceFile::can_cast(node.kind())
141+
})?;
142+
143+
let struct_ty = {
144+
let src = hir::Source { file_id: ctx.frange.file_id.into(), ast: strukt.clone() };
145+
hir::Struct::from_source(db, src).unwrap().ty(db)
146+
};
147+
148+
let mut found_new_fn = false;
149+
150+
let block = module.descendants().filter_map(ast::ImplBlock::cast).find(|impl_blk| {
151+
if found_new_fn {
152+
return false;
153+
}
154+
155+
let src = hir::Source { file_id: ctx.frange.file_id.into(), ast: impl_blk.clone() };
156+
let blk = hir::ImplBlock::from_source(db, src).unwrap();
157+
158+
let same_ty = blk.target_ty(db) == struct_ty;
159+
let not_trait_impl = blk.target_trait(db).is_none();
160+
161+
found_new_fn = has_new_fn(impl_blk);
162+
163+
same_ty && not_trait_impl
164+
});
165+
166+
if found_new_fn {
167+
None
168+
} else {
169+
Some(block)
170+
}
171+
}
172+
173+
fn has_new_fn(imp: &ast::ImplBlock) -> bool {
174+
if let Some(il) = imp.item_list() {
175+
for item in il.impl_items() {
176+
if let ast::ImplItem::FnDef(f) = item {
177+
if f.name().unwrap().text().eq_ignore_ascii_case("new") {
178+
return true;
179+
}
180+
}
181+
}
182+
}
183+
184+
false
185+
}
186+
187+
#[cfg(test)]
188+
mod tests {
189+
use super::*;
190+
use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target};
191+
192+
#[test]
193+
#[rustfmt::skip]
194+
fn test_add_new() {
195+
// Check output of generation
196+
check_assist(
197+
add_new,
198+
"struct Foo {<|>}",
199+
"struct Foo {}
200+
201+
impl Foo {
202+
fn new() -> Self { Self { } }<|>
203+
}
204+
",
205+
);
206+
check_assist(
207+
add_new,
208+
"struct Foo<T: Clone> {<|>}",
209+
"struct Foo<T: Clone> {}
210+
211+
impl<T: Clone> Foo<T> {
212+
fn new() -> Self { Self { } }<|>
213+
}
214+
",
215+
);
216+
check_assist(
217+
add_new,
218+
"struct Foo<'a, T: Foo<'a>> {<|>}",
219+
"struct Foo<'a, T: Foo<'a>> {}
220+
221+
impl<'a, T: Foo<'a>> Foo<'a, T> {
222+
fn new() -> Self { Self { } }<|>
223+
}
224+
",
225+
);
226+
check_assist(
227+
add_new,
228+
"struct Foo { baz: String <|>}",
229+
"struct Foo { baz: String }
230+
231+
impl Foo {
232+
fn new(baz: String) -> Self { Self { baz } }<|>
233+
}
234+
",
235+
);
236+
check_assist(
237+
add_new,
238+
"struct Foo { baz: String, qux: Vec<i32> <|>}",
239+
"struct Foo { baz: String, qux: Vec<i32> }
240+
241+
impl Foo {
242+
fn new(baz: String, qux: Vec<i32>) -> Self { Self { baz, qux } }<|>
243+
}
244+
",
245+
);
246+
247+
// Check that visibility modifiers don't get brought in for fields
248+
check_assist(
249+
add_new,
250+
"struct Foo { pub baz: String, pub qux: Vec<i32> <|>}",
251+
"struct Foo { pub baz: String, pub qux: Vec<i32> }
252+
253+
impl Foo {
254+
fn new(baz: String, qux: Vec<i32>) -> Self { Self { baz, qux } }<|>
255+
}
256+
",
257+
);
258+
259+
// Check that it reuses existing impls
260+
check_assist(
261+
add_new,
262+
"struct Foo {<|>}
263+
264+
impl Foo {}
265+
",
266+
"struct Foo {}
267+
268+
impl Foo {
269+
fn new() -> Self { Self { } }<|>
270+
}
271+
",
272+
);
273+
check_assist(
274+
add_new,
275+
"struct Foo {<|>}
276+
277+
impl Foo {
278+
fn qux(&self) {}
279+
}
280+
",
281+
"struct Foo {}
282+
283+
impl Foo {
284+
fn new() -> Self { Self { } }<|>
285+
286+
fn qux(&self) {}
287+
}
288+
",
289+
);
290+
291+
check_assist(
292+
add_new,
293+
"struct Foo {<|>}
294+
295+
impl Foo {
296+
fn qux(&self) {}
297+
fn baz() -> i32 {
298+
5
299+
}
300+
}
301+
",
302+
"struct Foo {}
303+
304+
impl Foo {
305+
fn new() -> Self { Self { } }<|>
306+
307+
fn qux(&self) {}
308+
fn baz() -> i32 {
309+
5
310+
}
311+
}
312+
",
313+
);
314+
315+
// Check visibility of new fn based on struct
316+
check_assist(
317+
add_new,
318+
"pub struct Foo {<|>}",
319+
"pub struct Foo {}
320+
321+
impl Foo {
322+
pub fn new() -> Self { Self { } }<|>
323+
}
324+
",
325+
);
326+
check_assist(
327+
add_new,
328+
"pub(crate) struct Foo {<|>}",
329+
"pub(crate) struct Foo {}
330+
331+
impl Foo {
332+
pub(crate) fn new() -> Self { Self { } }<|>
333+
}
334+
",
335+
);
336+
}
337+
338+
#[test]
339+
fn add_new_not_applicable_if_fn_exists() {
340+
check_assist_not_applicable(
341+
add_new,
342+
"
343+
struct Foo {<|>}
344+
345+
impl Foo {
346+
fn new() -> Self {
347+
Self
348+
}
349+
}",
350+
);
351+
352+
check_assist_not_applicable(
353+
add_new,
354+
"
355+
struct Foo {<|>}
356+
357+
impl Foo {
358+
fn New() -> Self {
359+
Self
360+
}
361+
}",
362+
);
363+
}
364+
365+
#[test]
366+
fn add_new_target() {
367+
check_assist_target(
368+
add_new,
369+
"
370+
struct SomeThingIrrelevant;
371+
/// Has a lifetime parameter
372+
struct Foo<'a, T: Foo<'a>> {<|>}
373+
struct EvenMoreIrrelevant;
374+
",
375+
"/// Has a lifetime parameter
376+
struct Foo<'a, T: Foo<'a>> {}",
377+
);
378+
}
379+
}

0 commit comments

Comments
 (0)