aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src')
-rw-r--r--crates/ide/src/diagnostics.rs63
-rw-r--r--crates/ide/src/diagnostics/field_shorthand.rs13
-rw-r--r--crates/ide/src/diagnostics/fixes.rs8
-rw-r--r--crates/ide/src/diagnostics/fixes/change_case.rs10
-rw-r--r--crates/ide/src/diagnostics/fixes/create_field.rs19
-rw-r--r--crates/ide/src/diagnostics/fixes/fill_missing_fields.rs12
-rw-r--r--crates/ide/src/diagnostics/fixes/remove_semicolon.rs10
-rw-r--r--crates/ide/src/diagnostics/fixes/replace_with_find_map.rs12
-rw-r--r--crates/ide/src/diagnostics/fixes/unresolved_module.rs66
-rw-r--r--crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs10
-rw-r--r--crates/ide/src/diagnostics/unlinked_file.rs54
-rw-r--r--crates/ide/src/lib.rs2
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
29use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange}; 29use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange};
30 30
31use self::fixes::DiagnosticWithFix; 31use self::fixes::DiagnosticWithFixes;
32 32
33#[derive(Debug)] 33#[derive(Debug)]
34pub struct Diagnostic { 34pub 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
44impl Diagnostic { 44impl 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
213fn diagnostic_with_fix<D: DiagnosticWithFix>( 213fn 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
223fn warning_with_fix<D: DiagnosticWithFix>( 223fn 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
548fn f() {} 562fn f() {}
549//- /foo.rs 563//- /foo.rs
550$0 564$0
551"#, 565"#,
552 r#" 566 vec![
567 r#"
553mod foo; 568mod foo;
554 569
555fn f() {} 570fn f() {}
556"#, 571"#,
572 r#"
573pub mod foo;
574
575fn 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
15use crate::Assist; 15use 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
20pub(crate) trait DiagnosticWithFix: Diagnostic { 20pub(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};
4use syntax::AstNode; 4use syntax::AstNode;
5 5
6use crate::{ 6use 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
11impl DiagnosticWithFix for IncorrectCase { 11impl 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::{
7use text_edit::TextEdit; 7use text_edit::TextEdit;
8 8
9use crate::{ 9use crate::{
10 diagnostics::{fix, DiagnosticWithFix}, 10 diagnostics::{fix, DiagnosticWithFixes},
11 Assist, AssistResolveStrategy, 11 Assist, AssistResolveStrategy,
12}; 12};
13 13impl DiagnosticWithFixes for NoSuchField {
14impl 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
29fn missing_record_expr_field_fix( 28fn 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};
5use text_edit::TextEdit; 5use text_edit::TextEdit;
6 6
7use crate::{ 7use crate::{
8 diagnostics::{fix, fixes::DiagnosticWithFix}, 8 diagnostics::{fix, fixes::DiagnosticWithFixes},
9 Assist, 9 Assist,
10}; 10};
11 11
12impl DiagnosticWithFix for MissingFields { 12impl 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};
4use syntax::{ast, AstNode}; 4use syntax::{ast, AstNode};
5use text_edit::TextEdit; 5use text_edit::TextEdit;
6 6
7use crate::diagnostics::{fix, DiagnosticWithFix}; 7use crate::diagnostics::{fix, DiagnosticWithFixes};
8 8
9impl DiagnosticWithFix for RemoveThisSemicolon { 9impl 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};
8use text_edit::TextEdit; 8use text_edit::TextEdit;
9 9
10use crate::diagnostics::{fix, DiagnosticWithFix}; 10use crate::diagnostics::{fix, DiagnosticWithFixes};
11 11
12impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { 12impl 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};
3use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit, RootDatabase}; 3use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit, RootDatabase};
4use syntax::AstNode; 4use syntax::AstNode;
5 5
6use crate::diagnostics::{fix, DiagnosticWithFix}; 6use crate::diagnostics::{fix, DiagnosticWithFixes};
7 7
8impl DiagnosticWithFix for UnresolvedModule { 8impl 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};
4use syntax::AstNode; 4use syntax::AstNode;
5use text_edit::TextEdit; 5use text_edit::TextEdit;
6 6
7use crate::diagnostics::{fix, DiagnosticWithFix}; 7use crate::diagnostics::{fix, DiagnosticWithFixes};
8 8
9impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { 9impl 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::{
18use text_edit::TextEdit; 18use text_edit::TextEdit;
19 19
20use crate::{ 20use 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
53impl DiagnosticWithFix for UnlinkedFile { 53impl 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
104fn make_fix( 104fn 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 {