diff options
-rw-r--r-- | crates/ra_assists/src/assist_ctx.rs | 6 | ||||
-rw-r--r-- | crates/ra_assists/src/assists/add_missing_impl_members.rs | 36 | ||||
-rw-r--r-- | crates/ra_assists/src/assists/move_bounds.rs | 12 | ||||
-rw-r--r-- | crates/ra_assists/src/ast_editor.rs | 245 | ||||
-rw-r--r-- | crates/ra_assists/src/lib.rs | 1 | ||||
-rw-r--r-- | crates/ra_ide_api/src/diagnostics.rs | 10 | ||||
-rw-r--r-- | crates/ra_syntax/src/algo.rs | 17 | ||||
-rw-r--r-- | crates/ra_syntax/src/ast.rs | 2 | ||||
-rw-r--r-- | crates/ra_syntax/src/ast/edit.rs | 225 | ||||
-rw-r--r-- | crates/ra_syntax/src/ast/extensions.rs | 12 |
10 files changed, 280 insertions, 286 deletions
diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs index c45262efa..5f564be0b 100644 --- a/crates/ra_assists/src/assist_ctx.rs +++ b/crates/ra_assists/src/assist_ctx.rs | |||
@@ -2,7 +2,7 @@ use hir::db::HirDatabase; | |||
2 | use ra_db::FileRange; | 2 | use ra_db::FileRange; |
3 | use ra_fmt::{leading_indent, reindent}; | 3 | use ra_fmt::{leading_indent, reindent}; |
4 | use ra_syntax::{ | 4 | use ra_syntax::{ |
5 | algo::{find_covering_element, find_node_at_offset}, | 5 | algo::{self, find_covering_element, find_node_at_offset}, |
6 | AstNode, SourceFile, SyntaxElement, SyntaxNode, SyntaxToken, TextRange, TextUnit, | 6 | AstNode, SourceFile, SyntaxElement, SyntaxNode, SyntaxToken, TextRange, TextUnit, |
7 | TokenAtOffset, | 7 | TokenAtOffset, |
8 | }; | 8 | }; |
@@ -177,6 +177,10 @@ impl AssistBuilder { | |||
177 | &mut self.edit | 177 | &mut self.edit |
178 | } | 178 | } |
179 | 179 | ||
180 | pub(crate) fn replace_ast<N: AstNode>(&mut self, old: N, new: N) { | ||
181 | algo::diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit) | ||
182 | } | ||
183 | |||
180 | fn build(self) -> AssistAction { | 184 | fn build(self) -> AssistAction { |
181 | AssistAction { | 185 | AssistAction { |
182 | edit: self.edit.finish(), | 186 | edit: self.edit.finish(), |
diff --git a/crates/ra_assists/src/assists/add_missing_impl_members.rs b/crates/ra_assists/src/assists/add_missing_impl_members.rs index 682455bce..6fd1c3753 100644 --- a/crates/ra_assists/src/assists/add_missing_impl_members.rs +++ b/crates/ra_assists/src/assists/add_missing_impl_members.rs | |||
@@ -1,10 +1,10 @@ | |||
1 | use hir::{db::HirDatabase, HasSource}; | 1 | use hir::{db::HirDatabase, HasSource}; |
2 | use ra_syntax::{ | 2 | use ra_syntax::{ |
3 | ast::{self, make, AstNode, NameOwner}, | 3 | ast::{self, edit, make, AstNode, NameOwner}, |
4 | SmolStr, | 4 | SmolStr, |
5 | }; | 5 | }; |
6 | 6 | ||
7 | use crate::{ast_editor::AstEditor, Assist, AssistCtx, AssistId}; | 7 | use crate::{Assist, AssistCtx, AssistId}; |
8 | 8 | ||
9 | #[derive(PartialEq)] | 9 | #[derive(PartialEq)] |
10 | enum AddMissingImplMembersMode { | 10 | enum AddMissingImplMembersMode { |
@@ -75,30 +75,26 @@ fn add_missing_impl_members_inner( | |||
75 | 75 | ||
76 | ctx.add_action(AssistId(assist_id), label, |edit| { | 76 | ctx.add_action(AssistId(assist_id), label, |edit| { |
77 | let n_existing_items = impl_item_list.impl_items().count(); | 77 | let n_existing_items = impl_item_list.impl_items().count(); |
78 | let items = missing_items.into_iter().map(|it| match it { | 78 | let items = missing_items |
79 | ast::ImplItem::FnDef(def) => strip_docstring(add_body(def).into()), | 79 | .into_iter() |
80 | _ => strip_docstring(it), | 80 | .map(|it| match it { |
81 | }); | 81 | ast::ImplItem::FnDef(def) => ast::ImplItem::FnDef(add_body(def)), |
82 | let mut ast_editor = AstEditor::new(impl_item_list); | 82 | _ => it, |
83 | 83 | }) | |
84 | ast_editor.append_items(items); | 84 | .map(|it| edit::strip_attrs_and_docs(&it)); |
85 | 85 | let new_impl_item_list = impl_item_list.append_items(items); | |
86 | let first_new_item = ast_editor.ast().impl_items().nth(n_existing_items).unwrap(); | 86 | let cursor_position = { |
87 | let cursor_position = first_new_item.syntax().text_range().start(); | 87 | let first_new_item = new_impl_item_list.impl_items().nth(n_existing_items).unwrap(); |
88 | ast_editor.into_text_edit(edit.text_edit_builder()); | 88 | first_new_item.syntax().text_range().start() |
89 | 89 | }; | |
90 | |||
91 | edit.replace_ast(impl_item_list, new_impl_item_list); | ||
90 | edit.set_cursor(cursor_position); | 92 | edit.set_cursor(cursor_position); |
91 | }); | 93 | }); |
92 | 94 | ||
93 | ctx.build() | 95 | ctx.build() |
94 | } | 96 | } |
95 | 97 | ||
96 | fn strip_docstring(item: ast::ImplItem) -> ast::ImplItem { | ||
97 | let mut ast_editor = AstEditor::new(item); | ||
98 | ast_editor.strip_attrs_and_docs(); | ||
99 | ast_editor.ast().to_owned() | ||
100 | } | ||
101 | |||
102 | fn add_body(fn_def: ast::FnDef) -> ast::FnDef { | 98 | fn add_body(fn_def: ast::FnDef) -> ast::FnDef { |
103 | if fn_def.body().is_none() { | 99 | if fn_def.body().is_none() { |
104 | fn_def.with_body(make::block_from_expr(make::expr_unimplemented())) | 100 | fn_def.with_body(make::block_from_expr(make::expr_unimplemented())) |
diff --git a/crates/ra_assists/src/assists/move_bounds.rs b/crates/ra_assists/src/assists/move_bounds.rs index fd4bdc55c..39ff51233 100644 --- a/crates/ra_assists/src/assists/move_bounds.rs +++ b/crates/ra_assists/src/assists/move_bounds.rs | |||
@@ -1,11 +1,11 @@ | |||
1 | use hir::db::HirDatabase; | 1 | use hir::db::HirDatabase; |
2 | use ra_syntax::{ | 2 | use ra_syntax::{ |
3 | ast::{self, make, AstNode, NameOwner, TypeBoundsOwner}, | 3 | ast::{self, edit, make, AstNode, NameOwner, TypeBoundsOwner}, |
4 | SyntaxElement, | 4 | SyntaxElement, |
5 | SyntaxKind::*, | 5 | SyntaxKind::*, |
6 | }; | 6 | }; |
7 | 7 | ||
8 | use crate::{ast_editor::AstEditor, Assist, AssistCtx, AssistId}; | 8 | use crate::{Assist, AssistCtx, AssistId}; |
9 | 9 | ||
10 | pub(crate) fn move_bounds_to_where_clause(mut ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | 10 | pub(crate) fn move_bounds_to_where_clause(mut ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { |
11 | let type_param_list = ctx.node_at_offset::<ast::TypeParamList>()?; | 11 | let type_param_list = ctx.node_at_offset::<ast::TypeParamList>()?; |
@@ -39,14 +39,12 @@ pub(crate) fn move_bounds_to_where_clause(mut ctx: AssistCtx<impl HirDatabase>) | |||
39 | .type_params() | 39 | .type_params() |
40 | .filter(|it| it.type_bound_list().is_some()) | 40 | .filter(|it| it.type_bound_list().is_some()) |
41 | .map(|type_param| { | 41 | .map(|type_param| { |
42 | let without_bounds = | 42 | let without_bounds = type_param.remove_bounds(); |
43 | AstEditor::new(type_param.clone()).remove_bounds().ast().clone(); | ||
44 | (type_param, without_bounds) | 43 | (type_param, without_bounds) |
45 | }); | 44 | }); |
46 | 45 | ||
47 | let mut ast_editor = AstEditor::new(type_param_list.clone()); | 46 | let new_type_param_list = edit::replace_descendants(&type_param_list, new_params); |
48 | ast_editor.replace_descendants(new_params); | 47 | edit.replace_ast(type_param_list.clone(), new_type_param_list); |
49 | ast_editor.into_text_edit(edit.text_edit_builder()); | ||
50 | 48 | ||
51 | let where_clause = { | 49 | let where_clause = { |
52 | let predicates = type_param_list.type_params().filter_map(build_predicate); | 50 | let predicates = type_param_list.type_params().filter_map(build_predicate); |
diff --git a/crates/ra_assists/src/ast_editor.rs b/crates/ra_assists/src/ast_editor.rs deleted file mode 100644 index 72c8c478a..000000000 --- a/crates/ra_assists/src/ast_editor.rs +++ /dev/null | |||
@@ -1,245 +0,0 @@ | |||
1 | use std::{iter, ops::RangeInclusive}; | ||
2 | |||
3 | use arrayvec::ArrayVec; | ||
4 | use rustc_hash::FxHashMap; | ||
5 | |||
6 | use ra_fmt::leading_indent; | ||
7 | use ra_syntax::{ | ||
8 | algo, | ||
9 | ast::{self, make::tokens, TypeBoundsOwner}, | ||
10 | AstNode, Direction, InsertPosition, SyntaxElement, | ||
11 | SyntaxKind::*, | ||
12 | T, | ||
13 | }; | ||
14 | use ra_text_edit::TextEditBuilder; | ||
15 | |||
16 | pub struct AstEditor<N: AstNode> { | ||
17 | original_ast: N, | ||
18 | ast: N, | ||
19 | } | ||
20 | |||
21 | impl<N: AstNode> AstEditor<N> { | ||
22 | pub fn new(node: N) -> AstEditor<N> | ||
23 | where | ||
24 | N: Clone, | ||
25 | { | ||
26 | AstEditor { original_ast: node.clone(), ast: node } | ||
27 | } | ||
28 | |||
29 | pub fn into_text_edit(self, builder: &mut TextEditBuilder) { | ||
30 | for (from, to) in algo::diff(&self.original_ast.syntax(), self.ast().syntax()) { | ||
31 | builder.replace(from.text_range(), to.to_string()) | ||
32 | } | ||
33 | } | ||
34 | |||
35 | pub fn ast(&self) -> &N { | ||
36 | &self.ast | ||
37 | } | ||
38 | |||
39 | pub fn replace_descendants<T: AstNode>( | ||
40 | &mut self, | ||
41 | replacement_map: impl Iterator<Item = (T, T)>, | ||
42 | ) -> &mut Self { | ||
43 | let map = replacement_map | ||
44 | .map(|(from, to)| (from.syntax().clone().into(), to.syntax().clone().into())) | ||
45 | .collect::<FxHashMap<_, _>>(); | ||
46 | let new_syntax = algo::replace_descendants(self.ast.syntax(), &map); | ||
47 | self.ast = N::cast(new_syntax).unwrap(); | ||
48 | self | ||
49 | } | ||
50 | |||
51 | #[must_use] | ||
52 | fn insert_children( | ||
53 | &self, | ||
54 | position: InsertPosition<SyntaxElement>, | ||
55 | mut to_insert: impl Iterator<Item = SyntaxElement>, | ||
56 | ) -> N { | ||
57 | let new_syntax = algo::insert_children(self.ast().syntax(), position, &mut to_insert); | ||
58 | N::cast(new_syntax).unwrap() | ||
59 | } | ||
60 | |||
61 | #[must_use] | ||
62 | fn replace_children( | ||
63 | &self, | ||
64 | to_delete: RangeInclusive<SyntaxElement>, | ||
65 | mut to_insert: impl Iterator<Item = SyntaxElement>, | ||
66 | ) -> N { | ||
67 | let new_syntax = algo::replace_children(self.ast().syntax(), to_delete, &mut to_insert); | ||
68 | N::cast(new_syntax).unwrap() | ||
69 | } | ||
70 | |||
71 | fn do_make_multiline(&mut self) { | ||
72 | let l_curly = | ||
73 | match self.ast().syntax().children_with_tokens().find(|it| it.kind() == T!['{']) { | ||
74 | Some(it) => it, | ||
75 | None => return, | ||
76 | }; | ||
77 | let sibling = match l_curly.next_sibling_or_token() { | ||
78 | Some(it) => it, | ||
79 | None => return, | ||
80 | }; | ||
81 | let existing_ws = match sibling.as_token() { | ||
82 | None => None, | ||
83 | Some(tok) if tok.kind() != WHITESPACE => None, | ||
84 | Some(ws) => { | ||
85 | if ws.text().contains('\n') { | ||
86 | return; | ||
87 | } | ||
88 | Some(ws.clone()) | ||
89 | } | ||
90 | }; | ||
91 | |||
92 | let indent = leading_indent(self.ast().syntax()).unwrap_or("".into()); | ||
93 | let ws = tokens::WsBuilder::new(&format!("\n{}", indent)); | ||
94 | let to_insert = iter::once(ws.ws().into()); | ||
95 | self.ast = match existing_ws { | ||
96 | None => self.insert_children(InsertPosition::After(l_curly), to_insert), | ||
97 | Some(ws) => { | ||
98 | self.replace_children(RangeInclusive::new(ws.clone().into(), ws.into()), to_insert) | ||
99 | } | ||
100 | }; | ||
101 | } | ||
102 | } | ||
103 | |||
104 | impl AstEditor<ast::RecordFieldList> { | ||
105 | pub fn append_field(&mut self, field: &ast::RecordField) { | ||
106 | self.insert_field(InsertPosition::Last, field) | ||
107 | } | ||
108 | |||
109 | pub fn insert_field( | ||
110 | &mut self, | ||
111 | position: InsertPosition<&'_ ast::RecordField>, | ||
112 | field: &ast::RecordField, | ||
113 | ) { | ||
114 | let is_multiline = self.ast().syntax().text().contains_char('\n'); | ||
115 | let ws; | ||
116 | let space = if is_multiline { | ||
117 | ws = tokens::WsBuilder::new(&format!( | ||
118 | "\n{} ", | ||
119 | leading_indent(self.ast().syntax()).unwrap_or("".into()) | ||
120 | )); | ||
121 | ws.ws() | ||
122 | } else { | ||
123 | tokens::single_space() | ||
124 | }; | ||
125 | |||
126 | let mut to_insert: ArrayVec<[SyntaxElement; 4]> = ArrayVec::new(); | ||
127 | to_insert.push(space.into()); | ||
128 | to_insert.push(field.syntax().clone().into()); | ||
129 | to_insert.push(tokens::comma().into()); | ||
130 | |||
131 | macro_rules! after_l_curly { | ||
132 | () => {{ | ||
133 | let anchor = match self.l_curly() { | ||
134 | Some(it) => it, | ||
135 | None => return, | ||
136 | }; | ||
137 | InsertPosition::After(anchor) | ||
138 | }}; | ||
139 | } | ||
140 | |||
141 | macro_rules! after_field { | ||
142 | ($anchor:expr) => { | ||
143 | if let Some(comma) = $anchor | ||
144 | .syntax() | ||
145 | .siblings_with_tokens(Direction::Next) | ||
146 | .find(|it| it.kind() == T![,]) | ||
147 | { | ||
148 | InsertPosition::After(comma) | ||
149 | } else { | ||
150 | to_insert.insert(0, tokens::comma().into()); | ||
151 | InsertPosition::After($anchor.syntax().clone().into()) | ||
152 | } | ||
153 | }; | ||
154 | }; | ||
155 | |||
156 | let position = match position { | ||
157 | InsertPosition::First => after_l_curly!(), | ||
158 | InsertPosition::Last => { | ||
159 | if !is_multiline { | ||
160 | // don't insert comma before curly | ||
161 | to_insert.pop(); | ||
162 | } | ||
163 | match self.ast().fields().last() { | ||
164 | Some(it) => after_field!(it), | ||
165 | None => after_l_curly!(), | ||
166 | } | ||
167 | } | ||
168 | InsertPosition::Before(anchor) => { | ||
169 | InsertPosition::Before(anchor.syntax().clone().into()) | ||
170 | } | ||
171 | InsertPosition::After(anchor) => after_field!(anchor), | ||
172 | }; | ||
173 | |||
174 | self.ast = self.insert_children(position, to_insert.iter().cloned()); | ||
175 | } | ||
176 | |||
177 | fn l_curly(&self) -> Option<SyntaxElement> { | ||
178 | self.ast().syntax().children_with_tokens().find(|it| it.kind() == T!['{']) | ||
179 | } | ||
180 | } | ||
181 | |||
182 | impl AstEditor<ast::ItemList> { | ||
183 | pub fn append_items(&mut self, items: impl Iterator<Item = ast::ImplItem>) { | ||
184 | if !self.ast().syntax().text().contains_char('\n') { | ||
185 | self.do_make_multiline(); | ||
186 | } | ||
187 | items.for_each(|it| self.append_item(it)); | ||
188 | } | ||
189 | |||
190 | pub fn append_item(&mut self, item: ast::ImplItem) { | ||
191 | let (indent, position) = match self.ast().impl_items().last() { | ||
192 | Some(it) => ( | ||
193 | leading_indent(it.syntax()).unwrap_or_default().to_string(), | ||
194 | InsertPosition::After(it.syntax().clone().into()), | ||
195 | ), | ||
196 | None => match self.l_curly() { | ||
197 | Some(it) => ( | ||
198 | " ".to_string() + &leading_indent(self.ast().syntax()).unwrap_or_default(), | ||
199 | InsertPosition::After(it), | ||
200 | ), | ||
201 | None => return, | ||
202 | }, | ||
203 | }; | ||
204 | let ws = tokens::WsBuilder::new(&format!("\n{}", indent)); | ||
205 | let to_insert: ArrayVec<[SyntaxElement; 2]> = | ||
206 | [ws.ws().into(), item.syntax().clone().into()].into(); | ||
207 | self.ast = self.insert_children(position, to_insert.into_iter()); | ||
208 | } | ||
209 | |||
210 | fn l_curly(&self) -> Option<SyntaxElement> { | ||
211 | self.ast().syntax().children_with_tokens().find(|it| it.kind() == T!['{']) | ||
212 | } | ||
213 | } | ||
214 | |||
215 | impl AstEditor<ast::ImplItem> { | ||
216 | pub fn strip_attrs_and_docs(&mut self) { | ||
217 | while let Some(start) = self | ||
218 | .ast() | ||
219 | .syntax() | ||
220 | .children_with_tokens() | ||
221 | .find(|it| it.kind() == ATTR || it.kind() == COMMENT) | ||
222 | { | ||
223 | let end = match &start.next_sibling_or_token() { | ||
224 | Some(el) if el.kind() == WHITESPACE => el.clone(), | ||
225 | Some(_) | None => start.clone(), | ||
226 | }; | ||
227 | self.ast = self.replace_children(RangeInclusive::new(start, end), iter::empty()); | ||
228 | } | ||
229 | } | ||
230 | } | ||
231 | |||
232 | impl AstEditor<ast::TypeParam> { | ||
233 | pub fn remove_bounds(&mut self) -> &mut Self { | ||
234 | let colon = match self.ast.colon_token() { | ||
235 | Some(it) => it, | ||
236 | None => return self, | ||
237 | }; | ||
238 | let end = match self.ast.type_bound_list() { | ||
239 | Some(it) => it.syntax().clone().into(), | ||
240 | None => colon.clone().into(), | ||
241 | }; | ||
242 | self.ast = self.replace_children(RangeInclusive::new(colon.into(), end), iter::empty()); | ||
243 | self | ||
244 | } | ||
245 | } | ||
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 3ca3320f7..91b2a1dce 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs | |||
@@ -7,7 +7,6 @@ | |||
7 | 7 | ||
8 | mod assist_ctx; | 8 | mod assist_ctx; |
9 | mod marks; | 9 | mod marks; |
10 | pub mod ast_editor; | ||
11 | 10 | ||
12 | use hir::db::HirDatabase; | 11 | use hir::db::HirDatabase; |
13 | use itertools::Itertools; | 12 | use itertools::Itertools; |
diff --git a/crates/ra_ide_api/src/diagnostics.rs b/crates/ra_ide_api/src/diagnostics.rs index 144bc0a70..4fa07e3dc 100644 --- a/crates/ra_ide_api/src/diagnostics.rs +++ b/crates/ra_ide_api/src/diagnostics.rs | |||
@@ -2,10 +2,10 @@ use std::cell::RefCell; | |||
2 | 2 | ||
3 | use hir::diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSink}; | 3 | use hir::diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSink}; |
4 | use itertools::Itertools; | 4 | use itertools::Itertools; |
5 | use ra_assists::ast_editor::AstEditor; | ||
6 | use ra_db::SourceDatabase; | 5 | use ra_db::SourceDatabase; |
7 | use ra_prof::profile; | 6 | use ra_prof::profile; |
8 | use ra_syntax::{ | 7 | use ra_syntax::{ |
8 | algo, | ||
9 | ast::{self, make, AstNode}, | 9 | ast::{self, make, AstNode}, |
10 | Location, SyntaxNode, TextRange, T, | 10 | Location, SyntaxNode, TextRange, T, |
11 | }; | 11 | }; |
@@ -56,15 +56,15 @@ pub(crate) fn diagnostics(db: &RootDatabase, file_id: FileId) -> Vec<Diagnostic> | |||
56 | }) | 56 | }) |
57 | }) | 57 | }) |
58 | .on::<hir::diagnostics::MissingFields, _>(|d| { | 58 | .on::<hir::diagnostics::MissingFields, _>(|d| { |
59 | let node = d.ast(db); | 59 | let mut field_list = d.ast(db); |
60 | let mut ast_editor = AstEditor::new(node); | ||
61 | for f in d.missed_fields.iter() { | 60 | for f in d.missed_fields.iter() { |
62 | let field = make::record_field(make::name_ref(&f.to_string()), Some(make::expr_unit())); | 61 | let field = make::record_field(make::name_ref(&f.to_string()), Some(make::expr_unit())); |
63 | ast_editor.append_field(&field); | 62 | field_list = field_list.append_field(&field); |
64 | } | 63 | } |
65 | 64 | ||
66 | let mut builder = TextEditBuilder::default(); | 65 | let mut builder = TextEditBuilder::default(); |
67 | ast_editor.into_text_edit(&mut builder); | 66 | algo::diff(&d.ast(db).syntax(), &field_list.syntax()).into_text_edit(&mut builder); |
67 | |||
68 | let fix = | 68 | let fix = |
69 | SourceChange::source_file_edit_from("fill struct fields", file_id, builder.finish()); | 69 | SourceChange::source_file_edit_from("fill struct fields", file_id, builder.finish()); |
70 | res.borrow_mut().push(Diagnostic { | 70 | res.borrow_mut().push(Diagnostic { |
diff --git a/crates/ra_syntax/src/algo.rs b/crates/ra_syntax/src/algo.rs index 46680a08f..f33d2ad4e 100644 --- a/crates/ra_syntax/src/algo.rs +++ b/crates/ra_syntax/src/algo.rs | |||
@@ -3,6 +3,7 @@ pub mod visit; | |||
3 | use std::ops::RangeInclusive; | 3 | use std::ops::RangeInclusive; |
4 | 4 | ||
5 | use itertools::Itertools; | 5 | use itertools::Itertools; |
6 | use ra_text_edit::TextEditBuilder; | ||
6 | use rustc_hash::FxHashMap; | 7 | use rustc_hash::FxHashMap; |
7 | 8 | ||
8 | use crate::{ | 9 | use crate::{ |
@@ -63,6 +64,18 @@ pub enum InsertPosition<T> { | |||
63 | After(T), | 64 | After(T), |
64 | } | 65 | } |
65 | 66 | ||
67 | pub struct TreeDiff { | ||
68 | replacements: FxHashMap<SyntaxElement, SyntaxElement>, | ||
69 | } | ||
70 | |||
71 | impl TreeDiff { | ||
72 | pub fn into_text_edit(&self, builder: &mut TextEditBuilder) { | ||
73 | for (from, to) in self.replacements.iter() { | ||
74 | builder.replace(from.text_range(), to.to_string()) | ||
75 | } | ||
76 | } | ||
77 | } | ||
78 | |||
66 | /// Finds minimal the diff, which, applied to `from`, will result in `to`. | 79 | /// Finds minimal the diff, which, applied to `from`, will result in `to`. |
67 | /// | 80 | /// |
68 | /// Specifically, returns a map whose keys are descendants of `from` and values | 81 | /// Specifically, returns a map whose keys are descendants of `from` and values |
@@ -70,12 +83,12 @@ pub enum InsertPosition<T> { | |||
70 | /// | 83 | /// |
71 | /// A trivial solution is a singletom map `{ from: to }`, but this function | 84 | /// A trivial solution is a singletom map `{ from: to }`, but this function |
72 | /// tries to find a more fine-grained diff. | 85 | /// tries to find a more fine-grained diff. |
73 | pub fn diff(from: &SyntaxNode, to: &SyntaxNode) -> FxHashMap<SyntaxElement, SyntaxElement> { | 86 | pub fn diff(from: &SyntaxNode, to: &SyntaxNode) -> TreeDiff { |
74 | let mut buf = FxHashMap::default(); | 87 | let mut buf = FxHashMap::default(); |
75 | // FIXME: this is both horrible inefficient and gives larger than | 88 | // FIXME: this is both horrible inefficient and gives larger than |
76 | // necessary diff. I bet there's a cool algorithm to diff trees properly. | 89 | // necessary diff. I bet there's a cool algorithm to diff trees properly. |
77 | go(&mut buf, from.clone().into(), to.clone().into()); | 90 | go(&mut buf, from.clone().into(), to.clone().into()); |
78 | return buf; | 91 | return TreeDiff { replacements: buf }; |
79 | 92 | ||
80 | fn go( | 93 | fn go( |
81 | buf: &mut FxHashMap<SyntaxElement, SyntaxElement>, | 94 | buf: &mut FxHashMap<SyntaxElement, SyntaxElement>, |
diff --git a/crates/ra_syntax/src/ast.rs b/crates/ra_syntax/src/ast.rs index fdffd8cb1..1b2ce921a 100644 --- a/crates/ra_syntax/src/ast.rs +++ b/crates/ra_syntax/src/ast.rs | |||
@@ -5,7 +5,7 @@ mod traits; | |||
5 | mod tokens; | 5 | mod tokens; |
6 | mod extensions; | 6 | mod extensions; |
7 | mod expr_extensions; | 7 | mod expr_extensions; |
8 | mod edit; | 8 | pub mod edit; |
9 | pub mod make; | 9 | pub mod make; |
10 | 10 | ||
11 | use std::marker::PhantomData; | 11 | use std::marker::PhantomData; |
diff --git a/crates/ra_syntax/src/ast/edit.rs b/crates/ra_syntax/src/ast/edit.rs index c65899812..03f3b5fbb 100644 --- a/crates/ra_syntax/src/ast/edit.rs +++ b/crates/ra_syntax/src/ast/edit.rs | |||
@@ -1,14 +1,21 @@ | |||
1 | //! This module contains functions for editing syntax trees. As the trees are | 1 | //! This module contains functions for editing syntax trees. As the trees are |
2 | //! immutable, all function here return a fresh copy of the tree, instead of | 2 | //! immutable, all function here return a fresh copy of the tree, instead of |
3 | //! doing an in-place modification. | 3 | //! doing an in-place modification. |
4 | use std::{iter, ops::RangeInclusive}; | ||
4 | 5 | ||
5 | use arrayvec::ArrayVec; | 6 | use arrayvec::ArrayVec; |
6 | use std::ops::RangeInclusive; | 7 | use rustc_hash::FxHashMap; |
7 | 8 | ||
8 | use crate::{ | 9 | use crate::{ |
9 | algo, | 10 | algo, |
10 | ast::{self, make, AstNode}, | 11 | ast::{ |
11 | InsertPosition, SyntaxElement, | 12 | self, |
13 | make::{self, tokens}, | ||
14 | AstNode, TypeBoundsOwner, | ||
15 | }, | ||
16 | AstToken, Direction, InsertPosition, SmolStr, SyntaxElement, | ||
17 | SyntaxKind::{ATTR, COMMENT, WHITESPACE}, | ||
18 | SyntaxNode, T, | ||
12 | }; | 19 | }; |
13 | 20 | ||
14 | impl ast::FnDef { | 21 | impl ast::FnDef { |
@@ -31,6 +38,218 @@ impl ast::FnDef { | |||
31 | } | 38 | } |
32 | } | 39 | } |
33 | 40 | ||
41 | impl ast::ItemList { | ||
42 | #[must_use] | ||
43 | pub fn append_items(&self, items: impl Iterator<Item = ast::ImplItem>) -> ast::ItemList { | ||
44 | let mut res = self.clone(); | ||
45 | if !self.syntax().text().contains_char('\n') { | ||
46 | res = res.make_multiline(); | ||
47 | } | ||
48 | items.for_each(|it| res = res.append_item(it)); | ||
49 | res | ||
50 | } | ||
51 | |||
52 | #[must_use] | ||
53 | pub fn append_item(&self, item: ast::ImplItem) -> ast::ItemList { | ||
54 | let (indent, position) = match self.impl_items().last() { | ||
55 | Some(it) => ( | ||
56 | leading_indent(it.syntax()).unwrap_or_default().to_string(), | ||
57 | InsertPosition::After(it.syntax().clone().into()), | ||
58 | ), | ||
59 | None => match self.l_curly() { | ||
60 | Some(it) => ( | ||
61 | " ".to_string() + &leading_indent(self.syntax()).unwrap_or_default(), | ||
62 | InsertPosition::After(it), | ||
63 | ), | ||
64 | None => return self.clone(), | ||
65 | }, | ||
66 | }; | ||
67 | let ws = tokens::WsBuilder::new(&format!("\n{}", indent)); | ||
68 | let to_insert: ArrayVec<[SyntaxElement; 2]> = | ||
69 | [ws.ws().into(), item.syntax().clone().into()].into(); | ||
70 | insert_children(self, position, to_insert.into_iter()) | ||
71 | } | ||
72 | |||
73 | fn l_curly(&self) -> Option<SyntaxElement> { | ||
74 | self.syntax().children_with_tokens().find(|it| it.kind() == T!['{']) | ||
75 | } | ||
76 | |||
77 | fn make_multiline(&self) -> ast::ItemList { | ||
78 | let l_curly = match self.syntax().children_with_tokens().find(|it| it.kind() == T!['{']) { | ||
79 | Some(it) => it, | ||
80 | None => return self.clone(), | ||
81 | }; | ||
82 | let sibling = match l_curly.next_sibling_or_token() { | ||
83 | Some(it) => it, | ||
84 | None => return self.clone(), | ||
85 | }; | ||
86 | let existing_ws = match sibling.as_token() { | ||
87 | None => None, | ||
88 | Some(tok) if tok.kind() != WHITESPACE => None, | ||
89 | Some(ws) => { | ||
90 | if ws.text().contains('\n') { | ||
91 | return self.clone(); | ||
92 | } | ||
93 | Some(ws.clone()) | ||
94 | } | ||
95 | }; | ||
96 | |||
97 | let indent = leading_indent(self.syntax()).unwrap_or("".into()); | ||
98 | let ws = tokens::WsBuilder::new(&format!("\n{}", indent)); | ||
99 | let to_insert = iter::once(ws.ws().into()); | ||
100 | match existing_ws { | ||
101 | None => insert_children(self, InsertPosition::After(l_curly), to_insert), | ||
102 | Some(ws) => { | ||
103 | replace_children(self, RangeInclusive::new(ws.clone().into(), ws.into()), to_insert) | ||
104 | } | ||
105 | } | ||
106 | } | ||
107 | } | ||
108 | |||
109 | impl ast::RecordFieldList { | ||
110 | #[must_use] | ||
111 | pub fn append_field(&self, field: &ast::RecordField) -> ast::RecordFieldList { | ||
112 | self.insert_field(InsertPosition::Last, field) | ||
113 | } | ||
114 | |||
115 | #[must_use] | ||
116 | pub fn insert_field( | ||
117 | &self, | ||
118 | position: InsertPosition<&'_ ast::RecordField>, | ||
119 | field: &ast::RecordField, | ||
120 | ) -> ast::RecordFieldList { | ||
121 | let is_multiline = self.syntax().text().contains_char('\n'); | ||
122 | let ws; | ||
123 | let space = if is_multiline { | ||
124 | ws = tokens::WsBuilder::new(&format!( | ||
125 | "\n{} ", | ||
126 | leading_indent(self.syntax()).unwrap_or("".into()) | ||
127 | )); | ||
128 | ws.ws() | ||
129 | } else { | ||
130 | tokens::single_space() | ||
131 | }; | ||
132 | |||
133 | let mut to_insert: ArrayVec<[SyntaxElement; 4]> = ArrayVec::new(); | ||
134 | to_insert.push(space.into()); | ||
135 | to_insert.push(field.syntax().clone().into()); | ||
136 | to_insert.push(tokens::comma().into()); | ||
137 | |||
138 | macro_rules! after_l_curly { | ||
139 | () => {{ | ||
140 | let anchor = match self.l_curly() { | ||
141 | Some(it) => it, | ||
142 | None => return self.clone(), | ||
143 | }; | ||
144 | InsertPosition::After(anchor) | ||
145 | }}; | ||
146 | } | ||
147 | |||
148 | macro_rules! after_field { | ||
149 | ($anchor:expr) => { | ||
150 | if let Some(comma) = $anchor | ||
151 | .syntax() | ||
152 | .siblings_with_tokens(Direction::Next) | ||
153 | .find(|it| it.kind() == T![,]) | ||
154 | { | ||
155 | InsertPosition::After(comma) | ||
156 | } else { | ||
157 | to_insert.insert(0, tokens::comma().into()); | ||
158 | InsertPosition::After($anchor.syntax().clone().into()) | ||
159 | } | ||
160 | }; | ||
161 | }; | ||
162 | |||
163 | let position = match position { | ||
164 | InsertPosition::First => after_l_curly!(), | ||
165 | InsertPosition::Last => { | ||
166 | if !is_multiline { | ||
167 | // don't insert comma before curly | ||
168 | to_insert.pop(); | ||
169 | } | ||
170 | match self.fields().last() { | ||
171 | Some(it) => after_field!(it), | ||
172 | None => after_l_curly!(), | ||
173 | } | ||
174 | } | ||
175 | InsertPosition::Before(anchor) => { | ||
176 | InsertPosition::Before(anchor.syntax().clone().into()) | ||
177 | } | ||
178 | InsertPosition::After(anchor) => after_field!(anchor), | ||
179 | }; | ||
180 | |||
181 | insert_children(self, position, to_insert.iter().cloned()) | ||
182 | } | ||
183 | |||
184 | fn l_curly(&self) -> Option<SyntaxElement> { | ||
185 | self.syntax().children_with_tokens().find(|it| it.kind() == T!['{']) | ||
186 | } | ||
187 | } | ||
188 | |||
189 | impl ast::TypeParam { | ||
190 | #[must_use] | ||
191 | pub fn remove_bounds(&self) -> ast::TypeParam { | ||
192 | let colon = match self.colon_token() { | ||
193 | Some(it) => it, | ||
194 | None => return self.clone(), | ||
195 | }; | ||
196 | let end = match self.type_bound_list() { | ||
197 | Some(it) => it.syntax().clone().into(), | ||
198 | None => colon.clone().into(), | ||
199 | }; | ||
200 | replace_children(self, RangeInclusive::new(colon.into(), end), iter::empty()) | ||
201 | } | ||
202 | } | ||
203 | |||
204 | #[must_use] | ||
205 | pub fn strip_attrs_and_docs<N: ast::AttrsOwner>(node: &N) -> N { | ||
206 | N::cast(strip_attrs_and_docs_inner(node.syntax().clone())).unwrap() | ||
207 | } | ||
208 | |||
209 | fn strip_attrs_and_docs_inner(mut node: SyntaxNode) -> SyntaxNode { | ||
210 | while let Some(start) = | ||
211 | node.children_with_tokens().find(|it| it.kind() == ATTR || it.kind() == COMMENT) | ||
212 | { | ||
213 | let end = match &start.next_sibling_or_token() { | ||
214 | Some(el) if el.kind() == WHITESPACE => el.clone(), | ||
215 | Some(_) | None => start.clone(), | ||
216 | }; | ||
217 | node = algo::replace_children(&node, RangeInclusive::new(start, end), &mut iter::empty()); | ||
218 | } | ||
219 | node | ||
220 | } | ||
221 | |||
222 | #[must_use] | ||
223 | pub fn replace_descendants<N: AstNode, D: AstNode>( | ||
224 | parent: &N, | ||
225 | replacement_map: impl Iterator<Item = (D, D)>, | ||
226 | ) -> N { | ||
227 | let map = replacement_map | ||
228 | .map(|(from, to)| (from.syntax().clone().into(), to.syntax().clone().into())) | ||
229 | .collect::<FxHashMap<_, _>>(); | ||
230 | let new_syntax = algo::replace_descendants(parent.syntax(), &map); | ||
231 | N::cast(new_syntax).unwrap() | ||
232 | } | ||
233 | |||
234 | // Note this is copy-pasted from fmt. It seems like fmt should be a separate | ||
235 | // crate, but basic tree building should be this crate. However, tree building | ||
236 | // might want to call into fmt... | ||
237 | fn leading_indent(node: &SyntaxNode) -> Option<SmolStr> { | ||
238 | let prev_tokens = std::iter::successors(node.first_token(), |token| token.prev_token()); | ||
239 | for token in prev_tokens { | ||
240 | if let Some(ws) = ast::Whitespace::cast(token.clone()) { | ||
241 | let ws_text = ws.text(); | ||
242 | if let Some(pos) = ws_text.rfind('\n') { | ||
243 | return Some(ws_text[pos + 1..].into()); | ||
244 | } | ||
245 | } | ||
246 | if token.text().contains('\n') { | ||
247 | break; | ||
248 | } | ||
249 | } | ||
250 | None | ||
251 | } | ||
252 | |||
34 | #[must_use] | 253 | #[must_use] |
35 | fn insert_children<N: AstNode>( | 254 | fn insert_children<N: AstNode>( |
36 | parent: &N, | 255 | parent: &N, |
diff --git a/crates/ra_syntax/src/ast/extensions.rs b/crates/ra_syntax/src/ast/extensions.rs index 0433edb84..8c5ece65d 100644 --- a/crates/ra_syntax/src/ast/extensions.rs +++ b/crates/ra_syntax/src/ast/extensions.rs | |||
@@ -4,7 +4,7 @@ | |||
4 | use itertools::Itertools; | 4 | use itertools::Itertools; |
5 | 5 | ||
6 | use crate::{ | 6 | use crate::{ |
7 | ast::{self, child_opt, children, AstNode, SyntaxNode}, | 7 | ast::{self, child_opt, children, AstChildren, AstNode, SyntaxNode}, |
8 | SmolStr, SyntaxElement, | 8 | SmolStr, SyntaxElement, |
9 | SyntaxKind::*, | 9 | SyntaxKind::*, |
10 | SyntaxToken, T, | 10 | SyntaxToken, T, |
@@ -203,6 +203,16 @@ impl ast::ImplBlock { | |||
203 | } | 203 | } |
204 | } | 204 | } |
205 | 205 | ||
206 | impl ast::AttrsOwner for ast::ImplItem { | ||
207 | fn attrs(&self) -> AstChildren<ast::Attr> { | ||
208 | match self { | ||
209 | ast::ImplItem::FnDef(it) => it.attrs(), | ||
210 | ast::ImplItem::TypeAliasDef(it) => it.attrs(), | ||
211 | ast::ImplItem::ConstDef(it) => it.attrs(), | ||
212 | } | ||
213 | } | ||
214 | } | ||
215 | |||
206 | #[derive(Debug, Clone, PartialEq, Eq)] | 216 | #[derive(Debug, Clone, PartialEq, Eq)] |
207 | pub enum StructKind { | 217 | pub enum StructKind { |
208 | Tuple(ast::TupleFieldDefList), | 218 | Tuple(ast::TupleFieldDefList), |