aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_assists/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_assists/src')
-rw-r--r--crates/ra_assists/src/add_derive.rs85
-rw-r--r--crates/ra_assists/src/add_impl.rs67
-rw-r--r--crates/ra_assists/src/assist_ctx.rs154
-rw-r--r--crates/ra_assists/src/change_visibility.rs166
-rw-r--r--crates/ra_assists/src/fill_match_arms.rs145
-rw-r--r--crates/ra_assists/src/flip_comma.rs33
-rw-r--r--crates/ra_assists/src/introduce_variable.rs432
-rw-r--r--crates/ra_assists/src/lib.rs170
-rw-r--r--crates/ra_assists/src/replace_if_let_with_match.rs80
-rw-r--r--crates/ra_assists/src/split_import.rs57
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 @@
1use hir::db::HirDatabase;
2use ra_syntax::{
3 ast::{self, AstNode, AttrsOwner},
4 SyntaxKind::{WHITESPACE, COMMENT},
5 TextUnit,
6};
7
8use crate::{AssistCtx, Assist};
9
10pub(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.
32fn 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)]
41mod 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.
75struct Foo { a: i32<|>, }
76 ",
77 "
78/// `Foo` is a pretty important struct.
79/// It does stuff.
80#[derive(<|>)]
81struct 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 @@
1use join_to_string::join;
2use hir::db::HirDatabase;
3use ra_syntax::{
4 ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner},
5 TextUnit,
6};
7
8use crate::{AssistCtx, Assist};
9
10pub(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)]
44mod 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 @@
1use hir::db::HirDatabase;
2use ra_text_edit::TextEditBuilder;
3use ra_db::FileRange;
4use ra_syntax::{
5 SourceFile, TextRange, AstNode, TextUnit, SyntaxNode,
6 algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset},
7};
8use ra_ide_api_light::formatting::{leading_indent, reindent};
9
10use crate::{AssistLabel, AssistAction};
11
12pub(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)]
48pub(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
55impl<'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
66impl<'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)]
113pub(crate) struct AssistBuilder {
114 edit: TextEditBuilder,
115 cursor_position: Option<TextUnit>,
116}
117
118impl 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 @@
1use hir::db::HirDatabase;
2use 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
8use crate::{AssistCtx, Assist};
9
10pub(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
17fn 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
50fn 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
61fn 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)]
78mod 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 @@
1use std::fmt::Write;
2
3use hir::{
4 AdtDef, Ty, FieldSource, source_binder,
5 db::HirDatabase,
6};
7use ra_syntax::ast::{self, AstNode};
8
9use crate::{AssistCtx, Assist};
10
11pub(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)]
77mod 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 @@
1use hir::db::HirDatabase;
2use ra_syntax::{
3 Direction,
4 SyntaxKind::COMMA,
5};
6
7use crate::{AssistCtx, Assist, non_trivia_sibling};
8
9pub(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)]
20mod 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 @@
1use hir::db::HirDatabase;
2use 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
9use crate::{AssistCtx, Assist};
10
11pub(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
58fn 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.
63fn 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.
78fn 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)]
105mod 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 "
114fn foo() {
115 foo(<|>1 + 1<|>);
116}",
117 "
118fn 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 "
130fn foo() {
131 <|>1 + 1<|>;
132}",
133 "
134fn 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 "
145fn foo() {
146 <|>1<|> + 1;
147}",
148 "
149fn 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 "
161fn foo() {
162 bar(<|>1 + 1<|>)
163}",
164 "
165fn 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 "
177fn foo() {
178 <|>bar(1 + 1)<|>
179}",
180 "
181fn 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 "
193fn foo() {
194 <|>{ let x = 0; x }<|>
195 something_else();
196}",
197 "
198fn 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 "
210fn main() {
211 let x = true;
212 let tuple = match x {
213 true => (<|>2 + 2<|>, true)
214 _ => (0, false)
215 };
216}
217",
218 "
219fn 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 "
235fn 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 "
247fn 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 "
267fn main() {
268 let lambda = |x: u32| <|>x * 2<|>;
269}
270",
271 "
272fn 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 "
284fn main() {
285 let lambda = |x: u32| { <|>x * 2<|> };
286}
287",
288 "
289fn 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 "
301fn main() {
302 let o = S<|>ome(true);
303}
304",
305 "
306fn 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 "
319fn main() {
320 let v = b<|>ar.foo();
321}
322",
323 "
324fn 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 "
337fn foo() -> u32 {
338 r<|>eturn 2 + 2;
339}
340",
341 "
342fn 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 "
355fn main() {
356 let result = loop {
357 b<|>reak 2 + 2;
358 };
359}
360",
361 "
362fn 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 "
377fn main() {
378 let v = 0f32 a<|>s u32;
379}
380",
381 "
382fn 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 "
395fn 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 "
407fn 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 "
421fn 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
8mod assist_ctx;
9
10use ra_text_edit::TextEdit;
11use ra_syntax::{TextUnit, SyntaxNode, Direction};
12use ra_db::FileRange;
13use hir::db::HirDatabase;
14
15pub(crate) use crate::assist_ctx::{AssistCtx, Assist};
16
17#[derive(Debug)]
18pub struct AssistLabel {
19 /// Short description of the assist, as shown in the UI.
20 pub label: String,
21}
22
23pub 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.
32pub fn applicable_assists<H>(db: &H, range: FileRange) -> Vec<AssistLabel>
33where
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.
52pub fn assists<H>(db: &H, range: FileRange) -> Vec<(AssistLabel, AssistAction)>
53where
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
68mod add_derive;
69mod add_impl;
70mod flip_comma;
71mod change_visibility;
72mod fill_match_arms;
73mod introduce_variable;
74mod replace_if_let_with_match;
75mod split_import;
76fn 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
89fn 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)]
96mod 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 @@
1use ra_syntax::{AstNode, ast};
2use ra_ide_api_light::formatting::extract_trivial_expression;
3use hir::db::HirDatabase;
4
5use crate::{AssistCtx, Assist};
6
7pub(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
25fn 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
43fn 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)]
51mod 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 "
60impl VariantData {
61 pub fn is_struct(&self) -> bool {
62 if <|>let VariantData::Struct(..) = *self {
63 true
64 } else {
65 false
66 }
67 }
68} ",
69 "
70impl 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 @@
1use hir::db::HirDatabase;
2use ra_syntax::{
3 TextUnit, AstNode, SyntaxKind::COLONCOLON,
4 ast,
5 algo::generate,
6};
7
8use crate::{AssistCtx, Assist};
9
10pub(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)]
36mod 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}