Skip to content

Commit

Permalink
More CodeAction moves in preparation for cohosting (#11135)
Browse files Browse the repository at this point in the history
Okay, I was wrong when I said the last PR was the final piece before
cohosting actually gets implemented, but this one is, I swear!

Code actions have to run in devenv for Roslyn, so the main thing here is
re-architecting so that the endpoints call the delegated servers,
because calling into the service. Each commit is an isolated step on the
way.
  • Loading branch information
davidwengier authored Nov 1, 2024
2 parents c37ef39 + 5799539 commit 70abe71
Show file tree
Hide file tree
Showing 25 changed files with 255 additions and 225 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public async Task SetupAsync()
{
CodeActionEndpoint = new CodeActionEndpoint(
codeActionsService: RazorLanguageServerHost.GetRequiredService<ICodeActionsService>(),
delegatedCodeActionProvider: RazorLanguageServerHost.GetRequiredService<IDelegatedCodeActionsProvider>(),
telemetryReporter: NoOpTelemetryReporter.Instance);

var projectRoot = Path.Combine(Helpers.GetTestAppsPath(), "ComponentApp");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
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.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;
using Microsoft.CodeAnalysis.Razor.Workspaces.Telemetry;
using Microsoft.VisualStudio.LanguageServer.Protocol;
Expand All @@ -16,32 +21,25 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;
[RazorLanguageServerEndpoint(LspEndpointName)]
internal sealed class CodeActionEndpoint(
ICodeActionsService codeActionsService,
IDelegatedCodeActionsProvider delegatedCodeActionProvider,
ITelemetryReporter telemetryReporter)
: IRazorRequestHandler<VSCodeActionParams, SumType<Command, CodeAction>[]?>, ICapabilitiesProvider
{
private const string LspEndpointName = Methods.TextDocumentCodeActionName;

private readonly ICodeActionsService _codeActionsService = codeActionsService;
private readonly IDelegatedCodeActionsProvider _delegatedCodeActionProvider = delegatedCodeActionProvider;
private readonly ITelemetryReporter _telemetryReporter = telemetryReporter;

internal bool _supportsCodeActionResolve = false;
private bool _supportsCodeActionResolve = false;

public bool MutatesSolutionState { get; } = false;

public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
{
_supportsCodeActionResolve = clientCapabilities.TextDocument?.CodeAction?.ResolveSupport is not null;

serverCapabilities.CodeActionProvider = new CodeActionOptions
{
CodeActionKinds =
[
CodeActionKind.RefactorExtract,
CodeActionKind.QuickFix,
CodeActionKind.Refactor
],
ResolveProvider = true,
};
serverCapabilities.EnableCodeActions();
}

public TextDocumentIdentifier GetTextDocumentIdentifier(VSCodeActionParams request)
Expand All @@ -61,6 +59,53 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSCodeActionParams reque
using var __ = _telemetryReporter.TrackLspRequest(LspEndpointName, LanguageServerConstants.RazorLanguageServerName, TelemetryThresholds.CodeActionRazorTelemetryThreshold, correlationId);
cancellationToken.ThrowIfCancellationRequested();

return await _codeActionsService.GetCodeActionsAsync(request, documentContext, _supportsCodeActionResolve, correlationId, cancellationToken).ConfigureAwait(false);
var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
if (!codeDocument.Source.Text.TryGetAbsoluteIndex(request.Range.Start, out var absoluteIndex))
{
return null;
}

var languageKind = codeDocument.GetLanguageKind(absoluteIndex, rightAssociative: false);
var documentSnapshot = documentContext.Snapshot;

var delegatedCodeActions = languageKind switch
{
RazorLanguageKind.Html => await GetHtmlCodeActionsAsync(documentSnapshot, request, correlationId, cancellationToken).ConfigureAwait(false),
RazorLanguageKind.CSharp => await GetCSharpCodeActionsAsync(documentSnapshot, request, correlationId, cancellationToken).ConfigureAwait(false),
_ => []
};

return await _codeActionsService.GetCodeActionsAsync(request, documentSnapshot, delegatedCodeActions, _supportsCodeActionResolve, cancellationToken).ConfigureAwait(false);
}

private async Task<RazorVSInternalCodeAction[]> GetHtmlCodeActionsAsync(IDocumentSnapshot documentSnapshot, VSCodeActionParams request, Guid correlationId, CancellationToken cancellationToken)
{
var htmlCodeActions = await _delegatedCodeActionProvider.GetDelegatedCodeActionsAsync(RazorLanguageKind.Html, request, documentSnapshot.Version, correlationId, cancellationToken).ConfigureAwait(false);
return htmlCodeActions ?? [];
}

private async Task<RazorVSInternalCodeAction[]> GetCSharpCodeActionsAsync(IDocumentSnapshot documentSnapshot, VSCodeActionParams request, Guid correlationId, CancellationToken cancellationToken)
{
var csharpRequest = await _codeActionsService.GetCSharpCodeActionsRequestAsync(documentSnapshot, request, cancellationToken).ConfigureAwait(false);
if (csharpRequest is null)
{
return [];
}

var csharpCodeActions = await _delegatedCodeActionProvider.GetDelegatedCodeActionsAsync(RazorLanguageKind.CSharp, csharpRequest, documentSnapshot.Version, correlationId, cancellationToken).ConfigureAwait(false);
return csharpCodeActions ?? [];
}

internal TestAccessor GetTestAccessor() => new(this);

internal readonly struct TestAccessor(CodeActionEndpoint instance)
{
public void SetSupportsCodeActionResolve(bool value)
{
instance._supportsCodeActionResolve = value;
}

public Task<RazorVSInternalCodeAction[]> GetCSharpCodeActionsAsync(IDocumentSnapshot documentSnapshot, VSCodeActionParams request, Guid correlationId, CancellationToken cancellationToken)
=> instance.GetCSharpCodeActionsAsync(documentSnapshot, request, correlationId, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
// 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.EndpointContracts;
using Microsoft.CodeAnalysis.Razor.CodeActions;
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;

[RazorLanguageServerEndpoint(Methods.CodeActionResolveName)]
internal sealed class CodeActionResolveEndpoint(
ICodeActionResolveService codeActionResolveService,
IDelegatedCodeActionResolver delegatedCodeActionResolver,
RazorLSPOptionsMonitor razorLSPOptionsMonitor) : IRazorRequestHandler<CodeAction, CodeAction>
{
private readonly ICodeActionResolveService _codeActionResolveService = codeActionResolveService;
private readonly IDelegatedCodeActionResolver _delegatedCodeActionResolver = delegatedCodeActionResolver;
private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor;

public bool MutatesSolutionState => false;

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

public async Task<CodeAction> HandleRequestAsync(CodeAction request, RazorRequestContext requestContext, CancellationToken cancellationToken)
{
Expand All @@ -33,7 +39,28 @@ public async Task<CodeAction> HandleRequestAsync(CodeAction request, RazorReques
};
var documentContext = requestContext.DocumentContext.AssumeNotNull();

return await _codeActionResolveService.ResolveCodeActionAsync(documentContext, request, options, cancellationToken).ConfigureAwait(false);
var context = CodeActionResolveService.GetRazorCodeActionResolutionParams(request);

var resolvedDelegatedCodeAction = context.Language != RazorLanguageKind.Razor
? await ResolveDelegatedCodeActionAsync(documentContext, request, context, cancellationToken).ConfigureAwait(false)
: null;

return await _codeActionResolveService.ResolveCodeActionAsync(documentContext, request, resolvedDelegatedCodeAction, options, cancellationToken).ConfigureAwait(false);
}

private async Task<CodeAction> ResolveDelegatedCodeActionAsync(DocumentContext documentContext, CodeAction request, RazorCodeActionResolutionParams context, CancellationToken cancellationToken)
{
var originalData = request.Data;
request.Data = context.Data;

try
{
var resolvedCodeAction = await _delegatedCodeActionResolver.ResolveCodeActionAsync(documentContext.GetTextDocumentIdentifier(), documentContext.Snapshot.Version, context.Language, request, cancellationToken).ConfigureAwait(false);
return resolvedCodeAction ?? request;
}
finally
{
request.Data = originalData;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.Razor.CodeActions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;

internal interface IDelegatedCodeActionResolver
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;

namespace Microsoft.CodeAnalysis.Razor.CodeActions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;

internal interface IDelegatedCodeActionsProvider
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ public static void EnableInlayHints(this VSInternalServerCapabilities serverCapa
};
}

public static void EnableCodeActions(this VSInternalServerCapabilities serverCapabilities)
{
serverCapabilities.CodeActionProvider = new CodeActionOptions().EnableCodeActions();
}

public static CodeActionOptions EnableCodeActions(this CodeActionOptions options)
{
options.CodeActionKinds =
[
CodeActionKind.RefactorExtract,
CodeActionKind.QuickFix,
CodeActionKind.Refactor
];
options.ResolveProvider = true;

return options;
}

public static void EnableSemanticTokens(this VSInternalServerCapabilities serverCapabilities, ISemanticTokensLegendService legend)
{
serverCapabilities.SemanticTokensOptions = new SemanticTokensOptions().EnableSemanticTokens(legend);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@

namespace Microsoft.CodeAnalysis.Razor.CodeActions;

internal sealed class CSharpCodeActionResolver(
IDelegatedCodeActionResolver delegatedCodeActionResolver,
IRazorFormattingService razorFormattingService) : ICSharpCodeActionResolver
internal sealed class CSharpCodeActionResolver(IRazorFormattingService razorFormattingService) : ICSharpCodeActionResolver
{
private readonly IDelegatedCodeActionResolver _delegatedCodeActionResolver = delegatedCodeActionResolver;
private readonly IRazorFormattingService _razorFormattingService = razorFormattingService;

public string Action => LanguageServerConstants.CodeActions.Default;
Expand All @@ -26,22 +23,21 @@ public async Task<CodeAction> ResolveAsync(
CodeAction codeAction,
CancellationToken cancellationToken)
{
var resolvedCodeAction = await _delegatedCodeActionResolver.ResolveCodeActionAsync(documentContext.GetTextDocumentIdentifier(), documentContext.Snapshot.Version, RazorLanguageKind.CSharp, codeAction, cancellationToken).ConfigureAwait(false);
if (resolvedCodeAction?.Edit?.DocumentChanges is null)
if (codeAction.Edit?.DocumentChanges is null)
{
// Unable to resolve code action with server, return original code action
return codeAction;
}

if (resolvedCodeAction.Edit.DocumentChanges.Value.Count() != 1)
if (codeAction.Edit.DocumentChanges.Value.Count() != 1)
{
// We don't yet support multi-document code actions, return original code action
return codeAction;
}

cancellationToken.ThrowIfCancellationRequested();

var documentChanged = resolvedCodeAction.Edit.DocumentChanges.Value.First();
var documentChanged = codeAction.Edit.DocumentChanges.Value.First();
if (!documentChanged.TryGetFirst(out var textDocumentEdit))
{
// Only Text Document Edit changes are supported currently, return original code action
Expand All @@ -68,7 +64,7 @@ public async Task<CodeAction> ResolveAsync(
{
Uri = documentContext.Uri
};
resolvedCodeAction.Edit = new WorkspaceEdit()
codeAction.Edit = new WorkspaceEdit()
{
DocumentChanges = new TextDocumentEdit[] {
new TextDocumentEdit()
Expand All @@ -79,6 +75,6 @@ public async Task<CodeAction> ResolveAsync(
}
};

return resolvedCodeAction;
return codeAction;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions;
/// <summary>
/// Resolves and remaps the code action, without running formatting passes.
/// </summary>
internal sealed class UnformattedRemappingCSharpCodeActionResolver(
IDelegatedCodeActionResolver delegatedCodeActionResolver,
IDocumentMappingService documentMappingService) : ICSharpCodeActionResolver
internal sealed class UnformattedRemappingCSharpCodeActionResolver(IDocumentMappingService documentMappingService) : ICSharpCodeActionResolver
{
private readonly IDelegatedCodeActionResolver _delegatedCodeActionResolver = delegatedCodeActionResolver;
private readonly IDocumentMappingService _documentMappingService = documentMappingService;

public string Action => LanguageServerConstants.CodeActions.UnformattedRemap;
Expand All @@ -32,21 +29,20 @@ public async Task<CodeAction> ResolveAsync(
{
cancellationToken.ThrowIfCancellationRequested();

var resolvedCodeAction = await _delegatedCodeActionResolver.ResolveCodeActionAsync(documentContext.GetTextDocumentIdentifier(), documentContext.Snapshot.Version, RazorLanguageKind.CSharp, codeAction, cancellationToken).ConfigureAwait(false);
if (resolvedCodeAction?.Edit?.DocumentChanges is null)
if (codeAction.Edit?.DocumentChanges is null)
{
// Unable to resolve code action with server, return original code action
return codeAction;
}

if (resolvedCodeAction.Edit.DocumentChanges.Value.Count() != 1)
if (codeAction.Edit.DocumentChanges.Value.Count() != 1)
{
// We don't yet support multi-document code actions, return original code action
Debug.Fail($"Encountered an unsupported multi-document code action edit with ${codeAction.Title}.");
return codeAction;
}

var documentChanged = resolvedCodeAction.Edit.DocumentChanges.Value.First();
var documentChanged = codeAction.Edit.DocumentChanges.Value.First();
if (!documentChanged.TryGetFirst(out var textDocumentEdit))
{
// Only Text Document Edit changes are supported currently, return original code action
Expand Down Expand Up @@ -78,7 +74,7 @@ public async Task<CodeAction> ResolveAsync(
{
Uri = documentContext.Uri,
};
resolvedCodeAction.Edit = new WorkspaceEdit()
codeAction.Edit = new WorkspaceEdit()
{
DocumentChanges = new[] {
new TextDocumentEdit()
Expand All @@ -89,6 +85,6 @@ public async Task<CodeAction> ResolveAsync(
},
};

return resolvedCodeAction;
return codeAction;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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.Formatting;
Expand All @@ -31,7 +32,7 @@ internal sealed class CodeActionResolveService(
private readonly FrozenDictionary<string, IHtmlCodeActionResolver> _htmlCodeActionResolvers = CreateResolverMap(htmlCodeActionResolvers);
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CodeActionResolveService>();

public async Task<CodeAction> ResolveCodeActionAsync(DocumentContext documentContext, CodeAction request, RazorFormattingOptions options, CancellationToken cancellationToken)
public async Task<CodeAction> ResolveCodeActionAsync(DocumentContext documentContext, CodeAction request, CodeAction? resolvedDelegatedCodeAction, RazorFormattingOptions options, CancellationToken cancellationToken)
{
var resolutionParams = GetRazorCodeActionResolutionParams(request);

Expand All @@ -47,8 +48,6 @@ public async Task<CodeAction> ResolveCodeActionAsync(DocumentContext documentCon
return request;
}

request.Data = resolutionParams.Data;

switch (resolutionParams.Language)
{
case RazorLanguageKind.Razor:
Expand All @@ -61,13 +60,13 @@ public async Task<CodeAction> ResolveCodeActionAsync(DocumentContext documentCon
case RazorLanguageKind.CSharp:
return await ResolveCSharpCodeActionAsync(
documentContext,
request,
resolvedDelegatedCodeAction.AssumeNotNull(),
resolutionParams,
cancellationToken).ConfigureAwait(false);
case RazorLanguageKind.Html:
return await ResolveHtmlCodeActionAsync(
documentContext,
request,
resolvedDelegatedCodeAction.AssumeNotNull(),
resolutionParams,
cancellationToken).ConfigureAwait(false);
default:
Expand All @@ -76,7 +75,7 @@ public async Task<CodeAction> ResolveCodeActionAsync(DocumentContext documentCon
}
}

public RazorCodeActionResolutionParams GetRazorCodeActionResolutionParams(CodeAction request)
public static RazorCodeActionResolutionParams GetRazorCodeActionResolutionParams(CodeAction request)
{
if (request.Data is not JsonElement paramsObj)
{
Expand Down
Loading

0 comments on commit 70abe71

Please sign in to comment.