Skip to content

Commit

Permalink
Cohosted Code Actions Part 2: Re-layout in advance of moving down (#1…
Browse files Browse the repository at this point in the history
…1083)

Part of #10742

The next round of changes is to introduce the right abstractions so that
the bulk of the code actions code can be moved to the Workspaces layer
successfully.

At least I think I got it all :D

Reviewing commit at a time probably makes sense, but up to you.
  • Loading branch information
davidwengier authored Oct 24, 2024
2 parents af22ba0 + 56ad7f7 commit d371b1e
Show file tree
Hide file tree
Showing 99 changed files with 1,630 additions and 1,396 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
Expand All @@ -12,11 +11,10 @@
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.AspNetCore.Razor.Telemetry;
using Microsoft.CodeAnalysis.Razor.CodeActions;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CommonLanguageServerProtocol.Framework;
using Microsoft.VisualStudio.LanguageServer.Protocol;
Expand Down Expand Up @@ -49,14 +47,8 @@ public enum FileTypes
public async Task SetupAsync()
{
CodeActionEndpoint = new CodeActionEndpoint(
documentMappingService: RazorLanguageServerHost.GetRequiredService<IDocumentMappingService>(),
razorCodeActionProviders: RazorLanguageServerHost.GetRequiredService<IEnumerable<IRazorCodeActionProvider>>(),
csharpCodeActionProviders: RazorLanguageServerHost.GetRequiredService<IEnumerable<ICSharpCodeActionProvider>>(),
htmlCodeActionProviders: RazorLanguageServerHost.GetRequiredService<IEnumerable<IHtmlCodeActionProvider>>(),
clientConnection: RazorLanguageServerHost.GetRequiredService<IClientConnection>(),
languageServerFeatureOptions: RazorLanguageServerHost.GetRequiredService<LanguageServerFeatureOptions>(),
loggerFactory: RazorLanguageServerHost.GetRequiredService<ILoggerFactory>(),
telemetryReporter: null);
codeActionsService: RazorLanguageServerHost.GetRequiredService<ICodeActionsService>(),
telemetryReporter: NoOpTelemetryReporter.Instance);

var projectRoot = Path.Combine(RepoRoot, "src", "Razor", "test", "testapps", "ComponentApp");
var projectFilePath = Path.Combine(projectRoot, "ComponentApp.csproj");
Expand Down

This file was deleted.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,177 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.CodeActions;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;

[RazorLanguageServerEndpoint(Methods.CodeActionResolveName)]
internal sealed class CodeActionResolveEndpoint(
IEnumerable<IRazorCodeActionResolver> razorCodeActionResolvers,
IEnumerable<CSharpCodeActionResolver> csharpCodeActionResolvers,
IEnumerable<HtmlCodeActionResolver> htmlCodeActionResolvers,
ILoggerFactory loggerFactory) : IRazorRequestHandler<CodeAction, CodeAction>
ICodeActionResolveService codeActionResolveService,
RazorLSPOptionsMonitor razorLSPOptionsMonitor) : IRazorRequestHandler<CodeAction, CodeAction>
{
private readonly FrozenDictionary<string, IRazorCodeActionResolver> _razorCodeActionResolvers = CreateResolverMap(razorCodeActionResolvers);
private readonly FrozenDictionary<string, BaseDelegatedCodeActionResolver> _csharpCodeActionResolvers = CreateResolverMap<BaseDelegatedCodeActionResolver>(csharpCodeActionResolvers);
private readonly FrozenDictionary<string, BaseDelegatedCodeActionResolver> _htmlCodeActionResolvers = CreateResolverMap<BaseDelegatedCodeActionResolver>(htmlCodeActionResolvers);
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CodeActionResolveEndpoint>();
private readonly ICodeActionResolveService _codeActionResolveService = codeActionResolveService;
private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor;

public bool MutatesSolutionState => false;

public TextDocumentIdentifier GetTextDocumentIdentifier(CodeAction request)
=> GetRazorCodeActionResolutionParams(request).TextDocument;
=> _codeActionResolveService.GetRazorCodeActionResolutionParams(request).TextDocument;

public async Task<CodeAction> HandleRequestAsync(CodeAction request, RazorRequestContext requestContext, CancellationToken cancellationToken)
{
var resolutionParams = GetRazorCodeActionResolutionParams(request);
var documentContext = requestContext.DocumentContext.AssumeNotNull();

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,
request,
resolutionParams,
cancellationToken).ConfigureAwait(false);
case RazorLanguageKind.Html:
return await ResolveHtmlCodeActionAsync(
documentContext,
request,
resolutionParams,
cancellationToken).ConfigureAwait(false);
default:
_logger.LogError($"Invalid CodeAction.Data.Language. Received {codeActionId}.");
return request;
}
}

private 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 edit = await resolver.ResolveAsync(documentContext, data, cancellationToken).ConfigureAwait(false);
codeAction.Edit = edit;
return codeAction;
}

