diff options
author | Aleksey Kladov <[email protected]> | 2019-02-03 18:26:35 +0000 |
---|---|---|
committer | Aleksey Kladov <[email protected]> | 2019-02-06 14:00:00 +0000 |
commit | 0c5fd8f7cbf04eda763e55bc9a38dad5f7ec917d (patch) | |
tree | 4af15c8906b85de01a15c717bc1fac388952cd3d /crates/ra_assists/src | |
parent | 736a55c97e69f95e6ff4a0c3dafb2018e8ea05f9 (diff) |
move assists to a separate crate
Diffstat (limited to 'crates/ra_assists/src')
-rw-r--r-- | crates/ra_assists/src/add_derive.rs | 85 | ||||
-rw-r--r-- | crates/ra_assists/src/add_impl.rs | 67 | ||||
-rw-r--r-- | crates/ra_assists/src/assist_ctx.rs | 154 | ||||
-rw-r--r-- | crates/ra_assists/src/change_visibility.rs | 166 | ||||
-rw-r--r-- | crates/ra_assists/src/fill_match_arms.rs | 145 | ||||
-rw-r--r-- | crates/ra_assists/src/flip_comma.rs | 33 | ||||
-rw-r--r-- | crates/ra_assists/src/introduce_variable.rs | 432 | ||||
-rw-r--r-- | crates/ra_assists/src/lib.rs | 170 | ||||
-rw-r--r-- | crates/ra_assists/src/replace_if_let_with_match.rs | 80 | ||||
-rw-r--r-- | crates/ra_assists/src/split_import.rs | 57 |
10 files changed, 1389 insertions, 0 deletions
diff --git a/crates/ra_assists/src/add_derive.rs b/crates/ra_assists/src/add_derive.rs new file mode 100644 index 000000000..01a4079f6 --- /dev/null +++ b/crates/ra_assists/src/add_derive.rs | |||
@@ -0,0 +1,85 @@ | |||
1 | use hir::db::HirDatabase; | ||
2 | use ra_syntax::{ | ||
3 | ast::{self, AstNode, AttrsOwner}, | ||
4 | SyntaxKind::{WHITESPACE, COMMENT}, | ||
5 | TextUnit, | ||
6 | }; | ||
7 | |||
8 | use crate::{AssistCtx, Assist}; | ||
9 | |||
10 | pub(crate) fn add_derive(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | ||
11 | let nominal = ctx.node_at_offset::<ast::NominalDef>()?; | ||
12 | let node_start = derive_insertion_offset(nominal)?; | ||
13 | ctx.build("add `#[derive]`", |edit| { | ||
14 | let derive_attr = nominal | ||
15 | .attrs() | ||
16 | .filter_map(|x| x.as_call()) | ||
17 | .filter(|(name, _arg)| name == "derive") | ||
18 | .map(|(_name, arg)| arg) | ||
19 | .next(); | ||
20 | let offset = match derive_attr { | ||
21 | None => { | ||
22 | edit.insert(node_start, "#[derive()]\n"); | ||
23 | node_start + TextUnit::of_str("#[derive(") | ||
24 | } | ||
25 | Some(tt) => tt.syntax().range().end() - TextUnit::of_char(')'), | ||
26 | }; | ||
27 | edit.set_cursor(offset) | ||
28 | }) | ||
29 | } | ||
30 | |||
31 | // Insert `derive` after doc comments. | ||
32 | fn derive_insertion_offset(nominal: &ast::NominalDef) -> Option<TextUnit> { | ||
33 | let non_ws_child = nominal | ||
34 | .syntax() | ||
35 | .children() | ||
36 | .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?; | ||
37 | Some(non_ws_child.range().start()) | ||
38 | } | ||
39 | |||
40 | #[cfg(test)] | ||
41 | mod tests { | ||
42 | use super::*; | ||
43 | use crate::helpers::check_assist; | ||
44 | |||
45 | #[test] | ||
46 | fn add_derive_new() { | ||
47 | check_assist( | ||
48 | add_derive, | ||
49 | "struct Foo { a: i32, <|>}", | ||
50 | "#[derive(<|>)]\nstruct Foo { a: i32, }", | ||
51 | ); | ||
52 | check_assist( | ||
53 | add_derive, | ||
54 | "struct Foo { <|> a: i32, }", | ||
55 | "#[derive(<|>)]\nstruct Foo { a: i32, }", | ||
56 | ); | ||
57 | } | ||
58 | |||
59 | #[test] | ||
60 | fn add_derive_existing() { | ||
61 | check_assist( | ||
62 | add_derive, | ||
63 | "#[derive(Clone)]\nstruct Foo { a: i32<|>, }", | ||
64 | "#[derive(Clone<|>)]\nstruct Foo { a: i32, }", | ||
65 | ); | ||
66 | } | ||
67 | |||
68 | #[test] | ||
69 | fn add_derive_new_with_doc_comment() { | ||
70 | check_assist( | ||
71 | add_derive, | ||
72 | " | ||
73 | /// `Foo` is a pretty important struct. | ||
74 | /// It does stuff. | ||
75 | struct Foo { a: i32<|>, } | ||
76 | ", | ||
77 | " | ||
78 | /// `Foo` is a pretty important struct. | ||
79 | /// It does stuff. | ||
80 | #[derive(<|>)] | ||
81 | struct Foo { a: i32, } | ||
82 | ", | ||
83 | ); | ||
84 | } | ||
85 | } | ||
diff --git a/crates/ra_assists/src/add_impl.rs b/crates/ra_assists/src/add_impl.rs new file mode 100644 index 000000000..699508f91 --- /dev/null +++ b/crates/ra_assists/src/add_impl.rs | |||
@@ -0,0 +1,67 @@ | |||
1 | use join_to_string::join; | ||
2 | use hir::db::HirDatabase; | ||
3 | use ra_syntax::{ | ||
4 | ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner}, | ||
5 | TextUnit, | ||
6 | }; | ||
7 | |||
8 | use crate::{AssistCtx, Assist}; | ||
9 | |||
10 | pub(crate) fn add_impl(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | ||
11 | let nominal = ctx.node_at_offset::<ast::NominalDef>()?; | ||
12 | let name = nominal.name()?; | ||
13 | ctx.build("add impl", |edit| { | ||
14 | let type_params = nominal.type_param_list(); | ||
15 | let start_offset = nominal.syntax().range().end(); | ||
16 | let mut buf = String::new(); | ||
17 | buf.push_str("\n\nimpl"); | ||
18 | if let Some(type_params) = type_params { | ||
19 | type_params.syntax().text().push_to(&mut buf); | ||
20 | } | ||
21 | buf.push_str(" "); | ||
22 | buf.push_str(name.text().as_str()); | ||
23 | if let Some(type_params) = type_params { | ||
24 | let lifetime_params = type_params | ||
25 | .lifetime_params() | ||
26 | .filter_map(|it| it.lifetime()) | ||
27 | .map(|it| it.text()); | ||
28 | let type_params = type_params | ||
29 | .type_params() | ||
30 | .filter_map(|it| it.name()) | ||
31 | .map(|it| it.text()); | ||
32 | join(lifetime_params.chain(type_params)) | ||
33 | .surround_with("<", ">") | ||
34 | .to_buf(&mut buf); | ||
35 | } | ||
36 | buf.push_str(" {\n"); | ||
37 | edit.set_cursor(start_offset + TextUnit::of_str(&buf)); | ||
38 | buf.push_str("\n}"); | ||
39 | edit.insert(start_offset, buf); | ||
40 | }) | ||
41 | } | ||
42 | |||
43 | #[cfg(test)] | ||
44 | mod tests { | ||
45 | use super::*; | ||
46 | use crate::helpers::check_assist; | ||
47 | |||
48 | #[test] | ||
49 | fn test_add_impl() { | ||
50 | check_assist( | ||
51 | add_impl, | ||
52 | "struct Foo {<|>}\n", | ||
53 | "struct Foo {}\n\nimpl Foo {\n<|>\n}\n", | ||
54 | ); | ||
55 | check_assist( | ||
56 | add_impl, | ||
57 | "struct Foo<T: Clone> {<|>}", | ||
58 | "struct Foo<T: Clone> {}\n\nimpl<T: Clone> Foo<T> {\n<|>\n}", | ||
59 | ); | ||
60 | check_assist( | ||
61 | add_impl, | ||
62 | "struct Foo<'a, T: Foo<'a>> {<|>}", | ||
63 | "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}", | ||
64 | ); | ||
65 | } | ||
66 | |||
67 | } | ||
diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs new file mode 100644 index 000000000..6d09bde52 --- /dev/null +++ b/crates/ra_assists/src/assist_ctx.rs | |||
@@ -0,0 +1,154 @@ | |||
1 | use hir::db::HirDatabase; | ||
2 | use ra_text_edit::TextEditBuilder; | ||
3 | use ra_db::FileRange; | ||
4 | use ra_syntax::{ | ||
5 | SourceFile, TextRange, AstNode, TextUnit, SyntaxNode, | ||
6 | algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset}, | ||
7 | }; | ||
8 | use ra_ide_api_light::formatting::{leading_indent, reindent}; | ||
9 | |||
10 | use crate::{AssistLabel, AssistAction}; | ||
11 | |||
12 | pub(crate) enum Assist { | ||
13 | Unresolved(AssistLabel), | ||
14 | Resolved(AssistLabel, AssistAction), | ||
15 | } | ||
16 | |||
17 | /// `AssistCtx` allows to apply an assist or check if it could be applied. | ||
18 | /// | ||
19 | /// Assists use a somewhat overengineered approach, given the current needs. The | ||
20 | /// assists workflow consists of two phases. In the first phase, a user asks for | ||
21 | /// the list of available assists. In the second phase, the user picks a | ||
22 | /// particular assist and it gets applied. | ||
23 | /// | ||
24 | /// There are two peculiarities here: | ||
25 | /// | ||
26 | /// * first, we ideally avoid computing more things then necessary to answer | ||
27 | /// "is assist applicable" in the first phase. | ||
28 | /// * second, when we are applying assist, we don't have a guarantee that there | ||
29 | /// weren't any changes between the point when user asked for assists and when | ||
30 | /// they applied a particular assist. So, when applying assist, we need to do | ||
31 | /// all the checks from scratch. | ||
32 | /// | ||
33 | /// To avoid repeating the same code twice for both "check" and "apply" | ||
34 | /// functions, we use an approach reminiscent of that of Django's function based | ||
35 | /// views dealing with forms. Each assist receives a runtime parameter, | ||
36 | /// `should_compute_edit`. It first check if an edit is applicable (potentially | ||
37 | /// computing info required to compute the actual edit). If it is applicable, | ||
38 | /// and `should_compute_edit` is `true`, it then computes the actual edit. | ||
39 | /// | ||
40 | /// So, to implement the original assists workflow, we can first apply each edit | ||
41 | /// with `should_compute_edit = false`, and then applying the selected edit | ||
42 | /// again, with `should_compute_edit = true` this time. | ||
43 | /// | ||
44 | /// Note, however, that we don't actually use such two-phase logic at the | ||
45 | /// moment, because the LSP API is pretty awkward in this place, and it's much | ||
46 | /// easier to just compute the edit eagerly :-)#[derive(Debug, Clone)] | ||
47 | #[derive(Debug)] | ||
48 | pub(crate) struct AssistCtx<'a, DB> { | ||
49 | pub(crate) db: &'a DB, | ||
50 | pub(crate) frange: FileRange, | ||
51 | source_file: &'a SourceFile, | ||
52 | should_compute_edit: bool, | ||
53 | } | ||
54 | |||
55 | impl<'a, DB> Clone for AssistCtx<'a, DB> { | ||
56 | fn clone(&self) -> Self { | ||
57 | AssistCtx { | ||
58 | db: self.db, | ||
59 | frange: self.frange, | ||
60 | source_file: self.source_file, | ||
61 | should_compute_edit: self.should_compute_edit, | ||
62 | } | ||
63 | } | ||
64 | } | ||
65 | |||
66 | impl<'a, DB: HirDatabase> AssistCtx<'a, DB> { | ||
67 | pub(crate) fn with_ctx<F, T>(db: &DB, frange: FileRange, should_compute_edit: bool, f: F) -> T | ||
68 | where | ||
69 | F: FnOnce(AssistCtx<DB>) -> T, | ||
70 | { | ||
71 | let source_file = &db.parse(frange.file_id); | ||
72 | let ctx = AssistCtx { | ||
73 | db, | ||
74 | frange, | ||
75 | source_file, | ||
76 | should_compute_edit, | ||
77 | }; | ||
78 | f(ctx) | ||
79 | } | ||
80 | |||
81 | pub(crate) fn build( | ||
82 | self, | ||
83 | label: impl Into<String>, | ||
84 | f: impl FnOnce(&mut AssistBuilder), | ||
85 | ) -> Option<Assist> { | ||
86 | let label = AssistLabel { | ||
87 | label: label.into(), | ||
88 | }; | ||
89 | if !self.should_compute_edit { | ||
90 | return Some(Assist::Unresolved(label)); | ||
91 | } | ||
92 | let action = { | ||
93 | let mut edit = AssistBuilder::default(); | ||
94 | f(&mut edit); | ||
95 | edit.build() | ||
96 | }; | ||
97 | Some(Assist::Resolved(label, action)) | ||
98 | } | ||
99 | |||
100 | pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<&'a SyntaxNode> { | ||
101 | find_leaf_at_offset(self.source_file.syntax(), self.frange.range.start()) | ||
102 | } | ||
103 | |||
104 | pub(crate) fn node_at_offset<N: AstNode>(&self) -> Option<&'a N> { | ||
105 | find_node_at_offset(self.source_file.syntax(), self.frange.range.start()) | ||
106 | } | ||
107 | pub(crate) fn covering_node(&self) -> &'a SyntaxNode { | ||
108 | find_covering_node(self.source_file.syntax(), self.frange.range) | ||
109 | } | ||
110 | } | ||
111 | |||
112 | #[derive(Default)] | ||
113 | pub(crate) struct AssistBuilder { | ||
114 | edit: TextEditBuilder, | ||
115 | cursor_position: Option<TextUnit>, | ||
116 | } | ||
117 | |||
118 | impl AssistBuilder { | ||
119 | pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) { | ||
120 | self.edit.replace(range, replace_with.into()) | ||
121 | } | ||
122 | |||
123 | pub(crate) fn replace_node_and_indent( | ||
124 | &mut self, | ||
125 | node: &SyntaxNode, | ||
126 | replace_with: impl Into<String>, | ||
127 | ) { | ||
128 | let mut replace_with = replace_with.into(); | ||
129 | if let Some(indent) = leading_indent(node) { | ||
130 | replace_with = reindent(&replace_with, indent) | ||
131 | } | ||
132 | self.replace(node.range(), replace_with) | ||
133 | } | ||
134 | |||
135 | #[allow(unused)] | ||
136 | pub(crate) fn delete(&mut self, range: TextRange) { | ||
137 | self.edit.delete(range) | ||
138 | } | ||
139 | |||
140 | pub(crate) fn insert(&mut self, offset: TextUnit, text: impl Into<String>) { | ||
141 | self.edit.insert(offset, text.into()) | ||
142 | } | ||
143 | |||
144 | pub(crate) fn set_cursor(&mut self, offset: TextUnit) { | ||
145 | self.cursor_position = Some(offset) | ||
146 | } | ||
147 | |||
148 | fn build(self) -> AssistAction { | ||
149 | AssistAction { | ||
150 | edit: self.edit.finish(), | ||
151 | cursor_position: self.cursor_position, | ||
152 | } | ||
153 | } | ||
154 | } | ||
diff --git a/crates/ra_assists/src/change_visibility.rs b/crates/ra_assists/src/change_visibility.rs new file mode 100644 index 000000000..4cd32985e --- /dev/null +++ b/crates/ra_assists/src/change_visibility.rs | |||
@@ -0,0 +1,166 @@ | |||
1 | use hir::db::HirDatabase; | ||
2 | use ra_syntax::{ | ||
3 | AstNode, SyntaxNode, TextUnit, | ||
4 | ast::{self, VisibilityOwner, NameOwner}, | ||
5 | SyntaxKind::{VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF, IDENT, WHITESPACE, COMMENT, ATTR}, | ||
6 | }; | ||
7 | |||
8 | use crate::{AssistCtx, Assist}; | ||
9 | |||
10 | pub(crate) fn change_visibility(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | ||
11 | if let Some(vis) = ctx.node_at_offset::<ast::Visibility>() { | ||
12 | return change_vis(ctx, vis); | ||
13 | } | ||
14 | add_vis(ctx) | ||
15 | } | ||
16 | |||
17 | fn add_vis(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | ||
18 | let item_keyword = ctx.leaf_at_offset().find(|leaf| match leaf.kind() { | ||
19 | FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true, | ||
20 | _ => false, | ||
21 | }); | ||
22 | |||
23 | let offset = if let Some(keyword) = item_keyword { | ||
24 | let parent = keyword.parent()?; | ||
25 | let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF]; | ||
26 | // Parent is not a definition, can't add visibility | ||
27 | if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) { | ||
28 | return None; | ||
29 | } | ||
30 | // Already have visibility, do nothing | ||
31 | if parent.children().any(|child| child.kind() == VISIBILITY) { | ||
32 | return None; | ||
33 | } | ||
34 | vis_offset(parent) | ||
35 | } else { | ||
36 | let ident = ctx.leaf_at_offset().find(|leaf| leaf.kind() == IDENT)?; | ||
37 | let field = ident.ancestors().find_map(ast::NamedFieldDef::cast)?; | ||
38 | if field.name()?.syntax().range() != ident.range() && field.visibility().is_some() { | ||
39 | return None; | ||
40 | } | ||
41 | vis_offset(field.syntax()) | ||
42 | }; | ||
43 | |||
44 | ctx.build("make pub(crate)", |edit| { | ||
45 | edit.insert(offset, "pub(crate) "); | ||
46 | edit.set_cursor(offset); | ||
47 | }) | ||
48 | } | ||
49 | |||
50 | fn vis_offset(node: &SyntaxNode) -> TextUnit { | ||
51 | node.children() | ||
52 | .skip_while(|it| match it.kind() { | ||
53 | WHITESPACE | COMMENT | ATTR => true, | ||
54 | _ => false, | ||
55 | }) | ||
56 | .next() | ||
57 | .map(|it| it.range().start()) | ||
58 | .unwrap_or(node.range().start()) | ||
59 | } | ||
60 | |||
61 | fn change_vis(ctx: AssistCtx<impl HirDatabase>, vis: &ast::Visibility) -> Option<Assist> { | ||
62 | if vis.syntax().text() == "pub" { | ||
63 | return ctx.build("chage to pub(crate)", |edit| { | ||
64 | edit.replace(vis.syntax().range(), "pub(crate)"); | ||
65 | edit.set_cursor(vis.syntax().range().start()); | ||
66 | }); | ||
67 | } | ||
68 | if vis.syntax().text() == "pub(crate)" { | ||
69 | return ctx.build("chage to pub", |edit| { | ||
70 | edit.replace(vis.syntax().range(), "pub"); | ||
71 | edit.set_cursor(vis.syntax().range().start()); | ||
72 | }); | ||
73 | } | ||
74 | None | ||
75 | } | ||
76 | |||
77 | #[cfg(test)] | ||
78 | mod tests { | ||
79 | use super::*; | ||
80 | use crate::helpers::check_assist; | ||
81 | |||
82 | #[test] | ||
83 | fn change_visibility_adds_pub_crate_to_items() { | ||
84 | check_assist( | ||
85 | change_visibility, | ||
86 | "<|>fn foo() {}", | ||
87 | "<|>pub(crate) fn foo() {}", | ||
88 | ); | ||
89 | check_assist( | ||
90 | change_visibility, | ||
91 | "f<|>n foo() {}", | ||
92 | "<|>pub(crate) fn foo() {}", | ||
93 | ); | ||
94 | check_assist( | ||
95 | change_visibility, | ||
96 | "<|>struct Foo {}", | ||
97 | "<|>pub(crate) struct Foo {}", | ||
98 | ); | ||
99 | check_assist( | ||
100 | change_visibility, | ||
101 | "<|>mod foo {}", | ||
102 | "<|>pub(crate) mod foo {}", | ||
103 | ); | ||
104 | check_assist( | ||
105 | change_visibility, | ||
106 | "<|>trait Foo {}", | ||
107 | "<|>pub(crate) trait Foo {}", | ||
108 | ); | ||
109 | check_assist(change_visibility, "m<|>od {}", "<|>pub(crate) mod {}"); | ||
110 | check_assist( | ||
111 | change_visibility, | ||
112 | "unsafe f<|>n foo() {}", | ||
113 | "<|>pub(crate) unsafe fn foo() {}", | ||
114 | ); | ||
115 | } | ||
116 | |||
117 | #[test] | ||
118 | fn change_visibility_works_with_struct_fields() { | ||
119 | check_assist( | ||
120 | change_visibility, | ||
121 | "struct S { <|>field: u32 }", | ||
122 | "struct S { <|>pub(crate) field: u32 }", | ||
123 | ) | ||
124 | } | ||
125 | |||
126 | #[test] | ||
127 | fn change_visibility_pub_to_pub_crate() { | ||
128 | check_assist( | ||
129 | change_visibility, | ||
130 | "<|>pub fn foo() {}", | ||
131 | "<|>pub(crate) fn foo() {}", | ||
132 | ) | ||
133 | } | ||
134 | |||
135 | #[test] | ||
136 | fn change_visibility_pub_crate_to_pub() { | ||
137 | check_assist( | ||
138 | change_visibility, | ||
139 | "<|>pub(crate) fn foo() {}", | ||
140 | "<|>pub fn foo() {}", | ||
141 | ) | ||
142 | } | ||
143 | |||
144 | #[test] | ||
145 | fn change_visibility_handles_comment_attrs() { | ||
146 | check_assist( | ||
147 | change_visibility, | ||
148 | " | ||
149 | /// docs | ||
150 | |||
151 | // comments | ||
152 | |||
153 | #[derive(Debug)] | ||
154 | <|>struct Foo; | ||
155 | ", | ||
156 | " | ||
157 | /// docs | ||
158 | |||
159 | // comments | ||
160 | |||
161 | #[derive(Debug)] | ||
162 | <|>pub(crate) struct Foo; | ||
163 | ", | ||
164 | ) | ||
165 | } | ||
166 | } | ||
diff --git a/crates/ra_assists/src/fill_match_arms.rs b/crates/ra_assists/src/fill_match_arms.rs new file mode 100644 index 000000000..9aa37d94c --- /dev/null +++ b/crates/ra_assists/src/fill_match_arms.rs | |||
@@ -0,0 +1,145 @@ | |||
1 | use std::fmt::Write; | ||
2 | |||
3 | use hir::{ | ||
4 | AdtDef, Ty, FieldSource, source_binder, | ||
5 | db::HirDatabase, | ||
6 | }; | ||
7 | use ra_syntax::ast::{self, AstNode}; | ||
8 | |||
9 | use crate::{AssistCtx, Assist}; | ||
10 | |||
11 | pub(crate) fn fill_match_arms(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | ||
12 | let match_expr = ctx.node_at_offset::<ast::MatchExpr>()?; | ||
13 | |||
14 | // We already have some match arms, so we don't provide any assists. | ||
15 | match match_expr.match_arm_list() { | ||
16 | Some(arm_list) if arm_list.arms().count() > 0 => { | ||
17 | return None; | ||
18 | } | ||
19 | _ => {} | ||
20 | } | ||
21 | |||
22 | let expr = match_expr.expr()?; | ||
23 | let function = | ||
24 | source_binder::function_from_child_node(ctx.db, ctx.frange.file_id, expr.syntax())?; | ||
25 | let infer_result = function.infer(ctx.db); | ||
26 | let syntax_mapping = function.body_syntax_mapping(ctx.db); | ||
27 | let node_expr = syntax_mapping.node_expr(expr)?; | ||
28 | let match_expr_ty = infer_result[node_expr].clone(); | ||
29 | let enum_def = match match_expr_ty { | ||
30 | Ty::Adt { | ||
31 | def_id: AdtDef::Enum(e), | ||
32 | .. | ||
33 | } => e, | ||
34 | _ => return None, | ||
35 | }; | ||
36 | let enum_name = enum_def.name(ctx.db)?; | ||
37 | let db = ctx.db; | ||
38 | |||
39 | ctx.build("fill match arms", |edit| { | ||
40 | let mut buf = format!("match {} {{\n", expr.syntax().text().to_string()); | ||
41 | let variants = enum_def.variants(db); | ||
42 | for variant in variants { | ||
43 | let name = match variant.name(db) { | ||
44 | Some(it) => it, | ||
45 | None => continue, | ||
46 | }; | ||
47 | write!(&mut buf, " {}::{}", enum_name, name.to_string()).unwrap(); | ||
48 | |||
49 | let pat = variant | ||
50 | .fields(db) | ||
51 | .into_iter() | ||
52 | .map(|field| { | ||
53 | let name = field.name(db).to_string(); | ||
54 | let (_, source) = field.source(db); | ||
55 | match source { | ||
56 | FieldSource::Named(_) => name, | ||
57 | FieldSource::Pos(_) => "_".to_string(), | ||
58 | } | ||
59 | }) | ||
60 | .collect::<Vec<_>>(); | ||
61 | |||
62 | match pat.first().map(|s| s.as_str()) { | ||
63 | Some("_") => write!(&mut buf, "({})", pat.join(", ")).unwrap(), | ||
64 | Some(_) => write!(&mut buf, "{{{}}}", pat.join(", ")).unwrap(), | ||
65 | None => (), | ||
66 | }; | ||
67 | |||
68 | buf.push_str(" => (),\n"); | ||
69 | } | ||
70 | buf.push_str("}"); | ||
71 | edit.set_cursor(expr.syntax().range().start()); | ||
72 | edit.replace_node_and_indent(match_expr.syntax(), buf); | ||
73 | }) | ||
74 | } | ||
75 | |||
76 | #[cfg(test)] | ||
77 | mod tests { | ||
78 | use crate::helpers::check_assist; | ||
79 | |||
80 | use super::fill_match_arms; | ||
81 | |||
82 | #[test] | ||
83 | fn fill_match_arms_empty_body() { | ||
84 | check_assist( | ||
85 | fill_match_arms, | ||
86 | r#" | ||
87 | enum A { | ||
88 | As, | ||
89 | Bs, | ||
90 | Cs(String), | ||
91 | Ds(String, String), | ||
92 | Es{x: usize, y: usize} | ||
93 | } | ||
94 | |||
95 | fn main() { | ||
96 | let a = A::As; | ||
97 | match a<|> {} | ||
98 | } | ||
99 | "#, | ||
100 | r#" | ||
101 | enum A { | ||
102 | As, | ||
103 | Bs, | ||
104 | Cs(String), | ||
105 | Ds(String, String), | ||
106 | Es{x: usize, y: usize} | ||
107 | } | ||
108 | |||
109 | fn main() { | ||
110 | let a = A::As; | ||
111 | match <|>a { | ||
112 | A::As => (), | ||
113 | A::Bs => (), | ||
114 | A::Cs(_) => (), | ||
115 | A::Ds(_, _) => (), | ||
116 | A::Es{x, y} => (), | ||
117 | } | ||
118 | } | ||
119 | "#, | ||
120 | ); | ||
121 | } | ||
122 | #[test] | ||
123 | fn fill_match_arms_no_body() { | ||
124 | check_assist( | ||
125 | fill_match_arms, | ||
126 | r#" | ||
127 | enum E { X, Y} | ||
128 | |||
129 | fn main() { | ||
130 | match E::X<|> | ||
131 | } | ||
132 | "#, | ||
133 | r#" | ||
134 | enum E { X, Y} | ||
135 | |||
136 | fn main() { | ||
137 | match <|>E::X { | ||
138 | E::X => (), | ||
139 | E::Y => (), | ||
140 | } | ||
141 | } | ||
142 | "#, | ||
143 | ); | ||
144 | } | ||
145 | } | ||
diff --git a/crates/ra_assists/src/flip_comma.rs b/crates/ra_assists/src/flip_comma.rs new file mode 100644 index 000000000..a49820c29 --- /dev/null +++ b/crates/ra_assists/src/flip_comma.rs | |||
@@ -0,0 +1,33 @@ | |||
1 | use hir::db::HirDatabase; | ||
2 | use ra_syntax::{ | ||
3 | Direction, | ||
4 | SyntaxKind::COMMA, | ||
5 | }; | ||
6 | |||
7 | use crate::{AssistCtx, Assist, non_trivia_sibling}; | ||
8 | |||
9 | pub(crate) fn flip_comma(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | ||
10 | let comma = ctx.leaf_at_offset().find(|leaf| leaf.kind() == COMMA)?; | ||
11 | let prev = non_trivia_sibling(comma, Direction::Prev)?; | ||
12 | let next = non_trivia_sibling(comma, Direction::Next)?; | ||
13 | ctx.build("flip comma", |edit| { | ||
14 | edit.replace(prev.range(), next.text()); | ||
15 | edit.replace(next.range(), prev.text()); | ||
16 | }) | ||
17 | } | ||
18 | |||
19 | #[cfg(test)] | ||
20 | mod tests { | ||
21 | use super::*; | ||
22 | |||
23 | use crate::helpers::check_assist; | ||
24 | |||
25 | #[test] | ||
26 | fn flip_comma_works_for_function_parameters() { | ||
27 | check_assist( | ||
28 | flip_comma, | ||
29 | "fn foo(x: i32,<|> y: Result<(), ()>) {}", | ||
30 | "fn foo(y: Result<(), ()>,<|> x: i32) {}", | ||
31 | ) | ||
32 | } | ||
33 | } | ||
diff --git a/crates/ra_assists/src/introduce_variable.rs b/crates/ra_assists/src/introduce_variable.rs new file mode 100644 index 000000000..c937a816c --- /dev/null +++ b/crates/ra_assists/src/introduce_variable.rs | |||
@@ -0,0 +1,432 @@ | |||
1 | use hir::db::HirDatabase; | ||
2 | use ra_syntax::{ | ||
3 | ast::{self, AstNode}, | ||
4 | SyntaxKind::{ | ||
5 | WHITESPACE, MATCH_ARM, LAMBDA_EXPR, PATH_EXPR, BREAK_EXPR, LOOP_EXPR, RETURN_EXPR, COMMENT | ||
6 | }, SyntaxNode, TextUnit, | ||
7 | }; | ||
8 | |||
9 | use crate::{AssistCtx, Assist}; | ||
10 | |||
11 | pub(crate) fn introduce_variable<'a>(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | ||
12 | let node = ctx.covering_node(); | ||
13 | if !valid_covering_node(node) { | ||
14 | return None; | ||
15 | } | ||
16 | let expr = node.ancestors().filter_map(valid_target_expr).next()?; | ||
17 | let (anchor_stmt, wrap_in_block) = anchor_stmt(expr)?; | ||
18 | let indent = anchor_stmt.prev_sibling()?; | ||
19 | if indent.kind() != WHITESPACE { | ||
20 | return None; | ||
21 | } | ||
22 | ctx.build("introduce variable", move |edit| { | ||
23 | let mut buf = String::new(); | ||
24 | |||
25 | let cursor_offset = if wrap_in_block { | ||
26 | buf.push_str("{ let var_name = "); | ||
27 | TextUnit::of_str("{ let ") | ||
28 | } else { | ||
29 | buf.push_str("let var_name = "); | ||
30 | TextUnit::of_str("let ") | ||
31 | }; | ||
32 | |||
33 | expr.syntax().text().push_to(&mut buf); | ||
34 | let full_stmt = ast::ExprStmt::cast(anchor_stmt); | ||
35 | let is_full_stmt = if let Some(expr_stmt) = full_stmt { | ||
36 | Some(expr.syntax()) == expr_stmt.expr().map(|e| e.syntax()) | ||
37 | } else { | ||
38 | false | ||
39 | }; | ||
40 | if is_full_stmt { | ||
41 | if !full_stmt.unwrap().has_semi() { | ||
42 | buf.push_str(";"); | ||
43 | } | ||
44 | edit.replace(expr.syntax().range(), buf); | ||
45 | } else { | ||
46 | buf.push_str(";"); | ||
47 | indent.text().push_to(&mut buf); | ||
48 | edit.replace(expr.syntax().range(), "var_name".to_string()); | ||
49 | edit.insert(anchor_stmt.range().start(), buf); | ||
50 | if wrap_in_block { | ||
51 | edit.insert(anchor_stmt.range().end(), " }"); | ||
52 | } | ||
53 | } | ||
54 | edit.set_cursor(anchor_stmt.range().start() + cursor_offset); | ||
55 | }) | ||
56 | } | ||
57 | |||
58 | fn valid_covering_node(node: &SyntaxNode) -> bool { | ||
59 | node.kind() != COMMENT | ||
60 | } | ||
61 | /// Check wether the node is a valid expression which can be extracted to a variable. | ||
62 | /// In general that's true for any expression, but in some cases that would produce invalid code. | ||
63 | fn valid_target_expr(node: &SyntaxNode) -> Option<&ast::Expr> { | ||
64 | return match node.kind() { | ||
65 | PATH_EXPR => None, | ||
66 | BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()), | ||
67 | RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()), | ||
68 | LOOP_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()), | ||
69 | _ => ast::Expr::cast(node), | ||
70 | }; | ||
71 | } | ||
72 | |||
73 | /// Returns the syntax node which will follow the freshly introduced var | ||
74 | /// and a boolean indicating whether we have to wrap it within a { } block | ||
75 | /// to produce correct code. | ||
76 | /// It can be a statement, the last in a block expression or a wanna be block | ||
77 | /// expression like a lamba or match arm. | ||
78 | fn anchor_stmt(expr: &ast::Expr) -> Option<(&SyntaxNode, bool)> { | ||
79 | expr.syntax().ancestors().find_map(|node| { | ||
80 | if ast::Stmt::cast(node).is_some() { | ||
81 | return Some((node, false)); | ||
82 | } | ||
83 | |||
84 | if let Some(expr) = node | ||
85 | .parent() | ||
86 | .and_then(ast::Block::cast) | ||
87 | .and_then(|it| it.expr()) | ||
88 | { | ||
89 | if expr.syntax() == node { | ||
90 | return Some((node, false)); | ||
91 | } | ||
92 | } | ||
93 | |||
94 | if let Some(parent) = node.parent() { | ||
95 | if parent.kind() == MATCH_ARM || parent.kind() == LAMBDA_EXPR { | ||
96 | return Some((node, true)); | ||
97 | } | ||
98 | } | ||
99 | |||
100 | None | ||
101 | }) | ||
102 | } | ||
103 | |||
104 | #[cfg(test)] | ||
105 | mod tests { | ||
106 | use super::*; | ||
107 | use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_range}; | ||
108 | |||
109 | #[test] | ||
110 | fn test_introduce_var_simple() { | ||
111 | check_assist_range( | ||
112 | introduce_variable, | ||
113 | " | ||
114 | fn foo() { | ||
115 | foo(<|>1 + 1<|>); | ||
116 | }", | ||
117 | " | ||
118 | fn foo() { | ||
119 | let <|>var_name = 1 + 1; | ||
120 | foo(var_name); | ||
121 | }", | ||
122 | ); | ||
123 | } | ||
124 | |||
125 | #[test] | ||
126 | fn test_introduce_var_expr_stmt() { | ||
127 | check_assist_range( | ||
128 | introduce_variable, | ||
129 | " | ||
130 | fn foo() { | ||
131 | <|>1 + 1<|>; | ||
132 | }", | ||
133 | " | ||
134 | fn foo() { | ||
135 | let <|>var_name = 1 + 1; | ||
136 | }", | ||
137 | ); | ||
138 | } | ||
139 | |||
140 | #[test] | ||
141 | fn test_introduce_var_part_of_expr_stmt() { | ||
142 | check_assist_range( | ||
143 | introduce_variable, | ||
144 | " | ||
145 | fn foo() { | ||
146 | <|>1<|> + 1; | ||
147 | }", | ||
148 | " | ||
149 | fn foo() { | ||
150 | let <|>var_name = 1; | ||
151 | var_name + 1; | ||
152 | }", | ||
153 | ); | ||
154 | } | ||
155 | |||
156 | #[test] | ||
157 | fn test_introduce_var_last_expr() { | ||
158 | check_assist_range( | ||
159 | introduce_variable, | ||
160 | " | ||
161 | fn foo() { | ||
162 | bar(<|>1 + 1<|>) | ||
163 | }", | ||
164 | " | ||
165 | fn foo() { | ||
166 | let <|>var_name = 1 + 1; | ||
167 | bar(var_name) | ||
168 | }", | ||
169 | ); | ||
170 | } | ||
171 | |||
172 | #[test] | ||
173 | fn test_introduce_var_last_full_expr() { | ||
174 | check_assist_range( | ||
175 | introduce_variable, | ||
176 | " | ||
177 | fn foo() { | ||
178 | <|>bar(1 + 1)<|> | ||
179 | }", | ||
180 | " | ||
181 | fn foo() { | ||
182 | let <|>var_name = bar(1 + 1); | ||
183 | var_name | ||
184 | }", | ||
185 | ); | ||
186 | } | ||
187 | |||
188 | #[test] | ||
189 | fn test_introduce_var_block_expr_second_to_last() { | ||
190 | check_assist_range( | ||
191 | introduce_variable, | ||
192 | " | ||
193 | fn foo() { | ||
194 | <|>{ let x = 0; x }<|> | ||
195 | something_else(); | ||
196 | }", | ||
197 | " | ||
198 | fn foo() { | ||
199 | let <|>var_name = { let x = 0; x }; | ||
200 | something_else(); | ||
201 | }", | ||
202 | ); | ||
203 | } | ||
204 | |||
205 | #[test] | ||
206 | fn test_introduce_var_in_match_arm_no_block() { | ||
207 | check_assist_range( | ||
208 | introduce_variable, | ||
209 | " | ||
210 | fn main() { | ||
211 | let x = true; | ||
212 | let tuple = match x { | ||
213 | true => (<|>2 + 2<|>, true) | ||
214 | _ => (0, false) | ||
215 | }; | ||
216 | } | ||
217 | ", | ||
218 | " | ||
219 | fn main() { | ||
220 | let x = true; | ||
221 | let tuple = match x { | ||
222 | true => { let <|>var_name = 2 + 2; (var_name, true) } | ||
223 | _ => (0, false) | ||
224 | }; | ||
225 | } | ||
226 | ", | ||
227 | ); | ||
228 | } | ||
229 | |||
230 | #[test] | ||
231 | fn test_introduce_var_in_match_arm_with_block() { | ||
232 | check_assist_range( | ||
233 | introduce_variable, | ||
234 | " | ||
235 | fn main() { | ||
236 | let x = true; | ||
237 | let tuple = match x { | ||
238 | true => { | ||
239 | let y = 1; | ||
240 | (<|>2 + y<|>, true) | ||
241 | } | ||
242 | _ => (0, false) | ||
243 | }; | ||
244 | } | ||
245 | ", | ||
246 | " | ||
247 | fn main() { | ||
248 | let x = true; | ||
249 | let tuple = match x { | ||
250 | true => { | ||
251 | let y = 1; | ||
252 | let <|>var_name = 2 + y; | ||
253 | (var_name, true) | ||
254 | } | ||
255 | _ => (0, false) | ||
256 | }; | ||
257 | } | ||
258 | ", | ||
259 | ); | ||
260 | } | ||
261 | |||
262 | #[test] | ||
263 | fn test_introduce_var_in_closure_no_block() { | ||
264 | check_assist_range( | ||
265 | introduce_variable, | ||
266 | " | ||
267 | fn main() { | ||
268 | let lambda = |x: u32| <|>x * 2<|>; | ||
269 | } | ||
270 | ", | ||
271 | " | ||
272 | fn main() { | ||
273 | let lambda = |x: u32| { let <|>var_name = x * 2; var_name }; | ||
274 | } | ||
275 | ", | ||
276 | ); | ||
277 | } | ||
278 | |||
279 | #[test] | ||
280 | fn test_introduce_var_in_closure_with_block() { | ||
281 | check_assist_range( | ||
282 | introduce_variable, | ||
283 | " | ||
284 | fn main() { | ||
285 | let lambda = |x: u32| { <|>x * 2<|> }; | ||
286 | } | ||
287 | ", | ||
288 | " | ||
289 | fn main() { | ||
290 | let lambda = |x: u32| { let <|>var_name = x * 2; var_name }; | ||
291 | } | ||
292 | ", | ||
293 | ); | ||
294 | } | ||
295 | |||
296 | #[test] | ||
297 | fn test_introduce_var_path_simple() { | ||
298 | check_assist( | ||
299 | introduce_variable, | ||
300 | " | ||
301 | fn main() { | ||
302 | let o = S<|>ome(true); | ||
303 | } | ||
304 | ", | ||
305 | " | ||
306 | fn main() { | ||
307 | let <|>var_name = Some(true); | ||
308 | let o = var_name; | ||
309 | } | ||
310 | ", | ||
311 | ); | ||
312 | } | ||
313 | |||
314 | #[test] | ||
315 | fn test_introduce_var_path_method() { | ||
316 | check_assist( | ||
317 | introduce_variable, | ||
318 | " | ||
319 | fn main() { | ||
320 | let v = b<|>ar.foo(); | ||
321 | } | ||
322 | ", | ||
323 | " | ||
324 | fn main() { | ||
325 | let <|>var_name = bar.foo(); | ||
326 | let v = var_name; | ||
327 | } | ||
328 | ", | ||
329 | ); | ||
330 | } | ||
331 | |||
332 | #[test] | ||
333 | fn test_introduce_var_return() { | ||
334 | check_assist( | ||
335 | introduce_variable, | ||
336 | " | ||
337 | fn foo() -> u32 { | ||
338 | r<|>eturn 2 + 2; | ||
339 | } | ||
340 | ", | ||
341 | " | ||
342 | fn foo() -> u32 { | ||
343 | let <|>var_name = 2 + 2; | ||
344 | return var_name; | ||
345 | } | ||
346 | ", | ||
347 | ); | ||
348 | } | ||
349 | |||
350 | #[test] | ||
351 | fn test_introduce_var_break() { | ||
352 | check_assist( | ||
353 | introduce_variable, | ||
354 | " | ||
355 | fn main() { | ||
356 | let result = loop { | ||
357 | b<|>reak 2 + 2; | ||
358 | }; | ||
359 | } | ||
360 | ", | ||
361 | " | ||
362 | fn main() { | ||
363 | let result = loop { | ||
364 | let <|>var_name = 2 + 2; | ||
365 | break var_name; | ||
366 | }; | ||
367 | } | ||
368 | ", | ||
369 | ); | ||
370 | } | ||
371 | |||
372 | #[test] | ||
373 | fn test_introduce_var_for_cast() { | ||
374 | check_assist( | ||
375 | introduce_variable, | ||
376 | " | ||
377 | fn main() { | ||
378 | let v = 0f32 a<|>s u32; | ||
379 | } | ||
380 | ", | ||
381 | " | ||
382 | fn main() { | ||
383 | let <|>var_name = 0f32 as u32; | ||
384 | let v = var_name; | ||
385 | } | ||
386 | ", | ||
387 | ); | ||
388 | } | ||
389 | |||
390 | #[test] | ||
391 | fn test_introduce_var_for_return_not_applicable() { | ||
392 | check_assist_not_applicable( | ||
393 | introduce_variable, | ||
394 | " | ||
395 | fn foo() { | ||
396 | r<|>eturn; | ||
397 | } | ||
398 | ", | ||
399 | ); | ||
400 | } | ||
401 | |||
402 | #[test] | ||
403 | fn test_introduce_var_for_break_not_applicable() { | ||
404 | check_assist_not_applicable( | ||
405 | introduce_variable, | ||
406 | " | ||
407 | fn main() { | ||
408 | loop { | ||
409 | b<|>reak; | ||
410 | }; | ||
411 | } | ||
412 | ", | ||
413 | ); | ||
414 | } | ||
415 | |||
416 | #[test] | ||
417 | fn test_introduce_var_in_comment_not_applicable() { | ||
418 | check_assist_not_applicable( | ||
419 | introduce_variable, | ||
420 | " | ||
421 | fn main() { | ||
422 | let x = true; | ||
423 | let tuple = match x { | ||
424 | // c<|>omment | ||
425 | true => (2 + 2, true) | ||
426 | _ => (0, false) | ||
427 | }; | ||
428 | } | ||
429 | ", | ||
430 | ); | ||
431 | } | ||
432 | } | ||
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs new file mode 100644 index 000000000..4e97a84c2 --- /dev/null +++ b/crates/ra_assists/src/lib.rs | |||
@@ -0,0 +1,170 @@ | |||
1 | //! `ra_assits` crate provides a bunch of code assists, aslo known as code | ||
2 | //! actions (in LSP) or intentions (in IntelliJ). | ||
3 | //! | ||
4 | //! An assist is a micro-refactoring, which is automatically activated in | ||
5 | //! certain context. For example, if the cursor is over `,`, a "swap `,`" assist | ||
6 | //! becomes available. | ||
7 | |||
8 | mod assist_ctx; | ||
9 | |||
10 | use ra_text_edit::TextEdit; | ||
11 | use ra_syntax::{TextUnit, SyntaxNode, Direction}; | ||
12 | use ra_db::FileRange; | ||
13 | use hir::db::HirDatabase; | ||
14 | |||
15 | pub(crate) use crate::assist_ctx::{AssistCtx, Assist}; | ||
16 | |||
17 | #[derive(Debug)] | ||
18 | pub struct AssistLabel { | ||
19 | /// Short description of the assist, as shown in the UI. | ||
20 | pub label: String, | ||
21 | } | ||
22 | |||
23 | pub struct AssistAction { | ||
24 | pub edit: TextEdit, | ||
25 | pub cursor_position: Option<TextUnit>, | ||
26 | } | ||
27 | |||
28 | /// Return all the assists applicable at the given position. | ||
29 | /// | ||
30 | /// Assists are returned in the "unresolved" state, that is only labels are | ||
31 | /// returned, without actual edits. | ||
32 | pub fn applicable_assists<H>(db: &H, range: FileRange) -> Vec<AssistLabel> | ||
33 | where | ||
34 | H: HirDatabase + 'static, | ||
35 | { | ||
36 | AssistCtx::with_ctx(db, range, false, |ctx| { | ||
37 | all_assists() | ||
38 | .iter() | ||
39 | .filter_map(|f| f(ctx.clone())) | ||
40 | .map(|a| match a { | ||
41 | Assist::Unresolved(label) => label, | ||
42 | Assist::Resolved(..) => unreachable!(), | ||
43 | }) | ||
44 | .collect() | ||
45 | }) | ||
46 | } | ||
47 | |||
48 | /// Return all the assists applicable at the given position. | ||
49 | /// | ||
50 | /// Assists are returned in the "resolved" state, that is with edit fully | ||
51 | /// computed. | ||
52 | pub fn assists<H>(db: &H, range: FileRange) -> Vec<(AssistLabel, AssistAction)> | ||
53 | where | ||
54 | H: HirDatabase + 'static, | ||
55 | { | ||
56 | AssistCtx::with_ctx(db, range, false, |ctx| { | ||
57 | all_assists() | ||
58 | .iter() | ||
59 | .filter_map(|f| f(ctx.clone())) | ||
60 | .map(|a| match a { | ||
61 | Assist::Resolved(label, action) => (label, action), | ||
62 | Assist::Unresolved(..) => unreachable!(), | ||
63 | }) | ||
64 | .collect() | ||
65 | }) | ||
66 | } | ||
67 | |||
68 | mod add_derive; | ||
69 | mod add_impl; | ||
70 | mod flip_comma; | ||
71 | mod change_visibility; | ||
72 | mod fill_match_arms; | ||
73 | mod introduce_variable; | ||
74 | mod replace_if_let_with_match; | ||
75 | mod split_import; | ||
76 | fn all_assists<DB: HirDatabase>() -> &'static [fn(AssistCtx<DB>) -> Option<Assist>] { | ||
77 | &[ | ||
78 | add_derive::add_derive, | ||
79 | add_impl::add_impl, | ||
80 | change_visibility::change_visibility, | ||
81 | fill_match_arms::fill_match_arms, | ||
82 | flip_comma::flip_comma, | ||
83 | introduce_variable::introduce_variable, | ||
84 | replace_if_let_with_match::replace_if_let_with_match, | ||
85 | split_import::split_import, | ||
86 | ] | ||
87 | } | ||
88 | |||
89 | fn non_trivia_sibling(node: &SyntaxNode, direction: Direction) -> Option<&SyntaxNode> { | ||
90 | node.siblings(direction) | ||
91 | .skip(1) | ||
92 | .find(|node| !node.kind().is_trivia()) | ||
93 | } | ||
94 | |||
95 | #[cfg(test)] | ||
96 | mod helpers { | ||
97 | use hir::mock::MockDatabase; | ||
98 | use ra_syntax::TextRange; | ||
99 | use ra_db::FileRange; | ||
100 | use test_utils::{extract_offset, assert_eq_text, add_cursor, extract_range}; | ||
101 | |||
102 | use crate::{AssistCtx, Assist}; | ||
103 | |||
104 | pub(crate) fn check_assist( | ||
105 | assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>, | ||
106 | before: &str, | ||
107 | after: &str, | ||
108 | ) { | ||
109 | let (before_cursor_pos, before) = extract_offset(before); | ||
110 | let (db, _source_root, file_id) = MockDatabase::with_single_file(&before); | ||
111 | let frange = FileRange { | ||
112 | file_id, | ||
113 | range: TextRange::offset_len(before_cursor_pos, 0.into()), | ||
114 | }; | ||
115 | let assist = | ||
116 | AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable"); | ||
117 | let action = match assist { | ||
118 | Assist::Unresolved(_) => unreachable!(), | ||
119 | Assist::Resolved(_, it) => it, | ||
120 | }; | ||
121 | |||
122 | let actual = action.edit.apply(&before); | ||
123 | let actual_cursor_pos = match action.cursor_position { | ||
124 | None => action | ||
125 | .edit | ||
126 | .apply_to_offset(before_cursor_pos) | ||
127 | .expect("cursor position is affected by the edit"), | ||
128 | Some(off) => off, | ||
129 | }; | ||
130 | let actual = add_cursor(&actual, actual_cursor_pos); | ||
131 | assert_eq_text!(after, &actual); | ||
132 | } | ||
133 | |||
134 | pub(crate) fn check_assist_range( | ||
135 | assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>, | ||
136 | before: &str, | ||
137 | after: &str, | ||
138 | ) { | ||
139 | let (range, before) = extract_range(before); | ||
140 | let (db, _source_root, file_id) = MockDatabase::with_single_file(&before); | ||
141 | let frange = FileRange { file_id, range }; | ||
142 | let assist = | ||
143 | AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable"); | ||
144 | let action = match assist { | ||
145 | Assist::Unresolved(_) => unreachable!(), | ||
146 | Assist::Resolved(_, it) => it, | ||
147 | }; | ||
148 | |||
149 | let mut actual = action.edit.apply(&before); | ||
150 | if let Some(pos) = action.cursor_position { | ||
151 | actual = add_cursor(&actual, pos); | ||
152 | } | ||
153 | assert_eq_text!(after, &actual); | ||
154 | } | ||
155 | |||
156 | pub(crate) fn check_assist_not_applicable( | ||
157 | assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>, | ||
158 | before: &str, | ||
159 | ) { | ||
160 | let (before_cursor_pos, before) = extract_offset(before); | ||
161 | let (db, _source_root, file_id) = MockDatabase::with_single_file(&before); | ||
162 | let frange = FileRange { | ||
163 | file_id, | ||
164 | range: TextRange::offset_len(before_cursor_pos, 0.into()), | ||
165 | }; | ||
166 | let assist = AssistCtx::with_ctx(&db, frange, true, assist); | ||
167 | assert!(assist.is_none()); | ||
168 | } | ||
169 | |||
170 | } | ||
diff --git a/crates/ra_assists/src/replace_if_let_with_match.rs b/crates/ra_assists/src/replace_if_let_with_match.rs new file mode 100644 index 000000000..f6af47ec9 --- /dev/null +++ b/crates/ra_assists/src/replace_if_let_with_match.rs | |||
@@ -0,0 +1,80 @@ | |||
1 | use ra_syntax::{AstNode, ast}; | ||
2 | use ra_ide_api_light::formatting::extract_trivial_expression; | ||
3 | use hir::db::HirDatabase; | ||
4 | |||
5 | use crate::{AssistCtx, Assist}; | ||
6 | |||
7 | pub(crate) fn replace_if_let_with_match(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | ||
8 | let if_expr: &ast::IfExpr = ctx.node_at_offset()?; | ||
9 | let cond = if_expr.condition()?; | ||
10 | let pat = cond.pat()?; | ||
11 | let expr = cond.expr()?; | ||
12 | let then_block = if_expr.then_branch()?; | ||
13 | let else_block = match if_expr.else_branch()? { | ||
14 | ast::ElseBranchFlavor::Block(it) => it, | ||
15 | ast::ElseBranchFlavor::IfExpr(_) => return None, | ||
16 | }; | ||
17 | |||
18 | ctx.build("replace with match", |edit| { | ||
19 | let match_expr = build_match_expr(expr, pat, then_block, else_block); | ||
20 | edit.replace_node_and_indent(if_expr.syntax(), match_expr); | ||
21 | edit.set_cursor(if_expr.syntax().range().start()) | ||
22 | }) | ||
23 | } | ||
24 | |||
25 | fn build_match_expr( | ||
26 | expr: &ast::Expr, | ||
27 | pat1: &ast::Pat, | ||
28 | arm1: &ast::Block, | ||
29 | arm2: &ast::Block, | ||
30 | ) -> String { | ||
31 | let mut buf = String::new(); | ||
32 | buf.push_str(&format!("match {} {{\n", expr.syntax().text())); | ||
33 | buf.push_str(&format!( | ||
34 | " {} => {}\n", | ||
35 | pat1.syntax().text(), | ||
36 | format_arm(arm1) | ||
37 | )); | ||
38 | buf.push_str(&format!(" _ => {}\n", format_arm(arm2))); | ||
39 | buf.push_str("}"); | ||
40 | buf | ||
41 | } | ||
42 | |||
43 | fn format_arm(block: &ast::Block) -> String { | ||
44 | match extract_trivial_expression(block) { | ||
45 | None => block.syntax().text().to_string(), | ||
46 | Some(e) => format!("{},", e.syntax().text()), | ||
47 | } | ||
48 | } | ||
49 | |||
50 | #[cfg(test)] | ||
51 | mod tests { | ||
52 | use super::*; | ||
53 | use crate::helpers::check_assist; | ||
54 | |||
55 | #[test] | ||
56 | fn test_replace_if_let_with_match_unwraps_simple_expressions() { | ||
57 | check_assist( | ||
58 | replace_if_let_with_match, | ||
59 | " | ||
60 | impl VariantData { | ||
61 | pub fn is_struct(&self) -> bool { | ||
62 | if <|>let VariantData::Struct(..) = *self { | ||
63 | true | ||
64 | } else { | ||
65 | false | ||
66 | } | ||
67 | } | ||
68 | } ", | ||
69 | " | ||
70 | impl VariantData { | ||
71 | pub fn is_struct(&self) -> bool { | ||
72 | <|>match *self { | ||
73 | VariantData::Struct(..) => true, | ||
74 | _ => false, | ||
75 | } | ||
76 | } | ||
77 | } ", | ||
78 | ) | ||
79 | } | ||
80 | } | ||
diff --git a/crates/ra_assists/src/split_import.rs b/crates/ra_assists/src/split_import.rs new file mode 100644 index 000000000..7e34be087 --- /dev/null +++ b/crates/ra_assists/src/split_import.rs | |||
@@ -0,0 +1,57 @@ | |||
1 | use hir::db::HirDatabase; | ||
2 | use ra_syntax::{ | ||
3 | TextUnit, AstNode, SyntaxKind::COLONCOLON, | ||
4 | ast, | ||
5 | algo::generate, | ||
6 | }; | ||
7 | |||
8 | use crate::{AssistCtx, Assist}; | ||
9 | |||
10 | pub(crate) fn split_import(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | ||
11 | let colon_colon = ctx | ||
12 | .leaf_at_offset() | ||
13 | .find(|leaf| leaf.kind() == COLONCOLON)?; | ||
14 | let path = colon_colon.parent().and_then(ast::Path::cast)?; | ||
15 | let top_path = generate(Some(path), |it| it.parent_path()).last()?; | ||
16 | |||
17 | let use_tree = top_path.syntax().ancestors().find_map(ast::UseTree::cast); | ||
18 | if use_tree.is_none() { | ||
19 | return None; | ||
20 | } | ||
21 | |||
22 | let l_curly = colon_colon.range().end(); | ||
23 | let r_curly = match top_path.syntax().parent().and_then(ast::UseTree::cast) { | ||
24 | Some(tree) => tree.syntax().range().end(), | ||
25 | None => top_path.syntax().range().end(), | ||
26 | }; | ||
27 | |||
28 | ctx.build("split import", |edit| { | ||
29 | edit.insert(l_curly, "{"); | ||
30 | edit.insert(r_curly, "}"); | ||
31 | edit.set_cursor(l_curly + TextUnit::of_str("{")); | ||
32 | }) | ||
33 | } | ||
34 | |||
35 | #[cfg(test)] | ||
36 | mod tests { | ||
37 | use super::*; | ||
38 | use crate::helpers::check_assist; | ||
39 | |||
40 | #[test] | ||
41 | fn test_split_import() { | ||
42 | check_assist( | ||
43 | split_import, | ||
44 | "use crate::<|>db::RootDatabase;", | ||
45 | "use crate::{<|>db::RootDatabase};", | ||
46 | ) | ||
47 | } | ||
48 | |||
49 | #[test] | ||
50 | fn split_import_works_with_trees() { | ||
51 | check_assist( | ||
52 | split_import, | ||
53 | "use algo:<|>:visitor::{Visitor, visit}", | ||
54 | "use algo::{<|>visitor::{Visitor, visit}}", | ||
55 | ) | ||
56 | } | ||
57 | } | ||