File: CodeActions\CodeActionResolveService.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;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Workspaces.Settings;
 
namespace Microsoft.CodeAnalysis.Razor.CodeActions;
 
internal class CodeActionResolveService(
    IEnumerable<IRazorCodeActionResolver> razorCodeActionResolvers,
    IEnumerable<ICSharpCodeActionResolver> csharpCodeActionResolvers,
    IEnumerable<IHtmlCodeActionResolver> htmlCodeActionResolvers,
    IClientSettingsManager clientSettingsManager,
    ILoggerFactory loggerFactory) : ICodeActionResolveService
 
{
    private readonly FrozenDictionary<string, IRazorCodeActionResolver> _razorCodeActionResolvers = CreateResolverMap(razorCodeActionResolvers);
    private readonly FrozenDictionary<string, ICSharpCodeActionResolver> _csharpCodeActionResolvers = CreateResolverMap(csharpCodeActionResolvers);
    private readonly FrozenDictionary<string, IHtmlCodeActionResolver> _htmlCodeActionResolvers = CreateResolverMap(htmlCodeActionResolvers);
    private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager;
    private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CodeActionResolveService>();
 
    public async Task<CodeAction> ResolveCodeActionAsync(DocumentContext documentContext, CodeAction request, CodeAction? resolvedDelegatedCodeAction, CancellationToken cancellationToken)
    {
        var resolutionParams = GetRazorCodeActionResolutionParams(request);
 
        var codeActionId = GetCodeActionId(resolutionParams);
        _logger.LogDebug($"Resolving workspace edit for action {codeActionId}.");
 
        // If it's a special "edit based code action" then the edit has been pre-computed and we
        // can extract the edit details and return to the client. This is only required for VSCode
        // as it does not support Command.Edit based code actions anymore.
        if (resolutionParams.Action == LanguageServerConstants.CodeActions.EditBasedCodeActionCommand)
        {
            request.Edit = (resolutionParams.Data as JsonElement?)?.Deserialize<WorkspaceEdit>();
            return request;
        }
 
        switch (resolutionParams.Language)
        {
            case RazorLanguageKind.Razor:
                return await ResolveRazorCodeActionAsync(
                    documentContext,
                    request,
                    resolutionParams,
                    cancellationToken).ConfigureAwait(false);
            case RazorLanguageKind.CSharp:
                return await ResolveCSharpCodeActionAsync(
                    documentContext,
                    resolvedDelegatedCodeAction.AssumeNotNull(),
                    resolutionParams,
                    cancellationToken).ConfigureAwait(false);
            case RazorLanguageKind.Html:
                return await ResolveHtmlCodeActionAsync(
                    documentContext,
                    resolvedDelegatedCodeAction.AssumeNotNull(),
                    resolutionParams,
                    cancellationToken).ConfigureAwait(false);
            default:
                _logger.LogError($"Invalid CodeAction.Data.Language. Received {codeActionId}.");
                return request;
        }
    }
 
    public static RazorCodeActionResolutionParams GetRazorCodeActionResolutionParams(CodeAction request)
    {
        if (request.Data is not JsonElement paramsObj)
        {
            throw new InvalidOperationException($"Invalid CodeAction Received '{request.Title}'.");
        }
 
        var resolutionParams = paramsObj.Deserialize<RazorCodeActionResolutionParams>();
        if (resolutionParams is null)
        {
            throw new InvalidOperationException($"request.Data should be convertible to {nameof(RazorCodeActionResolutionParams)}");
        }
 
        return resolutionParams;
    }
 
    private async Task<CodeAction> ResolveRazorCodeActionAsync(
        DocumentContext documentContext,
        CodeAction codeAction,
        RazorCodeActionResolutionParams resolutionParams,
        CancellationToken cancellationToken)
    {
        if (!_razorCodeActionResolvers.TryGetValue(resolutionParams.Action, out var resolver))
        {
            var codeActionId = GetCodeActionId(resolutionParams);
            _logger.LogWarning($"No resolver registered for {codeActionId}");
            Debug.Fail($"No resolver registered for {codeActionId}.");
            return codeAction;
        }
 
        if (resolutionParams.Data is not JsonElement data)
        {
            return codeAction;
        }
 
        var options = _clientSettingsManager.GetClientSettings().ToRazorFormattingOptions();
        var edit = await resolver.ResolveAsync(documentContext, data, options, cancellationToken).ConfigureAwait(false);
        codeAction.Edit = edit;
        return codeAction;
    }
 
    private async Task<CodeAction> ResolveCSharpCodeActionAsync(DocumentContext documentContext, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
    {
        if (TryGetResolver(resolutionParams, _csharpCodeActionResolvers, out var resolver))
        {
            return await resolver.ResolveAsync(documentContext, codeAction, cancellationToken).ConfigureAwait(false);
        }
 
        return codeAction;
    }
 
    private async Task<CodeAction> ResolveHtmlCodeActionAsync(DocumentContext documentContext, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
    {
        if (TryGetResolver(resolutionParams, _htmlCodeActionResolvers, out var resolver))
        {
            return await resolver.ResolveAsync(documentContext, codeAction, cancellationToken).ConfigureAwait(false);
        }
 
        return codeAction;
    }
 
    private bool TryGetResolver<TResolver>(RazorCodeActionResolutionParams resolutionParams, FrozenDictionary<string, TResolver> resolvers, [NotNullWhen(true)] out TResolver? resolver)
          where TResolver : ICodeActionResolver
    {
        if (!resolvers.TryGetValue(resolutionParams.Action, out resolver))
        {
            var codeActionId = GetCodeActionId(resolutionParams);
            _logger.LogWarning($"No resolver registered for {codeActionId}");
            Debug.Fail($"No resolver registered for {codeActionId}.");
            return false;
        }
 
        return resolver is not null;
    }
 
    private static FrozenDictionary<string, T> CreateResolverMap<T>(IEnumerable<T> codeActionResolvers)
        where T : ICodeActionResolver
    {
        using var _ = SpecializedPools.GetPooledStringDictionary<T>(out var resolverMap);
 
        foreach (var resolver in codeActionResolvers)
        {
            if (resolverMap.ContainsKey(resolver.Action))
            {
                Debug.Fail($"Duplicate resolver action for {resolver.Action} of type {typeof(T)}.");
            }
 
            resolverMap[resolver.Action] = resolver;
        }
 
        return resolverMap.ToFrozenDictionary();
    }
 
    private static string GetCodeActionId(RazorCodeActionResolutionParams resolutionParams) =>
        $"`{resolutionParams.Language}.{resolutionParams.Action}`";
 
    internal TestAccessor GetTestAccessor() => new(this);
 
    internal readonly struct TestAccessor(CodeActionResolveService instance)
    {
        public Task<CodeAction> ResolveRazorCodeActionAsync(DocumentContext documentContext, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
            => instance.ResolveRazorCodeActionAsync(documentContext, codeAction, resolutionParams, cancellationToken);
 
        public Task<CodeAction> ResolveCSharpCodeActionAsync(DocumentContext documentContext, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
            => instance.ResolveCSharpCodeActionAsync(documentContext, codeAction, resolutionParams, cancellationToken);
 
        public Task<CodeAction> ResolveHtmlCodeActionAsync(DocumentContext documentContext, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
            => instance.ResolveCSharpCodeActionAsync(documentContext, codeAction, resolutionParams, cancellationToken);
    }
}