| File: AddFileBanner\AbstractAddFileBannerCodeRefactoringProvider.cs | Web Access |
| Project: src\roslyn\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.AddFileBanner; internal abstract class AbstractAddFileBannerCodeRefactoringProvider : SyntaxEditorBasedCodeRefactoringProvider { private const string BannerFileNamePlaceholder = "{filename}"; protected abstract bool IsCommentStartCharacter(char ch); protected sealed override ImmutableArray<RefactorAllScope> SupportedRefactorAllScopes { get; } = [RefactorAllScope.Project, RefactorAllScope.Solution]; public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { var (document, span, cancellationToken) = context; if (!span.IsEmpty) { return; } var formattingOptions = await document.GetDocumentFormattingOptionsAsync(cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(formattingOptions.FileHeaderTemplate)) { // If we have a defined file header template, allow the analyzer and code fix to handle it return; } var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); var position = span.Start; var firstToken = root.GetFirstToken(); if (!firstToken.FullSpan.IntersectsWith(position)) return; if (HasExistingBanner(document, root)) 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( CodeAction.Create( CodeFixesResources.Add_file_header, cancellationToken => AddFileBannerHelpers.CopyBannerAsync( destinationDocument: document, document.FilePath, sourceDocument: siblingDocument, cancellationToken), equivalenceKey: GetEquivalenceKey(siblingDocument, siblingBanner)), new TextSpan(position, length: 0)); return; } } } private static bool HasExistingBanner(Document document, SyntaxNode root) { var bannerService = document.GetRequiredLanguageService<IFileBannerFactsService>(); var banner = bannerService.GetFileBanner(root); return banner.Length > 0; } private static string GetEquivalenceKey(Document document, ImmutableArray<SyntaxTrivia> banner) { var bannerText = banner.Select(trivia => trivia.ToFullString()).Join(string.Empty); var fileName = IOUtilities.PerformIO(() => Path.GetFileName(document.FilePath)); if (!string.IsNullOrEmpty(fileName)) bannerText = bannerText.Replace(fileName, BannerFileNamePlaceholder); return bannerText; } private static ImmutableArray<SyntaxTrivia> GetBannerFromEquivalenceKey(string equivalenceKey, Document document) { var fileName = IOUtilities.PerformIO(() => Path.GetFileName(document.FilePath)); if (!string.IsNullOrEmpty(fileName)) equivalenceKey = equivalenceKey.Replace(BannerFileNamePlaceholder, fileName); var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>(); var token = syntaxFacts.ParseToken(equivalenceKey); var bannerService = document.GetRequiredLanguageService<IFileBannerFactsService>(); return bannerService.GetFileBanner(token); } private async Task<ImmutableArray<SyntaxTrivia>> TryGetBannerAsync( Document document, SyntaxNode? root, CancellationToken cancellationToken) { var bannerService = document.GetRequiredLanguageService<IFileBannerFactsService>(); var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>(); // If we have a tree already for this document, then just check to see // if it has a banner. if (root != null) { return bannerService.GetFileBanner(root); } // Didn't have a tree. Don't want to parse the file if we can avoid it. var text = await document.GetValueTextAsync(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 []; } var token = syntaxFacts.ParseToken(text.ToString()); return bannerService.GetFileBanner(token); } protected sealed override async Task RefactorAllAsync( Document document, ImmutableArray<TextSpan> fixAllSpans, SyntaxEditor editor, string? equivalenceKey, CancellationToken cancellationToken) { Debug.Assert(equivalenceKey != null); // Bail out if the document to fix already has an existing banner. var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (HasExistingBanner(document, root)) return; // Get banner from the equivalence key. var banner = GetBannerFromEquivalenceKey(equivalenceKey, document); Debug.Assert(banner.Length > 0); // Finally add the banner to the document to be fixed. var newRoot = root.WithPrependedLeadingTrivia(new SyntaxTriviaList(banner)); editor.ReplaceNode(editor.OriginalRoot, newRoot); } }