From 3376535870ad672ea00755a107d53a6fd93b08a4 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 1 Nov 2024 15:59:32 +1100 Subject: [PATCH 01/20] Create basic cohosting infrastrcuture --- eng/targets/Services.props | 1 + .../CSharp/CSharpCodeActionProvider.cs | 2 +- .../TypeAccessibilityCodeActionProvider.cs | 2 +- .../CodeActions/CodeActionsService.cs | 2 +- .../Html/HtmlCodeActionProvider.cs | 2 +- ...omponentAccessibilityCodeActionProvider.cs | 2 +- .../ExtractToCodeBehindCodeActionProvider.cs | 2 +- .../ExtractToComponentCodeActionProvider.cs | 2 +- .../Razor/GenerateMethodCodeActionProvider.cs | 2 +- .../Remote/IRemoteCodeActionsService.cs | 14 ++++ .../Remote/RazorServices.cs | 1 + .../CodeActions/RemoteCodeActionsService.cs | 35 +++++++++ .../CodeActions/RemoteServices.cs | 46 ++++++++++++ .../Cohost/CohostCodeActionsEndpoint.cs | 75 +++++++++++++++++++ 14 files changed, 180 insertions(+), 8 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs diff --git a/eng/targets/Services.props b/eng/targets/Services.props index 0501df0e116..a2e19ca1476 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -33,5 +33,6 @@ + diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/CSharpCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/CSharpCodeActionProvider.cs index 5f893c6e624..b06724851f0 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/CSharpCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/CSharpCodeActionProvider.cs @@ -17,7 +17,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; -internal sealed class CSharpCodeActionProvider(LanguageServerFeatureOptions languageServerFeatureOptions) : ICSharpCodeActionProvider +internal class CSharpCodeActionProvider(LanguageServerFeatureOptions languageServerFeatureOptions) : ICSharpCodeActionProvider { // Internal for testing internal static readonly HashSet SupportedDefaultCodeActionNames = diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs index dde537f6a73..a123b788151 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs @@ -27,7 +27,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; -internal sealed class TypeAccessibilityCodeActionProvider : ICSharpCodeActionProvider +internal class TypeAccessibilityCodeActionProvider : ICSharpCodeActionProvider { private static readonly IEnumerable s_supportedDiagnostics = new[] { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs index 57d07cd17b1..2d65c6d8569 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs @@ -22,7 +22,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; -internal sealed class CodeActionsService( +internal class CodeActionsService( IDocumentMappingService documentMappingService, IEnumerable razorCodeActionProviders, IEnumerable csharpCodeActionProviders, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Html/HtmlCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Html/HtmlCodeActionProvider.cs index a29cbd0083c..4dbe2e9aa41 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Html/HtmlCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Html/HtmlCodeActionProvider.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; -internal sealed class HtmlCodeActionProvider(IEditMappingService editMappingService) : IHtmlCodeActionProvider +internal class HtmlCodeActionProvider(IEditMappingService editMappingService) : IHtmlCodeActionProvider { private readonly IEditMappingService _editMappingService = editMappingService; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs index 28a44f8d41e..c70008b1c32 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs @@ -25,7 +25,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; -internal sealed class ComponentAccessibilityCodeActionProvider : IRazorCodeActionProvider +internal class ComponentAccessibilityCodeActionProvider : IRazorCodeActionProvider { public async Task> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs index 9fd9cd646d8..264e259d0f0 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs @@ -20,7 +20,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; -internal sealed class ExtractToCodeBehindCodeActionProvider(ILoggerFactory loggerFactory) : IRazorCodeActionProvider +internal class ExtractToCodeBehindCodeActionProvider(ILoggerFactory loggerFactory) : IRazorCodeActionProvider { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs index 695e3a5f1fa..64ff97ff825 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs @@ -18,7 +18,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions.Razor; using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; -internal sealed class ExtractToComponentCodeActionProvider() : IRazorCodeActionProvider +internal class ExtractToComponentCodeActionProvider() : IRazorCodeActionProvider { public Task> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionProvider.cs index bd421fabff6..2706d5ac164 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionProvider.cs @@ -20,7 +20,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions.Razor; using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; -internal sealed class GenerateMethodCodeActionProvider : IRazorCodeActionProvider +internal class GenerateMethodCodeActionProvider : IRazorCodeActionProvider { public Task> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs new file mode 100644 index 00000000000..6326752bf9b --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +internal interface IRemoteCodeActionsService : IRemoteJsonService +{ + ValueTask GetCodeActionsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, CodeActionParams request, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs index 0fb7d6c8596..0f2f53574cb 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -36,6 +36,7 @@ internal static class RazorServices (typeof(IRemoteRenameService), null), (typeof(IRemoteGoToImplementationService), null), (typeof(IRemoteDiagnosticsService), null), + (typeof(IRemoteCodeActionsService), null), ]; private const string ComponentName = "Razor"; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs new file mode 100644 index 00000000000..e2660fe8c8f --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.CodeActions; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +internal sealed partial class RemoteCodeActionsService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteCodeActionsService +{ + internal sealed class Factory : FactoryBase + { + protected override IRemoteCodeActionsService CreateService(in ServiceArgs args) + => new RemoteCodeActionsService(in args); + } + + private readonly ICodeActionsService _codeActionsService = args.ExportProvider.GetExportedValue(); + + public ValueTask GetCodeActionsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, CodeActionParams request, CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + razorDocumentId, + context => GetCodeActionsAsync(context, request, cancellationToken), + cancellationToken); + + private ValueTask GetCodeActionsAsync(RemoteDocumentContext context, CodeActionParams request, CancellationToken cancellationToken) + { + return new([]); + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs new file mode 100644 index 00000000000..556fb8b0ca3 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Composition; +using Microsoft.CodeAnalysis.Razor.CodeActions; +using Microsoft.CodeAnalysis.Razor.CodeActions.Razor; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Workspaces; + +namespace Microsoft.CodeAnalysis.Remote.Razor.CodeActions; + +[Export(typeof(ICodeActionsService)), Shared] +[method: ImportingConstructor] +internal sealed class OOPCodeActionsService( + IDocumentMappingService documentMappingService, + [ImportMany] IEnumerable razorCodeActionProviders, + [ImportMany] IEnumerable csharpCodeActionProviders, + [ImportMany] IEnumerable htmlCodeActionProviders, + LanguageServerFeatureOptions languageServerFeatureOptions) + : CodeActionsService(documentMappingService, razorCodeActionProviders, csharpCodeActionProviders, htmlCodeActionProviders, languageServerFeatureOptions); + +[Export(typeof(IRazorCodeActionProvider)), Shared] +[method: ImportingConstructor] +internal sealed class OOPExtractToCodeBehindCodeActionProvider(ILoggerFactory loggerFactory) : ExtractToCodeBehindCodeActionProvider(loggerFactory); + +[Export(typeof(IRazorCodeActionProvider)), Shared] +internal sealed class OOPExtractToComponentCodeActionProvider : ExtractToComponentCodeActionProvider; + +[Export(typeof(IRazorCodeActionProvider)), Shared] +internal sealed class OOPComponentAccessibilityCodeActionProvider : ComponentAccessibilityCodeActionProvider; + +[Export(typeof(IRazorCodeActionProvider)), Shared] +internal sealed class OOPGenerateMethodCodeActionProvider : GenerateMethodCodeActionProvider; + +[Export(typeof(ICSharpCodeActionProvider)), Shared] +internal sealed class OOPTypeAccessibilityCodeActionProvider : TypeAccessibilityCodeActionProvider; + +[Export(typeof(ICSharpCodeActionProvider)), Shared] +[method: ImportingConstructor] +internal sealed class OOPDefaultCSharpCodeActionProvider(LanguageServerFeatureOptions languageServerFeatureOptions) : CSharpCodeActionProvider(languageServerFeatureOptions); + +[Export(typeof(IHtmlCodeActionProvider)), Shared] +[method: ImportingConstructor] +internal sealed class OOPDefaultHtmlCodeActionProvider(IEditMappingService editMappingService) : HtmlCodeActionProvider(editMappingService); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs new file mode 100644 index 00000000000..569883283ce --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Remote; +using Roslyn.LanguageServer.Protocol; +using VSLSP = Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +#pragma warning disable RS0030 // Do not use banned APIs +[Shared] +[CohostEndpoint(Methods.TextDocumentCodeActionName)] +[Export(typeof(IDynamicRegistrationProvider))] +[ExportCohostStatelessLspService(typeof(CohostCodeActionsEndpoint))] +[method: ImportingConstructor] +#pragma warning restore RS0030 // Do not use banned APIs +internal class CohostCodeActionsEndpoint(IRemoteServiceInvoker remoteServiceInvoker) + : AbstractRazorCohostDocumentRequestHandler, IDynamicRegistrationProvider +{ + private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker; + + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => true; + + public ImmutableArray GetRegistrations(VSLSP.VSInternalClientCapabilities clientCapabilities, VSLSP.DocumentFilter[] filter, RazorCohostRequestContext requestContext) + { + if (clientCapabilities.TextDocument?.CodeAction?.DynamicRegistration == true) + { + return [new VSLSP.Registration + { + Method = Methods.TextDocumentCodeActionName, + RegisterOptions = new VSLSP.CodeActionRegistrationOptions() + { + DocumentSelector = filter + }.EnableCodeActions() + }]; + } + + return []; + } + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(CodeActionParams request) + => request.TextDocument.ToRazorTextDocumentIdentifier(); + + protected override Task HandleRequestAsync(CodeActionParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync(context.TextDocument.AssumeNotNull(), request, cancellationToken); + + private async Task HandleRequestAsync(TextDocument razorDocument, CodeActionParams request, CancellationToken cancellationToken) + { + // Normally we could remove the await here, but in this case it neatly converts from ValueTask to Task for us, + // and more importantly this method is essentially a public API entry point (via LSP) so having it appear in + // call stacks is desirable + return await _remoteServiceInvoker.TryInvokeAsync( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => service.GetCodeActionsAsync(solutionInfo, razorDocument.Id, request, cancellationToken), + cancellationToken).ConfigureAwait(false); + } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(CohostCodeActionsEndpoint instance) + { + public Task HandleRequestAsync(TextDocument razorDocument, CodeActionParams request, CancellationToken cancellationToken) + => instance.HandleRequestAsync(razorDocument, request, cancellationToken); + } +} From 6f61999a446828f59b1c2bc1358c4f294d56917a Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 1 Nov 2024 18:08:19 +1100 Subject: [PATCH 02/20] Complete cohosting code actions functionality --- .../RazorLanguageServer.cs | 3 +- .../Remote/CodeActionRequestInfo.cs | 14 + .../Remote/IRemoteCodeActionsService.cs | 17 +- .../RemoteClientInitializationOptions.cs | 6 + .../CodeActions/RemoteCodeActionsService.cs | 49 +- .../RemoteLanguageServerFeatureOptions.cs | 4 +- .../Cohost/CohostCodeActionsEndpoint.cs | 142 ++++- .../Remote/RemoteServiceInvoker.cs | 4 +- .../Cohost/CohostCodeActionsEndpointTest.cs | 490 ++++++++++++++++++ .../Cohost/CohostEndpointTestBase.cs | 9 +- 10 files changed, 703 insertions(+), 35 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/CodeActionRequestInfo.cs create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index a646391abc0..2539ac23cac 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -130,7 +130,6 @@ protected override ILspServices ConstructLspServices() services.AddDocumentManagementServices(featureOptions); services.AddCompletionServices(); services.AddFormattingServices(featureOptions); - services.AddCodeActionsServices(); services.AddOptionsServices(_lspOptions); services.AddHoverServices(); services.AddTextDocumentServices(featureOptions); @@ -140,6 +139,8 @@ protected override ILspServices ConstructLspServices() // Diagnostics services.AddDiagnosticServices(); + services.AddCodeActionsServices(); + // Auto insert services.AddSingleton(); services.AddSingleton(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/CodeActionRequestInfo.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/CodeActionRequestInfo.cs new file mode 100644 index 00000000000..5ffa448ba8d --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/CodeActionRequestInfo.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Text.Json.Serialization; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +internal record CodeActionRequestInfo( + [property: JsonPropertyName("languageKind")] + RazorLanguageKind LanguageKind, + [property: JsonPropertyName("csharpRequest")] + VSCodeActionParams? CSharpRequest); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs index 6326752bf9b..c7f0b69feed 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs @@ -4,11 +4,24 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ExternalAccess.Razor; -using Roslyn.LanguageServer.Protocol; +using Microsoft.CodeAnalysis.Razor.CodeActions.Models; +using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; +using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Remote; internal interface IRemoteCodeActionsService : IRemoteJsonService { - ValueTask GetCodeActionsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, CodeActionParams request, CancellationToken cancellationToken); + ValueTask GetCodeActionRequestInfoAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId razorDocumentId, + VSCodeActionParams request, + CancellationToken cancellationToken); + + ValueTask[]?> GetCodeActionsAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId razorDocumentId, + VSCodeActionParams request, + RazorVSInternalCodeAction[] delegatedCodeActions, + CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteClientInitializationOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteClientInitializationOptions.cs index 5bd3a09b409..d3b6331cf3c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteClientInitializationOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteClientInitializationOptions.cs @@ -27,4 +27,10 @@ internal struct RemoteClientInitializationOptions [JsonPropertyName("forceRuntimeCodeGeneration")] public required bool ForceRuntimeCodeGeneration { get; set; } + + [JsonPropertyName("supportsFileManipulation")] + public required bool SupportsFileManipulation { get; set; } + + [JsonPropertyName("showAllCSharpCodeActions")] + public required bool ShowAllCSharpCodeActions { get; set; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs index e2660fe8c8f..249208c3167 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs @@ -3,11 +3,15 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.CodeActions; +using Microsoft.CodeAnalysis.Razor.CodeActions.Models; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; using Microsoft.CodeAnalysis.Razor.Remote; using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; -using Roslyn.LanguageServer.Protocol; +using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Remote.Razor; @@ -20,16 +24,49 @@ protected override IRemoteCodeActionsService CreateService(in ServiceArgs args) } private readonly ICodeActionsService _codeActionsService = args.ExportProvider.GetExportedValue(); + private readonly IClientCapabilitiesService _clientCapabilitiesService = args.ExportProvider.GetExportedValue(); - public ValueTask GetCodeActionsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, CodeActionParams request, CancellationToken cancellationToken) - => RunServiceAsync( + public ValueTask GetCodeActionRequestInfoAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, VSCodeActionParams request, CancellationToken cancellationToken) + => RunServiceAsync( solutionInfo, razorDocumentId, - context => GetCodeActionsAsync(context, request, cancellationToken), + context => GetCodeActionRequestInfoAsync(context, request, cancellationToken), cancellationToken); - private ValueTask GetCodeActionsAsync(RemoteDocumentContext context, CodeActionParams request, CancellationToken cancellationToken) + private async ValueTask GetCodeActionRequestInfoAsync(RemoteDocumentContext context, VSCodeActionParams request, CancellationToken cancellationToken) { - return new([]); + var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + var absoluteIndex = codeDocument.Source.Text.GetRequiredAbsoluteIndex(request.Range.Start); + + var languageKind = codeDocument.GetLanguageKind(absoluteIndex, rightAssociative: false); + + VSCodeActionParams? csharpRequest = null; + if (languageKind == RazorLanguageKind.CSharp) + { + csharpRequest = await _codeActionsService.GetCSharpCodeActionsRequestAsync(context.Snapshot, request, cancellationToken).ConfigureAwait(false); + + if (csharpRequest is not null) + { + // Since we're here, we may as well fill in the generated document Uri so the other caller won't have to calculate it + var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync(cancellationToken).ConfigureAwait(false); + csharpRequest.TextDocument.Uri = generatedDocument.CreateUri(); + } + } + + return new CodeActionRequestInfo(languageKind, csharpRequest); + } + + public ValueTask[]?> GetCodeActionsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, VSCodeActionParams request, RazorVSInternalCodeAction[] delegatedCodeActions, CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + razorDocumentId, + context => GetCodeActionsAsync(context, request, delegatedCodeActions, cancellationToken), + cancellationToken); + + private async ValueTask[]?> GetCodeActionsAsync(RemoteDocumentContext context, VSCodeActionParams request, RazorVSInternalCodeAction[] delegatedCodeActions, CancellationToken cancellationToken) + { + var supportsCodeActionResolve = _clientCapabilitiesService.ClientCapabilities.TextDocument?.CodeAction?.ResolveSupport is not null; + return await _codeActionsService.GetCodeActionsAsync(request, context.Snapshot, delegatedCodeActions, supportsCodeActionResolve, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs index ed6b2ddb602..c4fe60491a6 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Initialization/RemoteLanguageServerFeatureOptions.cs @@ -17,7 +17,7 @@ internal class RemoteLanguageServerFeatureOptions : LanguageServerFeatureOptions public void SetOptions(RemoteClientInitializationOptions options) => _options = options; - public override bool SupportsFileManipulation => throw new InvalidOperationException("This option has not been synced to OOP."); + public override bool SupportsFileManipulation => _options.SupportsFileManipulation; public override string CSharpVirtualDocumentSuffix => _options.CSharpVirtualDocumentSuffix; @@ -29,7 +29,7 @@ internal class RemoteLanguageServerFeatureOptions : LanguageServerFeatureOptions public override bool UsePreciseSemanticTokenRanges => _options.UsePreciseSemanticTokenRanges; - public override bool ShowAllCSharpCodeActions => throw new InvalidOperationException("This option has not been synced to OOP."); + public override bool ShowAllCSharpCodeActions => _options.ShowAllCSharpCodeActions; public override bool UpdateBuffersForClosedDocuments => throw new InvalidOperationException("This option has not been synced to OOP."); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs index 569883283ce..916199c07b4 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs @@ -1,17 +1,27 @@ // 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.Generic; using System.Collections.Immutable; using System.Composition; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; +using Microsoft.CodeAnalysis.Razor.CodeActions.Models; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; using Microsoft.CodeAnalysis.Razor.Remote; -using Roslyn.LanguageServer.Protocol; -using VSLSP = Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.CodeAnalysis.Razor.Workspaces.Telemetry; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; @@ -22,54 +32,148 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; [ExportCohostStatelessLspService(typeof(CohostCodeActionsEndpoint))] [method: ImportingConstructor] #pragma warning restore RS0030 // Do not use banned APIs -internal class CohostCodeActionsEndpoint(IRemoteServiceInvoker remoteServiceInvoker) - : AbstractRazorCohostDocumentRequestHandler, IDynamicRegistrationProvider +internal class CohostCodeActionsEndpoint( + IRemoteServiceInvoker remoteServiceInvoker, + IClientCapabilitiesService clientCapabilitiesService, + IHtmlDocumentSynchronizer htmlDocumentSynchronizer, + LSPRequestInvoker requestInvoker, + ITelemetryReporter telemetryReporter) + : AbstractRazorCohostDocumentRequestHandler[]?>, IDynamicRegistrationProvider { private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker; + private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService; + private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer; + private readonly LSPRequestInvoker _requestInvoker = requestInvoker; + private readonly ITelemetryReporter _telemetryReporter = telemetryReporter; protected override bool MutatesSolutionState => false; protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSLSP.VSInternalClientCapabilities clientCapabilities, VSLSP.DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.CodeAction?.DynamicRegistration == true) { - return [new VSLSP.Registration + return [new Registration { Method = Methods.TextDocumentCodeActionName, - RegisterOptions = new VSLSP.CodeActionRegistrationOptions() - { - DocumentSelector = filter - }.EnableCodeActions() + RegisterOptions = new CodeActionRegistrationOptions().EnableCodeActions() }]; } return []; } - protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(CodeActionParams request) + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(VSCodeActionParams request) => request.TextDocument.ToRazorTextDocumentIdentifier(); - protected override Task HandleRequestAsync(CodeActionParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + protected override Task[]?> HandleRequestAsync(VSCodeActionParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) => HandleRequestAsync(context.TextDocument.AssumeNotNull(), request, cancellationToken); - private async Task HandleRequestAsync(TextDocument razorDocument, CodeActionParams request, CancellationToken cancellationToken) + private async Task[]?> HandleRequestAsync(TextDocument razorDocument, VSCodeActionParams request, CancellationToken cancellationToken) { - // Normally we could remove the await here, but in this case it neatly converts from ValueTask to Task for us, - // and more importantly this method is essentially a public API entry point (via LSP) so having it appear in - // call stacks is desirable - return await _remoteServiceInvoker.TryInvokeAsync( + var correlationId = Guid.NewGuid(); + using var _ = _telemetryReporter.TrackLspRequest(Methods.TextDocumentCodeActionName, LanguageServerConstants.RazorLanguageServerName, TelemetryThresholds.CodeActionRazorTelemetryThreshold, correlationId); + + var requestInfo = await _remoteServiceInvoker.TryInvokeAsync( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => service.GetCodeActionRequestInfoAsync(solutionInfo, razorDocument.Id, request, cancellationToken), + cancellationToken).ConfigureAwait(false); + + if (requestInfo is null || + requestInfo.LanguageKind == RazorLanguageKind.CSharp && requestInfo.CSharpRequest is null) + { + return null; + } + + var delegatedCodeActions = requestInfo.LanguageKind switch + { + RazorLanguageKind.Html => await GetHtmlCodeActionsAsync(razorDocument, request, correlationId, cancellationToken).ConfigureAwait(false), + RazorLanguageKind.CSharp => await GetCSharpCodeActionsAsync(razorDocument, requestInfo.CSharpRequest.AssumeNotNull(), correlationId, cancellationToken).ConfigureAwait(false), + _ => [] + }; + + return await _remoteServiceInvoker.TryInvokeAsync[]?>( razorDocument.Project.Solution, - (service, solutionInfo, cancellationToken) => service.GetCodeActionsAsync(solutionInfo, razorDocument.Id, request, cancellationToken), + (service, solutionInfo, cancellationToken) => service.GetCodeActionsAsync(solutionInfo, razorDocument.Id, request, delegatedCodeActions, cancellationToken), cancellationToken).ConfigureAwait(false); } + private async Task GetCSharpCodeActionsAsync(TextDocument razorDocument, VSCodeActionParams request, Guid correlationId, CancellationToken cancellationToken) + { + var generatedDocumentIds = razorDocument.Project.Solution.GetDocumentIdsWithUri(request.TextDocument.Uri); + var generatedDocumentId = generatedDocumentIds.FirstOrDefault(d => d.ProjectId == razorDocument.Project.Id); + if (generatedDocumentId is null) + { + return []; + } + + if (razorDocument.Project.GetDocument(generatedDocumentId) is not { } generatedDocument) + { + return []; + } + + var options = new JsonSerializerOptions(); + foreach (var converter in RazorServiceDescriptorsWrapper.GetLspConverters()) + { + options.Converters.Add(converter); + } + + var csharpRequest = JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(request), options).AssumeNotNull(); + + using var _ = _telemetryReporter.TrackLspRequest(Methods.TextDocumentCodeActionName, "Razor.ExternalAccess", TelemetryThresholds.CodeActionSubLSPTelemetryThreshold, correlationId); + var csharpCodeActions = await CodeActions.GetCodeActionsAsync(generatedDocument, csharpRequest, _clientCapabilitiesService.ClientCapabilities.SupportsVisualStudioExtensions, cancellationToken).ConfigureAwait(false); + + return JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(csharpCodeActions), options).AssumeNotNull(); + } + + private async Task GetHtmlCodeActionsAsync(TextDocument razorDocument, VSCodeActionParams request, Guid correlationId, CancellationToken cancellationToken) + { + var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false); + if (htmlDocument is null) + { + return []; + } + + // We don't want to create a new request, and risk losing data, so we just tweak the Uri and + // set it back again at the end + var oldTdi = request.TextDocument; + try + { + request.TextDocument = new VSTextDocumentIdentifier { Uri = htmlDocument.Uri }; + + using var _ = _telemetryReporter.TrackLspRequest(Methods.TextDocumentCodeActionName, RazorLSPConstants.HtmlLanguageServerName, TelemetryThresholds.CodeActionSubLSPTelemetryThreshold, correlationId); + var result = await _requestInvoker.ReinvokeRequestOnServerAsync( + htmlDocument.Buffer, + Methods.TextDocumentCodeActionName, + RazorLSPConstants.HtmlLanguageServerName, + request, + cancellationToken).ConfigureAwait(false); + + if (result?.Response is null) + { + return []; + } + + // WebTools is still using Newtonsoft, so we have to convert to STJ + foreach (var codeAction in result.Response) + { + codeAction.Data = JsonHelpers.TryConvertFromJObject(codeAction.Data); + } + + return result.Response; + } + finally + { + request.TextDocument = oldTdi; + } + } + internal TestAccessor GetTestAccessor() => new(this); internal readonly struct TestAccessor(CohostCodeActionsEndpoint instance) { - public Task HandleRequestAsync(TextDocument razorDocument, CodeActionParams request, CancellationToken cancellationToken) + public Task[]?> HandleRequestAsync(TextDocument razorDocument, VSCodeActionParams request, CancellationToken cancellationToken) => instance.HandleRequestAsync(razorDocument, request, cancellationToken); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs index 02ed22ce44a..214463e6bd7 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs @@ -138,7 +138,9 @@ private async Task InitializeRemoteClientAsync(RazorRemoteHostClient remoteClien HtmlVirtualDocumentSuffix = _languageServerFeatureOptions.HtmlVirtualDocumentSuffix, IncludeProjectKeyInGeneratedFilePath = _languageServerFeatureOptions.IncludeProjectKeyInGeneratedFilePath, ReturnCodeActionAndRenamePathsWithPrefixedSlash = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash, - ForceRuntimeCodeGeneration = _languageServerFeatureOptions.ForceRuntimeCodeGeneration + ForceRuntimeCodeGeneration = _languageServerFeatureOptions.ForceRuntimeCodeGeneration, + SupportsFileManipulation = _languageServerFeatureOptions.SupportsFileManipulation, + ShowAllCSharpCodeActions = _languageServerFeatureOptions.ShowAllCSharpCodeActions, }; _logger.LogDebug($"First OOP call, so initializing OOP service."); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs new file mode 100644 index 00000000000..6b31cd14179 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs @@ -0,0 +1,490 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.Telemetry; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.CodeActions.Models; +using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +public class CohostCodeActionsEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +{ + [Fact] + public async Task GenerateConstructor() + { + var input = """ + +
+ + @code + { + public class [||]Goo + { + } + } + + """; + + var expected = """ + +
+ + @code + { + public class Goo + { + public Goo() + { + } + } + } + + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.GenerateDefaultConstructors); + } + + [Fact] + public async Task IntroduceLocal() + { + var input = """ + @using System.Linq + +
+ + @code + { + void M(string[] args) + { + if ([|args.First()|].Length > 0) + { + } + if (args.First().Length > 0) + { + } + } + } + + """; + + var expected = """ + @using System.Linq + +
+ + @code + { + void M(string[] args) + { + string v = args.First(); + if (v.Length > 0) + { + } + if (args.First().Length > 0) + { + } + } + } + + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.IntroduceVariable); + } + + [Fact] + public async Task IntroduceLocal_All() + { + var input = """ + @using System.Linq + +
+ + @code + { + void M(string[] args) + { + if ([|args.First()|].Length > 0) + { + } + if (args.First().Length > 0) + { + } + } + } + + """; + + var expected = """ + @using System.Linq + +
+ + @code + { + void M(string[] args) + { + string v = args.First(); + if (v.Length > 0) + { + } + if (v.Length > 0) + { + } + } + } + + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.IntroduceVariable, childActionIndex: 1); + } + + [Fact] + public async Task ConvertConcatenationToInterpolatedString_CSharpStatement() + { + var input = """ + @{ + var x = "he[||]l" + "lo" + Environment.NewLine + "world"; + } + """; + + var expected = """ + @{ + var x = $"hello{Environment.NewLine}world"; + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.ConvertConcatenationToInterpolatedString); + } + + [Fact] + public async Task ConvertConcatenationToInterpolatedString_ExplicitExpression() + { + var input = """ + @("he[||]l" + "lo" + Environment.NewLine + "world") + """; + + var expected = """ + @($"hello{Environment.NewLine}world") + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.ConvertConcatenationToInterpolatedString); + } + + [Fact] + public async Task ConvertConcatenationToInterpolatedString_CodeBlock() + { + var input = """ + @code + { + private string _x = "he[||]l" + "lo" + Environment.NewLine + "world"; + } + """; + + var expected = """ + @code + { + private string _x = $"hello{Environment.NewLine}world"; + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.ConvertConcatenationToInterpolatedString); + } + + [Fact] + public async Task ConvertBetweenRegularAndVerbatimInterpolatedString_CodeBlock() + { + var input = """ + @code + { + private string _x = $@"h[||]ello world"; + } + """; + + var expected = """ + @code + { + private string _x = $"hello world"; + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.ConvertBetweenRegularAndVerbatimInterpolatedString); + } + + [Fact] + public async Task ConvertBetweenRegularAndVerbatimInterpolatedString_CodeBlock2() + { + var input = """ + @code + { + private string _x = $"h[||]ello\\nworld"; + } + """; + + var expected = """ + @code + { + private string _x = $@"hello\nworld"; + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.ConvertBetweenRegularAndVerbatimInterpolatedString); + } + + [Fact] + public async Task ConvertBetweenRegularAndVerbatimString_CodeBlock() + { + var input = """ + @code + { + private string _x = @"h[||]ello world"; + } + """; + + var expected = """ + @code + { + private string _x = "hello world"; + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.ConvertBetweenRegularAndVerbatimString); + } + + [Fact] + public async Task ConvertBetweenRegularAndVerbatimString_CodeBlock2() + { + var input = """ + @code + { + private string _x = "h[||]ello\\nworld"; + } + """; + + var expected = """ + @code + { + private string _x = @"hello\nworld"; + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.ConvertBetweenRegularAndVerbatimString); + } + + [Fact] + public async Task ConvertPlaceholderToInterpolatedString_CodeBlock() + { + var input = """ + @code + { + private string _x = [|string.Format("hello{0}world", Environment.NewLine)|]; + } + """; + + var expected = """ + @code + { + private string _x = $"hello{Environment.NewLine}world"; + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.ConvertPlaceholderToInterpolatedString); + } + + [Fact] + public async Task ConvertToInterpolatedString_CodeBlock() + { + var input = """ + @code + { + private string _x = [||]"hello {"; + } + """; + + var expected = """ + @code + { + private string _x = $"hello {{"; + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.ConvertToInterpolatedString); + } + + [Fact] + public async Task AddDebuggerDisplay() + { + var input = """ + @code { + class Goo[||] + { + + } + } + """; + + var expected = """ + @using System.Diagnostics + @code { + [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] + class Goo + { + private string GetDebuggerDisplay() + { + return ToString(); + } + } + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.AddDebuggerDisplay); + } + + [Fact] + public async Task AddUsing() + { + var input = """ + @code + { + private [||]StringBuilder _x = new StringBuilder(); + } + """; + + var expected = """ + @using System.Text + @code + { + private StringBuilder _x = new StringBuilder(); + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeFixProviderNames.AddImport); + } + + [Fact] + public async Task AddUsing_WithExisting() + { + var input = """ + @using System + @using System.Collections.Generic + + @code + { + private [||]StringBuilder _x = new StringBuilder(); + } + """; + + var expected = """ + @using System + @using System.Collections.Generic + @using System.Text + + @code + { + private StringBuilder _x = new StringBuilder(); + } + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeFixProviderNames.AddImport); + } + + [Theory] + [InlineData("[||]DoesNotExist")] + [InlineData("Does[||]NotExist")] + [InlineData("DoesNotExist[||]")] + public async Task Handle_GenerateMethod_NoCodeBlock_NonEmptyTrailingLine(string cursorAndMethodName) + { + var input = $$""" + + """; + + var expected = """ + + @code { + private void DoesNotExist(global::Microsoft.AspNetCore.Components.Web.MouseEventArgs e) + { + throw new global::System.NotImplementedException(); + } + } + """; + + await VerifyCodeActionAsync(input, expected, "Generate Event Handler 'DoesNotExist'"); + } + + private async Task VerifyCodeActionAsync(TestCode input, string? expected, string codeActionName, int childActionIndex = 0, string? fileKind = null) + { + UpdateClientLSPInitializationOptions(options => + { + options.ClientCapabilities.TextDocument = new() + { + CodeAction = new() + { + ResolveSupport = new() + } + }; + + return options; + }); + + var document = await CreateProjectAndRazorDocumentAsync(input.Text, fileKind, createSeparateRemoteAndLocalWorkspaces: true); + var inputText = await document.GetTextAsync(DisposalToken); + + var requestInvoker = new TestLSPRequestInvoker(); + + var endpoint = new CohostCodeActionsEndpoint(RemoteServiceInvoker, ClientCapabilitiesService, TestHtmlDocumentSynchronizer.Instance, requestInvoker, NoOpTelemetryReporter.Instance); + + using var diagnostics = new PooledArrayBuilder(); + foreach (var (code, spans) in input.NamedSpans) + { + if (code.Length == 0) + { + continue; + } + + foreach (var textSpan in spans) + { + diagnostics.Add(new Diagnostic + { + Code = code, + Range = inputText.GetRange(textSpan) + }); + } + } + + var request = new VSCodeActionParams + { + TextDocument = new VSTextDocumentIdentifier { Uri = document.CreateUri() }, + Range = inputText.GetRange(input.NamedSpans[""].Single()), + Context = new VSInternalCodeActionContext() { Diagnostics = diagnostics.ToArray() } + }; + + var result = await endpoint.GetTestAccessor().HandleRequestAsync(document, request, DisposalToken); + + if (expected is null) + { + Assert.Null(result); + return; + } + + Assert.NotNull(result); + Assert.NotEmpty(result); + + var codeActionToRun = (VSInternalCodeAction?)result.SingleOrDefault(e => ((RazorVSInternalCodeAction)e.Value!).Name == codeActionName || ((RazorVSInternalCodeAction)e.Value!).Title == codeActionName).Value; + Assert.NotNull(codeActionToRun); + + if (codeActionToRun.Children?.Length > 0) + { + codeActionToRun = codeActionToRun.Children[childActionIndex]; + } + + Assert.NotNull(codeActionToRun); + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs index cc72ed18a4c..81334073c1e 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Basic.Reference.Assemblies; using Microsoft.AspNetCore.Razor; @@ -37,7 +36,7 @@ public abstract class CohostEndpointTestBase(ITestOutputHelper testOutputHelper) private protected TestRemoteServiceInvoker RemoteServiceInvoker => _remoteServiceInvoker.AssumeNotNull(); private protected IFilePathService FilePathService => _filePathService.AssumeNotNull(); private protected RemoteLanguageServerFeatureOptions FeatureOptions => OOPExportProvider.GetExportedValue(); - private protected RemoteClientCapabilitiesService ClientCapabilities => OOPExportProvider.GetExportedValue(); + private protected RemoteClientCapabilitiesService ClientCapabilitiesService => OOPExportProvider.GetExportedValue(); private protected RemoteSemanticTokensLegendService SemanticTokensLegendService => OOPExportProvider.GetExportedValue(); /// @@ -65,7 +64,9 @@ protected override async Task InitializeAsync() UsePreciseSemanticTokenRanges = false, UseRazorCohostServer = true, ReturnCodeActionAndRenamePathsWithPrefixedSlash = false, - ForceRuntimeCodeGeneration = false + ForceRuntimeCodeGeneration = false, + SupportsFileManipulation = true, + ShowAllCSharpCodeActions = false, }; UpdateClientInitializationOptions(c => c); @@ -92,7 +93,7 @@ private protected void UpdateClientInitializationOptions(Func mutation) { _clientLSPInitializationOptions = mutation(_clientLSPInitializationOptions); - ClientCapabilities.SetCapabilities(_clientLSPInitializationOptions.ClientCapabilities); + ClientCapabilitiesService.SetCapabilities(_clientLSPInitializationOptions.ClientCapabilities); SemanticTokensLegendService.SetLegend(_clientLSPInitializationOptions.TokenTypes, _clientLSPInitializationOptions.TokenModifiers); } From 61ca99bde754a4bf4c4fd9b8d5d0745b99e7e4b1 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 4 Nov 2024 11:48:30 +1100 Subject: [PATCH 03/20] Report better MEF composition errors in tests --- .../Cohost/CohostEndpointTestBase.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs index 81334073c1e..16ded3e8e93 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs @@ -20,6 +20,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Composition; using Microsoft.VisualStudio.LanguageServer.Protocol; +using Xunit; using Xunit.Abstractions; namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; @@ -50,7 +51,19 @@ protected override async Task InitializeAsync() // Create a new isolated MEF composition. // Note that this uses a cached catalog and configuration for performance. - _exportProvider = await RemoteMefComposition.CreateExportProviderAsync(DisposalToken); + try + { + _exportProvider = await RemoteMefComposition.CreateExportProviderAsync(DisposalToken); + } + catch (CompositionFailedException ex) when (ex.Errors is not null) + { + Assert.Fail($""" + Errors in the Remote MEF composition: + + {string.Join(Environment.NewLine, ex.Errors.SelectMany(e => e).Select(e => e.Message))} + """); + } + AddDisposable(_exportProvider); _remoteServiceInvoker = new TestRemoteServiceInvoker(JoinableTaskContext, _exportProvider, LoggerFactory); From 60ed8c036ce7d3a9eb2669ca1771b32917d935af Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 4 Nov 2024 12:38:38 +1100 Subject: [PATCH 04/20] Add delegated document Uri to code action data --- .../CodeActions/CodeActionEndpoint.cs | 8 +++++++- .../TypeAccessibilityCodeActionProvider.cs | 2 +- .../CodeActions/CodeActionsService.cs | 17 ++++++++++------- .../CodeActions/ICodeActionsService.cs | 9 ++++++++- .../CodeActions/Models/CodeActionExtensions.cs | 6 +++++- .../Models/RazorCodeActionResolutionParams.cs | 4 ++++ .../Razor/AddUsingsCodeActionResolver.cs | 3 ++- .../ComponentAccessibilityCodeActionProvider.cs | 3 ++- .../ExtractToCodeBehindCodeActionProvider.cs | 1 + .../ExtractToComponentCodeActionProvider.cs | 1 + .../Razor/GenerateMethodCodeActionProvider.cs | 4 ++-- .../CodeActions/Razor/RazorCodeActionFactory.cs | 6 ++++-- .../CodeActions/RazorCodeActionContext.cs | 2 ++ .../CodeActions/RemoteCodeActionsService.cs | 5 ++++- 14 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CodeActionEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CodeActionEndpoint.cs index 47e7c74ede1..006dd113295 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CodeActionEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CodeActionEndpoint.cs @@ -75,7 +75,13 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSCodeActionParams reque _ => [] }; - return await _codeActionsService.GetCodeActionsAsync(request, documentSnapshot, delegatedCodeActions, _supportsCodeActionResolve, cancellationToken).ConfigureAwait(false); + return await _codeActionsService.GetCodeActionsAsync( + request, + documentSnapshot, + delegatedCodeActions, + delegatedDocumentUri: null, // We don't use delegatedDocumentUri in the LSP server, as we can trivially recalculate it + _supportsCodeActionResolve, + cancellationToken).ConfigureAwait(false); } private async Task GetHtmlCodeActionsAsync(IDocumentSnapshot documentSnapshot, VSCodeActionParams request, Guid correlationId, CancellationToken cancellationToken) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs index a123b788151..d60297bbc00 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs @@ -144,7 +144,7 @@ private static ImmutableArray ProcessCodeActionsVSCod var fqnCodeAction = CreateFQNCodeAction(context, diagnostic, codeAction, fqn); typeAccessibilityCodeActions.Add(fqnCodeAction); - if (AddUsingsCodeActionResolver.TryCreateAddUsingResolutionParams(fqn, context.Request.TextDocument, additionalEdit: null, out var @namespace, out var resolutionParams)) + if (AddUsingsCodeActionResolver.TryCreateAddUsingResolutionParams(fqn, context.Request.TextDocument, additionalEdit: null, context.DelegatedDocumentUri, out var @namespace, out var resolutionParams)) { var addUsingCodeAction = RazorCodeActionFactory.CreateAddComponentUsing(@namespace, newTagName: null, resolutionParams); typeAccessibilityCodeActions.Add(addUsingCodeAction); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs index 2d65c6d8569..1733c98d3b1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs @@ -1,6 +1,7 @@ // 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.Generic; using System.Collections.Immutable; using System.Linq; @@ -37,9 +38,9 @@ internal class CodeActionsService( private readonly IEnumerable _htmlCodeActionProviders = htmlCodeActionProviders; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; - public async Task[]?> GetCodeActionsAsync(VSCodeActionParams request, IDocumentSnapshot documentSnapshot, RazorVSInternalCodeAction[] delegatedCodeActions, bool supportsCodeActionResolve, CancellationToken cancellationToken) + public async Task[]?> GetCodeActionsAsync(VSCodeActionParams request, IDocumentSnapshot documentSnapshot, RazorVSInternalCodeAction[] delegatedCodeActions, Uri? delegatedDocumentUri, bool supportsCodeActionResolve, CancellationToken cancellationToken) { - var razorCodeActionContext = await GenerateRazorCodeActionContextAsync(request, documentSnapshot, supportsCodeActionResolve, cancellationToken).ConfigureAwait(false); + var razorCodeActionContext = await GenerateRazorCodeActionContextAsync(request, documentSnapshot, delegatedDocumentUri, supportsCodeActionResolve, cancellationToken).ConfigureAwait(false); if (razorCodeActionContext is null) { return null; @@ -64,12 +65,12 @@ internal class CodeActionsService( // Grouping the code actions causes VS to sort them into groups, rather than just alphabetically sorting them // by title. The latter is bad for us because it can put "Remove
" at the top in some locales, and our fully // qualify component code action at the bottom, depending on the users namespace. - ConvertCodeActionsToSumType(request.TextDocument, razorCodeActions, "A-Razor"); - ConvertCodeActionsToSumType(request.TextDocument, filteredCodeActions, "B-Delegated"); + ConvertCodeActionsToSumType(razorCodeActions, "A-Razor"); + ConvertCodeActionsToSumType(filteredCodeActions, "B-Delegated"); return commandsOrCodeActions.ToArray(); - void ConvertCodeActionsToSumType(VSTextDocumentIdentifier textDocument, ImmutableArray codeActions, string groupName) + void ConvertCodeActionsToSumType(ImmutableArray codeActions, string groupName) { // We must cast the RazorCodeAction into a platform compliant code action // For VS (SupportsCodeActionResolve = true) this means just encapsulating the RazorCodeAction in the `CommandOrCodeAction` struct @@ -87,7 +88,7 @@ void ConvertCodeActionsToSumType(VSTextDocumentIdentifier textDocument, Immutabl { foreach (var action in codeActions) { - commandsOrCodeActions.Add(action.AsVSCodeCommandOrCodeAction(textDocument)); + commandsOrCodeActions.Add(action.AsVSCodeCommandOrCodeAction(request.TextDocument, delegatedDocumentUri)); } } } @@ -96,6 +97,7 @@ void ConvertCodeActionsToSumType(VSTextDocumentIdentifier textDocument, Immutabl private async Task GenerateRazorCodeActionContextAsync( VSCodeActionParams request, IDocumentSnapshot documentSnapshot, + Uri? delegatedDocumentUri, bool supportsCodeActionResolve, CancellationToken cancellationToken) { @@ -136,6 +138,7 @@ void ConvertCodeActionsToSumType(VSTextDocumentIdentifier textDocument, Immutabl request, documentSnapshot, codeDocument, + delegatedDocumentUri, startLocation, endLocation, languageKind, @@ -296,6 +299,6 @@ private static ImmutableHashSet GetAllAvailableCodeActionNames() internal readonly struct TestAccessor(CodeActionsService instance) { public Task GenerateRazorCodeActionContextAsync(VSCodeActionParams request, IDocumentSnapshot documentSnapshot, bool supportsCodeActionResolve, CancellationToken cancellationToken) - => instance.GenerateRazorCodeActionContextAsync(request, documentSnapshot, supportsCodeActionResolve, cancellationToken); + => instance.GenerateRazorCodeActionContextAsync(request, documentSnapshot, delegatedDocumentUri: null, supportsCodeActionResolve, cancellationToken); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/ICodeActionsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/ICodeActionsService.cs index d968064976b..3cd139b99c1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/ICodeActionsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/ICodeActionsService.cs @@ -1,6 +1,7 @@ // 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.CodeAnalysis.Razor.CodeActions.Models; @@ -12,7 +13,13 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; internal interface ICodeActionsService { - Task[]?> GetCodeActionsAsync(VSCodeActionParams request, IDocumentSnapshot documentSnapshot, RazorVSInternalCodeAction[] delegatedCodeActions, bool supportsCodeActionResolve, CancellationToken cancellationToken); + Task[]?> GetCodeActionsAsync( + VSCodeActionParams request, + IDocumentSnapshot documentSnapshot, + RazorVSInternalCodeAction[] delegatedCodeActions, + Uri? delegatedDocumentUri, + bool supportsCodeActionResolve, + CancellationToken cancellationToken); Task GetCSharpCodeActionsRequestAsync(IDocumentSnapshot documentSnapshot, VSCodeActionParams request, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Models/CodeActionExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Models/CodeActionExtensions.cs index 3661d10f485..088fd58e3c4 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Models/CodeActionExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Models/CodeActionExtensions.cs @@ -1,6 +1,7 @@ // 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.Linq; using System.Text.Json; using System.Text.Json.Nodes; @@ -12,7 +13,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions.Models; internal static class CodeActionExtensions { - public static SumType AsVSCodeCommandOrCodeAction(this VSInternalCodeAction razorCodeAction, VSTextDocumentIdentifier textDocument) + public static SumType AsVSCodeCommandOrCodeAction(this VSInternalCodeAction razorCodeAction, VSTextDocumentIdentifier textDocument, Uri? delegatedDocumentUri) { if (razorCodeAction.Data is null) { @@ -23,6 +24,7 @@ public static SumType AsVSCodeCommandOrCodeAction(this VSIn TextDocument = textDocument, Action = LanguageServerConstants.CodeActions.EditBasedCodeActionCommand, Language = RazorLanguageKind.Razor, + DelegatedDocumentUri = delegatedDocumentUri, Data = razorCodeAction.Edit ?? new WorkspaceEdit(), }; @@ -57,6 +59,7 @@ public static RazorVSInternalCodeAction WrapResolvableCodeAction( TextDocument = context.Request.TextDocument, Action = action, Language = language, + DelegatedDocumentUri = context.DelegatedDocumentUri, Data = razorCodeAction.Data }; razorCodeAction.Data = JsonSerializer.SerializeToElement(resolutionParams); @@ -89,6 +92,7 @@ private static VSInternalCodeAction WrapResolvableCodeAction( TextDocument = context.Request.TextDocument, Action = action, Language = language, + DelegatedDocumentUri = context.DelegatedDocumentUri, Data = razorCodeAction.Data }; razorCodeAction.Data = JsonSerializer.SerializeToElement(resolutionParams); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Models/RazorCodeActionResolutionParams.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Models/RazorCodeActionResolutionParams.cs index 5fa246b822f..750b45944d2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Models/RazorCodeActionResolutionParams.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Models/RazorCodeActionResolutionParams.cs @@ -1,6 +1,7 @@ // 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.Text.Json.Serialization; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -18,6 +19,9 @@ internal sealed class RazorCodeActionResolutionParams [JsonPropertyName("language")] public required RazorLanguageKind Language { get; set; } + [JsonPropertyName("delegatedDocumentUri")] + public required Uri? DelegatedDocumentUri { get; set; } + [JsonPropertyName("data")] public object? Data { get; set; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs index d5f7b7c335d..2ad85b8f330 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs @@ -39,7 +39,7 @@ internal sealed class AddUsingsCodeActionResolver : IRazorCodeActionResolver return AddUsingsHelper.CreateAddUsingWorkspaceEdit(actionParams.Namespace, actionParams.AdditionalEdit, codeDocument, codeDocumentIdentifier); } - internal static bool TryCreateAddUsingResolutionParams(string fullyQualifiedName, VSTextDocumentIdentifier textDocument, TextDocumentEdit? additionalEdit, [NotNullWhen(true)] out string? @namespace, [NotNullWhen(true)] out RazorCodeActionResolutionParams? resolutionParams) + internal static bool TryCreateAddUsingResolutionParams(string fullyQualifiedName, VSTextDocumentIdentifier textDocument, TextDocumentEdit? additionalEdit, Uri? delegatedDocumentUri, [NotNullWhen(true)] out string? @namespace, [NotNullWhen(true)] out RazorCodeActionResolutionParams? resolutionParams) { @namespace = GetNamespaceFromFQN(fullyQualifiedName); if (string.IsNullOrEmpty(@namespace)) @@ -60,6 +60,7 @@ internal static bool TryCreateAddUsingResolutionParams(string fullyQualifiedName TextDocument = textDocument, Action = LanguageServerConstants.CodeActions.AddUsing, Language = RazorLanguageKind.Razor, + DelegatedDocumentUri = delegatedDocumentUri, Data = actionParams, }; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs index c70008b1c32..9ee3afc06cd 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs @@ -118,6 +118,7 @@ private static void AddCreateComponentFromTag(RazorCodeActionContext context, IS TextDocument = context.Request.TextDocument, Action = LanguageServerConstants.CodeActions.CreateComponentFromTag, Language = RazorLanguageKind.Razor, + DelegatedDocumentUri = context.DelegatedDocumentUri, Data = actionParams, }; @@ -179,7 +180,7 @@ private static async Task AddComponentAccessFromTagAsync(RazorCodeActionContext // name to give the tag. if (!tagHelperPair.CaseInsensitiveMatch || newTagName is not null) { - if (AddUsingsCodeActionResolver.TryCreateAddUsingResolutionParams(fullyQualifiedName, context.Request.TextDocument, additionalEdit, out var @namespace, out var resolutionParams)) + if (AddUsingsCodeActionResolver.TryCreateAddUsingResolutionParams(fullyQualifiedName, context.Request.TextDocument, additionalEdit, context.DelegatedDocumentUri, out var @namespace, out var resolutionParams)) { var addUsingCodeAction = RazorCodeActionFactory.CreateAddComponentUsing(@namespace, newTagName, resolutionParams); container.Add(addUsingCodeAction); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs index 264e259d0f0..2a1348b82e5 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs @@ -113,6 +113,7 @@ public Task> ProvideAsync(RazorCodeAct TextDocument = context.Request.TextDocument, Action = LanguageServerConstants.CodeActions.ExtractToCodeBehindAction, Language = RazorLanguageKind.Razor, + DelegatedDocumentUri = context.DelegatedDocumentUri, Data = actionParams, }; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs index 64ff97ff825..f92e61e5679 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs @@ -63,6 +63,7 @@ public Task> ProvideAsync(RazorCodeAct TextDocument = context.Request.TextDocument, Action = LanguageServerConstants.CodeActions.ExtractToNewComponentAction, Language = RazorLanguageKind.Razor, + DelegatedDocumentUri = context.DelegatedDocumentUri, Data = actionParams, }; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionProvider.cs index 2706d5ac164..dab71fd0176 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionProvider.cs @@ -38,8 +38,8 @@ public Task> ProvideAsync(RazorCodeAct var textDocument = context.Request.TextDocument; return Task.FromResult>( [ - RazorCodeActionFactory.CreateGenerateMethod(textDocument, methodName, eventName), - RazorCodeActionFactory.CreateAsyncGenerateMethod(textDocument, methodName, eventName) + RazorCodeActionFactory.CreateGenerateMethod(textDocument, context.DelegatedDocumentUri, methodName, eventName), + RazorCodeActionFactory.CreateAsyncGenerateMethod(textDocument, context.DelegatedDocumentUri, methodName, eventName) ]); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/RazorCodeActionFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/RazorCodeActionFactory.cs index b7ba525d95e..3b028b60da2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/RazorCodeActionFactory.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/RazorCodeActionFactory.cs @@ -84,7 +84,7 @@ public static RazorVSInternalCodeAction CreateExtractToComponent(RazorCodeAction return codeAction; } - public static RazorVSInternalCodeAction CreateGenerateMethod(VSTextDocumentIdentifier textDocument, string methodName, string eventName) + public static RazorVSInternalCodeAction CreateGenerateMethod(VSTextDocumentIdentifier textDocument, Uri? delegatedDocumentUri, string methodName, string eventName) { var @params = new GenerateMethodCodeActionParams { @@ -97,6 +97,7 @@ public static RazorVSInternalCodeAction CreateGenerateMethod(VSTextDocumentIdent TextDocument = textDocument, Action = LanguageServerConstants.CodeActions.GenerateEventHandler, Language = RazorLanguageKind.Razor, + DelegatedDocumentUri = delegatedDocumentUri, Data = @params, }; @@ -111,7 +112,7 @@ public static RazorVSInternalCodeAction CreateGenerateMethod(VSTextDocumentIdent return codeAction; } - public static RazorVSInternalCodeAction CreateAsyncGenerateMethod(VSTextDocumentIdentifier textDocument, string methodName, string eventName) + public static RazorVSInternalCodeAction CreateAsyncGenerateMethod(VSTextDocumentIdentifier textDocument, Uri? delegatedDocumentUri, string methodName, string eventName) { var @params = new GenerateMethodCodeActionParams { @@ -124,6 +125,7 @@ public static RazorVSInternalCodeAction CreateAsyncGenerateMethod(VSTextDocument TextDocument = textDocument, Action = LanguageServerConstants.CodeActions.GenerateEventHandler, Language = RazorLanguageKind.Razor, + DelegatedDocumentUri = delegatedDocumentUri, Data = @params, }; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/RazorCodeActionContext.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/RazorCodeActionContext.cs index f5e847d3ecd..77a2d7e4498 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/RazorCodeActionContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/RazorCodeActionContext.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; @@ -12,6 +13,7 @@ internal sealed record class RazorCodeActionContext( VSCodeActionParams Request, IDocumentSnapshot DocumentSnapshot, RazorCodeDocument CodeDocument, + Uri? DelegatedDocumentUri, int StartAbsoluteIndex, int EndAbsoluteIndex, Protocol.RazorLanguageKind LanguageKind, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs index 249208c3167..f108ff8013e 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs @@ -66,7 +66,10 @@ private async ValueTask GetCodeActionRequestInfoAsync(Rem private async ValueTask[]?> GetCodeActionsAsync(RemoteDocumentContext context, VSCodeActionParams request, RazorVSInternalCodeAction[] delegatedCodeActions, CancellationToken cancellationToken) { + var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync(cancellationToken).ConfigureAwait(false); + var generatedDocumentUri = generatedDocument.CreateUri(); + var supportsCodeActionResolve = _clientCapabilitiesService.ClientCapabilities.TextDocument?.CodeAction?.ResolveSupport is not null; - return await _codeActionsService.GetCodeActionsAsync(request, context.Snapshot, delegatedCodeActions, supportsCodeActionResolve, cancellationToken).ConfigureAwait(false); + return await _codeActionsService.GetCodeActionsAsync(request, context.Snapshot, delegatedCodeActions, generatedDocumentUri, supportsCodeActionResolve, cancellationToken).ConfigureAwait(false); } } From e829ffbb46d7ce84dea7160bf36a40fe39d207dd Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 4 Nov 2024 14:19:16 +1100 Subject: [PATCH 05/20] Add cohost resolve endpoint and tests --- .../CSharp/CSharpCodeActionResolver.cs | 2 +- ...mattedRemappingCSharpCodeActionResolver.cs | 2 +- .../CodeActions/CodeActionResolveService.cs | 2 +- .../Html/HtmlCodeActionResolver.cs | 2 +- .../Razor/AddUsingsCodeActionResolver.cs | 2 +- .../CreateComponentCodeActionResolver.cs | 2 +- .../ExtractToCodeBehindCodeActionResolver.cs | 2 +- .../ExtractToComponentCodeActionResolver.cs | 2 +- .../Razor/GenerateMethodCodeActionResolver.cs | 2 +- .../Remote/IRemoteCodeActionsService.cs | 9 + .../CodeActions/RemoteCodeActionsService.cs | 14 ++ .../CodeActions/RemoteServices.cs | 54 ++++++ .../CodeActions/RoslynCodeActionHelpers.cs | 25 +++ .../CohostCodeActionsResolveEndpoint.cs | 164 ++++++++++++++++++ .../Cohost/CohostCodeActionsEndpointTest.cs | 88 +++++++++- 15 files changed, 358 insertions(+), 14 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RoslynCodeActionHelpers.cs create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsResolveEndpoint.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/CSharpCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/CSharpCodeActionResolver.cs index ab57ceb5406..46f23c36e7a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/CSharpCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/CSharpCodeActionResolver.cs @@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; -internal sealed class CSharpCodeActionResolver(IRazorFormattingService razorFormattingService) : ICSharpCodeActionResolver +internal class CSharpCodeActionResolver(IRazorFormattingService razorFormattingService) : ICSharpCodeActionResolver { private readonly IRazorFormattingService _razorFormattingService = razorFormattingService; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/UnformattedRemappingCSharpCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/UnformattedRemappingCSharpCodeActionResolver.cs index d1e20dbf914..56c2329a7fc 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/UnformattedRemappingCSharpCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CSharp/UnformattedRemappingCSharpCodeActionResolver.cs @@ -16,7 +16,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; /// /// Resolves and remaps the code action, without running formatting passes. /// -internal sealed class UnformattedRemappingCSharpCodeActionResolver(IDocumentMappingService documentMappingService) : ICSharpCodeActionResolver +internal class UnformattedRemappingCSharpCodeActionResolver(IDocumentMappingService documentMappingService) : ICSharpCodeActionResolver { private readonly IDocumentMappingService _documentMappingService = documentMappingService; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionResolveService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionResolveService.cs index ec24477bf30..d842c00bed1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionResolveService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionResolveService.cs @@ -20,7 +20,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; -internal sealed class CodeActionResolveService( +internal class CodeActionResolveService( IEnumerable razorCodeActionResolvers, IEnumerable csharpCodeActionResolvers, IEnumerable htmlCodeActionResolvers, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Html/HtmlCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Html/HtmlCodeActionResolver.cs index fc7317a539b..538b01ee571 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Html/HtmlCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Html/HtmlCodeActionResolver.cs @@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; -internal sealed class HtmlCodeActionResolver(IEditMappingService editMappingService) : IHtmlCodeActionResolver +internal class HtmlCodeActionResolver(IEditMappingService editMappingService) : IHtmlCodeActionResolver { private readonly IEditMappingService _editMappingService = editMappingService; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs index 2ad85b8f330..fffc8ec4b55 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; -internal sealed class AddUsingsCodeActionResolver : IRazorCodeActionResolver +internal class AddUsingsCodeActionResolver : IRazorCodeActionResolver { public string Action => LanguageServerConstants.CodeActions.AddUsing; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/CreateComponentCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/CreateComponentCodeActionResolver.cs index fa398c9662d..85084b0d596 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/CreateComponentCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/CreateComponentCodeActionResolver.cs @@ -20,7 +20,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; -internal sealed class CreateComponentCodeActionResolver(LanguageServerFeatureOptions languageServerFeatureOptions) : IRazorCodeActionResolver +internal class CreateComponentCodeActionResolver(LanguageServerFeatureOptions languageServerFeatureOptions) : IRazorCodeActionResolver { private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs index 25cc910e853..a58b1b9e268 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs @@ -22,7 +22,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions; -internal sealed class ExtractToCodeBehindCodeActionResolver( +internal class ExtractToCodeBehindCodeActionResolver( LanguageServerFeatureOptions languageServerFeatureOptions, IRoslynCodeActionHelpers roslynCodeActionHelpers) : IRazorCodeActionResolver { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionResolver.cs index 9ffc571f000..7ff931c52e7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToComponentCodeActionResolver.cs @@ -20,7 +20,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions.Razor; -internal sealed class ExtractToComponentCodeActionResolver( +internal class ExtractToComponentCodeActionResolver( LanguageServerFeatureOptions languageServerFeatureOptions) : IRazorCodeActionResolver { private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs index 129d7fca5da..4f4494d12d2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs @@ -24,7 +24,7 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions.Razor; -internal sealed class GenerateMethodCodeActionResolver( +internal class GenerateMethodCodeActionResolver( IRoslynCodeActionHelpers roslynCodeActionHelpers, IDocumentMappingService documentMappingService, IRazorFormattingService razorFormattingService) : IRazorCodeActionResolver diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs index c7f0b69feed..16dc36c7828 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCodeActionsService.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.CodeActions.Models; +using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -24,4 +25,12 @@ ValueTask GetCodeActionRequestInfoAsync( VSCodeActionParams request, RazorVSInternalCodeAction[] delegatedCodeActions, CancellationToken cancellationToken); + + ValueTask ResolveCodeActionAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId razorDocumentId, + CodeAction request, + CodeAction? delegatedCodeAction, + RazorFormattingOptions options, + CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs index f108ff8013e..63057e49b3c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteCodeActionsService.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.CodeActions; using Microsoft.CodeAnalysis.Razor.CodeActions.Models; +using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; using Microsoft.CodeAnalysis.Razor.Remote; @@ -24,6 +25,7 @@ protected override IRemoteCodeActionsService CreateService(in ServiceArgs args) } private readonly ICodeActionsService _codeActionsService = args.ExportProvider.GetExportedValue(); + private readonly ICodeActionResolveService _codeActionResolveService = args.ExportProvider.GetExportedValue(); private readonly IClientCapabilitiesService _clientCapabilitiesService = args.ExportProvider.GetExportedValue(); public ValueTask GetCodeActionRequestInfoAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, VSCodeActionParams request, CancellationToken cancellationToken) @@ -72,4 +74,16 @@ private async ValueTask GetCodeActionRequestInfoAsync(Rem var supportsCodeActionResolve = _clientCapabilitiesService.ClientCapabilities.TextDocument?.CodeAction?.ResolveSupport is not null; return await _codeActionsService.GetCodeActionsAsync(request, context.Snapshot, delegatedCodeActions, generatedDocumentUri, supportsCodeActionResolve, cancellationToken).ConfigureAwait(false); } + + public ValueTask ResolveCodeActionAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, CodeAction request, CodeAction? delegatedCodeAction, RazorFormattingOptions options, CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + razorDocumentId, + context => ResolveCodeActionAsync(context, request, delegatedCodeAction, options, cancellationToken), + cancellationToken); + + private async ValueTask ResolveCodeActionAsync(RemoteDocumentContext context, CodeAction request, CodeAction? delegatedCodeAction, RazorFormattingOptions options, CancellationToken cancellationToken) + { + return await _codeActionResolveService.ResolveCodeActionAsync(context, request, delegatedCodeAction, options, cancellationToken).ConfigureAwait(false); + } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs index 556fb8b0ca3..e67dff93e49 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs @@ -6,11 +6,14 @@ using Microsoft.CodeAnalysis.Razor.CodeActions; using Microsoft.CodeAnalysis.Razor.CodeActions.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Workspaces; namespace Microsoft.CodeAnalysis.Remote.Razor.CodeActions; +// Services + [Export(typeof(ICodeActionsService)), Shared] [method: ImportingConstructor] internal sealed class OOPCodeActionsService( @@ -21,6 +24,17 @@ internal sealed class OOPCodeActionsService( LanguageServerFeatureOptions languageServerFeatureOptions) : CodeActionsService(documentMappingService, razorCodeActionProviders, csharpCodeActionProviders, htmlCodeActionProviders, languageServerFeatureOptions); +[Export(typeof(ICodeActionResolveService)), Shared] +[method: ImportingConstructor] +internal sealed class OOPCodeActionResolveService( + [ImportMany] IEnumerable razorCodeActionResolvers, + [ImportMany] IEnumerable csharpCodeActionResolvers, + [ImportMany] IEnumerable htmlCodeActionResolvers, + ILoggerFactory loggerFactory) + : CodeActionResolveService(razorCodeActionResolvers, csharpCodeActionResolvers, htmlCodeActionResolvers, loggerFactory); + +// Code Action Providers + [Export(typeof(IRazorCodeActionProvider)), Shared] [method: ImportingConstructor] internal sealed class OOPExtractToCodeBehindCodeActionProvider(ILoggerFactory loggerFactory) : ExtractToCodeBehindCodeActionProvider(loggerFactory); @@ -44,3 +58,43 @@ internal sealed class OOPDefaultCSharpCodeActionProvider(LanguageServerFeatureOp [Export(typeof(IHtmlCodeActionProvider)), Shared] [method: ImportingConstructor] internal sealed class OOPDefaultHtmlCodeActionProvider(IEditMappingService editMappingService) : HtmlCodeActionProvider(editMappingService); + +// Code Action Resolvers + +[Export(typeof(IRazorCodeActionResolver)), Shared] +[method: ImportingConstructor] +internal sealed class OOPExtractToCodeBehindCodeActionResolver( + LanguageServerFeatureOptions languageServerFeatureOptions, + IRoslynCodeActionHelpers roslynCodeActionHelpers) + : ExtractToCodeBehindCodeActionResolver(languageServerFeatureOptions, roslynCodeActionHelpers); + +[Export(typeof(IRazorCodeActionResolver)), Shared] +[method: ImportingConstructor] +internal sealed class OOPExtractToComponentCodeActionResolver(LanguageServerFeatureOptions languageServerFeatureOptions) : ExtractToComponentCodeActionResolver(languageServerFeatureOptions); + +[Export(typeof(IRazorCodeActionResolver)), Shared] +[method: ImportingConstructor] +internal sealed class OOPCreateComponentCodeActionResolver(LanguageServerFeatureOptions languageServerFeatureOptions) : CreateComponentCodeActionResolver(languageServerFeatureOptions); + +[Export(typeof(IRazorCodeActionResolver)), Shared] +internal sealed class OOPAddUsingsCodeActionResolver : AddUsingsCodeActionResolver; + +[Export(typeof(IRazorCodeActionResolver)), Shared] +[method: ImportingConstructor] +internal sealed class OOPGenerateMethodCodeActionResolver( + IRoslynCodeActionHelpers roslynCodeActionHelpers, + IDocumentMappingService documentMappingService, + IRazorFormattingService razorFormattingService) + : GenerateMethodCodeActionResolver(roslynCodeActionHelpers, documentMappingService, razorFormattingService); + +[Export(typeof(ICSharpCodeActionResolver)), Shared] +[method: ImportingConstructor] +internal sealed class OOPCSharpCodeActionResolver(IRazorFormattingService razorFormattingService) : CSharpCodeActionResolver(razorFormattingService); + +[Export(typeof(ICSharpCodeActionResolver)), Shared] +[method: ImportingConstructor] +internal sealed class OOPUnformattedRemappingCSharpCodeActionResolver(IDocumentMappingService documentMappingService) : UnformattedRemappingCSharpCodeActionResolver(documentMappingService); + +[Export(typeof(IHtmlCodeActionResolver)), Shared] +[method: ImportingConstructor] +internal sealed class OOPHtmlCodeActionResolver(IEditMappingService editMappingService) : HtmlCodeActionResolver(editMappingService); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RoslynCodeActionHelpers.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RoslynCodeActionHelpers.cs new file mode 100644 index 00000000000..62526f2214e --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RoslynCodeActionHelpers.cs @@ -0,0 +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.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.CodeActions; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +[Export(typeof(IRoslynCodeActionHelpers)), Shared] +internal sealed class RoslynCodeActionHelpers : IRoslynCodeActionHelpers +{ + public Task GetFormattedNewFileContentsAsync(string projectFilePath, Uri csharpFileUri, string newFileContent, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetSimplifiedTextEditsAsync(Uri codeBehindUri, TextEdit edit, bool requiresVirtualDocument, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsResolveEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsResolveEndpoint.cs new file mode 100644 index 00000000000..31e23a3d7d2 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsResolveEndpoint.cs @@ -0,0 +1,164 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Composition; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; +using Microsoft.CodeAnalysis.Razor.CodeActions; +using Microsoft.CodeAnalysis.Razor.CodeActions.Models; +using Microsoft.CodeAnalysis.Razor.Formatting; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Razor.Settings; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +#pragma warning disable RS0030 // Do not use banned APIs +[Shared] +[CohostEndpoint(Methods.TextDocumentCodeActionName)] +[ExportCohostStatelessLspService(typeof(CohostCodeActionsResolveEndpoint))] +[method: ImportingConstructor] +#pragma warning restore RS0030 // Do not use banned APIs +internal class CohostCodeActionsResolveEndpoint( + IRemoteServiceInvoker remoteServiceInvoker, + IClientCapabilitiesService clientCapabilitiesService, + IClientSettingsManager clientSettingsManager, + IHtmlDocumentSynchronizer htmlDocumentSynchronizer, + LSPRequestInvoker requestInvoker) + : AbstractRazorCohostDocumentRequestHandler +{ + private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker; + private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService; + private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager; + private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer; + private readonly LSPRequestInvoker _requestInvoker = requestInvoker; + + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => true; + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(CodeAction request) + { + var resolveParams = CodeActionResolveService.GetRazorCodeActionResolutionParams(request); + return resolveParams.TextDocument.ToRazorTextDocumentIdentifier(); + } + + protected override Task HandleRequestAsync(CodeAction request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync(context.TextDocument.AssumeNotNull(), request, cancellationToken); + + private async Task HandleRequestAsync(TextDocument razorDocument, CodeAction request, CancellationToken cancellationToken) + { + var resolveParams = CodeActionResolveService.GetRazorCodeActionResolutionParams(request); + + var resolvedDelegatedCodeAction = resolveParams.Language switch + { + RazorLanguageKind.Html => await ResolvedHtmlCodeActionAsync(razorDocument, request, resolveParams, cancellationToken).ConfigureAwait(false), + RazorLanguageKind.CSharp => await ResolveCSharpCodeActionAsync(razorDocument, request, resolveParams, cancellationToken).ConfigureAwait(false), + _ => null + }; + + var clientSettings = _clientSettingsManager.GetClientSettings(); + var formattingOptions = new RazorFormattingOptions() + { + InsertSpaces = !clientSettings.ClientSpaceSettings.IndentWithTabs, + TabSize = clientSettings.ClientSpaceSettings.IndentSize, + CodeBlockBraceOnNextLine = clientSettings.AdvancedSettings.CodeBlockBraceOnNextLine + }; + + return await _remoteServiceInvoker.TryInvokeAsync( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => service.ResolveCodeActionAsync(solutionInfo, razorDocument.Id, request, resolvedDelegatedCodeAction, formattingOptions, cancellationToken), + cancellationToken).ConfigureAwait(false); + } + + private async Task ResolveCSharpCodeActionAsync(TextDocument razorDocument, CodeAction codeAction, RazorCodeActionResolutionParams resolveParams, CancellationToken cancellationToken) + { + var originalData = codeAction.Data; + try + { + codeAction.Data = resolveParams.Data; + + var uri = resolveParams.DelegatedDocumentUri.AssumeNotNull(); + + var generatedDocumentIds = razorDocument.Project.Solution.GetDocumentIdsWithUri(uri); + var generatedDocumentId = generatedDocumentIds.FirstOrDefault(d => d.ProjectId == razorDocument.Project.Id); + if (generatedDocumentId is null) + { + return codeAction; + } + + if (razorDocument.Project.GetDocument(generatedDocumentId) is not { } generatedDocument) + { + return codeAction; + } + + var options = new JsonSerializerOptions(); + foreach (var converter in RazorServiceDescriptorsWrapper.GetLspConverters()) + { + options.Converters.Add(converter); + } + + var resourceOptions = _clientCapabilitiesService.ClientCapabilities.Workspace?.WorkspaceEdit?.ResourceOperations ?? []; + var roslynCodeAction = JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(codeAction, options), options).AssumeNotNull(); + var roslynResourceOptions = JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(resourceOptions, options), options).AssumeNotNull(); + + var resolvedCodeAction = await CodeActions.ResolveCodeActionAsync(generatedDocument, roslynCodeAction, roslynResourceOptions, cancellationToken).ConfigureAwait(false); + + return JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(resolvedCodeAction, options), options).AssumeNotNull(); + } + finally + { + codeAction.Data = originalData; + } + } + + private async Task ResolvedHtmlCodeActionAsync(TextDocument razorDocument, CodeAction codeAction, RazorCodeActionResolutionParams resolveParams, CancellationToken cancellationToken) + { + var originalData = codeAction.Data; + codeAction.Data = resolveParams.Data; + + try + { + var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false); + if (htmlDocument is null) + { + return codeAction; + } + + var result = await _requestInvoker.ReinvokeRequestOnServerAsync( + htmlDocument.Buffer, + Methods.CodeActionResolveName, + RazorLSPConstants.HtmlLanguageServerName, + codeAction, + cancellationToken).ConfigureAwait(false); + + if (result?.Response is null) + { + return codeAction; + } + + return result.Response; + } + finally + { + codeAction.Data = originalData; + } + } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(CohostCodeActionsResolveEndpoint instance) + { + public Task HandleRequestAsync(TextDocument razorDocument, CodeAction request, CancellationToken cancellationToken) + => instance.HandleRequestAsync(razorDocument, request, cancellationToken); + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs index 6b31cd14179..df907c4f6aa 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs @@ -1,16 +1,22 @@ // 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.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using ICSharpCode.Decompiler.Semantics; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.CodeActions.Models; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; +using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Razor.Settings; +using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; @@ -53,6 +59,39 @@ public Goo() await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.GenerateDefaultConstructors); } + [Fact] + public async Task UseExpressionBodiedMember() + { + var input = """ + @using System.Linq + +
+ + @code + { + [||]void M(string[] args) + { + args.ToString(); + } + } + + """; + + var expected = """ + @using System.Linq + +
+ + @code + { + void M(string[] args) => args.ToString(); + } + + """; + + await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.UseExpressionBody); + } + [Fact] public async Task IntroduceLocal() { @@ -435,11 +474,23 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin }); var document = await CreateProjectAndRazorDocumentAsync(input.Text, fileKind, createSeparateRemoteAndLocalWorkspaces: true); - var inputText = await document.GetTextAsync(DisposalToken); - var requestInvoker = new TestLSPRequestInvoker(); + var codeAction = await VerifyCodeActionRequestAsync(document, input, codeActionName, childActionIndex); + + if (codeAction is null) + { + Assert.Null(expected); + return; + } + await VerifyCodeActionResolveAsync(document, codeAction, expected); + } + + private async Task VerifyCodeActionRequestAsync(CodeAnalysis.TextDocument document, TestCode input, string codeActionName, int childActionIndex) + { + var requestInvoker = new TestLSPRequestInvoker(); var endpoint = new CohostCodeActionsEndpoint(RemoteServiceInvoker, ClientCapabilitiesService, TestHtmlDocumentSynchronizer.Instance, requestInvoker, NoOpTelemetryReporter.Instance); + var inputText = await document.GetTextAsync(DisposalToken); using var diagnostics = new PooledArrayBuilder(); foreach (var (code, spans) in input.NamedSpans) @@ -468,10 +519,9 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin var result = await endpoint.GetTestAccessor().HandleRequestAsync(document, request, DisposalToken); - if (expected is null) + if (result is null) { - Assert.Null(result); - return; + return null; } Assert.NotNull(result); @@ -486,5 +536,33 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin } Assert.NotNull(codeActionToRun); + return codeActionToRun; + } + + private async Task VerifyCodeActionResolveAsync(CodeAnalysis.TextDocument document, CodeAction codeAction, string? expected) + { + var requestInvoker = new TestLSPRequestInvoker(); + var clientSettingsManager = new ClientSettingsManager(changeTriggers: []); + + var endpoint = new CohostCodeActionsResolveEndpoint(RemoteServiceInvoker, ClientCapabilitiesService, clientSettingsManager, TestHtmlDocumentSynchronizer.Instance, requestInvoker); + + var result = await endpoint.GetTestAccessor().HandleRequestAsync(document, codeAction, DisposalToken); + + Assert.NotNull(result?.Edit); + + var workspaceEdit = result.Edit; + Assert.True(workspaceEdit.TryGetTextDocumentEdits(out var documentEdits)); + + var documentUri = document.CreateUri(); + var sourceText = await document.GetTextAsync(DisposalToken).ConfigureAwait(false); + + foreach (var edit in documentEdits) + { + Assert.Equal(documentUri, edit.TextDocument.Uri); + + sourceText = sourceText.WithChanges(edit.Edits.Select(sourceText.GetTextChange)); + } + + AssertEx.EqualOrDiff(expected, sourceText.ToString()); } } From b509f67d1ca30309d81855c0c78d8f59341571d9 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 4 Nov 2024 14:19:28 +1100 Subject: [PATCH 06/20] Fix code action translation to Roslyn types --- .../LanguageClient/Cohost/CohostCodeActionsEndpoint.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs index 916199c07b4..ca3a8e97861 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs @@ -119,12 +119,12 @@ private async Task GetCSharpCodeActionsAsync(TextDo options.Converters.Add(converter); } - var csharpRequest = JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(request), options).AssumeNotNull(); + var csharpRequest = JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(request, options), options).AssumeNotNull(); using var _ = _telemetryReporter.TrackLspRequest(Methods.TextDocumentCodeActionName, "Razor.ExternalAccess", TelemetryThresholds.CodeActionSubLSPTelemetryThreshold, correlationId); var csharpCodeActions = await CodeActions.GetCodeActionsAsync(generatedDocument, csharpRequest, _clientCapabilitiesService.ClientCapabilities.SupportsVisualStudioExtensions, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(csharpCodeActions), options).AssumeNotNull(); + return JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(csharpCodeActions, options), options).AssumeNotNull(); } private async Task GetHtmlCodeActionsAsync(TextDocument razorDocument, VSCodeActionParams request, Guid correlationId, CancellationToken cancellationToken) From a813c229f513c70428935442cd5da0b5d08cbd23 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 4 Nov 2024 14:54:25 +1100 Subject: [PATCH 07/20] Move helpers to base class --- .../Cohost/CohostEndpointTestBase.cs | 6 ++++ .../Cohost/CohostRenameEndpointTest.cs | 17 ++++------- .../CohostSemanticTokensRangeEndpointTest.cs | 2 -- .../CohostTextPresentationEndpointTest.cs | 9 ------ .../CohostUriPresentationEndpointTest.cs | 30 +++++++------------ 5 files changed, 23 insertions(+), 41 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs index 16ded3e8e93..fc9cbf54ae3 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs @@ -247,4 +247,10 @@ @using Microsoft.AspNetCore.Components.Web return solution.GetAdditionalDocument(documentId).AssumeNotNull(); } + + protected static Uri FileUri(string projectRelativeFileName) + => new(FilePath(projectRelativeFileName)); + + protected static string FilePath(string projectRelativeFileName) + => Path.Combine(TestProjectData.SomeProjectPath, projectRelativeFileName); } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs index 6e1af1d4f06..978461b744c 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs @@ -2,11 +2,9 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Testing; @@ -81,7 +79,7 @@ The end. """, additionalFiles: [ // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file - (File("Component.cs"), """ + (FilePath("Component.cs"), """ namespace SomeProject; public class Component : Microsoft.AspNetCore.Components.ComponentBase @@ -89,7 +87,7 @@ public class Component : Microsoft.AspNetCore.Components.ComponentBase } """), // The above will make the component exist, but the .razor file needs to exist too for Uri presentation - (File("Component.razor"), "") + (FilePath("Component.razor"), "") ], newName: "DifferentName", expected: """ @@ -138,7 +136,7 @@ The end. """, additionalFiles: [ // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file - (File("Component.cs"), """ + (FilePath("Component.cs"), """ namespace SomeProject; public class Component : Microsoft.AspNetCore.Components.ComponentBase @@ -146,7 +144,7 @@ public class Component : Microsoft.AspNetCore.Components.ComponentBase } """), // The above will make the component exist, but the .razor file needs to exist too for Uri presentation - (File("Component.razor"), "") + (FilePath("Component.razor"), "") ], newName: "DifferentName", expected: """ @@ -181,7 +179,7 @@ The end. """, additionalFiles: [ // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file - (File("Component.cs"), """ + (FilePath("Component.cs"), """ namespace SomeProject; public class Component : Microsoft.AspNetCore.Components.ComponentBase @@ -189,7 +187,7 @@ public class Component : Microsoft.AspNetCore.Components.ComponentBase } """), // The above will make the component exist, but the .razor file needs to exist too for Uri presentation - (File("Component.razor"), "") + (FilePath("Component.razor"), "") ], newName: "DifferentName", expected: "", @@ -260,7 +258,4 @@ private static string ProcessRazorDocumentEdits(SourceText inputText, Uri razorD return inputText.ToString(); } - - private static string File(string projectRelativeFileName) - => Path.Combine(TestProjectData.SomeProjectPath, projectRelativeFileName); } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSemanticTokensRangeEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSemanticTokensRangeEndpointTest.cs index be4fe9f5f05..2b7e7a4ee0d 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSemanticTokensRangeEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSemanticTokensRangeEndpointTest.cs @@ -8,9 +8,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Telemetry; -using Microsoft.CodeAnalysis.Razor.SemanticTokens; using Microsoft.CodeAnalysis.Razor.Settings; -using Microsoft.CodeAnalysis.Remote.Razor.SemanticTokens; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Razor.Settings; using Microsoft.VisualStudio.Utilities; diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTextPresentationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTextPresentationEndpointTest.cs index f2a4acc5b67..ea74eafd7f7 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTextPresentationEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTextPresentationEndpointTest.cs @@ -1,10 +1,7 @@ // 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.IO; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -46,12 +43,6 @@ The end. expected: "Hello World"); } - private static Uri FileUri(string projectRelativeFileName) - => new(File(projectRelativeFileName)); - - private static string File(string projectRelativeFileName) - => Path.Combine(TestProjectData.SomeProjectPath, projectRelativeFileName); - private async Task VerifyUriPresentationAsync(string input, string text, string? expected, WorkspaceEdit? htmlResponse = null) { TestFileMarkupParser.GetSpan(input, out input, out var span); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs index 68b3be9565a..b2dfa448899 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.IO; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -83,7 +81,7 @@ The end. """, additionalFiles: [ // The source generator isn't hooked up to our test project, so we have to manually "compile" the razor file - (File("Component.cs"), """ + (FilePath("Component.cs"), """ namespace SomeProject; public class Component : Microsoft.AspNetCore.Components.ComponentBase @@ -91,7 +89,7 @@ public class Component : Microsoft.AspNetCore.Components.ComponentBase } """), // The above will make the component exist, but the .razor file needs to exist too for Uri presentation - (File("Component.razor"), "") + (FilePath("Component.razor"), "") ], uris: [FileUri("Component.razor")], expected: ""); @@ -111,7 +109,7 @@ This is a Razor document. The end. """, additionalFiles: [ - (File("_Imports.razor"), "") + (FilePath("_Imports.razor"), "") ], uris: [FileUri("_Imports.razor")], expected: null); @@ -167,14 +165,14 @@ This is a Razor document. } """, additionalFiles: [ - (File("Component.cs"), """ + (FilePath("Component.cs"), """ namespace SomeProject; public class Component : Microsoft.AspNetCore.Components.ComponentBase { } """), - (File("Component.razor"), "") + (FilePath("Component.razor"), "") ], uris: [FileUri("Component.razor")], expected: null); @@ -194,14 +192,14 @@ This is a Razor document. The end. """, additionalFiles: [ - (File("Component.cs"), """ + (FilePath("Component.cs"), """ namespace SomeProject; public class Component : Microsoft.AspNetCore.Components.ComponentBase { } """), - (File("Component.razor"), "") + (FilePath("Component.razor"), "") ], uris: [ FileUri("Component.razor"), @@ -225,14 +223,14 @@ This is a Razor document. The end. """, additionalFiles: [ - (File("Component.cs"), """ + (FilePath("Component.cs"), """ namespace SomeProject; public class Component : Microsoft.AspNetCore.Components.ComponentBase { } """), - (File("Component.razor"), "") + (FilePath("Component.razor"), "") ], uris: [ FileUri("Component.razor.css"), @@ -256,7 +254,7 @@ This is a Razor document. The end. """, additionalFiles: [ - (File("Component.cs"), """ + (FilePath("Component.cs"), """ using Microsoft.AspNetCore.Components; namespace SomeProject; @@ -271,18 +269,12 @@ public class Component : ComponentBase public string NormalParameter { get; set; } } """), - (File("Component.razor"), "") + (FilePath("Component.razor"), "") ], uris: [FileUri("Component.razor")], expected: """"""); } - private static Uri FileUri(string projectRelativeFileName) - => new(File(projectRelativeFileName)); - - private static string File(string projectRelativeFileName) - => Path.Combine(TestProjectData.SomeProjectPath, projectRelativeFileName); - private async Task VerifyUriPresentationAsync(string input, Uri[] uris, string? expected, WorkspaceEdit? htmlResponse = null, (string fileName, string contents)[]? additionalFiles = null) { TestFileMarkupParser.GetSpan(input, out input, out var span); From 8ba2bbf98d29ff541e986dd3c439fc9bd002c277 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 4 Nov 2024 15:49:37 +1100 Subject: [PATCH 08/20] Implement additional roslyn helpers, and add tests for more involved code actions --- .../CodeActions/RoslynCodeActionHelpers.cs | 14 +- .../ExtractToCodeBehindCodeActionResolver.cs | 2 +- .../Razor/GenerateMethodCodeActionResolver.cs | 6 +- .../Razor/IRoslynCodeActionHelpers.cs | 13 +- .../CodeActions/RoslynCodeActionHelpers.cs | 56 +++++- .../ProjectSystem/RemoteProjectSnapshot.cs | 2 + .../Cohost/CohostCodeActionsEndpointTest.cs | 170 ++++++++++++++++-- 7 files changed, 229 insertions(+), 34 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/RoslynCodeActionHelpers.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/RoslynCodeActionHelpers.cs index 0da843edba0..69d5aab1f23 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/RoslynCodeActionHelpers.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/RoslynCodeActionHelpers.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.CodeAnalysis.Razor.CodeActions; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -16,13 +17,13 @@ internal sealed class RoslynCodeActionHelpers(IClientConnection clientConnection { private readonly IClientConnection _clientConnection = clientConnection; - public Task GetFormattedNewFileContentsAsync(string projectFilePath, Uri csharpFileUri, string newFileContent, CancellationToken cancellationToken) + public Task GetFormattedNewFileContentsAsync(IProjectSnapshot projectSnapshot, Uri csharpFileUri, string newFileContent, CancellationToken cancellationToken) { var parameters = new FormatNewFileParams() { Project = new TextDocumentIdentifier { - Uri = new Uri(projectFilePath, UriKind.Absolute) + Uri = new Uri(projectSnapshot.FilePath, UriKind.Absolute) }, Document = new TextDocumentIdentifier { @@ -33,11 +34,14 @@ internal sealed class RoslynCodeActionHelpers(IClientConnection clientConnection return _clientConnection.SendRequestAsync(CustomMessageNames.RazorFormatNewFileEndpointName, parameters, cancellationToken); } - public Task GetSimplifiedTextEditsAsync(Uri codeBehindUri, TextEdit edit, bool requiresVirtualDocument, CancellationToken cancellationToken) + public Task GetSimplifiedTextEditsAsync(DocumentContext documentContext, Uri? codeBehindUri, TextEdit edit, CancellationToken cancellationToken) { + var tdi = codeBehindUri is null + ? documentContext.GetTextDocumentIdentifierAndVersion() + : new TextDocumentIdentifierAndVersion(new TextDocumentIdentifier() { Uri = codeBehindUri }, 1); var delegatedParams = new DelegatedSimplifyMethodParams( - new TextDocumentIdentifierAndVersion(new TextDocumentIdentifier() { Uri = codeBehindUri }, 1), - requiresVirtualDocument, + tdi, + RequiresVirtualDocument: codeBehindUri == null, edit); return _clientConnection.SendRequestAsync( diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs index a58b1b9e268..6cd6cb2a119 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs @@ -134,7 +134,7 @@ private async Task GenerateCodeBehindClassAsync(IProjectSnapshot project var newFileContent = builder.ToString(); - var fixedContent = await _roslynCodeActionHelpers.GetFormattedNewFileContentsAsync(project.FilePath, codeBehindUri, newFileContent, cancellationToken).ConfigureAwait(false); + var fixedContent = await _roslynCodeActionHelpers.GetFormattedNewFileContentsAsync(project, codeBehindUri, newFileContent, cancellationToken).ConfigureAwait(false); if (fixedContent is null) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs index 4f4494d12d2..d130d1faca3 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs @@ -110,7 +110,7 @@ razorClassName is null || character: 0, $"{formattedMethod}{Environment.NewLine}"); - var result = await _roslynCodeActionHelpers.GetSimplifiedTextEditsAsync(codeBehindUri, edit, requiresVirtualDocument: false, cancellationToken).ConfigureAwait(false); + var result = await _roslynCodeActionHelpers.GetSimplifiedTextEditsAsync(documentContext, codeBehindUri, edit, cancellationToken).ConfigureAwait(false); var codeBehindTextDocEdit = new TextDocumentEdit() { @@ -152,7 +152,7 @@ private async Task GenerateMethodInCodeBlockAsync( character: 0, editToSendToRoslyn.NewText); - var result = await _roslynCodeActionHelpers.GetSimplifiedTextEditsAsync(documentContext.Uri, tempTextEdit, requiresVirtualDocument: true, cancellationToken).ConfigureAwait(false); + var result = await _roslynCodeActionHelpers.GetSimplifiedTextEditsAsync(documentContext, codeBehindUri: null, tempTextEdit, cancellationToken).ConfigureAwait(false); // Roslyn should have passed back 2 edits. One that contains the simplified method stub and the other that contains the new // location for the class end brace since we had asked to insert the method stub at the original class end brace location. @@ -175,7 +175,7 @@ private async Task GenerateMethodInCodeBlockAsync( .Replace(FormattingUtilities.Indent, string.Empty); var remappedEdit = VsLspFactory.CreateTextEdit(remappedRange, unformattedMethodSignature); - var result = await _roslynCodeActionHelpers.GetSimplifiedTextEditsAsync(documentContext.Uri, remappedEdit, requiresVirtualDocument: true, cancellationToken).ConfigureAwait(false); + var result = await _roslynCodeActionHelpers.GetSimplifiedTextEditsAsync(documentContext, codeBehindUri: null, remappedEdit, cancellationToken).ConfigureAwait(false); if (result is not null) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/IRoslynCodeActionHelpers.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/IRoslynCodeActionHelpers.cs index 9d57b459fe9..1e22b41912b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/IRoslynCodeActionHelpers.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/IRoslynCodeActionHelpers.cs @@ -4,12 +4,21 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.CodeActions; internal interface IRoslynCodeActionHelpers { - Task GetFormattedNewFileContentsAsync(string projectFilePath, Uri csharpFileUri, string newFileContent, CancellationToken cancellationToken); - Task GetSimplifiedTextEditsAsync(Uri codeBehindUri, TextEdit edit, bool requiresVirtualDocument, CancellationToken cancellationToken); + Task GetFormattedNewFileContentsAsync(IProjectSnapshot projectSnapshot, Uri csharpFileUri, string newFileContent, CancellationToken cancellationToken); + + /// + /// Apply the edit to the specified document, get Roslyn to simplify it, and return the simplified edit + /// + /// The Razor document context for the edit + /// If present, the Roslyn document to apply the edit to. Otherwise the generated C# document will be used + /// The edit to apply + /// Cancellation token + Task GetSimplifiedTextEditsAsync(DocumentContext documentContext, Uri? codeBehindUri, TextEdit edit, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RoslynCodeActionHelpers.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RoslynCodeActionHelpers.cs index 62526f2214e..ac660909c24 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RoslynCodeActionHelpers.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RoslynCodeActionHelpers.cs @@ -2,24 +2,70 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Composition; +using System.Diagnostics; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.CodeActions; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; +using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; +using RoslynTextEdit = Roslyn.LanguageServer.Protocol.TextEdit; namespace Microsoft.CodeAnalysis.Remote.Razor; [Export(typeof(IRoslynCodeActionHelpers)), Shared] -internal sealed class RoslynCodeActionHelpers : IRoslynCodeActionHelpers +internal sealed class RoslynCodeActionHelpers() : IRoslynCodeActionHelpers { - public Task GetFormattedNewFileContentsAsync(string projectFilePath, Uri csharpFileUri, string newFileContent, CancellationToken cancellationToken) + public async Task GetFormattedNewFileContentsAsync(IProjectSnapshot projectSnapshot, Uri csharpFileUri, string newFileContent, CancellationToken cancellationToken) { - throw new NotImplementedException(); + Debug.Assert(projectSnapshot is RemoteProjectSnapshot); + var project = ((RemoteProjectSnapshot)projectSnapshot).Project; + + var document = project.AddDocument(RazorUri.GetDocumentFilePathFromUri(csharpFileUri), newFileContent); + + return await ExternalHandlers.CodeActions.GetFormattedNewFileContentAsync(document, cancellationToken).ConfigureAwait(false); } - public Task GetSimplifiedTextEditsAsync(Uri codeBehindUri, TextEdit edit, bool requiresVirtualDocument, CancellationToken cancellationToken) + public async Task GetSimplifiedTextEditsAsync(DocumentContext documentContext, Uri? codeBehindUri, TextEdit edit, CancellationToken cancellationToken) { - throw new NotImplementedException(); + Debug.Assert(documentContext is RemoteDocumentContext); + var context = (RemoteDocumentContext)documentContext; + + Document document; + if (codeBehindUri is null) + { + // Edit is for inserting into the generated document + document = await context.Snapshot.GetGeneratedDocumentAsync(cancellationToken).ConfigureAwait(false); + } + else + { + // Edit is for inserting into a C# document + var solution = context.TextDocument.Project.Solution; + var documentIds = solution.GetDocumentIdsWithUri(codeBehindUri); + if (documentIds.Length == 0) + { + return null; + } + + document = solution.GetRequiredDocument(documentIds.First(d => d.ProjectId == context.TextDocument.Project.Id)); + } + + var options = new JsonSerializerOptions(); + foreach (var converter in RazorServiceDescriptorsWrapper.GetLspConverters()) + { + options.Converters.Add(converter); + } + + var convertedEdit = JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(edit, options), options).AssumeNotNull(); + + var edits = await ExternalHandlers.CodeActions.GetSimplifiedEditsAsync(document, convertedEdit, cancellationToken).ConfigureAwait(false); + + return JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(edits, options), options); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteProjectSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteProjectSnapshot.cs index 1720c1a09be..82461bc1c54 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteProjectSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ProjectSystem/RemoteProjectSnapshot.cs @@ -84,6 +84,8 @@ public IEnumerable DocumentFilePaths public VersionStamp Version => _project.Version; + public Project Project => _project; + public LanguageVersion CSharpLanguageVersion => ((CSharpParseOptions)_project.ParseOptions.AssumeNotNull()).LanguageVersion; public ValueTask> GetTagHelpersAsync(CancellationToken cancellationToken) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs index df907c4f6aa..85e736e791d 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs @@ -4,9 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using ICSharpCode.Decompiler.Semantics; +using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.AspNetCore.Razor.Test.Common; @@ -19,6 +18,7 @@ using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; +using WorkspacesSR = Microsoft.CodeAnalysis.Razor.Workspaces.Resources.SR; namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; @@ -435,30 +435,158 @@ @using System.Text await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeFixProviderNames.AddImport); } - [Theory] - [InlineData("[||]DoesNotExist")] - [InlineData("Does[||]NotExist")] - [InlineData("DoesNotExist[||]")] - public async Task Handle_GenerateMethod_NoCodeBlock_NonEmptyTrailingLine(string cursorAndMethodName) + [Fact] + public async Task GenerateEventHandler_NoCodeBlock() + { + var input = """ + + """; + + var expected = """ + + @code { + private void DoesNotExist(MouseEventArgs e) + { + throw new NotImplementedException(); + } + } + """; + + await VerifyCodeActionAsync(input, expected, WorkspacesSR.FormatGenerate_Event_Handler_Title("DoesNotExist")); + } + + [Fact] + public async Task GenerateEventHandler_CodeBlock() + { + var input = """ + + + @code + { + } + """; + + var expected = """ + + + @code + { + private void DoesNotExist(MouseEventArgs e) + { + throw new NotImplementedException(); + } + } + """; + + await VerifyCodeActionAsync(input, expected, WorkspacesSR.FormatGenerate_Event_Handler_Title("DoesNotExist")); + } + + [Fact] + public async Task GenerateAsyncEventHandler_NoCodeBlock() { - var input = $$""" - + var input = """ + """; var expected = """ @code { - private void DoesNotExist(global::Microsoft.AspNetCore.Components.Web.MouseEventArgs e) + private Task DoesNotExist(MouseEventArgs e) { - throw new global::System.NotImplementedException(); + throw new NotImplementedException(); } } """; - await VerifyCodeActionAsync(input, expected, "Generate Event Handler 'DoesNotExist'"); + await VerifyCodeActionAsync(input, expected, WorkspacesSR.FormatGenerate_Async_Event_Handler_Title("DoesNotExist")); } - private async Task VerifyCodeActionAsync(TestCode input, string? expected, string codeActionName, int childActionIndex = 0, string? fileKind = null) + [Fact] + public async Task GenerateAsyncEventHandler_CodeBlock() + { + var input = """ + + + @code + { + } + """; + + var expected = """ + + + @code + { + private Task DoesNotExist(MouseEventArgs e) + { + throw new NotImplementedException(); + } + } + """; + + await VerifyCodeActionAsync(input, expected, WorkspacesSR.FormatGenerate_Async_Event_Handler_Title("DoesNotExist")); + } + + [Fact] + public async Task ExtractToCodeBehind() + { + await VerifyCodeActionAsync( + input: """ +
+ + @co[||]de + { + private int x = 1; + } + """, + expected: """ +
+ + + """, + codeActionName: WorkspacesSR.ExtractTo_CodeBehind_Title, + additionalExpectedFiles: [ + (FileUri("File1.razor.cs"), """ + namespace SomeProject + { + public partial class File1 + { + private int x = 1; + } + } + """)]); + } + + [Fact] + public async Task ExtractToComponent() + { + await VerifyCodeActionAsync( + input: """ +
+ + [|
+ Hello World +
|] + +
+ """, + expected: """ +
+ + + +
+ """, + codeActionName: WorkspacesSR.ExtractTo_Component_Title, + additionalExpectedFiles: [ + (FileUri("Component.razor"), """ +
+ Hello World +
+ """)]); + } + + private async Task VerifyCodeActionAsync(TestCode input, string? expected, string codeActionName, int childActionIndex = 0, string? fileKind = null, (Uri fileUri, string contents)[]? additionalExpectedFiles = null) { UpdateClientLSPInitializationOptions(options => { @@ -483,7 +611,7 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin return; } - await VerifyCodeActionResolveAsync(document, codeAction, expected); + await VerifyCodeActionResolveAsync(document, codeAction, expected, additionalExpectedFiles); } private async Task VerifyCodeActionRequestAsync(CodeAnalysis.TextDocument document, TestCode input, string codeActionName, int childActionIndex) @@ -539,7 +667,7 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin return codeActionToRun; } - private async Task VerifyCodeActionResolveAsync(CodeAnalysis.TextDocument document, CodeAction codeAction, string? expected) + private async Task VerifyCodeActionResolveAsync(CodeAnalysis.TextDocument document, CodeAction codeAction, string? expected, (Uri fileUri, string contents)[]? additionalExpectedFiles = null) { var requestInvoker = new TestLSPRequestInvoker(); var clientSettingsManager = new ClientSettingsManager(changeTriggers: []); @@ -558,9 +686,15 @@ private async Task VerifyCodeActionResolveAsync(CodeAnalysis.TextDocument docume foreach (var edit in documentEdits) { - Assert.Equal(documentUri, edit.TextDocument.Uri); - - sourceText = sourceText.WithChanges(edit.Edits.Select(sourceText.GetTextChange)); + if (edit.TextDocument.Uri == documentUri) + { + sourceText = sourceText.WithChanges(edit.Edits.Select(sourceText.GetTextChange)); + } + else + { + var contents = Assert.Single(additionalExpectedFiles.AssumeNotNull(), f => f.fileUri == edit.TextDocument.Uri).contents; + AssertEx.EqualOrDiff(contents, Assert.Single(edit.Edits).NewText); + } } AssertEx.EqualOrDiff(expected, sourceText.ToString()); From 0a3b9024147caa38e59a4abcb192974ca5c880c3 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 4 Nov 2024 16:17:12 +1100 Subject: [PATCH 09/20] Cohost eqivalent of https://github.com/dotnet/razor/pull/11141 --- .../CodeActions/CodeActionsService.cs | 14 -------------- .../Cohost/CohostCodeActionsEndpoint.cs | 14 ++++++++++++++ .../Cohost/CohostCodeActionsEndpointTest.cs | 14 ++++++++++---- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs index 1733c98d3b1..57b1b422ffb 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/CodeActionsService.cs @@ -109,20 +109,6 @@ void ConvertCodeActionsToSumType(ImmutableArray codeA var sourceText = codeDocument.Source.Text; - // VS Provides `CodeActionParams.Context.SelectionRange` in addition to - // `CodeActionParams.Range`. The `SelectionRange` is relative to where the - // code action was invoked (ex. line 14, char 3) whereas the `Range` is - // always at the start of the line (ex. line 14, char 0). We want to utilize - // the relative positioning to ensure we provide code actions for the appropriate - // context. - // - // Note: VS Code doesn't provide a `SelectionRange`. - var vsCodeActionContext = request.Context; - if (vsCodeActionContext.SelectionRange != null) - { - request.Range = vsCodeActionContext.SelectionRange; - } - if (!sourceText.TryGetAbsoluteIndex(request.Range.Start, out var startLocation)) { return null; diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs index ca3a8e97861..dfa15276b5a 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsEndpoint.cs @@ -75,6 +75,20 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie var correlationId = Guid.NewGuid(); using var _ = _telemetryReporter.TrackLspRequest(Methods.TextDocumentCodeActionName, LanguageServerConstants.RazorLanguageServerName, TelemetryThresholds.CodeActionRazorTelemetryThreshold, correlationId); + // VS Provides `CodeActionParams.Context.SelectionRange` in addition to + // `CodeActionParams.Range`. The `SelectionRange` is relative to where the + // code action was invoked (ex. line 14, char 3) whereas the `Range` is + // always at the start of the line (ex. line 14, char 0). We want to utilize + // the relative positioning to ensure we provide code actions for the appropriate + // context. + // + // Note: VS Code doesn't provide a `SelectionRange`. + var vsCodeActionContext = request.Context; + if (vsCodeActionContext.SelectionRange != null) + { + request.Range = vsCodeActionContext.SelectionRange; + } + var requestInfo = await _remoteServiceInvoker.TryInvokeAsync( razorDocument.Project.Solution, (service, solutionInfo, cancellationToken) => service.GetCodeActionRequestInfoAsync(solutionInfo, razorDocument.Id, request, cancellationToken), diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs index 85e736e791d..a986d07dc2f 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs @@ -69,7 +69,7 @@ @using System.Linq @code { - [||]void M(string[] args) + [|{|selection:|}void M(string[] args)|] { args.ToString(); } @@ -628,12 +628,12 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin continue; } - foreach (var textSpan in spans) + foreach (var diagnosticSpan in spans) { diagnostics.Add(new Diagnostic { Code = code, - Range = inputText.GetRange(textSpan) + Range = inputText.GetRange(diagnosticSpan) }); } } @@ -641,10 +641,16 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin var request = new VSCodeActionParams { TextDocument = new VSTextDocumentIdentifier { Uri = document.CreateUri() }, - Range = inputText.GetRange(input.NamedSpans[""].Single()), + Range = inputText.GetRange(input.Span), Context = new VSInternalCodeActionContext() { Diagnostics = diagnostics.ToArray() } }; + if (input.TryGetNamedSpans("selection", out var selectionSpans)) + { + // Simulate VS range vs selection range + request.Context.SelectionRange = inputText.GetRange(selectionSpans.Single()); + } + var result = await endpoint.GetTestAccessor().HandleRequestAsync(document, request, DisposalToken); if (result is null) From d7e1c01c9965e563f648ef01f8e14237435ddc75 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 4 Nov 2024 17:03:50 +1100 Subject: [PATCH 10/20] Silly mistake number 1 --- .../LanguageClient/Cohost/CohostCodeActionsResolveEndpoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsResolveEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsResolveEndpoint.cs index 31e23a3d7d2..aedb3a73948 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsResolveEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostCodeActionsResolveEndpoint.cs @@ -24,7 +24,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; #pragma warning disable RS0030 // Do not use banned APIs [Shared] -[CohostEndpoint(Methods.TextDocumentCodeActionName)] +[CohostEndpoint(Methods.CodeActionResolveName)] [ExportCohostStatelessLspService(typeof(CohostCodeActionsResolveEndpoint))] [method: ImportingConstructor] #pragma warning restore RS0030 // Do not use banned APIs From 4ef1668cd081ea9d5db491c877e2673c52531ef6 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 5 Nov 2024 16:17:17 +1100 Subject: [PATCH 11/20] Bump Roslyn to 4.13.0-2.24554.8 --- eng/Version.Details.xml | 76 ++++++++++++++++++++--------------------- eng/Versions.props | 38 ++++++++++----------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 0cff4c7a350..eafe3cd7add 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -11,82 +11,82 @@ ccd0927e3823fb178c7151594f5d2eaba81bba81 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 - + https://github.com/dotnet/roslyn - 77c6704c42b162e89095631f12e2661398a75ba7 + 8fc87e00bbe35131f7d3707621ae18e1af1aae55 diff --git a/eng/Versions.props b/eng/Versions.props index fc6cf0f08a9..281e8d7b835 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -53,25 +53,25 @@ 9.0.0-beta.24516.2 1.0.0-beta.23475.1 1.0.0-beta.23475.1 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 - 4.13.0-1.24522.7 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8 + 4.13.0-2.24554.8