aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_editor
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2018-09-16 10:54:24 +0100
committerAleksey Kladov <[email protected]>2018-09-16 11:07:39 +0100
commitb5021411a84822cb3f1e3aeffad9550dd15bdeb6 (patch)
tree9dca564f8e51b298dced01c4ce669c756dce3142 /crates/ra_editor
parentba0bfeee12e19da40b5eabc8d0408639af10e96f (diff)
rename all things
Diffstat (limited to 'crates/ra_editor')
-rw-r--r--crates/ra_editor/Cargo.toml15
-rw-r--r--crates/ra_editor/src/code_actions.rs218
-rw-r--r--crates/ra_editor/src/completion.rs480
-rw-r--r--crates/ra_editor/src/edit.rs84
-rw-r--r--crates/ra_editor/src/extend_selection.rs167
-rw-r--r--crates/ra_editor/src/lib.rs228
-rw-r--r--crates/ra_editor/src/line_index.rs62
-rw-r--r--crates/ra_editor/src/scope/fn_scope.rs329
-rw-r--r--crates/ra_editor/src/scope/mod.rs8
-rw-r--r--crates/ra_editor/src/scope/mod_scope.rs115
-rw-r--r--crates/ra_editor/src/symbols.rs167
-rw-r--r--crates/ra_editor/src/test_utils.rs37
-rw-r--r--crates/ra_editor/src/typing.rs348
13 files changed, 2258 insertions, 0 deletions
diff --git a/crates/ra_editor/Cargo.toml b/crates/ra_editor/Cargo.toml
new file mode 100644
index 000000000..40e3254ff
--- /dev/null
+++ b/crates/ra_editor/Cargo.toml
@@ -0,0 +1,15 @@
1[package]
2name = "ra_editor"
3version = "0.1.0"
4authors = ["Aleksey Kladov <[email protected]>"]
5publish = false
6
7[dependencies]
8itertools = "0.7.8"
9superslice = "0.1.0"
10join_to_string = "0.1.1"
11
12ra_syntax = { path = "../ra_syntax" }
13
14[dev-dependencies]
15test_utils = { path = "../test_utils" }
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 @@
1use join_to_string::join;
2
3use 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
16use {EditBuilder, Edit, find_node_at_offset};
17
18#[derive(Debug)]
19pub struct LocalEdit {
20 pub edit: Edit,
21 pub cursor_position: Option<TextUnit>,
22}
23
24pub 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
41pub 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
68pub 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
102pub 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
132fn 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)]
139mod 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 "
194fn foo() {
195 foo(<|>1 + 1<|>);
196}", "
197fn 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() {
206check_action_range(
207 "
208fn foo() {
209 <|>1 + 1<|>;
210}", "
211fn 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 @@
1use std::collections::{HashSet, HashMap};
2
3use 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
13use {
14 AtomEdit, find_node_at_offset,
15 scope::{FnScopes, ModuleScope},
16};
17
18#[derive(Debug)]
19pub 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
28pub 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
57fn 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
92fn 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
124fn 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
132fn 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
153fn 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
172fn 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
196fn 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
204fn 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)]
226mod 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 @@
1use {TextRange, TextUnit};
2use ra_syntax::{
3 AtomEdit,
4 text_utils::contains_offset_nonstrict,
5};
6
7#[derive(Debug, Clone)]
8pub struct Edit {
9 atoms: Vec<AtomEdit>,
10}
11
12#[derive(Debug)]
13pub struct EditBuilder {
14 atoms: Vec<AtomEdit>
15}
16
17impl 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
43impl 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 @@
1use ra_syntax::{
2 File, TextRange, SyntaxNodeRef, TextUnit,
3 SyntaxKind::*,
4 algo::{find_leaf_at_offset, LeafAtOffset, find_covering_node, ancestors, Direction, siblings},
5};
6
7pub fn extend_selection(file: &File, range: TextRange) -> Option<TextRange> {
8 let syntax = file.syntax();
9 extend(syntax.borrowed(), range)
10}
11
12pub(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
39fn 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
62fn 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
73fn 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
86fn 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)]
99mod 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#"
127impl 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#"
140fn 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#"
156fn main() { foo<|>+bar;}
157 "#,
158 &["foo", "foo+bar"]
159 );
160 do_check(
161 r#"
162fn 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 @@
1extern crate ra_syntax;
2extern crate superslice;
3extern crate itertools;
4extern crate join_to_string;
5#[cfg(test)]
6#[macro_use]
7extern crate test_utils as _test_utils;
8
9mod extend_selection;
10mod symbols;
11mod line_index;
12mod edit;
13mod code_actions;
14mod typing;
15mod completion;
16mod scope;
17#[cfg(test)]
18mod test_utils;
19
20use ra_syntax::{
21 File, TextUnit, TextRange, SyntaxNodeRef,
22 ast::{self, AstNode, NameOwner},
23 algo::{walk, find_leaf_at_offset, ancestors},
24 SyntaxKind::{self, *},
25};
26pub use ra_syntax::AtomEdit;
27pub 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)]
42pub struct HighlightedRange {
43 pub range: TextRange,
44 pub tag: &'static str,
45}
46
47#[derive(Debug)]
48pub struct Diagnostic {
49 pub range: TextRange,
50 pub msg: String,
51}
52
53#[derive(Debug)]
54pub struct Runnable {
55 pub range: TextRange,
56 pub kind: RunnableKind,
57}
58
59#[derive(Debug)]
60pub enum RunnableKind {
61 Test { name: String },
62 Bin,
63}
64
65pub 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
85pub 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
108pub 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
126pub fn syntax_tree(file: &File) -> String {
127 ::ra_syntax::utils::dump_tree(file.syntax())
128}
129
130pub 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
152pub 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)]
166mod 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
174fn 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#"
192fn main() {}
193
194#[test]
195fn test_foo() {}
196
197#[test]
198#[ignore]
199fn 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 @@
1use superslice::Ext;
2use ::TextUnit;
3
4#[derive(Clone, Debug, Hash)]
5pub struct LineIndex {
6 newlines: Vec<TextUnit>,
7}
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub struct LineCol {
11 pub line: u32,
12 pub col: TextUnit,
13}
14
15impl 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]
42fn 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 @@
1use std::{
2 fmt,
3 collections::HashMap,
4};
5
6use ra_syntax::{
7 SyntaxNodeRef, SyntaxNode, SmolStr, AstNode,
8 ast::{self, NameOwner, LoopBodyOwner, ArgListOwner},
9 algo::{ancestors, generate, walk::preorder}
10};
11
12type ScopeId = usize;
13
14#[derive(Debug)]
15pub struct FnScopes {
16 pub self_param: Option<SyntaxNode>,
17 scopes: Vec<ScopeData>,
18 scope_for: HashMap<SyntaxNode, ScopeId>,
19}
20
21impl 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
75pub struct ScopeEntry {
76 syntax: SyntaxNode
77}
78
79impl 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
98impl 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
107fn 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
134fn 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)]
239struct ScopeData {
240 parent: Option<ScopeId>,
241 entries: Vec<ScopeEntry>
242}
243
244#[cfg(test)]
245mod 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 @@
1mod fn_scope;
2mod mod_scope;
3
4pub 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 @@
1use ra_syntax::{
2 AstNode, SyntaxNode, SyntaxNodeRef, SmolStr,
3 ast::{self, AstChildren},
4};
5
6pub struct ModuleScope {
7 entries: Vec<Entry>,
8}
9
10pub struct Entry {
11 node: SyntaxNode,
12 kind: EntryKind,
13}
14
15enum EntryKind {
16 Item, Import,
17}
18
19impl 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
52impl 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
76fn 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)]
87mod 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 @@
1use 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};
9use TextRange;
10
11#[derive(Debug, Clone)]
12pub 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)]
21pub struct FileSymbol {
22 pub name: SmolStr,
23 pub node_range: TextRange,
24 pub kind: SyntaxKind,
25}
26
27pub fn file_symbols(file: &File) -> Vec<FileSymbol> {
28 preorder(file.syntax())
29 .filter_map(to_symbol)
30 .collect()
31}
32
33fn 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
55pub 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
81fn 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)]
128mod 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#"
135struct Foo {
136 x: i32
137}
138
139mod m {
140 fn bar() {}
141}
142
143enum E { X, Y(i32) }
144type T = ();
145static S: i32 = 92;
146const C: i32 = 92;
147
148impl E {}
149
150impl 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 @@
1use ra_syntax::{File, TextUnit, TextRange};
2pub use _test_utils::*;
3use LocalEdit;
4
5pub 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
22pub 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 @@
1use std::mem;
2
3use 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
14use {LocalEdit, EditBuilder, find_node_at_offset};
15
16pub 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
60pub 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
85fn 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
128fn 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
135fn no_space_required(left: SyntaxKind, right: SyntaxKind) -> bool {
136 match (left, right) {
137 (_, DOT) => true,
138 _ => false
139 }
140}
141
142fn 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
156fn 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
176fn 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)]
189mod 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"
204fn foo() {
205 <|>foo(1,
206 )
207}
208", r"
209fn foo() {
210 <|>foo(1)
211}
212");
213 }
214
215 #[test]
216 fn test_join_lines_lambda_block() {
217 check_join_lines(r"
218pub fn reparse(&self, edit: &AtomEdit) -> File {
219 <|>self.incremental_reparse(edit).unwrap_or_else(|| {
220 self.full_reparse(edit)
221 })
222}
223", r"
224pub 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"
233fn foo() {
234 foo(<|>{
235 92
236 })
237}", r"
238fn 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"
254fn foo() {
255 <|>foo(1,
256 2,
257 3,
258 <|>)
259}
260 ", r"
261fn 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"
270struct Foo <|>{
271 f: u32,
272}<|>
273 ", r"
274struct Foo { f: u32 }
275 ");
276 }
277
278 #[test]
279 fn test_join_lines_selection_dot_chain() {
280 check_join_lines_sel(r"
281fn foo() {
282 join(<|>type_params.type_params()
283 .filter_map(|it| it.name())
284 .map(|it| it.text())<|>)
285}", r"
286fn 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"
294pub 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"
301pub 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"
328fn foo() {
329 let foo =<|> 1 + 1
330}
331", r"
332fn 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}