diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-08-12 14:44:13 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2020-08-12 14:44:13 +0100 |
commit | 5b8fdfe23100b88e4fd8e210ccf6b852f5c9bf2a (patch) | |
tree | e321c900fe4997ec5ffe20cbb09946502745849c /crates/ra_ide | |
parent | 11de7ac2fb6514484076217acb8d93eb36468681 (diff) | |
parent | db12ccee96bf37367b39ad99638d06da7123c088 (diff) |
Merge #5553
5553: Add fix ranges for diagnostics r=matklad a=SomeoneToIgnore
A follow-up of https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Fwg-rls-2.2E0/topic/Less.20red.20in.20the.20code
Now diagnostics can apply fixes in a range that's different from the range used to highlight the diagnostics.
Previous logic did not consider the fix range, having both ranges equal, which could cause a lot of red noise in the editor.
Now, the fix range gets used with the fix, the diagnostics range is used for everything else which allows to improve the error highlighting.
before:
<img width="191" alt="image" src="https://user-images.githubusercontent.com/2690773/88590727-df9a6a00-d063-11ea-97ed-9809c1c5e6e6.png">
after:
<img width="222" alt="image" src="https://user-images.githubusercontent.com/2690773/88590734-e1fcc400-d063-11ea-9b7c-25701cbd5352.png">
`MissingFields` and `MissingPatFields` diagnostics now have the fix range as `ast::RecordFieldList` of the expression with an error (as it was before this PR), and the diagnostics range as a `ast::Path` of the expression, if it's present (do you have any example of `ast::Expr::RecordLit` that has no path btw?).
The rest of the diagnostics have both ranges equal, same as it was before this PR.
Co-authored-by: Kirill Bulatov <[email protected]>
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 | ||