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