File: CodeActions\CSharp\CSharpCodeActionProvider.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj (Microsoft.CodeAnalysis.Razor.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Threading;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
using Microsoft.CodeAnalysis.Razor.Workspaces;
 
namespace Microsoft.CodeAnalysis.Razor.CodeActions;
 
internal class CSharpCodeActionProvider(LanguageServerFeatureOptions languageServerFeatureOptions) : ICSharpCodeActionProvider
{
    // Internal for testing
    internal static readonly HashSet<string> SupportedDefaultCodeActionNames =
    [
        RazorPredefinedCodeRefactoringProviderNames.GenerateEqualsAndGetHashCodeFromMembers,
        RazorPredefinedCodeRefactoringProviderNames.AddAwait,
        RazorPredefinedCodeRefactoringProviderNames.AddDebuggerDisplay,
        RazorPredefinedCodeRefactoringProviderNames.InitializeMemberFromParameter, // Create and assign (property|field)
        RazorPredefinedCodeRefactoringProviderNames.AddParameterCheck, // Add Null checks
        RazorPredefinedCodeRefactoringProviderNames.AddConstructorParametersFromMembers,
        RazorPredefinedCodeRefactoringProviderNames.GenerateDefaultConstructors,
        RazorPredefinedCodeRefactoringProviderNames.GenerateConstructorFromMembers,
        RazorPredefinedCodeRefactoringProviderNames.UseExpressionBody,
        RazorPredefinedCodeRefactoringProviderNames.IntroduceVariable,
        RazorPredefinedCodeRefactoringProviderNames.ConvertBetweenRegularAndVerbatimInterpolatedString,
        RazorPredefinedCodeRefactoringProviderNames.ConvertBetweenRegularAndVerbatimString,
        RazorPredefinedCodeRefactoringProviderNames.ConvertConcatenationToInterpolatedString,
        RazorPredefinedCodeRefactoringProviderNames.ConvertPlaceholderToInterpolatedString,
        RazorPredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString,
        RazorPredefinedCodeFixProviderNames.ImplementAbstractClass,
        RazorPredefinedCodeFixProviderNames.ImplementInterface,
        RazorPredefinedCodeFixProviderNames.RemoveUnusedVariable,
        RazorPredefinedCodeFixProviderNames.GenerateMethod,
    ];
 
    internal static readonly HashSet<string> SupportedImplicitExpressionCodeActionNames =
    [
         RazorPredefinedCodeFixProviderNames.GenerateMethod,
    ];
 
    private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions;
 
    public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(
        RazorCodeActionContext context,
        ImmutableArray<RazorVSInternalCodeAction> codeActions,
        CancellationToken cancellationToken)
    {
        // Used to identify if this is VSCode which doesn't support
        // code action resolve.
        if (!context.SupportsCodeActionResolve)
        {
            return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
        }
 
        var root = context.CodeDocument.GetRequiredSyntaxRoot();
        var node = root.FindInnermostNode(context.StartAbsoluteIndex);
        var isInImplicitExpression = node?.AncestorsAndSelf().Any(n => n is CSharpImplicitExpressionSyntax) ?? false;
 
        var allowList = isInImplicitExpression
            ? SupportedImplicitExpressionCodeActionNames
            : SupportedDefaultCodeActionNames;
 
        using var results = new PooledArrayBuilder<RazorVSInternalCodeAction>();
 
        foreach (var codeAction in codeActions)
        {
            var isOnAllowList = codeAction.Name is not null && allowList.Contains(codeAction.Name);
 
            // If this code action isn't on the allow list, it might have been handled by another provider, which means
            // it will already have been wrapped, so we have to check not to double-wrap it.
            if (_languageServerFeatureOptions.ShowAllCSharpCodeActions &&
                CanDeserializeTo<RazorCodeActionResolutionParams>(codeAction.Data))
            {
                // This code action has already been wrapped by something else, so skip it here, or it could
                // be marked as experimental when its not, and more importantly would be duplicated in the list.
                continue;
            }
 
            if (_languageServerFeatureOptions.ShowAllCSharpCodeActions || isOnAllowList)
            {
                results.Add(codeAction.WrapResolvableCodeAction(context, isOnAllowList: isOnAllowList));
            }
        }
 
        return Task.FromResult(results.ToImmutable());
 
        static bool CanDeserializeTo<T>(object? data)
        {
            // We don't care about errors here, and there is no TryDeserialize method, so we can just brute force this.
            // Since this only happens if the feature flag is on, which is internal only and intended only for users of
            // this repo, any perf hit here isn't going to affect real users.
            try
            {
                if (data is JsonElement element &&
                    element.Deserialize<RazorCodeActionResolutionParams>() is not null)
                {
                    return true;
                }
            }
            catch { }
 
            return false;
        }
    }
}