aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWesley Norris <[email protected]>2019-11-09 15:56:36 +0000
committerWesley Norris <[email protected]>2019-11-09 15:56:36 +0000
commitcbc6f94573d7f4601b739e001de5d5f71ec9b552 (patch)
treeee481bcf679e739dfea13ec36b16fb593607e43a
parent9d786ea221b27fbdf7c7f7beea0290db448e0611 (diff)
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.
-rw-r--r--crates/ra_assists/src/assists/add_new.rs379
-rw-r--r--crates/ra_assists/src/doc_tests/generated.rs22
-rw-r--r--crates/ra_assists/src/lib.rs2
-rw-r--r--docs/user/assists.md21
4 files changed, 424 insertions, 0 deletions
diff --git a/crates/ra_assists/src/assists/add_new.rs b/crates/ra_assists/src/assists/add_new.rs
new file mode 100644
index 000000000..a8839cfba
--- /dev/null
+++ b/crates/ra_assists/src/assists/add_new.rs
@@ -0,0 +1,379 @@
1use format_buf::format;
2use hir::{db::HirDatabase, FromSource};
3use join_to_string::join;
4use ra_syntax::{
5 ast::{
6 self, AstNode, NameOwner, StructKind, TypeAscriptionOwner, TypeParamsOwner, VisibilityOwner,
7 },
8 TextUnit, T,
9};
10use std::fmt::Write;
11
12use 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// ```
34pub(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
103fn 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)
134fn 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
173fn 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)]
188mod 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
201impl 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
211impl<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
221impl<'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
231impl 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
241impl 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
253impl 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
264impl Foo {}
265",
266"struct Foo {}
267
268impl Foo {
269 fn new() -> Self { Self { } }<|>
270}
271",
272 );
273 check_assist(
274 add_new,
275"struct Foo {<|>}
276
277impl Foo {
278 fn qux(&self) {}
279}
280",
281"struct Foo {}
282
283impl 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
295impl Foo {
296 fn qux(&self) {}
297 fn baz() -> i32 {
298 5
299 }
300}
301",
302"struct Foo {}
303
304impl 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
321impl 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
331impl 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 "
343struct Foo {<|>}
344
345impl Foo {
346 fn new() -> Self {
347 Self
348 }
349}",
350 );
351
352 check_assist_not_applicable(
353 add_new,
354 "
355struct Foo {<|>}
356
357impl 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 "
370struct SomeThingIrrelevant;
371/// Has a lifetime parameter
372struct Foo<'a, T: Foo<'a>> {<|>}
373struct EvenMoreIrrelevant;
374",
375 "/// Has a lifetime parameter
376struct Foo<'a, T: Foo<'a>> {}",
377 );
378 }
379}
diff --git a/crates/ra_assists/src/doc_tests/generated.rs b/crates/ra_assists/src/doc_tests/generated.rs
index 1bee76f59..176761efb 100644
--- a/crates/ra_assists/src/doc_tests/generated.rs
+++ b/crates/ra_assists/src/doc_tests/generated.rs
@@ -157,6 +157,28 @@ fn process(map: HashMap<String, String>) {}
157} 157}
158 158
159#[test] 159#[test]
160fn doctest_add_new() {
161 check(
162 "add_new",
163 r#####"
164struct Ctx<T: Clone> {
165 data: T,<|>
166}
167"#####,
168 r#####"
169struct Ctx<T: Clone> {
170 data: T,
171}
172
173impl<T: Clone> Ctx<T> {
174 fn new(data: T) -> Self { Self { data } }
175}
176
177"#####,
178 )
179}
180
181#[test]
160fn doctest_apply_demorgan() { 182fn doctest_apply_demorgan() {
161 check( 183 check(
162 "apply_demorgan", 184 "apply_demorgan",
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs
index 39c1c283f..f2f0dacbf 100644
--- a/crates/ra_assists/src/lib.rs
+++ b/crates/ra_assists/src/lib.rs
@@ -95,6 +95,7 @@ mod assists {
95 mod add_derive; 95 mod add_derive;
96 mod add_explicit_type; 96 mod add_explicit_type;
97 mod add_impl; 97 mod add_impl;
98 mod add_new;
98 mod apply_demorgan; 99 mod apply_demorgan;
99 mod flip_comma; 100 mod flip_comma;
100 mod flip_binexpr; 101 mod flip_binexpr;
@@ -119,6 +120,7 @@ mod assists {
119 add_derive::add_derive, 120 add_derive::add_derive,
120 add_explicit_type::add_explicit_type, 121 add_explicit_type::add_explicit_type,
121 add_impl::add_impl, 122 add_impl::add_impl,
123 add_new::add_new,
122 apply_demorgan::apply_demorgan, 124 apply_demorgan::apply_demorgan,
123 change_visibility::change_visibility, 125 change_visibility::change_visibility,
124 fill_match_arms::fill_match_arms, 126 fill_match_arms::fill_match_arms,
diff --git a/docs/user/assists.md b/docs/user/assists.md
index 303353e74..8da7578e2 100644
--- a/docs/user/assists.md
+++ b/docs/user/assists.md
@@ -150,6 +150,27 @@ use std::collections::HashMap;
150fn process(map: HashMap<String, String>) {} 150fn process(map: HashMap<String, String>) {}
151``` 151```
152 152
153## `add_new`
154
155Adds a new inherent impl for a type.
156
157```rust
158// BEFORE
159struct Ctx<T: Clone> {
160 data: T,┃
161}
162
163// AFTER
164struct Ctx<T: Clone> {
165 data: T,
166}
167
168impl<T: Clone> Ctx<T> {
169 fn new(data: T) -> Self { Self { data } }
170}
171
172```
173
153## `apply_demorgan` 174## `apply_demorgan`
154 175
155Apply [De Morgan's law](https://en.wikipedia.org/wiki/De_Morgan%27s_laws). 176Apply [De Morgan's law](https://en.wikipedia.org/wiki/De_Morgan%27s_laws).