diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-02-27 08:54:18 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2020-02-27 08:54:18 +0000 |
commit | 50956f89fce93245e52b693ae63283274bc92142 (patch) | |
tree | 3393c2c20b6adb02faf8156308f18ff842f6968d /crates | |
parent | 7f7d96cffed6ed4836432a16cfc1dbd080bbab17 (diff) | |
parent | b1ee6d17a4af6a2fcbaeb0b98965cd610c7b1b72 (diff) |
Merge #3285
3285: Handle trivia in Structural Search and Replace r=matklad a=adamrk
Addresses the second point of https://github.com/rust-analyzer/rust-analyzer/issues/3186.
Structural search and replace will now match code that has varies from the pattern in whitespace or comments.
One issue is that it's not clear where comments in the matched code should go in the replacement. With this change they're just tacked on at the end, which can cause some unexpected moving of comments (see the last test example).
Co-authored-by: adamrk <[email protected]>
Diffstat (limited to 'crates')
-rw-r--r-- | crates/ra_ide/src/ssr.rs | 141 |
1 files changed, 115 insertions, 26 deletions
diff --git a/crates/ra_ide/src/ssr.rs b/crates/ra_ide/src/ssr.rs index 902c29fc6..c011a2e74 100644 --- a/crates/ra_ide/src/ssr.rs +++ b/crates/ra_ide/src/ssr.rs | |||
@@ -3,9 +3,8 @@ | |||
3 | use crate::source_change::SourceFileEdit; | 3 | use crate::source_change::SourceFileEdit; |
4 | use ra_ide_db::RootDatabase; | 4 | use ra_ide_db::RootDatabase; |
5 | use ra_syntax::ast::make::expr_from_text; | 5 | use ra_syntax::ast::make::expr_from_text; |
6 | use ra_syntax::AstNode; | 6 | use ra_syntax::ast::{AstToken, Comment}; |
7 | use ra_syntax::SyntaxElement; | 7 | use ra_syntax::{AstNode, SyntaxElement, SyntaxNode}; |
8 | use ra_syntax::SyntaxNode; | ||
9 | use ra_text_edit::{TextEdit, TextEditBuilder}; | 8 | use ra_text_edit::{TextEdit, TextEditBuilder}; |
10 | use rustc_hash::FxHashMap; | 9 | use rustc_hash::FxHashMap; |
11 | use std::collections::HashMap; | 10 | use std::collections::HashMap; |
@@ -72,6 +71,7 @@ type Binding = HashMap<Var, SyntaxNode>; | |||
72 | struct Match { | 71 | struct Match { |
73 | place: SyntaxNode, | 72 | place: SyntaxNode, |
74 | binding: Binding, | 73 | binding: Binding, |
74 | ignored_comments: Vec<Comment>, | ||
75 | } | 75 | } |
76 | 76 | ||
77 | #[derive(Debug)] | 77 | #[derive(Debug)] |
@@ -179,44 +179,61 @@ fn find(pattern: &SsrPattern, code: &SyntaxNode) -> SsrMatches { | |||
179 | pattern: &SyntaxElement, | 179 | pattern: &SyntaxElement, |
180 | code: &SyntaxElement, | 180 | code: &SyntaxElement, |
181 | placeholders: &[Var], | 181 | placeholders: &[Var], |
182 | match_: &mut Match, | 182 | mut match_: Match, |
183 | ) -> bool { | 183 | ) -> Option<Match> { |
184 | match (pattern, code) { | 184 | match (pattern, code) { |
185 | (SyntaxElement::Token(ref pattern), SyntaxElement::Token(ref code)) => { | 185 | (SyntaxElement::Token(ref pattern), SyntaxElement::Token(ref code)) => { |
186 | pattern.text() == code.text() | 186 | if pattern.text() == code.text() { |
187 | Some(match_) | ||
188 | } else { | ||
189 | None | ||
190 | } | ||
187 | } | 191 | } |
188 | (SyntaxElement::Node(ref pattern), SyntaxElement::Node(ref code)) => { | 192 | (SyntaxElement::Node(ref pattern), SyntaxElement::Node(ref code)) => { |
189 | if placeholders.iter().any(|n| n.0.as_str() == pattern.text()) { | 193 | if placeholders.iter().any(|n| n.0.as_str() == pattern.text()) { |
190 | match_.binding.insert(Var(pattern.text().to_string()), code.clone()); | 194 | match_.binding.insert(Var(pattern.text().to_string()), code.clone()); |
191 | true | 195 | Some(match_) |
192 | } else { | 196 | } else { |
193 | pattern.green().children().count() == code.green().children().count() | 197 | let mut pattern_children = pattern |
194 | && pattern | 198 | .children_with_tokens() |
195 | .children_with_tokens() | 199 | .filter(|element| !element.kind().is_trivia()); |
196 | .zip(code.children_with_tokens()) | 200 | let mut code_children = |
197 | .all(|(a, b)| check(&a, &b, placeholders, match_)) | 201 | code.children_with_tokens().filter(|element| !element.kind().is_trivia()); |
202 | let new_ignored_comments = code.children_with_tokens().filter_map(|element| { | ||
203 | element.as_token().and_then(|token| Comment::cast(token.clone())) | ||
204 | }); | ||
205 | match_.ignored_comments.extend(new_ignored_comments); | ||
206 | let match_from_children = pattern_children | ||
207 | .by_ref() | ||
208 | .zip(code_children.by_ref()) | ||
209 | .fold(Some(match_), |accum, (a, b)| { | ||
210 | accum.and_then(|match_| check(&a, &b, placeholders, match_)) | ||
211 | }); | ||
212 | match_from_children.and_then(|match_| { | ||
213 | if pattern_children.count() == 0 && code_children.count() == 0 { | ||
214 | Some(match_) | ||
215 | } else { | ||
216 | None | ||
217 | } | ||
218 | }) | ||
198 | } | 219 | } |
199 | } | 220 | } |
200 | _ => false, | 221 | _ => None, |
201 | } | 222 | } |
202 | } | 223 | } |
203 | let kind = pattern.pattern.kind(); | 224 | let kind = pattern.pattern.kind(); |
204 | let matches = code | 225 | let matches = code |
205 | .descendants_with_tokens() | 226 | .descendants() |
206 | .filter(|n| n.kind() == kind) | 227 | .filter(|n| n.kind() == kind) |
207 | .filter_map(|code| { | 228 | .filter_map(|code| { |
208 | let mut match_ = | 229 | let match_ = |
209 | Match { place: code.as_node().unwrap().clone(), binding: HashMap::new() }; | 230 | Match { place: code.clone(), binding: HashMap::new(), ignored_comments: vec![] }; |
210 | if check( | 231 | check( |
211 | &SyntaxElement::from(pattern.pattern.clone()), | 232 | &SyntaxElement::from(pattern.pattern.clone()), |
212 | &code, | 233 | &SyntaxElement::from(code), |
213 | &pattern.vars, | 234 | &pattern.vars, |
214 | &mut match_, | 235 | match_, |
215 | ) { | 236 | ) |
216 | Some(match_) | ||
217 | } else { | ||
218 | None | ||
219 | } | ||
220 | }) | 237 | }) |
221 | .collect(); | 238 | .collect(); |
222 | SsrMatches { matches } | 239 | SsrMatches { matches } |
@@ -225,18 +242,28 @@ fn find(pattern: &SsrPattern, code: &SyntaxNode) -> SsrMatches { | |||
225 | fn replace(matches: &SsrMatches, template: &SsrTemplate) -> TextEdit { | 242 | fn replace(matches: &SsrMatches, template: &SsrTemplate) -> TextEdit { |
226 | let mut builder = TextEditBuilder::default(); | 243 | let mut builder = TextEditBuilder::default(); |
227 | for match_ in &matches.matches { | 244 | for match_ in &matches.matches { |
228 | builder.replace(match_.place.text_range(), render_replace(&match_.binding, template)); | 245 | builder.replace( |
246 | match_.place.text_range(), | ||
247 | render_replace(&match_.binding, &match_.ignored_comments, template), | ||
248 | ); | ||
229 | } | 249 | } |
230 | builder.finish() | 250 | builder.finish() |
231 | } | 251 | } |
232 | 252 | ||
233 | fn render_replace(binding: &Binding, template: &SsrTemplate) -> String { | 253 | fn render_replace( |
254 | binding: &Binding, | ||
255 | ignored_comments: &Vec<Comment>, | ||
256 | template: &SsrTemplate, | ||
257 | ) -> String { | ||
234 | let mut builder = TextEditBuilder::default(); | 258 | let mut builder = TextEditBuilder::default(); |
235 | for element in template.template.descendants() { | 259 | for element in template.template.descendants() { |
236 | if let Some(var) = template.placeholders.get(&element) { | 260 | if let Some(var) = template.placeholders.get(&element) { |
237 | builder.replace(element.text_range(), binding[var].to_string()) | 261 | builder.replace(element.text_range(), binding[var].to_string()) |
238 | } | 262 | } |
239 | } | 263 | } |
264 | for comment in ignored_comments { | ||
265 | builder.insert(template.template.text_range().end(), comment.syntax().to_string()) | ||
266 | } | ||
240 | builder.finish().apply(&template.template.text().to_string()) | 267 | builder.finish().apply(&template.template.text().to_string()) |
241 | } | 268 | } |
242 | 269 | ||
@@ -325,4 +352,66 @@ mod tests { | |||
325 | let edit = replace(&matches, &query.template); | 352 | let edit = replace(&matches, &query.template); |
326 | assert_eq!(edit.apply(input), "fn main() { bar(1+2); }"); | 353 | assert_eq!(edit.apply(input), "fn main() { bar(1+2); }"); |
327 | } | 354 | } |
355 | |||
356 | fn assert_ssr_transform(query: &str, input: &str, result: &str) { | ||
357 | let query: SsrQuery = query.parse().unwrap(); | ||
358 | let code = SourceFile::parse(input).tree(); | ||
359 | let matches = find(&query.pattern, code.syntax()); | ||
360 | let edit = replace(&matches, &query.template); | ||
361 | assert_eq!(edit.apply(input), result); | ||
362 | } | ||
363 | |||
364 | #[test] | ||
365 | fn ssr_function_to_method() { | ||
366 | assert_ssr_transform( | ||
367 | "my_function($a:expr, $b:expr) ==>> ($a).my_method($b)", | ||
368 | "loop { my_function( other_func(x, y), z + w) }", | ||
369 | "loop { (other_func(x, y)).my_method(z + w) }", | ||
370 | ) | ||
371 | } | ||
372 | |||
373 | #[test] | ||
374 | fn ssr_nested_function() { | ||
375 | assert_ssr_transform( | ||
376 | "foo($a:expr, $b:expr, $c:expr) ==>> bar($c, baz($a, $b))", | ||
377 | "fn main { foo (x + value.method(b), x+y-z, true && false) }", | ||
378 | "fn main { bar(true && false, baz(x + value.method(b), x+y-z)) }", | ||
379 | ) | ||
380 | } | ||
381 | |||
382 | #[test] | ||
383 | fn ssr_expected_spacing() { | ||
384 | assert_ssr_transform( | ||
385 | "foo($x:expr) + bar() ==>> bar($x)", | ||
386 | "fn main() { foo(5) + bar() }", | ||
387 | "fn main() { bar(5) }", | ||
388 | ); | ||
389 | } | ||
390 | |||
391 | #[test] | ||
392 | fn ssr_with_extra_space() { | ||
393 | assert_ssr_transform( | ||
394 | "foo($x:expr ) + bar() ==>> bar($x)", | ||
395 | "fn main() { foo( 5 ) +bar( ) }", | ||
396 | "fn main() { bar(5) }", | ||
397 | ); | ||
398 | } | ||
399 | |||
400 | #[test] | ||
401 | fn ssr_keeps_nested_comment() { | ||
402 | assert_ssr_transform( | ||
403 | "foo($x:expr) ==>> bar($x)", | ||
404 | "fn main() { foo(other(5 /* using 5 */)) }", | ||
405 | "fn main() { bar(other(5 /* using 5 */)) }", | ||
406 | ) | ||
407 | } | ||
408 | |||
409 | #[test] | ||
410 | fn ssr_keeps_comment() { | ||
411 | assert_ssr_transform( | ||
412 | "foo($x:expr) ==>> bar($x)", | ||
413 | "fn main() { foo(5 /* using 5 */) }", | ||
414 | "fn main() { bar(5)/* using 5 */ }", | ||
415 | ) | ||
416 | } | ||
328 | } | 417 | } |