aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide/src/extend_selection.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide/src/extend_selection.rs')
-rw-r--r--crates/ra_ide/src/extend_selection.rs452
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
3use ra_db::SourceDatabase;
4use 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
12use crate::{db::RootDatabase, FileRange};
13
14// FIXME: restore macro support
15pub(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
20fn 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
90fn 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
117fn 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
140fn 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.
152fn 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
197fn 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
210fn 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)]
227mod 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#"
266const FOO: [usize; 2] = [
267 22,
268 <|>33,
269]"#,
270 &["33", "33,"],
271 );
272
273 do_check(
274 r#"
275const 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#"
287impl 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#"
300struct A;
301
302/// bla
303/// bla
304struct 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#"
316fn 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/*
344foo
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#"
359fn main() { foo<|>+bar;}
360"#,
361 &["foo", "foo+bar"],
362 );
363 do_check(
364 r#"
365fn 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#"
382impl S {
383fn 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#"
396fn 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#"
408fn 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}