diff options
author | Seivan Heidari <[email protected]> | 2019-11-28 07:19:14 +0000 |
---|---|---|
committer | Seivan Heidari <[email protected]> | 2019-11-28 07:19:14 +0000 |
commit | 18a0937585b836ec5ed054b9ae48e0156ab6d9ef (patch) | |
tree | 9de2c0267ddcc00df717f90034d0843d751a851b /crates/ra_ide/src/extend_selection.rs | |
parent | a7394b44c870f585eacfeb3036a33471aff49ff8 (diff) | |
parent | 484acc8a61d599662ed63a4cbda091d38a982551 (diff) |
Merge branch 'master' of https://github.com/rust-analyzer/rust-analyzer into feature/themes
Diffstat (limited to 'crates/ra_ide/src/extend_selection.rs')
-rw-r--r-- | crates/ra_ide/src/extend_selection.rs | 452 |
1 files changed, 452 insertions, 0 deletions
diff --git a/crates/ra_ide/src/extend_selection.rs b/crates/ra_ide/src/extend_selection.rs new file mode 100644 index 000000000..4b7bfc0b1 --- /dev/null +++ b/crates/ra_ide/src/extend_selection.rs | |||
@@ -0,0 +1,452 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_db::SourceDatabase; | ||
4 | use ra_syntax::{ | ||
5 | algo::find_covering_element, | ||
6 | ast::{self, AstNode, AstToken}, | ||
7 | Direction, NodeOrToken, | ||
8 | SyntaxKind::{self, *}, | ||
9 | SyntaxNode, SyntaxToken, TextRange, TextUnit, TokenAtOffset, T, | ||
10 | }; | ||
11 | |||
12 | use crate::{db::RootDatabase, FileRange}; | ||
13 | |||
14 | // FIXME: restore macro support | ||
15 | pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange { | ||
16 | let parse = db.parse(frange.file_id); | ||
17 | try_extend_selection(parse.tree().syntax(), frange.range).unwrap_or(frange.range) | ||
18 | } | ||
19 | |||
20 | fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange> { | ||
21 | let string_kinds = [COMMENT, STRING, RAW_STRING, BYTE_STRING, RAW_BYTE_STRING]; | ||
22 | let list_kinds = [ | ||
23 | RECORD_FIELD_PAT_LIST, | ||
24 | MATCH_ARM_LIST, | ||
25 | RECORD_FIELD_DEF_LIST, | ||
26 | TUPLE_FIELD_DEF_LIST, | ||
27 | RECORD_FIELD_LIST, | ||
28 | ENUM_VARIANT_LIST, | ||
29 | USE_TREE_LIST, | ||
30 | TYPE_PARAM_LIST, | ||
31 | TYPE_ARG_LIST, | ||
32 | TYPE_BOUND_LIST, | ||
33 | PARAM_LIST, | ||
34 | ARG_LIST, | ||
35 | ARRAY_EXPR, | ||
36 | TUPLE_EXPR, | ||
37 | WHERE_CLAUSE, | ||
38 | ]; | ||
39 | |||
40 | if range.is_empty() { | ||
41 | let offset = range.start(); | ||
42 | let mut leaves = root.token_at_offset(offset); | ||
43 | if leaves.clone().all(|it| it.kind() == WHITESPACE) { | ||
44 | return Some(extend_ws(root, leaves.next()?, offset)); | ||
45 | } | ||
46 | let leaf_range = match leaves { | ||
47 | TokenAtOffset::None => return None, | ||
48 | TokenAtOffset::Single(l) => { | ||
49 | if string_kinds.contains(&l.kind()) { | ||
50 | extend_single_word_in_comment_or_string(&l, offset) | ||
51 | .unwrap_or_else(|| l.text_range()) | ||
52 | } else { | ||
53 | l.text_range() | ||
54 | } | ||
55 | } | ||
56 | TokenAtOffset::Between(l, r) => pick_best(l, r).text_range(), | ||
57 | }; | ||
58 | return Some(leaf_range); | ||
59 | }; | ||
60 | let node = match find_covering_element(root, range) { | ||
61 | NodeOrToken::Token(token) => { | ||
62 | if token.text_range() != range { | ||
63 | return Some(token.text_range()); | ||
64 | } | ||
65 | if let Some(comment) = ast::Comment::cast(token.clone()) { | ||
66 | if let Some(range) = extend_comments(comment) { | ||
67 | return Some(range); | ||
68 | } | ||
69 | } | ||
70 | token.parent() | ||
71 | } | ||
72 | NodeOrToken::Node(node) => node, | ||
73 | }; | ||
74 | if node.text_range() != range { | ||
75 | return Some(node.text_range()); | ||
76 | } | ||
77 | |||
78 | // Using shallowest node with same range allows us to traverse siblings. | ||
79 | let node = node.ancestors().take_while(|n| n.text_range() == node.text_range()).last().unwrap(); | ||
80 | |||
81 | if node.parent().map(|n| list_kinds.contains(&n.kind())) == Some(true) { | ||
82 | if let Some(range) = extend_list_item(&node) { | ||
83 | return Some(range); | ||
84 | } | ||
85 | } | ||
86 | |||
87 | node.parent().map(|it| it.text_range()) | ||
88 | } | ||
89 | |||
90 | fn extend_single_word_in_comment_or_string( | ||
91 | leaf: &SyntaxToken, | ||
92 | offset: TextUnit, | ||
93 | ) -> Option<TextRange> { | ||
94 | let text: &str = leaf.text(); | ||
95 | let cursor_position: u32 = (offset - leaf.text_range().start()).into(); | ||
96 | |||
97 | let (before, after) = text.split_at(cursor_position as usize); | ||
98 | |||
99 | fn non_word_char(c: char) -> bool { | ||
100 | !(c.is_alphanumeric() || c == '_') | ||
101 | } | ||
102 | |||
103 | let start_idx = before.rfind(non_word_char)? as u32; | ||
104 | let end_idx = after.find(non_word_char).unwrap_or_else(|| after.len()) as u32; | ||
105 | |||
106 | let from: TextUnit = (start_idx + 1).into(); | ||
107 | let to: TextUnit = (cursor_position + end_idx).into(); | ||
108 | |||
109 | let range = TextRange::from_to(from, to); | ||
110 | if range.is_empty() { | ||
111 | None | ||
112 | } else { | ||
113 | Some(range + leaf.text_range().start()) | ||
114 | } | ||
115 | } | ||
116 | |||
117 | fn extend_ws(root: &SyntaxNode, ws: SyntaxToken, offset: TextUnit) -> TextRange { | ||
118 | let ws_text = ws.text(); | ||
119 | let suffix = TextRange::from_to(offset, ws.text_range().end()) - ws.text_range().start(); | ||
120 | let prefix = TextRange::from_to(ws.text_range().start(), offset) - ws.text_range().start(); | ||
121 | let ws_suffix = &ws_text.as_str()[suffix]; | ||
122 | let ws_prefix = &ws_text.as_str()[prefix]; | ||
123 | if ws_text.contains('\n') && !ws_suffix.contains('\n') { | ||
124 | if let Some(node) = ws.next_sibling_or_token() { | ||
125 | let start = match ws_prefix.rfind('\n') { | ||
126 | Some(idx) => ws.text_range().start() + TextUnit::from((idx + 1) as u32), | ||
127 | None => node.text_range().start(), | ||
128 | }; | ||
129 | let end = if root.text().char_at(node.text_range().end()) == Some('\n') { | ||
130 | node.text_range().end() + TextUnit::of_char('\n') | ||
131 | } else { | ||
132 | node.text_range().end() | ||
133 | }; | ||
134 | return TextRange::from_to(start, end); | ||
135 | } | ||
136 | } | ||
137 | ws.text_range() | ||
138 | } | ||
139 | |||
140 | fn pick_best<'a>(l: SyntaxToken, r: SyntaxToken) -> SyntaxToken { | ||
141 | return if priority(&r) > priority(&l) { r } else { l }; | ||
142 | fn priority(n: &SyntaxToken) -> usize { | ||
143 | match n.kind() { | ||
144 | WHITESPACE => 0, | ||
145 | IDENT | T![self] | T![super] | T![crate] | LIFETIME => 2, | ||
146 | _ => 1, | ||
147 | } | ||
148 | } | ||
149 | } | ||
150 | |||
151 | /// Extend list item selection to include nearby delimiter and whitespace. | ||
152 | fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> { | ||
153 | fn is_single_line_ws(node: &SyntaxToken) -> bool { | ||
154 | node.kind() == WHITESPACE && !node.text().contains('\n') | ||
155 | } | ||
156 | |||
157 | fn nearby_delimiter( | ||
158 | delimiter_kind: SyntaxKind, | ||
159 | node: &SyntaxNode, | ||
160 | dir: Direction, | ||
161 | ) -> Option<SyntaxToken> { | ||
162 | node.siblings_with_tokens(dir) | ||
163 | .skip(1) | ||
164 | .skip_while(|node| match node { | ||
165 | NodeOrToken::Node(_) => false, | ||
166 | NodeOrToken::Token(it) => is_single_line_ws(it), | ||
167 | }) | ||
168 | .next() | ||
169 | .and_then(|it| it.into_token()) | ||
170 | .filter(|node| node.kind() == delimiter_kind) | ||
171 | } | ||
172 | |||
173 | let delimiter = match node.kind() { | ||
174 | TYPE_BOUND => T![+], | ||
175 | _ => T![,], | ||
176 | }; | ||
177 | if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Prev) { | ||
178 | return Some(TextRange::from_to( | ||
179 | delimiter_node.text_range().start(), | ||
180 | node.text_range().end(), | ||
181 | )); | ||
182 | } | ||
183 | if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Next) { | ||
184 | // Include any following whitespace when delimiter is after list item. | ||
185 | let final_node = delimiter_node | ||
186 | .next_sibling_or_token() | ||
187 | .and_then(|it| it.into_token()) | ||
188 | .filter(|node| is_single_line_ws(node)) | ||
189 | .unwrap_or(delimiter_node); | ||
190 | |||
191 | return Some(TextRange::from_to(node.text_range().start(), final_node.text_range().end())); | ||
192 | } | ||
193 | |||
194 | None | ||
195 | } | ||
196 | |||
197 | fn extend_comments(comment: ast::Comment) -> Option<TextRange> { | ||
198 | let prev = adj_comments(&comment, Direction::Prev); | ||
199 | let next = adj_comments(&comment, Direction::Next); | ||
200 | if prev != next { | ||
201 | Some(TextRange::from_to( | ||
202 | prev.syntax().text_range().start(), | ||
203 | next.syntax().text_range().end(), | ||
204 | )) | ||
205 | } else { | ||
206 | None | ||
207 | } | ||
208 | } | ||
209 | |||
210 | fn adj_comments(comment: &ast::Comment, dir: Direction) -> ast::Comment { | ||
211 | let mut res = comment.clone(); | ||
212 | for element in comment.syntax().siblings_with_tokens(dir) { | ||
213 | let token = match element.as_token() { | ||
214 | None => break, | ||
215 | Some(token) => token, | ||
216 | }; | ||
217 | if let Some(c) = ast::Comment::cast(token.clone()) { | ||
218 | res = c | ||
219 | } else if token.kind() != WHITESPACE || token.text().contains("\n\n") { | ||
220 | break; | ||
221 | } | ||
222 | } | ||
223 | res | ||
224 | } | ||
225 | |||
226 | #[cfg(test)] | ||
227 | mod tests { | ||
228 | use ra_syntax::{AstNode, SourceFile}; | ||
229 | use test_utils::extract_offset; | ||
230 | |||
231 | use super::*; | ||
232 | |||
233 | fn do_check(before: &str, afters: &[&str]) { | ||
234 | let (cursor, before) = extract_offset(before); | ||
235 | let parse = SourceFile::parse(&before); | ||
236 | let mut range = TextRange::offset_len(cursor, 0.into()); | ||
237 | for &after in afters { | ||
238 | range = try_extend_selection(parse.tree().syntax(), range).unwrap(); | ||
239 | let actual = &before[range]; | ||
240 | assert_eq!(after, actual); | ||
241 | } | ||
242 | } | ||
243 | |||
244 | #[test] | ||
245 | fn test_extend_selection_arith() { | ||
246 | do_check(r#"fn foo() { <|>1 + 1 }"#, &["1", "1 + 1", "{ 1 + 1 }"]); | ||
247 | } | ||
248 | |||
249 | #[test] | ||
250 | fn test_extend_selection_list() { | ||
251 | do_check(r#"fn foo(<|>x: i32) {}"#, &["x", "x: i32"]); | ||
252 | do_check(r#"fn foo(<|>x: i32, y: i32) {}"#, &["x", "x: i32", "x: i32, "]); | ||
253 | do_check(r#"fn foo(<|>x: i32,y: i32) {}"#, &["x", "x: i32", "x: i32,"]); | ||
254 | do_check(r#"fn foo(x: i32, <|>y: i32) {}"#, &["y", "y: i32", ", y: i32"]); | ||
255 | do_check(r#"fn foo(x: i32, <|>y: i32, ) {}"#, &["y", "y: i32", ", y: i32"]); | ||
256 | do_check(r#"fn foo(x: i32,<|>y: i32) {}"#, &["y", "y: i32", ",y: i32"]); | ||
257 | |||
258 | do_check(r#"const FOO: [usize; 2] = [ 22<|> , 33];"#, &["22", "22 , "]); | ||
259 | do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|>];"#, &["33", ", 33"]); | ||
260 | do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|> ,];"#, &["33", ", 33"]); | ||
261 | |||
262 | do_check(r#"fn main() { (1, 2<|>) }"#, &["2", ", 2", "(1, 2)"]); | ||
263 | |||
264 | do_check( | ||
265 | r#" | ||
266 | const FOO: [usize; 2] = [ | ||
267 | 22, | ||
268 | <|>33, | ||
269 | ]"#, | ||
270 | &["33", "33,"], | ||
271 | ); | ||
272 | |||
273 | do_check( | ||
274 | r#" | ||
275 | const FOO: [usize; 2] = [ | ||
276 | 22 | ||
277 | , 33<|>, | ||
278 | ]"#, | ||
279 | &["33", ", 33"], | ||
280 | ); | ||
281 | } | ||
282 | |||
283 | #[test] | ||
284 | fn test_extend_selection_start_of_the_line() { | ||
285 | do_check( | ||
286 | r#" | ||
287 | impl S { | ||
288 | <|> fn foo() { | ||
289 | |||
290 | } | ||
291 | }"#, | ||
292 | &[" fn foo() {\n\n }\n"], | ||
293 | ); | ||
294 | } | ||
295 | |||
296 | #[test] | ||
297 | fn test_extend_selection_doc_comments() { | ||
298 | do_check( | ||
299 | r#" | ||
300 | struct A; | ||
301 | |||
302 | /// bla | ||
303 | /// bla | ||
304 | struct B { | ||
305 | <|> | ||
306 | } | ||
307 | "#, | ||
308 | &["\n \n", "{\n \n}", "/// bla\n/// bla\nstruct B {\n \n}"], | ||
309 | ) | ||
310 | } | ||
311 | |||
312 | #[test] | ||
313 | fn test_extend_selection_comments() { | ||
314 | do_check( | ||
315 | r#" | ||
316 | fn bar(){} | ||
317 | |||
318 | // fn foo() { | ||
319 | // 1 + <|>1 | ||
320 | // } | ||
321 | |||
322 | // fn foo(){} | ||
323 | "#, | ||
324 | &["1", "// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"], | ||
325 | ); | ||
326 | |||
327 | do_check( | ||
328 | r#" | ||
329 | // #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
330 | // pub enum Direction { | ||
331 | // <|> Next, | ||
332 | // Prev | ||
333 | // } | ||
334 | "#, | ||
335 | &[ | ||
336 | "// Next,", | ||
337 | "// #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n// pub enum Direction {\n// Next,\n// Prev\n// }", | ||
338 | ], | ||
339 | ); | ||
340 | |||
341 | do_check( | ||
342 | r#" | ||
343 | /* | ||
344 | foo | ||
345 | _bar1<|>*/ | ||
346 | "#, | ||
347 | &["_bar1", "/*\nfoo\n_bar1*/"], | ||
348 | ); | ||
349 | |||
350 | do_check(r#"//!<|>foo_2 bar"#, &["foo_2", "//!foo_2 bar"]); | ||
351 | |||
352 | do_check(r#"/<|>/foo bar"#, &["//foo bar"]); | ||
353 | } | ||
354 | |||
355 | #[test] | ||
356 | fn test_extend_selection_prefer_idents() { | ||
357 | do_check( | ||
358 | r#" | ||
359 | fn main() { foo<|>+bar;} | ||
360 | "#, | ||
361 | &["foo", "foo+bar"], | ||
362 | ); | ||
363 | do_check( | ||
364 | r#" | ||
365 | fn main() { foo+<|>bar;} | ||
366 | "#, | ||
367 | &["bar", "foo+bar"], | ||
368 | ); | ||
369 | } | ||
370 | |||
371 | #[test] | ||
372 | fn test_extend_selection_prefer_lifetimes() { | ||
373 | do_check(r#"fn foo<<|>'a>() {}"#, &["'a", "<'a>"]); | ||
374 | do_check(r#"fn foo<'a<|>>() {}"#, &["'a", "<'a>"]); | ||
375 | } | ||
376 | |||
377 | #[test] | ||
378 | fn test_extend_selection_select_first_word() { | ||
379 | do_check(r#"// foo bar b<|>az quxx"#, &["baz", "// foo bar baz quxx"]); | ||
380 | do_check( | ||
381 | r#" | ||
382 | impl S { | ||
383 | fn foo() { | ||
384 | // hel<|>lo world | ||
385 | } | ||
386 | } | ||
387 | "#, | ||
388 | &["hello", "// hello world"], | ||
389 | ); | ||
390 | } | ||
391 | |||
392 | #[test] | ||
393 | fn test_extend_selection_string() { | ||
394 | do_check( | ||
395 | r#" | ||
396 | fn bar(){} | ||
397 | |||
398 | " fn f<|>oo() {" | ||
399 | "#, | ||
400 | &["foo", "\" fn foo() {\""], | ||
401 | ); | ||
402 | } | ||
403 | |||
404 | #[test] | ||
405 | fn test_extend_trait_bounds_list_in_where_clause() { | ||
406 | do_check( | ||
407 | r#" | ||
408 | fn foo<R>() | ||
409 | where | ||
410 | R: req::Request + 'static, | ||
411 | R::Params: DeserializeOwned<|> + panic::UnwindSafe + 'static, | ||
412 | R::Result: Serialize + 'static, | ||
413 | "#, | ||
414 | &[ | ||
415 | "DeserializeOwned", | ||
416 | "DeserializeOwned + ", | ||
417 | "DeserializeOwned + panic::UnwindSafe + 'static", | ||
418 | "R::Params: DeserializeOwned + panic::UnwindSafe + 'static", | ||
419 | "R::Params: DeserializeOwned + panic::UnwindSafe + 'static,", | ||
420 | ], | ||
421 | ); | ||
422 | do_check(r#"fn foo<T>() where T: <|>Copy"#, &["Copy"]); | ||
423 | do_check(r#"fn foo<T>() where T: <|>Copy + Display"#, &["Copy", "Copy + "]); | ||
424 | do_check(r#"fn foo<T>() where T: <|>Copy +Display"#, &["Copy", "Copy +"]); | ||
425 | do_check(r#"fn foo<T>() where T: <|>Copy+Display"#, &["Copy", "Copy+"]); | ||
426 | do_check(r#"fn foo<T>() where T: Copy + <|>Display"#, &["Display", "+ Display"]); | ||
427 | do_check(r#"fn foo<T>() where T: Copy + <|>Display + Sync"#, &["Display", "+ Display"]); | ||
428 | do_check(r#"fn foo<T>() where T: Copy +<|>Display"#, &["Display", "+Display"]); | ||
429 | } | ||
430 | |||
431 | #[test] | ||
432 | fn test_extend_trait_bounds_list_inline() { | ||
433 | do_check(r#"fn foo<T: <|>Copy>() {}"#, &["Copy"]); | ||
434 | do_check(r#"fn foo<T: <|>Copy + Display>() {}"#, &["Copy", "Copy + "]); | ||
435 | do_check(r#"fn foo<T: <|>Copy +Display>() {}"#, &["Copy", "Copy +"]); | ||
436 | do_check(r#"fn foo<T: <|>Copy+Display>() {}"#, &["Copy", "Copy+"]); | ||
437 | do_check(r#"fn foo<T: Copy + <|>Display>() {}"#, &["Display", "+ Display"]); | ||
438 | do_check(r#"fn foo<T: Copy + <|>Display + Sync>() {}"#, &["Display", "+ Display"]); | ||
439 | do_check(r#"fn foo<T: Copy +<|>Display>() {}"#, &["Display", "+Display"]); | ||
440 | do_check( | ||
441 | r#"fn foo<T: Copy<|> + Display, U: Copy>() {}"#, | ||
442 | &[ | ||
443 | "Copy", | ||
444 | "Copy + ", | ||
445 | "Copy + Display", | ||
446 | "T: Copy + Display", | ||
447 | "T: Copy + Display, ", | ||
448 | "<T: Copy + Display, U: Copy>", | ||
449 | ], | ||
450 | ); | ||
451 | } | ||
452 | } | ||