diff options
Diffstat (limited to 'crates/ide')
27 files changed, 1518 insertions, 870 deletions
diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml index f04bcf531..88f3d09d3 100644 --- a/crates/ide/Cargo.toml +++ b/crates/ide/Cargo.toml | |||
@@ -20,6 +20,7 @@ oorandom = "11.1.2" | |||
20 | pulldown-cmark-to-cmark = "6.0.0" | 20 | pulldown-cmark-to-cmark = "6.0.0" |
21 | pulldown-cmark = { version = "0.8.0", default-features = false } | 21 | pulldown-cmark = { version = "0.8.0", default-features = false } |
22 | url = "2.1.1" | 22 | url = "2.1.1" |
23 | dot = "0.1.4" | ||
23 | 24 | ||
24 | stdx = { path = "../stdx", version = "0.0.0" } | 25 | stdx = { path = "../stdx", version = "0.0.0" } |
25 | syntax = { path = "../syntax", version = "0.0.0" } | 26 | syntax = { path = "../syntax", version = "0.0.0" } |
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index 273d8cfbb..4172f6cae 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs | |||
@@ -375,7 +375,7 @@ mod tests { | |||
375 | assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics); | 375 | assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics); |
376 | } | 376 | } |
377 | 377 | ||
378 | fn check_expect(ra_fixture: &str, expect: Expect) { | 378 | pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) { |
379 | let (analysis, file_id) = fixture::file(ra_fixture); | 379 | let (analysis, file_id) = fixture::file(ra_fixture); |
380 | let diagnostics = analysis | 380 | let diagnostics = analysis |
381 | .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id) | 381 | .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id) |
@@ -384,374 +384,6 @@ mod tests { | |||
384 | } | 384 | } |
385 | 385 | ||
386 | #[test] | 386 | #[test] |
387 | fn test_wrap_return_type_option() { | ||
388 | check_fix( | ||
389 | r#" | ||
390 | //- /main.rs crate:main deps:core | ||
391 | use core::option::Option::{self, Some, None}; | ||
392 | |||
393 | fn div(x: i32, y: i32) -> Option<i32> { | ||
394 | if y == 0 { | ||
395 | return None; | ||
396 | } | ||
397 | x / y$0 | ||
398 | } | ||
399 | //- /core/lib.rs crate:core | ||
400 | pub mod result { | ||
401 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
402 | } | ||
403 | pub mod option { | ||
404 | pub enum Option<T> { Some(T), None } | ||
405 | } | ||
406 | "#, | ||
407 | r#" | ||
408 | use core::option::Option::{self, Some, None}; | ||
409 | |||
410 | fn div(x: i32, y: i32) -> Option<i32> { | ||
411 | if y == 0 { | ||
412 | return None; | ||
413 | } | ||
414 | Some(x / y) | ||
415 | } | ||
416 | "#, | ||
417 | ); | ||
418 | } | ||
419 | |||
420 | #[test] | ||
421 | fn test_wrap_return_type() { | ||
422 | check_fix( | ||
423 | r#" | ||
424 | //- /main.rs crate:main deps:core | ||
425 | use core::result::Result::{self, Ok, Err}; | ||
426 | |||
427 | fn div(x: i32, y: i32) -> Result<i32, ()> { | ||
428 | if y == 0 { | ||
429 | return Err(()); | ||
430 | } | ||
431 | x / y$0 | ||
432 | } | ||
433 | //- /core/lib.rs crate:core | ||
434 | pub mod result { | ||
435 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
436 | } | ||
437 | pub mod option { | ||
438 | pub enum Option<T> { Some(T), None } | ||
439 | } | ||
440 | "#, | ||
441 | r#" | ||
442 | use core::result::Result::{self, Ok, Err}; | ||
443 | |||
444 | fn div(x: i32, y: i32) -> Result<i32, ()> { | ||
445 | if y == 0 { | ||
446 | return Err(()); | ||
447 | } | ||
448 | Ok(x / y) | ||
449 | } | ||
450 | "#, | ||
451 | ); | ||
452 | } | ||
453 | |||
454 | #[test] | ||
455 | fn test_wrap_return_type_handles_generic_functions() { | ||
456 | check_fix( | ||
457 | r#" | ||
458 | //- /main.rs crate:main deps:core | ||
459 | use core::result::Result::{self, Ok, Err}; | ||
460 | |||
461 | fn div<T>(x: T) -> Result<T, i32> { | ||
462 | if x == 0 { | ||
463 | return Err(7); | ||
464 | } | ||
465 | $0x | ||
466 | } | ||
467 | //- /core/lib.rs crate:core | ||
468 | pub mod result { | ||
469 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
470 | } | ||
471 | pub mod option { | ||
472 | pub enum Option<T> { Some(T), None } | ||
473 | } | ||
474 | "#, | ||
475 | r#" | ||
476 | use core::result::Result::{self, Ok, Err}; | ||
477 | |||
478 | fn div<T>(x: T) -> Result<T, i32> { | ||
479 | if x == 0 { | ||
480 | return Err(7); | ||
481 | } | ||
482 | Ok(x) | ||
483 | } | ||
484 | "#, | ||
485 | ); | ||
486 | } | ||
487 | |||
488 | #[test] | ||
489 | fn test_wrap_return_type_handles_type_aliases() { | ||
490 | check_fix( | ||
491 | r#" | ||
492 | //- /main.rs crate:main deps:core | ||
493 | use core::result::Result::{self, Ok, Err}; | ||
494 | |||
495 | type MyResult<T> = Result<T, ()>; | ||
496 | |||
497 | fn div(x: i32, y: i32) -> MyResult<i32> { | ||
498 | if y == 0 { | ||
499 | return Err(()); | ||
500 | } | ||
501 | x $0/ y | ||
502 | } | ||
503 | //- /core/lib.rs crate:core | ||
504 | pub mod result { | ||
505 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
506 | } | ||
507 | pub mod option { | ||
508 | pub enum Option<T> { Some(T), None } | ||
509 | } | ||
510 | "#, | ||
511 | r#" | ||
512 | use core::result::Result::{self, Ok, Err}; | ||
513 | |||
514 | type MyResult<T> = Result<T, ()>; | ||
515 | |||
516 | fn div(x: i32, y: i32) -> MyResult<i32> { | ||
517 | if y == 0 { | ||
518 | return Err(()); | ||
519 | } | ||
520 | Ok(x / y) | ||
521 | } | ||
522 | "#, | ||
523 | ); | ||
524 | } | ||
525 | |||
526 | #[test] | ||
527 | fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() { | ||
528 | check_no_diagnostics( | ||
529 | r#" | ||
530 | //- /main.rs crate:main deps:core | ||
531 | use core::result::Result::{self, Ok, Err}; | ||
532 | |||
533 | fn foo() -> Result<(), i32> { 0 } | ||
534 | |||
535 | //- /core/lib.rs crate:core | ||
536 | pub mod result { | ||
537 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
538 | } | ||
539 | pub mod option { | ||
540 | pub enum Option<T> { Some(T), None } | ||
541 | } | ||
542 | "#, | ||
543 | ); | ||
544 | } | ||
545 | |||
546 | #[test] | ||
547 | fn test_wrap_return_type_not_applicable_when_return_type_is_not_result_or_option() { | ||
548 | check_no_diagnostics( | ||
549 | r#" | ||
550 | //- /main.rs crate:main deps:core | ||
551 | use core::result::Result::{self, Ok, Err}; | ||
552 | |||
553 | enum SomeOtherEnum { Ok(i32), Err(String) } | ||
554 | |||
555 | fn foo() -> SomeOtherEnum { 0 } | ||
556 | |||
557 | //- /core/lib.rs crate:core | ||
558 | pub mod result { | ||
559 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
560 | } | ||
561 | pub mod option { | ||
562 | pub enum Option<T> { Some(T), None } | ||
563 | } | ||
564 | "#, | ||
565 | ); | ||
566 | } | ||
567 | |||
568 | #[test] | ||
569 | fn test_fill_struct_fields_empty() { | ||
570 | check_fix( | ||
571 | r#" | ||
572 | struct TestStruct { one: i32, two: i64 } | ||
573 | |||
574 | fn test_fn() { | ||
575 | let s = TestStruct {$0}; | ||
576 | } | ||
577 | "#, | ||
578 | r#" | ||
579 | struct TestStruct { one: i32, two: i64 } | ||
580 | |||
581 | fn test_fn() { | ||
582 | let s = TestStruct { one: (), two: ()}; | ||
583 | } | ||
584 | "#, | ||
585 | ); | ||
586 | } | ||
587 | |||
588 | #[test] | ||
589 | fn test_fill_struct_fields_self() { | ||
590 | check_fix( | ||
591 | r#" | ||
592 | struct TestStruct { one: i32 } | ||
593 | |||
594 | impl TestStruct { | ||
595 | fn test_fn() { let s = Self {$0}; } | ||
596 | } | ||
597 | "#, | ||
598 | r#" | ||
599 | struct TestStruct { one: i32 } | ||
600 | |||
601 | impl TestStruct { | ||
602 | fn test_fn() { let s = Self { one: ()}; } | ||
603 | } | ||
604 | "#, | ||
605 | ); | ||
606 | } | ||
607 | |||
608 | #[test] | ||
609 | fn test_fill_struct_fields_enum() { | ||
610 | check_fix( | ||
611 | r#" | ||
612 | enum Expr { | ||
613 | Bin { lhs: Box<Expr>, rhs: Box<Expr> } | ||
614 | } | ||
615 | |||
616 | impl Expr { | ||
617 | fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr { | ||
618 | Expr::Bin {$0 } | ||
619 | } | ||
620 | } | ||
621 | "#, | ||
622 | r#" | ||
623 | enum Expr { | ||
624 | Bin { lhs: Box<Expr>, rhs: Box<Expr> } | ||
625 | } | ||
626 | |||
627 | impl Expr { | ||
628 | fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr { | ||
629 | Expr::Bin { lhs: (), rhs: () } | ||
630 | } | ||
631 | } | ||
632 | "#, | ||
633 | ); | ||
634 | } | ||
635 | |||
636 | #[test] | ||
637 | fn test_fill_struct_fields_partial() { | ||
638 | check_fix( | ||
639 | r#" | ||
640 | struct TestStruct { one: i32, two: i64 } | ||
641 | |||
642 | fn test_fn() { | ||
643 | let s = TestStruct{ two: 2$0 }; | ||
644 | } | ||
645 | "#, | ||
646 | r" | ||
647 | struct TestStruct { one: i32, two: i64 } | ||
648 | |||
649 | fn test_fn() { | ||
650 | let s = TestStruct{ two: 2, one: () }; | ||
651 | } | ||
652 | ", | ||
653 | ); | ||
654 | } | ||
655 | |||
656 | #[test] | ||
657 | fn test_fill_struct_fields_raw_ident() { | ||
658 | check_fix( | ||
659 | r#" | ||
660 | struct TestStruct { r#type: u8 } | ||
661 | |||
662 | fn test_fn() { | ||
663 | TestStruct { $0 }; | ||
664 | } | ||
665 | "#, | ||
666 | r" | ||
667 | struct TestStruct { r#type: u8 } | ||
668 | |||
669 | fn test_fn() { | ||
670 | TestStruct { r#type: () }; | ||
671 | } | ||
672 | ", | ||
673 | ); | ||
674 | } | ||
675 | |||
676 | #[test] | ||
677 | fn test_fill_struct_fields_no_diagnostic() { | ||
678 | check_no_diagnostics( | ||
679 | r" | ||
680 | struct TestStruct { one: i32, two: i64 } | ||
681 | |||
682 | fn test_fn() { | ||
683 | let one = 1; | ||
684 | let s = TestStruct{ one, two: 2 }; | ||
685 | } | ||
686 | ", | ||
687 | ); | ||
688 | } | ||
689 | |||
690 | #[test] | ||
691 | fn test_fill_struct_fields_no_diagnostic_on_spread() { | ||
692 | check_no_diagnostics( | ||
693 | r" | ||
694 | struct TestStruct { one: i32, two: i64 } | ||
695 | |||
696 | fn test_fn() { | ||
697 | let one = 1; | ||
698 | let s = TestStruct{ ..a }; | ||
699 | } | ||
700 | ", | ||
701 | ); | ||
702 | } | ||
703 | |||
704 | #[test] | ||
705 | fn test_unresolved_module_diagnostic() { | ||
706 | check_expect( | ||
707 | r#"mod foo;"#, | ||
708 | expect![[r#" | ||
709 | [ | ||
710 | Diagnostic { | ||
711 | message: "unresolved module", | ||
712 | range: 0..8, | ||
713 | severity: Error, | ||
714 | fix: Some( | ||
715 | Assist { | ||
716 | id: AssistId( | ||
717 | "create_module", | ||
718 | QuickFix, | ||
719 | ), | ||
720 | label: "Create module", | ||
721 | group: None, | ||
722 | target: 0..8, | ||
723 | source_change: Some( | ||
724 | SourceChange { | ||
725 | source_file_edits: {}, | ||
726 | file_system_edits: [ | ||
727 | CreateFile { | ||
728 | dst: AnchoredPathBuf { | ||
729 | anchor: FileId( | ||
730 | 0, | ||
731 | ), | ||
732 | path: "foo.rs", | ||
733 | }, | ||
734 | initial_contents: "", | ||
735 | }, | ||
736 | ], | ||
737 | is_snippet: false, | ||
738 | }, | ||
739 | ), | ||
740 | }, | ||
741 | ), | ||
742 | unused: false, | ||
743 | code: Some( | ||
744 | DiagnosticCode( | ||
745 | "unresolved-module", | ||
746 | ), | ||
747 | ), | ||
748 | }, | ||
749 | ] | ||
750 | "#]], | ||
751 | ); | ||
752 | } | ||
753 | |||
754 | #[test] | ||
755 | fn test_unresolved_macro_range() { | 387 | fn test_unresolved_macro_range() { |
756 | check_expect( | 388 | check_expect( |
757 | r#"foo::bar!(92);"#, | 389 | r#"foo::bar!(92);"#, |
@@ -792,7 +424,7 @@ fn main() { | |||
792 | pub struct Foo { pub a: i32, pub b: i32 } | 424 | pub struct Foo { pub a: i32, pub b: i32 } |
793 | "#, | 425 | "#, |
794 | r#" | 426 | r#" |
795 | fn some(, b: ()) {} | 427 | fn some(, b: () ) {} |
796 | fn items() {} | 428 | fn items() {} |
797 | fn here() {} | 429 | fn here() {} |
798 | 430 | ||
@@ -891,53 +523,6 @@ mod a { | |||
891 | } | 523 | } |
892 | 524 | ||
893 | #[test] | 525 | #[test] |
894 | fn test_add_field_from_usage() { | ||
895 | check_fix( | ||
896 | r" | ||
897 | fn main() { | ||
898 | Foo { bar: 3, baz$0: false}; | ||
899 | } | ||
900 | struct Foo { | ||
901 | bar: i32 | ||
902 | } | ||
903 | ", | ||
904 | r" | ||
905 | fn main() { | ||
906 | Foo { bar: 3, baz: false}; | ||
907 | } | ||
908 | struct Foo { | ||
909 | bar: i32, | ||
910 | baz: bool | ||
911 | } | ||
912 | ", | ||
913 | ) | ||
914 | } | ||
915 | |||
916 | #[test] | ||
917 | fn test_add_field_in_other_file_from_usage() { | ||
918 | check_fix( | ||
919 | r#" | ||
920 | //- /main.rs | ||
921 | mod foo; | ||
922 | |||
923 | fn main() { | ||
924 | foo::Foo { bar: 3, $0baz: false}; | ||
925 | } | ||
926 | //- /foo.rs | ||
927 | struct Foo { | ||
928 | bar: i32 | ||
929 | } | ||
930 | "#, | ||
931 | r#" | ||
932 | struct Foo { | ||
933 | bar: i32, | ||
934 | pub(crate) baz: bool | ||
935 | } | ||
936 | "#, | ||
937 | ) | ||
938 | } | ||
939 | |||
940 | #[test] | ||
941 | fn test_disabled_diagnostics() { | 526 | fn test_disabled_diagnostics() { |
942 | let mut config = DiagnosticsConfig::default(); | 527 | let mut config = DiagnosticsConfig::default(); |
943 | config.disabled.insert("unresolved-module".into()); | 528 | config.disabled.insert("unresolved-module".into()); |
@@ -955,120 +540,6 @@ struct Foo { | |||
955 | } | 540 | } |
956 | 541 | ||
957 | #[test] | 542 | #[test] |
958 | fn test_rename_incorrect_case() { | ||
959 | check_fix( | ||
960 | r#" | ||
961 | pub struct test_struct$0 { one: i32 } | ||
962 | |||
963 | pub fn some_fn(val: test_struct) -> test_struct { | ||
964 | test_struct { one: val.one + 1 } | ||
965 | } | ||
966 | "#, | ||
967 | r#" | ||
968 | pub struct TestStruct { one: i32 } | ||
969 | |||
970 | pub fn some_fn(val: TestStruct) -> TestStruct { | ||
971 | TestStruct { one: val.one + 1 } | ||
972 | } | ||
973 | "#, | ||
974 | ); | ||
975 | |||
976 | check_fix( | ||
977 | r#" | ||
978 | pub fn some_fn(NonSnakeCase$0: u8) -> u8 { | ||
979 | NonSnakeCase | ||
980 | } | ||
981 | "#, | ||
982 | r#" | ||
983 | pub fn some_fn(non_snake_case: u8) -> u8 { | ||
984 | non_snake_case | ||
985 | } | ||
986 | "#, | ||
987 | ); | ||
988 | |||
989 | check_fix( | ||
990 | r#" | ||
991 | pub fn SomeFn$0(val: u8) -> u8 { | ||
992 | if val != 0 { SomeFn(val - 1) } else { val } | ||
993 | } | ||
994 | "#, | ||
995 | r#" | ||
996 | pub fn some_fn(val: u8) -> u8 { | ||
997 | if val != 0 { some_fn(val - 1) } else { val } | ||
998 | } | ||
999 | "#, | ||
1000 | ); | ||
1001 | |||
1002 | check_fix( | ||
1003 | r#" | ||
1004 | fn some_fn() { | ||
1005 | let whatAWeird_Formatting$0 = 10; | ||
1006 | another_func(whatAWeird_Formatting); | ||
1007 | } | ||
1008 | "#, | ||
1009 | r#" | ||
1010 | fn some_fn() { | ||
1011 | let what_a_weird_formatting = 10; | ||
1012 | another_func(what_a_weird_formatting); | ||
1013 | } | ||
1014 | "#, | ||
1015 | ); | ||
1016 | } | ||
1017 | |||
1018 | #[test] | ||
1019 | fn test_uppercase_const_no_diagnostics() { | ||
1020 | check_no_diagnostics( | ||
1021 | r#" | ||
1022 | fn foo() { | ||
1023 | const ANOTHER_ITEM$0: &str = "some_item"; | ||
1024 | } | ||
1025 | "#, | ||
1026 | ); | ||
1027 | } | ||
1028 | |||
1029 | #[test] | ||
1030 | fn test_rename_incorrect_case_struct_method() { | ||
1031 | check_fix( | ||
1032 | r#" | ||
1033 | pub struct TestStruct; | ||
1034 | |||
1035 | impl TestStruct { | ||
1036 | pub fn SomeFn$0() -> TestStruct { | ||
1037 | TestStruct | ||
1038 | } | ||
1039 | } | ||
1040 | "#, | ||
1041 | r#" | ||
1042 | pub struct TestStruct; | ||
1043 | |||
1044 | impl TestStruct { | ||
1045 | pub fn some_fn() -> TestStruct { | ||
1046 | TestStruct | ||
1047 | } | ||
1048 | } | ||
1049 | "#, | ||
1050 | ); | ||
1051 | } | ||
1052 | |||
1053 | #[test] | ||
1054 | fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() { | ||
1055 | let input = r#"fn FOO$0() {}"#; | ||
1056 | let expected = r#"fn foo() {}"#; | ||
1057 | |||
1058 | let (analysis, file_position) = fixture::position(input); | ||
1059 | let diagnostics = analysis | ||
1060 | .diagnostics( | ||
1061 | &DiagnosticsConfig::default(), | ||
1062 | AssistResolveStrategy::All, | ||
1063 | file_position.file_id, | ||
1064 | ) | ||
1065 | .unwrap(); | ||
1066 | assert_eq!(diagnostics.len(), 1); | ||
1067 | |||
1068 | check_fix(input, expected); | ||
1069 | } | ||
1070 | |||
1071 | #[test] | ||
1072 | fn unlinked_file_prepend_first_item() { | 543 | fn unlinked_file_prepend_first_item() { |
1073 | cov_mark::check!(unlinked_file_prepend_before_first_item); | 544 | cov_mark::check!(unlinked_file_prepend_before_first_item); |
1074 | check_fix( | 545 | check_fix( |
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs index 15821500f..92b3f5a2d 100644 --- a/crates/ide/src/diagnostics/fixes.rs +++ b/crates/ide/src/diagnostics/fixes.rs | |||
@@ -1,31 +1,18 @@ | |||
1 | //! Provides a way to attach fixes to the diagnostics. | 1 | //! Provides a way to attach fixes to the diagnostics. |
2 | //! The same module also has all curret custom fixes for the diagnostics implemented. | 2 | //! The same module also has all curret custom fixes for the diagnostics implemented. |
3 | use hir::{ | 3 | mod change_case; |
4 | db::AstDatabase, | 4 | mod create_field; |
5 | diagnostics::{ | 5 | mod fill_missing_fields; |
6 | Diagnostic, IncorrectCase, MissingFields, MissingOkOrSomeInTailExpr, NoSuchField, | 6 | mod remove_semicolon; |
7 | RemoveThisSemicolon, ReplaceFilterMapNextWithFindMap, UnresolvedModule, | 7 | mod replace_with_find_map; |
8 | }, | 8 | mod unresolved_module; |
9 | HasSource, HirDisplay, InFile, Semantics, VariantDef, | 9 | mod wrap_tail_expr; |
10 | }; | 10 | |
11 | use hir::{diagnostics::Diagnostic, Semantics}; | ||
11 | use ide_assists::AssistResolveStrategy; | 12 | use ide_assists::AssistResolveStrategy; |
12 | use ide_db::{ | 13 | use ide_db::RootDatabase; |
13 | base_db::{AnchoredPathBuf, FileId}, | ||
14 | source_change::{FileSystemEdit, SourceChange}, | ||
15 | RootDatabase, | ||
16 | }; | ||
17 | use syntax::{ | ||
18 | algo, | ||
19 | ast::{self, edit::IndentLevel, make, ArgListOwner}, | ||
20 | AstNode, TextRange, | ||
21 | }; | ||
22 | use text_edit::TextEdit; | ||
23 | 14 | ||
24 | use crate::{ | 15 | use crate::Assist; |
25 | diagnostics::{fix, unresolved_fix}, | ||
26 | references::rename::rename_with_semantics, | ||
27 | Assist, FilePosition, | ||
28 | }; | ||
29 | 16 | ||
30 | /// A [Diagnostic] that potentially has a fix available. | 17 | /// A [Diagnostic] that potentially has a fix available. |
31 | /// | 18 | /// |
@@ -42,256 +29,3 @@ pub(crate) trait DiagnosticWithFix: Diagnostic { | |||
42 | _resolve: &AssistResolveStrategy, | 29 | _resolve: &AssistResolveStrategy, |
43 | ) -> Option<Assist>; | 30 | ) -> Option<Assist>; |
44 | } | 31 | } |
45 | |||
46 | impl DiagnosticWithFix for UnresolvedModule { | ||
47 | fn fix( | ||
48 | &self, | ||
49 | sema: &Semantics<RootDatabase>, | ||
50 | _resolve: &AssistResolveStrategy, | ||
51 | ) -> Option<Assist> { | ||
52 | let root = sema.db.parse_or_expand(self.file)?; | ||
53 | let unresolved_module = self.decl.to_node(&root); | ||
54 | Some(fix( | ||
55 | "create_module", | ||
56 | "Create module", | ||
57 | FileSystemEdit::CreateFile { | ||
58 | dst: AnchoredPathBuf { | ||
59 | anchor: self.file.original_file(sema.db), | ||
60 | path: self.candidate.clone(), | ||
61 | }, | ||
62 | initial_contents: "".to_string(), | ||
63 | } | ||
64 | .into(), | ||
65 | unresolved_module.syntax().text_range(), | ||
66 | )) | ||
67 | } | ||
68 | } | ||
69 | |||
70 | impl DiagnosticWithFix for NoSuchField { | ||
71 | fn fix( | ||
72 | &self, | ||
73 | sema: &Semantics<RootDatabase>, | ||
74 | _resolve: &AssistResolveStrategy, | ||
75 | ) -> Option<Assist> { | ||
76 | let root = sema.db.parse_or_expand(self.file)?; | ||
77 | missing_record_expr_field_fix( | ||
78 | &sema, | ||
79 | self.file.original_file(sema.db), | ||
80 | &self.field.to_node(&root), | ||
81 | ) | ||
82 | } | ||
83 | } | ||
84 | |||
85 | impl DiagnosticWithFix for MissingFields { | ||
86 | fn fix( | ||
87 | &self, | ||
88 | sema: &Semantics<RootDatabase>, | ||
89 | _resolve: &AssistResolveStrategy, | ||
90 | ) -> Option<Assist> { | ||
91 | // Note that although we could add a diagnostics to | ||
92 | // fill the missing tuple field, e.g : | ||
93 | // `struct A(usize);` | ||
94 | // `let a = A { 0: () }` | ||
95 | // but it is uncommon usage and it should not be encouraged. | ||
96 | if self.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) { | ||
97 | return None; | ||
98 | } | ||
99 | |||
100 | let root = sema.db.parse_or_expand(self.file)?; | ||
101 | let field_list_parent = self.field_list_parent.to_node(&root); | ||
102 | let old_field_list = field_list_parent.record_expr_field_list()?; | ||
103 | let mut new_field_list = old_field_list.clone(); | ||
104 | for f in self.missed_fields.iter() { | ||
105 | let field = | ||
106 | make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit())); | ||
107 | new_field_list = new_field_list.append_field(&field); | ||
108 | } | ||
109 | |||
110 | let edit = { | ||
111 | let mut builder = TextEdit::builder(); | ||
112 | algo::diff(&old_field_list.syntax(), &new_field_list.syntax()) | ||
113 | .into_text_edit(&mut builder); | ||
114 | builder.finish() | ||
115 | }; | ||
116 | Some(fix( | ||
117 | "fill_missing_fields", | ||
118 | "Fill struct fields", | ||
119 | SourceChange::from_text_edit(self.file.original_file(sema.db), edit), | ||
120 | sema.original_range(&field_list_parent.syntax()).range, | ||
121 | )) | ||
122 | } | ||
123 | } | ||
124 | |||
125 | impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { | ||
126 | fn fix( | ||
127 | &self, | ||
128 | sema: &Semantics<RootDatabase>, | ||
129 | _resolve: &AssistResolveStrategy, | ||
130 | ) -> Option<Assist> { | ||
131 | let root = sema.db.parse_or_expand(self.file)?; | ||
132 | let tail_expr = self.expr.to_node(&root); | ||
133 | let tail_expr_range = tail_expr.syntax().text_range(); | ||
134 | let replacement = format!("{}({})", self.required, tail_expr.syntax()); | ||
135 | let edit = TextEdit::replace(tail_expr_range, replacement); | ||
136 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | ||
137 | let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; | ||
138 | Some(fix("wrap_tail_expr", name, source_change, tail_expr_range)) | ||
139 | } | ||
140 | } | ||
141 | |||
142 | impl DiagnosticWithFix for RemoveThisSemicolon { | ||
143 | fn fix( | ||
144 | &self, | ||
145 | sema: &Semantics<RootDatabase>, | ||
146 | _resolve: &AssistResolveStrategy, | ||
147 | ) -> Option<Assist> { | ||
148 | let root = sema.db.parse_or_expand(self.file)?; | ||
149 | |||
150 | let semicolon = self | ||
151 | .expr | ||
152 | .to_node(&root) | ||
153 | .syntax() | ||
154 | .parent() | ||
155 | .and_then(ast::ExprStmt::cast) | ||
156 | .and_then(|expr| expr.semicolon_token())? | ||
157 | .text_range(); | ||
158 | |||
159 | let edit = TextEdit::delete(semicolon); | ||
160 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | ||
161 | |||
162 | Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)) | ||
163 | } | ||
164 | } | ||
165 | |||
166 | impl DiagnosticWithFix for IncorrectCase { | ||
167 | fn fix( | ||
168 | &self, | ||
169 | sema: &Semantics<RootDatabase>, | ||
170 | resolve: &AssistResolveStrategy, | ||
171 | ) -> Option<Assist> { | ||
172 | let root = sema.db.parse_or_expand(self.file)?; | ||
173 | let name_node = self.ident.to_node(&root); | ||
174 | |||
175 | let name_node = InFile::new(self.file, name_node.syntax()); | ||
176 | let frange = name_node.original_file_range(sema.db); | ||
177 | let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; | ||
178 | |||
179 | let label = format!("Rename to {}", self.suggested_text); | ||
180 | let mut res = unresolved_fix("change_case", &label, frange.range); | ||
181 | if resolve.should_resolve(&res.id) { | ||
182 | let source_change = rename_with_semantics(sema, file_position, &self.suggested_text); | ||
183 | res.source_change = Some(source_change.ok().unwrap_or_default()); | ||
184 | } | ||
185 | |||
186 | Some(res) | ||
187 | } | ||
188 | } | ||
189 | |||
190 | impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { | ||
191 | fn fix( | ||
192 | &self, | ||
193 | sema: &Semantics<RootDatabase>, | ||
194 | _resolve: &AssistResolveStrategy, | ||
195 | ) -> Option<Assist> { | ||
196 | let root = sema.db.parse_or_expand(self.file)?; | ||
197 | let next_expr = self.next_expr.to_node(&root); | ||
198 | let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?; | ||
199 | |||
200 | let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?; | ||
201 | let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range(); | ||
202 | let filter_map_args = filter_map_call.arg_list()?; | ||
203 | |||
204 | let range_to_replace = | ||
205 | TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end()); | ||
206 | let replacement = format!("find_map{}", filter_map_args.syntax().text()); | ||
207 | let trigger_range = next_expr.syntax().text_range(); | ||
208 | |||
209 | let edit = TextEdit::replace(range_to_replace, replacement); | ||
210 | |||
211 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | ||
212 | |||
213 | Some(fix( | ||
214 | "replace_with_find_map", | ||
215 | "Replace filter_map(..).next() with find_map()", | ||
216 | source_change, | ||
217 | trigger_range, | ||
218 | )) | ||
219 | } | ||
220 | } | ||
221 | |||
222 | fn missing_record_expr_field_fix( | ||
223 | sema: &Semantics<RootDatabase>, | ||
224 | usage_file_id: FileId, | ||
225 | record_expr_field: &ast::RecordExprField, | ||
226 | ) -> Option<Assist> { | ||
227 | let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; | ||
228 | let def_id = sema.resolve_variant(record_lit)?; | ||
229 | let module; | ||
230 | let def_file_id; | ||
231 | let record_fields = match def_id { | ||
232 | VariantDef::Struct(s) => { | ||
233 | module = s.module(sema.db); | ||
234 | let source = s.source(sema.db)?; | ||
235 | def_file_id = source.file_id; | ||
236 | let fields = source.value.field_list()?; | ||
237 | record_field_list(fields)? | ||
238 | } | ||
239 | VariantDef::Union(u) => { | ||
240 | module = u.module(sema.db); | ||
241 | let source = u.source(sema.db)?; | ||
242 | def_file_id = source.file_id; | ||
243 | source.value.record_field_list()? | ||
244 | } | ||
245 | VariantDef::Variant(e) => { | ||
246 | module = e.module(sema.db); | ||
247 | let source = e.source(sema.db)?; | ||
248 | def_file_id = source.file_id; | ||
249 | let fields = source.value.field_list()?; | ||
250 | record_field_list(fields)? | ||
251 | } | ||
252 | }; | ||
253 | let def_file_id = def_file_id.original_file(sema.db); | ||
254 | |||
255 | let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?; | ||
256 | if new_field_type.is_unknown() { | ||
257 | return None; | ||
258 | } | ||
259 | let new_field = make::record_field( | ||
260 | None, | ||
261 | make::name(&record_expr_field.field_name()?.text()), | ||
262 | make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?), | ||
263 | ); | ||
264 | |||
265 | let last_field = record_fields.fields().last()?; | ||
266 | let last_field_syntax = last_field.syntax(); | ||
267 | let indent = IndentLevel::from_node(last_field_syntax); | ||
268 | |||
269 | let mut new_field = new_field.to_string(); | ||
270 | if usage_file_id != def_file_id { | ||
271 | new_field = format!("pub(crate) {}", new_field); | ||
272 | } | ||
273 | new_field = format!("\n{}{}", indent, new_field); | ||
274 | |||
275 | let needs_comma = !last_field_syntax.to_string().ends_with(','); | ||
276 | if needs_comma { | ||
277 | new_field = format!(",{}", new_field); | ||
278 | } | ||
279 | |||
280 | let source_change = SourceChange::from_text_edit( | ||
281 | def_file_id, | ||
282 | TextEdit::insert(last_field_syntax.text_range().end(), new_field), | ||
283 | ); | ||
284 | return Some(fix( | ||
285 | "create_field", | ||
286 | "Create field", | ||
287 | source_change, | ||
288 | record_expr_field.syntax().text_range(), | ||
289 | )); | ||
290 | |||
291 | fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { | ||
292 | match field_def_list { | ||
293 | ast::FieldList::RecordFieldList(it) => Some(it), | ||
294 | ast::FieldList::TupleFieldList(_) => None, | ||
295 | } | ||
296 | } | ||
297 | } | ||
diff --git a/crates/ide/src/diagnostics/fixes/change_case.rs b/crates/ide/src/diagnostics/fixes/change_case.rs new file mode 100644 index 000000000..80aca58a1 --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/change_case.rs | |||
@@ -0,0 +1,155 @@ | |||
1 | use hir::{db::AstDatabase, diagnostics::IncorrectCase, InFile, Semantics}; | ||
2 | use ide_assists::{Assist, AssistResolveStrategy}; | ||
3 | use ide_db::{base_db::FilePosition, RootDatabase}; | ||
4 | use syntax::AstNode; | ||
5 | |||
6 | use crate::{ | ||
7 | diagnostics::{unresolved_fix, DiagnosticWithFix}, | ||
8 | references::rename::rename_with_semantics, | ||
9 | }; | ||
10 | |||
11 | impl DiagnosticWithFix for IncorrectCase { | ||
12 | fn fix( | ||
13 | &self, | ||
14 | sema: &Semantics<RootDatabase>, | ||
15 | resolve: &AssistResolveStrategy, | ||
16 | ) -> Option<Assist> { | ||
17 | let root = sema.db.parse_or_expand(self.file)?; | ||
18 | let name_node = self.ident.to_node(&root); | ||
19 | |||
20 | let name_node = InFile::new(self.file, name_node.syntax()); | ||
21 | let frange = name_node.original_file_range(sema.db); | ||
22 | let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; | ||
23 | |||
24 | let label = format!("Rename to {}", self.suggested_text); | ||
25 | let mut res = unresolved_fix("change_case", &label, frange.range); | ||
26 | if resolve.should_resolve(&res.id) { | ||
27 | let source_change = rename_with_semantics(sema, file_position, &self.suggested_text); | ||
28 | res.source_change = Some(source_change.ok().unwrap_or_default()); | ||
29 | } | ||
30 | |||
31 | Some(res) | ||
32 | } | ||
33 | } | ||
34 | |||
35 | #[cfg(test)] | ||
36 | mod change_case { | ||
37 | use crate::{ | ||
38 | diagnostics::tests::{check_fix, check_no_diagnostics}, | ||
39 | fixture, AssistResolveStrategy, DiagnosticsConfig, | ||
40 | }; | ||
41 | |||
42 | #[test] | ||
43 | fn test_rename_incorrect_case() { | ||
44 | check_fix( | ||
45 | r#" | ||
46 | pub struct test_struct$0 { one: i32 } | ||
47 | |||
48 | pub fn some_fn(val: test_struct) -> test_struct { | ||
49 | test_struct { one: val.one + 1 } | ||
50 | } | ||
51 | "#, | ||
52 | r#" | ||
53 | pub struct TestStruct { one: i32 } | ||
54 | |||
55 | pub fn some_fn(val: TestStruct) -> TestStruct { | ||
56 | TestStruct { one: val.one + 1 } | ||
57 | } | ||
58 | "#, | ||
59 | ); | ||
60 | |||
61 | check_fix( | ||
62 | r#" | ||
63 | pub fn some_fn(NonSnakeCase$0: u8) -> u8 { | ||
64 | NonSnakeCase | ||
65 | } | ||
66 | "#, | ||
67 | r#" | ||
68 | pub fn some_fn(non_snake_case: u8) -> u8 { | ||
69 | non_snake_case | ||
70 | } | ||
71 | "#, | ||
72 | ); | ||
73 | |||
74 | check_fix( | ||
75 | r#" | ||
76 | pub fn SomeFn$0(val: u8) -> u8 { | ||
77 | if val != 0 { SomeFn(val - 1) } else { val } | ||
78 | } | ||
79 | "#, | ||
80 | r#" | ||
81 | pub fn some_fn(val: u8) -> u8 { | ||
82 | if val != 0 { some_fn(val - 1) } else { val } | ||
83 | } | ||
84 | "#, | ||
85 | ); | ||
86 | |||
87 | check_fix( | ||
88 | r#" | ||
89 | fn some_fn() { | ||
90 | let whatAWeird_Formatting$0 = 10; | ||
91 | another_func(whatAWeird_Formatting); | ||
92 | } | ||
93 | "#, | ||
94 | r#" | ||
95 | fn some_fn() { | ||
96 | let what_a_weird_formatting = 10; | ||
97 | another_func(what_a_weird_formatting); | ||
98 | } | ||
99 | "#, | ||
100 | ); | ||
101 | } | ||
102 | |||
103 | #[test] | ||
104 | fn test_uppercase_const_no_diagnostics() { | ||
105 | check_no_diagnostics( | ||
106 | r#" | ||
107 | fn foo() { | ||
108 | const ANOTHER_ITEM$0: &str = "some_item"; | ||
109 | } | ||
110 | "#, | ||
111 | ); | ||
112 | } | ||
113 | |||
114 | #[test] | ||
115 | fn test_rename_incorrect_case_struct_method() { | ||
116 | check_fix( | ||
117 | r#" | ||
118 | pub struct TestStruct; | ||
119 | |||
120 | impl TestStruct { | ||
121 | pub fn SomeFn$0() -> TestStruct { | ||
122 | TestStruct | ||
123 | } | ||
124 | } | ||
125 | "#, | ||
126 | r#" | ||
127 | pub struct TestStruct; | ||
128 | |||
129 | impl TestStruct { | ||
130 | pub fn some_fn() -> TestStruct { | ||
131 | TestStruct | ||
132 | } | ||
133 | } | ||
134 | "#, | ||
135 | ); | ||
136 | } | ||
137 | |||
138 | #[test] | ||
139 | fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() { | ||
140 | let input = r#"fn FOO$0() {}"#; | ||
141 | let expected = r#"fn foo() {}"#; | ||
142 | |||
143 | let (analysis, file_position) = fixture::position(input); | ||
144 | let diagnostics = analysis | ||
145 | .diagnostics( | ||
146 | &DiagnosticsConfig::default(), | ||
147 | AssistResolveStrategy::All, | ||
148 | file_position.file_id, | ||
149 | ) | ||
150 | .unwrap(); | ||
151 | assert_eq!(diagnostics.len(), 1); | ||
152 | |||
153 | check_fix(input, expected); | ||
154 | } | ||
155 | } | ||
diff --git a/crates/ide/src/diagnostics/fixes/create_field.rs b/crates/ide/src/diagnostics/fixes/create_field.rs new file mode 100644 index 000000000..24e0fda52 --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/create_field.rs | |||
@@ -0,0 +1,157 @@ | |||
1 | use hir::{db::AstDatabase, diagnostics::NoSuchField, HasSource, HirDisplay, Semantics}; | ||
2 | use ide_db::{base_db::FileId, source_change::SourceChange, RootDatabase}; | ||
3 | use syntax::{ | ||
4 | ast::{self, edit::IndentLevel, make}, | ||
5 | AstNode, | ||
6 | }; | ||
7 | use text_edit::TextEdit; | ||
8 | |||
9 | use crate::{ | ||
10 | diagnostics::{fix, DiagnosticWithFix}, | ||
11 | Assist, AssistResolveStrategy, | ||
12 | }; | ||
13 | |||
14 | impl DiagnosticWithFix for NoSuchField { | ||
15 | fn fix( | ||
16 | &self, | ||
17 | sema: &Semantics<RootDatabase>, | ||
18 | _resolve: &AssistResolveStrategy, | ||
19 | ) -> Option<Assist> { | ||
20 | let root = sema.db.parse_or_expand(self.file)?; | ||
21 | missing_record_expr_field_fix( | ||
22 | &sema, | ||
23 | self.file.original_file(sema.db), | ||
24 | &self.field.to_node(&root), | ||
25 | ) | ||
26 | } | ||
27 | } | ||
28 | |||
29 | fn missing_record_expr_field_fix( | ||
30 | sema: &Semantics<RootDatabase>, | ||
31 | usage_file_id: FileId, | ||
32 | record_expr_field: &ast::RecordExprField, | ||
33 | ) -> Option<Assist> { | ||
34 | let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; | ||
35 | let def_id = sema.resolve_variant(record_lit)?; | ||
36 | let module; | ||
37 | let def_file_id; | ||
38 | let record_fields = match def_id { | ||
39 | hir::VariantDef::Struct(s) => { | ||
40 | module = s.module(sema.db); | ||
41 | let source = s.source(sema.db)?; | ||
42 | def_file_id = source.file_id; | ||
43 | let fields = source.value.field_list()?; | ||
44 | record_field_list(fields)? | ||
45 | } | ||
46 | hir::VariantDef::Union(u) => { | ||
47 | module = u.module(sema.db); | ||
48 | let source = u.source(sema.db)?; | ||
49 | def_file_id = source.file_id; | ||
50 | source.value.record_field_list()? | ||
51 | } | ||
52 | hir::VariantDef::Variant(e) => { | ||
53 | module = e.module(sema.db); | ||
54 | let source = e.source(sema.db)?; | ||
55 | def_file_id = source.file_id; | ||
56 | let fields = source.value.field_list()?; | ||
57 | record_field_list(fields)? | ||
58 | } | ||
59 | }; | ||
60 | let def_file_id = def_file_id.original_file(sema.db); | ||
61 | |||
62 | let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?; | ||
63 | if new_field_type.is_unknown() { | ||
64 | return None; | ||
65 | } | ||
66 | let new_field = make::record_field( | ||
67 | None, | ||
68 | make::name(&record_expr_field.field_name()?.text()), | ||
69 | make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?), | ||
70 | ); | ||
71 | |||
72 | let last_field = record_fields.fields().last()?; | ||
73 | let last_field_syntax = last_field.syntax(); | ||
74 | let indent = IndentLevel::from_node(last_field_syntax); | ||
75 | |||
76 | let mut new_field = new_field.to_string(); | ||
77 | if usage_file_id != def_file_id { | ||
78 | new_field = format!("pub(crate) {}", new_field); | ||
79 | } | ||
80 | new_field = format!("\n{}{}", indent, new_field); | ||
81 | |||
82 | let needs_comma = !last_field_syntax.to_string().ends_with(','); | ||
83 | if needs_comma { | ||
84 | new_field = format!(",{}", new_field); | ||
85 | } | ||
86 | |||
87 | let source_change = SourceChange::from_text_edit( | ||
88 | def_file_id, | ||
89 | TextEdit::insert(last_field_syntax.text_range().end(), new_field), | ||
90 | ); | ||
91 | |||
92 | return Some(fix( | ||
93 | "create_field", | ||
94 | "Create field", | ||
95 | source_change, | ||
96 | record_expr_field.syntax().text_range(), | ||
97 | )); | ||
98 | |||
99 | fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { | ||
100 | match field_def_list { | ||
101 | ast::FieldList::RecordFieldList(it) => Some(it), | ||
102 | ast::FieldList::TupleFieldList(_) => None, | ||
103 | } | ||
104 | } | ||
105 | } | ||
106 | |||
107 | #[cfg(test)] | ||
108 | mod tests { | ||
109 | use crate::diagnostics::tests::check_fix; | ||
110 | |||
111 | #[test] | ||
112 | fn test_add_field_from_usage() { | ||
113 | check_fix( | ||
114 | r" | ||
115 | fn main() { | ||
116 | Foo { bar: 3, baz$0: false}; | ||
117 | } | ||
118 | struct Foo { | ||
119 | bar: i32 | ||
120 | } | ||
121 | ", | ||
122 | r" | ||
123 | fn main() { | ||
124 | Foo { bar: 3, baz: false}; | ||
125 | } | ||
126 | struct Foo { | ||
127 | bar: i32, | ||
128 | baz: bool | ||
129 | } | ||
130 | ", | ||
131 | ) | ||
132 | } | ||
133 | |||
134 | #[test] | ||
135 | fn test_add_field_in_other_file_from_usage() { | ||
136 | check_fix( | ||
137 | r#" | ||
138 | //- /main.rs | ||
139 | mod foo; | ||
140 | |||
141 | fn main() { | ||
142 | foo::Foo { bar: 3, $0baz: false}; | ||
143 | } | ||
144 | //- /foo.rs | ||
145 | struct Foo { | ||
146 | bar: i32 | ||
147 | } | ||
148 | "#, | ||
149 | r#" | ||
150 | struct Foo { | ||
151 | bar: i32, | ||
152 | pub(crate) baz: bool | ||
153 | } | ||
154 | "#, | ||
155 | ) | ||
156 | } | ||
157 | } | ||
diff --git a/crates/ide/src/diagnostics/fixes/fill_missing_fields.rs b/crates/ide/src/diagnostics/fixes/fill_missing_fields.rs new file mode 100644 index 000000000..37a0e37a9 --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/fill_missing_fields.rs | |||
@@ -0,0 +1,217 @@ | |||
1 | use hir::{db::AstDatabase, diagnostics::MissingFields, Semantics}; | ||
2 | use ide_assists::AssistResolveStrategy; | ||
3 | use ide_db::{source_change::SourceChange, RootDatabase}; | ||
4 | use syntax::{algo, ast::make, AstNode}; | ||
5 | use text_edit::TextEdit; | ||
6 | |||
7 | use crate::{ | ||
8 | diagnostics::{fix, fixes::DiagnosticWithFix}, | ||
9 | Assist, | ||
10 | }; | ||
11 | |||
12 | impl DiagnosticWithFix for MissingFields { | ||
13 | fn fix( | ||
14 | &self, | ||
15 | sema: &Semantics<RootDatabase>, | ||
16 | _resolve: &AssistResolveStrategy, | ||
17 | ) -> Option<Assist> { | ||
18 | // Note that although we could add a diagnostics to | ||
19 | // fill the missing tuple field, e.g : | ||
20 | // `struct A(usize);` | ||
21 | // `let a = A { 0: () }` | ||
22 | // but it is uncommon usage and it should not be encouraged. | ||
23 | if self.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) { | ||
24 | return None; | ||
25 | } | ||
26 | |||
27 | let root = sema.db.parse_or_expand(self.file)?; | ||
28 | let field_list_parent = self.field_list_parent.to_node(&root); | ||
29 | let old_field_list = field_list_parent.record_expr_field_list()?; | ||
30 | let new_field_list = old_field_list.clone_for_update(); | ||
31 | for f in self.missed_fields.iter() { | ||
32 | let field = | ||
33 | make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit())) | ||
34 | .clone_for_update(); | ||
35 | new_field_list.add_field(field); | ||
36 | } | ||
37 | |||
38 | let edit = { | ||
39 | let mut builder = TextEdit::builder(); | ||
40 | algo::diff(&old_field_list.syntax(), &new_field_list.syntax()) | ||
41 | .into_text_edit(&mut builder); | ||
42 | builder.finish() | ||
43 | }; | ||
44 | Some(fix( | ||
45 | "fill_missing_fields", | ||
46 | "Fill struct fields", | ||
47 | SourceChange::from_text_edit(self.file.original_file(sema.db), edit), | ||
48 | sema.original_range(&field_list_parent.syntax()).range, | ||
49 | )) | ||
50 | } | ||
51 | } | ||
52 | |||
53 | #[cfg(test)] | ||
54 | mod tests { | ||
55 | use crate::diagnostics::tests::{check_fix, check_no_diagnostics}; | ||
56 | |||
57 | #[test] | ||
58 | fn test_fill_struct_fields_empty() { | ||
59 | check_fix( | ||
60 | r#" | ||
61 | struct TestStruct { one: i32, two: i64 } | ||
62 | |||
63 | fn test_fn() { | ||
64 | let s = TestStruct {$0}; | ||
65 | } | ||
66 | "#, | ||
67 | r#" | ||
68 | struct TestStruct { one: i32, two: i64 } | ||
69 | |||
70 | fn test_fn() { | ||
71 | let s = TestStruct { one: (), two: () }; | ||
72 | } | ||
73 | "#, | ||
74 | ); | ||
75 | } | ||
76 | |||
77 | #[test] | ||
78 | fn test_fill_struct_fields_self() { | ||
79 | check_fix( | ||
80 | r#" | ||
81 | struct TestStruct { one: i32 } | ||
82 | |||
83 | impl TestStruct { | ||
84 | fn test_fn() { let s = Self {$0}; } | ||
85 | } | ||
86 | "#, | ||
87 | r#" | ||
88 | struct TestStruct { one: i32 } | ||
89 | |||
90 | impl TestStruct { | ||
91 | fn test_fn() { let s = Self { one: () }; } | ||
92 | } | ||
93 | "#, | ||
94 | ); | ||
95 | } | ||
96 | |||
97 | #[test] | ||
98 | fn test_fill_struct_fields_enum() { | ||
99 | check_fix( | ||
100 | r#" | ||
101 | enum Expr { | ||
102 | Bin { lhs: Box<Expr>, rhs: Box<Expr> } | ||
103 | } | ||
104 | |||
105 | impl Expr { | ||
106 | fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr { | ||
107 | Expr::Bin {$0 } | ||
108 | } | ||
109 | } | ||
110 | "#, | ||
111 | r#" | ||
112 | enum Expr { | ||
113 | Bin { lhs: Box<Expr>, rhs: Box<Expr> } | ||
114 | } | ||
115 | |||
116 | impl Expr { | ||
117 | fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr { | ||
118 | Expr::Bin { lhs: (), rhs: () } | ||
119 | } | ||
120 | } | ||
121 | "#, | ||
122 | ); | ||
123 | } | ||
124 | |||
125 | #[test] | ||
126 | fn test_fill_struct_fields_partial() { | ||
127 | check_fix( | ||
128 | r#" | ||
129 | struct TestStruct { one: i32, two: i64 } | ||
130 | |||
131 | fn test_fn() { | ||
132 | let s = TestStruct{ two: 2$0 }; | ||
133 | } | ||
134 | "#, | ||
135 | r" | ||
136 | struct TestStruct { one: i32, two: i64 } | ||
137 | |||
138 | fn test_fn() { | ||
139 | let s = TestStruct{ two: 2, one: () }; | ||
140 | } | ||
141 | ", | ||
142 | ); | ||
143 | } | ||
144 | |||
145 | #[test] | ||
146 | fn test_fill_struct_fields_raw_ident() { | ||
147 | check_fix( | ||
148 | r#" | ||
149 | struct TestStruct { r#type: u8 } | ||
150 | |||
151 | fn test_fn() { | ||
152 | TestStruct { $0 }; | ||
153 | } | ||
154 | "#, | ||
155 | r" | ||
156 | struct TestStruct { r#type: u8 } | ||
157 | |||
158 | fn test_fn() { | ||
159 | TestStruct { r#type: () }; | ||
160 | } | ||
161 | ", | ||
162 | ); | ||
163 | } | ||
164 | |||
165 | #[test] | ||
166 | fn test_fill_struct_fields_no_diagnostic() { | ||
167 | check_no_diagnostics( | ||
168 | r#" | ||
169 | struct TestStruct { one: i32, two: i64 } | ||
170 | |||
171 | fn test_fn() { | ||
172 | let one = 1; | ||
173 | let s = TestStruct{ one, two: 2 }; | ||
174 | } | ||
175 | "#, | ||
176 | ); | ||
177 | } | ||
178 | |||
179 | #[test] | ||
180 | fn test_fill_struct_fields_no_diagnostic_on_spread() { | ||
181 | check_no_diagnostics( | ||
182 | r#" | ||
183 | struct TestStruct { one: i32, two: i64 } | ||
184 | |||
185 | fn test_fn() { | ||
186 | let one = 1; | ||
187 | let s = TestStruct{ ..a }; | ||
188 | } | ||
189 | "#, | ||
190 | ); | ||
191 | } | ||
192 | |||
193 | #[test] | ||
194 | fn test_fill_struct_fields_blank_line() { | ||
195 | check_fix( | ||
196 | r#" | ||
197 | struct S { a: (), b: () } | ||
198 | |||
199 | fn f() { | ||
200 | S { | ||
201 | $0 | ||
202 | }; | ||
203 | } | ||
204 | "#, | ||
205 | r#" | ||
206 | struct S { a: (), b: () } | ||
207 | |||
208 | fn f() { | ||
209 | S { | ||
210 | a: (), | ||
211 | b: (), | ||
212 | }; | ||
213 | } | ||
214 | "#, | ||
215 | ); | ||
216 | } | ||
217 | } | ||
diff --git a/crates/ide/src/diagnostics/fixes/remove_semicolon.rs b/crates/ide/src/diagnostics/fixes/remove_semicolon.rs new file mode 100644 index 000000000..45471da41 --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/remove_semicolon.rs | |||
@@ -0,0 +1,41 @@ | |||
1 | use hir::{db::AstDatabase, diagnostics::RemoveThisSemicolon, Semantics}; | ||
2 | use ide_assists::{Assist, AssistResolveStrategy}; | ||
3 | use ide_db::{source_change::SourceChange, RootDatabase}; | ||
4 | use syntax::{ast, AstNode}; | ||
5 | use text_edit::TextEdit; | ||
6 | |||
7 | use crate::diagnostics::{fix, DiagnosticWithFix}; | ||
8 | |||
9 | impl DiagnosticWithFix for RemoveThisSemicolon { | ||
10 | fn fix( | ||
11 | &self, | ||
12 | sema: &Semantics<RootDatabase>, | ||
13 | _resolve: &AssistResolveStrategy, | ||
14 | ) -> Option<Assist> { | ||
15 | let root = sema.db.parse_or_expand(self.file)?; | ||
16 | |||
17 | let semicolon = self | ||
18 | .expr | ||
19 | .to_node(&root) | ||
20 | .syntax() | ||
21 | .parent() | ||
22 | .and_then(ast::ExprStmt::cast) | ||
23 | .and_then(|expr| expr.semicolon_token())? | ||
24 | .text_range(); | ||
25 | |||
26 | let edit = TextEdit::delete(semicolon); | ||
27 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | ||
28 | |||
29 | Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)) | ||
30 | } | ||
31 | } | ||
32 | |||
33 | #[cfg(test)] | ||
34 | mod tests { | ||
35 | use crate::diagnostics::tests::check_fix; | ||
36 | |||
37 | #[test] | ||
38 | fn remove_semicolon() { | ||
39 | check_fix(r#"fn f() -> i32 { 92$0; }"#, r#"fn f() -> i32 { 92 }"#); | ||
40 | } | ||
41 | } | ||
diff --git a/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs b/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs new file mode 100644 index 000000000..b0ef7b44a --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs | |||
@@ -0,0 +1,84 @@ | |||
1 | use hir::{db::AstDatabase, diagnostics::ReplaceFilterMapNextWithFindMap, Semantics}; | ||
2 | use ide_assists::{Assist, AssistResolveStrategy}; | ||
3 | use ide_db::{source_change::SourceChange, RootDatabase}; | ||
4 | use syntax::{ | ||
5 | ast::{self, ArgListOwner}, | ||
6 | AstNode, TextRange, | ||
7 | }; | ||
8 | use text_edit::TextEdit; | ||
9 | |||
10 | use crate::diagnostics::{fix, DiagnosticWithFix}; | ||
11 | |||
12 | impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { | ||
13 | fn fix( | ||
14 | &self, | ||
15 | sema: &Semantics<RootDatabase>, | ||
16 | _resolve: &AssistResolveStrategy, | ||
17 | ) -> Option<Assist> { | ||
18 | let root = sema.db.parse_or_expand(self.file)?; | ||
19 | let next_expr = self.next_expr.to_node(&root); | ||
20 | let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?; | ||
21 | |||
22 | let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?; | ||
23 | let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range(); | ||
24 | let filter_map_args = filter_map_call.arg_list()?; | ||
25 | |||
26 | let range_to_replace = | ||
27 | TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end()); | ||
28 | let replacement = format!("find_map{}", filter_map_args.syntax().text()); | ||
29 | let trigger_range = next_expr.syntax().text_range(); | ||
30 | |||
31 | let edit = TextEdit::replace(range_to_replace, replacement); | ||
32 | |||
33 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | ||
34 | |||
35 | Some(fix( | ||
36 | "replace_with_find_map", | ||
37 | "Replace filter_map(..).next() with find_map()", | ||
38 | source_change, | ||
39 | trigger_range, | ||
40 | )) | ||
41 | } | ||
42 | } | ||
43 | |||
44 | #[cfg(test)] | ||
45 | mod tests { | ||
46 | use crate::diagnostics::tests::check_fix; | ||
47 | |||
48 | #[test] | ||
49 | fn replace_with_wind_map() { | ||
50 | check_fix( | ||
51 | r#" | ||
52 | //- /main.rs crate:main deps:core | ||
53 | use core::iter::Iterator; | ||
54 | use core::option::Option::{self, Some, None}; | ||
55 | fn foo() { | ||
56 | let m = [1, 2, 3].iter().$0filter_map(|x| if *x == 2 { Some (4) } else { None }).next(); | ||
57 | } | ||
58 | //- /core/lib.rs crate:core | ||
59 | pub mod option { | ||
60 | pub enum Option<T> { Some(T), None } | ||
61 | } | ||
62 | pub mod iter { | ||
63 | pub trait Iterator { | ||
64 | type Item; | ||
65 | fn filter_map<B, F>(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option<B> { FilterMap } | ||
66 | fn next(&mut self) -> Option<Self::Item>; | ||
67 | } | ||
68 | pub struct FilterMap {} | ||
69 | impl Iterator for FilterMap { | ||
70 | type Item = i32; | ||
71 | fn next(&mut self) -> i32 { 7 } | ||
72 | } | ||
73 | } | ||
74 | "#, | ||
75 | r#" | ||
76 | use core::iter::Iterator; | ||
77 | use core::option::Option::{self, Some, None}; | ||
78 | fn foo() { | ||
79 | let m = [1, 2, 3].iter().find_map(|x| if *x == 2 { Some (4) } else { None }); | ||
80 | } | ||
81 | "#, | ||
82 | ) | ||
83 | } | ||
84 | } | ||
diff --git a/crates/ide/src/diagnostics/fixes/unresolved_module.rs b/crates/ide/src/diagnostics/fixes/unresolved_module.rs new file mode 100644 index 000000000..81244b293 --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/unresolved_module.rs | |||
@@ -0,0 +1,87 @@ | |||
1 | use hir::{db::AstDatabase, diagnostics::UnresolvedModule, Semantics}; | ||
2 | use ide_assists::{Assist, AssistResolveStrategy}; | ||
3 | use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit, RootDatabase}; | ||
4 | use syntax::AstNode; | ||
5 | |||
6 | use crate::diagnostics::{fix, DiagnosticWithFix}; | ||
7 | |||
8 | impl DiagnosticWithFix for UnresolvedModule { | ||
9 | fn fix( | ||
10 | &self, | ||
11 | sema: &Semantics<RootDatabase>, | ||
12 | _resolve: &AssistResolveStrategy, | ||
13 | ) -> Option<Assist> { | ||
14 | let root = sema.db.parse_or_expand(self.file)?; | ||
15 | let unresolved_module = self.decl.to_node(&root); | ||
16 | Some(fix( | ||
17 | "create_module", | ||
18 | "Create module", | ||
19 | FileSystemEdit::CreateFile { | ||
20 | dst: AnchoredPathBuf { | ||
21 | anchor: self.file.original_file(sema.db), | ||
22 | path: self.candidate.clone(), | ||
23 | }, | ||
24 | initial_contents: "".to_string(), | ||
25 | } | ||
26 | .into(), | ||
27 | unresolved_module.syntax().text_range(), | ||
28 | )) | ||
29 | } | ||
30 | } | ||
31 | |||
32 | #[cfg(test)] | ||
33 | mod tests { | ||
34 | use expect_test::expect; | ||
35 | |||
36 | use crate::diagnostics::tests::check_expect; | ||
37 | |||
38 | #[test] | ||
39 | fn test_unresolved_module_diagnostic() { | ||
40 | check_expect( | ||
41 | r#"mod foo;"#, | ||
42 | expect![[r#" | ||
43 | [ | ||
44 | Diagnostic { | ||
45 | message: "unresolved module", | ||
46 | range: 0..8, | ||
47 | severity: Error, | ||
48 | fix: Some( | ||
49 | Assist { | ||
50 | id: AssistId( | ||
51 | "create_module", | ||
52 | QuickFix, | ||
53 | ), | ||
54 | label: "Create module", | ||
55 | group: None, | ||
56 | target: 0..8, | ||
57 | source_change: Some( | ||
58 | SourceChange { | ||
59 | source_file_edits: {}, | ||
60 | file_system_edits: [ | ||
61 | CreateFile { | ||
62 | dst: AnchoredPathBuf { | ||
63 | anchor: FileId( | ||
64 | 0, | ||
65 | ), | ||
66 | path: "foo.rs", | ||
67 | }, | ||
68 | initial_contents: "", | ||
69 | }, | ||
70 | ], | ||
71 | is_snippet: false, | ||
72 | }, | ||
73 | ), | ||
74 | }, | ||
75 | ), | ||
76 | unused: false, | ||
77 | code: Some( | ||
78 | DiagnosticCode( | ||
79 | "unresolved-module", | ||
80 | ), | ||
81 | ), | ||
82 | }, | ||
83 | ] | ||
84 | "#]], | ||
85 | ); | ||
86 | } | ||
87 | } | ||
diff --git a/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs b/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs new file mode 100644 index 000000000..66676064a --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs | |||
@@ -0,0 +1,211 @@ | |||
1 | use hir::{db::AstDatabase, diagnostics::MissingOkOrSomeInTailExpr, Semantics}; | ||
2 | use ide_assists::{Assist, AssistResolveStrategy}; | ||
3 | use ide_db::{source_change::SourceChange, RootDatabase}; | ||
4 | use syntax::AstNode; | ||
5 | use text_edit::TextEdit; | ||
6 | |||
7 | use crate::diagnostics::{fix, DiagnosticWithFix}; | ||
8 | |||
9 | impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { | ||
10 | fn fix( | ||
11 | &self, | ||
12 | sema: &Semantics<RootDatabase>, | ||
13 | _resolve: &AssistResolveStrategy, | ||
14 | ) -> Option<Assist> { | ||
15 | let root = sema.db.parse_or_expand(self.file)?; | ||
16 | let tail_expr = self.expr.to_node(&root); | ||
17 | let tail_expr_range = tail_expr.syntax().text_range(); | ||
18 | let replacement = format!("{}({})", self.required, tail_expr.syntax()); | ||
19 | let edit = TextEdit::replace(tail_expr_range, replacement); | ||
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" }; | ||
22 | Some(fix("wrap_tail_expr", name, source_change, tail_expr_range)) | ||
23 | } | ||
24 | } | ||
25 | |||
26 | #[cfg(test)] | ||
27 | mod tests { | ||
28 | use crate::diagnostics::tests::{check_fix, check_no_diagnostics}; | ||
29 | |||
30 | #[test] | ||
31 | fn test_wrap_return_type_option() { | ||
32 | check_fix( | ||
33 | r#" | ||
34 | //- /main.rs crate:main deps:core | ||
35 | use core::option::Option::{self, Some, None}; | ||
36 | |||
37 | fn div(x: i32, y: i32) -> Option<i32> { | ||
38 | if y == 0 { | ||
39 | return None; | ||
40 | } | ||
41 | x / y$0 | ||
42 | } | ||
43 | //- /core/lib.rs crate:core | ||
44 | pub mod result { | ||
45 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
46 | } | ||
47 | pub mod option { | ||
48 | pub enum Option<T> { Some(T), None } | ||
49 | } | ||
50 | "#, | ||
51 | r#" | ||
52 | use core::option::Option::{self, Some, None}; | ||
53 | |||
54 | fn div(x: i32, y: i32) -> Option<i32> { | ||
55 | if y == 0 { | ||
56 | return None; | ||
57 | } | ||
58 | Some(x / y) | ||
59 | } | ||
60 | "#, | ||
61 | ); | ||
62 | } | ||
63 | |||
64 | #[test] | ||
65 | fn test_wrap_return_type() { | ||
66 | check_fix( | ||
67 | r#" | ||
68 | //- /main.rs crate:main deps:core | ||
69 | use core::result::Result::{self, Ok, Err}; | ||
70 | |||
71 | fn div(x: i32, y: i32) -> Result<i32, ()> { | ||
72 | if y == 0 { | ||
73 | return Err(()); | ||
74 | } | ||
75 | x / y$0 | ||
76 | } | ||
77 | //- /core/lib.rs crate:core | ||
78 | pub mod result { | ||
79 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
80 | } | ||
81 | pub mod option { | ||
82 | pub enum Option<T> { Some(T), None } | ||
83 | } | ||
84 | "#, | ||
85 | r#" | ||
86 | use core::result::Result::{self, Ok, Err}; | ||
87 | |||
88 | fn div(x: i32, y: i32) -> Result<i32, ()> { | ||
89 | if y == 0 { | ||
90 | return Err(()); | ||
91 | } | ||
92 | Ok(x / y) | ||
93 | } | ||
94 | "#, | ||
95 | ); | ||
96 | } | ||
97 | |||
98 | #[test] | ||
99 | fn test_wrap_return_type_handles_generic_functions() { | ||
100 | check_fix( | ||
101 | r#" | ||
102 | //- /main.rs crate:main deps:core | ||
103 | use core::result::Result::{self, Ok, Err}; | ||
104 | |||
105 | fn div<T>(x: T) -> Result<T, i32> { | ||
106 | if x == 0 { | ||
107 | return Err(7); | ||
108 | } | ||
109 | $0x | ||
110 | } | ||
111 | //- /core/lib.rs crate:core | ||
112 | pub mod result { | ||
113 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
114 | } | ||
115 | pub mod option { | ||
116 | pub enum Option<T> { Some(T), None } | ||
117 | } | ||
118 | "#, | ||
119 | r#" | ||
120 | use core::result::Result::{self, Ok, Err}; | ||
121 | |||
122 | fn div<T>(x: T) -> Result<T, i32> { | ||
123 | if x == 0 { | ||
124 | return Err(7); | ||
125 | } | ||
126 | Ok(x) | ||
127 | } | ||
128 | "#, | ||
129 | ); | ||
130 | } | ||
131 | |||
132 | #[test] | ||
133 | fn test_wrap_return_type_handles_type_aliases() { | ||
134 | check_fix( | ||
135 | r#" | ||
136 | //- /main.rs crate:main deps:core | ||
137 | use core::result::Result::{self, Ok, Err}; | ||
138 | |||
139 | type MyResult<T> = Result<T, ()>; | ||
140 | |||
141 | fn div(x: i32, y: i32) -> MyResult<i32> { | ||
142 | if y == 0 { | ||
143 | return Err(()); | ||
144 | } | ||
145 | x $0/ y | ||
146 | } | ||
147 | //- /core/lib.rs crate:core | ||
148 | pub mod result { | ||
149 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
150 | } | ||
151 | pub mod option { | ||
152 | pub enum Option<T> { Some(T), None } | ||
153 | } | ||
154 | "#, | ||
155 | r#" | ||
156 | use core::result::Result::{self, Ok, Err}; | ||
157 | |||
158 | type MyResult<T> = Result<T, ()>; | ||
159 | |||
160 | fn div(x: i32, y: i32) -> MyResult<i32> { | ||
161 | if y == 0 { | ||
162 | return Err(()); | ||
163 | } | ||
164 | Ok(x / y) | ||
165 | } | ||
166 | "#, | ||
167 | ); | ||
168 | } | ||
169 | |||
170 | #[test] | ||
171 | fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() { | ||
172 | check_no_diagnostics( | ||
173 | r#" | ||
174 | //- /main.rs crate:main deps:core | ||
175 | use core::result::Result::{self, Ok, Err}; | ||
176 | |||
177 | fn foo() -> Result<(), i32> { 0 } | ||
178 | |||
179 | //- /core/lib.rs crate:core | ||
180 | pub mod result { | ||
181 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
182 | } | ||
183 | pub mod option { | ||
184 | pub enum Option<T> { Some(T), None } | ||
185 | } | ||
186 | "#, | ||
187 | ); | ||
188 | } | ||
189 | |||
190 | #[test] | ||
191 | fn test_wrap_return_type_not_applicable_when_return_type_is_not_result_or_option() { | ||
192 | check_no_diagnostics( | ||
193 | r#" | ||
194 | //- /main.rs crate:main deps:core | ||
195 | use core::result::Result::{self, Ok, Err}; | ||
196 | |||
197 | enum SomeOtherEnum { Ok(i32), Err(String) } | ||
198 | |||
199 | fn foo() -> SomeOtherEnum { 0 } | ||
200 | |||
201 | //- /core/lib.rs crate:core | ||
202 | pub mod result { | ||
203 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
204 | } | ||
205 | pub mod option { | ||
206 | pub enum Option<T> { Some(T), None } | ||
207 | } | ||
208 | "#, | ||
209 | ); | ||
210 | } | ||
211 | } | ||
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs index cb5a8e19a..320694a17 100644 --- a/crates/ide/src/doc_links.rs +++ b/crates/ide/src/doc_links.rs | |||
@@ -29,7 +29,8 @@ pub(crate) type DocumentationLink = String; | |||
29 | /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) | 29 | /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) |
30 | pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { | 30 | pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { |
31 | let mut cb = broken_link_clone_cb; | 31 | let mut cb = broken_link_clone_cb; |
32 | let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb)); | 32 | let doc = |
33 | Parser::new_with_broken_link_callback(markdown, Options::ENABLE_TASKLISTS, Some(&mut cb)); | ||
33 | 34 | ||
34 | let doc = map_links(doc, |target, title: &str| { | 35 | let doc = map_links(doc, |target, title: &str| { |
35 | // This check is imperfect, there's some overlap between valid intra-doc links | 36 | // This check is imperfect, there's some overlap between valid intra-doc links |
@@ -64,8 +65,7 @@ pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Defi | |||
64 | pub(crate) fn remove_links(markdown: &str) -> String { | 65 | pub(crate) fn remove_links(markdown: &str) -> String { |
65 | let mut drop_link = false; | 66 | let mut drop_link = false; |
66 | 67 | ||
67 | let mut opts = Options::empty(); | 68 | let opts = Options::ENABLE_TASKLISTS | Options::ENABLE_FOOTNOTES; |
68 | opts.insert(Options::ENABLE_FOOTNOTES); | ||
69 | 69 | ||
70 | let mut cb = |_: BrokenLink| { | 70 | let mut cb = |_: BrokenLink| { |
71 | let empty = InlineStr::try_from("").unwrap(); | 71 | let empty = InlineStr::try_from("").unwrap(); |
@@ -123,7 +123,7 @@ pub(crate) fn extract_definitions_from_markdown( | |||
123 | ) -> Vec<(TextRange, String, Option<hir::Namespace>)> { | 123 | ) -> Vec<(TextRange, String, Option<hir::Namespace>)> { |
124 | Parser::new_with_broken_link_callback( | 124 | Parser::new_with_broken_link_callback( |
125 | markdown, | 125 | markdown, |
126 | Options::empty(), | 126 | Options::ENABLE_TASKLISTS, |
127 | Some(&mut broken_link_clone_cb), | 127 | Some(&mut broken_link_clone_cb), |
128 | ) | 128 | ) |
129 | .into_offset_iter() | 129 | .into_offset_iter() |
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs index e0bf660c4..960d169f4 100644 --- a/crates/ide/src/inlay_hints.rs +++ b/crates/ide/src/inlay_hints.rs | |||
@@ -1126,7 +1126,7 @@ fn main() { | |||
1126 | r#" | 1126 | r#" |
1127 | fn main() { | 1127 | fn main() { |
1128 | let data = &[1i32, 2, 3]; | 1128 | let data = &[1i32, 2, 3]; |
1129 | //^^^^ &[i32; _] | 1129 | //^^^^ &[i32; 3] |
1130 | for i | 1130 | for i |
1131 | }"#, | 1131 | }"#, |
1132 | ); | 1132 | ); |
diff --git a/crates/ide/src/join_lines.rs b/crates/ide/src/join_lines.rs index fe2a349e6..c67ccd1a9 100644 --- a/crates/ide/src/join_lines.rs +++ b/crates/ide/src/join_lines.rs | |||
@@ -1,8 +1,10 @@ | |||
1 | use std::convert::TryFrom; | ||
2 | |||
1 | use ide_assists::utils::extract_trivial_expression; | 3 | use ide_assists::utils::extract_trivial_expression; |
2 | use itertools::Itertools; | 4 | use itertools::Itertools; |
3 | use syntax::{ | 5 | use syntax::{ |
4 | algo::non_trivia_sibling, | 6 | algo::non_trivia_sibling, |
5 | ast::{self, AstNode, AstToken}, | 7 | ast::{self, AstNode, AstToken, IsString}, |
6 | Direction, NodeOrToken, SourceFile, | 8 | Direction, NodeOrToken, SourceFile, |
7 | SyntaxKind::{self, USE_TREE, WHITESPACE}, | 9 | SyntaxKind::{self, USE_TREE, WHITESPACE}, |
8 | SyntaxNode, SyntaxToken, TextRange, TextSize, T, | 10 | SyntaxNode, SyntaxToken, TextRange, TextSize, T, |
@@ -65,14 +67,6 @@ fn remove_newlines(edit: &mut TextEditBuilder, token: &SyntaxToken, range: TextR | |||
65 | 67 | ||
66 | fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextSize) { | 68 | fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextSize) { |
67 | if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 { | 69 | if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 { |
68 | let mut string_open_quote = false; | ||
69 | if let Some(string) = ast::String::cast(token.clone()) { | ||
70 | if let Some(range) = string.open_quote_text_range() { | ||
71 | cov_mark::hit!(join_string_literal); | ||
72 | string_open_quote = range.end() == offset; | ||
73 | } | ||
74 | } | ||
75 | |||
76 | let n_spaces_after_line_break = { | 70 | let n_spaces_after_line_break = { |
77 | let suff = &token.text()[TextRange::new( | 71 | let suff = &token.text()[TextRange::new( |
78 | offset - token.text_range().start() + TextSize::of('\n'), | 72 | offset - token.text_range().start() + TextSize::of('\n'), |
@@ -81,8 +75,23 @@ fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextS | |||
81 | suff.bytes().take_while(|&b| b == b' ').count() | 75 | suff.bytes().take_while(|&b| b == b' ').count() |
82 | }; | 76 | }; |
83 | 77 | ||
78 | let mut no_space = false; | ||
79 | if let Some(string) = ast::String::cast(token.clone()) { | ||
80 | if let Some(range) = string.open_quote_text_range() { | ||
81 | cov_mark::hit!(join_string_literal_open_quote); | ||
82 | no_space |= range.end() == offset; | ||
83 | } | ||
84 | if let Some(range) = string.close_quote_text_range() { | ||
85 | cov_mark::hit!(join_string_literal_close_quote); | ||
86 | no_space |= range.start() | ||
87 | == offset | ||
88 | + TextSize::of('\n') | ||
89 | + TextSize::try_from(n_spaces_after_line_break).unwrap(); | ||
90 | } | ||
91 | } | ||
92 | |||
84 | let range = TextRange::at(offset, ((n_spaces_after_line_break + 1) as u32).into()); | 93 | let range = TextRange::at(offset, ((n_spaces_after_line_break + 1) as u32).into()); |
85 | let replace_with = if string_open_quote { "" } else { " " }; | 94 | let replace_with = if no_space { "" } else { " " }; |
86 | edit.replace(range, replace_with.to_string()); | 95 | edit.replace(range, replace_with.to_string()); |
87 | return; | 96 | return; |
88 | } | 97 | } |
@@ -797,22 +806,54 @@ fn foo() { | |||
797 | 806 | ||
798 | #[test] | 807 | #[test] |
799 | fn join_string_literal() { | 808 | fn join_string_literal() { |
800 | cov_mark::check!(join_string_literal); | 809 | { |
801 | check_join_lines( | 810 | cov_mark::check!(join_string_literal_open_quote); |
802 | r#" | 811 | check_join_lines( |
812 | r#" | ||
803 | fn main() { | 813 | fn main() { |
804 | $0" | 814 | $0" |
805 | hello | 815 | hello |
806 | "; | 816 | "; |
807 | } | 817 | } |
808 | "#, | 818 | "#, |
809 | r#" | 819 | r#" |
810 | fn main() { | 820 | fn main() { |
811 | $0"hello | 821 | $0"hello |
812 | "; | 822 | "; |
813 | } | 823 | } |
814 | "#, | 824 | "#, |
815 | ); | 825 | ); |
826 | } | ||
827 | |||
828 | { | ||
829 | cov_mark::check!(join_string_literal_close_quote); | ||
830 | check_join_lines( | ||
831 | r#" | ||
832 | fn main() { | ||
833 | $0"hello | ||
834 | "; | ||
835 | } | ||
836 | "#, | ||
837 | r#" | ||
838 | fn main() { | ||
839 | $0"hello"; | ||
840 | } | ||
841 | "#, | ||
842 | ); | ||
843 | check_join_lines( | ||
844 | r#" | ||
845 | fn main() { | ||
846 | $0r"hello | ||
847 | "; | ||
848 | } | ||
849 | "#, | ||
850 | r#" | ||
851 | fn main() { | ||
852 | $0r"hello"; | ||
853 | } | ||
854 | "#, | ||
855 | ); | ||
856 | } | ||
816 | 857 | ||
817 | check_join_lines( | 858 | check_join_lines( |
818 | r#" | 859 | r#" |
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 8e5b72044..db08547d1 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs | |||
@@ -49,6 +49,7 @@ mod syntax_tree; | |||
49 | mod typing; | 49 | mod typing; |
50 | mod markdown_remove; | 50 | mod markdown_remove; |
51 | mod doc_links; | 51 | mod doc_links; |
52 | mod view_crate_graph; | ||
52 | 53 | ||
53 | use std::sync::Arc; | 54 | use std::sync::Arc; |
54 | 55 | ||
@@ -287,6 +288,11 @@ impl Analysis { | |||
287 | self.with_db(|db| view_hir::view_hir(&db, position)) | 288 | self.with_db(|db| view_hir::view_hir(&db, position)) |
288 | } | 289 | } |
289 | 290 | ||
291 | /// Renders the crate graph to GraphViz "dot" syntax. | ||
292 | pub fn view_crate_graph(&self) -> Cancelable<Result<String, String>> { | ||
293 | self.with_db(|db| view_crate_graph::view_crate_graph(&db)) | ||
294 | } | ||
295 | |||
290 | pub fn expand_macro(&self, position: FilePosition) -> Cancelable<Option<ExpandedMacro>> { | 296 | pub fn expand_macro(&self, position: FilePosition) -> Cancelable<Option<ExpandedMacro>> { |
291 | self.with_db(|db| expand_macro::expand_macro(db, position)) | 297 | self.with_db(|db| expand_macro::expand_macro(db, position)) |
292 | } | 298 | } |
diff --git a/crates/ide/src/matching_brace.rs b/crates/ide/src/matching_brace.rs index 261dcc255..011c8cc55 100644 --- a/crates/ide/src/matching_brace.rs +++ b/crates/ide/src/matching_brace.rs | |||
@@ -19,14 +19,10 @@ use syntax::{ | |||
19 | pub(crate) fn matching_brace(file: &SourceFile, offset: TextSize) -> Option<TextSize> { | 19 | pub(crate) fn matching_brace(file: &SourceFile, offset: TextSize) -> Option<TextSize> { |
20 | const BRACES: &[SyntaxKind] = | 20 | const BRACES: &[SyntaxKind] = |
21 | &[T!['{'], T!['}'], T!['['], T![']'], T!['('], T![')'], T![<], T![>], T![|], T![|]]; | 21 | &[T!['{'], T!['}'], T!['['], T![']'], T!['('], T![')'], T![<], T![>], T![|], T![|]]; |
22 | let (brace_token, brace_idx) = file | 22 | let (brace_token, brace_idx) = file.syntax().token_at_offset(offset).find_map(|node| { |
23 | .syntax() | 23 | let idx = BRACES.iter().position(|&brace| brace == node.kind())?; |
24 | .token_at_offset(offset) | 24 | Some((node, idx)) |
25 | .filter_map(|node| { | 25 | })?; |
26 | let idx = BRACES.iter().position(|&brace| brace == node.kind())?; | ||
27 | Some((node, idx)) | ||
28 | }) | ||
29 | .next()?; | ||
30 | let parent = brace_token.parent()?; | 26 | let parent = brace_token.parent()?; |
31 | if brace_token.kind() == T![|] && !ast::ParamList::can_cast(parent.kind()) { | 27 | if brace_token.kind() == T![|] && !ast::ParamList::can_cast(parent.kind()) { |
32 | cov_mark::hit!(pipes_not_braces); | 28 | cov_mark::hit!(pipes_not_braces); |
diff --git a/crates/ide/src/parent_module.rs b/crates/ide/src/parent_module.rs index 99365c8a7..9b1f48044 100644 --- a/crates/ide/src/parent_module.rs +++ b/crates/ide/src/parent_module.rs | |||
@@ -1,6 +1,8 @@ | |||
1 | use hir::Semantics; | 1 | use hir::Semantics; |
2 | use ide_db::base_db::{CrateId, FileId, FilePosition}; | 2 | use ide_db::{ |
3 | use ide_db::RootDatabase; | 3 | base_db::{CrateId, FileId, FilePosition}, |
4 | RootDatabase, | ||
5 | }; | ||
4 | use itertools::Itertools; | 6 | use itertools::Itertools; |
5 | use syntax::{ | 7 | use syntax::{ |
6 | algo::find_node_at_offset, | 8 | algo::find_node_at_offset, |
diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 11ca7ec6b..ae492a264 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs | |||
@@ -65,7 +65,7 @@ pub(crate) fn find_all_refs( | |||
65 | (find_def(&sema, &syntax, position)?, false) | 65 | (find_def(&sema, &syntax, position)?, false) |
66 | }; | 66 | }; |
67 | 67 | ||
68 | let mut usages = def.usages(sema).set_scope(search_scope).all(); | 68 | let mut usages = def.usages(sema).set_scope(search_scope).include_self_refs().all(); |
69 | if is_literal_search { | 69 | if is_literal_search { |
70 | // filter for constructor-literals | 70 | // filter for constructor-literals |
71 | let refs = usages.references.values_mut(); | 71 | let refs = usages.references.values_mut(); |
@@ -1163,22 +1163,76 @@ fn foo<const FOO$0: usize>() -> usize { | |||
1163 | } | 1163 | } |
1164 | 1164 | ||
1165 | #[test] | 1165 | #[test] |
1166 | fn test_find_self_ty_in_trait_def() { | 1166 | fn test_trait() { |
1167 | check( | 1167 | check( |
1168 | r#" | 1168 | r#" |
1169 | trait Foo { | 1169 | trait Foo$0 where Self: {} |
1170 | fn f() -> Self$0; | 1170 | |
1171 | impl Foo for () {} | ||
1172 | "#, | ||
1173 | expect![[r#" | ||
1174 | Foo Trait FileId(0) 0..24 6..9 | ||
1175 | |||
1176 | FileId(0) 31..34 | ||
1177 | "#]], | ||
1178 | ); | ||
1179 | } | ||
1180 | |||
1181 | #[test] | ||
1182 | fn test_trait_self() { | ||
1183 | check( | ||
1184 | r#" | ||
1185 | trait Foo where Self$0 { | ||
1186 | fn f() -> Self; | ||
1171 | } | 1187 | } |
1188 | |||
1189 | impl Foo for () {} | ||
1172 | "#, | 1190 | "#, |
1173 | expect![[r#" | 1191 | expect![[r#" |
1174 | Self TypeParam FileId(0) 6..9 6..9 | 1192 | Self TypeParam FileId(0) 6..9 6..9 |
1175 | 1193 | ||
1176 | FileId(0) 26..30 | 1194 | FileId(0) 16..20 |
1195 | FileId(0) 37..41 | ||
1177 | "#]], | 1196 | "#]], |
1178 | ); | 1197 | ); |
1179 | } | 1198 | } |
1180 | 1199 | ||
1181 | #[test] | 1200 | #[test] |
1201 | fn test_self_ty() { | ||
1202 | check( | ||
1203 | r#" | ||
1204 | struct $0Foo; | ||
1205 | |||
1206 | impl Foo where Self: { | ||
1207 | fn f() -> Self; | ||
1208 | } | ||
1209 | "#, | ||
1210 | expect![[r#" | ||
1211 | Foo Struct FileId(0) 0..11 7..10 | ||
1212 | |||
1213 | FileId(0) 18..21 | ||
1214 | FileId(0) 28..32 | ||
1215 | FileId(0) 50..54 | ||
1216 | "#]], | ||
1217 | ); | ||
1218 | check( | ||
1219 | r#" | ||
1220 | struct Foo; | ||
1221 | |||
1222 | impl Foo where Self: { | ||
1223 | fn f() -> Self$0; | ||
1224 | } | ||
1225 | "#, | ||
1226 | expect![[r#" | ||
1227 | impl Impl FileId(0) 13..57 18..21 | ||
1228 | |||
1229 | FileId(0) 18..21 | ||
1230 | FileId(0) 28..32 | ||
1231 | FileId(0) 50..54 | ||
1232 | "#]], | ||
1233 | ); | ||
1234 | } | ||
1235 | #[test] | ||
1182 | fn test_self_variant_with_payload() { | 1236 | fn test_self_variant_with_payload() { |
1183 | check( | 1237 | check( |
1184 | r#" | 1238 | r#" |
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs index 175e7a31d..2bf953305 100644 --- a/crates/ide/src/references/rename.rs +++ b/crates/ide/src/references/rename.rs | |||
@@ -1888,4 +1888,21 @@ impl Foo { | |||
1888 | "error: Cannot rename `Self`", | 1888 | "error: Cannot rename `Self`", |
1889 | ); | 1889 | ); |
1890 | } | 1890 | } |
1891 | |||
1892 | #[test] | ||
1893 | fn test_rename_ignores_self_ty() { | ||
1894 | check( | ||
1895 | "Fo0", | ||
1896 | r#" | ||
1897 | struct $0Foo; | ||
1898 | |||
1899 | impl Foo where Self: {} | ||
1900 | "#, | ||
1901 | r#" | ||
1902 | struct Fo0; | ||
1903 | |||
1904 | impl Fo0 where Self: {} | ||
1905 | "#, | ||
1906 | ); | ||
1907 | } | ||
1891 | } | 1908 | } |
diff --git a/crates/ide/src/syntax_highlighting/highlight.rs b/crates/ide/src/syntax_highlighting/highlight.rs index b586dcc17..baed8e217 100644 --- a/crates/ide/src/syntax_highlighting/highlight.rs +++ b/crates/ide/src/syntax_highlighting/highlight.rs | |||
@@ -227,8 +227,8 @@ pub(super) fn element( | |||
227 | k if k.is_keyword() => { | 227 | k if k.is_keyword() => { |
228 | let h = Highlight::new(HlTag::Keyword); | 228 | let h = Highlight::new(HlTag::Keyword); |
229 | match k { | 229 | match k { |
230 | T![await] | 230 | T![await] => h | HlMod::Async | HlMod::ControlFlow, |
231 | | T![break] | 231 | T![break] |
232 | | T![continue] | 232 | | T![continue] |
233 | | T![else] | 233 | | T![else] |
234 | | T![if] | 234 | | T![if] |
@@ -255,6 +255,7 @@ pub(super) fn element( | |||
255 | }) | 255 | }) |
256 | .map(|modifier| h | modifier) | 256 | .map(|modifier| h | modifier) |
257 | .unwrap_or(h), | 257 | .unwrap_or(h), |
258 | T![async] => h | HlMod::Async, | ||
258 | _ => h, | 259 | _ => h, |
259 | } | 260 | } |
260 | } | 261 | } |
@@ -310,6 +311,9 @@ fn highlight_def(db: &RootDatabase, def: Definition) -> Highlight { | |||
310 | if func.is_unsafe(db) { | 311 | if func.is_unsafe(db) { |
311 | h |= HlMod::Unsafe; | 312 | h |= HlMod::Unsafe; |
312 | } | 313 | } |
314 | if func.is_async(db) { | ||
315 | h |= HlMod::Async; | ||
316 | } | ||
313 | return h; | 317 | return h; |
314 | } | 318 | } |
315 | hir::ModuleDef::Adt(hir::Adt::Struct(_)) => HlTag::Symbol(SymbolKind::Struct), | 319 | hir::ModuleDef::Adt(hir::Adt::Struct(_)) => HlTag::Symbol(SymbolKind::Struct), |
@@ -409,6 +413,9 @@ fn highlight_method_call( | |||
409 | if func.is_unsafe(sema.db) || sema.is_unsafe_method_call(&method_call) { | 413 | if func.is_unsafe(sema.db) || sema.is_unsafe_method_call(&method_call) { |
410 | h |= HlMod::Unsafe; | 414 | h |= HlMod::Unsafe; |
411 | } | 415 | } |
416 | if func.is_async(sema.db) { | ||
417 | h |= HlMod::Async; | ||
418 | } | ||
412 | if func.as_assoc_item(sema.db).and_then(|it| it.containing_trait(sema.db)).is_some() { | 419 | if func.as_assoc_item(sema.db).and_then(|it| it.containing_trait(sema.db)).is_some() { |
413 | h |= HlMod::Trait | 420 | h |= HlMod::Trait |
414 | } | 421 | } |
diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs index bc221d599..4269d339e 100644 --- a/crates/ide/src/syntax_highlighting/inject.rs +++ b/crates/ide/src/syntax_highlighting/inject.rs | |||
@@ -6,7 +6,7 @@ use either::Either; | |||
6 | use hir::{InFile, Semantics}; | 6 | use hir::{InFile, Semantics}; |
7 | use ide_db::{call_info::ActiveParameter, helpers::rust_doc::is_rust_fence, SymbolKind}; | 7 | use ide_db::{call_info::ActiveParameter, helpers::rust_doc::is_rust_fence, SymbolKind}; |
8 | use syntax::{ | 8 | use syntax::{ |
9 | ast::{self, AstNode}, | 9 | ast::{self, AstNode, IsString}, |
10 | AstToken, NodeOrToken, SyntaxNode, SyntaxToken, TextRange, TextSize, | 10 | AstToken, NodeOrToken, SyntaxNode, SyntaxToken, TextRange, TextSize, |
11 | }; | 11 | }; |
12 | 12 | ||
diff --git a/crates/ide/src/syntax_highlighting/tags.rs b/crates/ide/src/syntax_highlighting/tags.rs index a304b3250..f4a2e7506 100644 --- a/crates/ide/src/syntax_highlighting/tags.rs +++ b/crates/ide/src/syntax_highlighting/tags.rs | |||
@@ -65,6 +65,8 @@ pub enum HlMod { | |||
65 | Static, | 65 | Static, |
66 | /// Used for items in traits and trait impls. | 66 | /// Used for items in traits and trait impls. |
67 | Trait, | 67 | Trait, |
68 | /// Used with keywords like `async` and `await`. | ||
69 | Async, | ||
68 | // Keep this last! | 70 | // Keep this last! |
69 | /// Used for unsafe functions, mutable statics, union accesses and unsafe operations. | 71 | /// Used for unsafe functions, mutable statics, union accesses and unsafe operations. |
70 | Unsafe, | 72 | Unsafe, |
@@ -186,6 +188,7 @@ impl HlMod { | |||
186 | HlMod::Mutable, | 188 | HlMod::Mutable, |
187 | HlMod::Static, | 189 | HlMod::Static, |
188 | HlMod::Trait, | 190 | HlMod::Trait, |
191 | HlMod::Async, | ||
189 | HlMod::Unsafe, | 192 | HlMod::Unsafe, |
190 | ]; | 193 | ]; |
191 | 194 | ||
@@ -203,6 +206,7 @@ impl HlMod { | |||
203 | HlMod::Mutable => "mutable", | 206 | HlMod::Mutable => "mutable", |
204 | HlMod::Static => "static", | 207 | HlMod::Static => "static", |
205 | HlMod::Trait => "trait", | 208 | HlMod::Trait => "trait", |
209 | HlMod::Async => "async", | ||
206 | HlMod::Unsafe => "unsafe", | 210 | HlMod::Unsafe => "unsafe", |
207 | } | 211 | } |
208 | } | 212 | } |
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html b/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html index 8d83ba206..921a956e6 100644 --- a/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html +++ b/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html | |||
@@ -37,13 +37,25 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd | |||
37 | 37 | ||
38 | .unresolved_reference { color: #FC5555; text-decoration: wavy underline; } | 38 | .unresolved_reference { color: #FC5555; text-decoration: wavy underline; } |
39 | </style> | 39 | </style> |
40 | <pre><code><span class="comment documentation">/// ```</span> | 40 | <pre><code><span class="comment documentation">//! This is a module to test doc injection.</span> |
41 | <span class="comment documentation">//! ```</span> | ||
42 | <span class="comment documentation">//! </span><span class="keyword injected">fn</span><span class="none injected"> </span><span class="function declaration injected">test</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="none injected"> </span><span class="brace injected">{</span><span class="brace injected">}</span> | ||
43 | <span class="comment documentation">//! ```</span> | ||
44 | |||
45 | <span class="comment documentation">/// ```</span> | ||
41 | <span class="comment documentation">/// </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="string_literal injected">"early doctests should not go boom"</span><span class="semicolon injected">;</span> | 46 | <span class="comment documentation">/// </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="string_literal injected">"early doctests should not go boom"</span><span class="semicolon injected">;</span> |
42 | <span class="comment documentation">/// ```</span> | 47 | <span class="comment documentation">/// ```</span> |
43 | <span class="keyword">struct</span> <span class="struct declaration">Foo</span> <span class="brace">{</span> | 48 | <span class="keyword">struct</span> <span class="struct declaration">Foo</span> <span class="brace">{</span> |
44 | <span class="field declaration">bar</span><span class="colon">:</span> <span class="builtin_type">bool</span><span class="comma">,</span> | 49 | <span class="field declaration">bar</span><span class="colon">:</span> <span class="builtin_type">bool</span><span class="comma">,</span> |
45 | <span class="brace">}</span> | 50 | <span class="brace">}</span> |
46 | 51 | ||
52 | <span class="comment documentation">/// This is an impl with a code block.</span> | ||
53 | <span class="comment documentation">///</span> | ||
54 | <span class="comment documentation">/// ```</span> | ||
55 | <span class="comment documentation">/// </span><span class="keyword injected">fn</span><span class="none injected"> </span><span class="function declaration injected">foo</span><span class="parenthesis injected">(</span><span class="parenthesis injected">)</span><span class="none injected"> </span><span class="brace injected">{</span> | ||
56 | <span class="comment documentation">///</span> | ||
57 | <span class="comment documentation">/// </span><span class="brace injected">}</span> | ||
58 | <span class="comment documentation">/// ```</span> | ||
47 | <span class="keyword">impl</span> <span class="struct">Foo</span> <span class="brace">{</span> | 59 | <span class="keyword">impl</span> <span class="struct">Foo</span> <span class="brace">{</span> |
48 | <span class="comment documentation">/// ```</span> | 60 | <span class="comment documentation">/// ```</span> |
49 | <span class="comment documentation">/// </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="string_literal injected">"Call me</span> | 61 | <span class="comment documentation">/// </span><span class="keyword injected">let</span><span class="none injected"> </span><span class="punctuation injected">_</span><span class="none injected"> </span><span class="operator injected">=</span><span class="none injected"> </span><span class="string_literal injected">"Call me</span> |
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlighting.html b/crates/ide/src/syntax_highlighting/test_data/highlighting.html index df4192194..0d325f3f3 100644 --- a/crates/ide/src/syntax_highlighting/test_data/highlighting.html +++ b/crates/ide/src/syntax_highlighting/test_data/highlighting.html | |||
@@ -66,11 +66,11 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd | |||
66 | <span class="keyword">pub</span> <span class="field declaration">y</span><span class="colon">:</span> <span class="builtin_type">i32</span><span class="comma">,</span> | 66 | <span class="keyword">pub</span> <span class="field declaration">y</span><span class="colon">:</span> <span class="builtin_type">i32</span><span class="comma">,</span> |
67 | <span class="brace">}</span> | 67 | <span class="brace">}</span> |
68 | 68 | ||
69 | <span class="keyword">trait</span> <span class="trait declaration">Bar</span> <span class="brace">{</span> | 69 | <span class="keyword">trait</span> <span class="trait declaration">Bar</span> <span class="keyword">where</span> <span class="type_param">Self</span><span class="colon">:</span> <span class="brace">{</span> |
70 | <span class="keyword">fn</span> <span class="function associated declaration trait">bar</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration">self</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">i32</span><span class="semicolon">;</span> | 70 | <span class="keyword">fn</span> <span class="function associated declaration trait">bar</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration">self</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">i32</span><span class="semicolon">;</span> |
71 | <span class="brace">}</span> | 71 | <span class="brace">}</span> |
72 | 72 | ||
73 | <span class="keyword">impl</span> <span class="trait">Bar</span> <span class="keyword">for</span> <span class="struct">Foo</span> <span class="brace">{</span> | 73 | <span class="keyword">impl</span> <span class="trait">Bar</span> <span class="keyword">for</span> <span class="struct">Foo</span> <span class="keyword">where</span> <span class="self_type">Self</span><span class="colon">:</span> <span class="brace">{</span> |
74 | <span class="keyword">fn</span> <span class="function associated declaration trait">bar</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration">self</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">i32</span> <span class="brace">{</span> | 74 | <span class="keyword">fn</span> <span class="function associated declaration trait">bar</span><span class="parenthesis">(</span><span class="operator">&</span><span class="self_keyword declaration">self</span><span class="parenthesis">)</span> <span class="operator">-></span> <span class="builtin_type">i32</span> <span class="brace">{</span> |
75 | <span class="self_keyword">self</span><span class="operator">.</span><span class="field">x</span> | 75 | <span class="self_keyword">self</span><span class="operator">.</span><span class="field">x</span> |
76 | <span class="brace">}</span> | 76 | <span class="brace">}</span> |
@@ -234,4 +234,15 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd | |||
234 | <span class="variable declaration">Nope</span> <span class="operator">=></span> <span class="variable">Nope</span><span class="comma">,</span> | 234 | <span class="variable declaration">Nope</span> <span class="operator">=></span> <span class="variable">Nope</span><span class="comma">,</span> |
235 | <span class="brace">}</span> | 235 | <span class="brace">}</span> |
236 | <span class="brace">}</span> | 236 | <span class="brace">}</span> |
237 | <span class="brace">}</span> | ||
238 | |||
239 | <span class="keyword async">async</span> <span class="keyword">fn</span> <span class="function declaration async">learn_and_sing</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> | ||
240 | <span class="keyword">let</span> <span class="variable declaration">song</span> <span class="operator">=</span> <span class="unresolved_reference">learn_song</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="operator">.</span><span class="keyword control async">await</span><span class="semicolon">;</span> | ||
241 | <span class="unresolved_reference">sing_song</span><span class="parenthesis">(</span><span class="variable consuming">song</span><span class="parenthesis">)</span><span class="operator">.</span><span class="keyword control async">await</span><span class="semicolon">;</span> | ||
242 | <span class="brace">}</span> | ||
243 | |||
244 | <span class="keyword async">async</span> <span class="keyword">fn</span> <span class="function declaration async">async_main</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span> | ||
245 | <span class="keyword">let</span> <span class="variable declaration">f1</span> <span class="operator">=</span> <span class="function async">learn_and_sing</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> | ||
246 | <span class="keyword">let</span> <span class="variable declaration">f2</span> <span class="operator">=</span> <span class="unresolved_reference">dance</span><span class="parenthesis">(</span><span class="parenthesis">)</span><span class="semicolon">;</span> | ||
247 | futures::<span class="macro">join!</span><span class="parenthesis">(</span>f1<span class="comma">,</span> f2<span class="parenthesis">)</span><span class="semicolon">;</span> | ||
237 | <span class="brace">}</span></code></pre> \ No newline at end of file | 248 | <span class="brace">}</span></code></pre> \ No newline at end of file |
diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs index b6e952b08..8c8878d36 100644 --- a/crates/ide/src/syntax_highlighting/tests.rs +++ b/crates/ide/src/syntax_highlighting/tests.rs | |||
@@ -39,11 +39,11 @@ struct Foo { | |||
39 | pub y: i32, | 39 | pub y: i32, |
40 | } | 40 | } |
41 | 41 | ||
42 | trait Bar { | 42 | trait Bar where Self: { |
43 | fn bar(&self) -> i32; | 43 | fn bar(&self) -> i32; |
44 | } | 44 | } |
45 | 45 | ||
46 | impl Bar for Foo { | 46 | impl Bar for Foo where Self: { |
47 | fn bar(&self) -> i32 { | 47 | fn bar(&self) -> i32 { |
48 | self.x | 48 | self.x |
49 | } | 49 | } |
@@ -208,6 +208,17 @@ impl<T> Option<T> { | |||
208 | } | 208 | } |
209 | } | 209 | } |
210 | } | 210 | } |
211 | |||
212 | async fn learn_and_sing() { | ||
213 | let song = learn_song().await; | ||
214 | sing_song(song).await; | ||
215 | } | ||
216 | |||
217 | async fn async_main() { | ||
218 | let f1 = learn_and_sing(); | ||
219 | let f2 = dance(); | ||
220 | futures::join!(f1, f2); | ||
221 | } | ||
211 | "# | 222 | "# |
212 | .trim(), | 223 | .trim(), |
213 | expect_file!["./test_data/highlighting.html"], | 224 | expect_file!["./test_data/highlighting.html"], |
@@ -513,6 +524,11 @@ fn main() { | |||
513 | fn test_highlight_doc_comment() { | 524 | fn test_highlight_doc_comment() { |
514 | check_highlighting( | 525 | check_highlighting( |
515 | r#" | 526 | r#" |
527 | //! This is a module to test doc injection. | ||
528 | //! ``` | ||
529 | //! fn test() {} | ||
530 | //! ``` | ||
531 | |||
516 | /// ``` | 532 | /// ``` |
517 | /// let _ = "early doctests should not go boom"; | 533 | /// let _ = "early doctests should not go boom"; |
518 | /// ``` | 534 | /// ``` |
@@ -520,6 +536,13 @@ struct Foo { | |||
520 | bar: bool, | 536 | bar: bool, |
521 | } | 537 | } |
522 | 538 | ||
539 | /// This is an impl with a code block. | ||
540 | /// | ||
541 | /// ``` | ||
542 | /// fn foo() { | ||
543 | /// | ||
544 | /// } | ||
545 | /// ``` | ||
523 | impl Foo { | 546 | impl Foo { |
524 | /// ``` | 547 | /// ``` |
525 | /// let _ = "Call me | 548 | /// let _ = "Call me |
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index 82c732390..4ad49eca0 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs | |||
@@ -88,7 +88,7 @@ fn on_char_typed_inner( | |||
88 | } | 88 | } |
89 | 89 | ||
90 | /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a | 90 | /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a |
91 | /// block. | 91 | /// block, or a part of a `use` item. |
92 | fn on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option<TextEdit> { | 92 | fn on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option<TextEdit> { |
93 | if !stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{')) { | 93 | if !stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{')) { |
94 | return None; | 94 | return None; |
@@ -99,30 +99,59 @@ fn on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option< | |||
99 | // Remove the `{` to get a better parse tree, and reparse | 99 | // Remove the `{` to get a better parse tree, and reparse |
100 | let file = file.reparse(&Indel::delete(brace_token.text_range())); | 100 | let file = file.reparse(&Indel::delete(brace_token.text_range())); |
101 | 101 | ||
102 | let mut expr: ast::Expr = find_node_at_offset(file.tree().syntax(), offset)?; | 102 | if let Some(edit) = brace_expr(&file.tree(), offset) { |
103 | if expr.syntax().text_range().start() != offset { | 103 | return Some(edit); |
104 | return None; | ||
105 | } | 104 | } |
106 | 105 | ||
107 | // Enclose the outermost expression starting at `offset` | 106 | if let Some(edit) = brace_use_path(&file.tree(), offset) { |
108 | while let Some(parent) = expr.syntax().parent() { | 107 | return Some(edit); |
109 | if parent.text_range().start() != expr.syntax().text_range().start() { | 108 | } |
110 | break; | ||
111 | } | ||
112 | 109 | ||
113 | match ast::Expr::cast(parent) { | 110 | return None; |
114 | Some(parent) => expr = parent, | 111 | |
115 | None => break, | 112 | fn brace_use_path(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { |
113 | let segment: ast::PathSegment = find_node_at_offset(file.syntax(), offset)?; | ||
114 | if segment.syntax().text_range().start() != offset { | ||
115 | return None; | ||
116 | } | 116 | } |
117 | } | ||
118 | 117 | ||
119 | // If it's a statement in a block, we don't know how many statements should be included | 118 | let tree: ast::UseTree = find_node_at_offset(file.syntax(), offset)?; |
120 | if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) { | 119 | |
121 | return None; | 120 | Some(TextEdit::insert( |
121 | tree.syntax().text_range().end() + TextSize::of("{"), | ||
122 | "}".to_string(), | ||
123 | )) | ||
122 | } | 124 | } |
123 | 125 | ||
124 | // Insert `}` right after the expression. | 126 | fn brace_expr(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { |
125 | Some(TextEdit::insert(expr.syntax().text_range().end() + TextSize::of("{"), "}".to_string())) | 127 | let mut expr: ast::Expr = find_node_at_offset(file.syntax(), offset)?; |
128 | if expr.syntax().text_range().start() != offset { | ||
129 | return None; | ||
130 | } | ||
131 | |||
132 | // Enclose the outermost expression starting at `offset` | ||
133 | while let Some(parent) = expr.syntax().parent() { | ||
134 | if parent.text_range().start() != expr.syntax().text_range().start() { | ||
135 | break; | ||
136 | } | ||
137 | |||
138 | match ast::Expr::cast(parent) { | ||
139 | Some(parent) => expr = parent, | ||
140 | None => break, | ||
141 | } | ||
142 | } | ||
143 | |||
144 | // If it's a statement in a block, we don't know how many statements should be included | ||
145 | if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) { | ||
146 | return None; | ||
147 | } | ||
148 | |||
149 | // Insert `}` right after the expression. | ||
150 | Some(TextEdit::insert( | ||
151 | expr.syntax().text_range().end() + TextSize::of("{"), | ||
152 | "}".to_string(), | ||
153 | )) | ||
154 | } | ||
126 | } | 155 | } |
127 | 156 | ||
128 | /// Returns an edit which should be applied after `=` was typed. Primarily, | 157 | /// Returns an edit which should be applied after `=` was typed. Primarily, |
@@ -440,7 +469,7 @@ fn foo() -> { 92 } | |||
440 | } | 469 | } |
441 | 470 | ||
442 | #[test] | 471 | #[test] |
443 | fn adds_closing_brace() { | 472 | fn adds_closing_brace_for_expr() { |
444 | type_char( | 473 | type_char( |
445 | '{', | 474 | '{', |
446 | r#" | 475 | r#" |
@@ -519,4 +548,87 @@ fn f() { | |||
519 | "#, | 548 | "#, |
520 | ); | 549 | ); |
521 | } | 550 | } |
551 | |||
552 | #[test] | ||
553 | fn adds_closing_brace_for_use_tree() { | ||
554 | type_char( | ||
555 | '{', | ||
556 | r#" | ||
557 | use some::$0Path; | ||
558 | "#, | ||
559 | r#" | ||
560 | use some::{Path}; | ||
561 | "#, | ||
562 | ); | ||
563 | type_char( | ||
564 | '{', | ||
565 | r#" | ||
566 | use some::{Path, $0Other}; | ||
567 | "#, | ||
568 | r#" | ||
569 | use some::{Path, {Other}}; | ||
570 | "#, | ||
571 | ); | ||
572 | type_char( | ||
573 | '{', | ||
574 | r#" | ||
575 | use some::{$0Path, Other}; | ||
576 | "#, | ||
577 | r#" | ||
578 | use some::{{Path}, Other}; | ||
579 | "#, | ||
580 | ); | ||
581 | type_char( | ||
582 | '{', | ||
583 | r#" | ||
584 | use some::path::$0to::Item; | ||
585 | "#, | ||
586 | r#" | ||
587 | use some::path::{to::Item}; | ||
588 | "#, | ||
589 | ); | ||
590 | type_char( | ||
591 | '{', | ||
592 | r#" | ||
593 | use some::$0path::to::Item; | ||
594 | "#, | ||
595 | r#" | ||
596 | use some::{path::to::Item}; | ||
597 | "#, | ||
598 | ); | ||
599 | type_char( | ||
600 | '{', | ||
601 | r#" | ||
602 | use $0some::path::to::Item; | ||
603 | "#, | ||
604 | r#" | ||
605 | use {some::path::to::Item}; | ||
606 | "#, | ||
607 | ); | ||
608 | type_char( | ||
609 | '{', | ||
610 | r#" | ||
611 | use some::path::$0to::{Item}; | ||
612 | "#, | ||
613 | r#" | ||
614 | use some::path::{to::{Item}}; | ||
615 | "#, | ||
616 | ); | ||
617 | type_char( | ||
618 | '{', | ||
619 | r#" | ||
620 | use $0Thing as _; | ||
621 | "#, | ||
622 | r#" | ||
623 | use {Thing as _}; | ||
624 | "#, | ||
625 | ); | ||
626 | |||
627 | type_char_noop( | ||
628 | '{', | ||
629 | r#" | ||
630 | use some::pa$0th::to::Item; | ||
631 | "#, | ||
632 | ); | ||
633 | } | ||
522 | } | 634 | } |
diff --git a/crates/ide/src/typing/on_enter.rs b/crates/ide/src/typing/on_enter.rs index 7d2db201a..81c4d95b1 100644 --- a/crates/ide/src/typing/on_enter.rs +++ b/crates/ide/src/typing/on_enter.rs | |||
@@ -54,6 +54,14 @@ pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<Text | |||
54 | cov_mark::hit!(indent_block_contents); | 54 | cov_mark::hit!(indent_block_contents); |
55 | return Some(edit); | 55 | return Some(edit); |
56 | } | 56 | } |
57 | |||
58 | // Typing enter after the `{` of a use tree list. | ||
59 | if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{')) | ||
60 | .and_then(|list| on_enter_in_use_tree_list(list, position)) | ||
61 | { | ||
62 | cov_mark::hit!(indent_block_contents); | ||
63 | return Some(edit); | ||
64 | } | ||
57 | } | 65 | } |
58 | 66 | ||
59 | None | 67 | None |
@@ -111,6 +119,21 @@ fn on_enter_in_block(block: ast::BlockExpr, position: FilePosition) -> Option<Te | |||
111 | Some(edit) | 119 | Some(edit) |
112 | } | 120 | } |
113 | 121 | ||
122 | fn on_enter_in_use_tree_list(list: ast::UseTreeList, position: FilePosition) -> Option<TextEdit> { | ||
123 | if list.syntax().text().contains_char('\n') { | ||
124 | return None; | ||
125 | } | ||
126 | |||
127 | let indent = IndentLevel::from_node(list.syntax()); | ||
128 | let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1)); | ||
129 | edit.union(TextEdit::insert( | ||
130 | list.r_curly_token()?.text_range().start(), | ||
131 | format!("\n{}", indent), | ||
132 | )) | ||
133 | .ok()?; | ||
134 | Some(edit) | ||
135 | } | ||
136 | |||
114 | fn block_contents(block: &ast::BlockExpr) -> Option<SyntaxNode> { | 137 | fn block_contents(block: &ast::BlockExpr) -> Option<SyntaxNode> { |
115 | let mut node = block.tail_expr().map(|e| e.syntax().clone()); | 138 | let mut node = block.tail_expr().map(|e| e.syntax().clone()); |
116 | 139 | ||
@@ -484,4 +507,96 @@ fn f() {$0 | |||
484 | "#, | 507 | "#, |
485 | ); | 508 | ); |
486 | } | 509 | } |
510 | |||
511 | #[test] | ||
512 | fn indents_use_tree_list() { | ||
513 | do_check( | ||
514 | r#" | ||
515 | use crate::{$0}; | ||
516 | "#, | ||
517 | r#" | ||
518 | use crate::{ | ||
519 | $0 | ||
520 | }; | ||
521 | "#, | ||
522 | ); | ||
523 | do_check( | ||
524 | r#" | ||
525 | use crate::{$0Object, path::to::OtherThing}; | ||
526 | "#, | ||
527 | r#" | ||
528 | use crate::{ | ||
529 | $0Object, path::to::OtherThing | ||
530 | }; | ||
531 | "#, | ||
532 | ); | ||
533 | do_check( | ||
534 | r#" | ||
535 | use {crate::{$0Object, path::to::OtherThing}}; | ||
536 | "#, | ||
537 | r#" | ||
538 | use {crate::{ | ||
539 | $0Object, path::to::OtherThing | ||
540 | }}; | ||
541 | "#, | ||
542 | ); | ||
543 | do_check( | ||
544 | r#" | ||
545 | use { | ||
546 | crate::{$0Object, path::to::OtherThing} | ||
547 | }; | ||
548 | "#, | ||
549 | r#" | ||
550 | use { | ||
551 | crate::{ | ||
552 | $0Object, path::to::OtherThing | ||
553 | } | ||
554 | }; | ||
555 | "#, | ||
556 | ); | ||
557 | } | ||
558 | |||
559 | #[test] | ||
560 | fn does_not_indent_use_tree_list_when_not_at_curly_brace() { | ||
561 | do_check_noop( | ||
562 | r#" | ||
563 | use path::{Thing$0}; | ||
564 | "#, | ||
565 | ); | ||
566 | } | ||
567 | |||
568 | #[test] | ||
569 | fn does_not_indent_use_tree_list_without_curly_braces() { | ||
570 | do_check_noop( | ||
571 | r#" | ||
572 | use path::Thing$0; | ||
573 | "#, | ||
574 | ); | ||
575 | do_check_noop( | ||
576 | r#" | ||
577 | use path::$0Thing; | ||
578 | "#, | ||
579 | ); | ||
580 | do_check_noop( | ||
581 | r#" | ||
582 | use path::Thing$0}; | ||
583 | "#, | ||
584 | ); | ||
585 | do_check_noop( | ||
586 | r#" | ||
587 | use path::{$0Thing; | ||
588 | "#, | ||
589 | ); | ||
590 | } | ||
591 | |||
592 | #[test] | ||
593 | fn does_not_indent_multiline_use_tree_list() { | ||
594 | do_check_noop( | ||
595 | r#" | ||
596 | use path::{$0 | ||
597 | Thing | ||
598 | }; | ||
599 | "#, | ||
600 | ); | ||
601 | } | ||
487 | } | 602 | } |
diff --git a/crates/ide/src/view_crate_graph.rs b/crates/ide/src/view_crate_graph.rs new file mode 100644 index 000000000..df6cc8aed --- /dev/null +++ b/crates/ide/src/view_crate_graph.rs | |||
@@ -0,0 +1,90 @@ | |||
1 | use std::sync::Arc; | ||
2 | |||
3 | use dot::{Id, LabelText}; | ||
4 | use ide_db::{ | ||
5 | base_db::{CrateGraph, CrateId, Dependency, SourceDatabase, SourceDatabaseExt}, | ||
6 | RootDatabase, | ||
7 | }; | ||
8 | use rustc_hash::FxHashSet; | ||
9 | |||
10 | // Feature: View Crate Graph | ||
11 | // | ||
12 | // Renders the currently loaded crate graph as an SVG graphic. Requires the `dot` tool, which | ||
13 | // is part of graphviz, to be installed. | ||
14 | // | ||
15 | // Only workspace crates are included, no crates.io dependencies or sysroot crates. | ||
16 | // | ||
17 | // |=== | ||
18 | // | Editor | Action Name | ||
19 | // | ||
20 | // | VS Code | **Rust Analyzer: View Crate Graph** | ||
21 | // |=== | ||
22 | pub(crate) fn view_crate_graph(db: &RootDatabase) -> Result<String, String> { | ||
23 | let crate_graph = db.crate_graph(); | ||
24 | let crates_to_render = crate_graph | ||
25 | .iter() | ||
26 | .filter(|krate| { | ||
27 | // Only render workspace crates | ||
28 | let root_id = db.file_source_root(crate_graph[*krate].root_file_id); | ||
29 | !db.source_root(root_id).is_library | ||
30 | }) | ||
31 | .collect(); | ||
32 | let graph = DotCrateGraph { graph: crate_graph, crates_to_render }; | ||
33 | |||
34 | let mut dot = Vec::new(); | ||
35 | dot::render(&graph, &mut dot).unwrap(); | ||
36 | Ok(String::from_utf8(dot).unwrap()) | ||
37 | } | ||
38 | |||
39 | struct DotCrateGraph { | ||
40 | graph: Arc<CrateGraph>, | ||
41 | crates_to_render: FxHashSet<CrateId>, | ||
42 | } | ||
43 | |||
44 | type Edge<'a> = (CrateId, &'a Dependency); | ||
45 | |||
46 | impl<'a> dot::GraphWalk<'a, CrateId, Edge<'a>> for DotCrateGraph { | ||
47 | fn nodes(&'a self) -> dot::Nodes<'a, CrateId> { | ||
48 | self.crates_to_render.iter().copied().collect() | ||
49 | } | ||
50 | |||
51 | fn edges(&'a self) -> dot::Edges<'a, Edge<'a>> { | ||
52 | self.crates_to_render | ||
53 | .iter() | ||
54 | .flat_map(|krate| { | ||
55 | self.graph[*krate] | ||
56 | .dependencies | ||
57 | .iter() | ||
58 | .filter(|dep| self.crates_to_render.contains(&dep.crate_id)) | ||
59 | .map(move |dep| (*krate, dep)) | ||
60 | }) | ||
61 | .collect() | ||
62 | } | ||
63 | |||
64 | fn source(&'a self, edge: &Edge<'a>) -> CrateId { | ||
65 | edge.0 | ||
66 | } | ||
67 | |||
68 | fn target(&'a self, edge: &Edge<'a>) -> CrateId { | ||
69 | edge.1.crate_id | ||
70 | } | ||
71 | } | ||
72 | |||
73 | impl<'a> dot::Labeller<'a, CrateId, Edge<'a>> for DotCrateGraph { | ||
74 | fn graph_id(&'a self) -> Id<'a> { | ||
75 | Id::new("rust_analyzer_crate_graph").unwrap() | ||
76 | } | ||
77 | |||
78 | fn node_id(&'a self, n: &CrateId) -> Id<'a> { | ||
79 | Id::new(format!("_{}", n.0)).unwrap() | ||
80 | } | ||
81 | |||
82 | fn node_shape(&'a self, _node: &CrateId) -> Option<LabelText<'a>> { | ||
83 | Some(LabelText::LabelStr("box".into())) | ||
84 | } | ||
85 | |||
86 | fn node_label(&'a self, n: &CrateId) -> LabelText<'a> { | ||
87 | let name = self.graph[*n].display_name.as_ref().map_or("(unnamed crate)", |name| &*name); | ||
88 | LabelText::LabelStr(name.into()) | ||
89 | } | ||
90 | } | ||