private Task<CodeAction> ResolveCSharpCodeActionAsync(DocumentContext documentContext, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
=> ResolveDelegatedCodeActionAsync(documentContext, _csharpCodeActionResolvers, codeAction, resolutionParams, cancellationToken);

private Task<CodeAction> ResolveHtmlCodeActionAsync(DocumentContext documentContext, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
=> ResolveDelegatedCodeActionAsync(documentContext, _htmlCodeActionResolvers, codeAction, resolutionParams, cancellationToken);

private async Task<CodeAction> ResolveDelegatedCodeActionAsync(DocumentContext documentContext, FrozenDictionary<string, BaseDelegatedCodeActionResolver> resolvers, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
{
codeAction.Data = resolutionParams.Data;

if (!resolvers.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;
}

var resolvedCodeAction = await resolver.ResolveAsync(documentContext, codeAction, cancellationToken).ConfigureAwait(false);
return resolvedCodeAction;
}

private static FrozenDictionary<string, T> CreateResolverMap<T>(IEnumerable<T> codeActionResolvers)
where T : ICodeActionResolver
{
using var _ = StringDictionaryPool<T>.GetPooledObject(out var resolverMap);

foreach (var resolver in codeActionResolvers)
var options = new RazorFormattingOptions
{
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(CodeActionResolveEndpoint instance)
{
public Task<CodeAction> ResolveRazorCodeActionAsync(DocumentContext documentContext, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
=> instance.ResolveRazorCodeActionAsync(documentContext, codeAction, resolutionParams, cancellationToken);
TabSize = _razorLSPOptionsMonitor.CurrentValue.TabSize,
InsertSpaces = _razorLSPOptionsMonitor.CurrentValue.InsertSpaces,
CodeBlockBraceOnNextLine = _razorLSPOptionsMonitor.CurrentValue.CodeBlockBraceOnNextLine
};
var documentContext = requestContext.DocumentContext.AssumeNotNull();

public Task<CodeAction> ResolveCSharpCodeActionAsync(DocumentContext documentContext, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
=> instance.ResolveCSharpCodeActionAsync(documentContext, codeAction, resolutionParams, cancellationToken);
return await _codeActionResolveService.ResolveCodeActionAsync(documentContext, request, options, cancellationToken).ConfigureAwait(false);

public Task<CodeAction> ResolveHtmlCodeActionAsync(DocumentContext documentContext, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
=> instance.ResolveCSharpCodeActionAsync(documentContext, codeAction, resolutionParams, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,22 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.CodeActions;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;

internal abstract class BaseDelegatedCodeActionResolver(IClientConnection clientConnection) : ICodeActionResolver
internal sealed class DelegatedCodeActionResolver(IClientConnection clientConnection) : IDelegatedCodeActionResolver
{
protected readonly IClientConnection ClientConnection = clientConnection;
private readonly IClientConnection _clientConnection = clientConnection;

public abstract string Action { get; }

public abstract Task<CodeAction> ResolveAsync(DocumentContext documentContext, CodeAction codeAction, CancellationToken cancellationToken);

protected async Task<CodeAction?> ResolveCodeActionWithServerAsync(TextDocumentIdentifier razorFileIdentifier, int hostDocumentVersion, RazorLanguageKind languageKind, CodeAction codeAction, CancellationToken cancellationToken)
public async Task<CodeAction?> ResolveCodeActionAsync(TextDocumentIdentifier razorFileIdentifier, int hostDocumentVersion, RazorLanguageKind languageKind, CodeAction codeAction, CancellationToken cancellationToken)
{
var resolveCodeActionParams = new RazorResolveCodeActionParams(razorFileIdentifier, hostDocumentVersion, languageKind, codeAction);

var resolvedCodeAction = await ClientConnection.SendRequestAsync<RazorResolveCodeActionParams, CodeAction?>(
var resolvedCodeAction = await _clientConnection.SendRequestAsync<RazorResolveCodeActionParams, CodeAction?>(
CustomMessageNames.RazorResolveCodeActionsEndpoint,
resolveCodeActionParams,
cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.Telemetry;
using Microsoft.CodeAnalysis.Razor.CodeActions;
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;
using StreamJsonRpc;

namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;

internal sealed class DelegatedCodeActionsProvider(
IClientConnection clientConnection,
ITelemetryReporter telemetryReporter,
ILoggerFactory loggerFactory) : IDelegatedCodeActionsProvider
{
private readonly IClientConnection _clientConnection = clientConnection;
private readonly ITelemetryReporter _telemetryReporter = telemetryReporter;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<DelegatedCodeActionsProvider>();

public async Task<RazorVSInternalCodeAction[]> GetDelegatedCodeActionsAsync(RazorLanguageKind languageKind, VSCodeActionParams request, int hostDocumentVersion, Guid correlationId, CancellationToken cancellationToken)
{
var delegatedParams = new DelegatedCodeActionParams()
{
HostDocumentVersion = hostDocumentVersion,
CodeActionParams = request,
LanguageKind = languageKind,
CorrelationId = correlationId
};

try
{
return await _clientConnection.SendRequestAsync<DelegatedCodeActionParams, RazorVSInternalCodeAction[]>(CustomMessageNames.RazorProvideCodeActionsEndpoint, delegatedParams, cancellationToken).ConfigureAwait(false);
}
catch (RemoteInvocationException e)
{
_telemetryReporter.ReportFault(e, "Error getting code actions from delegate language server for {languageKind}", languageKind);
_logger.LogError(e, $"Error getting code actions from delegate language server for {languageKind}");
return [];
}
}
}

This file was deleted.

This file was deleted.

Loading

0 comments on commit d371b1e

Please sign in to comment.