aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ssr/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ssr/src')
-rw-r--r--crates/ra_ssr/src/lib.rs76
-rw-r--r--crates/ra_ssr/src/matching.rs42
-rw-r--r--crates/ra_ssr/src/parsing.rs106
-rw-r--r--crates/ra_ssr/src/replacing.rs6
4 files changed, 123 insertions, 107 deletions
diff --git a/crates/ra_ssr/src/lib.rs b/crates/ra_ssr/src/lib.rs
index cca4576ce..3009dcb93 100644
--- a/crates/ra_ssr/src/lib.rs
+++ b/crates/ra_ssr/src/lib.rs
@@ -13,35 +13,27 @@ mod tests;
13 13
14pub use crate::errors::SsrError; 14pub use crate::errors::SsrError;
15pub use crate::matching::Match; 15pub use crate::matching::Match;
16use crate::matching::{record_match_fails_reasons_scope, MatchFailureReason}; 16use crate::matching::MatchFailureReason;
17use hir::Semantics; 17use hir::Semantics;
18use parsing::SsrTemplate;
18use ra_db::{FileId, FileRange}; 19use ra_db::{FileId, FileRange};
19use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, TextRange}; 20use ra_syntax::{ast, AstNode, SyntaxNode, TextRange};
20use ra_text_edit::TextEdit; 21use ra_text_edit::TextEdit;
21use rustc_hash::FxHashMap;
22 22
23// A structured search replace rule. Create by calling `parse` on a str. 23// A structured search replace rule. Create by calling `parse` on a str.
24#[derive(Debug)] 24#[derive(Debug)]
25pub struct SsrRule { 25pub struct SsrRule {
26 /// A structured pattern that we're searching for. 26 /// A structured pattern that we're searching for.
27 pattern: SsrPattern, 27 pattern: parsing::RawPattern,
28 /// What we'll replace it with. 28 /// What we'll replace it with.
29 template: parsing::SsrTemplate, 29 template: SsrTemplate,
30 parsed_rules: Vec<parsing::ParsedRule>,
30} 31}
31 32
32#[derive(Debug)] 33#[derive(Debug)]
33pub struct SsrPattern { 34pub struct SsrPattern {
34 raw: parsing::RawSearchPattern, 35 raw: parsing::RawPattern,
35 /// Placeholders keyed by the stand-in ident that we use in Rust source code. 36 parsed_rules: Vec<parsing::ParsedRule>,
36 placeholders_by_stand_in: FxHashMap<SmolStr, parsing::Placeholder>,
37 // We store our search pattern, parsed as each different kind of thing we can look for. As we
38 // traverse the AST, we get the appropriate one of these for the type of node we're on. For many
39 // search patterns, only some of these will be present.
40 expr: Option<SyntaxNode>,
41 type_ref: Option<SyntaxNode>,
42 item: Option<SyntaxNode>,
43 path: Option<SyntaxNode>,
44 pattern: Option<SyntaxNode>,
45} 37}
46 38
47#[derive(Debug, Default)] 39#[derive(Debug, Default)]
@@ -53,7 +45,7 @@ pub struct SsrMatches {
53pub struct MatchFinder<'db> { 45pub struct MatchFinder<'db> {
54 /// Our source of information about the user's code. 46 /// Our source of information about the user's code.
55 sema: Semantics<'db, ra_ide_db::RootDatabase>, 47 sema: Semantics<'db, ra_ide_db::RootDatabase>,
56 rules: Vec<SsrRule>, 48 rules: Vec<parsing::ParsedRule>,
57} 49}
58 50
59impl<'db> MatchFinder<'db> { 51impl<'db> MatchFinder<'db> {
@@ -61,14 +53,17 @@ impl<'db> MatchFinder<'db> {
61 MatchFinder { sema: Semantics::new(db), rules: Vec::new() } 53 MatchFinder { sema: Semantics::new(db), rules: Vec::new() }
62 } 54 }
63 55
56 /// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take
57 /// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
58 /// match to it.
64 pub fn add_rule(&mut self, rule: SsrRule) { 59 pub fn add_rule(&mut self, rule: SsrRule) {
65 self.rules.push(rule); 60 self.add_parsed_rules(rule.parsed_rules);
66 } 61 }
67 62
68 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you 63 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
69 /// intend to do replacement, use `add_rule` instead. 64 /// intend to do replacement, use `add_rule` instead.
70 pub fn add_search_pattern(&mut self, pattern: SsrPattern) { 65 pub fn add_search_pattern(&mut self, pattern: SsrPattern) {
71 self.add_rule(SsrRule { pattern, template: "()".parse().unwrap() }) 66 self.add_parsed_rules(pattern.parsed_rules);
72 } 67 }
73 68
74 pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> { 69 pub fn edits_for_file(&self, file_id: FileId) -> Option<TextEdit> {
@@ -115,6 +110,14 @@ impl<'db> MatchFinder<'db> {
115 res 110 res
116 } 111 }
117 112
113 fn add_parsed_rules(&mut self, parsed_rules: Vec<parsing::ParsedRule>) {
114 // FIXME: This doesn't need to be a for loop, but does in a subsequent commit. Justify it
115 // being a for-loop.
116 for parsed_rule in parsed_rules {
117 self.rules.push(parsed_rule);
118 }
119 }
120
118 fn find_matches( 121 fn find_matches(
119 &self, 122 &self,
120 code: &SyntaxNode, 123 code: &SyntaxNode,
@@ -177,8 +180,13 @@ impl<'db> MatchFinder<'db> {
177 } 180 }
178 if node_range.range == range.range { 181 if node_range.range == range.range {
179 for rule in &self.rules { 182 for rule in &self.rules {
180 let pattern = 183 // For now we ignore rules that have a different kind than our node, otherwise
181 rule.pattern.tree_for_kind_with_reason(node.kind()).map(|p| p.clone()); 184 // we get lots of noise. If at some point we add support for restricting rules
185 // to a particular kind of thing (e.g. only match type references), then we can
186 // relax this.
187 if rule.pattern.kind() != node.kind() {
188 continue;
189 }
182 out.push(MatchDebugInfo { 190 out.push(MatchDebugInfo {
183 matched: matching::get_match(true, rule, &node, restrict_range, &self.sema) 191 matched: matching::get_match(true, rule, &node, restrict_range, &self.sema)
184 .map_err(|e| MatchFailureReason { 192 .map_err(|e| MatchFailureReason {
@@ -186,7 +194,7 @@ impl<'db> MatchFinder<'db> {
186 "Match failed, but no reason was given".to_owned() 194 "Match failed, but no reason was given".to_owned()
187 }), 195 }),
188 }), 196 }),
189 pattern, 197 pattern: rule.pattern.clone(),
190 node: node.clone(), 198 node: node.clone(),
191 }); 199 });
192 } 200 }
@@ -209,9 +217,8 @@ impl<'db> MatchFinder<'db> {
209 217
210pub struct MatchDebugInfo { 218pub struct MatchDebugInfo {
211 node: SyntaxNode, 219 node: SyntaxNode,
212 /// Our search pattern parsed as the same kind of syntax node as `node`. e.g. expression, item, 220 /// Our search pattern parsed as an expression or item, etc
213 /// etc. Will be absent if the pattern can't be parsed as that kind. 221 pattern: SyntaxNode,
214 pattern: Result<SyntaxNode, MatchFailureReason>,
215 matched: Result<Match, MatchFailureReason>, 222 matched: Result<Match, MatchFailureReason>,
216} 223}
217 224
@@ -228,29 +235,12 @@ impl std::fmt::Debug for MatchDebugInfo {
228 self.node 235 self.node
229 )?; 236 )?;
230 writeln!(f, "========= PATTERN ==========")?; 237 writeln!(f, "========= PATTERN ==========")?;
231 match &self.pattern { 238 writeln!(f, "{:#?}", self.pattern)?;
232 Ok(pattern) => {
233 writeln!(f, "{:#?}", pattern)?;
234 }
235 Err(err) => {
236 writeln!(f, "{}", err.reason)?;
237 }
238 }
239 writeln!(f, "============================")?; 239 writeln!(f, "============================")?;
240 Ok(()) 240 Ok(())
241 } 241 }
242} 242}
243 243
244impl SsrPattern {
245 fn tree_for_kind_with_reason(
246 &self,
247 kind: SyntaxKind,
248 ) -> Result<&SyntaxNode, MatchFailureReason> {
249 record_match_fails_reasons_scope(true, || self.tree_for_kind(kind))
250 .map_err(|e| MatchFailureReason { reason: e.reason.unwrap() })
251 }
252}
253
254impl SsrMatches { 244impl SsrMatches {
255 /// Returns `self` with any nested matches removed and made into top-level matches. 245 /// Returns `self` with any nested matches removed and made into top-level matches.
256 pub fn flattened(self) -> SsrMatches { 246 pub fn flattened(self) -> SsrMatches {
diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs
index 50b29eab2..842f4b6f3 100644
--- a/crates/ra_ssr/src/matching.rs
+++ b/crates/ra_ssr/src/matching.rs
@@ -2,8 +2,8 @@
2//! process of matching, placeholder values are recorded. 2//! process of matching, placeholder values are recorded.
3 3
4use crate::{ 4use crate::{
5 parsing::{Constraint, NodeKind, Placeholder, SsrTemplate}, 5 parsing::{Constraint, NodeKind, ParsedRule, Placeholder, SsrTemplate},
6 SsrMatches, SsrPattern, SsrRule, 6 SsrMatches,
7}; 7};
8use hir::Semantics; 8use hir::Semantics;
9use ra_db::FileRange; 9use ra_db::FileRange;
@@ -50,7 +50,7 @@ pub struct Match {
50 pub(crate) ignored_comments: Vec<ast::Comment>, 50 pub(crate) ignored_comments: Vec<ast::Comment>,
51 // A copy of the template for the rule that produced this match. We store this on the match for 51 // A copy of the template for the rule that produced this match. We store this on the match for
52 // if/when we do replacement. 52 // if/when we do replacement.
53 pub(crate) template: SsrTemplate, 53 pub(crate) template: Option<SsrTemplate>,
54} 54}
55 55
56/// Represents a `$var` in an SSR query. 56/// Represents a `$var` in an SSR query.
@@ -86,7 +86,7 @@ pub(crate) struct MatchFailed {
86/// parent module, we don't populate nested matches. 86/// parent module, we don't populate nested matches.
87pub(crate) fn get_match( 87pub(crate) fn get_match(
88 debug_active: bool, 88 debug_active: bool,
89 rule: &SsrRule, 89 rule: &ParsedRule,
90 code: &SyntaxNode, 90 code: &SyntaxNode,
91 restrict_range: &Option<FileRange>, 91 restrict_range: &Option<FileRange>,
92 sema: &Semantics<ra_ide_db::RootDatabase>, 92 sema: &Semantics<ra_ide_db::RootDatabase>,
@@ -102,7 +102,7 @@ struct Matcher<'db, 'sema> {
102 /// If any placeholders come from anywhere outside of this range, then the match will be 102 /// If any placeholders come from anywhere outside of this range, then the match will be
103 /// rejected. 103 /// rejected.
104 restrict_range: Option<FileRange>, 104 restrict_range: Option<FileRange>,
105 rule: &'sema SsrRule, 105 rule: &'sema ParsedRule,
106} 106}
107 107
108/// Which phase of matching we're currently performing. We do two phases because most attempted 108/// Which phase of matching we're currently performing. We do two phases because most attempted
@@ -117,15 +117,14 @@ enum Phase<'a> {
117 117
118impl<'db, 'sema> Matcher<'db, 'sema> { 118impl<'db, 'sema> Matcher<'db, 'sema> {
119 fn try_match( 119 fn try_match(
120 rule: &'sema SsrRule, 120 rule: &ParsedRule,
121 code: &SyntaxNode, 121 code: &SyntaxNode,
122 restrict_range: &Option<FileRange>, 122 restrict_range: &Option<FileRange>,
123 sema: &'sema Semantics<'db, ra_ide_db::RootDatabase>, 123 sema: &'sema Semantics<'db, ra_ide_db::RootDatabase>,
124 ) -> Result<Match, MatchFailed> { 124 ) -> Result<Match, MatchFailed> {
125 let match_state = Matcher { sema, restrict_range: restrict_range.clone(), rule }; 125 let match_state = Matcher { sema, restrict_range: restrict_range.clone(), rule };
126 let pattern_tree = rule.pattern.tree_for_kind(code.kind())?;
127 // First pass at matching, where we check that node types and idents match. 126 // First pass at matching, where we check that node types and idents match.
128 match_state.attempt_match_node(&mut Phase::First, &pattern_tree, code)?; 127 match_state.attempt_match_node(&mut Phase::First, &rule.pattern, code)?;
129 match_state.validate_range(&sema.original_range(code))?; 128 match_state.validate_range(&sema.original_range(code))?;
130 let mut the_match = Match { 129 let mut the_match = Match {
131 range: sema.original_range(code), 130 range: sema.original_range(code),
@@ -136,7 +135,7 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
136 }; 135 };
137 // Second matching pass, where we record placeholder matches, ignored comments and maybe do 136 // Second matching pass, where we record placeholder matches, ignored comments and maybe do
138 // any other more expensive checks that we didn't want to do on the first pass. 137 // any other more expensive checks that we didn't want to do on the first pass.
139 match_state.attempt_match_node(&mut Phase::Second(&mut the_match), &pattern_tree, code)?; 138 match_state.attempt_match_node(&mut Phase::Second(&mut the_match), &rule.pattern, code)?;
140 Ok(the_match) 139 Ok(the_match)
141 } 140 }
142 141
@@ -444,8 +443,7 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
444 } 443 }
445 444
446 fn get_placeholder(&self, element: &SyntaxElement) -> Option<&Placeholder> { 445 fn get_placeholder(&self, element: &SyntaxElement) -> Option<&Placeholder> {
447 only_ident(element.clone()) 446 only_ident(element.clone()).and_then(|ident| self.rule.get_placeholder(&ident))
448 .and_then(|ident| self.rule.pattern.placeholders_by_stand_in.get(ident.text()))
449 } 447 }
450} 448}
451 449
@@ -510,28 +508,6 @@ impl PlaceholderMatch {
510 } 508 }
511} 509}
512 510
513impl SsrPattern {
514 pub(crate) fn tree_for_kind(&self, kind: SyntaxKind) -> Result<&SyntaxNode, MatchFailed> {
515 let (tree, kind_name) = if ast::Expr::can_cast(kind) {
516 (&self.expr, "expression")
517 } else if ast::TypeRef::can_cast(kind) {
518 (&self.type_ref, "type reference")
519 } else if ast::ModuleItem::can_cast(kind) {
520 (&self.item, "item")
521 } else if ast::Path::can_cast(kind) {
522 (&self.path, "path")
523 } else if ast::Pat::can_cast(kind) {
524 (&self.pattern, "pattern")
525 } else {
526 fail_match!("Matching nodes of kind {:?} is not supported", kind);
527 };
528 match tree {
529 Some(tree) => Ok(tree),
530 None => fail_match!("Pattern cannot be parsed as a {}", kind_name),
531 }
532 }
533}
534
535impl NodeKind { 511impl NodeKind {
536 fn matches(&self, node: &SyntaxNode) -> Result<(), MatchFailed> { 512 fn matches(&self, node: &SyntaxNode) -> Result<(), MatchFailed> {
537 let ok = match self { 513 let ok = match self {
diff --git a/crates/ra_ssr/src/parsing.rs b/crates/ra_ssr/src/parsing.rs
index 4aee97bb2..682b7011a 100644
--- a/crates/ra_ssr/src/parsing.rs
+++ b/crates/ra_ssr/src/parsing.rs
@@ -7,17 +7,24 @@
7 7
8use crate::errors::bail; 8use crate::errors::bail;
9use crate::{SsrError, SsrPattern, SsrRule}; 9use crate::{SsrError, SsrPattern, SsrRule};
10use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, T}; 10use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, SyntaxToken, T};
11use rustc_hash::{FxHashMap, FxHashSet}; 11use rustc_hash::{FxHashMap, FxHashSet};
12use std::str::FromStr; 12use std::str::FromStr;
13 13
14#[derive(Debug)]
15pub(crate) struct ParsedRule {
16 pub(crate) placeholders_by_stand_in: FxHashMap<SmolStr, Placeholder>,
17 pub(crate) pattern: SyntaxNode,
18 pub(crate) template: Option<SsrTemplate>,
19}
20
14#[derive(Clone, Debug)] 21#[derive(Clone, Debug)]
15pub(crate) struct SsrTemplate { 22pub(crate) struct SsrTemplate {
16 pub(crate) tokens: Vec<PatternElement>, 23 pub(crate) tokens: Vec<PatternElement>,
17} 24}
18 25
19#[derive(Debug)] 26#[derive(Debug)]
20pub(crate) struct RawSearchPattern { 27pub(crate) struct RawPattern {
21 tokens: Vec<PatternElement>, 28 tokens: Vec<PatternElement>,
22} 29}
23 30
@@ -54,6 +61,50 @@ pub(crate) struct Token {
54 pub(crate) text: SmolStr, 61 pub(crate) text: SmolStr,
55} 62}
56 63
64impl ParsedRule {
65 fn new(
66 pattern: &RawPattern,
67 template: Option<&SsrTemplate>,
68 ) -> Result<Vec<ParsedRule>, SsrError> {
69 let raw_pattern = pattern.as_rust_code();
70 let mut builder = RuleBuilder {
71 placeholders_by_stand_in: pattern.placeholders_by_stand_in(),
72 rules: Vec::new(),
73 };
74 builder.try_add(ast::Expr::parse(&raw_pattern), template);
75 builder.try_add(ast::TypeRef::parse(&raw_pattern), template);
76 builder.try_add(ast::ModuleItem::parse(&raw_pattern), template);
77 builder.try_add(ast::Path::parse(&raw_pattern), template);
78 builder.try_add(ast::Pat::parse(&raw_pattern), template);
79 builder.build()
80 }
81}
82
83struct RuleBuilder {
84 placeholders_by_stand_in: FxHashMap<SmolStr, Placeholder>,
85 rules: Vec<ParsedRule>,
86}
87
88impl RuleBuilder {
89 fn try_add<T: AstNode>(&mut self, pattern: Result<T, ()>, template: Option<&SsrTemplate>) {
90 match pattern {
91 Ok(pattern) => self.rules.push(ParsedRule {
92 placeholders_by_stand_in: self.placeholders_by_stand_in.clone(),
93 pattern: pattern.syntax().clone(),
94 template: template.cloned(),
95 }),
96 _ => {}
97 }
98 }
99
100 fn build(self) -> Result<Vec<ParsedRule>, SsrError> {
101 if self.rules.is_empty() {
102 bail!("Pattern is not a valid Rust expression, type, item, path or pattern");
103 }
104 Ok(self.rules)
105 }
106}
107
57impl FromStr for SsrRule { 108impl FromStr for SsrRule {
58 type Err = SsrError; 109 type Err = SsrError;
59 110
@@ -68,21 +119,24 @@ impl FromStr for SsrRule {
68 if it.next().is_some() { 119 if it.next().is_some() {
69 return Err(SsrError("More than one delimiter found".into())); 120 return Err(SsrError("More than one delimiter found".into()));
70 } 121 }
71 let rule = SsrRule { pattern: pattern.parse()?, template: template.parse()? }; 122 let raw_pattern = pattern.parse()?;
123 let raw_template = template.parse()?;
124 let parsed_rules = ParsedRule::new(&raw_pattern, Some(&raw_template))?;
125 let rule = SsrRule { pattern: raw_pattern, template: raw_template, parsed_rules };
72 validate_rule(&rule)?; 126 validate_rule(&rule)?;
73 Ok(rule) 127 Ok(rule)
74 } 128 }
75} 129}
76 130
77impl FromStr for RawSearchPattern { 131impl FromStr for RawPattern {
78 type Err = SsrError; 132 type Err = SsrError;
79 133
80 fn from_str(pattern_str: &str) -> Result<RawSearchPattern, SsrError> { 134 fn from_str(pattern_str: &str) -> Result<RawPattern, SsrError> {
81 Ok(RawSearchPattern { tokens: parse_pattern(pattern_str)? }) 135 Ok(RawPattern { tokens: parse_pattern(pattern_str)? })
82 } 136 }
83} 137}
84 138
85impl RawSearchPattern { 139impl RawPattern {
86 /// Returns this search pattern as Rust source code that we can feed to the Rust parser. 140 /// Returns this search pattern as Rust source code that we can feed to the Rust parser.
87 fn as_rust_code(&self) -> String { 141 fn as_rust_code(&self) -> String {
88 let mut res = String::new(); 142 let mut res = String::new();
@@ -95,7 +149,7 @@ impl RawSearchPattern {
95 res 149 res
96 } 150 }
97 151
98 fn placeholders_by_stand_in(&self) -> FxHashMap<SmolStr, Placeholder> { 152 pub(crate) fn placeholders_by_stand_in(&self) -> FxHashMap<SmolStr, Placeholder> {
99 let mut res = FxHashMap::default(); 153 let mut res = FxHashMap::default();
100 for t in &self.tokens { 154 for t in &self.tokens {
101 if let PatternElement::Placeholder(placeholder) = t { 155 if let PatternElement::Placeholder(placeholder) = t {
@@ -106,30 +160,22 @@ impl RawSearchPattern {
106 } 160 }
107} 161}
108 162
163impl ParsedRule {
164 pub(crate) fn get_placeholder(&self, token: &SyntaxToken) -> Option<&Placeholder> {
165 if token.kind() != SyntaxKind::IDENT {
166 return None;
167 }
168 self.placeholders_by_stand_in.get(token.text())
169 }
170}
171
109impl FromStr for SsrPattern { 172impl FromStr for SsrPattern {
110 type Err = SsrError; 173 type Err = SsrError;
111 174
112 fn from_str(pattern_str: &str) -> Result<SsrPattern, SsrError> { 175 fn from_str(pattern_str: &str) -> Result<SsrPattern, SsrError> {
113 let raw: RawSearchPattern = pattern_str.parse()?; 176 let raw_pattern = pattern_str.parse()?;
114 let raw_str = raw.as_rust_code(); 177 let parsed_rules = ParsedRule::new(&raw_pattern, None)?;
115 let res = SsrPattern { 178 Ok(SsrPattern { raw: raw_pattern, parsed_rules })
116 expr: ast::Expr::parse(&raw_str).ok().map(|n| n.syntax().clone()),
117 type_ref: ast::TypeRef::parse(&raw_str).ok().map(|n| n.syntax().clone()),
118 item: ast::ModuleItem::parse(&raw_str).ok().map(|n| n.syntax().clone()),
119 path: ast::Path::parse(&raw_str).ok().map(|n| n.syntax().clone()),
120 pattern: ast::Pat::parse(&raw_str).ok().map(|n| n.syntax().clone()),
121 placeholders_by_stand_in: raw.placeholders_by_stand_in(),
122 raw,
123 };
124 if res.expr.is_none()
125 && res.type_ref.is_none()
126 && res.item.is_none()
127 && res.path.is_none()
128 && res.pattern.is_none()
129 {
130 bail!("Pattern is not a valid Rust expression, type, item, path or pattern");
131 }
132 Ok(res)
133 } 179 }
134} 180}
135 181
@@ -173,7 +219,7 @@ fn parse_pattern(pattern_str: &str) -> Result<Vec<PatternElement>, SsrError> {
173/// pattern didn't define. 219/// pattern didn't define.
174fn validate_rule(rule: &SsrRule) -> Result<(), SsrError> { 220fn validate_rule(rule: &SsrRule) -> Result<(), SsrError> {
175 let mut defined_placeholders = FxHashSet::default(); 221 let mut defined_placeholders = FxHashSet::default();
176 for p in &rule.pattern.raw.tokens { 222 for p in &rule.pattern.tokens {
177 if let PatternElement::Placeholder(placeholder) = p { 223 if let PatternElement::Placeholder(placeholder) = p {
178 defined_placeholders.insert(&placeholder.ident); 224 defined_placeholders.insert(&placeholder.ident);
179 } 225 }
@@ -316,7 +362,7 @@ mod tests {
316 } 362 }
317 let result: SsrRule = "foo($a, $b) ==>> bar($b, $a)".parse().unwrap(); 363 let result: SsrRule = "foo($a, $b) ==>> bar($b, $a)".parse().unwrap();
318 assert_eq!( 364 assert_eq!(
319 result.pattern.raw.tokens, 365 result.pattern.tokens,
320 vec![ 366 vec![
321 token(SyntaxKind::IDENT, "foo"), 367 token(SyntaxKind::IDENT, "foo"),
322 token(T!['('], "("), 368 token(T!['('], "("),
diff --git a/crates/ra_ssr/src/replacing.rs b/crates/ra_ssr/src/replacing.rs
index e43cc5167..81f8634ba 100644
--- a/crates/ra_ssr/src/replacing.rs
+++ b/crates/ra_ssr/src/replacing.rs
@@ -31,7 +31,11 @@ fn matches_to_edit_at_offset(
31 31
32fn render_replace(match_info: &Match, file_src: &str) -> String { 32fn render_replace(match_info: &Match, file_src: &str) -> String {
33 let mut out = String::new(); 33 let mut out = String::new();
34 for r in &match_info.template.tokens { 34 let template = match_info
35 .template
36 .as_ref()
37 .expect("You called MatchFinder::edits after calling MatchFinder::add_search_pattern");
38 for r in &template.tokens {
35 match r { 39 match r {
36 PatternElement::Token(t) => out.push_str(t.text.as_str()), 40 PatternElement::Token(t) => out.push_str(t.text.as_str()),
37 PatternElement::Placeholder(p) => { 41 PatternElement::Placeholder(p) => {