diff options
author | Edwin Cheng <[email protected]> | 2019-12-31 18:19:59 +0000 |
---|---|---|
committer | Edwin Cheng <[email protected]> | 2020-01-12 12:25:58 +0000 |
commit | 83b2d78bbbe06cf7e00f8de732f8bd4264b82a46 (patch) | |
tree | d3b54332d18b389ec8d54cb66166a42c9d8f8906 /crates | |
parent | 8bb2a50ce6adda4d3d8dfb66cd67d302ec108d45 (diff) |
Supporting extend selection inside macro calls
Diffstat (limited to 'crates')
-rw-r--r-- | crates/ra_ide/src/extend_selection.rs | 123 |
1 files changed, 109 insertions, 14 deletions
diff --git a/crates/ra_ide/src/extend_selection.rs b/crates/ra_ide/src/extend_selection.rs index 1ec41a117..c6f558021 100644 --- a/crates/ra_ide/src/extend_selection.rs +++ b/crates/ra_ide/src/extend_selection.rs | |||
@@ -4,20 +4,27 @@ use ra_db::SourceDatabase; | |||
4 | use ra_syntax::{ | 4 | use ra_syntax::{ |
5 | algo::find_covering_element, | 5 | algo::find_covering_element, |
6 | ast::{self, AstNode, AstToken}, | 6 | ast::{self, AstNode, AstToken}, |
7 | Direction, NodeOrToken, | 7 | Direction, NodeOrToken, SyntaxElement, |
8 | SyntaxKind::{self, *}, | 8 | SyntaxKind::{self, *}, |
9 | SyntaxNode, SyntaxToken, TextRange, TextUnit, TokenAtOffset, T, | 9 | SyntaxNode, SyntaxToken, TextRange, TextUnit, TokenAtOffset, T, |
10 | }; | 10 | }; |
11 | 11 | ||
12 | use crate::{db::RootDatabase, FileRange}; | 12 | use crate::{db::RootDatabase, FileRange}; |
13 | use hir::{db::AstDatabase, InFile}; | ||
14 | use itertools::Itertools; | ||
13 | 15 | ||
14 | // FIXME: restore macro support | 16 | // FIXME: restore macro support |
15 | pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange { | 17 | pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange { |
16 | let parse = db.parse(frange.file_id); | 18 | let src = db.parse(frange.file_id).tree(); |
17 | try_extend_selection(parse.tree().syntax(), frange.range).unwrap_or(frange.range) | 19 | let root = InFile::new(frange.file_id.into(), src.syntax()); |
20 | try_extend_selection(db, root, frange.range).unwrap_or(frange.range) | ||
18 | } | 21 | } |
19 | 22 | ||
20 | fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange> { | 23 | fn try_extend_selection( |
24 | db: &RootDatabase, | ||
25 | root: InFile<&SyntaxNode>, | ||
26 | range: TextRange, | ||
27 | ) -> Option<TextRange> { | ||
21 | let string_kinds = [COMMENT, STRING, RAW_STRING, BYTE_STRING, RAW_BYTE_STRING]; | 28 | let string_kinds = [COMMENT, STRING, RAW_STRING, BYTE_STRING, RAW_BYTE_STRING]; |
22 | let list_kinds = [ | 29 | let list_kinds = [ |
23 | RECORD_FIELD_PAT_LIST, | 30 | RECORD_FIELD_PAT_LIST, |
@@ -40,9 +47,9 @@ fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange | |||
40 | 47 | ||
41 | if range.is_empty() { | 48 | if range.is_empty() { |
42 | let offset = range.start(); | 49 | let offset = range.start(); |
43 | let mut leaves = root.token_at_offset(offset); | 50 | let mut leaves = root.value.token_at_offset(offset); |
44 | if leaves.clone().all(|it| it.kind() == WHITESPACE) { | 51 | if leaves.clone().all(|it| it.kind() == WHITESPACE) { |
45 | return Some(extend_ws(root, leaves.next()?, offset)); | 52 | return Some(extend_ws(root.value, leaves.next()?, offset)); |
46 | } | 53 | } |
47 | let leaf_range = match leaves { | 54 | let leaf_range = match leaves { |
48 | TokenAtOffset::None => return None, | 55 | TokenAtOffset::None => return None, |
@@ -58,7 +65,7 @@ fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange | |||
58 | }; | 65 | }; |
59 | return Some(leaf_range); | 66 | return Some(leaf_range); |
60 | }; | 67 | }; |
61 | let node = match find_covering_element(root, range) { | 68 | let node = match find_covering_element(root.value, range) { |
62 | NodeOrToken::Token(token) => { | 69 | NodeOrToken::Token(token) => { |
63 | if token.text_range() != range { | 70 | if token.text_range() != range { |
64 | return Some(token.text_range()); | 71 | return Some(token.text_range()); |
@@ -72,6 +79,16 @@ fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange | |||
72 | } | 79 | } |
73 | NodeOrToken::Node(node) => node, | 80 | NodeOrToken::Node(node) => node, |
74 | }; | 81 | }; |
82 | |||
83 | // if we are in single token_tree, we maybe live in macro or attr | ||
84 | if node.kind() == TOKEN_TREE { | ||
85 | if let Some(macro_call) = node.ancestors().find_map(ast::MacroCall::cast) { | ||
86 | if let Some(range) = extend_tokens_from_range(db, &root, macro_call, range) { | ||
87 | return Some(range); | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | |||
75 | if node.text_range() != range { | 92 | if node.text_range() != range { |
76 | return Some(node.text_range()); | 93 | return Some(node.text_range()); |
77 | } | 94 | } |
@@ -88,6 +105,67 @@ fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange | |||
88 | node.parent().map(|it| it.text_range()) | 105 | node.parent().map(|it| it.text_range()) |
89 | } | 106 | } |
90 | 107 | ||
108 | fn extend_tokens_from_range( | ||
109 | db: &RootDatabase, | ||
110 | root: &InFile<&SyntaxNode>, | ||
111 | macro_call: ast::MacroCall, | ||
112 | original_range: TextRange, | ||
113 | ) -> Option<TextRange> { | ||
114 | let analyzer = hir::SourceAnalyzer::new(db, root.clone(), None); | ||
115 | let expansion = analyzer.expand(db, root.with_value(¯o_call))?; | ||
116 | |||
117 | // compute original mapped token range | ||
118 | let range = macro_call | ||
119 | .syntax() | ||
120 | .descendants_with_tokens() | ||
121 | .filter_map(|n| match n { | ||
122 | NodeOrToken::Token(token) if token.text_range().is_subrange(&original_range) => { | ||
123 | expansion | ||
124 | .map_token_down(db, root.with_value(&token)) | ||
125 | .map(|node| node.value.text_range()) | ||
126 | } | ||
127 | _ => None, | ||
128 | }) | ||
129 | .fold1(|x, y| union_range(x, y))?; | ||
130 | |||
131 | let src = db.parse_or_expand(expansion.file_id())?; | ||
132 | let parent = shallow_node(&find_covering_element(&src, range))?.parent()?; | ||
133 | |||
134 | // compute parent mapped token range | ||
135 | let range = macro_call | ||
136 | .syntax() | ||
137 | .descendants_with_tokens() | ||
138 | .filter_map(|n| match n { | ||
139 | NodeOrToken::Token(token) => { | ||
140 | expansion.map_token_down(db, root.with_value(&token)).and_then(|node| { | ||
141 | if node.value.text_range().is_subrange(&parent.text_range()) { | ||
142 | Some(token.text_range()) | ||
143 | } else { | ||
144 | None | ||
145 | } | ||
146 | }) | ||
147 | } | ||
148 | _ => None, | ||
149 | }) | ||
150 | .fold1(|x, y| union_range(x, y))?; | ||
151 | |||
152 | if original_range.is_subrange(&range) && original_range != range { | ||
153 | Some(range) | ||
154 | } else { | ||
155 | None | ||
156 | } | ||
157 | } | ||
158 | |||
159 | fn union_range(range: TextRange, r: TextRange) -> TextRange { | ||
160 | let start = range.start().min(r.start()); | ||
161 | let end = range.end().max(r.end()); | ||
162 | TextRange::from_to(start, end) | ||
163 | } | ||
164 | |||
165 | fn shallow_node(node: &SyntaxElement) -> Option<SyntaxNode> { | ||
166 | node.ancestors().take_while(|n| n.text_range() == node.text_range()).last() | ||
167 | } | ||
168 | |||
91 | fn extend_single_word_in_comment_or_string( | 169 | fn extend_single_word_in_comment_or_string( |
92 | leaf: &SyntaxToken, | 170 | leaf: &SyntaxToken, |
93 | offset: TextUnit, | 171 | offset: TextUnit, |
@@ -227,18 +305,19 @@ fn adj_comments(comment: &ast::Comment, dir: Direction) -> ast::Comment { | |||
227 | 305 | ||
228 | #[cfg(test)] | 306 | #[cfg(test)] |
229 | mod tests { | 307 | mod tests { |
230 | use ra_syntax::{AstNode, SourceFile}; | ||
231 | use test_utils::extract_offset; | ||
232 | |||
233 | use super::*; | 308 | use super::*; |
309 | use crate::mock_analysis::single_file; | ||
310 | use test_utils::extract_offset; | ||
234 | 311 | ||
235 | fn do_check(before: &str, afters: &[&str]) { | 312 | fn do_check(before: &str, afters: &[&str]) { |
236 | let (cursor, before) = extract_offset(before); | 313 | let (cursor, before) = extract_offset(before); |
237 | let parse = SourceFile::parse(&before); | 314 | let (analysis, file_id) = single_file(&before); |
238 | let mut range = TextRange::offset_len(cursor, 0.into()); | 315 | let range = TextRange::offset_len(cursor, 0.into()); |
316 | let mut frange = FileRange { file_id: file_id, range }; | ||
317 | |||
239 | for &after in afters { | 318 | for &after in afters { |
240 | range = try_extend_selection(parse.tree().syntax(), range).unwrap(); | 319 | frange.range = analysis.extend_selection(frange).unwrap(); |
241 | let actual = &before[range]; | 320 | let actual = &before[frange.range]; |
242 | assert_eq!(after, actual); | 321 | assert_eq!(after, actual); |
243 | } | 322 | } |
244 | } | 323 | } |
@@ -503,4 +582,20 @@ fn main() { let var = ( | |||
503 | ], | 582 | ], |
504 | ); | 583 | ); |
505 | } | 584 | } |
585 | |||
586 | #[test] | ||
587 | fn extend_selection_inside_macros() { | ||
588 | do_check( | ||
589 | r#"macro_rules! foo { ($item:item) => {$item} } | ||
590 | foo!{fn hello(na<|>me:usize){}}"#, | ||
591 | &[ | ||
592 | "name", | ||
593 | "name:usize", | ||
594 | "(name:usize)", | ||
595 | "fn hello(name:usize){}", | ||
596 | "{fn hello(name:usize){}}", | ||
597 | "foo!{fn hello(name:usize){}}", | ||
598 | ], | ||
599 | ); | ||
600 | } | ||
506 | } | 601 | } |