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