aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_assists/src/ast_editor.rs
diff options
context:
space:
mode:
authorbors[bot] <bors[bot]@users.noreply.github.com>2019-04-22 11:08:18 +0100
committerbors[bot] <bors[bot]@users.noreply.github.com>2019-04-22 11:08:18 +0100
commit38c0a1e3331f4b2b9efc7caa20b5927874024686 (patch)
tree321dd22a931c2ca11d2e207d734f283844af656d /crates/ra_assists/src/ast_editor.rs
parent76e0129a21661029dc6cdbea2412ab53efe33aa1 (diff)
parentb73a978b95810b188090a37e002a22403a9067bd (diff)
Merge #1184
1184: Start structured editing API r=matklad a=matklad I think I finally understand how to provide nice, mutable structured editing API on top of red-green trees. The problem I am trying to solve is that any modification to a particular `SyntaxNode` returns an independent new file. So, if you are editing a struct literal, and add a field, you get back a SourceFile, and you have to find the struct literal inside it yourself! This happens because our trees are immutable, but have parent pointers. The main idea here is to introduce `AstEditor<T>` type, which abstracts away that API. So, you create an `AstEditor` for node you want to edit and call various `&mut` taking methods on it. Internally, `AstEditor` stores both the original node and the current node. All edits are applied to the current node, which is replaced by the corresponding node in the new file. In the end, `AstEditor` computes a text edit between old and new nodes. Note that this also should sole a problem when you create an anchor pointing to a subnode and mutate the parent node, invalidating anchor. Because mutation needs `&mut`, all anchors must be killed before modification. Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'crates/ra_assists/src/ast_editor.rs')
-rw-r--r--crates/ra_assists/src/ast_editor.rs333
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 @@
1use std::{iter, ops::RangeInclusive};
2
3use arrayvec::ArrayVec;
4use ra_text_edit::TextEditBuilder;
5use ra_syntax::{AstNode, TreeArc, ast, SyntaxKind::*, SyntaxElement, SourceFile, InsertPosition, Direction};
6use ra_fmt::leading_indent;
7
8pub struct AstEditor<N: AstNode> {
9 original_ast: TreeArc<N>,
10 ast: TreeArc<N>,
11}
12
13impl<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
80impl 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
160impl 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
194impl 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
233pub struct AstBuilder<N: AstNode> {
234 _phantom: std::marker::PhantomData<N>,
235}
236
237impl 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
250impl 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
260impl 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
274impl 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
280fn 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
286mod 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}