diff options
Diffstat (limited to 'crates/ra_editor/src')
-rw-r--r-- | crates/ra_editor/src/code_actions.rs | 218 | ||||
-rw-r--r-- | crates/ra_editor/src/completion.rs | 480 | ||||
-rw-r--r-- | crates/ra_editor/src/edit.rs | 84 | ||||
-rw-r--r-- | crates/ra_editor/src/extend_selection.rs | 167 | ||||
-rw-r--r-- | crates/ra_editor/src/lib.rs | 228 | ||||
-rw-r--r-- | crates/ra_editor/src/line_index.rs | 62 | ||||
-rw-r--r-- | crates/ra_editor/src/scope/fn_scope.rs | 329 | ||||
-rw-r--r-- | crates/ra_editor/src/scope/mod.rs | 8 | ||||
-rw-r--r-- | crates/ra_editor/src/scope/mod_scope.rs | 115 | ||||
-rw-r--r-- | crates/ra_editor/src/symbols.rs | 167 | ||||
-rw-r--r-- | crates/ra_editor/src/test_utils.rs | 37 | ||||
-rw-r--r-- | crates/ra_editor/src/typing.rs | 348 |
12 files changed, 2243 insertions, 0 deletions
diff --git a/crates/ra_editor/src/code_actions.rs b/crates/ra_editor/src/code_actions.rs new file mode 100644 index 000000000..83f7956d2 --- /dev/null +++ b/crates/ra_editor/src/code_actions.rs | |||
@@ -0,0 +1,218 @@ | |||
1 | use join_to_string::join; | ||
2 | |||
3 | use ra_syntax::{ | ||
4 | File, TextUnit, TextRange, | ||
5 | ast::{self, AstNode, AttrsOwner, TypeParamsOwner, NameOwner}, | ||
6 | SyntaxKind::{COMMA, WHITESPACE}, | ||
7 | SyntaxNodeRef, | ||
8 | algo::{ | ||
9 | Direction, siblings, | ||
10 | find_leaf_at_offset, | ||
11 | find_covering_node, | ||
12 | ancestors, | ||
13 | }, | ||
14 | }; | ||
15 | |||
16 | use {EditBuilder, Edit, find_node_at_offset}; | ||
17 | |||
18 | #[derive(Debug)] | ||
19 | pub struct LocalEdit { | ||
20 | pub edit: Edit, | ||
21 | pub cursor_position: Option<TextUnit>, | ||
22 | } | ||
23 | |||
24 | pub fn flip_comma<'a>(file: &'a File, offset: TextUnit) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
25 | let syntax = file.syntax(); | ||
26 | |||
27 | let comma = find_leaf_at_offset(syntax, offset).find(|leaf| leaf.kind() == COMMA)?; | ||
28 | let left = non_trivia_sibling(comma, Direction::Backward)?; | ||
29 | let right = non_trivia_sibling(comma, Direction::Forward)?; | ||
30 | Some(move || { | ||
31 | let mut edit = EditBuilder::new(); | ||
32 | edit.replace(left.range(), right.text().to_string()); | ||
33 | edit.replace(right.range(), left.text().to_string()); | ||
34 | LocalEdit { | ||
35 | edit: edit.finish(), | ||
36 | cursor_position: None, | ||
37 | } | ||
38 | }) | ||
39 | } | ||
40 | |||
41 | pub fn add_derive<'a>(file: &'a File, offset: TextUnit) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
42 | let nominal = find_node_at_offset::<ast::NominalDef>(file.syntax(), offset)?; | ||
43 | Some(move || { | ||
44 | let derive_attr = nominal | ||
45 | .attrs() | ||
46 | .filter_map(|x| x.as_call()) | ||
47 | .filter(|(name, _arg)| name == "derive") | ||
48 | .map(|(_name, arg)| arg) | ||
49 | .next(); | ||
50 | let mut edit = EditBuilder::new(); | ||
51 | let offset = match derive_attr { | ||
52 | None => { | ||
53 | let node_start = nominal.syntax().range().start(); | ||
54 | edit.insert(node_start, "#[derive()]\n".to_string()); | ||
55 | node_start + TextUnit::of_str("#[derive(") | ||
56 | } | ||
57 | Some(tt) => { | ||
58 | tt.syntax().range().end() - TextUnit::of_char(')') | ||
59 | } | ||
60 | }; | ||
61 | LocalEdit { | ||
62 | edit: edit.finish(), | ||
63 | cursor_position: Some(offset), | ||
64 | } | ||
65 | }) | ||
66 | } | ||
67 | |||
68 | pub fn add_impl<'a>(file: &'a File, offset: TextUnit) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
69 | let nominal = find_node_at_offset::<ast::NominalDef>(file.syntax(), offset)?; | ||
70 | let name = nominal.name()?; | ||
71 | |||
72 | Some(move || { | ||
73 | let type_params = nominal.type_param_list(); | ||
74 | let mut edit = EditBuilder::new(); | ||
75 | let start_offset = nominal.syntax().range().end(); | ||
76 | let mut buf = String::new(); | ||
77 | buf.push_str("\n\nimpl"); | ||
78 | if let Some(type_params) = type_params { | ||
79 | type_params.syntax().text() | ||
80 | .push_to(&mut buf); | ||
81 | } | ||
82 | buf.push_str(" "); | ||
83 | buf.push_str(name.text().as_str()); | ||
84 | if let Some(type_params) = type_params { | ||
85 | let lifetime_params = type_params.lifetime_params().filter_map(|it| it.lifetime()).map(|it| it.text()); | ||
86 | let type_params = type_params.type_params().filter_map(|it| it.name()).map(|it| it.text()); | ||
87 | join(lifetime_params.chain(type_params)) | ||
88 | .surround_with("<", ">") | ||
89 | .to_buf(&mut buf); | ||
90 | } | ||
91 | buf.push_str(" {\n"); | ||
92 | let offset = start_offset + TextUnit::of_str(&buf); | ||
93 | buf.push_str("\n}"); | ||
94 | edit.insert(start_offset, buf); | ||
95 | LocalEdit { | ||
96 | edit: edit.finish(), | ||
97 | cursor_position: Some(offset), | ||
98 | } | ||
99 | }) | ||
100 | } | ||
101 | |||
102 | pub fn introduce_variable<'a>(file: &'a File, range: TextRange) -> Option<impl FnOnce() -> LocalEdit + 'a> { | ||
103 | let node = find_covering_node(file.syntax(), range); | ||
104 | let expr = ancestors(node).filter_map(ast::Expr::cast).next()?; | ||
105 | let anchor_stmt = ancestors(expr.syntax()).filter_map(ast::Stmt::cast).next()?; | ||
106 | let indent = anchor_stmt.syntax().prev_sibling()?; | ||
107 | if indent.kind() != WHITESPACE { | ||
108 | return None; | ||
109 | } | ||
110 | Some(move || { | ||
111 | let mut buf = String::new(); | ||
112 | let mut edit = EditBuilder::new(); | ||
113 | |||
114 | buf.push_str("let var_name = "); | ||
115 | expr.syntax().text().push_to(&mut buf); | ||
116 | if expr.syntax().range().start() == anchor_stmt.syntax().range().start() { | ||
117 | edit.replace(expr.syntax().range(), buf); | ||
118 | } else { | ||
119 | buf.push_str(";"); | ||
120 | indent.text().push_to(&mut buf); | ||
121 | edit.replace(expr.syntax().range(), "var_name".to_string()); | ||
122 | edit.insert(anchor_stmt.syntax().range().start(), buf); | ||
123 | } | ||
124 | let cursor_position = anchor_stmt.syntax().range().start() + TextUnit::of_str("let "); | ||
125 | LocalEdit { | ||
126 | edit: edit.finish(), | ||
127 | cursor_position: Some(cursor_position), | ||
128 | } | ||
129 | }) | ||
130 | } | ||
131 | |||
132 | fn non_trivia_sibling(node: SyntaxNodeRef, direction: Direction) -> Option<SyntaxNodeRef> { | ||
133 | siblings(node, direction) | ||
134 | .skip(1) | ||
135 | .find(|node| !node.kind().is_trivia()) | ||
136 | } | ||
137 | |||
138 | #[cfg(test)] | ||
139 | mod tests { | ||
140 | use super::*; | ||
141 | use test_utils::{check_action, check_action_range}; | ||
142 | |||
143 | #[test] | ||
144 | fn test_swap_comma() { | ||
145 | check_action( | ||
146 | "fn foo(x: i32,<|> y: Result<(), ()>) {}", | ||
147 | "fn foo(y: Result<(), ()>,<|> x: i32) {}", | ||
148 | |file, off| flip_comma(file, off).map(|f| f()), | ||
149 | ) | ||
150 | } | ||
151 | |||
152 | #[test] | ||
153 | fn test_add_derive() { | ||
154 | check_action( | ||
155 | "struct Foo { a: i32, <|>}", | ||
156 | "#[derive(<|>)]\nstruct Foo { a: i32, }", | ||
157 | |file, off| add_derive(file, off).map(|f| f()), | ||
158 | ); | ||
159 | check_action( | ||
160 | "struct Foo { <|> a: i32, }", | ||
161 | "#[derive(<|>)]\nstruct Foo { a: i32, }", | ||
162 | |file, off| add_derive(file, off).map(|f| f()), | ||
163 | ); | ||
164 | check_action( | ||
165 | "#[derive(Clone)]\nstruct Foo { a: i32<|>, }", | ||
166 | "#[derive(Clone<|>)]\nstruct Foo { a: i32, }", | ||
167 | |file, off| add_derive(file, off).map(|f| f()), | ||
168 | ); | ||
169 | } | ||
170 | |||
171 | #[test] | ||
172 | fn test_add_impl() { | ||
173 | check_action( | ||
174 | "struct Foo {<|>}\n", | ||
175 | "struct Foo {}\n\nimpl Foo {\n<|>\n}\n", | ||
176 | |file, off| add_impl(file, off).map(|f| f()), | ||
177 | ); | ||
178 | check_action( | ||
179 | "struct Foo<T: Clone> {<|>}", | ||
180 | "struct Foo<T: Clone> {}\n\nimpl<T: Clone> Foo<T> {\n<|>\n}", | ||
181 | |file, off| add_impl(file, off).map(|f| f()), | ||
182 | ); | ||
183 | check_action( | ||
184 | "struct Foo<'a, T: Foo<'a>> {<|>}", | ||
185 | "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}", | ||
186 | |file, off| add_impl(file, off).map(|f| f()), | ||
187 | ); | ||
188 | } | ||
189 | |||
190 | #[test] | ||
191 | fn test_intrdoduce_var_simple() { | ||
192 | check_action_range( | ||
193 | " | ||
194 | fn foo() { | ||
195 | foo(<|>1 + 1<|>); | ||
196 | }", " | ||
197 | fn foo() { | ||
198 | let <|>var_name = 1 + 1; | ||
199 | foo(var_name); | ||
200 | }", | ||
201 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
202 | ); | ||
203 | } | ||
204 | #[test] | ||
205 | fn test_intrdoduce_var_expr_stmt() { | ||
206 | check_action_range( | ||
207 | " | ||
208 | fn foo() { | ||
209 | <|>1 + 1<|>; | ||
210 | }", " | ||
211 | fn foo() { | ||
212 | let <|>var_name = 1 + 1; | ||
213 | }", | ||
214 | |file, range| introduce_variable(file, range).map(|f| f()), | ||
215 | ); | ||
216 | } | ||
217 | |||
218 | } | ||
diff --git a/crates/ra_editor/src/completion.rs b/crates/ra_editor/src/completion.rs new file mode 100644 index 000000000..5000b32a0 --- /dev/null +++ b/crates/ra_editor/src/completion.rs | |||
@@ -0,0 +1,480 @@ | |||
1 | use std::collections::{HashSet, HashMap}; | ||
2 | |||
3 | use ra_syntax::{ | ||
4 | File, TextUnit, AstNode, SyntaxNodeRef, SyntaxKind::*, | ||
5 | ast::{self, LoopBodyOwner, ModuleItemOwner}, | ||
6 | algo::{ | ||
7 | ancestors, | ||
8 | visit::{visitor, Visitor, visitor_ctx, VisitorCtx}, | ||
9 | }, | ||
10 | text_utils::is_subrange, | ||
11 | }; | ||
12 | |||
13 | use { | ||
14 | AtomEdit, find_node_at_offset, | ||
15 | scope::{FnScopes, ModuleScope}, | ||
16 | }; | ||
17 | |||
18 | #[derive(Debug)] | ||
19 | pub struct CompletionItem { | ||
20 | /// What user sees in pop-up | ||
21 | pub label: String, | ||
22 | /// What string is used for filtering, defaults to label | ||
23 | pub lookup: Option<String>, | ||
24 | /// What is inserted, defaults to label | ||
25 | pub snippet: Option<String> | ||
26 | } | ||
27 | |||
28 | pub fn scope_completion(file: &File, offset: TextUnit) -> Option<Vec<CompletionItem>> { | ||
29 | // Insert a fake ident to get a valid parse tree | ||
30 | let file = { | ||
31 | let edit = AtomEdit::insert(offset, "intellijRulezz".to_string()); | ||
32 | file.reparse(&edit) | ||
33 | }; | ||
34 | let mut has_completions = false; | ||
35 | let mut res = Vec::new(); | ||
36 | if let Some(name_ref) = find_node_at_offset::<ast::NameRef>(file.syntax(), offset) { | ||
37 | has_completions = true; | ||
38 | complete_name_ref(&file, name_ref, &mut res); | ||
39 | // special case, `trait T { fn foo(i_am_a_name_ref) {} }` | ||
40 | if is_node::<ast::Param>(name_ref.syntax()) { | ||
41 | param_completions(name_ref.syntax(), &mut res); | ||
42 | } | ||
43 | } | ||
44 | if let Some(name) = find_node_at_offset::<ast::Name>(file.syntax(), offset) { | ||
45 | if is_node::<ast::Param>(name.syntax()) { | ||
46 | has_completions = true; | ||
47 | param_completions(name.syntax(), &mut res); | ||
48 | } | ||
49 | } | ||
50 | if has_completions { | ||
51 | Some(res) | ||
52 | } else { | ||
53 | None | ||
54 | } | ||
55 | } | ||
56 | |||
57 | fn complete_name_ref(file: &File, name_ref: ast::NameRef, acc: &mut Vec<CompletionItem>) { | ||
58 | if !is_node::<ast::Path>(name_ref.syntax()) { | ||
59 | return; | ||
60 | } | ||
61 | let mut visited_fn = false; | ||
62 | for node in ancestors(name_ref.syntax()) { | ||
63 | if let Some(items) = visitor() | ||
64 | .visit::<ast::Root, _>(|it| Some(it.items())) | ||
65 | .visit::<ast::Module, _>(|it| Some(it.item_list()?.items())) | ||
66 | .accept(node) { | ||
67 | if let Some(items) = items { | ||
68 | let scope = ModuleScope::new(items); | ||
69 | acc.extend( | ||
70 | scope.entries().iter() | ||
71 | .filter(|entry| entry.syntax() != name_ref.syntax()) | ||
72 | .map(|entry| CompletionItem { | ||
73 | label: entry.name().to_string(), | ||
74 | lookup: None, | ||
75 | snippet: None, | ||
76 | }) | ||
77 | ); | ||
78 | } | ||
79 | break; | ||
80 | |||
81 | } else if !visited_fn { | ||
82 | if let Some(fn_def) = ast::FnDef::cast(node) { | ||
83 | visited_fn = true; | ||
84 | complete_expr_keywords(&file, fn_def, name_ref, acc); | ||
85 | let scopes = FnScopes::new(fn_def); | ||
86 | complete_fn(name_ref, &scopes, acc); | ||
87 | } | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | |||
92 | fn param_completions(ctx: SyntaxNodeRef, acc: &mut Vec<CompletionItem>) { | ||
93 | let mut params = HashMap::new(); | ||
94 | for node in ancestors(ctx) { | ||
95 | let _ = visitor_ctx(&mut params) | ||
96 | .visit::<ast::Root, _>(process) | ||
97 | .visit::<ast::ItemList, _>(process) | ||
98 | .accept(node); | ||
99 | } | ||
100 | params.into_iter() | ||
101 | .filter_map(|(label, (count, param))| { | ||
102 | let lookup = param.pat()?.syntax().text().to_string(); | ||
103 | if count < 2 { None } else { Some((label, lookup)) } | ||
104 | }) | ||
105 | .for_each(|(label, lookup)| { | ||
106 | acc.push(CompletionItem { | ||
107 | label, lookup: Some(lookup), snippet: None | ||
108 | }) | ||
109 | }); | ||
110 | |||
111 | fn process<'a, N: ast::FnDefOwner<'a>>(node: N, params: &mut HashMap<String, (u32, ast::Param<'a>)>) { | ||
112 | node.functions() | ||
113 | .filter_map(|it| it.param_list()) | ||
114 | .flat_map(|it| it.params()) | ||
115 | .for_each(|param| { | ||
116 | let text = param.syntax().text().to_string(); | ||
117 | params.entry(text) | ||
118 | .or_insert((0, param)) | ||
119 | .0 += 1; | ||
120 | }) | ||
121 | } | ||
122 | } | ||
123 | |||
124 | fn is_node<'a, N: AstNode<'a>>(node: SyntaxNodeRef<'a>) -> bool { | ||
125 | match ancestors(node).filter_map(N::cast).next() { | ||
126 | None => false, | ||
127 | Some(n) => n.syntax().range() == node.range(), | ||
128 | } | ||
129 | } | ||
130 | |||
131 | |||
132 | fn complete_expr_keywords(file: &File, fn_def: ast::FnDef, name_ref: ast::NameRef, acc: &mut Vec<CompletionItem>) { | ||
133 | acc.push(keyword("if", "if $0 {}")); | ||
134 | acc.push(keyword("match", "match $0 {}")); | ||
135 | acc.push(keyword("while", "while $0 {}")); | ||
136 | acc.push(keyword("loop", "loop {$0}")); | ||
137 | |||
138 | if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { | ||
139 | if let Some(if_expr) = find_node_at_offset::<ast::IfExpr>(file.syntax(), off) { | ||
140 | if if_expr.syntax().range().end() < name_ref.syntax().range().start() { | ||
141 | acc.push(keyword("else", "else {$0}")); | ||
142 | acc.push(keyword("else if", "else if $0 {}")); | ||
143 | } | ||
144 | } | ||
145 | } | ||
146 | if is_in_loop_body(name_ref) { | ||
147 | acc.push(keyword("continue", "continue")); | ||
148 | acc.push(keyword("break", "break")); | ||
149 | } | ||
150 | acc.extend(complete_return(fn_def, name_ref)); | ||
151 | } | ||
152 | |||
153 | fn is_in_loop_body(name_ref: ast::NameRef) -> bool { | ||
154 | for node in ancestors(name_ref.syntax()) { | ||
155 | if node.kind() == FN_DEF || node.kind() == LAMBDA_EXPR { | ||
156 | break; | ||
157 | } | ||
158 | let loop_body = visitor() | ||
159 | .visit::<ast::ForExpr, _>(LoopBodyOwner::loop_body) | ||
160 | .visit::<ast::WhileExpr, _>(LoopBodyOwner::loop_body) | ||
161 | .visit::<ast::LoopExpr, _>(LoopBodyOwner::loop_body) | ||
162 | .accept(node); | ||
163 | if let Some(Some(body)) = loop_body { | ||
164 | if is_subrange(body.syntax().range(), name_ref.syntax().range()) { | ||
165 | return true; | ||
166 | } | ||
167 | } | ||
168 | } | ||
169 | false | ||
170 | } | ||
171 | |||
172 | fn complete_return(fn_def: ast::FnDef, name_ref: ast::NameRef) -> Option<CompletionItem> { | ||
173 | // let is_last_in_block = ancestors(name_ref.syntax()).filter_map(ast::Expr::cast) | ||
174 | // .next() | ||
175 | // .and_then(|it| it.syntax().parent()) | ||
176 | // .and_then(ast::Block::cast) | ||
177 | // .is_some(); | ||
178 | |||
179 | // if is_last_in_block { | ||
180 | // return None; | ||
181 | // } | ||
182 | |||
183 | let is_stmt = match ancestors(name_ref.syntax()).filter_map(ast::ExprStmt::cast).next() { | ||
184 | None => false, | ||
185 | Some(expr_stmt) => expr_stmt.syntax().range() == name_ref.syntax().range() | ||
186 | }; | ||
187 | let snip = match (is_stmt, fn_def.ret_type().is_some()) { | ||
188 | (true, true) => "return $0;", | ||
189 | (true, false) => "return;", | ||
190 | (false, true) => "return $0", | ||
191 | (false, false) => "return", | ||
192 | }; | ||
193 | Some(keyword("return", snip)) | ||
194 | } | ||
195 | |||
196 | fn keyword(kw: &str, snip: &str) -> CompletionItem { | ||
197 | CompletionItem { | ||
198 | label: kw.to_string(), | ||
199 | lookup: None, | ||
200 | snippet: Some(snip.to_string()), | ||
201 | } | ||
202 | } | ||
203 | |||
204 | fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Vec<CompletionItem>) { | ||
205 | let mut shadowed = HashSet::new(); | ||
206 | acc.extend( | ||
207 | scopes.scope_chain(name_ref.syntax()) | ||
208 | .flat_map(|scope| scopes.entries(scope).iter()) | ||
209 | .filter(|entry| shadowed.insert(entry.name())) | ||
210 | .map(|entry| CompletionItem { | ||
211 | label: entry.name().to_string(), | ||
212 | lookup: None, | ||
213 | snippet: None, | ||
214 | }) | ||
215 | ); | ||
216 | if scopes.self_param.is_some() { | ||
217 | acc.push(CompletionItem { | ||
218 | label: "self".to_string(), | ||
219 | lookup: None, | ||
220 | snippet: None, | ||
221 | }) | ||
222 | } | ||
223 | } | ||
224 | |||
225 | #[cfg(test)] | ||
226 | mod tests { | ||
227 | use super::*; | ||
228 | use test_utils::{assert_eq_dbg, extract_offset}; | ||
229 | |||
230 | fn check_scope_completion(code: &str, expected_completions: &str) { | ||
231 | let (off, code) = extract_offset(&code); | ||
232 | let file = File::parse(&code); | ||
233 | let completions = scope_completion(&file, off) | ||
234 | .unwrap() | ||
235 | .into_iter() | ||
236 | .filter(|c| c.snippet.is_none()) | ||
237 | .collect::<Vec<_>>(); | ||
238 | assert_eq_dbg(expected_completions, &completions); | ||
239 | } | ||
240 | |||
241 | fn check_snippet_completion(code: &str, expected_completions: &str) { | ||
242 | let (off, code) = extract_offset(&code); | ||
243 | let file = File::parse(&code); | ||
244 | let completions = scope_completion(&file, off) | ||
245 | .unwrap() | ||
246 | .into_iter() | ||
247 | .filter(|c| c.snippet.is_some()) | ||
248 | .collect::<Vec<_>>(); | ||
249 | assert_eq_dbg(expected_completions, &completions); | ||
250 | } | ||
251 | |||
252 | #[test] | ||
253 | fn test_completion_let_scope() { | ||
254 | check_scope_completion(r" | ||
255 | fn quux(x: i32) { | ||
256 | let y = 92; | ||
257 | 1 + <|>; | ||
258 | let z = (); | ||
259 | } | ||
260 | ", r#"[CompletionItem { label: "y", lookup: None, snippet: None }, | ||
261 | CompletionItem { label: "x", lookup: None, snippet: None }, | ||
262 | CompletionItem { label: "quux", lookup: None, snippet: None }]"#); | ||
263 | } | ||
264 | |||
265 | #[test] | ||
266 | fn test_completion_if_let_scope() { | ||
267 | check_scope_completion(r" | ||
268 | fn quux() { | ||
269 | if let Some(x) = foo() { | ||
270 | let y = 92; | ||
271 | }; | ||
272 | if let Some(a) = bar() { | ||
273 | let b = 62; | ||
274 | 1 + <|> | ||
275 | } | ||
276 | } | ||
277 | ", r#"[CompletionItem { label: "b", lookup: None, snippet: None }, | ||
278 | CompletionItem { label: "a", lookup: None, snippet: None }, | ||
279 | CompletionItem { label: "quux", lookup: None, snippet: None }]"#); | ||
280 | } | ||
281 | |||
282 | #[test] | ||
283 | fn test_completion_for_scope() { | ||
284 | check_scope_completion(r" | ||
285 | fn quux() { | ||
286 | for x in &[1, 2, 3] { | ||
287 | <|> | ||
288 | } | ||
289 | } | ||
290 | ", r#"[CompletionItem { label: "x", lookup: None, snippet: None }, | ||
291 | CompletionItem { label: "quux", lookup: None, snippet: None }]"#); | ||
292 | } | ||
293 | |||
294 | #[test] | ||
295 | fn test_completion_mod_scope() { | ||
296 | check_scope_completion(r" | ||
297 | struct Foo; | ||
298 | enum Baz {} | ||
299 | fn quux() { | ||
300 | <|> | ||
301 | } | ||
302 | ", r#"[CompletionItem { label: "Foo", lookup: None, snippet: None }, | ||
303 | CompletionItem { label: "Baz", lookup: None, snippet: None }, | ||
304 | CompletionItem { label: "quux", lookup: None, snippet: None }]"#); | ||
305 | } | ||
306 | |||
307 | #[test] | ||
308 | fn test_completion_mod_scope_no_self_use() { | ||
309 | check_scope_completion(r" | ||
310 | use foo<|>; | ||
311 | ", r#"[]"#); | ||
312 | } | ||
313 | |||
314 | #[test] | ||
315 | fn test_completion_mod_scope_nested() { | ||
316 | check_scope_completion(r" | ||
317 | struct Foo; | ||
318 | mod m { | ||
319 | struct Bar; | ||
320 | fn quux() { <|> } | ||
321 | } | ||
322 | ", r#"[CompletionItem { label: "Bar", lookup: None, snippet: None }, | ||
323 | CompletionItem { label: "quux", lookup: None, snippet: None }]"#); | ||
324 | } | ||
325 | |||
326 | #[test] | ||
327 | fn test_complete_type() { | ||
328 | check_scope_completion(r" | ||
329 | struct Foo; | ||
330 | fn x() -> <|> | ||
331 | ", r#"[CompletionItem { label: "Foo", lookup: None, snippet: None }, | ||
332 | CompletionItem { label: "x", lookup: None, snippet: None }]"#) | ||
333 | } | ||
334 | |||
335 | #[test] | ||
336 | fn test_complete_shadowing() { | ||
337 | check_scope_completion(r" | ||
338 | fn foo() -> { | ||
339 | let bar = 92; | ||
340 | { | ||
341 | let bar = 62; | ||
342 | <|> | ||
343 | } | ||
344 | } | ||
345 | ", r#"[CompletionItem { label: "bar", lookup: None, snippet: None }, | ||
346 | CompletionItem { label: "foo", lookup: None, snippet: None }]"#) | ||
347 | } | ||
348 | |||
349 | #[test] | ||
350 | fn test_complete_self() { | ||
351 | check_scope_completion(r" | ||
352 | impl S { fn foo(&self) { <|> } } | ||
353 | ", r#"[CompletionItem { label: "self", lookup: None, snippet: None }]"#) | ||
354 | } | ||
355 | |||
356 | #[test] | ||
357 | fn test_completion_kewords() { | ||
358 | check_snippet_completion(r" | ||
359 | fn quux() { | ||
360 | <|> | ||
361 | } | ||
362 | ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, | ||
363 | CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, | ||
364 | CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, | ||
365 | CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, | ||
366 | CompletionItem { label: "return", lookup: None, snippet: Some("return") }]"#); | ||
367 | } | ||
368 | |||
369 | #[test] | ||
370 | fn test_completion_else() { | ||
371 | check_snippet_completion(r" | ||
372 | fn quux() { | ||
373 | if true { | ||
374 | () | ||
375 | } <|> | ||
376 | } | ||
377 | ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, | ||
378 | CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, | ||
379 | CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, | ||
380 | CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, | ||
381 | CompletionItem { label: "else", lookup: None, snippet: Some("else {$0}") }, | ||
382 | CompletionItem { label: "else if", lookup: None, snippet: Some("else if $0 {}") }, | ||
383 | CompletionItem { label: "return", lookup: None, snippet: Some("return") }]"#); | ||
384 | } | ||
385 | |||
386 | #[test] | ||
387 | fn test_completion_return_value() { | ||
388 | check_snippet_completion(r" | ||
389 | fn quux() -> i32 { | ||
390 | <|> | ||
391 | 92 | ||
392 | } | ||
393 | ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, | ||
394 | CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, | ||
395 | CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, | ||
396 | CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, | ||
397 | CompletionItem { label: "return", lookup: None, snippet: Some("return $0;") }]"#); | ||
398 | check_snippet_completion(r" | ||
399 | fn quux() { | ||
400 | <|> | ||
401 | 92 | ||
402 | } | ||
403 | ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, | ||
404 | CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, | ||
405 | CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, | ||
406 | CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, | ||
407 | CompletionItem { label: "return", lookup: None, snippet: Some("return;") }]"#); | ||
408 | } | ||
409 | |||
410 | #[test] | ||
411 | fn test_completion_return_no_stmt() { | ||
412 | check_snippet_completion(r" | ||
413 | fn quux() -> i32 { | ||
414 | match () { | ||
415 | () => <|> | ||
416 | } | ||
417 | } | ||
418 | ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, | ||
419 | CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, | ||
420 | CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, | ||
421 | CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, | ||
422 | CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }]"#); | ||
423 | } | ||
424 | |||
425 | #[test] | ||
426 | fn test_continue_break_completion() { | ||
427 | check_snippet_completion(r" | ||
428 | fn quux() -> i32 { | ||
429 | loop { <|> } | ||
430 | } | ||
431 | ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, | ||
432 | CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, | ||
433 | CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, | ||
434 | CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, | ||
435 | CompletionItem { label: "continue", lookup: None, snippet: Some("continue") }, | ||
436 | CompletionItem { label: "break", lookup: None, snippet: Some("break") }, | ||
437 | CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }]"#); | ||
438 | check_snippet_completion(r" | ||
439 | fn quux() -> i32 { | ||
440 | loop { || { <|> } } | ||
441 | } | ||
442 | ", r#"[CompletionItem { label: "if", lookup: None, snippet: Some("if $0 {}") }, | ||
443 | CompletionItem { label: "match", lookup: None, snippet: Some("match $0 {}") }, | ||
444 | CompletionItem { label: "while", lookup: None, snippet: Some("while $0 {}") }, | ||
445 | CompletionItem { label: "loop", lookup: None, snippet: Some("loop {$0}") }, | ||
446 | CompletionItem { label: "return", lookup: None, snippet: Some("return $0") }]"#); | ||
447 | } | ||
448 | |||
449 | #[test] | ||
450 | fn test_param_completion_last_param() { | ||
451 | check_scope_completion(r" | ||
452 | fn foo(file_id: FileId) {} | ||
453 | fn bar(file_id: FileId) {} | ||
454 | fn baz(file<|>) {} | ||
455 | ", r#"[CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); | ||
456 | } | ||
457 | |||
458 | #[test] | ||
459 | fn test_param_completion_nth_param() { | ||
460 | check_scope_completion(r" | ||
461 | fn foo(file_id: FileId) {} | ||
462 | fn bar(file_id: FileId) {} | ||
463 | fn baz(file<|>, x: i32) {} | ||
464 | ", r#"[CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); | ||
465 | } | ||
466 | |||
467 | #[test] | ||
468 | fn test_param_completion_trait_param() { | ||
469 | check_scope_completion(r" | ||
470 | pub(crate) trait SourceRoot { | ||
471 | pub fn contains(&self, file_id: FileId) -> bool; | ||
472 | pub fn module_map(&self) -> &ModuleMap; | ||
473 | pub fn lines(&self, file_id: FileId) -> &LineIndex; | ||
474 | pub fn syntax(&self, file<|>) | ||
475 | } | ||
476 | ", r#"[CompletionItem { label: "self", lookup: None, snippet: None }, | ||
477 | CompletionItem { label: "SourceRoot", lookup: None, snippet: None }, | ||
478 | CompletionItem { label: "file_id: FileId", lookup: Some("file_id"), snippet: None }]"#); | ||
479 | } | ||
480 | } | ||
diff --git a/crates/ra_editor/src/edit.rs b/crates/ra_editor/src/edit.rs new file mode 100644 index 000000000..2839ac20a --- /dev/null +++ b/crates/ra_editor/src/edit.rs | |||
@@ -0,0 +1,84 @@ | |||
1 | use {TextRange, TextUnit}; | ||
2 | use ra_syntax::{ | ||
3 | AtomEdit, | ||
4 | text_utils::contains_offset_nonstrict, | ||
5 | }; | ||
6 | |||
7 | #[derive(Debug, Clone)] | ||
8 | pub struct Edit { | ||
9 | atoms: Vec<AtomEdit>, | ||
10 | } | ||
11 | |||
12 | #[derive(Debug)] | ||
13 | pub struct EditBuilder { | ||
14 | atoms: Vec<AtomEdit> | ||
15 | } | ||
16 | |||
17 | impl EditBuilder { | ||
18 | pub fn new() -> EditBuilder { | ||
19 | EditBuilder { atoms: Vec::new() } | ||
20 | } | ||
21 | pub fn replace(&mut self, range: TextRange, replace_with: String) { | ||
22 | self.atoms.push(AtomEdit::replace(range, replace_with)) | ||
23 | } | ||
24 | pub fn delete(&mut self, range: TextRange) { | ||
25 | self.atoms.push(AtomEdit::delete(range)) | ||
26 | } | ||
27 | pub fn insert(&mut self, offset: TextUnit, text: String) { | ||
28 | self.atoms.push(AtomEdit::insert(offset, text)) | ||
29 | } | ||
30 | pub fn finish(self) -> Edit { | ||
31 | let mut atoms = self.atoms; | ||
32 | atoms.sort_by_key(|a| a.delete.start()); | ||
33 | for (a1, a2) in atoms.iter().zip(atoms.iter().skip(1)) { | ||
34 | assert!(a1.delete.end() <= a2.delete.start()) | ||
35 | } | ||
36 | Edit { atoms } | ||
37 | } | ||
38 | pub fn invalidates_offset(&self, offset: TextUnit) -> bool { | ||
39 | self.atoms.iter().any(|atom| contains_offset_nonstrict(atom.delete, offset)) | ||
40 | } | ||
41 | } | ||
42 | |||
43 | impl Edit { | ||
44 | pub fn into_atoms(self) -> Vec<AtomEdit> { | ||
45 | self.atoms | ||
46 | } | ||
47 | |||
48 | pub fn apply(&self, text: &str) -> String { | ||
49 | let mut total_len = text.len(); | ||
50 | for atom in self.atoms.iter() { | ||
51 | total_len += atom.insert.len(); | ||
52 | total_len -= u32::from(atom.delete.end() - atom.delete.start()) as usize; | ||
53 | } | ||
54 | let mut buf = String::with_capacity(total_len); | ||
55 | let mut prev = 0; | ||
56 | for atom in self.atoms.iter() { | ||
57 | let start = u32::from(atom.delete.start()) as usize; | ||
58 | let end = u32::from(atom.delete.end()) as usize; | ||
59 | if start > prev { | ||
60 | buf.push_str(&text[prev..start]); | ||
61 | } | ||
62 | buf.push_str(&atom.insert); | ||
63 | prev = end; | ||
64 | } | ||
65 | buf.push_str(&text[prev..text.len()]); | ||
66 | assert_eq!(buf.len(), total_len); | ||
67 | buf | ||
68 | } | ||
69 | |||
70 | pub fn apply_to_offset(&self, offset: TextUnit) -> Option<TextUnit> { | ||
71 | let mut res = offset; | ||
72 | for atom in self.atoms.iter() { | ||
73 | if atom.delete.start() >= offset { | ||
74 | break; | ||
75 | } | ||
76 | if offset < atom.delete.end() { | ||
77 | return None | ||
78 | } | ||
79 | res += TextUnit::of_str(&atom.insert); | ||
80 | res -= atom.delete.len(); | ||
81 | } | ||
82 | Some(res) | ||
83 | } | ||
84 | } | ||
diff --git a/crates/ra_editor/src/extend_selection.rs b/crates/ra_editor/src/extend_selection.rs new file mode 100644 index 000000000..5fd1ca4fc --- /dev/null +++ b/crates/ra_editor/src/extend_selection.rs | |||
@@ -0,0 +1,167 @@ | |||
1 | use ra_syntax::{ | ||
2 | File, TextRange, SyntaxNodeRef, TextUnit, | ||
3 | SyntaxKind::*, | ||
4 | algo::{find_leaf_at_offset, LeafAtOffset, find_covering_node, ancestors, Direction, siblings}, | ||
5 | }; | ||
6 | |||
7 | pub fn extend_selection(file: &File, range: TextRange) -> Option<TextRange> { | ||
8 | let syntax = file.syntax(); | ||
9 | extend(syntax.borrowed(), range) | ||
10 | } | ||
11 | |||
12 | pub(crate) fn extend(root: SyntaxNodeRef, range: TextRange) -> Option<TextRange> { | ||
13 | if range.is_empty() { | ||
14 | let offset = range.start(); | ||
15 | let mut leaves = find_leaf_at_offset(root, offset); | ||
16 | if leaves.clone().all(|it| it.kind() == WHITESPACE) { | ||
17 | return Some(extend_ws(root, leaves.next()?, offset)); | ||
18 | } | ||
19 | let leaf = match leaves { | ||
20 | LeafAtOffset::None => return None, | ||
21 | LeafAtOffset::Single(l) => l, | ||
22 | LeafAtOffset::Between(l, r) => pick_best(l, r), | ||
23 | }; | ||
24 | return Some(leaf.range()); | ||
25 | }; | ||
26 | let node = find_covering_node(root, range); | ||
27 | if node.kind() == COMMENT && range == node.range() { | ||
28 | if let Some(range) = extend_comments(node) { | ||
29 | return Some(range); | ||
30 | } | ||
31 | } | ||
32 | |||
33 | match ancestors(node).skip_while(|n| n.range() == range).next() { | ||
34 | None => None, | ||
35 | Some(parent) => Some(parent.range()), | ||
36 | } | ||
37 | } | ||
38 | |||
39 | fn extend_ws(root: SyntaxNodeRef, ws: SyntaxNodeRef, offset: TextUnit) -> TextRange { | ||
40 | let ws_text = ws.leaf_text().unwrap(); | ||
41 | let suffix = TextRange::from_to(offset, ws.range().end()) - ws.range().start(); | ||
42 | let prefix = TextRange::from_to(ws.range().start(), offset) - ws.range().start(); | ||
43 | let ws_suffix = &ws_text.as_str()[suffix]; | ||
44 | let ws_prefix = &ws_text.as_str()[prefix]; | ||
45 | if ws_text.contains("\n") && !ws_suffix.contains("\n") { | ||
46 | if let Some(node) = ws.next_sibling() { | ||
47 | let start = match ws_prefix.rfind('\n') { | ||
48 | Some(idx) => ws.range().start() + TextUnit::from((idx + 1) as u32), | ||
49 | None => node.range().start() | ||
50 | }; | ||
51 | let end = if root.text().char_at(node.range().end()) == Some('\n') { | ||
52 | node.range().end() + TextUnit::of_char('\n') | ||
53 | } else { | ||
54 | node.range().end() | ||
55 | }; | ||
56 | return TextRange::from_to(start, end); | ||
57 | } | ||
58 | } | ||
59 | ws.range() | ||
60 | } | ||
61 | |||
62 | fn pick_best<'a>(l: SyntaxNodeRef<'a>, r: SyntaxNodeRef<'a>) -> SyntaxNodeRef<'a> { | ||
63 | return if priority(r) > priority(l) { r } else { l }; | ||
64 | fn priority(n: SyntaxNodeRef) -> usize { | ||
65 | match n.kind() { | ||
66 | WHITESPACE => 0, | ||
67 | IDENT | SELF_KW | SUPER_KW | CRATE_KW => 2, | ||
68 | _ => 1, | ||
69 | } | ||
70 | } | ||
71 | } | ||
72 | |||
73 | fn extend_comments(node: SyntaxNodeRef) -> Option<TextRange> { | ||
74 | let left = adj_comments(node, Direction::Backward); | ||
75 | let right = adj_comments(node, Direction::Forward); | ||
76 | if left != right { | ||
77 | Some(TextRange::from_to( | ||
78 | left.range().start(), | ||
79 | right.range().end(), | ||
80 | )) | ||
81 | } else { | ||
82 | None | ||
83 | } | ||
84 | } | ||
85 | |||
86 | fn adj_comments(node: SyntaxNodeRef, dir: Direction) -> SyntaxNodeRef { | ||
87 | let mut res = node; | ||
88 | for node in siblings(node, dir) { | ||
89 | match node.kind() { | ||
90 | COMMENT => res = node, | ||
91 | WHITESPACE if !node.leaf_text().unwrap().as_str().contains("\n\n") => (), | ||
92 | _ => break | ||
93 | } | ||
94 | } | ||
95 | res | ||
96 | } | ||
97 | |||
98 | #[cfg(test)] | ||
99 | mod tests { | ||
100 | use super::*; | ||
101 | use test_utils::extract_offset; | ||
102 | |||
103 | fn do_check(before: &str, afters: &[&str]) { | ||
104 | let (cursor, before) = extract_offset(before); | ||
105 | let file = File::parse(&before); | ||
106 | let mut range = TextRange::offset_len(cursor, 0.into()); | ||
107 | for &after in afters { | ||
108 | range = extend_selection(&file, range) | ||
109 | .unwrap(); | ||
110 | let actual = &before[range]; | ||
111 | assert_eq!(after, actual); | ||
112 | } | ||
113 | } | ||
114 | |||
115 | #[test] | ||
116 | fn test_extend_selection_arith() { | ||
117 | do_check( | ||
118 | r#"fn foo() { <|>1 + 1 }"#, | ||
119 | &["1", "1 + 1", "{ 1 + 1 }"], | ||
120 | ); | ||
121 | } | ||
122 | |||
123 | #[test] | ||
124 | fn test_extend_selection_start_of_the_lind() { | ||
125 | do_check( | ||
126 | r#" | ||
127 | impl S { | ||
128 | <|> fn foo() { | ||
129 | |||
130 | } | ||
131 | }"#, | ||
132 | &[" fn foo() {\n\n }\n"] | ||
133 | ); | ||
134 | } | ||
135 | |||
136 | #[test] | ||
137 | fn test_extend_selection_comments() { | ||
138 | do_check( | ||
139 | r#" | ||
140 | fn bar(){} | ||
141 | |||
142 | // fn foo() { | ||
143 | // 1 + <|>1 | ||
144 | // } | ||
145 | |||
146 | // fn foo(){} | ||
147 | "#, | ||
148 | &["// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"] | ||
149 | ); | ||
150 | } | ||
151 | |||
152 | #[test] | ||
153 | fn test_extend_selection_prefer_idents() { | ||
154 | do_check( | ||
155 | r#" | ||
156 | fn main() { foo<|>+bar;} | ||
157 | "#, | ||
158 | &["foo", "foo+bar"] | ||
159 | ); | ||
160 | do_check( | ||
161 | r#" | ||
162 | fn main() { foo+<|>bar;} | ||
163 | "#, | ||
164 | &["bar", "foo+bar"] | ||
165 | ); | ||
166 | } | ||
167 | } | ||
diff --git a/crates/ra_editor/src/lib.rs b/crates/ra_editor/src/lib.rs new file mode 100644 index 000000000..78ed34c7c --- /dev/null +++ b/crates/ra_editor/src/lib.rs | |||
@@ -0,0 +1,228 @@ | |||
1 | extern crate ra_syntax; | ||
2 | extern crate superslice; | ||
3 | extern crate itertools; | ||
4 | extern crate join_to_string; | ||
5 | #[cfg(test)] | ||
6 | #[macro_use] | ||
7 | extern crate test_utils as _test_utils; | ||
8 | |||
9 | mod extend_selection; | ||
10 | mod symbols; | ||
11 | mod line_index; | ||
12 | mod edit; | ||
13 | mod code_actions; | ||
14 | mod typing; | ||
15 | mod completion; | ||
16 | mod scope; | ||
17 | #[cfg(test)] | ||
18 | mod test_utils; | ||
19 | |||
20 | use ra_syntax::{ | ||
21 | File, TextUnit, TextRange, SyntaxNodeRef, | ||
22 | ast::{self, AstNode, NameOwner}, | ||
23 | algo::{walk, find_leaf_at_offset, ancestors}, | ||
24 | SyntaxKind::{self, *}, | ||
25 | }; | ||
26 | pub use ra_syntax::AtomEdit; | ||
27 | pub use self::{ | ||
28 | line_index::{LineIndex, LineCol}, | ||
29 | extend_selection::extend_selection, | ||
30 | symbols::{StructureNode, file_structure, FileSymbol, file_symbols}, | ||
31 | edit::{EditBuilder, Edit}, | ||
32 | code_actions::{ | ||
33 | LocalEdit, | ||
34 | flip_comma, add_derive, add_impl, | ||
35 | introduce_variable, | ||
36 | }, | ||
37 | typing::{join_lines, on_eq_typed}, | ||
38 | completion::{scope_completion, CompletionItem}, | ||
39 | }; | ||
40 | |||
41 | #[derive(Debug)] | ||
42 | pub struct HighlightedRange { | ||
43 | pub range: TextRange, | ||
44 | pub tag: &'static str, | ||
45 | } | ||
46 | |||
47 | #[derive(Debug)] | ||
48 | pub struct Diagnostic { | ||
49 | pub range: TextRange, | ||
50 | pub msg: String, | ||
51 | } | ||
52 | |||
53 | #[derive(Debug)] | ||
54 | pub struct Runnable { | ||
55 | pub range: TextRange, | ||
56 | pub kind: RunnableKind, | ||
57 | } | ||
58 | |||
59 | #[derive(Debug)] | ||
60 | pub enum RunnableKind { | ||
61 | Test { name: String }, | ||
62 | Bin, | ||
63 | } | ||
64 | |||
65 | pub fn matching_brace(file: &File, offset: TextUnit) -> Option<TextUnit> { | ||
66 | const BRACES: &[SyntaxKind] = &[ | ||
67 | L_CURLY, R_CURLY, | ||
68 | L_BRACK, R_BRACK, | ||
69 | L_PAREN, R_PAREN, | ||
70 | L_ANGLE, R_ANGLE, | ||
71 | ]; | ||
72 | let (brace_node, brace_idx) = find_leaf_at_offset(file.syntax(), offset) | ||
73 | .filter_map(|node| { | ||
74 | let idx = BRACES.iter().position(|&brace| brace == node.kind())?; | ||
75 | Some((node, idx)) | ||
76 | }) | ||
77 | .next()?; | ||
78 | let parent = brace_node.parent()?; | ||
79 | let matching_kind = BRACES[brace_idx ^ 1]; | ||
80 | let matching_node = parent.children() | ||
81 | .find(|node| node.kind() == matching_kind)?; | ||
82 | Some(matching_node.range().start()) | ||
83 | } | ||
84 | |||
85 | pub fn highlight(file: &File) -> Vec<HighlightedRange> { | ||
86 | let mut res = Vec::new(); | ||
87 | for node in walk::preorder(file.syntax()) { | ||
88 | let tag = match node.kind() { | ||
89 | ERROR => "error", | ||
90 | COMMENT | DOC_COMMENT => "comment", | ||
91 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string", | ||
92 | ATTR => "attribute", | ||
93 | NAME_REF => "text", | ||
94 | NAME => "function", | ||
95 | INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal", | ||
96 | LIFETIME => "parameter", | ||
97 | k if k.is_keyword() => "keyword", | ||
98 | _ => continue, | ||
99 | }; | ||
100 | res.push(HighlightedRange { | ||
101 | range: node.range(), | ||
102 | tag, | ||
103 | }) | ||
104 | } | ||
105 | res | ||
106 | } | ||
107 | |||
108 | pub fn diagnostics(file: &File) -> Vec<Diagnostic> { | ||
109 | let mut res = Vec::new(); | ||
110 | |||
111 | for node in walk::preorder(file.syntax()) { | ||
112 | if node.kind() == ERROR { | ||
113 | res.push(Diagnostic { | ||
114 | range: node.range(), | ||
115 | msg: "Syntax Error".to_string(), | ||
116 | }); | ||
117 | } | ||
118 | } | ||
119 | res.extend(file.errors().into_iter().map(|err| Diagnostic { | ||
120 | range: TextRange::offset_len(err.offset, 1.into()), | ||
121 | msg: err.msg, | ||
122 | })); | ||
123 | res | ||
124 | } | ||
125 | |||
126 | pub fn syntax_tree(file: &File) -> String { | ||
127 | ::ra_syntax::utils::dump_tree(file.syntax()) | ||
128 | } | ||
129 | |||
130 | pub fn runnables(file: &File) -> Vec<Runnable> { | ||
131 | walk::preorder(file.syntax()) | ||
132 | .filter_map(ast::FnDef::cast) | ||
133 | .filter_map(|f| { | ||
134 | let name = f.name()?.text(); | ||
135 | let kind = if name == "main" { | ||
136 | RunnableKind::Bin | ||
137 | } else if f.has_atom_attr("test") { | ||
138 | RunnableKind::Test { | ||
139 | name: name.to_string() | ||
140 | } | ||
141 | } else { | ||
142 | return None; | ||
143 | }; | ||
144 | Some(Runnable { | ||
145 | range: f.syntax().range(), | ||
146 | kind, | ||
147 | }) | ||
148 | }) | ||
149 | .collect() | ||
150 | } | ||
151 | |||
152 | pub fn find_node_at_offset<'a, N: AstNode<'a>>( | ||
153 | syntax: SyntaxNodeRef<'a>, | ||
154 | offset: TextUnit, | ||
155 | ) -> Option<N> { | ||
156 | let leaves = find_leaf_at_offset(syntax, offset); | ||
157 | let leaf = leaves.clone() | ||
158 | .find(|leaf| !leaf.kind().is_trivia()) | ||
159 | .or_else(|| leaves.right_biased())?; | ||
160 | ancestors(leaf) | ||
161 | .filter_map(N::cast) | ||
162 | .next() | ||
163 | } | ||
164 | |||
165 | #[cfg(test)] | ||
166 | mod tests { | ||
167 | use super::*; | ||
168 | use test_utils::{assert_eq_dbg, extract_offset, add_cursor}; | ||
169 | |||
170 | #[test] | ||
171 | fn test_highlighting() { | ||
172 | let file = File::parse(r#" | ||
173 | // comment | ||
174 | fn main() {} | ||
175 | println!("Hello, {}!", 92); | ||
176 | "#); | ||
177 | let hls = highlight(&file); | ||
178 | assert_eq_dbg( | ||
179 | r#"[HighlightedRange { range: [1; 11), tag: "comment" }, | ||
180 | HighlightedRange { range: [12; 14), tag: "keyword" }, | ||
181 | HighlightedRange { range: [15; 19), tag: "function" }, | ||
182 | HighlightedRange { range: [29; 36), tag: "text" }, | ||
183 | HighlightedRange { range: [38; 50), tag: "string" }, | ||
184 | HighlightedRange { range: [52; 54), tag: "literal" }]"#, | ||
185 | &hls, | ||
186 | ); | ||
187 | } | ||
188 | |||
189 | #[test] | ||
190 | fn test_runnables() { | ||
191 | let file = File::parse(r#" | ||
192 | fn main() {} | ||
193 | |||
194 | #[test] | ||
195 | fn test_foo() {} | ||
196 | |||
197 | #[test] | ||
198 | #[ignore] | ||
199 | fn test_foo() {} | ||
200 | "#); | ||
201 | let runnables = runnables(&file); | ||
202 | assert_eq_dbg( | ||
203 | r#"[Runnable { range: [1; 13), kind: Bin }, | ||
204 | Runnable { range: [15; 39), kind: Test { name: "test_foo" } }, | ||
205 | Runnable { range: [41; 75), kind: Test { name: "test_foo" } }]"#, | ||
206 | &runnables, | ||
207 | ) | ||
208 | } | ||
209 | |||
210 | #[test] | ||
211 | fn test_matching_brace() { | ||
212 | fn do_check(before: &str, after: &str) { | ||
213 | let (pos, before) = extract_offset(before); | ||
214 | let file = File::parse(&before); | ||
215 | let new_pos = match matching_brace(&file, pos) { | ||
216 | None => pos, | ||
217 | Some(pos) => pos, | ||
218 | }; | ||
219 | let actual = add_cursor(&before, new_pos); | ||
220 | assert_eq_text!(after, &actual); | ||
221 | } | ||
222 | |||
223 | do_check( | ||
224 | "struct Foo { a: i32, }<|>", | ||
225 | "struct Foo <|>{ a: i32, }", | ||
226 | ); | ||
227 | } | ||
228 | } | ||
diff --git a/crates/ra_editor/src/line_index.rs b/crates/ra_editor/src/line_index.rs new file mode 100644 index 000000000..9cd8da3a8 --- /dev/null +++ b/crates/ra_editor/src/line_index.rs | |||
@@ -0,0 +1,62 @@ | |||
1 | use superslice::Ext; | ||
2 | use ::TextUnit; | ||
3 | |||
4 | #[derive(Clone, Debug, Hash)] | ||
5 | pub struct LineIndex { | ||
6 | newlines: Vec<TextUnit>, | ||
7 | } | ||
8 | |||
9 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | ||
10 | pub struct LineCol { | ||
11 | pub line: u32, | ||
12 | pub col: TextUnit, | ||
13 | } | ||
14 | |||
15 | impl LineIndex { | ||
16 | pub fn new(text: &str) -> LineIndex { | ||
17 | let mut newlines = vec![0.into()]; | ||
18 | let mut curr = 0.into(); | ||
19 | for c in text.chars() { | ||
20 | curr += TextUnit::of_char(c); | ||
21 | if c == '\n' { | ||
22 | newlines.push(curr); | ||
23 | } | ||
24 | } | ||
25 | LineIndex { newlines } | ||
26 | } | ||
27 | |||
28 | pub fn line_col(&self, offset: TextUnit) -> LineCol { | ||
29 | let line = self.newlines.upper_bound(&offset) - 1; | ||
30 | let line_start_offset = self.newlines[line]; | ||
31 | let col = offset - line_start_offset; | ||
32 | return LineCol { line: line as u32, col }; | ||
33 | } | ||
34 | |||
35 | pub fn offset(&self, line_col: LineCol) -> TextUnit { | ||
36 | //TODO: return Result | ||
37 | self.newlines[line_col.line as usize] + line_col.col | ||
38 | } | ||
39 | } | ||
40 | |||
41 | #[test] | ||
42 | fn test_line_index() { | ||
43 | let text = "hello\nworld"; | ||
44 | let index = LineIndex::new(text); | ||
45 | assert_eq!(index.line_col(0.into()), LineCol { line: 0, col: 0.into() }); | ||
46 | assert_eq!(index.line_col(1.into()), LineCol { line: 0, col: 1.into() }); | ||
47 | assert_eq!(index.line_col(5.into()), LineCol { line: 0, col: 5.into() }); | ||
48 | assert_eq!(index.line_col(6.into()), LineCol { line: 1, col: 0.into() }); | ||
49 | assert_eq!(index.line_col(7.into()), LineCol { line: 1, col: 1.into() }); | ||
50 | assert_eq!(index.line_col(8.into()), LineCol { line: 1, col: 2.into() }); | ||
51 | assert_eq!(index.line_col(10.into()), LineCol { line: 1, col: 4.into() }); | ||
52 | assert_eq!(index.line_col(11.into()), LineCol { line: 1, col: 5.into() }); | ||
53 | assert_eq!(index.line_col(12.into()), LineCol { line: 1, col: 6.into() }); | ||
54 | |||
55 | let text = "\nhello\nworld"; | ||
56 | let index = LineIndex::new(text); | ||
57 | assert_eq!(index.line_col(0.into()), LineCol { line: 0, col: 0.into() }); | ||
58 | assert_eq!(index.line_col(1.into()), LineCol { line: 1, col: 0.into() }); | ||
59 | assert_eq!(index.line_col(2.into()), LineCol { line: 1, col: 1.into() }); | ||
60 | assert_eq!(index.line_col(6.into()), LineCol { line: 1, col: 5.into() }); | ||
61 | assert_eq!(index.line_col(7.into()), LineCol { line: 2, col: 0.into() }); | ||
62 | } | ||
diff --git a/crates/ra_editor/src/scope/fn_scope.rs b/crates/ra_editor/src/scope/fn_scope.rs new file mode 100644 index 000000000..3ae5276a2 --- /dev/null +++ b/crates/ra_editor/src/scope/fn_scope.rs | |||
@@ -0,0 +1,329 @@ | |||
1 | use std::{ | ||
2 | fmt, | ||
3 | collections::HashMap, | ||
4 | }; | ||
5 | |||
6 | use ra_syntax::{ | ||
7 | SyntaxNodeRef, SyntaxNode, SmolStr, AstNode, | ||
8 | ast::{self, NameOwner, LoopBodyOwner, ArgListOwner}, | ||
9 | algo::{ancestors, generate, walk::preorder} | ||
10 | }; | ||
11 | |||
12 | type ScopeId = usize; | ||
13 | |||
14 | #[derive(Debug)] | ||
15 | pub struct FnScopes { | ||
16 | pub self_param: Option<SyntaxNode>, | ||
17 | scopes: Vec<ScopeData>, | ||
18 | scope_for: HashMap<SyntaxNode, ScopeId>, | ||
19 | } | ||
20 | |||
21 | impl FnScopes { | ||
22 | pub fn new(fn_def: ast::FnDef) -> FnScopes { | ||
23 | let mut scopes = FnScopes { | ||
24 | self_param: fn_def.param_list() | ||
25 | .and_then(|it| it.self_param()) | ||
26 | .map(|it| it.syntax().owned()), | ||
27 | scopes: Vec::new(), | ||
28 | scope_for: HashMap::new() | ||
29 | }; | ||
30 | let root = scopes.root_scope(); | ||
31 | scopes.add_params_bindings(root, fn_def.param_list()); | ||
32 | if let Some(body) = fn_def.body() { | ||
33 | compute_block_scopes(body, &mut scopes, root) | ||
34 | } | ||
35 | scopes | ||
36 | } | ||
37 | pub fn entries(&self, scope: ScopeId) -> &[ScopeEntry] { | ||
38 | &self.scopes[scope].entries | ||
39 | } | ||
40 | pub fn scope_chain<'a>(&'a self, node: SyntaxNodeRef) -> impl Iterator<Item=ScopeId> + 'a { | ||
41 | generate(self.scope_for(node), move |&scope| self.scopes[scope].parent) | ||
42 | } | ||
43 | fn root_scope(&mut self) -> ScopeId { | ||
44 | let res = self.scopes.len(); | ||
45 | self.scopes.push(ScopeData { parent: None, entries: vec![] }); | ||
46 | res | ||
47 | } | ||
48 | fn new_scope(&mut self, parent: ScopeId) -> ScopeId { | ||
49 | let res = self.scopes.len(); | ||
50 | self.scopes.push(ScopeData { parent: Some(parent), entries: vec![] }); | ||
51 | res | ||
52 | } | ||
53 | fn add_bindings(&mut self, scope: ScopeId, pat: ast::Pat) { | ||
54 | let entries = preorder(pat.syntax()) | ||
55 | .filter_map(ast::BindPat::cast) | ||
56 | .filter_map(ScopeEntry::new); | ||
57 | self.scopes[scope].entries.extend(entries); | ||
58 | } | ||
59 | fn add_params_bindings(&mut self, scope: ScopeId, params: Option<ast::ParamList>) { | ||
60 | params.into_iter() | ||
61 | .flat_map(|it| it.params()) | ||
62 | .filter_map(|it| it.pat()) | ||
63 | .for_each(|it| self.add_bindings(scope, it)); | ||
64 | } | ||
65 | fn set_scope(&mut self, node: SyntaxNodeRef, scope: ScopeId) { | ||
66 | self.scope_for.insert(node.owned(), scope); | ||
67 | } | ||
68 | fn scope_for(&self, node: SyntaxNodeRef) -> Option<ScopeId> { | ||
69 | ancestors(node) | ||
70 | .filter_map(|it| self.scope_for.get(&it.owned()).map(|&scope| scope)) | ||
71 | .next() | ||
72 | } | ||
73 | } | ||
74 | |||
75 | pub struct ScopeEntry { | ||
76 | syntax: SyntaxNode | ||
77 | } | ||
78 | |||
79 | impl ScopeEntry { | ||
80 | fn new(pat: ast::BindPat) -> Option<ScopeEntry> { | ||
81 | if pat.name().is_some() { | ||
82 | Some(ScopeEntry { syntax: pat.syntax().owned() }) | ||
83 | } else { | ||
84 | None | ||
85 | } | ||
86 | } | ||
87 | pub fn name(&self) -> SmolStr { | ||
88 | self.ast().name() | ||
89 | .unwrap() | ||
90 | .text() | ||
91 | } | ||
92 | fn ast(&self) -> ast::BindPat { | ||
93 | ast::BindPat::cast(self.syntax.borrowed()) | ||
94 | .unwrap() | ||
95 | } | ||
96 | } | ||
97 | |||
98 | impl fmt::Debug for ScopeEntry { | ||
99 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
100 | f.debug_struct("ScopeEntry") | ||
101 | .field("name", &self.name()) | ||
102 | .field("syntax", &self.syntax) | ||
103 | .finish() | ||
104 | } | ||
105 | } | ||
106 | |||
107 | fn compute_block_scopes(block: ast::Block, scopes: &mut FnScopes, mut scope: ScopeId) { | ||
108 | for stmt in block.statements() { | ||
109 | match stmt { | ||
110 | ast::Stmt::LetStmt(stmt) => { | ||
111 | if let Some(expr) = stmt.initializer() { | ||
112 | scopes.set_scope(expr.syntax(), scope); | ||
113 | compute_expr_scopes(expr, scopes, scope); | ||
114 | } | ||
115 | scope = scopes.new_scope(scope); | ||
116 | if let Some(pat) = stmt.pat() { | ||
117 | scopes.add_bindings(scope, pat); | ||
118 | } | ||
119 | } | ||
120 | ast::Stmt::ExprStmt(expr_stmt) => { | ||
121 | if let Some(expr) = expr_stmt.expr() { | ||
122 | scopes.set_scope(expr.syntax(), scope); | ||
123 | compute_expr_scopes(expr, scopes, scope); | ||
124 | } | ||
125 | } | ||
126 | } | ||
127 | } | ||
128 | if let Some(expr) = block.expr() { | ||
129 | scopes.set_scope(expr.syntax(), scope); | ||
130 | compute_expr_scopes(expr, scopes, scope); | ||
131 | } | ||
132 | } | ||
133 | |||
134 | fn compute_expr_scopes(expr: ast::Expr, scopes: &mut FnScopes, scope: ScopeId) { | ||
135 | match expr { | ||
136 | ast::Expr::IfExpr(e) => { | ||
137 | let cond_scope = e.condition().and_then(|cond| { | ||
138 | compute_cond_scopes(cond, scopes, scope) | ||
139 | }); | ||
140 | if let Some(block) = e.then_branch() { | ||
141 | compute_block_scopes(block, scopes, cond_scope.unwrap_or(scope)); | ||
142 | } | ||
143 | if let Some(block) = e.else_branch() { | ||
144 | compute_block_scopes(block, scopes, scope); | ||
145 | } | ||
146 | }, | ||
147 | ast::Expr::BlockExpr(e) => { | ||
148 | if let Some(block) = e.block() { | ||
149 | compute_block_scopes(block, scopes, scope); | ||
150 | } | ||
151 | } | ||
152 | ast::Expr::LoopExpr(e) => { | ||
153 | if let Some(block) = e.loop_body() { | ||
154 | compute_block_scopes(block, scopes, scope); | ||
155 | } | ||
156 | } | ||
157 | ast::Expr::WhileExpr(e) => { | ||
158 | let cond_scope = e.condition().and_then(|cond| { | ||
159 | compute_cond_scopes(cond, scopes, scope) | ||
160 | }); | ||
161 | if let Some(block) = e.loop_body() { | ||
162 | compute_block_scopes(block, scopes, cond_scope.unwrap_or(scope)); | ||
163 | } | ||
164 | } | ||
165 | ast::Expr::ForExpr(e) => { | ||
166 | if let Some(expr) = e.iterable() { | ||
167 | compute_expr_scopes(expr, scopes, scope); | ||
168 | } | ||
169 | let mut scope = scope; | ||
170 | if let Some(pat) = e.pat() { | ||
171 | scope = scopes.new_scope(scope); | ||
172 | scopes.add_bindings(scope, pat); | ||
173 | } | ||
174 | if let Some(block) = e.loop_body() { | ||
175 | compute_block_scopes(block, scopes, scope); | ||
176 | } | ||
177 | } | ||
178 | ast::Expr::LambdaExpr(e) => { | ||
179 | let mut scope = scopes.new_scope(scope); | ||
180 | scopes.add_params_bindings(scope, e.param_list()); | ||
181 | if let Some(body) = e.body() { | ||
182 | scopes.set_scope(body.syntax(), scope); | ||
183 | compute_expr_scopes(body, scopes, scope); | ||
184 | } | ||
185 | } | ||
186 | ast::Expr::CallExpr(e) => { | ||
187 | compute_call_scopes(e.expr(), e.arg_list(), scopes, scope); | ||
188 | } | ||
189 | ast::Expr::MethodCallExpr(e) => { | ||
190 | compute_call_scopes(e.expr(), e.arg_list(), scopes, scope); | ||
191 | } | ||
192 | ast::Expr::MatchExpr(e) => { | ||
193 | if let Some(expr) = e.expr() { | ||
194 | compute_expr_scopes(expr, scopes, scope); | ||
195 | } | ||
196 | for arm in e.match_arm_list().into_iter().flat_map(|it| it.arms()) { | ||
197 | let scope = scopes.new_scope(scope); | ||
198 | for pat in arm.pats() { | ||
199 | scopes.add_bindings(scope, pat); | ||
200 | } | ||
201 | if let Some(expr) = arm.expr() { | ||
202 | compute_expr_scopes(expr, scopes, scope); | ||
203 | } | ||
204 | } | ||
205 | } | ||
206 | _ => { | ||
207 | expr.syntax().children() | ||
208 | .filter_map(ast::Expr::cast) | ||
209 | .for_each(|expr| compute_expr_scopes(expr, scopes, scope)) | ||
210 | } | ||
211 | }; | ||
212 | |||
213 | fn compute_call_scopes( | ||
214 | receiver: Option<ast::Expr>, | ||
215 | arg_list: Option<ast::ArgList>, | ||
216 | scopes: &mut FnScopes, scope: ScopeId, | ||
217 | ) { | ||
218 | arg_list.into_iter() | ||
219 | .flat_map(|it| it.args()) | ||
220 | .chain(receiver) | ||
221 | .for_each(|expr| compute_expr_scopes(expr, scopes, scope)); | ||
222 | } | ||
223 | |||
224 | fn compute_cond_scopes(cond: ast::Condition, scopes: &mut FnScopes, scope: ScopeId) -> Option<ScopeId> { | ||
225 | if let Some(expr) = cond.expr() { | ||
226 | compute_expr_scopes(expr, scopes, scope); | ||
227 | } | ||
228 | if let Some(pat) = cond.pat() { | ||
229 | let s = scopes.new_scope(scope); | ||
230 | scopes.add_bindings(s, pat); | ||
231 | Some(s) | ||
232 | } else { | ||
233 | None | ||
234 | } | ||
235 | } | ||
236 | } | ||
237 | |||
238 | #[derive(Debug)] | ||
239 | struct ScopeData { | ||
240 | parent: Option<ScopeId>, | ||
241 | entries: Vec<ScopeEntry> | ||
242 | } | ||
243 | |||
244 | #[cfg(test)] | ||
245 | mod tests { | ||
246 | use super::*; | ||
247 | use ra_syntax::File; | ||
248 | use {find_node_at_offset, test_utils::extract_offset}; | ||
249 | |||
250 | fn do_check(code: &str, expected: &[&str]) { | ||
251 | let (off, code) = extract_offset(code); | ||
252 | let code = { | ||
253 | let mut buf = String::new(); | ||
254 | let off = u32::from(off) as usize; | ||
255 | buf.push_str(&code[..off]); | ||
256 | buf.push_str("marker"); | ||
257 | buf.push_str(&code[off..]); | ||
258 | buf | ||
259 | }; | ||
260 | let file = File::parse(&code); | ||
261 | let marker: ast::PathExpr = find_node_at_offset(file.syntax(), off).unwrap(); | ||
262 | let fn_def: ast::FnDef = find_node_at_offset(file.syntax(), off).unwrap(); | ||
263 | let scopes = FnScopes::new(fn_def); | ||
264 | let actual = scopes.scope_chain(marker.syntax()) | ||
265 | .flat_map(|scope| scopes.entries(scope)) | ||
266 | .map(|it| it.name()) | ||
267 | .collect::<Vec<_>>(); | ||
268 | assert_eq!(expected, actual.as_slice()); | ||
269 | } | ||
270 | |||
271 | #[test] | ||
272 | fn test_lambda_scope() { | ||
273 | do_check(r" | ||
274 | fn quux(foo: i32) { | ||
275 | let f = |bar, baz: i32| { | ||
276 | <|> | ||
277 | }; | ||
278 | }", | ||
279 | &["bar", "baz", "foo"], | ||
280 | ); | ||
281 | } | ||
282 | |||
283 | #[test] | ||
284 | fn test_call_scope() { | ||
285 | do_check(r" | ||
286 | fn quux() { | ||
287 | f(|x| <|> ); | ||
288 | }", | ||
289 | &["x"], | ||
290 | ); | ||
291 | } | ||
292 | |||
293 | #[test] | ||
294 | fn test_metod_call_scope() { | ||
295 | do_check(r" | ||
296 | fn quux() { | ||
297 | z.f(|x| <|> ); | ||
298 | }", | ||
299 | &["x"], | ||
300 | ); | ||
301 | } | ||
302 | |||
303 | #[test] | ||
304 | fn test_loop_scope() { | ||
305 | do_check(r" | ||
306 | fn quux() { | ||
307 | loop { | ||
308 | let x = (); | ||
309 | <|> | ||
310 | }; | ||
311 | }", | ||
312 | &["x"], | ||
313 | ); | ||
314 | } | ||
315 | |||
316 | #[test] | ||
317 | fn test_match() { | ||
318 | do_check(r" | ||
319 | fn quux() { | ||
320 | match () { | ||
321 | Some(x) => { | ||
322 | <|> | ||
323 | } | ||
324 | }; | ||
325 | }", | ||
326 | &["x"], | ||
327 | ); | ||
328 | } | ||
329 | } | ||
diff --git a/crates/ra_editor/src/scope/mod.rs b/crates/ra_editor/src/scope/mod.rs new file mode 100644 index 000000000..2f25230f8 --- /dev/null +++ b/crates/ra_editor/src/scope/mod.rs | |||
@@ -0,0 +1,8 @@ | |||
1 | mod fn_scope; | ||
2 | mod mod_scope; | ||
3 | |||
4 | pub use self::{ | ||
5 | fn_scope::FnScopes, | ||
6 | mod_scope::ModuleScope, | ||
7 | }; | ||
8 | |||
diff --git a/crates/ra_editor/src/scope/mod_scope.rs b/crates/ra_editor/src/scope/mod_scope.rs new file mode 100644 index 000000000..d2a3e7c58 --- /dev/null +++ b/crates/ra_editor/src/scope/mod_scope.rs | |||
@@ -0,0 +1,115 @@ | |||
1 | use ra_syntax::{ | ||
2 | AstNode, SyntaxNode, SyntaxNodeRef, SmolStr, | ||
3 | ast::{self, AstChildren}, | ||
4 | }; | ||
5 | |||
6 | pub struct ModuleScope { | ||
7 | entries: Vec<Entry>, | ||
8 | } | ||
9 | |||
10 | pub struct Entry { | ||
11 | node: SyntaxNode, | ||
12 | kind: EntryKind, | ||
13 | } | ||
14 | |||
15 | enum EntryKind { | ||
16 | Item, Import, | ||
17 | } | ||
18 | |||
19 | impl ModuleScope { | ||
20 | pub fn new(items: AstChildren<ast::ModuleItem>) -> ModuleScope { | ||
21 | let mut entries = Vec::new(); | ||
22 | for item in items { | ||
23 | let entry = match item { | ||
24 | ast::ModuleItem::StructDef(item) => Entry::new(item), | ||
25 | ast::ModuleItem::EnumDef(item) => Entry::new(item), | ||
26 | ast::ModuleItem::FnDef(item) => Entry::new(item), | ||
27 | ast::ModuleItem::ConstDef(item) => Entry::new(item), | ||
28 | ast::ModuleItem::StaticDef(item) => Entry::new(item), | ||
29 | ast::ModuleItem::TraitDef(item) => Entry::new(item), | ||
30 | ast::ModuleItem::TypeDef(item) => Entry::new(item), | ||
31 | ast::ModuleItem::Module(item) => Entry::new(item), | ||
32 | ast::ModuleItem::UseItem(item) => { | ||
33 | if let Some(tree) = item.use_tree() { | ||
34 | collect_imports(tree, &mut entries); | ||
35 | } | ||
36 | continue; | ||
37 | }, | ||
38 | ast::ModuleItem::ExternCrateItem(_) | | ||
39 | ast::ModuleItem::ImplItem(_) => continue, | ||
40 | }; | ||
41 | entries.extend(entry) | ||
42 | } | ||
43 | |||
44 | ModuleScope { entries } | ||
45 | } | ||
46 | |||
47 | pub fn entries(&self) -> &[Entry] { | ||
48 | self.entries.as_slice() | ||
49 | } | ||
50 | } | ||
51 | |||
52 | impl Entry { | ||
53 | fn new<'a>(item: impl ast::NameOwner<'a>) -> Option<Entry> { | ||
54 | let name = item.name()?; | ||
55 | Some(Entry { node: name.syntax().owned(), kind: EntryKind::Item }) | ||
56 | } | ||
57 | fn new_import(path: ast::Path) -> Option<Entry> { | ||
58 | let name_ref = path.segment()?.name_ref()?; | ||
59 | Some(Entry { node: name_ref.syntax().owned(), kind: EntryKind::Import }) | ||
60 | } | ||
61 | pub fn name(&self) -> SmolStr { | ||
62 | match self.kind { | ||
63 | EntryKind::Item => | ||
64 | ast::Name::cast(self.node.borrowed()).unwrap() | ||
65 | .text(), | ||
66 | EntryKind::Import => | ||
67 | ast::NameRef::cast(self.node.borrowed()).unwrap() | ||
68 | .text(), | ||
69 | } | ||
70 | } | ||
71 | pub fn syntax(&self) -> SyntaxNodeRef { | ||
72 | self.node.borrowed() | ||
73 | } | ||
74 | } | ||
75 | |||
76 | fn collect_imports(tree: ast::UseTree, acc: &mut Vec<Entry>) { | ||
77 | if let Some(use_tree_list) = tree.use_tree_list() { | ||
78 | return use_tree_list.use_trees().for_each(|it| collect_imports(it, acc)); | ||
79 | } | ||
80 | if let Some(path) = tree.path() { | ||
81 | acc.extend(Entry::new_import(path)); | ||
82 | } | ||
83 | } | ||
84 | |||
85 | |||
86 | #[cfg(test)] | ||
87 | mod tests { | ||
88 | use super::*; | ||
89 | use ra_syntax::{File, ast::ModuleItemOwner}; | ||
90 | |||
91 | fn do_check(code: &str, expected: &[&str]) { | ||
92 | let file = File::parse(&code); | ||
93 | let scope = ModuleScope::new(file.ast().items()); | ||
94 | let actual = scope.entries | ||
95 | .iter() | ||
96 | .map(|it| it.name()) | ||
97 | .collect::<Vec<_>>(); | ||
98 | assert_eq!(expected, actual.as_slice()); | ||
99 | } | ||
100 | |||
101 | #[test] | ||
102 | fn test_module_scope() { | ||
103 | do_check(" | ||
104 | struct Foo; | ||
105 | enum Bar {} | ||
106 | mod baz {} | ||
107 | fn quux() {} | ||
108 | use x::{ | ||
109 | y::z, | ||
110 | t, | ||
111 | }; | ||
112 | type T = (); | ||
113 | ", &["Foo", "Bar", "baz", "quux", "z", "t", "T"]) | ||
114 | } | ||
115 | } | ||
diff --git a/crates/ra_editor/src/symbols.rs b/crates/ra_editor/src/symbols.rs new file mode 100644 index 000000000..917984177 --- /dev/null +++ b/crates/ra_editor/src/symbols.rs | |||
@@ -0,0 +1,167 @@ | |||
1 | use ra_syntax::{ | ||
2 | SyntaxKind, SyntaxNodeRef, AstNode, File, SmolStr, | ||
3 | ast::{self, NameOwner}, | ||
4 | algo::{ | ||
5 | visit::{visitor, Visitor}, | ||
6 | walk::{walk, WalkEvent, preorder}, | ||
7 | }, | ||
8 | }; | ||
9 | use TextRange; | ||
10 | |||
11 | #[derive(Debug, Clone)] | ||
12 | pub struct StructureNode { | ||
13 | pub parent: Option<usize>, | ||
14 | pub label: String, | ||
15 | pub navigation_range: TextRange, | ||
16 | pub node_range: TextRange, | ||
17 | pub kind: SyntaxKind, | ||
18 | } | ||
19 | |||
20 | #[derive(Debug, Clone, Hash)] | ||
21 | pub struct FileSymbol { | ||
22 | pub name: SmolStr, | ||
23 | pub node_range: TextRange, | ||
24 | pub kind: SyntaxKind, | ||
25 | } | ||
26 | |||
27 | pub fn file_symbols(file: &File) -> Vec<FileSymbol> { | ||
28 | preorder(file.syntax()) | ||
29 | .filter_map(to_symbol) | ||
30 | .collect() | ||
31 | } | ||
32 | |||
33 | fn to_symbol(node: SyntaxNodeRef) -> Option<FileSymbol> { | ||
34 | fn decl<'a, N: NameOwner<'a>>(node: N) -> Option<FileSymbol> { | ||
35 | let name = node.name()?; | ||
36 | Some(FileSymbol { | ||
37 | name: name.text(), | ||
38 | node_range: node.syntax().range(), | ||
39 | kind: node.syntax().kind(), | ||
40 | }) | ||
41 | } | ||
42 | visitor() | ||
43 | .visit(decl::<ast::FnDef>) | ||
44 | .visit(decl::<ast::StructDef>) | ||
45 | .visit(decl::<ast::EnumDef>) | ||
46 | .visit(decl::<ast::TraitDef>) | ||
47 | .visit(decl::<ast::Module>) | ||
48 | .visit(decl::<ast::TypeDef>) | ||
49 | .visit(decl::<ast::ConstDef>) | ||
50 | .visit(decl::<ast::StaticDef>) | ||
51 | .accept(node)? | ||
52 | } | ||
53 | |||
54 | |||
55 | pub fn file_structure(file: &File) -> Vec<StructureNode> { | ||
56 | let mut res = Vec::new(); | ||
57 | let mut stack = Vec::new(); | ||
58 | |||
59 | for event in walk(file.syntax()) { | ||
60 | match event { | ||
61 | WalkEvent::Enter(node) => { | ||
62 | match structure_node(node) { | ||
63 | Some(mut symbol) => { | ||
64 | symbol.parent = stack.last().map(|&n| n); | ||
65 | stack.push(res.len()); | ||
66 | res.push(symbol); | ||
67 | } | ||
68 | None => (), | ||
69 | } | ||
70 | } | ||
71 | WalkEvent::Exit(node) => { | ||
72 | if structure_node(node).is_some() { | ||
73 | stack.pop().unwrap(); | ||
74 | } | ||
75 | } | ||
76 | } | ||
77 | } | ||
78 | res | ||
79 | } | ||
80 | |||
81 | fn structure_node(node: SyntaxNodeRef) -> Option<StructureNode> { | ||
82 | fn decl<'a, N: NameOwner<'a>>(node: N) -> Option<StructureNode> { | ||
83 | let name = node.name()?; | ||
84 | Some(StructureNode { | ||
85 | parent: None, | ||
86 | label: name.text().to_string(), | ||
87 | navigation_range: name.syntax().range(), | ||
88 | node_range: node.syntax().range(), | ||
89 | kind: node.syntax().kind(), | ||
90 | }) | ||
91 | } | ||
92 | |||
93 | visitor() | ||
94 | .visit(decl::<ast::FnDef>) | ||
95 | .visit(decl::<ast::StructDef>) | ||
96 | .visit(decl::<ast::NamedFieldDef>) | ||
97 | .visit(decl::<ast::EnumDef>) | ||
98 | .visit(decl::<ast::TraitDef>) | ||
99 | .visit(decl::<ast::Module>) | ||
100 | .visit(decl::<ast::TypeDef>) | ||
101 | .visit(decl::<ast::ConstDef>) | ||
102 | .visit(decl::<ast::StaticDef>) | ||
103 | .visit(|im: ast::ImplItem| { | ||
104 | let target_type = im.target_type()?; | ||
105 | let target_trait = im.target_trait(); | ||
106 | let label = match target_trait { | ||
107 | None => format!("impl {}", target_type.syntax().text()), | ||
108 | Some(t) => format!( | ||
109 | "impl {} for {}", | ||
110 | t.syntax().text(), | ||
111 | target_type.syntax().text(), | ||
112 | ), | ||
113 | }; | ||
114 | |||
115 | let node = StructureNode { | ||
116 | parent: None, | ||
117 | label, | ||
118 | navigation_range: target_type.syntax().range(), | ||
119 | node_range: im.syntax().range(), | ||
120 | kind: im.syntax().kind(), | ||
121 | }; | ||
122 | Some(node) | ||
123 | }) | ||
124 | .accept(node)? | ||
125 | } | ||
126 | |||
127 | #[cfg(test)] | ||
128 | mod tests { | ||
129 | use super::*; | ||
130 | use test_utils::assert_eq_dbg; | ||
131 | |||
132 | #[test] | ||
133 | fn test_file_structure() { | ||
134 | let file = File::parse(r#" | ||
135 | struct Foo { | ||
136 | x: i32 | ||
137 | } | ||
138 | |||
139 | mod m { | ||
140 | fn bar() {} | ||
141 | } | ||
142 | |||
143 | enum E { X, Y(i32) } | ||
144 | type T = (); | ||
145 | static S: i32 = 92; | ||
146 | const C: i32 = 92; | ||
147 | |||
148 | impl E {} | ||
149 | |||
150 | impl fmt::Debug for E {} | ||
151 | "#); | ||
152 | let symbols = file_structure(&file); | ||
153 | assert_eq_dbg( | ||
154 | r#"[StructureNode { parent: None, label: "Foo", navigation_range: [8; 11), node_range: [1; 26), kind: STRUCT_DEF }, | ||
155 | StructureNode { parent: Some(0), label: "x", navigation_range: [18; 19), node_range: [18; 24), kind: NAMED_FIELD_DEF }, | ||
156 | StructureNode { parent: None, label: "m", navigation_range: [32; 33), node_range: [28; 53), kind: MODULE }, | ||
157 | StructureNode { parent: Some(2), label: "bar", navigation_range: [43; 46), node_range: [40; 51), kind: FN_DEF }, | ||
158 | StructureNode { parent: None, label: "E", navigation_range: [60; 61), node_range: [55; 75), kind: ENUM_DEF }, | ||
159 | StructureNode { parent: None, label: "T", navigation_range: [81; 82), node_range: [76; 88), kind: TYPE_DEF }, | ||
160 | StructureNode { parent: None, label: "S", navigation_range: [96; 97), node_range: [89; 108), kind: STATIC_DEF }, | ||
161 | StructureNode { parent: None, label: "C", navigation_range: [115; 116), node_range: [109; 127), kind: CONST_DEF }, | ||
162 | StructureNode { parent: None, label: "impl E", navigation_range: [134; 135), node_range: [129; 138), kind: IMPL_ITEM }, | ||
163 | StructureNode { parent: None, label: "impl fmt::Debug for E", navigation_range: [160; 161), node_range: [140; 164), kind: IMPL_ITEM }]"#, | ||
164 | &symbols, | ||
165 | ) | ||
166 | } | ||
167 | } | ||
diff --git a/crates/ra_editor/src/test_utils.rs b/crates/ra_editor/src/test_utils.rs new file mode 100644 index 000000000..c4ea4db6c --- /dev/null +++ b/crates/ra_editor/src/test_utils.rs | |||
@@ -0,0 +1,37 @@ | |||
1 | use ra_syntax::{File, TextUnit, TextRange}; | ||
2 | pub use _test_utils::*; | ||
3 | use LocalEdit; | ||
4 | |||
5 | pub fn check_action<F: Fn(&File, TextUnit) -> Option<LocalEdit>> ( | ||
6 | before: &str, | ||
7 | after: &str, | ||
8 | f: F, | ||
9 | ) { | ||
10 | let (before_cursor_pos, before) = extract_offset(before); | ||
11 | let file = File::parse(&before); | ||
12 | let result = f(&file, before_cursor_pos).expect("code action is not applicable"); | ||
13 | let actual = result.edit.apply(&before); | ||
14 | let actual_cursor_pos = match result.cursor_position { | ||
15 | None => result.edit.apply_to_offset(before_cursor_pos).unwrap(), | ||
16 | Some(off) => off, | ||
17 | }; | ||
18 | let actual = add_cursor(&actual, actual_cursor_pos); | ||
19 | assert_eq_text!(after, &actual); | ||
20 | } | ||
21 | |||
22 | pub fn check_action_range<F: Fn(&File, TextRange) -> Option<LocalEdit>> ( | ||
23 | before: &str, | ||
24 | after: &str, | ||
25 | f: F, | ||
26 | ) { | ||
27 | let (range, before) = extract_range(before); | ||
28 | let file = File::parse(&before); | ||
29 | let result = f(&file, range).expect("code action is not applicable"); | ||
30 | let actual = result.edit.apply(&before); | ||
31 | let actual_cursor_pos = match result.cursor_position { | ||
32 | None => result.edit.apply_to_offset(range.start()).unwrap(), | ||
33 | Some(off) => off, | ||
34 | }; | ||
35 | let actual = add_cursor(&actual, actual_cursor_pos); | ||
36 | assert_eq_text!(after, &actual); | ||
37 | } | ||
diff --git a/crates/ra_editor/src/typing.rs b/crates/ra_editor/src/typing.rs new file mode 100644 index 000000000..0f4e7e0d0 --- /dev/null +++ b/crates/ra_editor/src/typing.rs | |||
@@ -0,0 +1,348 @@ | |||
1 | use std::mem; | ||
2 | |||
3 | use ra_syntax::{ | ||
4 | TextUnit, TextRange, SyntaxNodeRef, File, AstNode, SyntaxKind, | ||
5 | ast, | ||
6 | algo::{ | ||
7 | walk::preorder, | ||
8 | find_covering_node, | ||
9 | }, | ||
10 | text_utils::{intersect, contains_offset_nonstrict}, | ||
11 | SyntaxKind::*, | ||
12 | }; | ||
13 | |||
14 | use {LocalEdit, EditBuilder, find_node_at_offset}; | ||
15 | |||
16 | pub fn join_lines(file: &File, range: TextRange) -> LocalEdit { | ||
17 | let range = if range.is_empty() { | ||
18 | let syntax = file.syntax(); | ||
19 | let text = syntax.text().slice(range.start()..); | ||
20 | let pos = match text.find('\n') { | ||
21 | None => return LocalEdit { | ||
22 | edit: EditBuilder::new().finish(), | ||
23 | cursor_position: None | ||
24 | }, | ||
25 | Some(pos) => pos | ||
26 | }; | ||
27 | TextRange::offset_len( | ||
28 | range.start() + pos, | ||
29 | TextUnit::of_char('\n'), | ||
30 | ) | ||
31 | } else { | ||
32 | range | ||
33 | }; | ||
34 | let node = find_covering_node(file.syntax(), range); | ||
35 | let mut edit = EditBuilder::new(); | ||
36 | for node in preorder(node) { | ||
37 | let text = match node.leaf_text() { | ||
38 | Some(text) => text, | ||
39 | None => continue, | ||
40 | }; | ||
41 | let range = match intersect(range, node.range()) { | ||
42 | Some(range) => range, | ||
43 | None => continue, | ||
44 | } - node.range().start(); | ||
45 | for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') { | ||
46 | let pos: TextUnit = (pos as u32).into(); | ||
47 | let off = node.range().start() + range.start() + pos; | ||
48 | if !edit.invalidates_offset(off) { | ||
49 | remove_newline(&mut edit, node, text.as_str(), off); | ||
50 | } | ||
51 | } | ||
52 | } | ||
53 | |||
54 | LocalEdit { | ||
55 | edit: edit.finish(), | ||
56 | cursor_position: None, | ||
57 | } | ||
58 | } | ||
59 | |||
60 | pub fn on_eq_typed(file: &File, offset: TextUnit) -> Option<LocalEdit> { | ||
61 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | ||
62 | if let_stmt.has_semi() { | ||
63 | return None; | ||
64 | } | ||
65 | if let Some(expr) = let_stmt.initializer() { | ||
66 | let expr_range = expr.syntax().range(); | ||
67 | if contains_offset_nonstrict(expr_range, offset) && offset != expr_range.start() { | ||
68 | return None; | ||
69 | } | ||
70 | if file.syntax().text().slice(offset..expr_range.start()).contains('\n') { | ||
71 | return None; | ||
72 | } | ||
73 | } else { | ||
74 | return None; | ||
75 | } | ||
76 | let offset = let_stmt.syntax().range().end(); | ||
77 | let mut edit = EditBuilder::new(); | ||
78 | edit.insert(offset, ";".to_string()); | ||
79 | Some(LocalEdit { | ||
80 | edit: edit.finish(), | ||
81 | cursor_position: None, | ||
82 | }) | ||
83 | } | ||
84 | |||
85 | fn remove_newline( | ||
86 | edit: &mut EditBuilder, | ||
87 | node: SyntaxNodeRef, | ||
88 | node_text: &str, | ||
89 | offset: TextUnit, | ||
90 | ) { | ||
91 | if node.kind() == WHITESPACE && node_text.bytes().filter(|&b| b == b'\n').count() == 1 { | ||
92 | if join_single_expr_block(edit, node).is_some() { | ||
93 | return | ||
94 | } | ||
95 | match (node.prev_sibling(), node.next_sibling()) { | ||
96 | (Some(prev), Some(next)) => { | ||
97 | let range = TextRange::from_to(prev.range().start(), node.range().end()); | ||
98 | if is_trailing_comma(prev.kind(), next.kind()) { | ||
99 | edit.delete(range); | ||
100 | } else if no_space_required(prev.kind(), next.kind()) { | ||
101 | edit.delete(node.range()); | ||
102 | } else if prev.kind() == COMMA && next.kind() == R_CURLY { | ||
103 | edit.replace(range, " ".to_string()); | ||
104 | } else { | ||
105 | edit.replace( | ||
106 | node.range(), | ||
107 | compute_ws(prev, next).to_string(), | ||
108 | ); | ||
109 | } | ||
110 | return; | ||
111 | } | ||
112 | _ => (), | ||
113 | } | ||
114 | } | ||
115 | |||
116 | let suff = &node_text[TextRange::from_to( | ||
117 | offset - node.range().start() + TextUnit::of_char('\n'), | ||
118 | TextUnit::of_str(node_text), | ||
119 | )]; | ||
120 | let spaces = suff.bytes().take_while(|&b| b == b' ').count(); | ||
121 | |||
122 | edit.replace( | ||
123 | TextRange::offset_len(offset, ((spaces + 1) as u32).into()), | ||
124 | " ".to_string(), | ||
125 | ); | ||
126 | } | ||
127 | |||
128 | fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool { | ||
129 | match (left, right) { | ||
130 | (COMMA, R_PAREN) | (COMMA, R_BRACK) => true, | ||
131 | _ => false | ||
132 | } | ||
133 | } | ||
134 | |||
135 | fn no_space_required(left: SyntaxKind, right: SyntaxKind) -> bool { | ||
136 | match (left, right) { | ||
137 | (_, DOT) => true, | ||
138 | _ => false | ||
139 | } | ||
140 | } | ||
141 | |||
142 | fn join_single_expr_block( | ||
143 | edit: &mut EditBuilder, | ||
144 | node: SyntaxNodeRef, | ||
145 | ) -> Option<()> { | ||
146 | let block = ast::Block::cast(node.parent()?)?; | ||
147 | let block_expr = ast::BlockExpr::cast(block.syntax().parent()?)?; | ||
148 | let expr = single_expr(block)?; | ||
149 | edit.replace( | ||
150 | block_expr.syntax().range(), | ||
151 | expr.syntax().text().to_string(), | ||
152 | ); | ||
153 | Some(()) | ||
154 | } | ||
155 | |||
156 | fn single_expr(block: ast::Block) -> Option<ast::Expr> { | ||
157 | let mut res = None; | ||
158 | for child in block.syntax().children() { | ||
159 | if let Some(expr) = ast::Expr::cast(child) { | ||
160 | if expr.syntax().text().contains('\n') { | ||
161 | return None; | ||
162 | } | ||
163 | if mem::replace(&mut res, Some(expr)).is_some() { | ||
164 | return None; | ||
165 | } | ||
166 | } else { | ||
167 | match child.kind() { | ||
168 | WHITESPACE | L_CURLY | R_CURLY => (), | ||
169 | _ => return None, | ||
170 | } | ||
171 | } | ||
172 | } | ||
173 | res | ||
174 | } | ||
175 | |||
176 | fn compute_ws(left: SyntaxNodeRef, right: SyntaxNodeRef) -> &'static str { | ||
177 | match left.kind() { | ||
178 | L_PAREN | L_BRACK => return "", | ||
179 | _ => (), | ||
180 | } | ||
181 | match right.kind() { | ||
182 | R_PAREN | R_BRACK => return "", | ||
183 | _ => (), | ||
184 | } | ||
185 | " " | ||
186 | } | ||
187 | |||
188 | #[cfg(test)] | ||
189 | mod tests { | ||
190 | use super::*; | ||
191 | use test_utils::{check_action, extract_range, extract_offset}; | ||
192 | |||
193 | fn check_join_lines(before: &str, after: &str) { | ||
194 | check_action(before, after, |file, offset| { | ||
195 | let range = TextRange::offset_len(offset, 0.into()); | ||
196 | let res = join_lines(file, range); | ||
197 | Some(res) | ||
198 | }) | ||
199 | } | ||
200 | |||
201 | #[test] | ||
202 | fn test_join_lines_comma() { | ||
203 | check_join_lines(r" | ||
204 | fn foo() { | ||
205 | <|>foo(1, | ||
206 | ) | ||
207 | } | ||
208 | ", r" | ||
209 | fn foo() { | ||
210 | <|>foo(1) | ||
211 | } | ||
212 | "); | ||
213 | } | ||
214 | |||
215 | #[test] | ||
216 | fn test_join_lines_lambda_block() { | ||
217 | check_join_lines(r" | ||
218 | pub fn reparse(&self, edit: &AtomEdit) -> File { | ||
219 | <|>self.incremental_reparse(edit).unwrap_or_else(|| { | ||
220 | self.full_reparse(edit) | ||
221 | }) | ||
222 | } | ||
223 | ", r" | ||
224 | pub fn reparse(&self, edit: &AtomEdit) -> File { | ||
225 | <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit)) | ||
226 | } | ||
227 | "); | ||
228 | } | ||
229 | |||
230 | #[test] | ||
231 | fn test_join_lines_block() { | ||
232 | check_join_lines(r" | ||
233 | fn foo() { | ||
234 | foo(<|>{ | ||
235 | 92 | ||
236 | }) | ||
237 | }", r" | ||
238 | fn foo() { | ||
239 | foo(<|>92) | ||
240 | }"); | ||
241 | } | ||
242 | |||
243 | fn check_join_lines_sel(before: &str, after: &str) { | ||
244 | let (sel, before) = extract_range(before); | ||
245 | let file = File::parse(&before); | ||
246 | let result = join_lines(&file, sel); | ||
247 | let actual = result.edit.apply(&before); | ||
248 | assert_eq_text!(after, &actual); | ||
249 | } | ||
250 | |||
251 | #[test] | ||
252 | fn test_join_lines_selection_fn_args() { | ||
253 | check_join_lines_sel(r" | ||
254 | fn foo() { | ||
255 | <|>foo(1, | ||
256 | 2, | ||
257 | 3, | ||
258 | <|>) | ||
259 | } | ||
260 | ", r" | ||
261 | fn foo() { | ||
262 | foo(1, 2, 3) | ||
263 | } | ||
264 | "); | ||
265 | } | ||
266 | |||
267 | #[test] | ||
268 | fn test_join_lines_selection_struct() { | ||
269 | check_join_lines_sel(r" | ||
270 | struct Foo <|>{ | ||
271 | f: u32, | ||
272 | }<|> | ||
273 | ", r" | ||
274 | struct Foo { f: u32 } | ||
275 | "); | ||
276 | } | ||
277 | |||
278 | #[test] | ||
279 | fn test_join_lines_selection_dot_chain() { | ||
280 | check_join_lines_sel(r" | ||
281 | fn foo() { | ||
282 | join(<|>type_params.type_params() | ||
283 | .filter_map(|it| it.name()) | ||
284 | .map(|it| it.text())<|>) | ||
285 | }", r" | ||
286 | fn foo() { | ||
287 | join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text())) | ||
288 | }"); | ||
289 | } | ||
290 | |||
291 | #[test] | ||
292 | fn test_join_lines_selection_lambda_block_body() { | ||
293 | check_join_lines_sel(r" | ||
294 | pub fn handle_find_matching_brace() { | ||
295 | params.offsets | ||
296 | .map(|offset| <|>{ | ||
297 | world.analysis().matching_brace(&file, offset).unwrap_or(offset) | ||
298 | }<|>) | ||
299 | .collect(); | ||
300 | }", r" | ||
301 | pub fn handle_find_matching_brace() { | ||
302 | params.offsets | ||
303 | .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset)) | ||
304 | .collect(); | ||
305 | }"); | ||
306 | } | ||
307 | |||
308 | #[test] | ||
309 | fn test_on_eq_typed() { | ||
310 | fn do_check(before: &str, after: &str) { | ||
311 | let (offset, before) = extract_offset(before); | ||
312 | let file = File::parse(&before); | ||
313 | let result = on_eq_typed(&file, offset).unwrap(); | ||
314 | let actual = result.edit.apply(&before); | ||
315 | assert_eq_text!(after, &actual); | ||
316 | } | ||
317 | |||
318 | // do_check(r" | ||
319 | // fn foo() { | ||
320 | // let foo =<|> | ||
321 | // } | ||
322 | // ", r" | ||
323 | // fn foo() { | ||
324 | // let foo =; | ||
325 | // } | ||
326 | // "); | ||
327 | do_check(r" | ||
328 | fn foo() { | ||
329 | let foo =<|> 1 + 1 | ||
330 | } | ||
331 | ", r" | ||
332 | fn foo() { | ||
333 | let foo = 1 + 1; | ||
334 | } | ||
335 | "); | ||
336 | // do_check(r" | ||
337 | // fn foo() { | ||
338 | // let foo =<|> | ||
339 | // let bar = 1; | ||
340 | // } | ||
341 | // ", r" | ||
342 | // fn foo() { | ||
343 | // let foo =; | ||
344 | // let bar = 1; | ||
345 | // } | ||
346 | // "); | ||
347 | } | ||
348 | } | ||