diff options
Diffstat (limited to 'crates/ra_ssr/src/replacing.rs')
-rw-r--r-- | crates/ra_ssr/src/replacing.rs | 204 |
1 files changed, 167 insertions, 37 deletions
diff --git a/crates/ra_ssr/src/replacing.rs b/crates/ra_ssr/src/replacing.rs index 70ce1c185..0943244ff 100644 --- a/crates/ra_ssr/src/replacing.rs +++ b/crates/ra_ssr/src/replacing.rs | |||
@@ -1,64 +1,194 @@ | |||
1 | //! Code for applying replacement templates for matches that have previously been found. | 1 | //! Code for applying replacement templates for matches that have previously been found. |
2 | 2 | ||
3 | use crate::matching::Var; | 3 | use crate::matching::Var; |
4 | use crate::parsing::PatternElement; | 4 | use crate::{resolving::ResolvedRule, Match, SsrMatches}; |
5 | use crate::{Match, SsrMatches}; | 5 | use ra_syntax::ast::{self, AstToken}; |
6 | use ra_syntax::ast::AstToken; | 6 | use ra_syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize}; |
7 | use ra_syntax::TextSize; | ||
8 | use ra_text_edit::TextEdit; | 7 | use ra_text_edit::TextEdit; |
8 | use rustc_hash::{FxHashMap, FxHashSet}; | ||
9 | 9 | ||
10 | /// Returns a text edit that will replace each match in `matches` with its corresponding replacement | 10 | /// Returns a text edit that will replace each match in `matches` with its corresponding replacement |
11 | /// template. Placeholders in the template will have been substituted with whatever they matched to | 11 | /// template. Placeholders in the template will have been substituted with whatever they matched to |
12 | /// in the original code. | 12 | /// in the original code. |
13 | pub(crate) fn matches_to_edit(matches: &SsrMatches, file_src: &str) -> TextEdit { | 13 | pub(crate) fn matches_to_edit( |
14 | matches_to_edit_at_offset(matches, file_src, 0.into()) | 14 | matches: &SsrMatches, |
15 | file_src: &str, | ||
16 | rules: &[ResolvedRule], | ||
17 | ) -> TextEdit { | ||
18 | matches_to_edit_at_offset(matches, file_src, 0.into(), rules) | ||
15 | } | 19 | } |
16 | 20 | ||
17 | fn matches_to_edit_at_offset( | 21 | fn matches_to_edit_at_offset( |
18 | matches: &SsrMatches, | 22 | matches: &SsrMatches, |
19 | file_src: &str, | 23 | file_src: &str, |
20 | relative_start: TextSize, | 24 | relative_start: TextSize, |
25 | rules: &[ResolvedRule], | ||
21 | ) -> TextEdit { | 26 | ) -> TextEdit { |
22 | let mut edit_builder = ra_text_edit::TextEditBuilder::default(); | 27 | let mut edit_builder = ra_text_edit::TextEditBuilder::default(); |
23 | for m in &matches.matches { | 28 | for m in &matches.matches { |
24 | edit_builder | 29 | edit_builder.replace( |
25 | .replace(m.range.checked_sub(relative_start).unwrap(), render_replace(m, file_src)); | 30 | m.range.range.checked_sub(relative_start).unwrap(), |
31 | render_replace(m, file_src, rules), | ||
32 | ); | ||
26 | } | 33 | } |
27 | edit_builder.finish() | 34 | edit_builder.finish() |
28 | } | 35 | } |
29 | 36 | ||
30 | fn render_replace(match_info: &Match, file_src: &str) -> String { | 37 | struct ReplacementRenderer<'a> { |
31 | let mut out = String::new(); | 38 | match_info: &'a Match, |
32 | for r in &match_info.template.tokens { | 39 | file_src: &'a str, |
33 | match r { | 40 | rules: &'a [ResolvedRule], |
34 | PatternElement::Token(t) => out.push_str(t.text.as_str()), | 41 | rule: &'a ResolvedRule, |
35 | PatternElement::Placeholder(p) => { | 42 | out: String, |
36 | if let Some(placeholder_value) = | 43 | // Map from a range within `out` to a token in `template` that represents a placeholder. This is |
37 | match_info.placeholder_values.get(&Var(p.ident.to_string())) | 44 | // used to validate that the generated source code doesn't split any placeholder expansions (see |
38 | { | 45 | // below). |
39 | let range = &placeholder_value.range.range; | 46 | placeholder_tokens_by_range: FxHashMap<TextRange, SyntaxToken>, |
40 | let mut matched_text = | 47 | // Which placeholder tokens need to be wrapped in parenthesis in order to ensure that when `out` |
41 | file_src[usize::from(range.start())..usize::from(range.end())].to_owned(); | 48 | // is parsed, placeholders don't get split. e.g. if a template of `$a.to_string()` results in `1 |
42 | let edit = matches_to_edit_at_offset( | 49 | // + 2.to_string()` then the placeholder value `1 + 2` was split and needs parenthesis. |
43 | &placeholder_value.inner_matches, | 50 | placeholder_tokens_requiring_parenthesis: FxHashSet<SyntaxToken>, |
44 | file_src, | 51 | } |
45 | range.start(), | 52 | |
46 | ); | 53 | fn render_replace(match_info: &Match, file_src: &str, rules: &[ResolvedRule]) -> String { |
47 | edit.apply(&mut matched_text); | 54 | let rule = &rules[match_info.rule_index]; |
48 | out.push_str(&matched_text); | 55 | let template = rule |
49 | } else { | 56 | .template |
50 | // We validated that all placeholder references were valid before we | 57 | .as_ref() |
51 | // started, so this shouldn't happen. | 58 | .expect("You called MatchFinder::edits after calling MatchFinder::add_search_pattern"); |
52 | panic!( | 59 | let mut renderer = ReplacementRenderer { |
53 | "Internal error: replacement referenced unknown placeholder {}", | 60 | match_info, |
54 | p.ident | 61 | file_src, |
55 | ); | 62 | rules, |
63 | rule, | ||
64 | out: String::new(), | ||
65 | placeholder_tokens_requiring_parenthesis: FxHashSet::default(), | ||
66 | placeholder_tokens_by_range: FxHashMap::default(), | ||
67 | }; | ||
68 | renderer.render_node(&template.node); | ||
69 | renderer.maybe_rerender_with_extra_parenthesis(&template.node); | ||
70 | for comment in &match_info.ignored_comments { | ||
71 | renderer.out.push_str(&comment.syntax().to_string()); | ||
72 | } | ||
73 | renderer.out | ||
74 | } | ||
75 | |||
76 | impl ReplacementRenderer<'_> { | ||
77 | fn render_node_children(&mut self, node: &SyntaxNode) { | ||
78 | for node_or_token in node.children_with_tokens() { | ||
79 | self.render_node_or_token(&node_or_token); | ||
80 | } | ||
81 | } | ||
82 | |||
83 | fn render_node_or_token(&mut self, node_or_token: &SyntaxElement) { | ||
84 | match node_or_token { | ||
85 | SyntaxElement::Token(token) => { | ||
86 | self.render_token(&token); | ||
87 | } | ||
88 | SyntaxElement::Node(child_node) => { | ||
89 | self.render_node(&child_node); | ||
90 | } | ||
91 | } | ||
92 | } | ||
93 | |||
94 | fn render_node(&mut self, node: &SyntaxNode) { | ||
95 | use ra_syntax::ast::AstNode; | ||
96 | if let Some(mod_path) = self.match_info.rendered_template_paths.get(&node) { | ||
97 | self.out.push_str(&mod_path.to_string()); | ||
98 | // Emit everything except for the segment's name-ref, since we already effectively | ||
99 | // emitted that as part of `mod_path`. | ||
100 | if let Some(path) = ast::Path::cast(node.clone()) { | ||
101 | if let Some(segment) = path.segment() { | ||
102 | for node_or_token in segment.syntax().children_with_tokens() { | ||
103 | if node_or_token.kind() != SyntaxKind::NAME_REF { | ||
104 | self.render_node_or_token(&node_or_token); | ||
105 | } | ||
106 | } | ||
56 | } | 107 | } |
57 | } | 108 | } |
109 | } else { | ||
110 | self.render_node_children(&node); | ||
58 | } | 111 | } |
59 | } | 112 | } |
60 | for comment in &match_info.ignored_comments { | 113 | |
61 | out.push_str(&comment.syntax().to_string()); | 114 | fn render_token(&mut self, token: &SyntaxToken) { |
115 | if let Some(placeholder) = self.rule.get_placeholder(&token) { | ||
116 | if let Some(placeholder_value) = | ||
117 | self.match_info.placeholder_values.get(&Var(placeholder.ident.to_string())) | ||
118 | { | ||
119 | let range = &placeholder_value.range.range; | ||
120 | let mut matched_text = | ||
121 | self.file_src[usize::from(range.start())..usize::from(range.end())].to_owned(); | ||
122 | let edit = matches_to_edit_at_offset( | ||
123 | &placeholder_value.inner_matches, | ||
124 | self.file_src, | ||
125 | range.start(), | ||
126 | self.rules, | ||
127 | ); | ||
128 | let needs_parenthesis = | ||
129 | self.placeholder_tokens_requiring_parenthesis.contains(token); | ||
130 | edit.apply(&mut matched_text); | ||
131 | if needs_parenthesis { | ||
132 | self.out.push('('); | ||
133 | } | ||
134 | self.placeholder_tokens_by_range.insert( | ||
135 | TextRange::new( | ||
136 | TextSize::of(&self.out), | ||
137 | TextSize::of(&self.out) + TextSize::of(&matched_text), | ||
138 | ), | ||
139 | token.clone(), | ||
140 | ); | ||
141 | self.out.push_str(&matched_text); | ||
142 | if needs_parenthesis { | ||
143 | self.out.push(')'); | ||
144 | } | ||
145 | } else { | ||
146 | // We validated that all placeholder references were valid before we | ||
147 | // started, so this shouldn't happen. | ||
148 | panic!( | ||
149 | "Internal error: replacement referenced unknown placeholder {}", | ||
150 | placeholder.ident | ||
151 | ); | ||
152 | } | ||
153 | } else { | ||
154 | self.out.push_str(token.text().as_str()); | ||
155 | } | ||
156 | } | ||
157 | |||
158 | // Checks if the resulting code, when parsed doesn't split any placeholders due to different | ||
159 | // order of operations between the search pattern and the replacement template. If any do, then | ||
160 | // we rerender the template and wrap the problematic placeholders with parenthesis. | ||
161 | fn maybe_rerender_with_extra_parenthesis(&mut self, template: &SyntaxNode) { | ||
162 | if let Some(node) = parse_as_kind(&self.out, template.kind()) { | ||
163 | self.remove_node_ranges(node); | ||
164 | if self.placeholder_tokens_by_range.is_empty() { | ||
165 | return; | ||
166 | } | ||
167 | self.placeholder_tokens_requiring_parenthesis = | ||
168 | self.placeholder_tokens_by_range.values().cloned().collect(); | ||
169 | self.out.clear(); | ||
170 | self.render_node(template); | ||
171 | } | ||
172 | } | ||
173 | |||
174 | fn remove_node_ranges(&mut self, node: SyntaxNode) { | ||
175 | self.placeholder_tokens_by_range.remove(&node.text_range()); | ||
176 | for child in node.children() { | ||
177 | self.remove_node_ranges(child); | ||
178 | } | ||
179 | } | ||
180 | } | ||
181 | |||
182 | fn parse_as_kind(code: &str, kind: SyntaxKind) -> Option<SyntaxNode> { | ||
183 | use ra_syntax::ast::AstNode; | ||
184 | if ast::Expr::can_cast(kind) { | ||
185 | if let Ok(expr) = ast::Expr::parse(code) { | ||
186 | return Some(expr.syntax().clone()); | ||
187 | } | ||
188 | } else if ast::Item::can_cast(kind) { | ||
189 | if let Ok(item) = ast::Item::parse(code) { | ||
190 | return Some(item.syntax().clone()); | ||
191 | } | ||
62 | } | 192 | } |
63 | out | 193 | None |
64 | } | 194 | } |