diff options
Diffstat (limited to 'crates/ra_editor/src')
-rw-r--r-- | crates/ra_editor/src/assists.rs | 34 | ||||
-rw-r--r-- | crates/ra_editor/src/assists/add_derive.rs | 97 | ||||
-rw-r--r-- | crates/ra_editor/src/assists/add_impl.rs | 78 | ||||
-rw-r--r-- | crates/ra_editor/src/assists/change_visibility.rs | 90 | ||||
-rw-r--r-- | crates/ra_editor/src/assists/flip_comma.rs | 45 | ||||
-rw-r--r-- | crates/ra_editor/src/assists/introduce_variable.rs | 156 | ||||
-rw-r--r-- | crates/ra_editor/src/code_actions.rs | 415 | ||||
-rw-r--r-- | crates/ra_editor/src/lib.rs | 6 |
8 files changed, 503 insertions, 418 deletions
diff --git a/crates/ra_editor/src/assists.rs b/crates/ra_editor/src/assists.rs new file mode 100644 index 000000000..b6e6dd628 --- /dev/null +++ b/crates/ra_editor/src/assists.rs | |||
@@ -0,0 +1,34 @@ | |||
1 | //! This modules contains various "assits": suggestions for source code edits | ||
2 | //! which are likely to occur at a given cursor positon. For example, if the | ||
3 | //! cursor is on the `,`, a possible assist is swapping the elments around the | ||
4 | //! comma. | ||
5 | |||
6 | mod flip_comma; | ||
7 | mod add_derive; | ||
8 | mod add_impl; | ||
9 | mod introduce_variable; | ||
10 | mod change_visibility; | ||
11 | |||
12 | use ra_text_edit::TextEdit; | ||
13 | use ra_syntax::{Direction, SyntaxNodeRef, TextUnit}; | ||
14 | |||
15 | pub use self::{ | ||
16 | flip_comma::flip_comma, | ||
17 | add_derive::add_derive, | ||
18 | add_impl::add_impl, | ||
19 | introduce_variable::introduce_variable, | ||
20 | change_visibility::change_visibility, | ||
21 | }; | ||
22 | |||
23 | #[derive(Debug)] | ||
24 | pub struct LocalEdit { | ||
25 | pub label: String, | ||
26 | pub edit: TextEdit, | ||
27 | pub cursor_position: Option<TextUnit>, | ||
28 | } | ||
29 | |||
30 | fn non_trivia_sibling(node: SyntaxNodeRef, direction: Direction) -> Option<SyntaxNodeRef> { | ||
31 | node.siblings(direction) | ||
32 | .skip(1) | ||
33 | .find(|node| !node.kind().is_trivia()) | ||
34 | } | ||
diff --git a/crates/ra_editor/src/assists/add_derive.rs b/crates/ra_editor/src/assists/add_derive.rs new file mode 100644 index 000000000..33d9d2c31 --- /dev/null +++ b/crates/ra_editor/src/assists/add_derive.rs | |||
@@ -0,0 +1,97 @@ | |||
1 | use ra_text_edit::TextEditBuilder; | ||
2 | use ra_syntax::{ | ||
3 | ast::{self, AstNode, AttrsOwner}, | ||
4 | SourceFileNode, | ||
5 | SyntaxKind::{WHITESPACE, COMMENT}, | ||
6 | TextUnit, | ||
7 | }; | ||
8 | |||
9 | use crate::{ | ||
10 | find_node_at_offset, | ||
11 | assists::LocalEdit, | ||
12 | }; | ||
13 | |||
14 | pub fn add_derive<'a>( | ||
15 | file: &'a SourceFileNode, | ||
16 | offset: TextUnit, | ||
17 | ) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
18 | let nominal = find_node_at_offset::<ast::NominalDef>(file.syntax(), offset)?; | ||
19 | let node_start = derive_insertion_offset(nominal)?; | ||
20 | return Some(move || { | ||
21 | let derive_attr = nominal | ||
22 | .attrs() | ||
23 | .filter_map(|x| x.as_call()) | ||
24 | .filter(|(name, _arg)| name == "derive") | ||
25 | .map(|(_name, arg)| arg) | ||
26 | .next(); | ||
27 | let mut edit = TextEditBuilder::new(); | ||
28 | let offset = match derive_attr { | ||
29 | None => { | ||
30 | edit.insert(node_start, "#[derive()]\n".to_string()); | ||
31 | node_start + TextUnit::of_str("#[derive(") | ||
32 | } | ||
33 | Some(tt) => tt.syntax().range().end() - TextUnit::of_char(')'), | ||
34 | }; | ||
35 | LocalEdit { | ||
36 | label: "add `#[derive]`".to_string(), | ||
37 | edit: edit.finish(), | ||
38 | cursor_position: Some(offset), | ||
39 | } | ||
40 | }); | ||
41 | |||
42 | // Insert `derive` after doc comments. | ||
43 | fn derive_insertion_offset(nominal: ast::NominalDef) -> Option<TextUnit> { | ||
44 | let non_ws_child = nominal | ||
45 | .syntax() | ||
46 | .children() | ||
47 | .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?; | ||
48 | Some(non_ws_child.range().start()) | ||
49 | } | ||
50 | } | ||
51 | |||
52 | #[cfg(test)] | ||
53 | mod tests { | ||
54 | use super::*; | ||
55 | use crate::test_utils::check_action; | ||
56 | |||
57 | #[test] | ||
58 | fn add_derive_new() { | ||
59 | check_action( | ||
60 | "struct Foo { a: i32, <|>}", | ||
61 | "#[derive(<|>)]\nstruct Foo { a: i32, }", | ||
62 | |file, off| add_derive(file, off).map(|f| f()), | ||
63 | ); | ||
64 | check_action( | ||
65 | "struct Foo { <|> a: i32, }", | ||
66 | "#[derive(<|>)]\nstruct Foo { a: i32, }", | ||
67 | |file, off| add_derive(file, off).map(|f| f()), | ||
68 | ); | ||
69 | } | ||
70 | |||
71 | #[test] | ||
72 | fn add_derive_existing() { | ||
73 | check_action( | ||
74 | "#[derive(Clone)]\nstruct Foo { a: i32<|>, }", | ||
75 | "#[derive(Clone<|>)]\nstruct Foo { a: i32, }", | ||
76 | |file, off| add_derive(file, off).map(|f| f()), | ||
77 | ); | ||
78 | } | ||
79 | |||
80 | #[test] | ||
81 | fn add_derive_new_with_doc_comment() { | ||
82 | check_action( | ||
83 | " | ||
84 | /// `Foo` is a pretty important struct. | ||
85 | /// It does stuff. | ||
86 | struct Foo { a: i32<|>, } | ||
87 | ", | ||
88 | " | ||
89 | /// `Foo` is a pretty important struct. | ||
90 | /// It does stuff. | ||
91 | #[derive(<|>)] | ||
92 | struct Foo { a: i32, } | ||
93 | ", | ||
94 | |file, off| add_derive(file, off).map(|f| f()), | ||
95 | ); | ||
96 | } | ||
97 | } | ||
diff --git a/crates/ra_editor/src/assists/add_impl.rs b/crates/ra_editor/src/assists/add_impl.rs new file mode 100644 index 000000000..50e00688e --- /dev/null +++ b/crates/ra_editor/src/assists/add_impl.rs | |||
@@ -0,0 +1,78 @@ | |||
1 | use join_to_string::join; | ||
2 | use ra_text_edit::TextEditBuilder; | ||
3 | use ra_syntax::{ | ||
4 | ast::{self, AstNode, NameOwner, TypeParamsOwner}, | ||
5 | SourceFileNode, | ||
6 | TextUnit, | ||
7 | }; | ||
8 | |||
9 | use crate::{find_node_at_offset, assists::LocalEdit}; | ||
10 | |||
11 | pub fn add_impl<'a>( | ||
12 | file: &'a SourceFileNode, | ||
13 | offset: TextUnit, | ||
14 | ) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
15 | let nominal = find_node_at_offset::<ast::NominalDef>(file.syntax(), offset)?; | ||
16 | let name = nominal.name()?; | ||
17 | |||
18 | Some(move || { | ||
19 | let type_params = nominal.type_param_list(); | ||
20 | let mut edit = TextEditBuilder::new(); | ||
21 | let start_offset = nominal.syntax().range().end(); | ||
22 | let mut buf = String::new(); | ||
23 | buf.push_str("\n\nimpl"); | ||
24 | if let Some(type_params) = type_params { | ||
25 | type_params.syntax().text().push_to(&mut buf); | ||
26 | } | ||
27 | buf.push_str(" "); | ||
28 | buf.push_str(name.text().as_str()); | ||
29 | if let Some(type_params) = type_params { | ||
30 | let lifetime_params = type_params | ||
31 | .lifetime_params() | ||
32 | .filter_map(|it| it.lifetime()) | ||
33 | .map(|it| it.text()); | ||
34 | let type_params = type_params | ||
35 | .type_params() | ||
36 | .filter_map(|it| it.name()) | ||
37 | .map(|it| it.text()); | ||
38 | join(lifetime_params.chain(type_params)) | ||
39 | .surround_with("<", ">") | ||
40 | .to_buf(&mut buf); | ||
41 | } | ||
42 | buf.push_str(" {\n"); | ||
43 | let offset = start_offset + TextUnit::of_str(&buf); | ||
44 | buf.push_str("\n}"); | ||
45 | edit.insert(start_offset, buf); | ||
46 | LocalEdit { | ||
47 | label: "add impl".to_string(), | ||
48 | edit: edit.finish(), | ||
49 | cursor_position: Some(offset), | ||
50 | } | ||
51 | }) | ||
52 | } | ||
53 | |||
54 | #[cfg(test)] | ||
55 | mod tests { | ||
56 | use super::*; | ||
57 | use crate::test_utils::check_action; | ||
58 | |||
59 | #[test] | ||
60 | fn test_add_impl() { | ||
61 | check_action( | ||
62 | "struct Foo {<|>}\n", | ||
63 | "struct Foo {}\n\nimpl Foo {\n<|>\n}\n", | ||
64 | |file, off| add_impl(file, off).map(|f| f()), | ||
65 | ); | ||
66 | check_action( | ||
67 | "struct Foo<T: Clone> {<|>}", | ||
68 | "struct Foo<T: Clone> {}\n\nimpl<T: Clone> Foo<T> {\n<|>\n}", | ||
69 | |file, off| add_impl(file, off).map(|f| f()), | ||
70 | ); | ||
71 | check_action( | ||
72 | "struct Foo<'a, T: Foo<'a>> {<|>}", | ||
73 | "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}", | ||
74 | |file, off| add_impl(file, off).map(|f| f()), | ||
75 | ); | ||
76 | } | ||
77 | |||
78 | } | ||
diff --git a/crates/ra_editor/src/assists/change_visibility.rs b/crates/ra_editor/src/assists/change_visibility.rs new file mode 100644 index 000000000..98c218f32 --- /dev/null +++ b/crates/ra_editor/src/assists/change_visibility.rs | |||
@@ -0,0 +1,90 @@ | |||
1 | use ra_text_edit::TextEditBuilder; | ||
2 | use ra_syntax::{ | ||
3 | SourceFileNode, | ||
4 | algo::find_leaf_at_offset, | ||
5 | SyntaxKind::{VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF}, | ||
6 | TextUnit, | ||
7 | }; | ||
8 | |||
9 | use crate::assists::LocalEdit; | ||
10 | |||
11 | pub fn change_visibility<'a>( | ||
12 | file: &'a SourceFileNode, | ||
13 | offset: TextUnit, | ||
14 | ) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
15 | let syntax = file.syntax(); | ||
16 | |||
17 | let keyword = find_leaf_at_offset(syntax, offset).find(|leaf| match leaf.kind() { | ||
18 | FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true, | ||
19 | _ => false, | ||
20 | })?; | ||
21 | let parent = keyword.parent()?; | ||
22 | let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF]; | ||
23 | let node_start = parent.range().start(); | ||
24 | Some(move || { | ||
25 | let mut edit = TextEditBuilder::new(); | ||
26 | |||
27 | if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) | ||
28 | || parent.children().any(|child| child.kind() == VISIBILITY) | ||
29 | { | ||
30 | return LocalEdit { | ||
31 | label: "make pub crate".to_string(), | ||
32 | edit: edit.finish(), | ||
33 | cursor_position: Some(offset), | ||
34 | }; | ||
35 | } | ||
36 | |||
37 | edit.insert(node_start, "pub(crate) ".to_string()); | ||
38 | LocalEdit { | ||
39 | label: "make pub crate".to_string(), | ||
40 | edit: edit.finish(), | ||
41 | cursor_position: Some(node_start), | ||
42 | } | ||
43 | }) | ||
44 | } | ||
45 | |||
46 | #[cfg(test)] | ||
47 | mod tests { | ||
48 | use super::*; | ||
49 | use crate::test_utils::check_action; | ||
50 | |||
51 | #[test] | ||
52 | fn test_change_visibility() { | ||
53 | check_action( | ||
54 | "<|>fn foo() {}", | ||
55 | "<|>pub(crate) fn foo() {}", | ||
56 | |file, off| change_visibility(file, off).map(|f| f()), | ||
57 | ); | ||
58 | check_action( | ||
59 | "f<|>n foo() {}", | ||
60 | "<|>pub(crate) fn foo() {}", | ||
61 | |file, off| change_visibility(file, off).map(|f| f()), | ||
62 | ); | ||
63 | check_action( | ||
64 | "<|>struct Foo {}", | ||
65 | "<|>pub(crate) struct Foo {}", | ||
66 | |file, off| change_visibility(file, off).map(|f| f()), | ||
67 | ); | ||
68 | check_action("<|>mod foo {}", "<|>pub(crate) mod foo {}", |file, off| { | ||
69 | change_visibility(file, off).map(|f| f()) | ||
70 | }); | ||
71 | check_action( | ||
72 | "<|>trait Foo {}", | ||
73 | "<|>pub(crate) trait Foo {}", | ||
74 | |file, off| change_visibility(file, off).map(|f| f()), | ||
75 | ); | ||
76 | check_action("m<|>od {}", "<|>pub(crate) mod {}", |file, off| { | ||
77 | change_visibility(file, off).map(|f| f()) | ||
78 | }); | ||
79 | check_action( | ||
80 | "pub(crate) f<|>n foo() {}", | ||
81 | "pub(crate) f<|>n foo() {}", | ||
82 | |file, off| change_visibility(file, off).map(|f| f()), | ||
83 | ); | ||
84 | check_action( | ||
85 | "unsafe f<|>n foo() {}", | ||
86 | "<|>pub(crate) unsafe fn foo() {}", | ||
87 | |file, off| change_visibility(file, off).map(|f| f()), | ||
88 | ); | ||
89 | } | ||
90 | } | ||
diff --git a/crates/ra_editor/src/assists/flip_comma.rs b/crates/ra_editor/src/assists/flip_comma.rs new file mode 100644 index 000000000..d8727db0d --- /dev/null +++ b/crates/ra_editor/src/assists/flip_comma.rs | |||
@@ -0,0 +1,45 @@ | |||
1 | use ra_text_edit::TextEditBuilder; | ||
2 | use ra_syntax::{ | ||
3 | algo::find_leaf_at_offset, | ||
4 | Direction, SourceFileNode, | ||
5 | SyntaxKind::COMMA, | ||
6 | TextUnit, | ||
7 | }; | ||
8 | |||
9 | use crate::assists::{LocalEdit, non_trivia_sibling}; | ||
10 | |||
11 | pub fn flip_comma<'a>( | ||
12 | file: &'a SourceFileNode, | ||
13 | offset: TextUnit, | ||
14 | ) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
15 | let syntax = file.syntax(); | ||
16 | |||
17 | let comma = find_leaf_at_offset(syntax, offset).find(|leaf| leaf.kind() == COMMA)?; | ||
18 | let prev = non_trivia_sibling(comma, Direction::Prev)?; | ||
19 | let next = non_trivia_sibling(comma, Direction::Next)?; | ||
20 | Some(move || { | ||
21 | let mut edit = TextEditBuilder::new(); | ||
22 | edit.replace(prev.range(), next.text().to_string()); | ||
23 | edit.replace(next.range(), prev.text().to_string()); | ||
24 | LocalEdit { | ||
25 | label: "flip comma".to_string(), | ||
26 | edit: edit.finish(), | ||
27 | cursor_position: None, | ||
28 | } | ||
29 | }) | ||
30 | } | ||
31 | |||
32 | #[cfg(test)] | ||
33 | mod tests { | ||
34 | use super::*; | ||
35 | use crate::test_utils::check_action; | ||
36 | |||
37 | #[test] | ||
38 | fn test_swap_comma() { | ||
39 | check_action( | ||
40 | "fn foo(x: i32,<|> y: Result<(), ()>) {}", | ||
41 | "fn foo(y: Result<(), ()>,<|> x: i32) {}", | ||
42 | |file, off| flip_comma(file, off).map(|f| f()), | ||
43 | ) | ||
44 | } | ||
45 | } | ||
diff --git a/crates/ra_editor/src/assists/introduce_variable.rs b/crates/ra_editor/src/assists/introduce_variable.rs new file mode 100644 index 000000000..17ab521fa --- /dev/null +++ b/crates/ra_editor/src/assists/introduce_variable.rs | |||
@@ -0,0 +1,156 @@ | |||
1 | use ra_text_edit::TextEditBuilder; | ||
2 | use ra_syntax::{ | ||
3 | algo::{find_covering_node}, | ||
4 | ast::{self, AstNode}, | ||
5 | SourceFileNode, | ||
6 | SyntaxKind::{WHITESPACE}, | ||
7 | SyntaxNodeRef, TextRange, TextUnit, | ||
8 | }; | ||
9 | |||
10 | use crate::assists::LocalEdit; | ||
11 | |||
12 | pub fn introduce_variable<'a>( | ||
13 | file: &'a SourceFileNode, | ||
14 | range: TextRange, | ||
15 | ) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
16 | let node = find_covering_node(file.syntax(), range); | ||
17 | let expr = node.ancestors().filter_map(ast::Expr::cast).next()?; | ||
18 | |||
19 | let anchor_stmt = anchor_stmt(expr)?; | ||
20 | let indent = anchor_stmt.prev_sibling()?; | ||
21 | if indent.kind() != WHITESPACE { | ||
22 | return None; | ||
23 | } | ||
24 | return Some(move || { | ||
25 | let mut buf = String::new(); | ||
26 | let mut edit = TextEditBuilder::new(); | ||
27 | |||
28 | buf.push_str("let var_name = "); | ||
29 | expr.syntax().text().push_to(&mut buf); | ||
30 | let is_full_stmt = if let Some(expr_stmt) = ast::ExprStmt::cast(anchor_stmt) { | ||
31 | Some(expr.syntax()) == expr_stmt.expr().map(|e| e.syntax()) | ||
32 | } else { | ||
33 | false | ||
34 | }; | ||
35 | if is_full_stmt { | ||
36 | edit.replace(expr.syntax().range(), buf); | ||
37 | } else { | ||
38 | buf.push_str(";"); | ||
39 | indent.text().push_to(&mut buf); | ||
40 | edit.replace(expr.syntax().range(), "var_name".to_string()); | ||
41 | edit.insert(anchor_stmt.range().start(), buf); | ||
42 | } | ||
43 | let cursor_position = anchor_stmt.range().start() + TextUnit::of_str("let "); | ||
44 | LocalEdit { | ||
45 | label: "introduce variable".to_string(), | ||
46 | edit: edit.finish(), | ||
47 | cursor_position: Some(cursor_position), | ||
48 | } | ||
49 | }); | ||
50 | |||
51 | /// Statement or last in the block expression, which will follow | ||
52 | /// the freshly introduced var. | ||
53 | fn anchor_stmt(expr: ast::Expr) -> Option<SyntaxNodeRef> { | ||
54 | expr.syntax().ancestors().find(|&node| { | ||
55 | if ast::Stmt::cast(node).is_some() { | ||
56 | return true; | ||
57 | } | ||
58 | if let Some(expr) = node | ||
59 | .parent() | ||
60 | .and_then(ast::Block::cast) | ||
61 | .and_then(|it| it.expr()) | ||
62 | { | ||
63 | if expr.syntax() == node { | ||
64 | return true; | ||
65 | } | ||
66 | } | ||
67 | false | ||
68 | }) | ||
69 | } | ||
70 | } | ||
71 | |||
72 | #[cfg(test)] | ||
73 | mod tests { | ||
74 | use super::*; | ||
75 | use crate::test_utils::check_action_range; | ||
76 | |||
77 | #[test] | ||
78 | fn test_introduce_var_simple() { | ||
79 | check_action_range( | ||
80 | " | ||
81 | fn foo() { | ||
82 | foo(<|>1 + 1<|>); | ||
83 | }", | ||
84 | " | ||
85 | fn foo() { | ||
86 | let <|>var_name = 1 + 1; | ||
87 | foo(var_name); | ||
88 | }", | ||
89 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
90 | ); | ||
91 | } | ||
92 | |||
93 | #[test] | ||
94 | fn test_introduce_var_expr_stmt() { | ||
95 | check_action_range( | ||
96 | " | ||
97 | fn foo() { | ||
98 | <|>1 + 1<|>; | ||
99 | }", | ||
100 | " | ||
101 | fn foo() { | ||
102 | let <|>var_name = 1 + 1; | ||
103 | }", | ||
104 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
105 | ); | ||
106 | } | ||
107 | |||
108 | #[test] | ||
109 | fn test_introduce_var_part_of_expr_stmt() { | ||
110 | check_action_range( | ||
111 | " | ||
112 | fn foo() { | ||
113 | <|>1<|> + 1; | ||
114 | }", | ||
115 | " | ||
116 | fn foo() { | ||
117 | let <|>var_name = 1; | ||
118 | var_name + 1; | ||
119 | }", | ||
120 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
121 | ); | ||
122 | } | ||
123 | |||
124 | #[test] | ||
125 | fn test_introduce_var_last_expr() { | ||
126 | check_action_range( | ||
127 | " | ||
128 | fn foo() { | ||
129 | bar(<|>1 + 1<|>) | ||
130 | }", | ||
131 | " | ||
132 | fn foo() { | ||
133 | let <|>var_name = 1 + 1; | ||
134 | bar(var_name) | ||
135 | }", | ||
136 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
137 | ); | ||
138 | } | ||
139 | |||
140 | #[test] | ||
141 | fn test_introduce_var_last_full_expr() { | ||
142 | check_action_range( | ||
143 | " | ||
144 | fn foo() { | ||
145 | <|>bar(1 + 1)<|> | ||
146 | }", | ||
147 | " | ||
148 | fn foo() { | ||
149 | let <|>var_name = bar(1 + 1); | ||
150 | var_name | ||
151 | }", | ||
152 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
153 | ); | ||
154 | } | ||
155 | |||
156 | } | ||
diff --git a/crates/ra_editor/src/code_actions.rs b/crates/ra_editor/src/code_actions.rs deleted file mode 100644 index 7615f37a6..000000000 --- a/crates/ra_editor/src/code_actions.rs +++ /dev/null | |||
@@ -1,415 +0,0 @@ | |||
1 | use join_to_string::join; | ||
2 | |||
3 | use ra_syntax::{ | ||
4 | algo::{find_covering_node, find_leaf_at_offset}, | ||
5 | ast::{self, AstNode, AttrsOwner, NameOwner, TypeParamsOwner}, | ||
6 | Direction, SourceFileNode, | ||
7 | SyntaxKind::{COMMA, WHITESPACE, COMMENT, VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF}, | ||
8 | SyntaxNodeRef, TextRange, TextUnit, | ||
9 | }; | ||
10 | |||
11 | use crate::{find_node_at_offset, TextEdit, TextEditBuilder}; | ||
12 | |||
13 | #[derive(Debug)] | ||
14 | pub struct LocalEdit { | ||
15 | pub label: String, | ||
16 | pub edit: TextEdit, | ||
17 | pub cursor_position: Option<TextUnit>, | ||
18 | } | ||
19 | |||
20 | pub fn flip_comma<'a>( | ||
21 | file: &'a SourceFileNode, | ||
22 | offset: TextUnit, | ||
23 | ) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
24 | let syntax = file.syntax(); | ||
25 | |||
26 | let comma = find_leaf_at_offset(syntax, offset).find(|leaf| leaf.kind() == COMMA)?; | ||
27 | let prev = non_trivia_sibling(comma, Direction::Prev)?; | ||
28 | let next = non_trivia_sibling(comma, Direction::Next)?; | ||
29 | Some(move || { | ||
30 | let mut edit = TextEditBuilder::new(); | ||
31 | edit.replace(prev.range(), next.text().to_string()); | ||
32 | edit.replace(next.range(), prev.text().to_string()); | ||
33 | LocalEdit { | ||
34 | label: "flip comma".to_string(), | ||
35 | edit: edit.finish(), | ||
36 | cursor_position: None, | ||
37 | } | ||
38 | }) | ||
39 | } | ||
40 | |||
41 | pub fn add_derive<'a>( | ||
42 | file: &'a SourceFileNode, | ||
43 | offset: TextUnit, | ||
44 | ) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
45 | let nominal = find_node_at_offset::<ast::NominalDef>(file.syntax(), offset)?; | ||
46 | let node_start = derive_insertion_offset(nominal)?; | ||
47 | return Some(move || { | ||
48 | let derive_attr = nominal | ||
49 | .attrs() | ||
50 | .filter_map(|x| x.as_call()) | ||
51 | .filter(|(name, _arg)| name == "derive") | ||
52 | .map(|(_name, arg)| arg) | ||
53 | .next(); | ||
54 | let mut edit = TextEditBuilder::new(); | ||
55 | let offset = match derive_attr { | ||
56 | None => { | ||
57 | edit.insert(node_start, "#[derive()]\n".to_string()); | ||
58 | node_start + TextUnit::of_str("#[derive(") | ||
59 | } | ||
60 | Some(tt) => tt.syntax().range().end() - TextUnit::of_char(')'), | ||
61 | }; | ||
62 | LocalEdit { | ||
63 | label: "add `#[derive]`".to_string(), | ||
64 | edit: edit.finish(), | ||
65 | cursor_position: Some(offset), | ||
66 | } | ||
67 | }); | ||
68 | |||
69 | // Insert `derive` after doc comments. | ||
70 | fn derive_insertion_offset(nominal: ast::NominalDef) -> Option<TextUnit> { | ||
71 | let non_ws_child = nominal | ||
72 | .syntax() | ||
73 | .children() | ||
74 | .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?; | ||
75 | Some(non_ws_child.range().start()) | ||
76 | } | ||
77 | } | ||
78 | |||
79 | pub fn add_impl<'a>( | ||
80 | file: &'a SourceFileNode, | ||
81 | offset: TextUnit, | ||
82 | ) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
83 | let nominal = find_node_at_offset::<ast::NominalDef>(file.syntax(), offset)?; | ||
84 | let name = nominal.name()?; | ||
85 | |||
86 | Some(move || { | ||
87 | let type_params = nominal.type_param_list(); | ||
88 | let mut edit = TextEditBuilder::new(); | ||
89 | let start_offset = nominal.syntax().range().end(); | ||
90 | let mut buf = String::new(); | ||
91 | buf.push_str("\n\nimpl"); | ||
92 | if let Some(type_params) = type_params { | ||
93 | type_params.syntax().text().push_to(&mut buf); | ||
94 | } | ||
95 | buf.push_str(" "); | ||
96 | buf.push_str(name.text().as_str()); | ||
97 | if let Some(type_params) = type_params { | ||
98 | let lifetime_params = type_params | ||
99 | .lifetime_params() | ||
100 | .filter_map(|it| it.lifetime()) | ||
101 | .map(|it| it.text()); | ||
102 | let type_params = type_params | ||
103 | .type_params() | ||
104 | .filter_map(|it| it.name()) | ||
105 | .map(|it| it.text()); | ||
106 | join(lifetime_params.chain(type_params)) | ||
107 | .surround_with("<", ">") | ||
108 | .to_buf(&mut buf); | ||
109 | } | ||
110 | buf.push_str(" {\n"); | ||
111 | let offset = start_offset + TextUnit::of_str(&buf); | ||
112 | buf.push_str("\n}"); | ||
113 | edit.insert(start_offset, buf); | ||
114 | LocalEdit { | ||
115 | label: "add impl".to_string(), | ||
116 | edit: edit.finish(), | ||
117 | cursor_position: Some(offset), | ||
118 | } | ||
119 | }) | ||
120 | } | ||
121 | |||
122 | pub fn introduce_variable<'a>( | ||
123 | file: &'a SourceFileNode, | ||
124 | range: TextRange, | ||
125 | ) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
126 | let node = find_covering_node(file.syntax(), range); | ||
127 | let expr = node.ancestors().filter_map(ast::Expr::cast).next()?; | ||
128 | |||
129 | let anchor_stmt = anchor_stmt(expr)?; | ||
130 | let indent = anchor_stmt.prev_sibling()?; | ||
131 | if indent.kind() != WHITESPACE { | ||
132 | return None; | ||
133 | } | ||
134 | return Some(move || { | ||
135 | let mut buf = String::new(); | ||
136 | let mut edit = TextEditBuilder::new(); | ||
137 | |||
138 | buf.push_str("let var_name = "); | ||
139 | expr.syntax().text().push_to(&mut buf); | ||
140 | let is_full_stmt = if let Some(expr_stmt) = ast::ExprStmt::cast(anchor_stmt) { | ||
141 | Some(expr.syntax()) == expr_stmt.expr().map(|e| e.syntax()) | ||
142 | } else { | ||
143 | false | ||
144 | }; | ||
145 | if is_full_stmt { | ||
146 | edit.replace(expr.syntax().range(), buf); | ||
147 | } else { | ||
148 | buf.push_str(";"); | ||
149 | indent.text().push_to(&mut buf); | ||
150 | edit.replace(expr.syntax().range(), "var_name".to_string()); | ||
151 | edit.insert(anchor_stmt.range().start(), buf); | ||
152 | } | ||
153 | let cursor_position = anchor_stmt.range().start() + TextUnit::of_str("let "); | ||
154 | LocalEdit { | ||
155 | label: "introduce variable".to_string(), | ||
156 | edit: edit.finish(), | ||
157 | cursor_position: Some(cursor_position), | ||
158 | } | ||
159 | }); | ||
160 | |||
161 | /// Statement or last in the block expression, which will follow | ||
162 | /// the freshly introduced var. | ||
163 | fn anchor_stmt(expr: ast::Expr) -> Option<SyntaxNodeRef> { | ||
164 | expr.syntax().ancestors().find(|&node| { | ||
165 | if ast::Stmt::cast(node).is_some() { | ||
166 | return true; | ||
167 | } | ||
168 | if let Some(expr) = node | ||
169 | .parent() | ||
170 | .and_then(ast::Block::cast) | ||
171 | .and_then(|it| it.expr()) | ||
172 | { | ||
173 | if expr.syntax() == node { | ||
174 | return true; | ||
175 | } | ||
176 | } | ||
177 | false | ||
178 | }) | ||
179 | } | ||
180 | } | ||
181 | |||
182 | pub fn make_pub_crate<'a>( | ||
183 | file: &'a SourceFileNode, | ||
184 | offset: TextUnit, | ||
185 | ) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
186 | let syntax = file.syntax(); | ||
187 | |||
188 | let keyword = find_leaf_at_offset(syntax, offset).find(|leaf| match leaf.kind() { | ||
189 | FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true, | ||
190 | _ => false, | ||
191 | })?; | ||
192 | let parent = keyword.parent()?; | ||
193 | let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF]; | ||
194 | let node_start = parent.range().start(); | ||
195 | Some(move || { | ||
196 | let mut edit = TextEditBuilder::new(); | ||
197 | |||
198 | if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) | ||
199 | || parent.children().any(|child| child.kind() == VISIBILITY) | ||
200 | { | ||
201 | return LocalEdit { | ||
202 | label: "make pub crate".to_string(), | ||
203 | edit: edit.finish(), | ||
204 | cursor_position: Some(offset), | ||
205 | }; | ||
206 | } | ||
207 | |||
208 | edit.insert(node_start, "pub(crate) ".to_string()); | ||
209 | LocalEdit { | ||
210 | label: "make pub crate".to_string(), | ||
211 | edit: edit.finish(), | ||
212 | cursor_position: Some(node_start), | ||
213 | } | ||
214 | }) | ||
215 | } | ||
216 | |||
217 | fn non_trivia_sibling(node: SyntaxNodeRef, direction: Direction) -> Option<SyntaxNodeRef> { | ||
218 | node.siblings(direction) | ||
219 | .skip(1) | ||
220 | .find(|node| !node.kind().is_trivia()) | ||
221 | } | ||
222 | |||
223 | #[cfg(test)] | ||
224 | mod tests { | ||
225 | use super::*; | ||
226 | use crate::test_utils::{check_action, check_action_range}; | ||
227 | |||
228 | #[test] | ||
229 | fn test_swap_comma() { | ||
230 | check_action( | ||
231 | "fn foo(x: i32,<|> y: Result<(), ()>) {}", | ||
232 | "fn foo(y: Result<(), ()>,<|> x: i32) {}", | ||
233 | |file, off| flip_comma(file, off).map(|f| f()), | ||
234 | ) | ||
235 | } | ||
236 | |||
237 | #[test] | ||
238 | fn add_derive_new() { | ||
239 | check_action( | ||
240 | "struct Foo { a: i32, <|>}", | ||
241 | "#[derive(<|>)]\nstruct Foo { a: i32, }", | ||
242 | |file, off| add_derive(file, off).map(|f| f()), | ||
243 | ); | ||
244 | check_action( | ||
245 | "struct Foo { <|> a: i32, }", | ||
246 | "#[derive(<|>)]\nstruct Foo { a: i32, }", | ||
247 | |file, off| add_derive(file, off).map(|f| f()), | ||
248 | ); | ||
249 | } | ||
250 | |||
251 | #[test] | ||
252 | fn add_derive_existing() { | ||
253 | check_action( | ||
254 | "#[derive(Clone)]\nstruct Foo { a: i32<|>, }", | ||
255 | "#[derive(Clone<|>)]\nstruct Foo { a: i32, }", | ||
256 | |file, off| add_derive(file, off).map(|f| f()), | ||
257 | ); | ||
258 | } | ||
259 | |||
260 | #[test] | ||
261 | fn add_derive_new_with_doc_comment() { | ||
262 | check_action( | ||
263 | " | ||
264 | /// `Foo` is a pretty important struct. | ||
265 | /// It does stuff. | ||
266 | struct Foo { a: i32<|>, } | ||
267 | ", | ||
268 | " | ||
269 | /// `Foo` is a pretty important struct. | ||
270 | /// It does stuff. | ||
271 | #[derive(<|>)] | ||
272 | struct Foo { a: i32, } | ||
273 | ", | ||
274 | |file, off| add_derive(file, off).map(|f| f()), | ||
275 | ); | ||
276 | } | ||
277 | |||
278 | #[test] | ||
279 | fn test_add_impl() { | ||
280 | check_action( | ||
281 | "struct Foo {<|>}\n", | ||
282 | "struct Foo {}\n\nimpl Foo {\n<|>\n}\n", | ||
283 | |file, off| add_impl(file, off).map(|f| f()), | ||
284 | ); | ||
285 | check_action( | ||
286 | "struct Foo<T: Clone> {<|>}", | ||
287 | "struct Foo<T: Clone> {}\n\nimpl<T: Clone> Foo<T> {\n<|>\n}", | ||
288 | |file, off| add_impl(file, off).map(|f| f()), | ||
289 | ); | ||
290 | check_action( | ||
291 | "struct Foo<'a, T: Foo<'a>> {<|>}", | ||
292 | "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}", | ||
293 | |file, off| add_impl(file, off).map(|f| f()), | ||
294 | ); | ||
295 | } | ||
296 | |||
297 | #[test] | ||
298 | fn test_introduce_var_simple() { | ||
299 | check_action_range( | ||
300 | " | ||
301 | fn foo() { | ||
302 | foo(<|>1 + 1<|>); | ||
303 | }", | ||
304 | " | ||
305 | fn foo() { | ||
306 | let <|>var_name = 1 + 1; | ||
307 | foo(var_name); | ||
308 | }", | ||
309 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
310 | ); | ||
311 | } | ||
312 | |||
313 | #[test] | ||
314 | fn test_introduce_var_expr_stmt() { | ||
315 | check_action_range( | ||
316 | " | ||
317 | fn foo() { | ||
318 | <|>1 + 1<|>; | ||
319 | }", | ||
320 | " | ||
321 | fn foo() { | ||
322 | let <|>var_name = 1 + 1; | ||
323 | }", | ||
324 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
325 | ); | ||
326 | } | ||
327 | |||
328 | #[test] | ||
329 | fn test_introduce_var_part_of_expr_stmt() { | ||
330 | check_action_range( | ||
331 | " | ||
332 | fn foo() { | ||
333 | <|>1<|> + 1; | ||
334 | }", | ||
335 | " | ||
336 | fn foo() { | ||
337 | let <|>var_name = 1; | ||
338 | var_name + 1; | ||
339 | }", | ||
340 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
341 | ); | ||
342 | } | ||
343 | |||
344 | #[test] | ||
345 | fn test_introduce_var_last_expr() { | ||
346 | check_action_range( | ||
347 | " | ||
348 | fn foo() { | ||
349 | bar(<|>1 + 1<|>) | ||
350 | }", | ||
351 | " | ||
352 | fn foo() { | ||
353 | let <|>var_name = 1 + 1; | ||
354 | bar(var_name) | ||
355 | }", | ||
356 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
357 | ); | ||
358 | } | ||
359 | |||
360 | #[test] | ||
361 | fn test_introduce_var_last_full_expr() { | ||
362 | check_action_range( | ||
363 | " | ||
364 | fn foo() { | ||
365 | <|>bar(1 + 1)<|> | ||
366 | }", | ||
367 | " | ||
368 | fn foo() { | ||
369 | let <|>var_name = bar(1 + 1); | ||
370 | var_name | ||
371 | }", | ||
372 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
373 | ); | ||
374 | } | ||
375 | |||
376 | #[test] | ||
377 | fn test_make_pub_crate() { | ||
378 | check_action( | ||
379 | "<|>fn foo() {}", | ||
380 | "<|>pub(crate) fn foo() {}", | ||
381 | |file, off| make_pub_crate(file, off).map(|f| f()), | ||
382 | ); | ||
383 | check_action( | ||
384 | "f<|>n foo() {}", | ||
385 | "<|>pub(crate) fn foo() {}", | ||
386 | |file, off| make_pub_crate(file, off).map(|f| f()), | ||
387 | ); | ||
388 | check_action( | ||
389 | "<|>struct Foo {}", | ||
390 | "<|>pub(crate) struct Foo {}", | ||
391 | |file, off| make_pub_crate(file, off).map(|f| f()), | ||
392 | ); | ||
393 | check_action("<|>mod foo {}", "<|>pub(crate) mod foo {}", |file, off| { | ||
394 | make_pub_crate(file, off).map(|f| f()) | ||
395 | }); | ||
396 | check_action( | ||
397 | "<|>trait Foo {}", | ||
398 | "<|>pub(crate) trait Foo {}", | ||
399 | |file, off| make_pub_crate(file, off).map(|f| f()), | ||
400 | ); | ||
401 | check_action("m<|>od {}", "<|>pub(crate) mod {}", |file, off| { | ||
402 | make_pub_crate(file, off).map(|f| f()) | ||
403 | }); | ||
404 | check_action( | ||
405 | "pub(crate) f<|>n foo() {}", | ||
406 | "pub(crate) f<|>n foo() {}", | ||
407 | |file, off| make_pub_crate(file, off).map(|f| f()), | ||
408 | ); | ||
409 | check_action( | ||
410 | "unsafe f<|>n foo() {}", | ||
411 | "<|>pub(crate) unsafe fn foo() {}", | ||
412 | |file, off| make_pub_crate(file, off).map(|f| f()), | ||
413 | ); | ||
414 | } | ||
415 | } | ||
diff --git a/crates/ra_editor/src/lib.rs b/crates/ra_editor/src/lib.rs index bfc745e58..ac283e2e0 100644 --- a/crates/ra_editor/src/lib.rs +++ b/crates/ra_editor/src/lib.rs | |||
@@ -1,4 +1,4 @@ | |||
1 | mod code_actions; | 1 | pub mod assists; |
2 | mod extend_selection; | 2 | mod extend_selection; |
3 | mod folding_ranges; | 3 | mod folding_ranges; |
4 | mod line_index; | 4 | mod line_index; |
@@ -10,7 +10,7 @@ mod typing; | |||
10 | mod diagnostics; | 10 | mod diagnostics; |
11 | 11 | ||
12 | pub use self::{ | 12 | pub use self::{ |
13 | code_actions::{add_derive, add_impl, flip_comma, introduce_variable, make_pub_crate, LocalEdit}, | 13 | assists::LocalEdit, |
14 | extend_selection::extend_selection, | 14 | extend_selection::extend_selection, |
15 | folding_ranges::{folding_ranges, Fold, FoldKind}, | 15 | folding_ranges::{folding_ranges, Fold, FoldKind}, |
16 | line_index::{LineCol, LineIndex}, | 16 | line_index::{LineCol, LineIndex}, |
@@ -19,7 +19,7 @@ pub use self::{ | |||
19 | typing::{join_lines, on_enter, on_eq_typed}, | 19 | typing::{join_lines, on_enter, on_eq_typed}, |
20 | diagnostics::diagnostics | 20 | diagnostics::diagnostics |
21 | }; | 21 | }; |
22 | use ra_text_edit::{TextEdit, TextEditBuilder}; | 22 | use ra_text_edit::TextEditBuilder; |
23 | use ra_syntax::{ | 23 | use ra_syntax::{ |
24 | algo::find_leaf_at_offset, | 24 | algo::find_leaf_at_offset, |
25 | ast::{self, AstNode}, | 25 | ast::{self, AstNode}, |