diff --git a/src/EditorFeatures/CSharpTest/AddFileBanner/AddFileBannerTests.cs b/src/EditorFeatures/CSharpTest/AddFileBanner/AddFileBannerTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..2e15b02610c3fccb7c616899ac18a3684684dbb0 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/AddFileBanner/AddFileBannerTests.cs @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.CSharp.AddFileBanner; +using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeRefactorings; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.AddFileBanner +{ + public partial class AddFileBannerTests : AbstractCSharpCodeActionTest + { + protected override CodeRefactoringProvider CreateCodeRefactoringProvider(Workspace workspace, TestParameters parameters) + => new CSharpAddFileBannerCodeRefactoringProvider(); + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsAddFileBanner)] + public async Task TestBanner1() + { + await TestInRegularAndScriptAsync( +@" + + + [||]using System; + +class Program1 +{ + static void Main() + { + } +} + + // This is the banner + +class Program2 +{ +} + + +", +@" + + + // This is the banner + +using System; + +class Program1 +{ + static void Main() + { + } +} + + // This is the banner + +class Program2 +{ +} + + +", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsAddFileBanner)] + public async Task TestMultiLineBanner1() + { + await TestInRegularAndScriptAsync( +@" + + + [||]using System; + +class Program1 +{ + static void Main() + { + } +} + + // This is the banner +// It goes over multiple lines + +class Program2 +{ +} + + +", +@" + + + // This is the banner +// It goes over multiple lines + +using System; + +class Program1 +{ + static void Main() + { + } +} + + // This is the banner +// It goes over multiple lines + +class Program2 +{ +} + + +", ignoreTrivia: false); + } + + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsAddFileBanner)] + public async Task TestMissingWhenAlreadyThere() + { + await TestMissingAsync( +@" + + + [||]// I already have a banner + +using System; + +class Program1 +{ + static void Main() + { + } +} + + // This is the banner + +class Program2 +{ +} + + +"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsAddFileBanner)] + public async Task TestMissingIfOtherFileDoesNotHaveBanner() + { + await TestMissingAsync( +@" + + + [||] + +using System; + +class Program1 +{ + static void Main() + { + } +} + + + +class Program2 +{ +} + + +"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsAddFileBanner)] + public async Task TestMissingIfOtherFileIsAutoGenerated() + { + await TestMissingAsync( +@" + + + [||] + +using System; + +class Program1 +{ + static void Main() + { + } +} + + // <autogenerated /> + +class Program2 +{ +} + + +"); + } + } +} diff --git a/src/EditorFeatures/TestUtilities/Traits.cs b/src/EditorFeatures/TestUtilities/Traits.cs index 192e017aab00030a8f149ff97acc40461cef1190..8e03c0de4999671562d2d7712df52d7998303fe5 100644 --- a/src/EditorFeatures/TestUtilities/Traits.cs +++ b/src/EditorFeatures/TestUtilities/Traits.cs @@ -34,6 +34,7 @@ public static class Features public const string CodeActionsUpgradeProject = "CodeActions.UpgradeProject"; public const string CodeActionsAddAccessibilityModifiers = "CodeActions.AddAccessibilityModifiers"; public const string CodeActionsAddBraces = "CodeActions.AddBraces"; + public const string CodeActionsAddFileBanner = "CodeActions.AddFileBanner"; public const string CodeActionsAddImport = "CodeActions.AddImport"; public const string CodeActionsAddMissingReference = "CodeActions.AddMissingReference"; public const string CodeActionsAddParameter = "CodeActions.AddParameter"; diff --git a/src/EditorFeatures/VisualBasicTest/AddFileBanner/AddFileBannerTests.vb b/src/EditorFeatures/VisualBasicTest/AddFileBanner/AddFileBannerTests.vb new file mode 100644 index 0000000000000000000000000000000000000000..aacc1b16d022bad99a0665b66c5dcedcee3569a6 --- /dev/null +++ b/src/EditorFeatures/VisualBasicTest/AddFileBanner/AddFileBannerTests.vb @@ -0,0 +1,175 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports System.Threading.Tasks +Imports Microsoft.CodeAnalysis.CodeRefactorings +Imports Microsoft.CodeAnalysis.VisualBasic.AddFileBanner +Imports Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.CodeRefactorings +Imports Roslyn.Test.Utilities +Imports Xunit + +Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.AddFileBanner + Partial Public Class AddFileBannerTests + Inherits AbstractVisualBasicCodeActionTest + + Protected Overrides Function CreateCodeRefactoringProvider(workspace As Workspace, parameters As TestParameters) As CodeRefactoringProvider + Return New VisualBasicAddFileBannerCodeRefactoringProvider() + End Function + + + Public Async Function TestBanner1() As Task + Await TestInRegularAndScriptAsync( +" + + + [||]Imports System + +class Program1 + sub Main() + end sub +end class + + ' This is the banner + +class Program2 +end class + + +", +" + + + ' This is the banner + +Imports System + +class Program1 + sub Main() + end sub +end class + + ' This is the banner + +class Program2 +end class + + +", ignoreTrivia:=False) + End Function + + + Public Async Function TestMultiLineBanner1() As Task + Await TestInRegularAndScriptAsync( +" + + + [||]Imports System + +class Program1 + sub Main() + end sub +end class + + ' This is the banner +' It goes over multiple lines + +class Program2 +end class + + +", +" + + + ' This is the banner +' It goes over multiple lines + +Imports System + +class Program1 + sub Main() + end sub +end class + + ' This is the banner +' It goes over multiple lines + +class Program2 +end class + + +", ignoreTrivia:=False) + End Function + + + Public Async Function TestMissingWhenAlreadyThere() As Task + Await TestMissingAsync( +" + + + [||]' I already have a banner + +Imports System + +class Program1 + sub Main() + end sub +end class + + ' This is the banner + +class Program2 +end class + + +") + End Function + + + Public Async Function TestMissingIfOtherFileDoesNotHaveBanner() As Task + Await TestMissingAsync( +" + + + [||] + +Imports System + +class Program1 + sub Main() + end sub +end class + + + +class Program2 +end class + + +") + End Function + + + Public Async Function TestMissingIfOtherFileIsAutoGenerated() As Task + Await TestMissingAsync( +" + + + [||] + +Imports System + +class Program1 + sub Main() + end sub +end class + + ' <autogenerated /> + +class Program2 +end class + + +") + End Function + End Class +End Namespace diff --git a/src/Features/CSharp/Portable/AddFileBanner/CSharpAddFileBannerCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/AddFileBanner/CSharpAddFileBannerCodeRefactoringProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..e15b7fd3f9ecd508ab40c2c62327bab7904000e6 --- /dev/null +++ b/src/Features/CSharp/Portable/AddFileBanner/CSharpAddFileBannerCodeRefactoringProvider.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Composition; +using Microsoft.CodeAnalysis.AddFileBanner; +using Microsoft.CodeAnalysis.CodeRefactorings; + +namespace Microsoft.CodeAnalysis.CSharp.AddFileBanner +{ + [ExportCodeRefactoringProvider(LanguageNames.CSharp), Shared] + internal class CSharpAddFileBannerCodeRefactoringProvider : AbstractAddFileBannerCodeRefactoringProvider + { + protected override bool IsCommentStartCharacter(char ch) + => ch == '/'; + } +} diff --git a/src/Features/Core/Portable/AddFileBanner/AbstractAddFileBannerCodeRefactoringProvider.cs b/src/Features/Core/Portable/AddFileBanner/AbstractAddFileBannerCodeRefactoringProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..c138ce19a767c567db77a9936e69fcf5da4b565e --- /dev/null +++ b/src/Features/Core/Portable/AddFileBanner/AbstractAddFileBannerCodeRefactoringProvider.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.AddFileBanner +{ + internal abstract class AbstractAddFileBannerCodeRefactoringProvider : CodeRefactoringProvider + { + protected abstract bool IsCommentStartCharacter(char ch); + + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var cancellationToken = context.CancellationToken; + var document = context.Document; + + if (!context.Span.IsEmpty) + { + return; + } + + var position = context.Span.Start; + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var firstToken = root.GetFirstToken(); + if (!firstToken.FullSpan.IntersectsWith(position)) + { + return; + } + + var syntaxFacts = document.GetLanguageService(); + var banner = syntaxFacts.GetFileBanner(root); + + if (banner.Length > 0) + { + // Already has a banner. + return; + } + + // Process the other documents in this document's project. Look at the + // ones that we can get a root from (without having to parse). Then + // look at the ones we'd need to parse. + var siblingDocumentsAndRoots = + document.Project.Documents + .Where(d => d != document) + .Select(d => + { + d.TryGetSyntaxRoot(out var siblingRoot); + return (document: d, root: siblingRoot); + }) + .OrderBy((t1, t2) => (t1.root != null) == (t2.root != null) ? 0 : t1.root != null ? -1 : 1); + + foreach (var (siblingDocument, siblingRoot) in siblingDocumentsAndRoots) + { + cancellationToken.ThrowIfCancellationRequested(); + + var siblingBanner = await TryGetBannerAsync(siblingDocument, siblingRoot, cancellationToken).ConfigureAwait(false); + if (siblingBanner.Length > 0 && !siblingDocument.IsGeneratedCode(cancellationToken)) + { + context.RegisterRefactoring( + new MyCodeAction(c => AddBannerAsync(document, root, siblingBanner, c))); + return; + } + } + } + + private Task AddBannerAsync( + Document document, SyntaxNode root, ImmutableArray banner, CancellationToken c) + { + var newRoot = root.WithPrependedLeadingTrivia(new SyntaxTriviaList(banner)); + return Task.FromResult(document.WithSyntaxRoot(newRoot)); + } + + private async Task> TryGetBannerAsync( + Document document, SyntaxNode root, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetLanguageService(); + + // If we have a tree already for this document, then just check to see + // if it has a banner. + if (root != null) + { + return syntaxFacts.GetFileBanner(root); + } + + // Didn't have a tree. Don't want to parse the file if we can avoid it. + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + if (text.Length == 0 || !IsCommentStartCharacter(text[0])) + { + // Didn't start with a comment character, don't bother looking at + // this file. + return ImmutableArray.Empty; + } + + var token = syntaxFacts.ParseToken(text.ToString()); + return syntaxFacts.GetFileBanner(token); + } + + private class MyCodeAction : CodeAction.DocumentChangeAction + { + public MyCodeAction(Func> createChangedDocument) + : base(FeaturesResources.Add_file_banner, createChangedDocument) + { + } + } + } +} diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index 370bba7d7ad4b9ab29d4c71dfe6d29ea25220adb..b51f74a48943d861501ddc8661c2babb4d3476cf 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -161,6 +161,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Add file banner. + /// + internal static string Add_file_banner { + get { + return ResourceManager.GetString("Add_file_banner", resourceCulture); + } + } + /// /// Looks up a localized string similar to Add missing cases. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index ae2e8df26dbf76aab64af4e8ae1bbbe9bb5bfaf5..6828538ccdbc3bae422139e3dc6289d1b7bc9286 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -1295,4 +1295,7 @@ This version used in: {2} Generate constructor in '{0}' (without fields) + + Add file banner + \ No newline at end of file diff --git a/src/Features/VisualBasic/Portable/AddFileBanner/VisualBasicAddFileBannerCodeRefactoringProvider.vb b/src/Features/VisualBasic/Portable/AddFileBanner/VisualBasicAddFileBannerCodeRefactoringProvider.vb new file mode 100644 index 0000000000000000000000000000000000000000..d8c1929abb5743098d7354e94f9542bda9794b62 --- /dev/null +++ b/src/Features/VisualBasic/Portable/AddFileBanner/VisualBasicAddFileBannerCodeRefactoringProvider.vb @@ -0,0 +1,16 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports System.Composition +Imports Microsoft.CodeAnalysis.AddFileBanner +Imports Microsoft.CodeAnalysis.CodeRefactorings + +Namespace Microsoft.CodeAnalysis.VisualBasic.AddFileBanner + + Friend Class VisualBasicAddFileBannerCodeRefactoringProvider + Inherits AbstractAddFileBannerCodeRefactoringProvider + + Protected Overrides Function IsCommentStartCharacter(ch As Char) As Boolean + Return ch = "'"c + End Function + End Class +End Namespace diff --git a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs index e1ebf40f3ad553c9a92fb02460ef35ad1ad7402e..f04663fd044a1bddbebe215485d287f74f7cf700 100644 --- a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs +++ b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs @@ -45,6 +45,9 @@ public bool SupportsIndexingInitializer(ParseOptions options) public bool SupportsThrowExpression(ParseOptions options) => ((CSharpParseOptions)options).LanguageVersion >= LanguageVersion.CSharp7; + public SyntaxToken ParseToken(string text) + => SyntaxFactory.ParseToken(text); + public bool IsAwaitKeyword(SyntaxToken token) { return token.IsKind(SyntaxKind.AwaitKeyword); diff --git a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/AbstractSyntaxFactsService.cs b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/AbstractSyntaxFactsService.cs index eb5b77ff73d43e6e619100dc4481735bbc2eea1d..04b1b8a5765a5d9abaee9095e4dc9db59588bb28 100644 --- a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/AbstractSyntaxFactsService.cs +++ b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/AbstractSyntaxFactsService.cs @@ -373,8 +373,14 @@ public ImmutableArray GetLeadingBannerAndPreprocessorDirectives GetFileBanner(SyntaxNode root) { Debug.Assert(root.FullSpan.Start == 0); + return GetFileBanner(root.GetFirstToken(includeZeroWidth: true)); + } + + public ImmutableArray GetFileBanner(SyntaxToken firstToken) + { + Debug.Assert(firstToken.FullSpan.Start == 0); - var leadingTrivia = root.GetLeadingTrivia(); + var leadingTrivia = firstToken.LeadingTrivia; var index = 0; _fileBannerMatcher.TryMatch(leadingTrivia.ToList(), ref index); diff --git a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs index 2b34bbeaf934d4be893a2c82acaa6d294cb454e9..12509240dfb1d2a955f5568115d1cf812bf77747 100644 --- a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs +++ b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs @@ -19,6 +19,8 @@ internal interface ISyntaxFactsService : ILanguageService bool SupportsIndexingInitializer(ParseOptions options); bool SupportsThrowExpression(ParseOptions options); + SyntaxToken ParseToken(string text); + bool IsAwaitKeyword(SyntaxToken token); bool IsIdentifier(SyntaxToken token); bool IsGlobalNamespaceKeyword(SyntaxToken token); @@ -303,6 +305,7 @@ internal interface ISyntaxFactsService : ILanguageService TSyntaxNode GetNodeWithoutLeadingBlankLines(TSyntaxNode node) where TSyntaxNode : SyntaxNode; ImmutableArray GetFileBanner(SyntaxNode root); + ImmutableArray GetFileBanner(SyntaxToken firstToken); bool ContainsInterleavedDirective(SyntaxNode node, CancellationToken cancellationToken); bool ContainsInterleavedDirective(ImmutableArray nodes, CancellationToken cancellationToken); diff --git a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb index 0ad08fd1fed74bb6e1f1e5d652a6d2084b18ae47..fa35376af377c68f9273b007c64dbff891632359 100644 --- a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb +++ b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb @@ -66,6 +66,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Return False End Function + Public Function ParseToken(text As String) As SyntaxToken Implements ISyntaxFactsService.ParseToken + Return SyntaxFactory.ParseToken(text, startStatement:=True) + End Function + Public Function IsAwaitKeyword(token As SyntaxToken) As Boolean Implements ISyntaxFactsService.IsAwaitKeyword Return token.Kind = SyntaxKind.AwaitKeyword End Function @@ -1586,6 +1590,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Return GetFileBanner(root) End Function + Private Function ISyntaxFactsService_GetFileBanner(firstToken As SyntaxToken) As ImmutableArray(Of SyntaxTrivia) Implements ISyntaxFactsService.GetFileBanner + Return GetFileBanner(firstToken) + End Function + Protected Overrides Function ContainsInterleavedDirective(span As TextSpan, token As SyntaxToken, cancellationToken As CancellationToken) As Boolean Return token.ContainsInterleavedDirective(span, cancellationToken) End Function