diff options
author | Aleksey Kladov <[email protected]> | 2019-04-21 20:13:52 +0100 |
---|---|---|
committer | Aleksey Kladov <[email protected]> | 2019-04-21 20:13:52 +0100 |
commit | f313ac45af0b9c81cf411a8a4a6873e742bb8562 (patch) | |
tree | 61f3e898b7e86c64b113e690ee55a94882c25911 /crates/ra_assists | |
parent | 7cc845e88d870173e1baa39ce4d3885a5b1f7043 (diff) |
use structured editing API for fill struct assist
Diffstat (limited to 'crates/ra_assists')
-rw-r--r-- | crates/ra_assists/src/assist_ctx.rs | 4 | ||||
-rw-r--r-- | crates/ra_assists/src/ast_editor.rs | 140 | ||||
-rw-r--r-- | crates/ra_assists/src/fill_struct_fields.rs | 113 |
3 files changed, 138 insertions, 119 deletions
diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs index e80e35738..f46de61eb 100644 --- a/crates/ra_assists/src/assist_ctx.rs +++ b/crates/ra_assists/src/assist_ctx.rs | |||
@@ -161,6 +161,10 @@ impl AssistBuilder { | |||
161 | self.target = Some(target) | 161 | self.target = Some(target) |
162 | } | 162 | } |
163 | 163 | ||
164 | pub(crate) fn text_edit_builder(&mut self) -> &mut TextEditBuilder { | ||
165 | &mut self.edit | ||
166 | } | ||
167 | |||
164 | fn build(self) -> AssistAction { | 168 | fn build(self) -> AssistAction { |
165 | AssistAction { | 169 | AssistAction { |
166 | edit: self.edit.finish(), | 170 | edit: self.edit.finish(), |
diff --git a/crates/ra_assists/src/ast_editor.rs b/crates/ra_assists/src/ast_editor.rs index 7347c1738..5bf1fc0ed 100644 --- a/crates/ra_assists/src/ast_editor.rs +++ b/crates/ra_assists/src/ast_editor.rs | |||
@@ -1,6 +1,9 @@ | |||
1 | use std::iter; | ||
2 | |||
1 | use arrayvec::ArrayVec; | 3 | use arrayvec::ArrayVec; |
2 | use ra_text_edit::{TextEdit, TextEditBuilder}; | 4 | use ra_text_edit::TextEditBuilder; |
3 | use ra_syntax::{AstNode, TreeArc, ast, SyntaxKind::*, SyntaxElement, SourceFile, InsertPosition, Direction}; | 5 | use ra_syntax::{AstNode, TreeArc, ast, SyntaxKind::*, SyntaxElement, SourceFile, InsertPosition, Direction}; |
6 | use ra_fmt::leading_indent; | ||
4 | 7 | ||
5 | pub struct AstEditor<N: AstNode> { | 8 | pub struct AstEditor<N: AstNode> { |
6 | original_ast: TreeArc<N>, | 9 | original_ast: TreeArc<N>, |
@@ -12,18 +15,25 @@ impl<N: AstNode> AstEditor<N> { | |||
12 | AstEditor { original_ast: node.to_owned(), ast: node.to_owned() } | 15 | AstEditor { original_ast: node.to_owned(), ast: node.to_owned() } |
13 | } | 16 | } |
14 | 17 | ||
15 | pub fn into_text_edit(self) -> TextEdit { | 18 | pub fn into_text_edit(self, builder: &mut TextEditBuilder) { |
16 | // FIXME: compute a more fine-grained diff here. | 19 | // FIXME: compute a more fine-grained diff here. |
17 | // If *you* know a nice algorithm to compute diff between two syntax | 20 | // If *you* know a nice algorithm to compute diff between two syntax |
18 | // tree, tell me about it! | 21 | // tree, tell me about it! |
19 | let mut builder = TextEditBuilder::default(); | ||
20 | builder.replace(self.original_ast.syntax().range(), self.ast().syntax().text().to_string()); | 22 | builder.replace(self.original_ast.syntax().range(), self.ast().syntax().text().to_string()); |
21 | builder.finish() | ||
22 | } | 23 | } |
23 | 24 | ||
24 | pub fn ast(&self) -> &N { | 25 | pub fn ast(&self) -> &N { |
25 | &*self.ast | 26 | &*self.ast |
26 | } | 27 | } |
28 | |||
29 | fn insert_children<'a>( | ||
30 | &self, | ||
31 | position: InsertPosition<SyntaxElement<'_>>, | ||
32 | to_insert: impl Iterator<Item = SyntaxElement<'a>>, | ||
33 | ) -> TreeArc<N> { | ||
34 | let new_syntax = self.ast().syntax().insert_children(position, to_insert); | ||
35 | N::cast(&new_syntax).unwrap().to_owned() | ||
36 | } | ||
27 | } | 37 | } |
28 | 38 | ||
29 | impl AstEditor<ast::NamedFieldList> { | 39 | impl AstEditor<ast::NamedFieldList> { |
@@ -31,41 +41,61 @@ impl AstEditor<ast::NamedFieldList> { | |||
31 | self.insert_field(InsertPosition::Last, field) | 41 | self.insert_field(InsertPosition::Last, field) |
32 | } | 42 | } |
33 | 43 | ||
44 | pub fn make_multiline(&mut self) { | ||
45 | let l_curly = match self.l_curly() { | ||
46 | Some(it) => it, | ||
47 | None => return, | ||
48 | }; | ||
49 | let sibling = match l_curly.next_sibling_or_token() { | ||
50 | Some(it) => it, | ||
51 | None => return, | ||
52 | }; | ||
53 | if sibling.as_token().map(|it| it.text().contains('\n')) == Some(true) { | ||
54 | return; | ||
55 | } | ||
56 | |||
57 | let ws = tokens::WsBuilder::new(&format!( | ||
58 | "\n{}", | ||
59 | leading_indent(self.ast().syntax()).unwrap_or("") | ||
60 | )); | ||
61 | self.ast = self.insert_children(InsertPosition::After(l_curly), iter::once(ws.ws().into())); | ||
62 | } | ||
63 | |||
34 | pub fn insert_field( | 64 | pub fn insert_field( |
35 | &mut self, | 65 | &mut self, |
36 | position: InsertPosition<&'_ ast::NamedField>, | 66 | position: InsertPosition<&'_ ast::NamedField>, |
37 | field: &ast::NamedField, | 67 | field: &ast::NamedField, |
38 | ) { | 68 | ) { |
39 | let mut to_insert: ArrayVec<[SyntaxElement; 2]> = | 69 | let is_multiline = self.ast().syntax().text().contains('\n'); |
40 | [field.syntax().into(), tokens::comma().into()].into(); | 70 | let ws; |
41 | let position = match position { | 71 | let space = if is_multiline { |
42 | InsertPosition::First => { | 72 | ws = tokens::WsBuilder::new(&format!( |
43 | let anchor = match self | 73 | "\n{} ", |
44 | .ast() | 74 | leading_indent(self.ast().syntax()).unwrap_or("") |
45 | .syntax() | 75 | )); |
46 | .children_with_tokens() | 76 | ws.ws() |
47 | .find(|it| it.kind() == L_CURLY) | 77 | } else { |
48 | { | 78 | tokens::single_space() |
79 | }; | ||
80 | |||
81 | let mut to_insert: ArrayVec<[SyntaxElement; 4]> = ArrayVec::new(); | ||
82 | to_insert.push(space.into()); | ||
83 | to_insert.push(field.syntax().into()); | ||
84 | to_insert.push(tokens::comma().into()); | ||
85 | |||
86 | macro_rules! after_l_curly { | ||
87 | () => {{ | ||
88 | let anchor = match self.l_curly() { | ||
49 | Some(it) => it, | 89 | Some(it) => it, |
50 | None => return, | 90 | None => return, |
51 | }; | 91 | }; |
52 | InsertPosition::After(anchor) | 92 | InsertPosition::After(anchor) |
53 | } | 93 | }}; |
54 | InsertPosition::Last => { | 94 | } |
55 | let anchor = match self | 95 | |
56 | .ast() | 96 | macro_rules! after_field { |
57 | .syntax() | 97 | ($anchor:expr) => { |
58 | .children_with_tokens() | 98 | if let Some(comma) = $anchor |
59 | .find(|it| it.kind() == R_CURLY) | ||
60 | { | ||
61 | Some(it) => it, | ||
62 | None => return, | ||
63 | }; | ||
64 | InsertPosition::Before(anchor) | ||
65 | } | ||
66 | InsertPosition::Before(anchor) => InsertPosition::Before(anchor.syntax().into()), | ||
67 | InsertPosition::After(anchor) => { | ||
68 | if let Some(comma) = anchor | ||
69 | .syntax() | 99 | .syntax() |
70 | .siblings_with_tokens(Direction::Next) | 100 | .siblings_with_tokens(Direction::Next) |
71 | .find(|it| it.kind() == COMMA) | 101 | .find(|it| it.kind() == COMMA) |
@@ -73,21 +103,26 @@ impl AstEditor<ast::NamedFieldList> { | |||
73 | InsertPosition::After(comma) | 103 | InsertPosition::After(comma) |
74 | } else { | 104 | } else { |
75 | to_insert.insert(0, tokens::comma().into()); | 105 | to_insert.insert(0, tokens::comma().into()); |
76 | InsertPosition::After(anchor.syntax().into()) | 106 | InsertPosition::After($anchor.syntax().into()) |
77 | } | 107 | } |
78 | } | 108 | }; |
109 | }; | ||
110 | |||
111 | let position = match position { | ||
112 | InsertPosition::First => after_l_curly!(), | ||
113 | InsertPosition::Last => match self.ast().fields().last() { | ||
114 | Some(it) => after_field!(it), | ||
115 | None => after_l_curly!(), | ||
116 | }, | ||
117 | InsertPosition::Before(anchor) => InsertPosition::Before(anchor.syntax().into()), | ||
118 | InsertPosition::After(anchor) => after_field!(anchor), | ||
79 | }; | 119 | }; |
80 | self.ast = insert_children_into_ast(self.ast(), position, to_insert.iter().cloned()); | 120 | self.ast = self.insert_children(position, to_insert.iter().cloned()); |
81 | } | 121 | } |
82 | } | ||
83 | 122 | ||
84 | fn insert_children_into_ast<'a, N: AstNode>( | 123 | fn l_curly(&self) -> Option<SyntaxElement> { |
85 | node: &N, | 124 | self.ast().syntax().children_with_tokens().find(|it| it.kind() == L_CURLY) |
86 | position: InsertPosition<SyntaxElement<'_>>, | 125 | } |
87 | to_insert: impl Iterator<Item = SyntaxElement<'a>>, | ||
88 | ) -> TreeArc<N> { | ||
89 | let new_syntax = node.syntax().insert_children(position, to_insert); | ||
90 | N::cast(&new_syntax).unwrap().to_owned() | ||
91 | } | 126 | } |
92 | 127 | ||
93 | pub struct AstBuilder<N: AstNode> { | 128 | pub struct AstBuilder<N: AstNode> { |
@@ -111,7 +146,7 @@ mod tokens { | |||
111 | use ra_syntax::{AstNode, SourceFile, TreeArc, SyntaxToken, SyntaxKind::*}; | 146 | use ra_syntax::{AstNode, SourceFile, TreeArc, SyntaxToken, SyntaxKind::*}; |
112 | 147 | ||
113 | lazy_static! { | 148 | lazy_static! { |
114 | static ref SOURCE_FILE: TreeArc<SourceFile> = SourceFile::parse(","); | 149 | static ref SOURCE_FILE: TreeArc<SourceFile> = SourceFile::parse(",\n; ;"); |
115 | } | 150 | } |
116 | 151 | ||
117 | pub(crate) fn comma() -> SyntaxToken<'static> { | 152 | pub(crate) fn comma() -> SyntaxToken<'static> { |
@@ -122,6 +157,27 @@ mod tokens { | |||
122 | .find(|it| it.kind() == COMMA) | 157 | .find(|it| it.kind() == COMMA) |
123 | .unwrap() | 158 | .unwrap() |
124 | } | 159 | } |
160 | |||
161 | pub(crate) fn single_space() -> SyntaxToken<'static> { | ||
162 | SOURCE_FILE | ||
163 | .syntax() | ||
164 | .descendants_with_tokens() | ||
165 | .filter_map(|it| it.as_token()) | ||
166 | .find(|it| it.kind() == WHITESPACE && it.text().as_str() == " ") | ||
167 | .unwrap() | ||
168 | } | ||
169 | |||
170 | pub(crate) struct WsBuilder(TreeArc<SourceFile>); | ||
171 | |||
172 | impl WsBuilder { | ||
173 | pub(crate) fn new(text: &str) -> WsBuilder { | ||
174 | WsBuilder(SourceFile::parse(text)) | ||
175 | } | ||
176 | pub(crate) fn ws(&self) -> SyntaxToken<'_> { | ||
177 | self.0.syntax().first_child_or_token().unwrap().as_token().unwrap() | ||
178 | } | ||
179 | } | ||
180 | |||
125 | } | 181 | } |
126 | 182 | ||
127 | #[cfg(test)] | 183 | #[cfg(test)] |
diff --git a/crates/ra_assists/src/fill_struct_fields.rs b/crates/ra_assists/src/fill_struct_fields.rs index 663b4f669..ca128168a 100644 --- a/crates/ra_assists/src/fill_struct_fields.rs +++ b/crates/ra_assists/src/fill_struct_fields.rs | |||
@@ -1,94 +1,53 @@ | |||
1 | use std::fmt::Write; | ||
2 | |||
3 | use hir::{AdtDef, db::HirDatabase}; | 1 | use hir::{AdtDef, db::HirDatabase}; |
4 | 2 | ||
5 | use ra_syntax::ast::{self, AstNode}; | 3 | use ra_syntax::ast::{self, AstNode}; |
6 | 4 | ||
7 | use crate::{AssistCtx, Assist, AssistId}; | 5 | use crate::{AssistCtx, Assist, AssistId, ast_editor::{AstEditor, AstBuilder}}; |
8 | 6 | ||
9 | pub(crate) fn fill_struct_fields(mut ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | 7 | pub(crate) fn fill_struct_fields(mut ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { |
10 | let struct_lit = ctx.node_at_offset::<ast::StructLit>()?; | 8 | let struct_lit = ctx.node_at_offset::<ast::StructLit>()?; |
11 | let mut fsf = FillStructFields { | 9 | let named_field_list = struct_lit.named_field_list()?; |
12 | ctx: &mut ctx, | ||
13 | named_field_list: struct_lit.named_field_list()?, | ||
14 | struct_fields: vec![], | ||
15 | struct_lit, | ||
16 | }; | ||
17 | fsf.evaluate_struct_def_fields()?; | ||
18 | if fsf.struct_lit_and_def_have_the_same_number_of_fields() { | ||
19 | return None; | ||
20 | } | ||
21 | fsf.remove_already_included_fields()?; | ||
22 | fsf.add_action()?; | ||
23 | ctx.build() | ||
24 | } | ||
25 | |||
26 | struct FillStructFields<'a, 'b: 'a, DB> { | ||
27 | ctx: &'a mut AssistCtx<'b, DB>, | ||
28 | named_field_list: &'a ast::NamedFieldList, | ||
29 | struct_fields: Vec<(String, String)>, | ||
30 | struct_lit: &'a ast::StructLit, | ||
31 | } | ||
32 | 10 | ||
33 | impl<DB> FillStructFields<'_, '_, DB> | 11 | // Collect all fields from struct definition |
34 | where | 12 | let mut fields = { |
35 | DB: HirDatabase, | 13 | let analyzer = |
36 | { | 14 | hir::SourceAnalyzer::new(ctx.db, ctx.frange.file_id, struct_lit.syntax(), None); |
37 | fn add_action(&mut self) -> Option<()> { | 15 | let struct_lit_ty = analyzer.type_of(ctx.db, struct_lit.into())?; |
38 | let named_field_list = self.named_field_list; | ||
39 | let struct_fields_string = self.struct_fields_string()?; | ||
40 | let struct_lit = self.struct_lit; | ||
41 | self.ctx.add_action(AssistId("fill_struct_fields"), "fill struct fields", |edit| { | ||
42 | edit.target(struct_lit.syntax().range()); | ||
43 | edit.set_cursor(struct_lit.syntax().range().start()); | ||
44 | edit.replace_node_and_indent(named_field_list.syntax(), struct_fields_string); | ||
45 | }); | ||
46 | Some(()) | ||
47 | } | ||
48 | |||
49 | fn struct_lit_and_def_have_the_same_number_of_fields(&self) -> bool { | ||
50 | self.named_field_list.fields().count() == self.struct_fields.len() | ||
51 | } | ||
52 | |||
53 | fn evaluate_struct_def_fields(&mut self) -> Option<()> { | ||
54 | let analyzer = hir::SourceAnalyzer::new( | ||
55 | self.ctx.db, | ||
56 | self.ctx.frange.file_id, | ||
57 | self.struct_lit.syntax(), | ||
58 | None, | ||
59 | ); | ||
60 | let struct_lit_ty = analyzer.type_of(self.ctx.db, self.struct_lit.into())?; | ||
61 | let struct_def = match struct_lit_ty.as_adt() { | 16 | let struct_def = match struct_lit_ty.as_adt() { |
62 | Some((AdtDef::Struct(s), _)) => s, | 17 | Some((AdtDef::Struct(s), _)) => s, |
63 | _ => return None, | 18 | _ => return None, |
64 | }; | 19 | }; |
65 | self.struct_fields = struct_def | 20 | struct_def.fields(ctx.db) |
66 | .fields(self.ctx.db) | 21 | }; |
67 | .into_iter() | ||
68 | .map(|f| (f.name(self.ctx.db).to_string(), "()".into())) | ||
69 | .collect(); | ||
70 | Some(()) | ||
71 | } | ||
72 | 22 | ||
73 | fn remove_already_included_fields(&mut self) -> Option<()> { | 23 | // Filter out existing fields |
74 | for ast_field in self.named_field_list.fields() { | 24 | for ast_field in named_field_list.fields() { |
75 | let expr = ast_field.expr()?.syntax().text().to_string(); | 25 | let name_from_ast = ast_field.name_ref()?.text().to_string(); |
76 | let name_from_ast = ast_field.name_ref()?.text().to_string(); | 26 | fields.retain(|field| field.name(ctx.db).to_string() != name_from_ast); |
77 | if let Some(idx) = self.struct_fields.iter().position(|(n, _)| n == &name_from_ast) { | 27 | } |
78 | self.struct_fields[idx] = (name_from_ast, expr); | 28 | if fields.is_empty() { |
79 | } | 29 | return None; |
80 | } | ||
81 | Some(()) | ||
82 | } | 30 | } |
83 | 31 | ||
84 | fn struct_fields_string(&mut self) -> Option<String> { | 32 | let db = ctx.db; |
85 | let mut buf = String::from("{\n"); | 33 | ctx.add_action(AssistId("fill_struct_fields"), "fill struct fields", |edit| { |
86 | for (name, expr) in &self.struct_fields { | 34 | let mut ast_editor = AstEditor::new(named_field_list); |
87 | write!(&mut buf, " {}: {},\n", name, expr).unwrap(); | 35 | if named_field_list.fields().count() == 0 && fields.len() > 2 { |
36 | ast_editor.make_multiline(); | ||
37 | }; | ||
38 | |||
39 | for field in fields { | ||
40 | let field = | ||
41 | AstBuilder::<ast::NamedField>::from_text(&format!("{}: ()", field.name(db))); | ||
42 | ast_editor.append_field(&field); | ||
88 | } | 43 | } |
89 | buf.push_str("}"); | 44 | |
90 | Some(buf) | 45 | edit.target(struct_lit.syntax().range()); |
91 | } | 46 | edit.set_cursor(struct_lit.syntax().range().start()); |
47 | |||
48 | ast_editor.into_text_edit(edit.text_edit_builder()); | ||
49 | }); | ||
50 | ctx.build() | ||
92 | } | 51 | } |
93 | 52 | ||
94 | #[cfg(test)] | 53 | #[cfg(test)] |
@@ -225,11 +184,11 @@ mod tests { | |||
225 | 184 | ||
226 | fn main() { | 185 | fn main() { |
227 | let s = <|>S { | 186 | let s = <|>S { |
187 | c: (1, 2), | ||
188 | e: "foo", | ||
228 | a: (), | 189 | a: (), |
229 | b: (), | 190 | b: (), |
230 | c: (1, 2), | ||
231 | d: (), | 191 | d: (), |
232 | e: "foo", | ||
233 | } | 192 | } |
234 | } | 193 | } |
235 | "#, | 194 | "#, |