Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cohosted Code Actions Part 1: Make code action resolve work on a document #11082

Merged
merged 30 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f12ba51
Remove individual Uri/TextDocument properties, and centralize
davidwengier Oct 23, 2024
0035c8d
Pass in a text document instead of a uri for creation
davidwengier Oct 23, 2024
fefbf23
Fixup: Centralize, or maybe pass
davidwengier Oct 23, 2024
b562c76
Elevate DocumentContext to a parameter on Resolve, instead of each re…
davidwengier Oct 23, 2024
f115584
Make the code actions resolve handler take a document, and plumb through
davidwengier Oct 23, 2024
723ba49
Update tests to pass in the right things
davidwengier Oct 23, 2024
d99d609
Fix generate method tests to pass tag helpers around
davidwengier Oct 23, 2024
aaabd59
Remove unused parameter
davidwengier Oct 23, 2024
e895580
Remove largely unused type
davidwengier Oct 23, 2024
d56e20d
Use a test accessor
davidwengier Oct 23, 2024
a20ca3e
Fix build
davidwengier Oct 23, 2024
af22ba0
Strongly type the language property of the params object
davidwengier Oct 23, 2024
5b5539f
Simplify code action context
davidwengier Oct 23, 2024
1e1a0fc
Remove unnecessary parameter
davidwengier Oct 23, 2024
03e900f
Convert base resolver type to a service
davidwengier Oct 24, 2024
001ebcf
Introduce service for getting code actions from delegated servers
davidwengier Oct 24, 2024
b0926df
Introduce service for using our custom Roslyn endpoints
davidwengier Oct 24, 2024
3eacfbd
Pass options around rather than using the monitor
davidwengier Oct 24, 2024
0f8af98
Move helper method
davidwengier Oct 24, 2024
5e16098
Remove unused usings
davidwengier Oct 24, 2024
6d7d0ab
Move files around
davidwengier Oct 24, 2024
ab55c38
Move string resources
davidwengier Oct 24, 2024
ab20192
Fix all namespace and using directives
davidwengier Oct 24, 2024
cabeb82
Extract code actions functionality to a service
davidwengier Oct 24, 2024
1476b71
Extract code action resolve to a service
davidwengier Oct 24, 2024
52c3c4f
PR Feedback, and a few sneaky using directives that weren't sorted
davidwengier Oct 24, 2024
8ebdce8
Rename files to remove Default from the name
davidwengier Oct 24, 2024
56ad7f7
Cohosted Code Actions Part 3: Move files and extract services (#11084)
davidwengier Oct 24, 2024
d371b1e
Cohosted Code Actions Part 2: Re-layout in advance of moving down (#1…
davidwengier Oct 24, 2024
611af16
Merge remote-tracking branch 'upstream/main' into dev/dawengie/CodeAc…
davidwengier Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,164 +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.Collections.Immutable;
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.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) : IRazorDocumentlessRequestHandler<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 async Task<CodeAction> HandleRequestAsync(CodeAction request, RazorRequestContext requestContext, CancellationToken cancellationToken)
{
if (request.Data is not JsonElement paramsObj)
{
_logger.LogError($"Invalid CodeAction Received '{request.Title}'.");
return request;
}

var resolutionParams = paramsObj.Deserialize<RazorCodeActionResolutionParams>();
if (resolutionParams is null)
{
throw new ArgumentOutOfRangeException($"request.Data should be convertible to {nameof(RazorCodeActionResolutionParams)}");
}
public TextDocumentIdentifier GetTextDocumentIdentifier(CodeAction request)
=> _codeActionResolveService.GetRazorCodeActionResolutionParams(request).TextDocument;

var codeActionId = GetCodeActionId(resolutionParams);
_logger.LogInformation($"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 LanguageServerConstants.CodeActions.Languages.Razor:
return await ResolveRazorCodeActionAsync(
request,
resolutionParams,
cancellationToken).ConfigureAwait(false);
case LanguageServerConstants.CodeActions.Languages.CSharp:
return await ResolveCSharpCodeActionAsync(
request,
resolutionParams,
cancellationToken).ConfigureAwait(false);
case LanguageServerConstants.CodeActions.Languages.Html:
return await ResolveHtmlCodeActionAsync(
request,
resolutionParams,
cancellationToken).ConfigureAwait(false);
default:
_logger.LogError($"Invalid CodeAction.Data.Language. Received {codeActionId}.");
return request;
}
}

// Internal for testing
internal async Task<CodeAction> ResolveRazorCodeActionAsync(
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(data, cancellationToken).ConfigureAwait(false);
codeAction.Edit = edit;
return codeAction;
}

// Internal for testing
internal Task<CodeAction> ResolveCSharpCodeActionAsync(CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
=> ResolveDelegatedCodeActionAsync(_csharpCodeActionResolvers, codeAction, resolutionParams, cancellationToken);

// Internal for testing
internal Task<CodeAction> ResolveHtmlCodeActionAsync(CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
=> ResolveDelegatedCodeActionAsync(_htmlCodeActionResolvers, codeAction, resolutionParams, cancellationToken);

private async Task<CodeAction> ResolveDelegatedCodeActionAsync(FrozenDictionary<string, BaseDelegatedCodeActionResolver> resolvers, CodeAction codeAction, RazorCodeActionResolutionParams resolutionParams, CancellationToken cancellationToken)
{
if (resolutionParams.Data is not JsonElement csharpParamsObj)
{
_logger.LogError($"Invalid CodeAction Received.");
Debug.Fail($"Invalid CSharp CodeAction Received.");
return codeAction;
}

var csharpParams = csharpParamsObj.Deserialize<CodeActionResolveParams>();
if (csharpParams is null)
{
throw new ArgumentOutOfRangeException($"Data was not convertible to {nameof(CodeActionResolveParams)}");
}

codeAction.Data = csharpParams.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(csharpParams, codeAction, cancellationToken).ConfigureAwait(false);
return resolvedCodeAction;
}

private static FrozenDictionary<string, T> CreateResolverMap<T>(IEnumerable<T> codeActionResolvers)
where T : ICodeActionResolver
public async Task<CodeAction> HandleRequestAsync(CodeAction request, RazorRequestContext requestContext, CancellationToken cancellationToken)
{
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)}.");
}
TabSize = _razorLSPOptionsMonitor.CurrentValue.TabSize,
InsertSpaces = _razorLSPOptionsMonitor.CurrentValue.InsertSpaces,
CodeBlockBraceOnNextLine = _razorLSPOptionsMonitor.CurrentValue.CodeBlockBraceOnNextLine
};
var documentContext = requestContext.DocumentContext.AssumeNotNull();

resolverMap[resolver.Action] = resolver;
}
return await _codeActionResolveService.ResolveCodeActionAsync(documentContext, request, options, cancellationToken).ConfigureAwait(false);

return resolverMap.ToFrozenDictionary();
}

private static string GetCodeActionId(RazorCodeActionResolutionParams resolutionParams) =>
$"`{resolutionParams.Language}.{resolutionParams.Action}`";
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
// 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.CodeActions.Models;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
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(CodeActionResolveParams resolveParams, 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.

This file was deleted.

Loading