diff options
Diffstat (limited to 'crates/ra_assists/src/ast_editor.rs')
-rw-r--r-- | crates/ra_assists/src/ast_editor.rs | 333 |
1 files changed, 333 insertions, 0 deletions
diff --git a/crates/ra_assists/src/ast_editor.rs b/crates/ra_assists/src/ast_editor.rs new file mode 100644 index 000000000..6854294ae --- /dev/null +++ b/crates/ra_assists/src/ast_editor.rs | |||
@@ -0,0 +1,333 @@ | |||
1 | use std::{iter, ops::RangeInclusive}; | ||
2 | |||
3 | use arrayvec::ArrayVec; | ||
4 | use ra_text_edit::TextEditBuilder; | ||
5 | use ra_syntax::{AstNode, TreeArc, ast, SyntaxKind::*, SyntaxElement, SourceFile, InsertPosition, Direction}; | ||
6 | use ra_fmt::leading_indent; | ||
7 | |||
8 | pub struct AstEditor<N: AstNode> { | ||
9 | original_ast: TreeArc<N>, | ||
10 | ast: TreeArc<N>, | ||
11 | } | ||
12 | |||
13 | impl<N: AstNode> AstEditor<N> { | ||
14 | pub fn new(node: &N) -> AstEditor<N> { | ||
15 | AstEditor { original_ast: node.to_owned(), ast: node.to_owned() } | ||
16 | } | ||
17 | |||
18 | pub fn into_text_edit(self, builder: &mut TextEditBuilder) { | ||
19 | // FIXME: compute a more fine-grained diff here. | ||
20 | // If *you* know a nice algorithm to compute diff between two syntax | ||
21 | // tree, tell me about it! | ||
22 | builder.replace(self.original_ast.syntax().range(), self.ast().syntax().text().to_string()); | ||
23 | } | ||
24 | |||
25 | pub fn ast(&self) -> &N { | ||
26 | &*self.ast | ||
27 | } | ||
28 | |||
29 | #[must_use] | ||
30 | fn insert_children<'a>( | ||
31 | &self, | ||
32 | position: InsertPosition<SyntaxElement<'_>>, | ||
33 | to_insert: impl Iterator<Item = SyntaxElement<'a>>, | ||
34 | ) -> TreeArc<N> { | ||
35 | let new_syntax = self.ast().syntax().insert_children(position, to_insert); | ||
36 | N::cast(&new_syntax).unwrap().to_owned() | ||
37 | } | ||
38 | |||
39 | #[must_use] | ||
40 | fn replace_children<'a>( | ||
41 | &self, | ||
42 | to_delete: RangeInclusive<SyntaxElement<'_>>, | ||
43 | to_insert: impl Iterator<Item = SyntaxElement<'a>>, | ||
44 | ) -> TreeArc<N> { | ||
45 | let new_syntax = self.ast().syntax().replace_children(to_delete, to_insert); | ||
46 | N::cast(&new_syntax).unwrap().to_owned() | ||
47 | } | ||
48 | |||
49 | fn do_make_multiline(&mut self) { | ||
50 | let l_curly = | ||
51 | match self.ast().syntax().children_with_tokens().find(|it| it.kind() == L_CURLY) { | ||
52 | Some(it) => it, | ||
53 | None => return, | ||
54 | }; | ||
55 | let sibling = match l_curly.next_sibling_or_token() { | ||
56 | Some(it) => it, | ||
57 | None => return, | ||
58 | }; | ||
59 | let existing_ws = match sibling.as_token() { | ||
60 | None => None, | ||
61 | Some(tok) if tok.kind() != WHITESPACE => None, | ||
62 | Some(ws) => { | ||
63 | if ws.text().contains('\n') { | ||
64 | return; | ||
65 | } | ||
66 | Some(ws) | ||
67 | } | ||
68 | }; | ||
69 | |||
70 | let indent = leading_indent(self.ast().syntax()).unwrap_or(""); | ||
71 | let ws = tokens::WsBuilder::new(&format!("\n{}", indent)); | ||
72 | let to_insert = iter::once(ws.ws().into()); | ||
73 | self.ast = match existing_ws { | ||
74 | None => self.insert_children(InsertPosition::After(l_curly), to_insert), | ||
75 | Some(ws) => self.replace_children(RangeInclusive::new(ws.into(), ws.into()), to_insert), | ||
76 | }; | ||
77 | } | ||
78 | } | ||
79 | |||
80 | impl AstEditor<ast::NamedFieldList> { | ||
81 | pub fn append_field(&mut self, field: &ast::NamedField) { | ||
82 | self.insert_field(InsertPosition::Last, field) | ||
83 | } | ||
84 | |||
85 | pub fn make_multiline(&mut self) { | ||
86 | self.do_make_multiline() | ||
87 | } | ||
88 | |||
89 | pub fn insert_field( | ||
90 | &mut self, | ||
91 | position: InsertPosition<&'_ ast::NamedField>, | ||
92 | field: &ast::NamedField, | ||
93 | ) { | ||
94 | let is_multiline = self.ast().syntax().text().contains('\n'); | ||
95 | let ws; | ||
96 | let space = if is_multiline { | ||
97 | ws = tokens::WsBuilder::new(&format!( | ||
98 | "\n{} ", | ||
99 | leading_indent(self.ast().syntax()).unwrap_or("") | ||
100 | )); | ||
101 | ws.ws() | ||
102 | } else { | ||
103 | tokens::single_space() | ||
104 | }; | ||
105 | |||
106 | let mut to_insert: ArrayVec<[SyntaxElement; 4]> = ArrayVec::new(); | ||
107 | to_insert.push(space.into()); | ||
108 | to_insert.push(field.syntax().into()); | ||
109 | to_insert.push(tokens::comma().into()); | ||
110 | |||
111 | macro_rules! after_l_curly { | ||
112 | () => {{ | ||
113 | let anchor = match self.l_curly() { | ||
114 | Some(it) => it, | ||
115 | None => return, | ||
116 | }; | ||
117 | InsertPosition::After(anchor) | ||
118 | }}; | ||
119 | } | ||
120 | |||
121 | macro_rules! after_field { | ||
122 | ($anchor:expr) => { | ||
123 | if let Some(comma) = $anchor | ||
124 | .syntax() | ||
125 | .siblings_with_tokens(Direction::Next) | ||
126 | .find(|it| it.kind() == COMMA) | ||
127 | { | ||
128 | InsertPosition::After(comma) | ||
129 | } else { | ||
130 | to_insert.insert(0, tokens::comma().into()); | ||
131 | InsertPosition::After($anchor.syntax().into()) | ||
132 | } | ||
133 | }; | ||
134 | }; | ||
135 | |||
136 | let position = match position { | ||
137 | InsertPosition::First => after_l_curly!(), | ||
138 | InsertPosition::Last => { | ||
139 | if !is_multiline { | ||
140 | // don't insert comma before curly | ||
141 | to_insert.pop(); | ||
142 | } | ||
143 | match self.ast().fields().last() { | ||
144 | Some(it) => after_field!(it), | ||
145 | None => after_l_curly!(), | ||
146 | } | ||
147 | } | ||
148 | InsertPosition::Before(anchor) => InsertPosition::Before(anchor.syntax().into()), | ||
149 | InsertPosition::After(anchor) => after_field!(anchor), | ||
150 | }; | ||
151 | |||
152 | self.ast = self.insert_children(position, to_insert.iter().cloned()); | ||
153 | } | ||
154 | |||
155 | fn l_curly(&self) -> Option<SyntaxElement> { | ||
156 | self.ast().syntax().children_with_tokens().find(|it| it.kind() == L_CURLY) | ||
157 | } | ||
158 | } | ||
159 | |||
160 | impl AstEditor<ast::ItemList> { | ||
161 | pub fn make_multiline(&mut self) { | ||
162 | self.do_make_multiline() | ||
163 | } | ||
164 | |||
165 | pub fn append_functions<'a>(&mut self, fns: impl Iterator<Item = &'a ast::FnDef>) { | ||
166 | fns.for_each(|it| self.append_function(it)) | ||
167 | } | ||
168 | |||
169 | pub fn append_function(&mut self, fn_def: &ast::FnDef) { | ||
170 | let (indent, position) = match self.ast().impl_items().last() { | ||
171 | Some(it) => ( | ||
172 | leading_indent(it.syntax()).unwrap_or("").to_string(), | ||
173 | InsertPosition::After(it.syntax().into()), | ||
174 | ), | ||
175 | None => match self.l_curly() { | ||
176 | Some(it) => ( | ||
177 | " ".to_string() + leading_indent(self.ast().syntax()).unwrap_or(""), | ||
178 | InsertPosition::After(it), | ||
179 | ), | ||
180 | None => return, | ||
181 | }, | ||
182 | }; | ||
183 | let ws = tokens::WsBuilder::new(&format!("\n{}", indent)); | ||
184 | let to_insert: ArrayVec<[SyntaxElement; 2]> = | ||
185 | [ws.ws().into(), fn_def.syntax().into()].into(); | ||
186 | self.ast = self.insert_children(position, to_insert.into_iter()); | ||
187 | } | ||
188 | |||
189 | fn l_curly(&self) -> Option<SyntaxElement> { | ||
190 | self.ast().syntax().children_with_tokens().find(|it| it.kind() == L_CURLY) | ||
191 | } | ||
192 | } | ||
193 | |||
194 | impl AstEditor<ast::FnDef> { | ||
195 | pub fn set_body(&mut self, body: &ast::Block) { | ||
196 | let mut to_insert: ArrayVec<[SyntaxElement; 2]> = ArrayVec::new(); | ||
197 | let old_body_or_semi: SyntaxElement = if let Some(old_body) = self.ast().body() { | ||
198 | old_body.syntax().into() | ||
199 | } else if let Some(semi) = self.ast().semicolon_token() { | ||
200 | to_insert.push(tokens::single_space().into()); | ||
201 | semi.into() | ||
202 | } else { | ||
203 | to_insert.push(tokens::single_space().into()); | ||
204 | to_insert.push(body.syntax().into()); | ||
205 | self.ast = self.insert_children(InsertPosition::Last, to_insert.into_iter()); | ||
206 | return; | ||
207 | }; | ||
208 | to_insert.push(body.syntax().into()); | ||
209 | let replace_range = RangeInclusive::new(old_body_or_semi, old_body_or_semi); | ||
210 | self.ast = self.replace_children(replace_range, to_insert.into_iter()) | ||
211 | } | ||
212 | |||
213 | pub fn strip_attrs_and_docs(&mut self) { | ||
214 | loop { | ||
215 | if let Some(start) = self | ||
216 | .ast() | ||
217 | .syntax() | ||
218 | .children_with_tokens() | ||
219 | .find(|it| it.kind() == ATTR || it.kind() == COMMENT) | ||
220 | { | ||
221 | let end = match start.next_sibling_or_token() { | ||
222 | Some(el) if el.kind() == WHITESPACE => el, | ||
223 | Some(_) | None => start, | ||
224 | }; | ||
225 | self.ast = self.replace_children(RangeInclusive::new(start, end), iter::empty()); | ||
226 | } else { | ||
227 | break; | ||
228 | } | ||
229 | } | ||
230 | } | ||
231 | } | ||
232 | |||
233 | pub struct AstBuilder<N: AstNode> { | ||
234 | _phantom: std::marker::PhantomData<N>, | ||
235 | } | ||
236 | |||
237 | impl AstBuilder<ast::NamedField> { | ||
238 | fn from_text(text: &str) -> TreeArc<ast::NamedField> { | ||
239 | ast_node_from_file_text(&format!("fn f() {{ S {{ {}, }} }}", text)) | ||
240 | } | ||
241 | |||
242 | pub fn from_pieces(name: &ast::NameRef, expr: Option<&ast::Expr>) -> TreeArc<ast::NamedField> { | ||
243 | match expr { | ||
244 | Some(expr) => Self::from_text(&format!("{}: {}", name.syntax(), expr.syntax())), | ||
245 | None => Self::from_text(&name.syntax().to_string()), | ||
246 | } | ||
247 | } | ||
248 | } | ||
249 | |||
250 | impl AstBuilder<ast::Block> { | ||
251 | fn from_text(text: &str) -> TreeArc<ast::Block> { | ||
252 | ast_node_from_file_text(&format!("fn f() {}", text)) | ||
253 | } | ||
254 | |||
255 | pub fn single_expr(e: &ast::Expr) -> TreeArc<ast::Block> { | ||
256 | Self::from_text(&format!("{{ {} }}", e.syntax())) | ||
257 | } | ||
258 | } | ||
259 | |||
260 | impl AstBuilder<ast::Expr> { | ||
261 | fn from_text(text: &str) -> TreeArc<ast::Expr> { | ||
262 | ast_node_from_file_text(&format!("fn f() {{ {}; }}", text)) | ||
263 | } | ||
264 | |||
265 | pub fn unit() -> TreeArc<ast::Expr> { | ||
266 | Self::from_text("()") | ||
267 | } | ||
268 | |||
269 | pub fn unimplemented() -> TreeArc<ast::Expr> { | ||
270 | Self::from_text("unimplemented!()") | ||
271 | } | ||
272 | } | ||
273 | |||
274 | impl AstBuilder<ast::NameRef> { | ||
275 | pub fn new(text: &str) -> TreeArc<ast::NameRef> { | ||
276 | ast_node_from_file_text(&format!("fn f() {{ {}; }}", text)) | ||
277 | } | ||
278 | } | ||
279 | |||
280 | fn ast_node_from_file_text<N: AstNode>(text: &str) -> TreeArc<N> { | ||
281 | let file = SourceFile::parse(text); | ||
282 | let res = file.syntax().descendants().find_map(N::cast).unwrap().to_owned(); | ||
283 | res | ||
284 | } | ||
285 | |||
286 | mod tokens { | ||
287 | use lazy_static::lazy_static; | ||
288 | use ra_syntax::{AstNode, SourceFile, TreeArc, SyntaxToken, SyntaxKind::*}; | ||
289 | |||
290 | lazy_static! { | ||
291 | static ref SOURCE_FILE: TreeArc<SourceFile> = SourceFile::parse(",\n; ;"); | ||
292 | } | ||
293 | |||
294 | pub(crate) fn comma() -> SyntaxToken<'static> { | ||
295 | SOURCE_FILE | ||
296 | .syntax() | ||
297 | .descendants_with_tokens() | ||
298 | .filter_map(|it| it.as_token()) | ||
299 | .find(|it| it.kind() == COMMA) | ||
300 | .unwrap() | ||
301 | } | ||
302 | |||
303 | pub(crate) fn single_space() -> SyntaxToken<'static> { | ||
304 | SOURCE_FILE | ||
305 | .syntax() | ||
306 | .descendants_with_tokens() | ||
307 | .filter_map(|it| it.as_token()) | ||
308 | .find(|it| it.kind() == WHITESPACE && it.text().as_str() == " ") | ||
309 | .unwrap() | ||
310 | } | ||
311 | |||
312 | #[allow(unused)] | ||
313 | pub(crate) fn single_newline() -> SyntaxToken<'static> { | ||
314 | SOURCE_FILE | ||
315 | .syntax() | ||
316 | .descendants_with_tokens() | ||
317 | .filter_map(|it| it.as_token()) | ||
318 | .find(|it| it.kind() == WHITESPACE && it.text().as_str() == "\n") | ||
319 | .unwrap() | ||
320 | } | ||
321 | |||
322 | pub(crate) struct WsBuilder(TreeArc<SourceFile>); | ||
323 | |||
324 | impl WsBuilder { | ||
325 | pub(crate) fn new(text: &str) -> WsBuilder { | ||
326 | WsBuilder(SourceFile::parse(text)) | ||
327 | } | ||
328 | pub(crate) fn ws(&self) -> SyntaxToken<'_> { | ||
329 | self.0.syntax().first_child_or_token().unwrap().as_token().unwrap() | ||
330 | } | ||
331 | } | ||
332 | |||
333 | } | ||