diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2021-05-18 03:02:34 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2021-05-18 03:02:34 +0100 |
commit | e3d0d89d7e3e253234271008df9324ec1faf1746 (patch) | |
tree | 13c4972204ac32dd1a1702c254ffef4b85a76bf3 | |
parent | c04eaa1f37f31d7125372ba14da3d5059297e8b2 (diff) | |
parent | e0b01f34bb994ca8959f3040dbacafc6c56e4778 (diff) |
Merge #8345
8345: Add pub mod option for UnlinkedFile r=rainy-me a=rainy-me
close #8228
This is a draft that changes `Diagnostic` to contain multiple fixes. Pre analysis is in https://github.com/rust-analyzer/rust-analyzer/issues/8228#issuecomment-812887085 Because this solution is straightforward so I decided to type it out for discussion.
Currently the `check_fix` is not able to test the situation when multiple fixes available. <del>Also because `Insert 'mod x;'` and `Insert 'pub mod x;'` are so similar, I don't know how to test them correctly and want some suggestions.</del>. I added
`check_fixes` to allow checking mutiple possible fixes.
In additional, instead of append after possible existing `mod y`, I think it's possible to Insert `pub mod x;` after `pub mod y`. Should I implement this too?
Co-authored-by: rainy-me <[email protected]>
-rw-r--r-- | crates/ide/src/diagnostics.rs | 63 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/field_shorthand.rs | 13 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes.rs | 8 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes/change_case.rs | 10 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes/create_field.rs | 19 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes/fill_missing_fields.rs | 12 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes/remove_semicolon.rs | 10 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes/replace_with_find_map.rs | 12 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes/unresolved_module.rs | 66 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs | 10 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/unlinked_file.rs | 54 | ||||
-rw-r--r-- | crates/ide/src/lib.rs | 2 |
12 files changed, 158 insertions, 121 deletions
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index 4172f6cae..27d347dbd 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs | |||
@@ -28,7 +28,7 @@ use unlinked_file::UnlinkedFile; | |||
28 | 28 | ||
29 | use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange}; | 29 | use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange}; |
30 | 30 | ||
31 | use self::fixes::DiagnosticWithFix; | 31 | use self::fixes::DiagnosticWithFixes; |
32 | 32 | ||
33 | #[derive(Debug)] | 33 | #[derive(Debug)] |
34 | pub struct Diagnostic { | 34 | pub struct Diagnostic { |
@@ -36,14 +36,14 @@ pub struct Diagnostic { | |||
36 | pub message: String, | 36 | pub message: String, |
37 | pub range: TextRange, | 37 | pub range: TextRange, |
38 | pub severity: Severity, | 38 | pub severity: Severity, |
39 | pub fix: Option<Assist>, | 39 | pub fixes: Option<Vec<Assist>>, |
40 | pub unused: bool, | 40 | pub unused: bool, |
41 | pub code: Option<DiagnosticCode>, | 41 | pub code: Option<DiagnosticCode>, |
42 | } | 42 | } |
43 | 43 | ||
44 | impl Diagnostic { | 44 | impl Diagnostic { |
45 | fn error(range: TextRange, message: String) -> Self { | 45 | fn error(range: TextRange, message: String) -> Self { |
46 | Self { message, range, severity: Severity::Error, fix: None, unused: false, code: None } | 46 | Self { message, range, severity: Severity::Error, fixes: None, unused: false, code: None } |
47 | } | 47 | } |
48 | 48 | ||
49 | fn hint(range: TextRange, message: String) -> Self { | 49 | fn hint(range: TextRange, message: String) -> Self { |
@@ -51,14 +51,14 @@ impl Diagnostic { | |||
51 | message, | 51 | message, |
52 | range, | 52 | range, |
53 | severity: Severity::WeakWarning, | 53 | severity: Severity::WeakWarning, |
54 | fix: None, | 54 | fixes: None, |
55 | unused: false, | 55 | unused: false, |
56 | code: None, | 56 | code: None, |
57 | } | 57 | } |
58 | } | 58 | } |
59 | 59 | ||
60 | fn with_fix(self, fix: Option<Assist>) -> Self { | 60 | fn with_fixes(self, fixes: Option<Vec<Assist>>) -> Self { |
61 | Self { fix, ..self } | 61 | Self { fixes, ..self } |
62 | } | 62 | } |
63 | 63 | ||
64 | fn with_unused(self, unused: bool) -> Self { | 64 | fn with_unused(self, unused: bool) -> Self { |
@@ -154,7 +154,7 @@ pub(crate) fn diagnostics( | |||
154 | // Override severity and mark as unused. | 154 | // Override severity and mark as unused. |
155 | res.borrow_mut().push( | 155 | res.borrow_mut().push( |
156 | Diagnostic::hint(range, d.message()) | 156 | Diagnostic::hint(range, d.message()) |
157 | .with_fix(d.fix(&sema, resolve)) | 157 | .with_fixes(d.fixes(&sema, resolve)) |
158 | .with_code(Some(d.code())), | 158 | .with_code(Some(d.code())), |
159 | ); | 159 | ); |
160 | }) | 160 | }) |
@@ -210,23 +210,23 @@ pub(crate) fn diagnostics( | |||
210 | res.into_inner() | 210 | res.into_inner() |
211 | } | 211 | } |
212 | 212 | ||
213 | fn diagnostic_with_fix<D: DiagnosticWithFix>( | 213 | fn diagnostic_with_fix<D: DiagnosticWithFixes>( |
214 | d: &D, | 214 | d: &D, |
215 | sema: &Semantics<RootDatabase>, | 215 | sema: &Semantics<RootDatabase>, |
216 | resolve: &AssistResolveStrategy, | 216 | resolve: &AssistResolveStrategy, |
217 | ) -> Diagnostic { | 217 | ) -> Diagnostic { |
218 | Diagnostic::error(sema.diagnostics_display_range(d.display_source()).range, d.message()) | 218 | Diagnostic::error(sema.diagnostics_display_range(d.display_source()).range, d.message()) |
219 | .with_fix(d.fix(&sema, resolve)) | 219 | .with_fixes(d.fixes(&sema, resolve)) |
220 | .with_code(Some(d.code())) | 220 | .with_code(Some(d.code())) |
221 | } | 221 | } |
222 | 222 | ||
223 | fn warning_with_fix<D: DiagnosticWithFix>( | 223 | fn warning_with_fix<D: DiagnosticWithFixes>( |
224 | d: &D, | 224 | d: &D, |
225 | sema: &Semantics<RootDatabase>, | 225 | sema: &Semantics<RootDatabase>, |
226 | resolve: &AssistResolveStrategy, | 226 | resolve: &AssistResolveStrategy, |
227 | ) -> Diagnostic { | 227 | ) -> Diagnostic { |
228 | Diagnostic::hint(sema.diagnostics_display_range(d.display_source()).range, d.message()) | 228 | Diagnostic::hint(sema.diagnostics_display_range(d.display_source()).range, d.message()) |
229 | .with_fix(d.fix(&sema, resolve)) | 229 | .with_fixes(d.fixes(&sema, resolve)) |
230 | .with_code(Some(d.code())) | 230 | .with_code(Some(d.code())) |
231 | } | 231 | } |
232 | 232 | ||
@@ -256,12 +256,12 @@ fn check_unnecessary_braces_in_use_statement( | |||
256 | 256 | ||
257 | acc.push( | 257 | acc.push( |
258 | Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string()) | 258 | Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string()) |
259 | .with_fix(Some(fix( | 259 | .with_fixes(Some(vec![fix( |
260 | "remove_braces", | 260 | "remove_braces", |
261 | "Remove unnecessary braces", | 261 | "Remove unnecessary braces", |
262 | SourceChange::from_text_edit(file_id, edit), | 262 | SourceChange::from_text_edit(file_id, edit), |
263 | use_range, | 263 | use_range, |
264 | ))), | 264 | )])), |
265 | ); | 265 | ); |
266 | } | 266 | } |
267 | 267 | ||
@@ -309,9 +309,23 @@ mod tests { | |||
309 | /// Takes a multi-file input fixture with annotated cursor positions, | 309 | /// Takes a multi-file input fixture with annotated cursor positions, |
310 | /// and checks that: | 310 | /// and checks that: |
311 | /// * a diagnostic is produced | 311 | /// * a diagnostic is produced |
312 | /// * this diagnostic fix trigger range touches the input cursor position | 312 | /// * the first diagnostic fix trigger range touches the input cursor position |
313 | /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied | 313 | /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied |
314 | pub(crate) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) { | 314 | pub(crate) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) { |
315 | check_nth_fix(0, ra_fixture_before, ra_fixture_after); | ||
316 | } | ||
317 | /// Takes a multi-file input fixture with annotated cursor positions, | ||
318 | /// and checks that: | ||
319 | /// * a diagnostic is produced | ||
320 | /// * every diagnostic fixes trigger range touches the input cursor position | ||
321 | /// * that the contents of the file containing the cursor match `after` after each diagnostic fix is applied | ||
322 | pub(crate) fn check_fixes(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) { | ||
323 | for (i, ra_fixture_after) in ra_fixtures_after.iter().enumerate() { | ||
324 | check_nth_fix(i, ra_fixture_before, ra_fixture_after) | ||
325 | } | ||
326 | } | ||
327 | |||
328 | fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) { | ||
315 | let after = trim_indent(ra_fixture_after); | 329 | let after = trim_indent(ra_fixture_after); |
316 | 330 | ||
317 | let (analysis, file_position) = fixture::position(ra_fixture_before); | 331 | let (analysis, file_position) = fixture::position(ra_fixture_before); |
@@ -324,9 +338,9 @@ mod tests { | |||
324 | .unwrap() | 338 | .unwrap() |
325 | .pop() | 339 | .pop() |
326 | .unwrap(); | 340 | .unwrap(); |
327 | let fix = diagnostic.fix.unwrap(); | 341 | let fix = &diagnostic.fixes.unwrap()[nth]; |
328 | let actual = { | 342 | let actual = { |
329 | let source_change = fix.source_change.unwrap(); | 343 | let source_change = fix.source_change.as_ref().unwrap(); |
330 | let file_id = *source_change.source_file_edits.keys().next().unwrap(); | 344 | let file_id = *source_change.source_file_edits.keys().next().unwrap(); |
331 | let mut actual = analysis.file_text(file_id).unwrap().to_string(); | 345 | let mut actual = analysis.file_text(file_id).unwrap().to_string(); |
332 | 346 | ||
@@ -344,7 +358,6 @@ mod tests { | |||
344 | file_position.offset | 358 | file_position.offset |
345 | ); | 359 | ); |
346 | } | 360 | } |
347 | |||
348 | /// Checks that there's a diagnostic *without* fix at `$0`. | 361 | /// Checks that there's a diagnostic *without* fix at `$0`. |
349 | fn check_no_fix(ra_fixture: &str) { | 362 | fn check_no_fix(ra_fixture: &str) { |
350 | let (analysis, file_position) = fixture::position(ra_fixture); | 363 | let (analysis, file_position) = fixture::position(ra_fixture); |
@@ -357,7 +370,7 @@ mod tests { | |||
357 | .unwrap() | 370 | .unwrap() |
358 | .pop() | 371 | .pop() |
359 | .unwrap(); | 372 | .unwrap(); |
360 | assert!(diagnostic.fix.is_none(), "got a fix when none was expected: {:?}", diagnostic); | 373 | assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic); |
361 | } | 374 | } |
362 | 375 | ||
363 | /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics | 376 | /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics |
@@ -393,7 +406,7 @@ mod tests { | |||
393 | message: "unresolved macro `foo::bar!`", | 406 | message: "unresolved macro `foo::bar!`", |
394 | range: 5..8, | 407 | range: 5..8, |
395 | severity: Error, | 408 | severity: Error, |
396 | fix: None, | 409 | fixes: None, |
397 | unused: false, | 410 | unused: false, |
398 | code: Some( | 411 | code: Some( |
399 | DiagnosticCode( | 412 | DiagnosticCode( |
@@ -542,18 +555,26 @@ mod a { | |||
542 | #[test] | 555 | #[test] |
543 | fn unlinked_file_prepend_first_item() { | 556 | fn unlinked_file_prepend_first_item() { |
544 | cov_mark::check!(unlinked_file_prepend_before_first_item); | 557 | cov_mark::check!(unlinked_file_prepend_before_first_item); |
545 | check_fix( | 558 | // Only tests the first one for `pub mod` since the rest are the same |
559 | check_fixes( | ||
546 | r#" | 560 | r#" |
547 | //- /main.rs | 561 | //- /main.rs |
548 | fn f() {} | 562 | fn f() {} |
549 | //- /foo.rs | 563 | //- /foo.rs |
550 | $0 | 564 | $0 |
551 | "#, | 565 | "#, |
552 | r#" | 566 | vec![ |
567 | r#" | ||
553 | mod foo; | 568 | mod foo; |
554 | 569 | ||
555 | fn f() {} | 570 | fn f() {} |
556 | "#, | 571 | "#, |
572 | r#" | ||
573 | pub mod foo; | ||
574 | |||
575 | fn f() {} | ||
576 | "#, | ||
577 | ], | ||
557 | ); | 578 | ); |
558 | } | 579 | } |
559 | 580 | ||
diff --git a/crates/ide/src/diagnostics/field_shorthand.rs b/crates/ide/src/diagnostics/field_shorthand.rs index 2b1787f9b..01bd2dba6 100644 --- a/crates/ide/src/diagnostics/field_shorthand.rs +++ b/crates/ide/src/diagnostics/field_shorthand.rs | |||
@@ -46,14 +46,13 @@ fn check_expr_field_shorthand( | |||
46 | 46 | ||
47 | let field_range = record_field.syntax().text_range(); | 47 | let field_range = record_field.syntax().text_range(); |
48 | acc.push( | 48 | acc.push( |
49 | Diagnostic::hint(field_range, "Shorthand struct initialization".to_string()).with_fix( | 49 | Diagnostic::hint(field_range, "Shorthand struct initialization".to_string()) |
50 | Some(fix( | 50 | .with_fixes(Some(vec![fix( |
51 | "use_expr_field_shorthand", | 51 | "use_expr_field_shorthand", |
52 | "Use struct shorthand initialization", | 52 | "Use struct shorthand initialization", |
53 | SourceChange::from_text_edit(file_id, edit), | 53 | SourceChange::from_text_edit(file_id, edit), |
54 | field_range, | 54 | field_range, |
55 | )), | 55 | )])), |
56 | ), | ||
57 | ); | 56 | ); |
58 | } | 57 | } |
59 | } | 58 | } |
@@ -86,13 +85,13 @@ fn check_pat_field_shorthand( | |||
86 | let edit = edit_builder.finish(); | 85 | let edit = edit_builder.finish(); |
87 | 86 | ||
88 | let field_range = record_pat_field.syntax().text_range(); | 87 | let field_range = record_pat_field.syntax().text_range(); |
89 | acc.push(Diagnostic::hint(field_range, "Shorthand struct pattern".to_string()).with_fix( | 88 | acc.push(Diagnostic::hint(field_range, "Shorthand struct pattern".to_string()).with_fixes( |
90 | Some(fix( | 89 | Some(vec![fix( |
91 | "use_pat_field_shorthand", | 90 | "use_pat_field_shorthand", |
92 | "Use struct field shorthand", | 91 | "Use struct field shorthand", |
93 | SourceChange::from_text_edit(file_id, edit), | 92 | SourceChange::from_text_edit(file_id, edit), |
94 | field_range, | 93 | field_range, |
95 | )), | 94 | )]), |
96 | )); | 95 | )); |
97 | } | 96 | } |
98 | } | 97 | } |
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs index 92b3f5a2d..258ac6974 100644 --- a/crates/ide/src/diagnostics/fixes.rs +++ b/crates/ide/src/diagnostics/fixes.rs | |||
@@ -14,18 +14,18 @@ use ide_db::RootDatabase; | |||
14 | 14 | ||
15 | use crate::Assist; | 15 | use crate::Assist; |
16 | 16 | ||
17 | /// A [Diagnostic] that potentially has a fix available. | 17 | /// A [Diagnostic] that potentially has some fixes available. |
18 | /// | 18 | /// |
19 | /// [Diagnostic]: hir::diagnostics::Diagnostic | 19 | /// [Diagnostic]: hir::diagnostics::Diagnostic |
20 | pub(crate) trait DiagnosticWithFix: Diagnostic { | 20 | pub(crate) trait DiagnosticWithFixes: Diagnostic { |
21 | /// `resolve` determines if the diagnostic should fill in the `edit` field | 21 | /// `resolve` determines if the diagnostic should fill in the `edit` field |
22 | /// of the assist. | 22 | /// of the assist. |
23 | /// | 23 | /// |
24 | /// If `resolve` is false, the edit will be computed later, on demand, and | 24 | /// If `resolve` is false, the edit will be computed later, on demand, and |
25 | /// can be omitted. | 25 | /// can be omitted. |
26 | fn fix( | 26 | fn fixes( |
27 | &self, | 27 | &self, |
28 | sema: &Semantics<RootDatabase>, | 28 | sema: &Semantics<RootDatabase>, |
29 | _resolve: &AssistResolveStrategy, | 29 | _resolve: &AssistResolveStrategy, |
30 | ) -> Option<Assist>; | 30 | ) -> Option<Vec<Assist>>; |
31 | } | 31 | } |
diff --git a/crates/ide/src/diagnostics/fixes/change_case.rs b/crates/ide/src/diagnostics/fixes/change_case.rs index 80aca58a1..42be3375f 100644 --- a/crates/ide/src/diagnostics/fixes/change_case.rs +++ b/crates/ide/src/diagnostics/fixes/change_case.rs | |||
@@ -4,16 +4,16 @@ use ide_db::{base_db::FilePosition, RootDatabase}; | |||
4 | use syntax::AstNode; | 4 | use syntax::AstNode; |
5 | 5 | ||
6 | use crate::{ | 6 | use crate::{ |
7 | diagnostics::{unresolved_fix, DiagnosticWithFix}, | 7 | diagnostics::{unresolved_fix, DiagnosticWithFixes}, |
8 | references::rename::rename_with_semantics, | 8 | references::rename::rename_with_semantics, |
9 | }; | 9 | }; |
10 | 10 | ||
11 | impl DiagnosticWithFix for IncorrectCase { | 11 | impl DiagnosticWithFixes for IncorrectCase { |
12 | fn fix( | 12 | fn fixes( |
13 | &self, | 13 | &self, |
14 | sema: &Semantics<RootDatabase>, | 14 | sema: &Semantics<RootDatabase>, |
15 | resolve: &AssistResolveStrategy, | 15 | resolve: &AssistResolveStrategy, |
16 | ) -> Option<Assist> { | 16 | ) -> Option<Vec<Assist>> { |
17 | let root = sema.db.parse_or_expand(self.file)?; | 17 | let root = sema.db.parse_or_expand(self.file)?; |
18 | let name_node = self.ident.to_node(&root); | 18 | let name_node = self.ident.to_node(&root); |
19 | 19 | ||
@@ -28,7 +28,7 @@ impl DiagnosticWithFix for IncorrectCase { | |||
28 | res.source_change = Some(source_change.ok().unwrap_or_default()); | 28 | res.source_change = Some(source_change.ok().unwrap_or_default()); |
29 | } | 29 | } |
30 | 30 | ||
31 | Some(res) | 31 | Some(vec![res]) |
32 | } | 32 | } |
33 | } | 33 | } |
34 | 34 | ||
diff --git a/crates/ide/src/diagnostics/fixes/create_field.rs b/crates/ide/src/diagnostics/fixes/create_field.rs index 24e0fda52..a5f457dce 100644 --- a/crates/ide/src/diagnostics/fixes/create_field.rs +++ b/crates/ide/src/diagnostics/fixes/create_field.rs | |||
@@ -7,18 +7,17 @@ use syntax::{ | |||
7 | use text_edit::TextEdit; | 7 | use text_edit::TextEdit; |
8 | 8 | ||
9 | use crate::{ | 9 | use crate::{ |
10 | diagnostics::{fix, DiagnosticWithFix}, | 10 | diagnostics::{fix, DiagnosticWithFixes}, |
11 | Assist, AssistResolveStrategy, | 11 | Assist, AssistResolveStrategy, |
12 | }; | 12 | }; |
13 | 13 | impl DiagnosticWithFixes for NoSuchField { | |
14 | impl DiagnosticWithFix for NoSuchField { | 14 | fn fixes( |
15 | fn fix( | ||
16 | &self, | 15 | &self, |
17 | sema: &Semantics<RootDatabase>, | 16 | sema: &Semantics<RootDatabase>, |
18 | _resolve: &AssistResolveStrategy, | 17 | _resolve: &AssistResolveStrategy, |
19 | ) -> Option<Assist> { | 18 | ) -> Option<Vec<Assist>> { |
20 | let root = sema.db.parse_or_expand(self.file)?; | 19 | let root = sema.db.parse_or_expand(self.file)?; |
21 | missing_record_expr_field_fix( | 20 | missing_record_expr_field_fixes( |
22 | &sema, | 21 | &sema, |
23 | self.file.original_file(sema.db), | 22 | self.file.original_file(sema.db), |
24 | &self.field.to_node(&root), | 23 | &self.field.to_node(&root), |
@@ -26,11 +25,11 @@ impl DiagnosticWithFix for NoSuchField { | |||
26 | } | 25 | } |
27 | } | 26 | } |
28 | 27 | ||
29 | fn missing_record_expr_field_fix( | 28 | fn missing_record_expr_field_fixes( |
30 | sema: &Semantics<RootDatabase>, | 29 | sema: &Semantics<RootDatabase>, |
31 | usage_file_id: FileId, | 30 | usage_file_id: FileId, |
32 | record_expr_field: &ast::RecordExprField, | 31 | record_expr_field: &ast::RecordExprField, |
33 | ) -> Option<Assist> { | 32 | ) -> Option<Vec<Assist>> { |
34 | let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; | 33 | let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; |
35 | let def_id = sema.resolve_variant(record_lit)?; | 34 | let def_id = sema.resolve_variant(record_lit)?; |
36 | let module; | 35 | let module; |
@@ -89,12 +88,12 @@ fn missing_record_expr_field_fix( | |||
89 | TextEdit::insert(last_field_syntax.text_range().end(), new_field), | 88 | TextEdit::insert(last_field_syntax.text_range().end(), new_field), |
90 | ); | 89 | ); |
91 | 90 | ||
92 | return Some(fix( | 91 | return Some(vec![fix( |
93 | "create_field", | 92 | "create_field", |
94 | "Create field", | 93 | "Create field", |
95 | source_change, | 94 | source_change, |
96 | record_expr_field.syntax().text_range(), | 95 | record_expr_field.syntax().text_range(), |
97 | )); | 96 | )]); |
98 | 97 | ||
99 | fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { | 98 | fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { |
100 | match field_def_list { | 99 | match field_def_list { |
diff --git a/crates/ide/src/diagnostics/fixes/fill_missing_fields.rs b/crates/ide/src/diagnostics/fixes/fill_missing_fields.rs index 37a0e37a9..b5dd64c08 100644 --- a/crates/ide/src/diagnostics/fixes/fill_missing_fields.rs +++ b/crates/ide/src/diagnostics/fixes/fill_missing_fields.rs | |||
@@ -5,16 +5,16 @@ use syntax::{algo, ast::make, AstNode}; | |||
5 | use text_edit::TextEdit; | 5 | use text_edit::TextEdit; |
6 | 6 | ||
7 | use crate::{ | 7 | use crate::{ |
8 | diagnostics::{fix, fixes::DiagnosticWithFix}, | 8 | diagnostics::{fix, fixes::DiagnosticWithFixes}, |
9 | Assist, | 9 | Assist, |
10 | }; | 10 | }; |
11 | 11 | ||
12 | impl DiagnosticWithFix for MissingFields { | 12 | impl DiagnosticWithFixes for MissingFields { |
13 | fn fix( | 13 | fn fixes( |
14 | &self, | 14 | &self, |
15 | sema: &Semantics<RootDatabase>, | 15 | sema: &Semantics<RootDatabase>, |
16 | _resolve: &AssistResolveStrategy, | 16 | _resolve: &AssistResolveStrategy, |
17 | ) -> Option<Assist> { | 17 | ) -> Option<Vec<Assist>> { |
18 | // Note that although we could add a diagnostics to | 18 | // Note that although we could add a diagnostics to |
19 | // fill the missing tuple field, e.g : | 19 | // fill the missing tuple field, e.g : |
20 | // `struct A(usize);` | 20 | // `struct A(usize);` |
@@ -41,12 +41,12 @@ impl DiagnosticWithFix for MissingFields { | |||
41 | .into_text_edit(&mut builder); | 41 | .into_text_edit(&mut builder); |
42 | builder.finish() | 42 | builder.finish() |
43 | }; | 43 | }; |
44 | Some(fix( | 44 | Some(vec![fix( |
45 | "fill_missing_fields", | 45 | "fill_missing_fields", |
46 | "Fill struct fields", | 46 | "Fill struct fields", |
47 | SourceChange::from_text_edit(self.file.original_file(sema.db), edit), | 47 | SourceChange::from_text_edit(self.file.original_file(sema.db), edit), |
48 | sema.original_range(&field_list_parent.syntax()).range, | 48 | sema.original_range(&field_list_parent.syntax()).range, |
49 | )) | 49 | )]) |
50 | } | 50 | } |
51 | } | 51 | } |
52 | 52 | ||
diff --git a/crates/ide/src/diagnostics/fixes/remove_semicolon.rs b/crates/ide/src/diagnostics/fixes/remove_semicolon.rs index 45471da41..f1724d479 100644 --- a/crates/ide/src/diagnostics/fixes/remove_semicolon.rs +++ b/crates/ide/src/diagnostics/fixes/remove_semicolon.rs | |||
@@ -4,14 +4,14 @@ use ide_db::{source_change::SourceChange, RootDatabase}; | |||
4 | use syntax::{ast, AstNode}; | 4 | use syntax::{ast, AstNode}; |
5 | use text_edit::TextEdit; | 5 | use text_edit::TextEdit; |
6 | 6 | ||
7 | use crate::diagnostics::{fix, DiagnosticWithFix}; | 7 | use crate::diagnostics::{fix, DiagnosticWithFixes}; |
8 | 8 | ||
9 | impl DiagnosticWithFix for RemoveThisSemicolon { | 9 | impl DiagnosticWithFixes for RemoveThisSemicolon { |
10 | fn fix( | 10 | fn fixes( |
11 | &self, | 11 | &self, |
12 | sema: &Semantics<RootDatabase>, | 12 | sema: &Semantics<RootDatabase>, |
13 | _resolve: &AssistResolveStrategy, | 13 | _resolve: &AssistResolveStrategy, |
14 | ) -> Option<Assist> { | 14 | ) -> Option<Vec<Assist>> { |
15 | let root = sema.db.parse_or_expand(self.file)?; | 15 | let root = sema.db.parse_or_expand(self.file)?; |
16 | 16 | ||
17 | let semicolon = self | 17 | let semicolon = self |
@@ -26,7 +26,7 @@ impl DiagnosticWithFix for RemoveThisSemicolon { | |||
26 | let edit = TextEdit::delete(semicolon); | 26 | let edit = TextEdit::delete(semicolon); |
27 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | 27 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); |
28 | 28 | ||
29 | Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)) | 29 | Some(vec![fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)]) |
30 | } | 30 | } |
31 | } | 31 | } |
32 | 32 | ||
diff --git a/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs b/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs index b0ef7b44a..444bf563b 100644 --- a/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs +++ b/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs | |||
@@ -7,14 +7,14 @@ use syntax::{ | |||
7 | }; | 7 | }; |
8 | use text_edit::TextEdit; | 8 | use text_edit::TextEdit; |
9 | 9 | ||
10 | use crate::diagnostics::{fix, DiagnosticWithFix}; | 10 | use crate::diagnostics::{fix, DiagnosticWithFixes}; |
11 | 11 | ||
12 | impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { | 12 | impl DiagnosticWithFixes for ReplaceFilterMapNextWithFindMap { |
13 | fn fix( | 13 | fn fixes( |
14 | &self, | 14 | &self, |
15 | sema: &Semantics<RootDatabase>, | 15 | sema: &Semantics<RootDatabase>, |
16 | _resolve: &AssistResolveStrategy, | 16 | _resolve: &AssistResolveStrategy, |
17 | ) -> Option<Assist> { | 17 | ) -> Option<Vec<Assist>> { |
18 | let root = sema.db.parse_or_expand(self.file)?; | 18 | let root = sema.db.parse_or_expand(self.file)?; |
19 | let next_expr = self.next_expr.to_node(&root); | 19 | let next_expr = self.next_expr.to_node(&root); |
20 | let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?; | 20 | let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?; |
@@ -32,12 +32,12 @@ impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { | |||
32 | 32 | ||
33 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | 33 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); |
34 | 34 | ||
35 | Some(fix( | 35 | Some(vec![fix( |
36 | "replace_with_find_map", | 36 | "replace_with_find_map", |
37 | "Replace filter_map(..).next() with find_map()", | 37 | "Replace filter_map(..).next() with find_map()", |
38 | source_change, | 38 | source_change, |
39 | trigger_range, | 39 | trigger_range, |
40 | )) | 40 | )]) |
41 | } | 41 | } |
42 | } | 42 | } |
43 | 43 | ||
diff --git a/crates/ide/src/diagnostics/fixes/unresolved_module.rs b/crates/ide/src/diagnostics/fixes/unresolved_module.rs index 81244b293..b3d0283bb 100644 --- a/crates/ide/src/diagnostics/fixes/unresolved_module.rs +++ b/crates/ide/src/diagnostics/fixes/unresolved_module.rs | |||
@@ -3,17 +3,17 @@ use ide_assists::{Assist, AssistResolveStrategy}; | |||
3 | use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit, RootDatabase}; | 3 | use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit, RootDatabase}; |
4 | use syntax::AstNode; | 4 | use syntax::AstNode; |
5 | 5 | ||
6 | use crate::diagnostics::{fix, DiagnosticWithFix}; | 6 | use crate::diagnostics::{fix, DiagnosticWithFixes}; |
7 | 7 | ||
8 | impl DiagnosticWithFix for UnresolvedModule { | 8 | impl DiagnosticWithFixes for UnresolvedModule { |
9 | fn fix( | 9 | fn fixes( |
10 | &self, | 10 | &self, |
11 | sema: &Semantics<RootDatabase>, | 11 | sema: &Semantics<RootDatabase>, |
12 | _resolve: &AssistResolveStrategy, | 12 | _resolve: &AssistResolveStrategy, |
13 | ) -> Option<Assist> { | 13 | ) -> Option<Vec<Assist>> { |
14 | let root = sema.db.parse_or_expand(self.file)?; | 14 | let root = sema.db.parse_or_expand(self.file)?; |
15 | let unresolved_module = self.decl.to_node(&root); | 15 | let unresolved_module = self.decl.to_node(&root); |
16 | Some(fix( | 16 | Some(vec![fix( |
17 | "create_module", | 17 | "create_module", |
18 | "Create module", | 18 | "Create module", |
19 | FileSystemEdit::CreateFile { | 19 | FileSystemEdit::CreateFile { |
@@ -25,7 +25,7 @@ impl DiagnosticWithFix for UnresolvedModule { | |||
25 | } | 25 | } |
26 | .into(), | 26 | .into(), |
27 | unresolved_module.syntax().text_range(), | 27 | unresolved_module.syntax().text_range(), |
28 | )) | 28 | )]) |
29 | } | 29 | } |
30 | } | 30 | } |
31 | 31 | ||
@@ -45,33 +45,35 @@ mod tests { | |||
45 | message: "unresolved module", | 45 | message: "unresolved module", |
46 | range: 0..8, | 46 | range: 0..8, |
47 | severity: Error, | 47 | severity: Error, |
48 | fix: Some( | 48 | fixes: Some( |
49 | Assist { | 49 | [ |
50 | id: AssistId( | 50 | Assist { |
51 | "create_module", | 51 | id: AssistId( |
52 | QuickFix, | 52 | "create_module", |
53 | ), | 53 | QuickFix, |
54 | label: "Create module", | 54 | ), |
55 | group: None, | 55 | label: "Create module", |
56 | target: 0..8, | 56 | group: None, |
57 | source_change: Some( | 57 | target: 0..8, |
58 | SourceChange { | 58 | source_change: Some( |
59 | source_file_edits: {}, | 59 | SourceChange { |
60 | file_system_edits: [ | 60 | source_file_edits: {}, |
61 | CreateFile { | 61 | file_system_edits: [ |
62 | dst: AnchoredPathBuf { | 62 | CreateFile { |
63 | anchor: FileId( | 63 | dst: AnchoredPathBuf { |
64 | 0, | 64 | anchor: FileId( |
65 | ), | 65 | 0, |
66 | path: "foo.rs", | 66 | ), |
67 | path: "foo.rs", | ||
68 | }, | ||
69 | initial_contents: "", | ||
67 | }, | 70 | }, |
68 | initial_contents: "", | 71 | ], |
69 | }, | 72 | is_snippet: false, |
70 | ], | 73 | }, |
71 | is_snippet: false, | 74 | ), |
72 | }, | 75 | }, |
73 | ), | 76 | ], |
74 | }, | ||
75 | ), | 77 | ), |
76 | unused: false, | 78 | unused: false, |
77 | code: Some( | 79 | code: Some( |
diff --git a/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs b/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs index 66676064a..715a403b9 100644 --- a/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs +++ b/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs | |||
@@ -4,14 +4,14 @@ use ide_db::{source_change::SourceChange, RootDatabase}; | |||
4 | use syntax::AstNode; | 4 | use syntax::AstNode; |
5 | use text_edit::TextEdit; | 5 | use text_edit::TextEdit; |
6 | 6 | ||
7 | use crate::diagnostics::{fix, DiagnosticWithFix}; | 7 | use crate::diagnostics::{fix, DiagnosticWithFixes}; |
8 | 8 | ||
9 | impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { | 9 | impl DiagnosticWithFixes for MissingOkOrSomeInTailExpr { |
10 | fn fix( | 10 | fn fixes( |
11 | &self, | 11 | &self, |
12 | sema: &Semantics<RootDatabase>, | 12 | sema: &Semantics<RootDatabase>, |
13 | _resolve: &AssistResolveStrategy, | 13 | _resolve: &AssistResolveStrategy, |
14 | ) -> Option<Assist> { | 14 | ) -> Option<Vec<Assist>> { |
15 | let root = sema.db.parse_or_expand(self.file)?; | 15 | let root = sema.db.parse_or_expand(self.file)?; |
16 | let tail_expr = self.expr.to_node(&root); | 16 | let tail_expr = self.expr.to_node(&root); |
17 | let tail_expr_range = tail_expr.syntax().text_range(); | 17 | let tail_expr_range = tail_expr.syntax().text_range(); |
@@ -19,7 +19,7 @@ impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { | |||
19 | let edit = TextEdit::replace(tail_expr_range, replacement); | 19 | let edit = TextEdit::replace(tail_expr_range, replacement); |
20 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | 20 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); |
21 | let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; | 21 | let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; |
22 | Some(fix("wrap_tail_expr", name, source_change, tail_expr_range)) | 22 | Some(vec![fix("wrap_tail_expr", name, source_change, tail_expr_range)]) |
23 | } | 23 | } |
24 | } | 24 | } |
25 | 25 | ||
diff --git a/crates/ide/src/diagnostics/unlinked_file.rs b/crates/ide/src/diagnostics/unlinked_file.rs index 93fd25dea..51fe0f360 100644 --- a/crates/ide/src/diagnostics/unlinked_file.rs +++ b/crates/ide/src/diagnostics/unlinked_file.rs | |||
@@ -18,7 +18,7 @@ use syntax::{ | |||
18 | use text_edit::TextEdit; | 18 | use text_edit::TextEdit; |
19 | 19 | ||
20 | use crate::{ | 20 | use crate::{ |
21 | diagnostics::{fix, fixes::DiagnosticWithFix}, | 21 | diagnostics::{fix, fixes::DiagnosticWithFixes}, |
22 | Assist, | 22 | Assist, |
23 | }; | 23 | }; |
24 | 24 | ||
@@ -50,13 +50,13 @@ impl Diagnostic for UnlinkedFile { | |||
50 | } | 50 | } |
51 | } | 51 | } |
52 | 52 | ||
53 | impl DiagnosticWithFix for UnlinkedFile { | 53 | impl DiagnosticWithFixes for UnlinkedFile { |
54 | fn fix( | 54 | fn fixes( |
55 | &self, | 55 | &self, |
56 | sema: &hir::Semantics<RootDatabase>, | 56 | sema: &hir::Semantics<RootDatabase>, |
57 | _resolve: &AssistResolveStrategy, | 57 | _resolve: &AssistResolveStrategy, |
58 | ) -> Option<Assist> { | 58 | ) -> Option<Vec<Assist>> { |
59 | // If there's an existing module that could add a `mod` item to include the unlinked file, | 59 | // If there's an existing module that could add `mod` or `pub mod` items to include the unlinked file, |
60 | // suggest that as a fix. | 60 | // suggest that as a fix. |
61 | 61 | ||
62 | let source_root = sema.db.source_root(sema.db.file_source_root(self.file_id)); | 62 | let source_root = sema.db.source_root(sema.db.file_source_root(self.file_id)); |
@@ -90,7 +90,7 @@ impl DiagnosticWithFix for UnlinkedFile { | |||
90 | } | 90 | } |
91 | 91 | ||
92 | if module.origin.file_id() == Some(*parent_id) { | 92 | if module.origin.file_id() == Some(*parent_id) { |
93 | return make_fix(sema.db, *parent_id, module_name, self.file_id); | 93 | return make_fixes(sema.db, *parent_id, module_name, self.file_id); |
94 | } | 94 | } |
95 | } | 95 | } |
96 | } | 96 | } |
@@ -101,20 +101,23 @@ impl DiagnosticWithFix for UnlinkedFile { | |||
101 | } | 101 | } |
102 | } | 102 | } |
103 | 103 | ||
104 | fn make_fix( | 104 | fn make_fixes( |
105 | db: &RootDatabase, | 105 | db: &RootDatabase, |
106 | parent_file_id: FileId, | 106 | parent_file_id: FileId, |
107 | new_mod_name: &str, | 107 | new_mod_name: &str, |
108 | added_file_id: FileId, | 108 | added_file_id: FileId, |
109 | ) -> Option<Assist> { | 109 | ) -> Option<Vec<Assist>> { |
110 | fn is_outline_mod(item: &ast::Item) -> bool { | 110 | fn is_outline_mod(item: &ast::Item) -> bool { |
111 | matches!(item, ast::Item::Module(m) if m.item_list().is_none()) | 111 | matches!(item, ast::Item::Module(m) if m.item_list().is_none()) |
112 | } | 112 | } |
113 | 113 | ||
114 | let mod_decl = format!("mod {};", new_mod_name); | 114 | let mod_decl = format!("mod {};", new_mod_name); |
115 | let pub_mod_decl = format!("pub mod {};", new_mod_name); | ||
116 | |||
115 | let ast: ast::SourceFile = db.parse(parent_file_id).tree(); | 117 | let ast: ast::SourceFile = db.parse(parent_file_id).tree(); |
116 | 118 | ||
117 | let mut builder = TextEdit::builder(); | 119 | let mut mod_decl_builder = TextEdit::builder(); |
120 | let mut pub_mod_decl_builder = TextEdit::builder(); | ||
118 | 121 | ||
119 | // If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's | 122 | // If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's |
120 | // probably `#[cfg]`d out). | 123 | // probably `#[cfg]`d out). |
@@ -138,30 +141,43 @@ fn make_fix( | |||
138 | { | 141 | { |
139 | Some(last) => { | 142 | Some(last) => { |
140 | cov_mark::hit!(unlinked_file_append_to_existing_mods); | 143 | cov_mark::hit!(unlinked_file_append_to_existing_mods); |
141 | builder.insert(last.syntax().text_range().end(), format!("\n{}", mod_decl)); | 144 | let offset = last.syntax().text_range().end(); |
145 | mod_decl_builder.insert(offset, format!("\n{}", mod_decl)); | ||
146 | pub_mod_decl_builder.insert(offset, format!("\n{}", pub_mod_decl)); | ||
142 | } | 147 | } |
143 | None => { | 148 | None => { |
144 | // Prepend before the first item in the file. | 149 | // Prepend before the first item in the file. |
145 | match ast.items().next() { | 150 | match ast.items().next() { |
146 | Some(item) => { | 151 | Some(item) => { |
147 | cov_mark::hit!(unlinked_file_prepend_before_first_item); | 152 | cov_mark::hit!(unlinked_file_prepend_before_first_item); |
148 | builder.insert(item.syntax().text_range().start(), format!("{}\n\n", mod_decl)); | 153 | let offset = item.syntax().text_range().start(); |
154 | mod_decl_builder.insert(offset, format!("{}\n\n", mod_decl)); | ||
155 | pub_mod_decl_builder.insert(offset, format!("{}\n\n", pub_mod_decl)); | ||
149 | } | 156 | } |
150 | None => { | 157 | None => { |
151 | // No items in the file, so just append at the end. | 158 | // No items in the file, so just append at the end. |
152 | cov_mark::hit!(unlinked_file_empty_file); | 159 | cov_mark::hit!(unlinked_file_empty_file); |
153 | builder.insert(ast.syntax().text_range().end(), format!("{}\n", mod_decl)); | 160 | let offset = ast.syntax().text_range().end(); |
161 | mod_decl_builder.insert(offset, format!("{}\n", mod_decl)); | ||
162 | pub_mod_decl_builder.insert(offset, format!("{}\n", pub_mod_decl)); | ||
154 | } | 163 | } |
155 | } | 164 | } |
156 | } | 165 | } |
157 | } | 166 | } |
158 | 167 | ||
159 | let edit = builder.finish(); | ||
160 | let trigger_range = db.parse(added_file_id).tree().syntax().text_range(); | 168 | let trigger_range = db.parse(added_file_id).tree().syntax().text_range(); |
161 | Some(fix( | 169 | Some(vec![ |
162 | "add_mod_declaration", | 170 | fix( |
163 | &format!("Insert `{}`", mod_decl), | 171 | "add_mod_declaration", |
164 | SourceChange::from_text_edit(parent_file_id, edit), | 172 | &format!("Insert `{}`", mod_decl), |
165 | trigger_range, | 173 | SourceChange::from_text_edit(parent_file_id, mod_decl_builder.finish()), |
166 | )) | 174 | trigger_range, |
175 | ), | ||
176 | fix( | ||
177 | "add_pub_mod_declaration", | ||
178 | &format!("Insert `{}`", pub_mod_decl), | ||
179 | SourceChange::from_text_edit(parent_file_id, pub_mod_decl_builder.finish()), | ||
180 | trigger_range, | ||
181 | ), | ||
182 | ]) | ||
167 | } | 183 | } |
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index db08547d1..f4b90db3a 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs | |||
@@ -565,7 +565,7 @@ impl Analysis { | |||
565 | let diagnostic_assists = if include_fixes { | 565 | let diagnostic_assists = if include_fixes { |
566 | diagnostics::diagnostics(db, diagnostics_config, &resolve, frange.file_id) | 566 | diagnostics::diagnostics(db, diagnostics_config, &resolve, frange.file_id) |
567 | .into_iter() | 567 | .into_iter() |
568 | .filter_map(|it| it.fix) | 568 | .flat_map(|it| it.fixes.unwrap_or_default()) |
569 | .filter(|it| it.target.intersect(frange.range).is_some()) | 569 | .filter(|it| it.target.intersect(frange.range).is_some()) |
570 | .collect() | 570 | .collect() |
571 | } else { | 571 | } else { |