diff options
Diffstat (limited to 'crates/ra_ide')
-rw-r--r-- | crates/ra_ide/src/diagnostics.rs | 184 | ||||
-rw-r--r-- | crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs | 171 | ||||
-rw-r--r-- | crates/ra_ide/src/lib.rs | 10 |
3 files changed, 211 insertions, 154 deletions
diff --git a/crates/ra_ide/src/diagnostics.rs b/crates/ra_ide/src/diagnostics.rs index 73c0b8275..1046d7ab3 100644 --- a/crates/ra_ide/src/diagnostics.rs +++ b/crates/ra_ide/src/diagnostics.rs | |||
@@ -6,22 +6,21 @@ | |||
6 | 6 | ||
7 | use std::cell::RefCell; | 7 | use std::cell::RefCell; |
8 | 8 | ||
9 | use hir::{ | 9 | use hir::{diagnostics::DiagnosticSinkBuilder, Semantics}; |
10 | diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSinkBuilder}, | ||
11 | HasSource, HirDisplay, Semantics, VariantDef, | ||
12 | }; | ||
13 | use itertools::Itertools; | 10 | use itertools::Itertools; |
14 | use ra_db::SourceDatabase; | 11 | use ra_db::SourceDatabase; |
15 | use ra_ide_db::RootDatabase; | 12 | use ra_ide_db::RootDatabase; |
16 | use ra_prof::profile; | 13 | use ra_prof::profile; |
17 | use ra_syntax::{ | 14 | use ra_syntax::{ |
18 | algo, | 15 | ast::{self, AstNode}, |
19 | ast::{self, edit::IndentLevel, make, AstNode}, | ||
20 | SyntaxNode, TextRange, T, | 16 | SyntaxNode, TextRange, T, |
21 | }; | 17 | }; |
22 | use ra_text_edit::{TextEdit, TextEditBuilder}; | 18 | use ra_text_edit::{TextEdit, TextEditBuilder}; |
23 | 19 | ||
24 | use crate::{Diagnostic, FileId, FileSystemEdit, Fix, SourceFileEdit}; | 20 | use crate::{Diagnostic, FileId, Fix, SourceFileEdit}; |
21 | |||
22 | mod diagnostics_with_fix; | ||
23 | use diagnostics_with_fix::DiagnosticWithFix; | ||
25 | 24 | ||
26 | #[derive(Debug, Copy, Clone)] | 25 | #[derive(Debug, Copy, Clone)] |
27 | pub enum Severity { | 26 | pub enum Severity { |
@@ -54,73 +53,16 @@ pub(crate) fn diagnostics( | |||
54 | let res = RefCell::new(res); | 53 | let res = RefCell::new(res); |
55 | let mut sink = DiagnosticSinkBuilder::new() | 54 | let mut sink = DiagnosticSinkBuilder::new() |
56 | .on::<hir::diagnostics::UnresolvedModule, _>(|d| { | 55 | .on::<hir::diagnostics::UnresolvedModule, _>(|d| { |
57 | let original_file = d.source().file_id.original_file(db); | 56 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); |
58 | let fix = Fix::new( | ||
59 | "Create module", | ||
60 | FileSystemEdit::CreateFile { anchor: original_file, dst: d.candidate.clone() } | ||
61 | .into(), | ||
62 | ); | ||
63 | res.borrow_mut().push(Diagnostic { | ||
64 | range: sema.diagnostics_range(d).range, | ||
65 | message: d.message(), | ||
66 | severity: Severity::Error, | ||
67 | fix: Some(fix), | ||
68 | }) | ||
69 | }) | 57 | }) |
70 | .on::<hir::diagnostics::MissingFields, _>(|d| { | 58 | .on::<hir::diagnostics::MissingFields, _>(|d| { |
71 | // Note that although we could add a diagnostics to | 59 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); |
72 | // fill the missing tuple field, e.g : | ||
73 | // `struct A(usize);` | ||
74 | // `let a = A { 0: () }` | ||
75 | // but it is uncommon usage and it should not be encouraged. | ||
76 | let fix = if d.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) { | ||
77 | None | ||
78 | } else { | ||
79 | let mut field_list = d.ast(db); | ||
80 | for f in d.missed_fields.iter() { | ||
81 | let field = make::record_expr_field( | ||
82 | make::name_ref(&f.to_string()), | ||
83 | Some(make::expr_unit()), | ||
84 | ); | ||
85 | field_list = field_list.append_field(&field); | ||
86 | } | ||
87 | |||
88 | let edit = { | ||
89 | let mut builder = TextEditBuilder::default(); | ||
90 | algo::diff(&d.ast(db).syntax(), &field_list.syntax()) | ||
91 | .into_text_edit(&mut builder); | ||
92 | builder.finish() | ||
93 | }; | ||
94 | Some(Fix::new("Fill struct fields", SourceFileEdit { file_id, edit }.into())) | ||
95 | }; | ||
96 | |||
97 | res.borrow_mut().push(Diagnostic { | ||
98 | range: sema.diagnostics_range(d).range, | ||
99 | message: d.message(), | ||
100 | severity: Severity::Error, | ||
101 | fix, | ||
102 | }) | ||
103 | }) | 60 | }) |
104 | .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { | 61 | .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { |
105 | let node = d.ast(db); | 62 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); |
106 | let replacement = format!("Ok({})", node.syntax()); | ||
107 | let edit = TextEdit::replace(node.syntax().text_range(), replacement); | ||
108 | let source_change = SourceFileEdit { file_id, edit }.into(); | ||
109 | let fix = Fix::new("Wrap with ok", source_change); | ||
110 | res.borrow_mut().push(Diagnostic { | ||
111 | range: sema.diagnostics_range(d).range, | ||
112 | message: d.message(), | ||
113 | severity: Severity::Error, | ||
114 | fix: Some(fix), | ||
115 | }) | ||
116 | }) | 63 | }) |
117 | .on::<hir::diagnostics::NoSuchField, _>(|d| { | 64 | .on::<hir::diagnostics::NoSuchField, _>(|d| { |
118 | res.borrow_mut().push(Diagnostic { | 65 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); |
119 | range: sema.diagnostics_range(d).range, | ||
120 | message: d.message(), | ||
121 | severity: Severity::Error, | ||
122 | fix: missing_struct_field_fix(&sema, file_id, d), | ||
123 | }) | ||
124 | }) | 66 | }) |
125 | // Only collect experimental diagnostics when they're enabled. | 67 | // Only collect experimental diagnostics when they're enabled. |
126 | .filter(|diag| !diag.is_experimental() || enable_experimental) | 68 | .filter(|diag| !diag.is_experimental() || enable_experimental) |
@@ -128,7 +70,7 @@ pub(crate) fn diagnostics( | |||
128 | .build(|d| { | 70 | .build(|d| { |
129 | res.borrow_mut().push(Diagnostic { | 71 | res.borrow_mut().push(Diagnostic { |
130 | message: d.message(), | 72 | message: d.message(), |
131 | range: sema.diagnostics_range(d).range, | 73 | range: sema.diagnostics_display_range(d).range, |
132 | severity: Severity::Error, | 74 | severity: Severity::Error, |
133 | fix: None, | 75 | fix: None, |
134 | }) | 76 | }) |
@@ -141,77 +83,12 @@ pub(crate) fn diagnostics( | |||
141 | res.into_inner() | 83 | res.into_inner() |
142 | } | 84 | } |
143 | 85 | ||
144 | fn missing_struct_field_fix( | 86 | fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { |
145 | sema: &Semantics<RootDatabase>, | 87 | Diagnostic { |
146 | usage_file_id: FileId, | 88 | range: sema.diagnostics_display_range(d).range, |
147 | d: &hir::diagnostics::NoSuchField, | 89 | message: d.message(), |
148 | ) -> Option<Fix> { | 90 | severity: Severity::Error, |
149 | let record_expr = sema.ast(d); | 91 | fix: d.fix(&sema), |
150 | |||
151 | let record_lit = ast::RecordExpr::cast(record_expr.syntax().parent()?.parent()?)?; | ||
152 | let def_id = sema.resolve_variant(record_lit)?; | ||
153 | let module; | ||
154 | let def_file_id; | ||
155 | let record_fields = match VariantDef::from(def_id) { | ||
156 | VariantDef::Struct(s) => { | ||
157 | module = s.module(sema.db); | ||
158 | let source = s.source(sema.db); | ||
159 | def_file_id = source.file_id; | ||
160 | let fields = source.value.field_list()?; | ||
161 | record_field_list(fields)? | ||
162 | } | ||
163 | VariantDef::Union(u) => { | ||
164 | module = u.module(sema.db); | ||
165 | let source = u.source(sema.db); | ||
166 | def_file_id = source.file_id; | ||
167 | source.value.record_field_list()? | ||
168 | } | ||
169 | VariantDef::EnumVariant(e) => { | ||
170 | module = e.module(sema.db); | ||
171 | let source = e.source(sema.db); | ||
172 | def_file_id = source.file_id; | ||
173 | let fields = source.value.field_list()?; | ||
174 | record_field_list(fields)? | ||
175 | } | ||
176 | }; | ||
177 | let def_file_id = def_file_id.original_file(sema.db); | ||
178 | |||
179 | let new_field_type = sema.type_of_expr(&record_expr.expr()?)?; | ||
180 | if new_field_type.is_unknown() { | ||
181 | return None; | ||
182 | } | ||
183 | let new_field = make::record_field( | ||
184 | record_expr.field_name()?, | ||
185 | make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?), | ||
186 | ); | ||
187 | |||
188 | let last_field = record_fields.fields().last()?; | ||
189 | let last_field_syntax = last_field.syntax(); | ||
190 | let indent = IndentLevel::from_node(last_field_syntax); | ||
191 | |||
192 | let mut new_field = new_field.to_string(); | ||
193 | if usage_file_id != def_file_id { | ||
194 | new_field = format!("pub(crate) {}", new_field); | ||
195 | } | ||
196 | new_field = format!("\n{}{}", indent, new_field); | ||
197 | |||
198 | let needs_comma = !last_field_syntax.to_string().ends_with(','); | ||
199 | if needs_comma { | ||
200 | new_field = format!(",{}", new_field); | ||
201 | } | ||
202 | |||
203 | let source_change = SourceFileEdit { | ||
204 | file_id: def_file_id, | ||
205 | edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field), | ||
206 | }; | ||
207 | let fix = Fix::new("Create field", source_change.into()); | ||
208 | return Some(fix); | ||
209 | |||
210 | fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { | ||
211 | match field_def_list { | ||
212 | ast::FieldList::RecordFieldList(it) => Some(it), | ||
213 | ast::FieldList::TupleFieldList(_) => None, | ||
214 | } | ||
215 | } | 92 | } |
216 | } | 93 | } |
217 | 94 | ||
@@ -222,24 +99,25 @@ fn check_unnecessary_braces_in_use_statement( | |||
222 | ) -> Option<()> { | 99 | ) -> Option<()> { |
223 | let use_tree_list = ast::UseTreeList::cast(node.clone())?; | 100 | let use_tree_list = ast::UseTreeList::cast(node.clone())?; |
224 | if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() { | 101 | if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() { |
225 | let range = use_tree_list.syntax().text_range(); | 102 | let use_range = use_tree_list.syntax().text_range(); |
226 | let edit = | 103 | let edit = |
227 | text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree) | 104 | text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree) |
228 | .unwrap_or_else(|| { | 105 | .unwrap_or_else(|| { |
229 | let to_replace = single_use_tree.syntax().text().to_string(); | 106 | let to_replace = single_use_tree.syntax().text().to_string(); |
230 | let mut edit_builder = TextEditBuilder::default(); | 107 | let mut edit_builder = TextEditBuilder::default(); |
231 | edit_builder.delete(range); | 108 | edit_builder.delete(use_range); |
232 | edit_builder.insert(range.start(), to_replace); | 109 | edit_builder.insert(use_range.start(), to_replace); |
233 | edit_builder.finish() | 110 | edit_builder.finish() |
234 | }); | 111 | }); |
235 | 112 | ||
236 | acc.push(Diagnostic { | 113 | acc.push(Diagnostic { |
237 | range, | 114 | range: use_range, |
238 | message: "Unnecessary braces in use statement".to_string(), | 115 | message: "Unnecessary braces in use statement".to_string(), |
239 | severity: Severity::WeakWarning, | 116 | severity: Severity::WeakWarning, |
240 | fix: Some(Fix::new( | 117 | fix: Some(Fix::new( |
241 | "Remove unnecessary braces", | 118 | "Remove unnecessary braces", |
242 | SourceFileEdit { file_id, edit }.into(), | 119 | SourceFileEdit { file_id, edit }.into(), |
120 | use_range, | ||
243 | )), | 121 | )), |
244 | }); | 122 | }); |
245 | } | 123 | } |
@@ -254,8 +132,7 @@ fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement( | |||
254 | if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] { | 132 | if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] { |
255 | let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start(); | 133 | let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start(); |
256 | let end = use_tree_list_node.text_range().end(); | 134 | let end = use_tree_list_node.text_range().end(); |
257 | let range = TextRange::new(start, end); | 135 | return Some(TextEdit::delete(TextRange::new(start, end))); |
258 | return Some(TextEdit::delete(range)); | ||
259 | } | 136 | } |
260 | None | 137 | None |
261 | } | 138 | } |
@@ -278,13 +155,15 @@ fn check_struct_shorthand_initialization( | |||
278 | edit_builder.insert(record_field.syntax().text_range().start(), field_name); | 155 | edit_builder.insert(record_field.syntax().text_range().start(), field_name); |
279 | let edit = edit_builder.finish(); | 156 | let edit = edit_builder.finish(); |
280 | 157 | ||
158 | let field_range = record_field.syntax().text_range(); | ||
281 | acc.push(Diagnostic { | 159 | acc.push(Diagnostic { |
282 | range: record_field.syntax().text_range(), | 160 | range: field_range, |
283 | message: "Shorthand struct initialization".to_string(), | 161 | message: "Shorthand struct initialization".to_string(), |
284 | severity: Severity::WeakWarning, | 162 | severity: Severity::WeakWarning, |
285 | fix: Some(Fix::new( | 163 | fix: Some(Fix::new( |
286 | "Use struct shorthand initialization", | 164 | "Use struct shorthand initialization", |
287 | SourceFileEdit { file_id, edit }.into(), | 165 | SourceFileEdit { file_id, edit }.into(), |
166 | field_range, | ||
288 | )), | 167 | )), |
289 | }); | 168 | }); |
290 | } | 169 | } |
@@ -304,7 +183,7 @@ mod tests { | |||
304 | /// Takes a multi-file input fixture with annotated cursor positions, | 183 | /// Takes a multi-file input fixture with annotated cursor positions, |
305 | /// and checks that: | 184 | /// and checks that: |
306 | /// * a diagnostic is produced | 185 | /// * a diagnostic is produced |
307 | /// * this diagnostic touches the input cursor position | 186 | /// * this diagnostic fix trigger range touches the input cursor position |
308 | /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied | 187 | /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied |
309 | fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) { | 188 | fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) { |
310 | let after = trim_indent(ra_fixture_after); | 189 | let after = trim_indent(ra_fixture_after); |
@@ -322,10 +201,10 @@ mod tests { | |||
322 | 201 | ||
323 | assert_eq_text!(&after, &actual); | 202 | assert_eq_text!(&after, &actual); |
324 | assert!( | 203 | assert!( |
325 | diagnostic.range.start() <= file_position.offset | 204 | fix.fix_trigger_range.start() <= file_position.offset |
326 | && diagnostic.range.end() >= file_position.offset, | 205 | && fix.fix_trigger_range.end() >= file_position.offset, |
327 | "diagnostic range {:?} does not touch cursor position {:?}", | 206 | "diagnostic fix range {:?} does not touch cursor position {:?}", |
328 | diagnostic.range, | 207 | fix.fix_trigger_range, |
329 | file_position.offset | 208 | file_position.offset |
330 | ); | 209 | ); |
331 | } | 210 | } |
@@ -642,6 +521,7 @@ fn test_fn() { | |||
642 | ], | 521 | ], |
643 | is_snippet: false, | 522 | is_snippet: false, |
644 | }, | 523 | }, |
524 | fix_trigger_range: 0..8, | ||
645 | }, | 525 | }, |
646 | ), | 526 | ), |
647 | }, | 527 | }, |
diff --git a/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs b/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs new file mode 100644 index 000000000..f7c73773f --- /dev/null +++ b/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs | |||
@@ -0,0 +1,171 @@ | |||
1 | //! Provides a way to attach fixes to the diagnostics. | ||
2 | //! The same module also has all curret custom fixes for the diagnostics implemented. | ||
3 | use crate::Fix; | ||
4 | use ast::{edit::IndentLevel, make}; | ||
5 | use hir::{ | ||
6 | db::AstDatabase, | ||
7 | diagnostics::{Diagnostic, MissingFields, MissingOkInTailExpr, NoSuchField, UnresolvedModule}, | ||
8 | HasSource, HirDisplay, Semantics, VariantDef, | ||
9 | }; | ||
10 | use ra_db::FileId; | ||
11 | use ra_ide_db::{ | ||
12 | source_change::{FileSystemEdit, SourceFileEdit}, | ||
13 | RootDatabase, | ||
14 | }; | ||
15 | use ra_syntax::{algo, ast, AstNode}; | ||
16 | use ra_text_edit::{TextEdit, TextEditBuilder}; | ||
17 | |||
18 | /// A [Diagnostic] that potentially has a fix available. | ||
19 | /// | ||
20 | /// [Diagnostic]: hir::diagnostics::Diagnostic | ||
21 | pub trait DiagnosticWithFix: Diagnostic { | ||
22 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix>; | ||
23 | } | ||
24 | |||
25 | impl DiagnosticWithFix for UnresolvedModule { | ||
26 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
27 | let root = sema.db.parse_or_expand(self.file)?; | ||
28 | let unresolved_module = self.decl.to_node(&root); | ||
29 | Some(Fix::new( | ||
30 | "Create module", | ||
31 | FileSystemEdit::CreateFile { | ||
32 | anchor: self.file.original_file(sema.db), | ||
33 | dst: self.candidate.clone(), | ||
34 | } | ||
35 | .into(), | ||
36 | unresolved_module.syntax().text_range(), | ||
37 | )) | ||
38 | } | ||
39 | } | ||
40 | |||
41 | impl DiagnosticWithFix for NoSuchField { | ||
42 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
43 | let root = sema.db.parse_or_expand(self.file)?; | ||
44 | missing_record_expr_field_fix( | ||
45 | &sema, | ||
46 | self.file.original_file(sema.db), | ||
47 | &self.field.to_node(&root), | ||
48 | ) | ||
49 | } | ||
50 | } | ||
51 | |||
52 | impl DiagnosticWithFix for MissingFields { | ||
53 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
54 | // Note that although we could add a diagnostics to | ||
55 | // fill the missing tuple field, e.g : | ||
56 | // `struct A(usize);` | ||
57 | // `let a = A { 0: () }` | ||
58 | // but it is uncommon usage and it should not be encouraged. | ||
59 | if self.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) { | ||
60 | return None; | ||
61 | } | ||
62 | |||
63 | let root = sema.db.parse_or_expand(self.file)?; | ||
64 | let old_field_list = self.field_list_parent.to_node(&root).record_expr_field_list()?; | ||
65 | let mut new_field_list = old_field_list.clone(); | ||
66 | for f in self.missed_fields.iter() { | ||
67 | let field = | ||
68 | make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit())); | ||
69 | new_field_list = new_field_list.append_field(&field); | ||
70 | } | ||
71 | |||
72 | let edit = { | ||
73 | let mut builder = TextEditBuilder::default(); | ||
74 | algo::diff(&old_field_list.syntax(), &new_field_list.syntax()) | ||
75 | .into_text_edit(&mut builder); | ||
76 | builder.finish() | ||
77 | }; | ||
78 | Some(Fix::new( | ||
79 | "Fill struct fields", | ||
80 | SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(), | ||
81 | sema.original_range(&old_field_list.syntax()).range, | ||
82 | )) | ||
83 | } | ||
84 | } | ||
85 | |||
86 | impl DiagnosticWithFix for MissingOkInTailExpr { | ||
87 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
88 | let root = sema.db.parse_or_expand(self.file)?; | ||
89 | let tail_expr = self.expr.to_node(&root); | ||
90 | let tail_expr_range = tail_expr.syntax().text_range(); | ||
91 | let edit = TextEdit::replace(tail_expr_range, format!("Ok({})", tail_expr.syntax())); | ||
92 | let source_change = | ||
93 | SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(); | ||
94 | Some(Fix::new("Wrap with ok", source_change, tail_expr_range)) | ||
95 | } | ||
96 | } | ||
97 | |||
98 | fn missing_record_expr_field_fix( | ||
99 | sema: &Semantics<RootDatabase>, | ||
100 | usage_file_id: FileId, | ||
101 | record_expr_field: &ast::RecordExprField, | ||
102 | ) -> Option<Fix> { | ||
103 | let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; | ||
104 | let def_id = sema.resolve_variant(record_lit)?; | ||
105 | let module; | ||
106 | let def_file_id; | ||
107 | let record_fields = match VariantDef::from(def_id) { | ||
108 | VariantDef::Struct(s) => { | ||
109 | module = s.module(sema.db); | ||
110 | let source = s.source(sema.db); | ||
111 | def_file_id = source.file_id; | ||
112 | let fields = source.value.field_list()?; | ||
113 | record_field_list(fields)? | ||
114 | } | ||
115 | VariantDef::Union(u) => { | ||
116 | module = u.module(sema.db); | ||
117 | let source = u.source(sema.db); | ||
118 | def_file_id = source.file_id; | ||
119 | source.value.record_field_list()? | ||
120 | } | ||
121 | VariantDef::EnumVariant(e) => { | ||
122 | module = e.module(sema.db); | ||
123 | let source = e.source(sema.db); | ||
124 | def_file_id = source.file_id; | ||
125 | let fields = source.value.field_list()?; | ||
126 | record_field_list(fields)? | ||
127 | } | ||
128 | }; | ||
129 | let def_file_id = def_file_id.original_file(sema.db); | ||
130 | |||
131 | let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?; | ||
132 | if new_field_type.is_unknown() { | ||
133 | return None; | ||
134 | } | ||
135 | let new_field = make::record_field( | ||
136 | record_expr_field.field_name()?, | ||
137 | make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?), | ||
138 | ); | ||
139 | |||
140 | let last_field = record_fields.fields().last()?; | ||
141 | let last_field_syntax = last_field.syntax(); | ||
142 | let indent = IndentLevel::from_node(last_field_syntax); | ||
143 | |||
144 | let mut new_field = new_field.to_string(); | ||
145 | if usage_file_id != def_file_id { | ||
146 | new_field = format!("pub(crate) {}", new_field); | ||
147 | } | ||
148 | new_field = format!("\n{}{}", indent, new_field); | ||
149 | |||
150 | let needs_comma = !last_field_syntax.to_string().ends_with(','); | ||
151 | if needs_comma { | ||
152 | new_field = format!(",{}", new_field); | ||
153 | } | ||
154 | |||
155 | let source_change = SourceFileEdit { | ||
156 | file_id: def_file_id, | ||
157 | edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field), | ||
158 | }; | ||
159 | return Some(Fix::new( | ||
160 | "Create field", | ||
161 | source_change.into(), | ||
162 | record_expr_field.syntax().text_range(), | ||
163 | )); | ||
164 | |||
165 | fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { | ||
166 | match field_def_list { | ||
167 | ast::FieldList::RecordFieldList(it) => Some(it), | ||
168 | ast::FieldList::TupleFieldList(_) => None, | ||
169 | } | ||
170 | } | ||
171 | } | ||
diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index 0fede0d87..89fcb6f17 100644 --- a/crates/ra_ide/src/lib.rs +++ b/crates/ra_ide/src/lib.rs | |||
@@ -112,13 +112,19 @@ pub struct Diagnostic { | |||
112 | pub struct Fix { | 112 | pub struct Fix { |
113 | pub label: String, | 113 | pub label: String, |
114 | pub source_change: SourceChange, | 114 | pub source_change: SourceChange, |
115 | /// Allows to trigger the fix only when the caret is in the range given | ||
116 | pub fix_trigger_range: TextRange, | ||
115 | } | 117 | } |
116 | 118 | ||
117 | impl Fix { | 119 | impl Fix { |
118 | pub fn new(label: impl Into<String>, source_change: SourceChange) -> Self { | 120 | pub fn new( |
121 | label: impl Into<String>, | ||
122 | source_change: SourceChange, | ||
123 | fix_trigger_range: TextRange, | ||
124 | ) -> Self { | ||
119 | let label = label.into(); | 125 | let label = label.into(); |
120 | assert!(label.starts_with(char::is_uppercase) && !label.ends_with('.')); | 126 | assert!(label.starts_with(char::is_uppercase) && !label.ends_with('.')); |
121 | Self { label, source_change } | 127 | Self { label, source_change, fix_trigger_range } |
122 | } | 128 | } |
123 | } | 129 | } |
124 | 130 | ||