diff options
Diffstat (limited to 'crates/ra_ide')
-rw-r--r-- | crates/ra_ide/src/completion.rs | 3 | ||||
-rw-r--r-- | crates/ra_ide/src/completion/complete_trait_impl.rs | 436 | ||||
-rw-r--r-- | crates/ra_ide/src/completion/completion_context.rs | 9 | ||||
-rw-r--r-- | crates/ra_ide/src/lib.rs | 14 | ||||
-rw-r--r-- | crates/ra_ide/src/runnables.rs | 109 | ||||
-rw-r--r-- | crates/ra_ide/src/snapshots/highlighting.html | 1 | ||||
-rw-r--r-- | crates/ra_ide/src/snapshots/rainbow_highlighting.html | 1 | ||||
-rw-r--r-- | crates/ra_ide/src/ssr.rs | 324 | ||||
-rw-r--r-- | crates/ra_ide/src/syntax_highlighting.rs | 1 |
9 files changed, 873 insertions, 25 deletions
diff --git a/crates/ra_ide/src/completion.rs b/crates/ra_ide/src/completion.rs index fedc02e14..4bdc6ba23 100644 --- a/crates/ra_ide/src/completion.rs +++ b/crates/ra_ide/src/completion.rs | |||
@@ -15,6 +15,7 @@ mod complete_path; | |||
15 | mod complete_scope; | 15 | mod complete_scope; |
16 | mod complete_postfix; | 16 | mod complete_postfix; |
17 | mod complete_macro_in_item_position; | 17 | mod complete_macro_in_item_position; |
18 | mod complete_trait_impl; | ||
18 | 19 | ||
19 | use ra_db::SourceDatabase; | 20 | use ra_db::SourceDatabase; |
20 | use ra_ide_db::RootDatabase; | 21 | use ra_ide_db::RootDatabase; |
@@ -74,5 +75,7 @@ pub(crate) fn completions(db: &RootDatabase, position: FilePosition) -> Option<C | |||
74 | complete_pattern::complete_pattern(&mut acc, &ctx); | 75 | complete_pattern::complete_pattern(&mut acc, &ctx); |
75 | complete_postfix::complete_postfix(&mut acc, &ctx); | 76 | complete_postfix::complete_postfix(&mut acc, &ctx); |
76 | complete_macro_in_item_position::complete_macro_in_item_position(&mut acc, &ctx); | 77 | complete_macro_in_item_position::complete_macro_in_item_position(&mut acc, &ctx); |
78 | complete_trait_impl::complete_trait_impl(&mut acc, &ctx); | ||
79 | |||
77 | Some(acc) | 80 | Some(acc) |
78 | } | 81 | } |
diff --git a/crates/ra_ide/src/completion/complete_trait_impl.rs b/crates/ra_ide/src/completion/complete_trait_impl.rs new file mode 100644 index 000000000..6ff10c017 --- /dev/null +++ b/crates/ra_ide/src/completion/complete_trait_impl.rs | |||
@@ -0,0 +1,436 @@ | |||
1 | //! Completion for associated items in a trait implementation. | ||
2 | //! | ||
3 | //! This module adds the completion items related to implementing associated | ||
4 | //! items within a `impl Trait for Struct` block. The current context node | ||
5 | //! must be within either a `FN_DEF`, `TYPE_ALIAS_DEF`, or `CONST_DEF` node | ||
6 | //! and an direct child of an `IMPL_BLOCK`. | ||
7 | //! | ||
8 | //! # Examples | ||
9 | //! | ||
10 | //! Considering the following trait `impl`: | ||
11 | //! | ||
12 | //! ```ignore | ||
13 | //! trait SomeTrait { | ||
14 | //! fn foo(); | ||
15 | //! } | ||
16 | //! | ||
17 | //! impl SomeTrait for () { | ||
18 | //! fn f<|> | ||
19 | //! } | ||
20 | //! ``` | ||
21 | //! | ||
22 | //! may result in the completion of the following method: | ||
23 | //! | ||
24 | //! ```ignore | ||
25 | //! # trait SomeTrait { | ||
26 | //! # fn foo(); | ||
27 | //! # } | ||
28 | //! | ||
29 | //! impl SomeTrait for () { | ||
30 | //! fn foo() {}<|> | ||
31 | //! } | ||
32 | //! ``` | ||
33 | |||
34 | use hir::{self, Docs, HasSource}; | ||
35 | use ra_assists::utils::get_missing_impl_items; | ||
36 | use ra_syntax::{ | ||
37 | ast::{self, edit}, | ||
38 | AstNode, SyntaxKind, SyntaxNode, TextRange, | ||
39 | }; | ||
40 | use ra_text_edit::TextEdit; | ||
41 | |||
42 | use crate::{ | ||
43 | completion::{ | ||
44 | CompletionContext, CompletionItem, CompletionItemKind, CompletionKind, Completions, | ||
45 | }, | ||
46 | display::FunctionSignature, | ||
47 | }; | ||
48 | |||
49 | pub(crate) fn complete_trait_impl(acc: &mut Completions, ctx: &CompletionContext) { | ||
50 | let trigger = ctx.token.ancestors().find(|p| match p.kind() { | ||
51 | SyntaxKind::FN_DEF | ||
52 | | SyntaxKind::TYPE_ALIAS_DEF | ||
53 | | SyntaxKind::CONST_DEF | ||
54 | | SyntaxKind::BLOCK_EXPR => true, | ||
55 | _ => false, | ||
56 | }); | ||
57 | |||
58 | let impl_block = trigger | ||
59 | .as_ref() | ||
60 | .and_then(|node| node.parent()) | ||
61 | .and_then(|node| node.parent()) | ||
62 | .and_then(|node| ast::ImplBlock::cast(node)); | ||
63 | |||
64 | if let (Some(trigger), Some(impl_block)) = (trigger, impl_block) { | ||
65 | match trigger.kind() { | ||
66 | SyntaxKind::FN_DEF => { | ||
67 | for missing_fn in get_missing_impl_items(ctx.db, &ctx.analyzer, &impl_block) | ||
68 | .iter() | ||
69 | .filter_map(|item| match item { | ||
70 | hir::AssocItem::Function(fn_item) => Some(fn_item), | ||
71 | _ => None, | ||
72 | }) | ||
73 | { | ||
74 | add_function_impl(&trigger, acc, ctx, &missing_fn); | ||
75 | } | ||
76 | } | ||
77 | |||
78 | SyntaxKind::TYPE_ALIAS_DEF => { | ||
79 | for missing_fn in get_missing_impl_items(ctx.db, &ctx.analyzer, &impl_block) | ||
80 | .iter() | ||
81 | .filter_map(|item| match item { | ||
82 | hir::AssocItem::TypeAlias(type_item) => Some(type_item), | ||
83 | _ => None, | ||
84 | }) | ||
85 | { | ||
86 | add_type_alias_impl(&trigger, acc, ctx, &missing_fn); | ||
87 | } | ||
88 | } | ||
89 | |||
90 | SyntaxKind::CONST_DEF => { | ||
91 | for missing_fn in get_missing_impl_items(ctx.db, &ctx.analyzer, &impl_block) | ||
92 | .iter() | ||
93 | .filter_map(|item| match item { | ||
94 | hir::AssocItem::Const(const_item) => Some(const_item), | ||
95 | _ => None, | ||
96 | }) | ||
97 | { | ||
98 | add_const_impl(&trigger, acc, ctx, &missing_fn); | ||
99 | } | ||
100 | } | ||
101 | |||
102 | _ => {} | ||
103 | } | ||
104 | } | ||
105 | } | ||
106 | |||
107 | fn add_function_impl( | ||
108 | fn_def_node: &SyntaxNode, | ||
109 | acc: &mut Completions, | ||
110 | ctx: &CompletionContext, | ||
111 | func: &hir::Function, | ||
112 | ) { | ||
113 | let display = FunctionSignature::from_hir(ctx.db, func.clone()); | ||
114 | |||
115 | let fn_name = func.name(ctx.db).to_string(); | ||
116 | |||
117 | let label = if func.params(ctx.db).len() > 0 { | ||
118 | format!("fn {}(..)", fn_name) | ||
119 | } else { | ||
120 | format!("fn {}()", fn_name) | ||
121 | }; | ||
122 | |||
123 | let builder = CompletionItem::new(CompletionKind::Magic, ctx.source_range(), label.clone()) | ||
124 | .lookup_by(fn_name) | ||
125 | .set_documentation(func.docs(ctx.db)); | ||
126 | |||
127 | let completion_kind = if func.has_self_param(ctx.db) { | ||
128 | CompletionItemKind::Method | ||
129 | } else { | ||
130 | CompletionItemKind::Function | ||
131 | }; | ||
132 | |||
133 | let snippet = format!("{} {{}}", display); | ||
134 | |||
135 | let range = TextRange::from_to(fn_def_node.text_range().start(), ctx.source_range().end()); | ||
136 | |||
137 | builder.text_edit(TextEdit::replace(range, snippet)).kind(completion_kind).add_to(acc); | ||
138 | } | ||
139 | |||
140 | fn add_type_alias_impl( | ||
141 | type_def_node: &SyntaxNode, | ||
142 | acc: &mut Completions, | ||
143 | ctx: &CompletionContext, | ||
144 | type_alias: &hir::TypeAlias, | ||
145 | ) { | ||
146 | let alias_name = type_alias.name(ctx.db).to_string(); | ||
147 | |||
148 | let snippet = format!("type {} = ", alias_name); | ||
149 | |||
150 | let range = TextRange::from_to(type_def_node.text_range().start(), ctx.source_range().end()); | ||
151 | |||
152 | CompletionItem::new(CompletionKind::Magic, ctx.source_range(), snippet.clone()) | ||
153 | .text_edit(TextEdit::replace(range, snippet)) | ||
154 | .lookup_by(alias_name) | ||
155 | .kind(CompletionItemKind::TypeAlias) | ||
156 | .set_documentation(type_alias.docs(ctx.db)) | ||
157 | .add_to(acc); | ||
158 | } | ||
159 | |||
160 | fn add_const_impl( | ||
161 | const_def_node: &SyntaxNode, | ||
162 | acc: &mut Completions, | ||
163 | ctx: &CompletionContext, | ||
164 | const_: &hir::Const, | ||
165 | ) { | ||
166 | let const_name = const_.name(ctx.db).map(|n| n.to_string()); | ||
167 | |||
168 | if let Some(const_name) = const_name { | ||
169 | let snippet = make_const_compl_syntax(&const_.source(ctx.db).value); | ||
170 | |||
171 | let range = | ||
172 | TextRange::from_to(const_def_node.text_range().start(), ctx.source_range().end()); | ||
173 | |||
174 | CompletionItem::new(CompletionKind::Magic, ctx.source_range(), snippet.clone()) | ||
175 | .text_edit(TextEdit::replace(range, snippet)) | ||
176 | .lookup_by(const_name) | ||
177 | .kind(CompletionItemKind::Const) | ||
178 | .set_documentation(const_.docs(ctx.db)) | ||
179 | .add_to(acc); | ||
180 | } | ||
181 | } | ||
182 | |||
183 | fn make_const_compl_syntax(const_: &ast::ConstDef) -> String { | ||
184 | let const_ = edit::strip_attrs_and_docs(const_); | ||
185 | |||
186 | let const_start = const_.syntax().text_range().start(); | ||
187 | let const_end = const_.syntax().text_range().end(); | ||
188 | |||
189 | let start = | ||
190 | const_.syntax().first_child_or_token().map_or(const_start, |f| f.text_range().start()); | ||
191 | |||
192 | let end = const_ | ||
193 | .syntax() | ||
194 | .children_with_tokens() | ||
195 | .find(|s| s.kind() == SyntaxKind::SEMI || s.kind() == SyntaxKind::EQ) | ||
196 | .map_or(const_end, |f| f.text_range().start()); | ||
197 | |||
198 | let len = end - start; | ||
199 | let range = TextRange::from_to(0.into(), len); | ||
200 | |||
201 | let syntax = const_.syntax().text().slice(range).to_string(); | ||
202 | |||
203 | format!("{} = ", syntax.trim_end()) | ||
204 | } | ||
205 | |||
206 | #[cfg(test)] | ||
207 | mod tests { | ||
208 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
209 | use insta::assert_debug_snapshot; | ||
210 | |||
211 | fn complete(code: &str) -> Vec<CompletionItem> { | ||
212 | do_completion(code, CompletionKind::Magic) | ||
213 | } | ||
214 | |||
215 | #[test] | ||
216 | fn single_function() { | ||
217 | let completions = complete( | ||
218 | r" | ||
219 | trait Test { | ||
220 | fn foo(); | ||
221 | } | ||
222 | |||
223 | struct T1; | ||
224 | |||
225 | impl Test for T1 { | ||
226 | fn f<|> | ||
227 | } | ||
228 | ", | ||
229 | ); | ||
230 | assert_debug_snapshot!(completions, @r###" | ||
231 | [ | ||
232 | CompletionItem { | ||
233 | label: "fn foo()", | ||
234 | source_range: [141; 142), | ||
235 | delete: [138; 142), | ||
236 | insert: "fn foo() {}", | ||
237 | kind: Function, | ||
238 | lookup: "foo", | ||
239 | }, | ||
240 | ] | ||
241 | "###); | ||
242 | } | ||
243 | |||
244 | #[test] | ||
245 | fn hide_implemented_fn() { | ||
246 | let completions = complete( | ||
247 | r" | ||
248 | trait Test { | ||
249 | fn foo(); | ||
250 | fn foo_bar(); | ||
251 | } | ||
252 | |||
253 | struct T1; | ||
254 | |||
255 | impl Test for T1 { | ||
256 | fn foo() {} | ||
257 | |||
258 | fn f<|> | ||
259 | } | ||
260 | ", | ||
261 | ); | ||
262 | assert_debug_snapshot!(completions, @r###" | ||
263 | [ | ||
264 | CompletionItem { | ||
265 | label: "fn foo_bar()", | ||
266 | source_range: [200; 201), | ||
267 | delete: [197; 201), | ||
268 | insert: "fn foo_bar() {}", | ||
269 | kind: Function, | ||
270 | lookup: "foo_bar", | ||
271 | }, | ||
272 | ] | ||
273 | "###); | ||
274 | } | ||
275 | |||
276 | #[test] | ||
277 | fn completes_only_on_top_level() { | ||
278 | let completions = complete( | ||
279 | r" | ||
280 | trait Test { | ||
281 | fn foo(); | ||
282 | |||
283 | fn foo_bar(); | ||
284 | } | ||
285 | |||
286 | struct T1; | ||
287 | |||
288 | impl Test for T1 { | ||
289 | fn foo() { | ||
290 | <|> | ||
291 | } | ||
292 | } | ||
293 | ", | ||
294 | ); | ||
295 | assert_debug_snapshot!(completions, @r###"[]"###); | ||
296 | } | ||
297 | |||
298 | #[test] | ||
299 | fn generic_fn() { | ||
300 | let completions = complete( | ||
301 | r" | ||
302 | trait Test { | ||
303 | fn foo<T>(); | ||
304 | } | ||
305 | |||
306 | struct T1; | ||
307 | |||
308 | impl Test for T1 { | ||
309 | fn f<|> | ||
310 | } | ||
311 | ", | ||
312 | ); | ||
313 | assert_debug_snapshot!(completions, @r###" | ||
314 | [ | ||
315 | CompletionItem { | ||
316 | label: "fn foo()", | ||
317 | source_range: [144; 145), | ||
318 | delete: [141; 145), | ||
319 | insert: "fn foo<T>() {}", | ||
320 | kind: Function, | ||
321 | lookup: "foo", | ||
322 | }, | ||
323 | ] | ||
324 | "###); | ||
325 | } | ||
326 | |||
327 | #[test] | ||
328 | fn generic_constrait_fn() { | ||
329 | let completions = complete( | ||
330 | r" | ||
331 | trait Test { | ||
332 | fn foo<T>() where T: Into<String>; | ||
333 | } | ||
334 | |||
335 | struct T1; | ||
336 | |||
337 | impl Test for T1 { | ||
338 | fn f<|> | ||
339 | } | ||
340 | ", | ||
341 | ); | ||
342 | assert_debug_snapshot!(completions, @r###" | ||
343 | [ | ||
344 | CompletionItem { | ||
345 | label: "fn foo()", | ||
346 | source_range: [166; 167), | ||
347 | delete: [163; 167), | ||
348 | insert: "fn foo<T>()\nwhere T: Into<String> {}", | ||
349 | kind: Function, | ||
350 | lookup: "foo", | ||
351 | }, | ||
352 | ] | ||
353 | "###); | ||
354 | } | ||
355 | |||
356 | #[test] | ||
357 | fn associated_type() { | ||
358 | let completions = complete( | ||
359 | r" | ||
360 | trait Test { | ||
361 | type SomeType; | ||
362 | } | ||
363 | |||
364 | impl Test for () { | ||
365 | type S<|> | ||
366 | } | ||
367 | ", | ||
368 | ); | ||
369 | assert_debug_snapshot!(completions, @r###" | ||
370 | [ | ||
371 | CompletionItem { | ||
372 | label: "type SomeType = ", | ||
373 | source_range: [124; 125), | ||
374 | delete: [119; 125), | ||
375 | insert: "type SomeType = ", | ||
376 | kind: TypeAlias, | ||
377 | lookup: "SomeType", | ||
378 | }, | ||
379 | ] | ||
380 | "###); | ||
381 | } | ||
382 | |||
383 | #[test] | ||
384 | fn associated_const() { | ||
385 | let completions = complete( | ||
386 | r" | ||
387 | trait Test { | ||
388 | const SOME_CONST: u16; | ||
389 | } | ||
390 | |||
391 | impl Test for () { | ||
392 | const S<|> | ||
393 | } | ||
394 | ", | ||
395 | ); | ||
396 | assert_debug_snapshot!(completions, @r###" | ||
397 | [ | ||
398 | CompletionItem { | ||
399 | label: "const SOME_CONST: u16 = ", | ||
400 | source_range: [133; 134), | ||
401 | delete: [127; 134), | ||
402 | insert: "const SOME_CONST: u16 = ", | ||
403 | kind: Const, | ||
404 | lookup: "SOME_CONST", | ||
405 | }, | ||
406 | ] | ||
407 | "###); | ||
408 | } | ||
409 | |||
410 | #[test] | ||
411 | fn associated_const_with_default() { | ||
412 | let completions = complete( | ||
413 | r" | ||
414 | trait Test { | ||
415 | const SOME_CONST: u16 = 42; | ||
416 | } | ||
417 | |||
418 | impl Test for () { | ||
419 | const S<|> | ||
420 | } | ||
421 | ", | ||
422 | ); | ||
423 | assert_debug_snapshot!(completions, @r###" | ||
424 | [ | ||
425 | CompletionItem { | ||
426 | label: "const SOME_CONST: u16 = ", | ||
427 | source_range: [138; 139), | ||
428 | delete: [132; 139), | ||
429 | insert: "const SOME_CONST: u16 = ", | ||
430 | kind: Const, | ||
431 | lookup: "SOME_CONST", | ||
432 | }, | ||
433 | ] | ||
434 | "###); | ||
435 | } | ||
436 | } | ||
diff --git a/crates/ra_ide/src/completion/completion_context.rs b/crates/ra_ide/src/completion/completion_context.rs index 5a0407fd7..8678a3234 100644 --- a/crates/ra_ide/src/completion/completion_context.rs +++ b/crates/ra_ide/src/completion/completion_context.rs | |||
@@ -25,6 +25,7 @@ pub(crate) struct CompletionContext<'a> { | |||
25 | pub(super) use_item_syntax: Option<ast::UseItem>, | 25 | pub(super) use_item_syntax: Option<ast::UseItem>, |
26 | pub(super) record_lit_syntax: Option<ast::RecordLit>, | 26 | pub(super) record_lit_syntax: Option<ast::RecordLit>, |
27 | pub(super) record_lit_pat: Option<ast::RecordPat>, | 27 | pub(super) record_lit_pat: Option<ast::RecordPat>, |
28 | pub(super) impl_block: Option<ast::ImplBlock>, | ||
28 | pub(super) is_param: bool, | 29 | pub(super) is_param: bool, |
29 | /// If a name-binding or reference to a const in a pattern. | 30 | /// If a name-binding or reference to a const in a pattern. |
30 | /// Irrefutable patterns (like let) are excluded. | 31 | /// Irrefutable patterns (like let) are excluded. |
@@ -72,6 +73,7 @@ impl<'a> CompletionContext<'a> { | |||
72 | use_item_syntax: None, | 73 | use_item_syntax: None, |
73 | record_lit_syntax: None, | 74 | record_lit_syntax: None, |
74 | record_lit_pat: None, | 75 | record_lit_pat: None, |
76 | impl_block: None, | ||
75 | is_param: false, | 77 | is_param: false, |
76 | is_pat_binding: false, | 78 | is_pat_binding: false, |
77 | is_trivial_path: false, | 79 | is_trivial_path: false, |
@@ -148,6 +150,13 @@ impl<'a> CompletionContext<'a> { | |||
148 | self.record_lit_syntax = find_node_at_offset(original_file.syntax(), self.offset); | 150 | self.record_lit_syntax = find_node_at_offset(original_file.syntax(), self.offset); |
149 | } | 151 | } |
150 | 152 | ||
153 | self.impl_block = self | ||
154 | .token | ||
155 | .parent() | ||
156 | .ancestors() | ||
157 | .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) | ||
158 | .find_map(ast::ImplBlock::cast); | ||
159 | |||
151 | let top_node = name_ref | 160 | let top_node = name_ref |
152 | .syntax() | 161 | .syntax() |
153 | .ancestors() | 162 | .ancestors() |
diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index 689921f3f..f86f98be7 100644 --- a/crates/ra_ide/src/lib.rs +++ b/crates/ra_ide/src/lib.rs | |||
@@ -37,6 +37,7 @@ mod display; | |||
37 | mod inlay_hints; | 37 | mod inlay_hints; |
38 | mod expand; | 38 | mod expand; |
39 | mod expand_macro; | 39 | mod expand_macro; |
40 | mod ssr; | ||
40 | 41 | ||
41 | #[cfg(test)] | 42 | #[cfg(test)] |
42 | mod marks; | 43 | mod marks; |
@@ -71,8 +72,9 @@ pub use crate::{ | |||
71 | references::{ | 72 | references::{ |
72 | Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult, SearchScope, | 73 | Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult, SearchScope, |
73 | }, | 74 | }, |
74 | runnables::{Runnable, RunnableKind}, | 75 | runnables::{Runnable, RunnableKind, TestId}, |
75 | source_change::{FileSystemEdit, SourceChange, SourceFileEdit}, | 76 | source_change::{FileSystemEdit, SourceChange, SourceFileEdit}, |
77 | ssr::SsrError, | ||
76 | syntax_highlighting::HighlightedRange, | 78 | syntax_highlighting::HighlightedRange, |
77 | }; | 79 | }; |
78 | 80 | ||
@@ -464,6 +466,16 @@ impl Analysis { | |||
464 | self.with_db(|db| references::rename(db, position, new_name)) | 466 | self.with_db(|db| references::rename(db, position, new_name)) |
465 | } | 467 | } |
466 | 468 | ||
469 | pub fn structural_search_replace( | ||
470 | &self, | ||
471 | query: &str, | ||
472 | ) -> Cancelable<Result<SourceChange, SsrError>> { | ||
473 | self.with_db(|db| { | ||
474 | let edits = ssr::parse_search_replace(query, db)?; | ||
475 | Ok(SourceChange::source_file_edits("ssr", edits)) | ||
476 | }) | ||
477 | } | ||
478 | |||
467 | /// Performs an operation on that may be Canceled. | 479 | /// Performs an operation on that may be Canceled. |
468 | fn with_db<F: FnOnce(&RootDatabase) -> T + std::panic::UnwindSafe, T>( | 480 | fn with_db<F: FnOnce(&RootDatabase) -> T + std::panic::UnwindSafe, T>( |
469 | &self, | 481 | &self, |
diff --git a/crates/ra_ide/src/runnables.rs b/crates/ra_ide/src/runnables.rs index b6b0c70f9..be2a67d0a 100644 --- a/crates/ra_ide/src/runnables.rs +++ b/crates/ra_ide/src/runnables.rs | |||
@@ -1,6 +1,6 @@ | |||
1 | //! FIXME: write short doc here | 1 | //! FIXME: write short doc here |
2 | 2 | ||
3 | use hir::InFile; | 3 | use hir::{InFile, SourceBinder}; |
4 | use itertools::Itertools; | 4 | use itertools::Itertools; |
5 | use ra_db::SourceDatabase; | 5 | use ra_db::SourceDatabase; |
6 | use ra_ide_db::RootDatabase; | 6 | use ra_ide_db::RootDatabase; |
@@ -10,6 +10,7 @@ use ra_syntax::{ | |||
10 | }; | 10 | }; |
11 | 11 | ||
12 | use crate::FileId; | 12 | use crate::FileId; |
13 | use std::fmt::Display; | ||
13 | 14 | ||
14 | #[derive(Debug)] | 15 | #[derive(Debug)] |
15 | pub struct Runnable { | 16 | pub struct Runnable { |
@@ -18,38 +19,84 @@ pub struct Runnable { | |||
18 | } | 19 | } |
19 | 20 | ||
20 | #[derive(Debug)] | 21 | #[derive(Debug)] |
22 | pub enum TestId { | ||
23 | Name(String), | ||
24 | Path(String), | ||
25 | } | ||
26 | |||
27 | impl Display for TestId { | ||
28 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { | ||
29 | match self { | ||
30 | TestId::Name(name) => write!(f, "{}", name), | ||
31 | TestId::Path(path) => write!(f, "{}", path), | ||
32 | } | ||
33 | } | ||
34 | } | ||
35 | |||
36 | #[derive(Debug)] | ||
21 | pub enum RunnableKind { | 37 | pub enum RunnableKind { |
22 | Test { name: String }, | 38 | Test { test_id: TestId }, |
23 | TestMod { path: String }, | 39 | TestMod { path: String }, |
24 | Bench { name: String }, | 40 | Bench { test_id: TestId }, |
25 | Bin, | 41 | Bin, |
26 | } | 42 | } |
27 | 43 | ||
28 | pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> { | 44 | pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> { |
29 | let parse = db.parse(file_id); | 45 | let parse = db.parse(file_id); |
30 | parse.tree().syntax().descendants().filter_map(|i| runnable(db, file_id, i)).collect() | 46 | let mut sb = SourceBinder::new(db); |
47 | parse.tree().syntax().descendants().filter_map(|i| runnable(db, &mut sb, file_id, i)).collect() | ||
31 | } | 48 | } |
32 | 49 | ||
33 | fn runnable(db: &RootDatabase, file_id: FileId, item: SyntaxNode) -> Option<Runnable> { | 50 | fn runnable( |
51 | db: &RootDatabase, | ||
52 | source_binder: &mut SourceBinder<RootDatabase>, | ||
53 | file_id: FileId, | ||
54 | item: SyntaxNode, | ||
55 | ) -> Option<Runnable> { | ||
34 | match_ast! { | 56 | match_ast! { |
35 | match item { | 57 | match item { |
36 | ast::FnDef(it) => { runnable_fn(it) }, | 58 | ast::FnDef(it) => { runnable_fn(db, source_binder, file_id, it) }, |
37 | ast::Module(it) => { runnable_mod(db, file_id, it) }, | 59 | ast::Module(it) => { runnable_mod(db, source_binder, file_id, it) }, |
38 | _ => { None }, | 60 | _ => { None }, |
39 | } | 61 | } |
40 | } | 62 | } |
41 | } | 63 | } |
42 | 64 | ||
43 | fn runnable_fn(fn_def: ast::FnDef) -> Option<Runnable> { | 65 | fn runnable_fn( |
44 | let name = fn_def.name()?.text().clone(); | 66 | db: &RootDatabase, |
45 | let kind = if name == "main" { | 67 | source_binder: &mut SourceBinder<RootDatabase>, |
68 | file_id: FileId, | ||
69 | fn_def: ast::FnDef, | ||
70 | ) -> Option<Runnable> { | ||
71 | let name_string = fn_def.name()?.text().to_string(); | ||
72 | |||
73 | let kind = if name_string == "main" { | ||
46 | RunnableKind::Bin | 74 | RunnableKind::Bin |
47 | } else if has_test_related_attribute(&fn_def) { | ||
48 | RunnableKind::Test { name: name.to_string() } | ||
49 | } else if fn_def.has_atom_attr("bench") { | ||
50 | RunnableKind::Bench { name: name.to_string() } | ||
51 | } else { | 75 | } else { |
52 | return None; | 76 | let test_id = if let Some(module) = source_binder |
77 | .to_def(InFile::new(file_id.into(), fn_def.clone())) | ||
78 | .map(|def| def.module(db)) | ||
79 | { | ||
80 | let path = module | ||
81 | .path_to_root(db) | ||
82 | .into_iter() | ||
83 | .rev() | ||
84 | .filter_map(|it| it.name(db)) | ||
85 | .map(|name| name.to_string()) | ||
86 | .chain(std::iter::once(name_string)) | ||
87 | .join("::"); | ||
88 | TestId::Path(path) | ||
89 | } else { | ||
90 | TestId::Name(name_string) | ||
91 | }; | ||
92 | |||
93 | if has_test_related_attribute(&fn_def) { | ||
94 | RunnableKind::Test { test_id } | ||
95 | } else if fn_def.has_atom_attr("bench") { | ||
96 | RunnableKind::Bench { test_id } | ||
97 | } else { | ||
98 | return None; | ||
99 | } | ||
53 | }; | 100 | }; |
54 | Some(Runnable { range: fn_def.syntax().text_range(), kind }) | 101 | Some(Runnable { range: fn_def.syntax().text_range(), kind }) |
55 | } | 102 | } |
@@ -68,7 +115,12 @@ fn has_test_related_attribute(fn_def: &ast::FnDef) -> bool { | |||
68 | .any(|attribute_text| attribute_text.contains("test")) | 115 | .any(|attribute_text| attribute_text.contains("test")) |
69 | } | 116 | } |
70 | 117 | ||
71 | fn runnable_mod(db: &RootDatabase, file_id: FileId, module: ast::Module) -> Option<Runnable> { | 118 | fn runnable_mod( |
119 | db: &RootDatabase, | ||
120 | source_binder: &mut SourceBinder<RootDatabase>, | ||
121 | file_id: FileId, | ||
122 | module: ast::Module, | ||
123 | ) -> Option<Runnable> { | ||
72 | let has_test_function = module | 124 | let has_test_function = module |
73 | .item_list()? | 125 | .item_list()? |
74 | .items() | 126 | .items() |
@@ -76,13 +128,12 @@ fn runnable_mod(db: &RootDatabase, file_id: FileId, module: ast::Module) -> Opti | |||
76 | ast::ModuleItem::FnDef(it) => Some(it), | 128 | ast::ModuleItem::FnDef(it) => Some(it), |
77 | _ => None, | 129 | _ => None, |
78 | }) | 130 | }) |
79 | .any(|f| f.has_atom_attr("test")); | 131 | .any(|f| has_test_related_attribute(&f)); |
80 | if !has_test_function { | 132 | if !has_test_function { |
81 | return None; | 133 | return None; |
82 | } | 134 | } |
83 | let range = module.syntax().text_range(); | 135 | let range = module.syntax().text_range(); |
84 | let mut sb = hir::SourceBinder::new(db); | 136 | let module = source_binder.to_def(InFile::new(file_id.into(), module))?; |
85 | let module = sb.to_def(InFile::new(file_id.into(), module))?; | ||
86 | 137 | ||
87 | let path = module.path_to_root(db).into_iter().rev().filter_map(|it| it.name(db)).join("::"); | 138 | let path = module.path_to_root(db).into_iter().rev().filter_map(|it| it.name(db)).join("::"); |
88 | Some(Runnable { range, kind: RunnableKind::TestMod { path } }) | 139 | Some(Runnable { range, kind: RunnableKind::TestMod { path } }) |
@@ -121,13 +172,17 @@ mod tests { | |||
121 | Runnable { | 172 | Runnable { |
122 | range: [22; 46), | 173 | range: [22; 46), |
123 | kind: Test { | 174 | kind: Test { |
124 | name: "test_foo", | 175 | test_id: Path( |
176 | "test_foo", | ||
177 | ), | ||
125 | }, | 178 | }, |
126 | }, | 179 | }, |
127 | Runnable { | 180 | Runnable { |
128 | range: [47; 81), | 181 | range: [47; 81), |
129 | kind: Test { | 182 | kind: Test { |
130 | name: "test_foo", | 183 | test_id: Path( |
184 | "test_foo", | ||
185 | ), | ||
131 | }, | 186 | }, |
132 | }, | 187 | }, |
133 | ] | 188 | ] |
@@ -160,7 +215,9 @@ mod tests { | |||
160 | Runnable { | 215 | Runnable { |
161 | range: [28; 57), | 216 | range: [28; 57), |
162 | kind: Test { | 217 | kind: Test { |
163 | name: "test_foo1", | 218 | test_id: Path( |
219 | "test_mod::test_foo1", | ||
220 | ), | ||
164 | }, | 221 | }, |
165 | }, | 222 | }, |
166 | ] | 223 | ] |
@@ -195,7 +252,9 @@ mod tests { | |||
195 | Runnable { | 252 | Runnable { |
196 | range: [46; 79), | 253 | range: [46; 79), |
197 | kind: Test { | 254 | kind: Test { |
198 | name: "test_foo1", | 255 | test_id: Path( |
256 | "foo::test_mod::test_foo1", | ||
257 | ), | ||
199 | }, | 258 | }, |
200 | }, | 259 | }, |
201 | ] | 260 | ] |
@@ -232,7 +291,9 @@ mod tests { | |||
232 | Runnable { | 291 | Runnable { |
233 | range: [68; 105), | 292 | range: [68; 105), |
234 | kind: Test { | 293 | kind: Test { |
235 | name: "test_foo1", | 294 | test_id: Path( |
295 | "foo::bar::test_mod::test_foo1", | ||
296 | ), | ||
236 | }, | 297 | }, |
237 | }, | 298 | }, |
238 | ] | 299 | ] |
diff --git a/crates/ra_ide/src/snapshots/highlighting.html b/crates/ra_ide/src/snapshots/highlighting.html index 1cc55e78b..a02dbaf2f 100644 --- a/crates/ra_ide/src/snapshots/highlighting.html +++ b/crates/ra_ide/src/snapshots/highlighting.html | |||
@@ -16,6 +16,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd | |||
16 | .literal { color: #BFEBBF; } | 16 | .literal { color: #BFEBBF; } |
17 | .literal\.numeric { color: #6A8759; } | 17 | .literal\.numeric { color: #6A8759; } |
18 | .macro { color: #94BFF3; } | 18 | .macro { color: #94BFF3; } |
19 | .module { color: #AFD8AF; } | ||
19 | .variable { color: #DCDCCC; } | 20 | .variable { color: #DCDCCC; } |
20 | .variable\.mut { color: #DCDCCC; text-decoration: underline; } | 21 | .variable\.mut { color: #DCDCCC; text-decoration: underline; } |
21 | 22 | ||
diff --git a/crates/ra_ide/src/snapshots/rainbow_highlighting.html b/crates/ra_ide/src/snapshots/rainbow_highlighting.html index 918fd4b97..95f038f00 100644 --- a/crates/ra_ide/src/snapshots/rainbow_highlighting.html +++ b/crates/ra_ide/src/snapshots/rainbow_highlighting.html | |||
@@ -16,6 +16,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd | |||
16 | .literal { color: #BFEBBF; } | 16 | .literal { color: #BFEBBF; } |
17 | .literal\.numeric { color: #6A8759; } | 17 | .literal\.numeric { color: #6A8759; } |
18 | .macro { color: #94BFF3; } | 18 | .macro { color: #94BFF3; } |
19 | .module { color: #AFD8AF; } | ||
19 | .variable { color: #DCDCCC; } | 20 | .variable { color: #DCDCCC; } |
20 | .variable\.mut { color: #DCDCCC; text-decoration: underline; } | 21 | .variable\.mut { color: #DCDCCC; text-decoration: underline; } |
21 | 22 | ||
diff --git a/crates/ra_ide/src/ssr.rs b/crates/ra_ide/src/ssr.rs new file mode 100644 index 000000000..14eb0b8b2 --- /dev/null +++ b/crates/ra_ide/src/ssr.rs | |||
@@ -0,0 +1,324 @@ | |||
1 | //! structural search replace | ||
2 | |||
3 | use crate::source_change::SourceFileEdit; | ||
4 | use ra_ide_db::RootDatabase; | ||
5 | use ra_syntax::ast::make::expr_from_text; | ||
6 | use ra_syntax::AstNode; | ||
7 | use ra_syntax::SyntaxElement; | ||
8 | use ra_syntax::SyntaxNode; | ||
9 | use ra_text_edit::{TextEdit, TextEditBuilder}; | ||
10 | use rustc_hash::FxHashMap; | ||
11 | use std::collections::HashMap; | ||
12 | use std::str::FromStr; | ||
13 | |||
14 | pub use ra_db::{SourceDatabase, SourceDatabaseExt}; | ||
15 | use ra_ide_db::symbol_index::SymbolsDatabase; | ||
16 | |||
17 | #[derive(Debug, PartialEq)] | ||
18 | pub struct SsrError(String); | ||
19 | |||
20 | impl std::fmt::Display for SsrError { | ||
21 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||
22 | write!(f, "Parse error: {}", self.0) | ||
23 | } | ||
24 | } | ||
25 | |||
26 | impl std::error::Error for SsrError {} | ||
27 | |||
28 | pub fn parse_search_replace( | ||
29 | query: &str, | ||
30 | db: &RootDatabase, | ||
31 | ) -> Result<Vec<SourceFileEdit>, SsrError> { | ||
32 | let mut edits = vec![]; | ||
33 | let query: SsrQuery = query.parse()?; | ||
34 | for &root in db.local_roots().iter() { | ||
35 | let sr = db.source_root(root); | ||
36 | for file_id in sr.walk() { | ||
37 | dbg!(db.file_relative_path(file_id)); | ||
38 | let matches = find(&query.pattern, db.parse(file_id).tree().syntax()); | ||
39 | if !matches.matches.is_empty() { | ||
40 | edits.push(SourceFileEdit { file_id, edit: replace(&matches, &query.template) }); | ||
41 | } | ||
42 | } | ||
43 | } | ||
44 | Ok(edits) | ||
45 | } | ||
46 | |||
47 | #[derive(Debug)] | ||
48 | struct SsrQuery { | ||
49 | pattern: SsrPattern, | ||
50 | template: SsrTemplate, | ||
51 | } | ||
52 | |||
53 | #[derive(Debug)] | ||
54 | struct SsrPattern { | ||
55 | pattern: SyntaxNode, | ||
56 | vars: Vec<Var>, | ||
57 | } | ||
58 | |||
59 | /// represents an `$var` in an SSR query | ||
60 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
61 | struct Var(String); | ||
62 | |||
63 | #[derive(Debug)] | ||
64 | struct SsrTemplate { | ||
65 | template: SyntaxNode, | ||
66 | placeholders: FxHashMap<SyntaxNode, Var>, | ||
67 | } | ||
68 | |||
69 | type Binding = HashMap<Var, SyntaxNode>; | ||
70 | |||
71 | #[derive(Debug)] | ||
72 | struct Match { | ||
73 | place: SyntaxNode, | ||
74 | binding: Binding, | ||
75 | } | ||
76 | |||
77 | #[derive(Debug)] | ||
78 | struct SsrMatches { | ||
79 | matches: Vec<Match>, | ||
80 | } | ||
81 | |||
82 | impl FromStr for SsrQuery { | ||
83 | type Err = SsrError; | ||
84 | |||
85 | fn from_str(query: &str) -> Result<SsrQuery, SsrError> { | ||
86 | let mut it = query.split("==>>"); | ||
87 | let pattern = it.next().expect("at least empty string").trim(); | ||
88 | let mut template = | ||
89 | it.next().ok_or(SsrError("Cannot find delemiter `==>>`".into()))?.trim().to_string(); | ||
90 | if it.next().is_some() { | ||
91 | return Err(SsrError("More than one delimiter found".into())); | ||
92 | } | ||
93 | let mut vars = vec![]; | ||
94 | let mut it = pattern.split('$'); | ||
95 | let mut pattern = it.next().expect("something").to_string(); | ||
96 | |||
97 | for part in it.map(split_by_var) { | ||
98 | let (var, var_type, remainder) = part?; | ||
99 | is_expr(var_type)?; | ||
100 | let new_var = create_name(var, &mut vars)?; | ||
101 | pattern.push_str(new_var); | ||
102 | pattern.push_str(remainder); | ||
103 | template = replace_in_template(template, var, new_var); | ||
104 | } | ||
105 | |||
106 | let template = expr_from_text(&template).syntax().clone(); | ||
107 | let mut placeholders = FxHashMap::default(); | ||
108 | |||
109 | traverse(&template, &mut |n| { | ||
110 | if let Some(v) = vars.iter().find(|v| v.0.as_str() == n.text()) { | ||
111 | placeholders.insert(n.clone(), v.clone()); | ||
112 | false | ||
113 | } else { | ||
114 | true | ||
115 | } | ||
116 | }); | ||
117 | |||
118 | let pattern = SsrPattern { pattern: expr_from_text(&pattern).syntax().clone(), vars }; | ||
119 | let template = SsrTemplate { template, placeholders }; | ||
120 | Ok(SsrQuery { pattern, template }) | ||
121 | } | ||
122 | } | ||
123 | |||
124 | fn traverse(node: &SyntaxNode, go: &mut impl FnMut(&SyntaxNode) -> bool) { | ||
125 | if !go(node) { | ||
126 | return; | ||
127 | } | ||
128 | for ref child in node.children() { | ||
129 | traverse(child, go); | ||
130 | } | ||
131 | } | ||
132 | |||
133 | fn split_by_var(s: &str) -> Result<(&str, &str, &str), SsrError> { | ||
134 | let end_of_name = s.find(":").ok_or(SsrError("Use $<name>:expr".into()))?; | ||
135 | let name = &s[0..end_of_name]; | ||
136 | is_name(name)?; | ||
137 | let type_begin = end_of_name + 1; | ||
138 | let type_length = s[type_begin..].find(|c| !char::is_ascii_alphanumeric(&c)).unwrap_or(s.len()); | ||
139 | let type_name = &s[type_begin..type_begin + type_length]; | ||
140 | Ok((name, type_name, &s[type_begin + type_length..])) | ||
141 | } | ||
142 | |||
143 | fn is_name(s: &str) -> Result<(), SsrError> { | ||
144 | if s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { | ||
145 | Ok(()) | ||
146 | } else { | ||
147 | Err(SsrError("Name can contain only alphanumerics and _".into())) | ||
148 | } | ||
149 | } | ||
150 | |||
151 | fn is_expr(s: &str) -> Result<(), SsrError> { | ||
152 | if s == "expr" { | ||
153 | Ok(()) | ||
154 | } else { | ||
155 | Err(SsrError("Only $<name>:expr is supported".into())) | ||
156 | } | ||
157 | } | ||
158 | |||
159 | fn replace_in_template(template: String, var: &str, new_var: &str) -> String { | ||
160 | let name = format!("${}", var); | ||
161 | template.replace(&name, new_var) | ||
162 | } | ||
163 | |||
164 | fn create_name<'a>(name: &str, vars: &'a mut Vec<Var>) -> Result<&'a str, SsrError> { | ||
165 | let sanitized_name = format!("__search_pattern_{}", name); | ||
166 | if vars.iter().any(|a| a.0 == sanitized_name) { | ||
167 | return Err(SsrError(format!("Name `{}` repeats more than once", name))); | ||
168 | } | ||
169 | vars.push(Var(sanitized_name)); | ||
170 | Ok(&vars.last().unwrap().0) | ||
171 | } | ||
172 | |||
173 | fn find(pattern: &SsrPattern, code: &SyntaxNode) -> SsrMatches { | ||
174 | fn check( | ||
175 | pattern: &SyntaxElement, | ||
176 | code: &SyntaxElement, | ||
177 | placeholders: &[Var], | ||
178 | match_: &mut Match, | ||
179 | ) -> bool { | ||
180 | match (pattern, code) { | ||
181 | (SyntaxElement::Token(ref pattern), SyntaxElement::Token(ref code)) => { | ||
182 | pattern.text() == code.text() | ||
183 | } | ||
184 | (SyntaxElement::Node(ref pattern), SyntaxElement::Node(ref code)) => { | ||
185 | if placeholders.iter().find(|&n| n.0.as_str() == pattern.text()).is_some() { | ||
186 | match_.binding.insert(Var(pattern.text().to_string()), code.clone()); | ||
187 | true | ||
188 | } else { | ||
189 | pattern.green().children().count() == code.green().children().count() | ||
190 | && pattern | ||
191 | .children_with_tokens() | ||
192 | .zip(code.children_with_tokens()) | ||
193 | .all(|(a, b)| check(&a, &b, placeholders, match_)) | ||
194 | } | ||
195 | } | ||
196 | _ => false, | ||
197 | } | ||
198 | } | ||
199 | let kind = pattern.pattern.kind(); | ||
200 | let matches = code | ||
201 | .descendants_with_tokens() | ||
202 | .filter(|n| n.kind() == kind) | ||
203 | .filter_map(|code| { | ||
204 | let mut match_ = | ||
205 | Match { place: code.as_node().unwrap().clone(), binding: HashMap::new() }; | ||
206 | if check( | ||
207 | &SyntaxElement::from(pattern.pattern.clone()), | ||
208 | &code, | ||
209 | &pattern.vars, | ||
210 | &mut match_, | ||
211 | ) { | ||
212 | Some(match_) | ||
213 | } else { | ||
214 | None | ||
215 | } | ||
216 | }) | ||
217 | .collect(); | ||
218 | SsrMatches { matches } | ||
219 | } | ||
220 | |||
221 | fn replace(matches: &SsrMatches, template: &SsrTemplate) -> TextEdit { | ||
222 | let mut builder = TextEditBuilder::default(); | ||
223 | for match_ in &matches.matches { | ||
224 | builder.replace(match_.place.text_range(), render_replace(&match_.binding, template)); | ||
225 | } | ||
226 | builder.finish() | ||
227 | } | ||
228 | |||
229 | fn render_replace(binding: &Binding, template: &SsrTemplate) -> String { | ||
230 | let mut builder = TextEditBuilder::default(); | ||
231 | for element in template.template.descendants() { | ||
232 | if let Some(var) = template.placeholders.get(&element) { | ||
233 | builder.replace(element.text_range(), binding[var].to_string()) | ||
234 | } | ||
235 | } | ||
236 | builder.finish().apply(&template.template.text().to_string()) | ||
237 | } | ||
238 | |||
239 | #[cfg(test)] | ||
240 | mod tests { | ||
241 | use super::*; | ||
242 | use ra_syntax::SourceFile; | ||
243 | |||
244 | fn parse_error_text(query: &str) -> String { | ||
245 | format!("{}", query.parse::<SsrQuery>().unwrap_err()) | ||
246 | } | ||
247 | |||
248 | #[test] | ||
249 | fn parser_happy_case() { | ||
250 | let result: SsrQuery = "foo($a:expr, $b:expr) ==>> bar($b, $a)".parse().unwrap(); | ||
251 | assert_eq!(&result.pattern.pattern.text(), "foo(__search_pattern_a, __search_pattern_b)"); | ||
252 | assert_eq!(result.pattern.vars.len(), 2); | ||
253 | assert_eq!(result.pattern.vars[0].0, "__search_pattern_a"); | ||
254 | assert_eq!(result.pattern.vars[1].0, "__search_pattern_b"); | ||
255 | assert_eq!(&result.template.template.text(), "bar(__search_pattern_b, __search_pattern_a)"); | ||
256 | dbg!(result.template.placeholders); | ||
257 | } | ||
258 | |||
259 | #[test] | ||
260 | fn parser_empty_query() { | ||
261 | assert_eq!(parse_error_text(""), "Parse error: Cannot find delemiter `==>>`"); | ||
262 | } | ||
263 | |||
264 | #[test] | ||
265 | fn parser_no_delimiter() { | ||
266 | assert_eq!(parse_error_text("foo()"), "Parse error: Cannot find delemiter `==>>`"); | ||
267 | } | ||
268 | |||
269 | #[test] | ||
270 | fn parser_two_delimiters() { | ||
271 | assert_eq!( | ||
272 | parse_error_text("foo() ==>> a ==>> b "), | ||
273 | "Parse error: More than one delimiter found" | ||
274 | ); | ||
275 | } | ||
276 | |||
277 | #[test] | ||
278 | fn parser_no_pattern_type() { | ||
279 | assert_eq!(parse_error_text("foo($a) ==>>"), "Parse error: Use $<name>:expr"); | ||
280 | } | ||
281 | |||
282 | #[test] | ||
283 | fn parser_invalid_name() { | ||
284 | assert_eq!( | ||
285 | parse_error_text("foo($a+:expr) ==>>"), | ||
286 | "Parse error: Name can contain only alphanumerics and _" | ||
287 | ); | ||
288 | } | ||
289 | |||
290 | #[test] | ||
291 | fn parser_invalid_type() { | ||
292 | assert_eq!( | ||
293 | parse_error_text("foo($a:ident) ==>>"), | ||
294 | "Parse error: Only $<name>:expr is supported" | ||
295 | ); | ||
296 | } | ||
297 | |||
298 | #[test] | ||
299 | fn parser_repeated_name() { | ||
300 | assert_eq!( | ||
301 | parse_error_text("foo($a:expr, $a:expr) ==>>"), | ||
302 | "Parse error: Name `a` repeats more than once" | ||
303 | ); | ||
304 | } | ||
305 | |||
306 | #[test] | ||
307 | fn parse_match_replace() { | ||
308 | let query: SsrQuery = "foo($x:expr) ==>> bar($x)".parse().unwrap(); | ||
309 | let input = "fn main() { foo(1+2); }"; | ||
310 | |||
311 | let code = SourceFile::parse(input).tree(); | ||
312 | let matches = find(&query.pattern, code.syntax()); | ||
313 | assert_eq!(matches.matches.len(), 1); | ||
314 | assert_eq!(matches.matches[0].place.text(), "foo(1+2)"); | ||
315 | assert_eq!(matches.matches[0].binding.len(), 1); | ||
316 | assert_eq!( | ||
317 | matches.matches[0].binding[&Var("__search_pattern_x".to_string())].text(), | ||
318 | "1+2" | ||
319 | ); | ||
320 | |||
321 | let edit = replace(&matches, &query.template); | ||
322 | assert_eq!(edit.apply(input), "fn main() { bar(1+2); }"); | ||
323 | } | ||
324 | } | ||
diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs index 174e13595..20c414ca1 100644 --- a/crates/ra_ide/src/syntax_highlighting.rs +++ b/crates/ra_ide/src/syntax_highlighting.rs | |||
@@ -365,6 +365,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd | |||
365 | .literal { color: #BFEBBF; } | 365 | .literal { color: #BFEBBF; } |
366 | .literal\\.numeric { color: #6A8759; } | 366 | .literal\\.numeric { color: #6A8759; } |
367 | .macro { color: #94BFF3; } | 367 | .macro { color: #94BFF3; } |
368 | .module { color: #AFD8AF; } | ||
368 | .variable { color: #DCDCCC; } | 369 | .variable { color: #DCDCCC; } |
369 | .variable\\.mut { color: #DCDCCC; text-decoration: underline; } | 370 | .variable\\.mut { color: #DCDCCC; text-decoration: underline; } |
370 | 371 | ||