aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide_api')
-rw-r--r--crates/ra_ide_api/src/extend_selection.rs377
-rw-r--r--crates/ra_ide_api/src/lib.rs17
2 files changed, 388 insertions, 6 deletions
diff --git a/crates/ra_ide_api/src/extend_selection.rs b/crates/ra_ide_api/src/extend_selection.rs
index d23290b74..63879a0b5 100644
--- a/crates/ra_ide_api/src/extend_selection.rs
+++ b/crates/ra_ide_api/src/extend_selection.rs
@@ -1,13 +1,378 @@
1use ra_db::SourceDatabase; 1use ra_db::SourceDatabase;
2use ra_syntax::AstNode; 2use ra_syntax::{
3 3 Direction, SyntaxNode, TextRange, TextUnit, AstNode,
4use crate::{ 4 algo::{find_covering_node, find_leaf_at_offset, LeafAtOffset},
5 TextRange, FileRange, 5 SyntaxKind::*,
6 db::RootDatabase,
7}; 6};
8 7
8use crate::{FileRange, db::RootDatabase};
9
9// FIXME: restore macro support 10// FIXME: restore macro support
10pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange { 11pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange {
11 let source_file = db.parse(frange.file_id); 12 let source_file = db.parse(frange.file_id);
12 ra_ide_api_light::extend_selection(source_file.syntax(), frange.range).unwrap_or(frange.range) 13 try_extend_selection(source_file.syntax(), frange.range).unwrap_or(frange.range)
14}
15
16fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange> {
17 let string_kinds = [COMMENT, STRING, RAW_STRING, BYTE_STRING, RAW_BYTE_STRING];
18 let list_kinds = [
19 FIELD_PAT_LIST,
20 MATCH_ARM_LIST,
21 NAMED_FIELD_DEF_LIST,
22 POS_FIELD_DEF_LIST,
23 NAMED_FIELD_LIST,
24 ENUM_VARIANT_LIST,
25 USE_TREE_LIST,
26 TYPE_PARAM_LIST,
27 TYPE_ARG_LIST,
28 PARAM_LIST,
29 ARG_LIST,
30 ARRAY_EXPR,
31 ];
32
33 if range.is_empty() {
34 let offset = range.start();
35 let mut leaves = find_leaf_at_offset(root, offset);
36 if leaves.clone().all(|it| it.kind() == WHITESPACE) {
37 return Some(extend_ws(root, leaves.next()?, offset));
38 }
39 let leaf_range = match leaves {
40 LeafAtOffset::None => return None,
41 LeafAtOffset::Single(l) => {
42 if string_kinds.contains(&l.kind()) {
43 extend_single_word_in_comment_or_string(l, offset).unwrap_or_else(|| l.range())
44 } else {
45 l.range()
46 }
47 }
48 LeafAtOffset::Between(l, r) => pick_best(l, r).range(),
49 };
50 return Some(leaf_range);
51 };
52 let node = find_covering_node(root, range);
53
54 // Using shallowest node with same range allows us to traverse siblings.
55 let node = node.ancestors().take_while(|n| n.range() == node.range()).last().unwrap();
56
57 if range == node.range() {
58 if string_kinds.contains(&node.kind()) {
59 if let Some(range) = extend_comments(node) {
60 return Some(range);
61 }
62 }
63
64 if node.parent().map(|n| list_kinds.contains(&n.kind())) == Some(true) {
65 if let Some(range) = extend_list_item(node) {
66 return Some(range);
67 }
68 }
69 }
70
71 match node.ancestors().skip_while(|n| n.range() == range).next() {
72 None => None,
73 Some(parent) => Some(parent.range()),
74 }
75}
76
77fn extend_single_word_in_comment_or_string(
78 leaf: &SyntaxNode,
79 offset: TextUnit,
80) -> Option<TextRange> {
81 let text: &str = leaf.leaf_text()?;
82 let cursor_position: u32 = (offset - leaf.range().start()).into();
83
84 let (before, after) = text.split_at(cursor_position as usize);
85
86 fn non_word_char(c: char) -> bool {
87 !(c.is_alphanumeric() || c == '_')
88 }
89
90 let start_idx = before.rfind(non_word_char)? as u32;
91 let end_idx = after.find(non_word_char).unwrap_or(after.len()) as u32;
92
93 let from: TextUnit = (start_idx + 1).into();
94 let to: TextUnit = (cursor_position + end_idx).into();
95
96 let range = TextRange::from_to(from, to);
97 if range.is_empty() {
98 None
99 } else {
100 Some(range + leaf.range().start())
101 }
102}
103
104fn extend_ws(root: &SyntaxNode, ws: &SyntaxNode, offset: TextUnit) -> TextRange {
105 let ws_text = ws.leaf_text().unwrap();
106 let suffix = TextRange::from_to(offset, ws.range().end()) - ws.range().start();
107 let prefix = TextRange::from_to(ws.range().start(), offset) - ws.range().start();
108 let ws_suffix = &ws_text.as_str()[suffix];
109 let ws_prefix = &ws_text.as_str()[prefix];
110 if ws_text.contains('\n') && !ws_suffix.contains('\n') {
111 if let Some(node) = ws.next_sibling() {
112 let start = match ws_prefix.rfind('\n') {
113 Some(idx) => ws.range().start() + TextUnit::from((idx + 1) as u32),
114 None => node.range().start(),
115 };
116 let end = if root.text().char_at(node.range().end()) == Some('\n') {
117 node.range().end() + TextUnit::of_char('\n')
118 } else {
119 node.range().end()
120 };
121 return TextRange::from_to(start, end);
122 }
123 }
124 ws.range()
125}
126
127fn pick_best<'a>(l: &'a SyntaxNode, r: &'a SyntaxNode) -> &'a SyntaxNode {
128 return if priority(r) > priority(l) { r } else { l };
129 fn priority(n: &SyntaxNode) -> usize {
130 match n.kind() {
131 WHITESPACE => 0,
132 IDENT | SELF_KW | SUPER_KW | CRATE_KW | LIFETIME => 2,
133 _ => 1,
134 }
135 }
136}
137
138/// Extend list item selection to include nearby comma and whitespace.
139fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> {
140 fn is_single_line_ws(node: &SyntaxNode) -> bool {
141 node.kind() == WHITESPACE && !node.leaf_text().unwrap().contains('\n')
142 }
143
144 fn nearby_comma(node: &SyntaxNode, dir: Direction) -> Option<&SyntaxNode> {
145 node.siblings(dir)
146 .skip(1)
147 .skip_while(|node| is_single_line_ws(node))
148 .next()
149 .filter(|node| node.kind() == COMMA)
150 }
151
152 if let Some(comma_node) = nearby_comma(node, Direction::Prev) {
153 return Some(TextRange::from_to(comma_node.range().start(), node.range().end()));
154 }
155
156 if let Some(comma_node) = nearby_comma(node, Direction::Next) {
157 // Include any following whitespace when comma if after list item.
158 let final_node = comma_node
159 .siblings(Direction::Next)
160 .skip(1)
161 .next()
162 .filter(|node| is_single_line_ws(node))
163 .unwrap_or(comma_node);
164
165 return Some(TextRange::from_to(node.range().start(), final_node.range().end()));
166 }
167
168 return None;
169}
170
171fn extend_comments(node: &SyntaxNode) -> Option<TextRange> {
172 let prev = adj_comments(node, Direction::Prev);
173 let next = adj_comments(node, Direction::Next);
174 if prev != next {
175 Some(TextRange::from_to(prev.range().start(), next.range().end()))
176 } else {
177 None
178 }
179}
180
181fn adj_comments(node: &SyntaxNode, dir: Direction) -> &SyntaxNode {
182 let mut res = node;
183 for node in node.siblings(dir) {
184 match node.kind() {
185 COMMENT => res = node,
186 WHITESPACE if !node.leaf_text().unwrap().as_str().contains("\n\n") => (),
187 _ => break,
188 }
189 }
190 res
191}
192
193#[cfg(test)]
194mod tests {
195 use ra_syntax::{SourceFile, AstNode};
196 use test_utils::extract_offset;
197
198 use super::*;
199
200 fn do_check(before: &str, afters: &[&str]) {
201 let (cursor, before) = extract_offset(before);
202 let file = SourceFile::parse(&before);
203 let mut range = TextRange::offset_len(cursor, 0.into());
204 for &after in afters {
205 range = try_extend_selection(file.syntax(), range).unwrap();
206 let actual = &before[range];
207 assert_eq!(after, actual);
208 }
209 }
210
211 #[test]
212 fn test_extend_selection_arith() {
213 do_check(r#"fn foo() { <|>1 + 1 }"#, &["1", "1 + 1", "{ 1 + 1 }"]);
214 }
215
216 #[test]
217 fn test_extend_selection_list() {
218 do_check(r#"fn foo(<|>x: i32) {}"#, &["x", "x: i32"]);
219 do_check(r#"fn foo(<|>x: i32, y: i32) {}"#, &["x", "x: i32", "x: i32, "]);
220 do_check(r#"fn foo(<|>x: i32,y: i32) {}"#, &["x", "x: i32", "x: i32,"]);
221 do_check(r#"fn foo(x: i32, <|>y: i32) {}"#, &["y", "y: i32", ", y: i32"]);
222 do_check(r#"fn foo(x: i32, <|>y: i32, ) {}"#, &["y", "y: i32", ", y: i32"]);
223 do_check(r#"fn foo(x: i32,<|>y: i32) {}"#, &["y", "y: i32", ",y: i32"]);
224
225 do_check(r#"const FOO: [usize; 2] = [ 22<|> , 33];"#, &["22", "22 , "]);
226 do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|>];"#, &["33", ", 33"]);
227 do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|> ,];"#, &["33", ", 33"]);
228
229 do_check(
230 r#"
231const FOO: [usize; 2] = [
232 22,
233 <|>33,
234]"#,
235 &["33", "33,"],
236 );
237
238 do_check(
239 r#"
240const FOO: [usize; 2] = [
241 22
242 , 33<|>,
243]"#,
244 &["33", ", 33"],
245 );
246 }
247
248 #[test]
249 fn test_extend_selection_start_of_the_line() {
250 do_check(
251 r#"
252impl S {
253<|> fn foo() {
254
255 }
256}"#,
257 &[" fn foo() {\n\n }\n"],
258 );
259 }
260
261 #[test]
262 fn test_extend_selection_doc_comments() {
263 do_check(
264 r#"
265struct A;
266
267/// bla
268/// bla
269struct B {
270 <|>
271}
272 "#,
273 &["\n \n", "{\n \n}", "/// bla\n/// bla\nstruct B {\n \n}"],
274 )
275 }
276
277 #[test]
278 fn test_extend_selection_comments() {
279 do_check(
280 r#"
281fn bar(){}
282
283// fn foo() {
284// 1 + <|>1
285// }
286
287// fn foo(){}
288 "#,
289 &["1", "// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"],
290 );
291
292 do_check(
293 r#"
294// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
295// pub enum Direction {
296// <|> Next,
297// Prev
298// }
299"#,
300 &[
301 "// Next,",
302 "// #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n// pub enum Direction {\n// Next,\n// Prev\n// }",
303 ],
304 );
305
306 do_check(
307 r#"
308/*
309foo
310_bar1<|>*/
311 "#,
312 &["_bar1", "/*\nfoo\n_bar1*/"],
313 );
314
315 do_check(
316 r#"
317//!<|>foo_2 bar
318 "#,
319 &["foo_2", "//!foo_2 bar"],
320 );
321
322 do_check(
323 r#"
324/<|>/foo bar
325 "#,
326 &["//foo bar"],
327 );
328 }
329
330 #[test]
331 fn test_extend_selection_prefer_idents() {
332 do_check(
333 r#"
334fn main() { foo<|>+bar;}
335 "#,
336 &["foo", "foo+bar"],
337 );
338 do_check(
339 r#"
340fn main() { foo+<|>bar;}
341 "#,
342 &["bar", "foo+bar"],
343 );
344 }
345
346 #[test]
347 fn test_extend_selection_prefer_lifetimes() {
348 do_check(r#"fn foo<<|>'a>() {}"#, &["'a", "<'a>"]);
349 do_check(r#"fn foo<'a<|>>() {}"#, &["'a", "<'a>"]);
350 }
351
352 #[test]
353 fn test_extend_selection_select_first_word() {
354 do_check(r#"// foo bar b<|>az quxx"#, &["baz", "// foo bar baz quxx"]);
355 do_check(
356 r#"
357impl S {
358 fn foo() {
359 // hel<|>lo world
360 }
361}
362 "#,
363 &["hello", "// hello world"],
364 );
365 }
366
367 #[test]
368 fn test_extend_selection_string() {
369 do_check(
370 r#"
371fn bar(){}
372
373" fn f<|>oo() {"
374 "#,
375 &["foo", "\" fn foo() {\""],
376 );
377 }
13} 378}
diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs
index b8a4adbce..81ca57035 100644
--- a/crates/ra_ide_api/src/lib.rs
+++ b/crates/ra_ide_api/src/lib.rs
@@ -211,6 +211,23 @@ pub struct Analysis {
211// API, the API should in theory be usable as a library, or via a different 211// API, the API should in theory be usable as a library, or via a different
212// protocol. 212// protocol.
213impl Analysis { 213impl Analysis {
214 // Creates an analysis instance for a single file, without any extenal
215 // dependencies, stdlib support or ability to apply changes. See
216 // `AnalysisHost` for creating a fully-featured analysis.
217 pub fn from_single_file(text: String) -> (Analysis, FileId) {
218 let mut host = AnalysisHost::default();
219 let source_root = SourceRootId(0);
220 let mut change = AnalysisChange::new();
221 change.add_root(source_root, true);
222 let mut crate_graph = CrateGraph::default();
223 let file_id = FileId(0);
224 crate_graph.add_crate_root(file_id, Edition::Edition2018);
225 change.add_file(source_root, file_id, "main.rs".into(), Arc::new(text));
226 change.set_crate_graph(crate_graph);
227 host.apply_change(change);
228 (host.analysis(), file_id)
229 }
230
214 /// Debug info about the current state of the analysis 231 /// Debug info about the current state of the analysis
215 pub fn status(&self) -> String { 232 pub fn status(&self) -> String {
216 status::status(&*self.db) 233 status::status(&*self.db)