From 771cbe5b60af9569ef51e1c3ecf9ca086d7874ce Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 25 Jan 2024 15:27:56 +1100 Subject: [PATCH 01/14] Add textDocument/inlayHints support --- .../DelegatedTypes.cs | 5 + .../Common/CustomMessageNames.cs | 2 + .../InlayHints/InlayHintEndpoint.cs | 99 +++++++++++++++++++ .../RazorLanguageServer.cs | 3 + .../Endpoints/InlayHints.cs | 39 ++++++++ 5 files changed, 148 insertions(+) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs index d1d4bfde8ec..5613328273e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs @@ -23,6 +23,11 @@ internal record DelegatedPositionParams( Position ProjectedPosition, RazorLanguageKind ProjectedKind) : IDelegatedParams; +internal record DelegatedInlayHintParams( + TextDocumentIdentifierAndVersion Identifier, + Range ProjectedRange, + RazorLanguageKind ProjectedKind) : IDelegatedParams; + internal record DelegatedValidateBreakpointRangeParams( TextDocumentIdentifierAndVersion Identifier, Range ProjectedRange, diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs index 1ecf036700f..2ac75b9eb41 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs @@ -56,6 +56,8 @@ internal static class CustomMessageNames public const string RazorReferencesEndpointName = "razor/references"; + public const string RazorInlayHintEndpoint = "razor/inlayHint"; + // Called to get C# diagnostics from Roslyn when publishing diagnostics for VS Code public const string RazorCSharpPullDiagnosticsEndpointName = "razor/csharpPullDiagnostics"; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs new file mode 100644 index 00000000000..8f2af077764 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs @@ -0,0 +1,99 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; +using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; +using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; +using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; + +[LanguageServerEndpoint(Methods.TextDocumentInlayHintName)] +internal sealed class InlayHintEndpoint(LanguageServerFeatureOptions featureOptions, IRazorDocumentMappingService documentMappingService, IClientConnection clientConnection) + : IRazorRequestHandler, ICapabilitiesProvider +{ + private readonly LanguageServerFeatureOptions _featureOptions = featureOptions; + private readonly IRazorDocumentMappingService _documentMappingService = documentMappingService; + private readonly IClientConnection _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); + + public bool MutatesSolutionState => false; + + public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) + { + // Not supported in VS Code + if (!_featureOptions.SingleServerSupport) + { + return; + } + + serverCapabilities.InlayHintOptions = new InlayHintOptions + { + ResolveProvider = false, + WorkDoneProgress = false + }; + } + + public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request) + => request.TextDocument; + + public async Task HandleRequestAsync(InlayHintParams request, RazorRequestContext context, CancellationToken cancellationToken) + { + var documentContext = context.GetRequiredDocumentContext(); + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + var csharpDocument = codeDocument.GetCSharpDocument(); + + // We are given a range by the client, but our mapping only succeeds if the start and end of the range can both be mapped + // to C#. Since that doesn't logically match what we want from inlay hints, we instead get the minimum range of mappable + // C# to get hints for. We'll filter that later, to remove the sections that can't be mapped back. + if (!_documentMappingService.TryMapToGeneratedDocumentRange(csharpDocument, request.Range, out var projectedRange) && + !RazorSemanticTokensInfoService.TryGetMinimalCSharpRange(codeDocument, request.Range, out projectedRange)) + { + // There's no C# in the range. + return null; + } + + // For now we only support C# inlay hints. Once Web Tools adds support we'll need to request from both servers and combine + // the results, much like folding ranges. + var delegatedRequest = new DelegatedInlayHintParams( + Identifier: documentContext.Identifier, + ProjectedRange: projectedRange, + ProjectedKind: RazorLanguageKind.CSharp + ); + + var inlayHints = await _clientConnection.SendRequestAsync( + CustomMessageNames.RazorInlayHintEndpoint, + delegatedRequest, + cancellationToken).ConfigureAwait(false); + + if (inlayHints is null) + { + return null; + } + + var csharpSourceText = codeDocument.GetCSharpSourceText(); + + using var _1 = ArrayBuilderPool.GetPooledObject(out var inlayHintsBuilder); + + foreach (var hint in inlayHints) + { + if (hint.Position.TryGetAbsoluteIndex(csharpSourceText, null, out var absoluteIndex) && + _documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteIndex, out Position? hostDocumentPosition, out _)) + { + hint.Position = hostDocumentPosition; + inlayHintsBuilder.Add(hint); + } + } + + return inlayHintsBuilder.ToArray(); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index d53d5a58183..4a031f9905c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.FindAllReferences; using Microsoft.AspNetCore.Razor.LanguageServer.Folding; using Microsoft.AspNetCore.Razor.LanguageServer.Implementation; +using Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; using Microsoft.AspNetCore.Razor.LanguageServer.LinkedEditingRange; using Microsoft.AspNetCore.Razor.LanguageServer.MapCode; using Microsoft.AspNetCore.Razor.LanguageServer.ProjectContexts; @@ -197,6 +198,8 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); services.AddHandler(); + + services.AddHandlerWithCapabilities(); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs new file mode 100644 index 00000000000..d2f3224ef22 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.LanguageServerClient.Razor.Extensions; +using StreamJsonRpc; + +namespace Microsoft.VisualStudio.LanguageServerClient.Razor; + +internal partial class RazorCustomMessageTarget +{ + [JsonRpcMethod(CustomMessageNames.RazorInlayHintEndpoint, UseSingleObjectParameterDeserialization = true)] + public async Task ProvideInlayHintsAsync(DelegatedInlayHintParams request, CancellationToken cancellationToken) + { + var delegationDetails = await GetProjectedRequestDetailsAsync(request, cancellationToken).ConfigureAwait(false); + if (delegationDetails is null) + { + return default; + } + + var inlayHintParams = new InlayHintParams + { + TextDocument = request.Identifier.TextDocumentIdentifier.WithUri(delegationDetails.Value.ProjectedUri), + Range = request.ProjectedRange + }; + + var response = await _requestInvoker.ReinvokeRequestOnServerAsync( + delegationDetails.Value.TextBuffer, + Methods.TextDocumentInlayHintName, + delegationDetails.Value.LanguageServerName, + inlayHintParams, + cancellationToken).ConfigureAwait(false); + return response?.Response; + } +} From 59011a23a1d4ccbce048ca7ead21178c230ee243 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 31 Jan 2024 14:26:45 +1100 Subject: [PATCH 02/14] Add inlayHintResolve support --- .../DelegatedTypes.cs | 5 ++ .../Common/CustomMessageNames.cs | 1 + .../InlayHints/InlayHintEndpoint.cs | 8 ++- .../InlayHints/InlayHintResolveEndpoint.cs | 59 +++++++++++++++++++ .../InlayHints/RazorInlayHintWrapper.cs | 13 ++++ .../RazorLanguageServer.cs | 1 + .../Endpoints/InlayHints.cs | 50 ++++++++++++++++ 7 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintResolveEndpoint.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/RazorInlayHintWrapper.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs index 5613328273e..d4eb639229e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs @@ -28,6 +28,11 @@ internal record DelegatedInlayHintParams( Range ProjectedRange, RazorLanguageKind ProjectedKind) : IDelegatedParams; +internal record DelegatedInlayHintResolveParams( + TextDocumentIdentifier Identifier, + InlayHint InlayHint, + RazorLanguageKind ProjectedKind); + internal record DelegatedValidateBreakpointRangeParams( TextDocumentIdentifierAndVersion Identifier, Range ProjectedRange, diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs index 2ac75b9eb41..67b1584e604 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs @@ -57,6 +57,7 @@ internal static class CustomMessageNames public const string RazorReferencesEndpointName = "razor/references"; public const string RazorInlayHintEndpoint = "razor/inlayHint"; + public const string RazorInlayHintResolveEndpoint = "razor/inlayHintResolve"; // Called to get C# diagnostics from Roslyn when publishing diagnostics for VS Code public const string RazorCSharpPullDiagnosticsEndpointName = "razor/csharpPullDiagnostics"; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs index 8f2af077764..99f6ca7e3f6 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs @@ -38,7 +38,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V serverCapabilities.InlayHintOptions = new InlayHintOptions { - ResolveProvider = false, + ResolveProvider = true, WorkDoneProgress = false }; } @@ -89,6 +89,12 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request) if (hint.Position.TryGetAbsoluteIndex(csharpSourceText, null, out var absoluteIndex) && _documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteIndex, out Position? hostDocumentPosition, out _)) { + hint.Data = new RazorInlayHintWrapper + { + TextDocument = request.TextDocument, + OriginalData = hint.Data, + OriginalPosition = hint.Position + }; hint.Position = hostDocumentPosition; inlayHintsBuilder.Add(hint); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintResolveEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintResolveEndpoint.cs new file mode 100644 index 00000000000..b9e8b132ceb --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintResolveEndpoint.cs @@ -0,0 +1,59 @@ +// 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.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; +using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; + +[LanguageServerEndpoint(Methods.InlayHintResolveName)] +internal sealed class InlayHintResolveEndpoint(IRazorDocumentMappingService documentMappingService, IClientConnection clientConnection) + : IRazorDocumentlessRequestHandler +{ + private readonly IRazorDocumentMappingService _documentMappingService = documentMappingService; + private readonly IClientConnection _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); + + public bool MutatesSolutionState => false; + + public async Task HandleRequestAsync(InlayHint request, RazorRequestContext context, CancellationToken cancellationToken) + { + if (request.Data is not RazorInlayHintWrapper inlayHintWrapper) + { + return null; + } + + var razorPosition = request.Position; + request.Position = inlayHintWrapper.OriginalPosition; + request.Data = inlayHintWrapper.OriginalData; + + // For now we only support C# inlay hints. Once Web Tools adds support we'll need to request from both servers and combine + // the results, much like folding ranges. + var delegatedRequest = new DelegatedInlayHintResolveParams( + Identifier: inlayHintWrapper.TextDocument, + InlayHint: request, + ProjectedKind: RazorLanguageKind.CSharp + ); + + var inlayHint = await _clientConnection.SendRequestAsync( + CustomMessageNames.RazorInlayHintResolveEndpoint, + delegatedRequest, + cancellationToken).ConfigureAwait(false); + + if (inlayHint is null) + { + return null; + } + + Debug.Assert(request.Position == inlayHint.Position, "Resolving inlay hints should not change the position of them."); + inlayHint.Position = razorPosition; + + return inlayHint; + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/RazorInlayHintWrapper.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/RazorInlayHintWrapper.cs new file mode 100644 index 00000000000..105382d2a6a --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/RazorInlayHintWrapper.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; + +internal class RazorInlayHintWrapper +{ + public required TextDocumentIdentifier TextDocument { get; set; } + public required object? OriginalData { get; set; } + public required Position OriginalPosition { get; set; } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 4a031f9905c..4dadb2eaabc 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -200,6 +200,7 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddHandler(); services.AddHandlerWithCapabilities(); + services.AddHandler(); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs index d2f3224ef22..0606c4c3482 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs @@ -1,10 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.LanguageServerClient.Razor.Extensions; using StreamJsonRpc; @@ -36,4 +38,52 @@ internal partial class RazorCustomMessageTarget cancellationToken).ConfigureAwait(false); return response?.Response; } + + [JsonRpcMethod(CustomMessageNames.RazorInlayHintResolveEndpoint, UseSingleObjectParameterDeserialization = true)] + public async Task ProvideInlayHintsResolveAsync(DelegatedInlayHintResolveParams request, CancellationToken cancellationToken) + { + // We don't really need the text document for inlay hint resolve, but we need to at least know which text + // buffer should get the request, so we just ask for any version above version 0, to get the right buffer. + string languageServerName; + var synchronized = false; + VirtualDocumentSnapshot? virtualDocumentSnapshot = null; + if (request.ProjectedKind == RazorLanguageKind.Html) + { + var syncResult = TryReturnPossiblyFutureSnapshot(0, request.Identifier); + if (syncResult?.Synchronized == true) + { + virtualDocumentSnapshot = syncResult.VirtualSnapshot; + } + + languageServerName = RazorLSPConstants.HtmlLanguageServerName; + } + else if (request.ProjectedKind == RazorLanguageKind.CSharp) + { + var syncResult = TryReturnPossiblyFutureSnapshot(0, request.Identifier); + if (syncResult?.Synchronized == true) + { + virtualDocumentSnapshot = syncResult.VirtualSnapshot; + } + + languageServerName = RazorLSPConstants.RazorCSharpLanguageServerName; + } + else + { + Debug.Fail("Unexpected RazorLanguageKind. This shouldn't really happen in a real scenario."); + return null; + } + + if (!synchronized || virtualDocumentSnapshot is null) + { + return null; + } + + var response = await _requestInvoker.ReinvokeRequestOnServerAsync( + virtualDocumentSnapshot.Snapshot.TextBuffer, + Methods.TextDocumentInlayHintName, + languageServerName, + request.InlayHint, + cancellationToken).ConfigureAwait(false); + return response?.Response; + } } From 42b77f295c4731f1b1fa772ba1ae919d907a25fa Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 31 Jan 2024 16:59:13 +1100 Subject: [PATCH 03/14] Allow configuring global options in the Roslyn server --- .../LanguageServer/CSharpTestLspServer.cs | 29 +++++++++++++++++-- .../CSharpTestLspServerHelpers.cs | 4 +++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs index 8efb7d18d1e..22d28b0ad83 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.VisualStudio.Composition; @@ -53,8 +54,6 @@ private CSharpTestLspServer( ExceptionStrategy = ExceptionProcessing.ISerializable, }; - _languageServer = CreateLanguageServer(_serverRpc, testWorkspace, languageServerFactory, exportProvider, serverCapabilities); - _clientMessageFormatter = CreateJsonMessageFormatter(languageServerFactory); _clientMessageHandler = new HeaderDelimitedMessageHandler(clientStream, clientStream, _clientMessageFormatter); _clientRpc = new JsonRpc(_clientMessageHandler) @@ -62,8 +61,14 @@ private CSharpTestLspServer( ExceptionStrategy = ExceptionProcessing.ISerializable, }; + // Roslyn will call back to us to get configuration options when the server is initialized, so this is how we configure + // what it options we need + _clientRpc.AddLocalRpcTarget(new WorkspaceConfigurationHandler()); + _clientRpc.StartListening(); + _languageServer = CreateLanguageServer(_serverRpc, testWorkspace, languageServerFactory, exportProvider, serverCapabilities); + static JsonMessageFormatter CreateJsonMessageFormatter(IRazorLanguageServerFactoryWrapper languageServerFactory) { var messageFormatter = new JsonMessageFormatter(); @@ -206,4 +211,24 @@ public string GetServerCapabilitiesJson(string clientCapabilitiesJson) return JsonConvert.SerializeObject(_serverCapabilities); } } + + private class WorkspaceConfigurationHandler + { + [JsonRpcMethod(Methods.WorkspaceConfigurationName, UseSingleObjectParameterDeserialization = true)] + public string[]? GetConfigurationOptions(ConfigurationParams configurationParams) + { + using var _ = ListPool.GetPooledObject(out var values); + values.SetCapacityIfLarger(configurationParams.Items.Length); + + foreach (var item in configurationParams.Items) + { + values.Add(item.Section switch + { + _ => "" + }); + } + + return values.ToArray(); + } + } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs index fe8a7c8f33c..f434824b60c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs @@ -74,6 +74,10 @@ public static async Task CreateCSharpLspServerAsync(IEnumer }, }, SupportsDiagnosticRequests = true, + Workspace = new() + { + Configuration = true + } }; return await CSharpTestLspServer.CreateAsync( From 784a198d976d0176a5bb2c01bb28bdc3885bbd50 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 31 Jan 2024 16:59:44 +1100 Subject: [PATCH 04/14] Add tests for inlay hints --- .../InlayHints/InlayHintEndpointTest.cs | 86 +++++++++++++++++++ ...tingEndpointTestBase.TestLanguageServer.cs | 20 +++++ .../LanguageServer/CSharpTestLspServer.cs | 2 + .../CSharpTestLspServerHelpers.cs | 4 + 4 files changed, 112 insertions(+) create mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs new file mode 100644 index 00000000000..5270adb208e --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs @@ -0,0 +1,86 @@ +// 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.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; +using Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; +using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.InlayHints; + +public class InlayHintEndpointTest(ITestOutputHelper testOutput) : SingleServerDelegatingEndpointTestBase(testOutput) +{ + [Fact] + public Task InlayHints() + => VerifyInlayHintsAsync( + """ + +
+ + @functions { + private void M(string thisIsMyString) + { + var {|int:x|} = 5; + + var {|string:y|} = "Hello"; + + M({|thisIsMyString:"Hello"|}); + } + } + + """); + + private async Task VerifyInlayHintsAsync(string input) + { + TestFileMarkupParser.GetSpans(input, out input, out ImmutableDictionary> spansDict); + var codeDocument = CreateCodeDocument(input); + var razorFilePath = "C:/path/to/file.razor"; + + var languageServer = await CreateLanguageServerAsync(codeDocument, razorFilePath); + + var endpoint = new InlayHintEndpoint(TestLanguageServerFeatureOptions.Instance, DocumentMappingService, languageServer); + + var request = new InlayHintParams() + { + TextDocument = new VSTextDocumentIdentifier + { + Uri = new Uri(razorFilePath) + }, + Range = new() + { + Start = new(0, 0), + End = new(codeDocument.Source.Text.Lines.Count, 0) + } + }; + var documentContext = DocumentContextFactory.TryCreateForOpenDocument(request.TextDocument); + var requestContext = CreateRazorRequestContext(documentContext); + + // Act + var hints = await endpoint.HandleRequestAsync(request, requestContext, DisposalToken); + + // Assert + Assert.NotNull(hints); + Assert.Equal(spansDict.Values.Count(), hints.Length); + + var sourceText = SourceText.From(input); + foreach (var hint in hints) + { + // Because our test input data can't have colons in the input, but parameter info returned from Roslyn does, we have to strip them off. + var label = hint.Label.First.TrimEnd(':'); + Assert.True(spansDict.TryGetValue(label, out var spans), $"Expected {label} to be in test provided markers"); + + var span = Assert.Single(spans); + var expectedRange = span.ToRange(sourceText); + // Inlay hints only have a position, so we ignore the end of the range that comes from the test input + Assert.Equal(expectedRange.Start, hint.Position); + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.TestLanguageServer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.TestLanguageServer.cs index e245adf6de2..abdeb9d79f2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.TestLanguageServer.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.TestLanguageServer.cs @@ -62,6 +62,7 @@ public async Task SendRequestAsync(string method, CustomMessageNames.RazorDocumentSymbolEndpoint => await HandleDocumentSymbolAsync(@params), CustomMessageNames.RazorProjectContextsEndpoint => await HandleProjectContextsAsync(@params), CustomMessageNames.RazorSimplifyMethodEndpointName => HandleSimplifyMethod(@params), + CustomMessageNames.RazorInlayHintEndpoint => await HandleInlayHintAsync(@params), _ => throw new NotImplementedException($"I don't know how to handle the '{method}' method.") }; @@ -92,6 +93,25 @@ private Task HandleProjectContextsAsync(TParams @ _cancellationToken); } + private Task HandleInlayHintAsync(TParams @params) + { + var delegatedParams = Assert.IsType(@params); + + var delegatedRequest = new InlayHintParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = _csharpDocumentUri, + }, + Range = delegatedParams.ProjectedRange + }; + + return _csharpServer.ExecuteRequestAsync( + Methods.TextDocumentInlayHintName, + delegatedRequest, + _cancellationToken); + } + private Task HandleDocumentSymbolAsync(TParams @params) { Assert.IsType(@params); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs index 22d28b0ad83..0c0cd6f4eb0 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs @@ -224,6 +224,8 @@ private class WorkspaceConfigurationHandler { values.Add(item.Section switch { + "csharp|inlay_hints.dotnet_enable_inlay_hints_for_parameters" => "true", + "csharp|inlay_hints.csharp_enable_inlay_hints_for_types" => "true", _ => "" }); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs index f434824b60c..49e99d38e01 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs @@ -72,6 +72,10 @@ public static async Task CreateCSharpLspServerAsync(IEnumer SnippetSupport = true } }, + InlayHint = new() + { + ResolveSupport = new InlayHintResolveSupportSetting { Properties = ["tooltip"] } + } }, SupportsDiagnosticRequests = true, Workspace = new() From 227bee94ba59a14261b9985f096a8c23abc941df Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 31 Jan 2024 17:42:18 +1100 Subject: [PATCH 05/14] Add test coverage for hint resolve --- .../InlayHints/InlayHintEndpointTest.cs | 34 +++++++++++++++++-- ...tingEndpointTestBase.TestLanguageServer.cs | 13 +++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs index 5270adb208e..5e83d08b982 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs @@ -2,6 +2,7 @@ // 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; using System.Threading.Tasks; @@ -36,9 +37,15 @@ private void M(string thisIsMyString) } } - """); + """, + new Dictionary + { + { "int", "struct System.Int32" }, + {"string", "class System.String" }, + {"thisIsMyString", "(parameter) string thisIsMyStr" } + }); - private async Task VerifyInlayHintsAsync(string input) + private async Task VerifyInlayHintsAsync(string input, Dictionary toolTipMap) { TestFileMarkupParser.GetSpans(input, out input, out ImmutableDictionary> spansDict); var codeDocument = CreateCodeDocument(input); @@ -46,7 +53,10 @@ private async Task VerifyInlayHintsAsync(string input) var languageServer = await CreateLanguageServerAsync(codeDocument, razorFilePath); - var endpoint = new InlayHintEndpoint(TestLanguageServerFeatureOptions.Instance, DocumentMappingService, languageServer); + var service = new InlayHintService(DocumentMappingService); + + var endpoint = new InlayHintEndpoint(TestLanguageServerFeatureOptions.Instance, service, languageServer); + var resolveEndpoint = new InlayHintResolveEndpoint(service, languageServer); var request = new InlayHintParams() { @@ -81,6 +91,24 @@ private async Task VerifyInlayHintsAsync(string input) var expectedRange = span.ToRange(sourceText); // Inlay hints only have a position, so we ignore the end of the range that comes from the test input Assert.Equal(expectedRange.Start, hint.Position); + + // This looks weird, but its what we have to do to satisfy the compiler :) + string? expectedTooltip = null; + Assert.True(toolTipMap?.TryGetValue(label, out expectedTooltip)); + Assert.NotNull(expectedTooltip); + + var resolvedHint = await resolveEndpoint.HandleRequestAsync(hint, requestContext, DisposalToken); + Assert.NotNull(resolvedHint); + Assert.NotNull(resolvedHint.ToolTip); + + if (resolvedHint.ToolTip.Value.TryGetFirst(out var plainTextTooltip)) + { + Assert.Equal(expectedTooltip, plainTextTooltip); + } + else if (resolvedHint.ToolTip.Value.TryGetSecond(out var markupTooltip)) + { + Assert.Contains(expectedTooltip, markupTooltip.Value); + } } } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.TestLanguageServer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.TestLanguageServer.cs index abdeb9d79f2..f246d5667f9 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.TestLanguageServer.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.TestLanguageServer.cs @@ -63,6 +63,7 @@ public async Task SendRequestAsync(string method, CustomMessageNames.RazorProjectContextsEndpoint => await HandleProjectContextsAsync(@params), CustomMessageNames.RazorSimplifyMethodEndpointName => HandleSimplifyMethod(@params), CustomMessageNames.RazorInlayHintEndpoint => await HandleInlayHintAsync(@params), + CustomMessageNames.RazorInlayHintResolveEndpoint => await HandleInlayHintResolveAsync(@params), _ => throw new NotImplementedException($"I don't know how to handle the '{method}' method.") }; @@ -112,6 +113,18 @@ private Task HandleInlayHintAsync(TParams @params) _cancellationToken); } + private Task HandleInlayHintResolveAsync(TParams @params) + { + var delegatedParams = Assert.IsType(@params); + + var delegatedRequest = delegatedParams.InlayHint; + + return _csharpServer.ExecuteRequestAsync( + Methods.InlayHintResolveName, + delegatedRequest, + _cancellationToken); + } + private Task HandleDocumentSymbolAsync(TParams @params) { Assert.IsType(@params); From 584cbc561a44747aadbad754550a032865fbc190 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 1 Feb 2024 19:33:10 +1100 Subject: [PATCH 06/14] Minor improvements to the endpoints, some to prepare for cohosting --- .../DelegatedTypes.cs | 4 +- .../VSInternalServerCapabilitiesExtensions.cs | 9 ++++ .../InlayHints/InlayHintEndpoint.cs | 19 ++++----- .../InlayHints/RazorInlayHintWrapper.cs | 3 +- .../Endpoints/InlayHints.cs | 42 +++---------------- 5 files changed, 27 insertions(+), 50 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs index d4eb639229e..1ab3f4b443b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Protocol/DelegatedTypes.cs @@ -29,9 +29,9 @@ internal record DelegatedInlayHintParams( RazorLanguageKind ProjectedKind) : IDelegatedParams; internal record DelegatedInlayHintResolveParams( - TextDocumentIdentifier Identifier, + TextDocumentIdentifierAndVersion Identifier, InlayHint InlayHint, - RazorLanguageKind ProjectedKind); + RazorLanguageKind ProjectedKind) : IDelegatedParams; internal record DelegatedValidateBreakpointRangeParams( TextDocumentIdentifierAndVersion Identifier, diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/VSInternalServerCapabilitiesExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/VSInternalServerCapabilitiesExtensions.cs index dc2c094f0c5..a510a98e9a1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/VSInternalServerCapabilitiesExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/VSInternalServerCapabilitiesExtensions.cs @@ -7,6 +7,15 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Common; internal static class VSInternalServerCapabilitiesExtensions { + public static void EnableInlayHints(this VSInternalServerCapabilities serverCapabilities) + { + serverCapabilities.InlayHintOptions = new InlayHintOptions + { + ResolveProvider = true, + WorkDoneProgress = false + }; + } + public static void EnableDocumentColorProvider(this VSInternalServerCapabilities serverCapabilities) { serverCapabilities.DocumentColorProvider = new DocumentColorOptions(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs index 99f6ca7e3f6..65491fb15c4 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs @@ -36,11 +36,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V return; } - serverCapabilities.InlayHintOptions = new InlayHintOptions - { - ResolveProvider = true, - WorkDoneProgress = false - }; + serverCapabilities.EnableInlayHints(); } public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request) @@ -49,14 +45,19 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request) public async Task HandleRequestAsync(InlayHintParams request, RazorRequestContext context, CancellationToken cancellationToken) { var documentContext = context.GetRequiredDocumentContext(); + return await GetInlayHintsAsync(documentContext, request.Range, cancellationToken).ConfigureAwait(false); + } + + private async Task GetInlayHintsAsync(VersionedDocumentContext documentContext, Range range, CancellationToken cancellationToken) + { var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); var csharpDocument = codeDocument.GetCSharpDocument(); // We are given a range by the client, but our mapping only succeeds if the start and end of the range can both be mapped // to C#. Since that doesn't logically match what we want from inlay hints, we instead get the minimum range of mappable // C# to get hints for. We'll filter that later, to remove the sections that can't be mapped back. - if (!_documentMappingService.TryMapToGeneratedDocumentRange(csharpDocument, request.Range, out var projectedRange) && - !RazorSemanticTokensInfoService.TryGetMinimalCSharpRange(codeDocument, request.Range, out projectedRange)) + if (!_documentMappingService.TryMapToGeneratedDocumentRange(csharpDocument, range, out var projectedRange) && + !RazorSemanticTokensInfoService.TryGetMinimalCSharpRange(codeDocument, range, out projectedRange)) { // There's no C# in the range. return null; @@ -81,9 +82,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request) } var csharpSourceText = codeDocument.GetCSharpSourceText(); - using var _1 = ArrayBuilderPool.GetPooledObject(out var inlayHintsBuilder); - foreach (var hint in inlayHints) { if (hint.Position.TryGetAbsoluteIndex(csharpSourceText, null, out var absoluteIndex) && @@ -91,7 +90,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request) { hint.Data = new RazorInlayHintWrapper { - TextDocument = request.TextDocument, + TextDocument = documentContext.Identifier, OriginalData = hint.Data, OriginalPosition = hint.Position }; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/RazorInlayHintWrapper.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/RazorInlayHintWrapper.cs index 105382d2a6a..252ddaac552 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/RazorInlayHintWrapper.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/RazorInlayHintWrapper.cs @@ -1,13 +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 Microsoft.AspNetCore.Razor.LanguageServer.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; internal class RazorInlayHintWrapper { - public required TextDocumentIdentifier TextDocument { get; set; } + public required TextDocumentIdentifierAndVersion TextDocument { get; set; } public required object? OriginalData { get; set; } public required Position OriginalPosition { get; set; } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs index 0606c4c3482..9171e481000 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/InlayHints.cs @@ -1,12 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; -using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.LanguageServerClient.Razor.Extensions; using StreamJsonRpc; @@ -42,46 +40,16 @@ internal partial class RazorCustomMessageTarget [JsonRpcMethod(CustomMessageNames.RazorInlayHintResolveEndpoint, UseSingleObjectParameterDeserialization = true)] public async Task ProvideInlayHintsResolveAsync(DelegatedInlayHintResolveParams request, CancellationToken cancellationToken) { - // We don't really need the text document for inlay hint resolve, but we need to at least know which text - // buffer should get the request, so we just ask for any version above version 0, to get the right buffer. - string languageServerName; - var synchronized = false; - VirtualDocumentSnapshot? virtualDocumentSnapshot = null; - if (request.ProjectedKind == RazorLanguageKind.Html) - { - var syncResult = TryReturnPossiblyFutureSnapshot(0, request.Identifier); - if (syncResult?.Synchronized == true) - { - virtualDocumentSnapshot = syncResult.VirtualSnapshot; - } - - languageServerName = RazorLSPConstants.HtmlLanguageServerName; - } - else if (request.ProjectedKind == RazorLanguageKind.CSharp) - { - var syncResult = TryReturnPossiblyFutureSnapshot(0, request.Identifier); - if (syncResult?.Synchronized == true) - { - virtualDocumentSnapshot = syncResult.VirtualSnapshot; - } - - languageServerName = RazorLSPConstants.RazorCSharpLanguageServerName; - } - else - { - Debug.Fail("Unexpected RazorLanguageKind. This shouldn't really happen in a real scenario."); - return null; - } - - if (!synchronized || virtualDocumentSnapshot is null) + var delegationDetails = await GetProjectedRequestDetailsAsync(request, cancellationToken).ConfigureAwait(false); + if (delegationDetails is null) { - return null; + return default; } var response = await _requestInvoker.ReinvokeRequestOnServerAsync( - virtualDocumentSnapshot.Snapshot.TextBuffer, + delegationDetails.Value.TextBuffer, Methods.TextDocumentInlayHintName, - languageServerName, + delegationDetails.Value.LanguageServerName, request.InlayHint, cancellationToken).ConfigureAwait(false); return response?.Response; From 0de594f13e07f2f3d0bb88450a060148bec5b413 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 1 Feb 2024 19:46:36 +1100 Subject: [PATCH 07/14] Extract inlay hint logic to a service --- .../InlayHints/IInlayHintService.cs | 15 +++ .../InlayHints/InlayHintEndpoint.cs | 71 +--------- .../InlayHints/InlayHintResolveEndpoint.cs | 44 +------ .../InlayHints/InlayHintService.cs | 121 ++++++++++++++++++ .../RazorLanguageServer.cs | 9 +- 5 files changed, 153 insertions(+), 107 deletions(-) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/IInlayHintService.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/IInlayHintService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/IInlayHintService.cs new file mode 100644 index 00000000000..e5934e23b55 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/IInlayHintService.cs @@ -0,0 +1,15 @@ +// 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.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; + +internal interface IInlayHintService +{ + Task GetInlayHintsAsync(IClientConnection clientConnection, VersionedDocumentContext documentContext, Range range, CancellationToken cancellationToken); + + Task ResolveInlayHintAsync(IClientConnection clientConnection, InlayHint inlayHint, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs index 65491fb15c4..0dabf907d10 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs @@ -1,30 +1,23 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; -using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; -using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; -using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; -using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.Workspaces; -using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; [LanguageServerEndpoint(Methods.TextDocumentInlayHintName)] -internal sealed class InlayHintEndpoint(LanguageServerFeatureOptions featureOptions, IRazorDocumentMappingService documentMappingService, IClientConnection clientConnection) +internal sealed class InlayHintEndpoint(LanguageServerFeatureOptions featureOptions, IInlayHintService inlayHintService, IClientConnection clientConnection) : IRazorRequestHandler, ICapabilitiesProvider { private readonly LanguageServerFeatureOptions _featureOptions = featureOptions; - private readonly IRazorDocumentMappingService _documentMappingService = documentMappingService; - private readonly IClientConnection _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); + private readonly IInlayHintService _inlayHintService = inlayHintService; + private readonly IClientConnection _clientConnection = clientConnection; public bool MutatesSolutionState => false; @@ -42,63 +35,9 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request) => request.TextDocument; - public async Task HandleRequestAsync(InlayHintParams request, RazorRequestContext context, CancellationToken cancellationToken) + public Task HandleRequestAsync(InlayHintParams request, RazorRequestContext context, CancellationToken cancellationToken) { var documentContext = context.GetRequiredDocumentContext(); - return await GetInlayHintsAsync(documentContext, request.Range, cancellationToken).ConfigureAwait(false); - } - - private async Task GetInlayHintsAsync(VersionedDocumentContext documentContext, Range range, CancellationToken cancellationToken) - { - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - var csharpDocument = codeDocument.GetCSharpDocument(); - - // We are given a range by the client, but our mapping only succeeds if the start and end of the range can both be mapped - // to C#. Since that doesn't logically match what we want from inlay hints, we instead get the minimum range of mappable - // C# to get hints for. We'll filter that later, to remove the sections that can't be mapped back. - if (!_documentMappingService.TryMapToGeneratedDocumentRange(csharpDocument, range, out var projectedRange) && - !RazorSemanticTokensInfoService.TryGetMinimalCSharpRange(codeDocument, range, out projectedRange)) - { - // There's no C# in the range. - return null; - } - - // For now we only support C# inlay hints. Once Web Tools adds support we'll need to request from both servers and combine - // the results, much like folding ranges. - var delegatedRequest = new DelegatedInlayHintParams( - Identifier: documentContext.Identifier, - ProjectedRange: projectedRange, - ProjectedKind: RazorLanguageKind.CSharp - ); - - var inlayHints = await _clientConnection.SendRequestAsync( - CustomMessageNames.RazorInlayHintEndpoint, - delegatedRequest, - cancellationToken).ConfigureAwait(false); - - if (inlayHints is null) - { - return null; - } - - var csharpSourceText = codeDocument.GetCSharpSourceText(); - using var _1 = ArrayBuilderPool.GetPooledObject(out var inlayHintsBuilder); - foreach (var hint in inlayHints) - { - if (hint.Position.TryGetAbsoluteIndex(csharpSourceText, null, out var absoluteIndex) && - _documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteIndex, out Position? hostDocumentPosition, out _)) - { - hint.Data = new RazorInlayHintWrapper - { - TextDocument = documentContext.Identifier, - OriginalData = hint.Data, - OriginalPosition = hint.Position - }; - hint.Position = hostDocumentPosition; - inlayHintsBuilder.Add(hint); - } - } - - return inlayHintsBuilder.ToArray(); + return _inlayHintService.GetInlayHintsAsync(_clientConnection, documentContext, request.Range, cancellationToken); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintResolveEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintResolveEndpoint.cs index b9e8b132ceb..bda2a341da4 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintResolveEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintResolveEndpoint.cs @@ -1,59 +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.Diagnostics; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; -using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; [LanguageServerEndpoint(Methods.InlayHintResolveName)] -internal sealed class InlayHintResolveEndpoint(IRazorDocumentMappingService documentMappingService, IClientConnection clientConnection) +internal sealed class InlayHintResolveEndpoint(IInlayHintService inlayHintService, IClientConnection clientConnection) : IRazorDocumentlessRequestHandler { - private readonly IRazorDocumentMappingService _documentMappingService = documentMappingService; - private readonly IClientConnection _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); + private readonly IInlayHintService _inlayHintService = inlayHintService; + private readonly IClientConnection _clientConnection = clientConnection; public bool MutatesSolutionState => false; - public async Task HandleRequestAsync(InlayHint request, RazorRequestContext context, CancellationToken cancellationToken) + public Task HandleRequestAsync(InlayHint request, RazorRequestContext context, CancellationToken cancellationToken) { - if (request.Data is not RazorInlayHintWrapper inlayHintWrapper) - { - return null; - } - - var razorPosition = request.Position; - request.Position = inlayHintWrapper.OriginalPosition; - request.Data = inlayHintWrapper.OriginalData; - - // For now we only support C# inlay hints. Once Web Tools adds support we'll need to request from both servers and combine - // the results, much like folding ranges. - var delegatedRequest = new DelegatedInlayHintResolveParams( - Identifier: inlayHintWrapper.TextDocument, - InlayHint: request, - ProjectedKind: RazorLanguageKind.CSharp - ); - - var inlayHint = await _clientConnection.SendRequestAsync( - CustomMessageNames.RazorInlayHintResolveEndpoint, - delegatedRequest, - cancellationToken).ConfigureAwait(false); - - if (inlayHint is null) - { - return null; - } - - Debug.Assert(request.Position == inlayHint.Position, "Resolving inlay hints should not change the position of them."); - inlayHint.Position = razorPosition; - - return inlayHint; + return _inlayHintService.ResolveInlayHintAsync(_clientConnection, request, cancellationToken); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs new file mode 100644 index 00000000000..97fb1a005b5 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.ComponentModel.Composition; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; +using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; +using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; + +[Export(typeof(IInlayHintService))] +[method: ImportingConstructor] +internal sealed class InlayHintService(IRazorDocumentMappingService documentMappingService) : IInlayHintService +{ + private readonly IRazorDocumentMappingService _documentMappingService = documentMappingService; + + public async Task GetInlayHintsAsync(IClientConnection clientConnection, VersionedDocumentContext documentContext, Range range, CancellationToken cancellationToken) + { + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + var csharpDocument = codeDocument.GetCSharpDocument(); + + // We are given a range by the client, but our mapping only succeeds if the start and end of the range can both be mapped + // to C#. Since that doesn't logically match what we want from inlay hints, we instead get the minimum range of mappable + // C# to get hints for. We'll filter that later, to remove the sections that can't be mapped back. + if (!_documentMappingService.TryMapToGeneratedDocumentRange(csharpDocument, range, out var projectedRange) && + !RazorSemanticTokensInfoService.TryGetMinimalCSharpRange(codeDocument, range, out projectedRange)) + { + // There's no C# in the range. + return null; + } + + // For now we only support C# inlay hints. Once Web Tools adds support we'll need to request from both servers and combine + // the results, much like folding ranges. + var delegatedRequest = new DelegatedInlayHintParams( + Identifier: documentContext.Identifier, + ProjectedRange: projectedRange, + ProjectedKind: RazorLanguageKind.CSharp + ); + + var inlayHints = await clientConnection.SendRequestAsync( + CustomMessageNames.RazorInlayHintEndpoint, + delegatedRequest, + cancellationToken).ConfigureAwait(false); + + if (inlayHints is null) + { + return null; + } + + var csharpSourceText = codeDocument.GetCSharpSourceText(); + using var _1 = ArrayBuilderPool.GetPooledObject(out var inlayHintsBuilder); + foreach (var hint in inlayHints) + { + if (hint.Position.TryGetAbsoluteIndex(csharpSourceText, null, out var absoluteIndex) && + _documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteIndex, out Position? hostDocumentPosition, out _)) + { + hint.TextEdits = _documentMappingService.GetHostDocumentEdits(csharpDocument, hint.TextEdits); + hint.Data = new RazorInlayHintWrapper + { + TextDocument = documentContext.Identifier, + OriginalData = hint.Data, + OriginalPosition = hint.Position + }; + hint.Position = hostDocumentPosition; + inlayHintsBuilder.Add(hint); + } + } + + return inlayHintsBuilder.ToArray(); + } + + public async Task ResolveInlayHintAsync(IClientConnection clientConnection, InlayHint inlayHint, CancellationToken cancellationToken) + { + if (inlayHint.Data is not JObject dataObj) + { + return null; + } + + var inlayHintWrapper = dataObj.ToObject(); + if (inlayHintWrapper is null) + { + return null; + } + + var razorPosition = inlayHint.Position; + inlayHint.Position = inlayHintWrapper.OriginalPosition; + inlayHint.Data = inlayHintWrapper.OriginalData; + + // For now we only support C# inlay hints. Once Web Tools adds support we'll need to inlayHint from both servers and combine + // the results, much like folding ranges. + var delegatedRequest = new DelegatedInlayHintResolveParams( + Identifier: inlayHintWrapper.TextDocument, + InlayHint: inlayHint, + ProjectedKind: RazorLanguageKind.CSharp + ); + + var resolvedHint = await clientConnection.SendRequestAsync( + CustomMessageNames.RazorInlayHintResolveEndpoint, + delegatedRequest, + cancellationToken).ConfigureAwait(false); + + if (resolvedHint is null) + { + return null; + } + + Debug.Assert(inlayHint.Position == resolvedHint.Position, "Resolving inlay hints should not change the position of them."); + resolvedHint.Position = razorPosition; + + return resolvedHint; + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 4dadb2eaabc..77a8e4b3e82 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -199,8 +199,13 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddHandlerWithCapabilities(); services.AddHandler(); - services.AddHandlerWithCapabilities(); - services.AddHandler(); + if (!featureOptions.UseRazorCohostServer) + { + services.AddSingleton(); + + services.AddHandlerWithCapabilities(); + services.AddHandler(); + } } } From 1dc73c89927a7f671c030d0f1119d81ee1510739 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 1 Feb 2024 19:48:52 +1100 Subject: [PATCH 08/14] Create cohost endpoints that use the service --- .../Cohost/CohostInlayHintEndpoint.cs | 59 +++++++++++++++++++ .../Cohost/CohostInlayHintResolveEndpoint.cs | 42 +++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintEndpoint.cs create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintResolveEndpoint.cs diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintEndpoint.cs new file mode 100644 index 00000000000..de9e82aaff9 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintEndpoint.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer; +using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.LanguageServerClient.Razor.Extensions; + +namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; + +[Shared] +[LanguageServerEndpoint(Methods.TextDocumentInlayHintName)] +[ExportRazorStatelessLspService(typeof(CohostInlayHintEndpoint))] +[Export(typeof(ICapabilitiesProvider))] +[method: ImportingConstructor] +internal sealed class CohostInlayHintEndpoint( + IInlayHintService inlayHintService, + IRazorLoggerFactory loggerFactory) + : AbstractRazorCohostDocumentRequestHandler, ICapabilitiesProvider +{ + private readonly IInlayHintService _inlayHintService = inlayHintService; + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + protected override bool MutatesSolutionState => false; + protected override bool RequiresLSPSolution => true; + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(InlayHintParams request) + => request.TextDocument.ToRazorTextDocumentIdentifier(); + + public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) + => serverCapabilities.EnableInlayHints(); + + protected override Task HandleRequestAsync(InlayHintParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + { + // TODO: Create document context from request.TextDocument, by looking at request.Solution instead of our project snapshots + var documentContext = context.GetRequiredDocumentContext(); + + _logger.LogDebug("[Cohost] Received inlay hint request for {requestPath} and got document {documentPath}", request.TextDocument.Uri, documentContext.FilePath); + + // TODO: We can't MEF import IRazorCohostClientLanguageServerManager in the constructor. We can make this work + // by having it implement a base class, RazorClientConnectionBase or something, that in turn implements + // AbstractRazorLspService (defined in Roslyn) and then move everything from importing IClientConnection + // to importing the new base class, so we can continue to share services. + // + // Until then we have to get the service from the request context. + var clientLanguageServerManager = context.GetRequiredService(); + var clientConnection = new RazorCohostClientConnection(clientLanguageServerManager); + + return _inlayHintService.GetInlayHintsAsync(clientConnection, documentContext, request.Range, cancellationToken); + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintResolveEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintResolveEndpoint.cs new file mode 100644 index 00000000000..03a1f9370e9 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintResolveEndpoint.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer; +using Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; + +[Shared] +[LanguageServerEndpoint(Methods.InlayHintResolveName)] +[ExportRazorStatelessLspService(typeof(CohostInlayHintResolveEndpoint))] +[Export(typeof(ICapabilitiesProvider))] +[method: ImportingConstructor] +internal sealed class CohostInlayHintResolveEndpoint( + IInlayHintService inlayHintService) + : AbstractRazorCohostRequestHandler +{ + private readonly IInlayHintService _inlayHintService = inlayHintService; + + protected override bool MutatesSolutionState => false; + protected override bool RequiresLSPSolution => true; + + protected override Task HandleRequestAsync(InlayHint request, RazorCohostRequestContext context, CancellationToken cancellationToken) + { + // TODO: We can't MEF import IRazorCohostClientLanguageServerManager in the constructor. We can make this work + // by having it implement a base class, RazorClientConnectionBase or something, that in turn implements + // AbstractRazorLspService (defined in Roslyn) and then move everything from importing IClientConnection + // to importing the new base class, so we can continue to share services. + // + // Until then we have to get the service from the request context. + var clientLanguageServerManager = context.GetRequiredService(); + var clientConnection = new RazorCohostClientConnection(clientLanguageServerManager); + + return _inlayHintService.ResolveInlayHintAsync(clientConnection, request, cancellationToken); + } +} From 1bba617d58cf5c32c131909e53c4d8e0e2d880d9 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 1 Feb 2024 19:49:45 +1100 Subject: [PATCH 09/14] Fix up comment (and the place it was copied from :P) --- .../Cohost/CohostDocumentColorEndpoint.cs | 1 - .../Cohost/CohostInlayHintEndpoint.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostDocumentColorEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostDocumentColorEndpoint.cs index 491ef208dae..911a18d4457 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostDocumentColorEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostDocumentColorEndpoint.cs @@ -39,7 +39,6 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V protected override Task HandleRequestAsync(DocumentColorParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) { - // TODO: Create document context from request.TextDocument, by looking at request.Solution instead of our project snapshots var documentContext = context.GetRequiredDocumentContext(); _logger.LogDebug("[Cohost] Received document color request for {requestPath} and got document {documentPath}", request.TextDocument.Uri, documentContext?.FilePath); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintEndpoint.cs index de9e82aaff9..390154d6370 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostInlayHintEndpoint.cs @@ -40,7 +40,6 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V protected override Task HandleRequestAsync(InlayHintParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) { - // TODO: Create document context from request.TextDocument, by looking at request.Solution instead of our project snapshots var documentContext = context.GetRequiredDocumentContext(); _logger.LogDebug("[Cohost] Received inlay hint request for {requestPath} and got document {documentPath}", request.TextDocument.Uri, documentContext.FilePath); From 85cf5219e821ff6b754db5f8ec0aa2d56b5d40ec Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 5 Feb 2024 22:35:33 +1100 Subject: [PATCH 10/14] Allow the endpoint to work in VS Code --- .../InlayHints/InlayHintEndpoint.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs index 0dabf907d10..25201bc33b5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintEndpoint.cs @@ -23,12 +23,6 @@ internal sealed class InlayHintEndpoint(LanguageServerFeatureOptions featureOpti public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) { - // Not supported in VS Code - if (!_featureOptions.SingleServerSupport) - { - return; - } - serverCapabilities.EnableInlayHints(); } From ce791f04225bf05d83c7cdacf0ae27bfdc3da99d Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 6 Feb 2024 09:10:26 +1100 Subject: [PATCH 11/14] Add test coverage for edits --- .../InlayHints/InlayHintService.cs | 7 +- .../InlayHints/InlayHintEndpointTest.cs | 65 ++++++++++++++----- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs index 97fb1a005b5..8c391e4adac 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs @@ -80,12 +80,13 @@ internal sealed class InlayHintService(IRazorDocumentMappingService documentMapp public async Task ResolveInlayHintAsync(IClientConnection clientConnection, InlayHint inlayHint, CancellationToken cancellationToken) { - if (inlayHint.Data is not JObject dataObj) + var inlayHintWrapper = inlayHint.Data as RazorInlayHintWrapper; + if (inlayHintWrapper is null && + inlayHint.Data is JObject dataObj) { - return null; + inlayHintWrapper = dataObj.ToObject(); } - var inlayHintWrapper = dataObj.ToObject(); if (inlayHintWrapper is null) { return null; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs index 5e83d08b982..ac3018ff0e6 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; +using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; @@ -22,30 +23,46 @@ public class InlayHintEndpointTest(ITestOutputHelper testOutput) : SingleServerD [Fact] public Task InlayHints() => VerifyInlayHintsAsync( - """ + input: """ -
+
- @functions { - private void M(string thisIsMyString) + @functions { + private void M(string thisIsMyString) + { + var {|int:x|} = 5; + + var {|string:y|} = "Hello"; + + M({|thisIsMyString:"Hello"|}); + } + } + + """, + toolTipMap: new Dictionary { - var {|int:x|} = 5; + { "int", "struct System.Int32" }, + {"string", "class System.String" }, + {"thisIsMyString", "(parameter) string thisIsMyStr" } + }, + output: """ - var {|string:y|} = "Hello"; +
- M({|thisIsMyString:"Hello"|}); + @functions { + private void M(string thisIsMyString) + { + int x = 5; + + string y = "Hello"; + + M(thisIsMyString: "Hello"); + } } - } - - """, - new Dictionary - { - { "int", "struct System.Int32" }, - {"string", "class System.String" }, - {"thisIsMyString", "(parameter) string thisIsMyStr" } - }); - private async Task VerifyInlayHintsAsync(string input, Dictionary toolTipMap) + """); + + private async Task VerifyInlayHintsAsync(string input, Dictionary toolTipMap, string output) { TestFileMarkupParser.GetSpans(input, out input, out ImmutableDictionary> spansDict); var codeDocument = CreateCodeDocument(input); @@ -110,5 +127,19 @@ private async Task VerifyInlayHintsAsync(string input, Dictionary h.TextEdits ?? []) + .OrderByDescending(e => e.Range.Start.Line) + .ThenByDescending(e => e.Range.Start.Character) + .ToArray(); + foreach (var edit in edits) + { + var change = edit.ToTextChange(sourceText); + sourceText = sourceText.WithChanges(change); + } + + AssertEx.EqualOrDiff(output, sourceText.ToString()); } } From d74e3afbb8ec8f92c9e7bd721909b25c64888728 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 6 Feb 2024 13:07:53 +1100 Subject: [PATCH 12/14] Move custom message name up to the right section --- .../Common/CustomMessageNames.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs index 67b1584e604..fff05759c9f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Common/CustomMessageNames.cs @@ -38,6 +38,8 @@ internal static class CustomMessageNames public const string RazorHtmlOnTypeFormattingEndpoint = "razor/htmlOnTypeFormatting"; public const string RazorSimplifyMethodEndpointName = "razor/simplifyMethod"; public const string RazorFormatNewFileEndpointName = "razor/formatNewFile"; + public const string RazorInlayHintEndpoint = "razor/inlayHint"; + public const string RazorInlayHintResolveEndpoint = "razor/inlayHintResolve"; // VS Windows only at the moment, but could/should be migrated public const string RazorDocumentSymbolEndpoint = "razor/documentSymbol"; @@ -56,9 +58,6 @@ internal static class CustomMessageNames public const string RazorReferencesEndpointName = "razor/references"; - public const string RazorInlayHintEndpoint = "razor/inlayHint"; - public const string RazorInlayHintResolveEndpoint = "razor/inlayHintResolve"; - // Called to get C# diagnostics from Roslyn when publishing diagnostics for VS Code public const string RazorCSharpPullDiagnosticsEndpointName = "razor/csharpPullDiagnostics"; } From 7be1145ac12598bbc4fac36c60805dd8ce841969 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 6 Feb 2024 13:34:25 +1100 Subject: [PATCH 13/14] Fix nullable warning --- .../InlayHints/InlayHintService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs index 8c391e4adac..255dfaa29f4 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs @@ -63,7 +63,11 @@ internal sealed class InlayHintService(IRazorDocumentMappingService documentMapp if (hint.Position.TryGetAbsoluteIndex(csharpSourceText, null, out var absoluteIndex) && _documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteIndex, out Position? hostDocumentPosition, out _)) { - hint.TextEdits = _documentMappingService.GetHostDocumentEdits(csharpDocument, hint.TextEdits); + if (hint.TextEdits is not null) + { + hint.TextEdits = _documentMappingService.GetHostDocumentEdits(csharpDocument, hint.TextEdits); + } + hint.Data = new RazorInlayHintWrapper { TextDocument = documentContext.Identifier, From e5268fc4f957fd29ddc2bdd09d16137f6505d135 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 6 Feb 2024 21:03:16 +1100 Subject: [PATCH 14/14] Move method out of semantic tokens service --- .../Extensions/RazorCodeDocumentExtensions.cs | 50 +++++++++++++++++++ .../InlayHints/InlayHintService.cs | 3 +- .../RazorSemanticTokensInfoService.cs | 49 +----------------- .../Semantic/SemanticTokensTest.cs | 4 +- 4 files changed, 54 insertions(+), 52 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/RazorCodeDocumentExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/RazorCodeDocumentExtensions.cs index 45e6ebdc198..af3366b49ef 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/RazorCodeDocumentExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/RazorCodeDocumentExtensions.cs @@ -1,8 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; namespace Microsoft.AspNetCore.Razor.LanguageServer.Extensions; @@ -15,4 +19,50 @@ public static IRazorGeneratedDocument GetGeneratedDocument(this RazorCodeDocumen RazorLanguageKind.Html => document.GetHtmlDocument(), _ => throw new System.InvalidOperationException(), }; + + public static bool TryGetMinimalCSharpRange(this RazorCodeDocument codeDocument, Range razorRange, [NotNullWhen(true)] out Range? csharpRange) + { + SourceSpan? minGeneratedSpan = null; + SourceSpan? maxGeneratedSpan = null; + + var sourceText = codeDocument.GetSourceText(); + var textSpan = razorRange.ToTextSpan(sourceText); + var csharpDoc = codeDocument.GetCSharpDocument(); + + // We want to find the min and max C# source mapping that corresponds with our Razor range. + foreach (var mapping in csharpDoc.SourceMappings) + { + var mappedTextSpan = mapping.OriginalSpan.AsTextSpan(); + + if (textSpan.OverlapsWith(mappedTextSpan)) + { + if (minGeneratedSpan is null || mapping.GeneratedSpan.AbsoluteIndex < minGeneratedSpan.Value.AbsoluteIndex) + { + minGeneratedSpan = mapping.GeneratedSpan; + } + + var mappingEndIndex = mapping.GeneratedSpan.AbsoluteIndex + mapping.GeneratedSpan.Length; + if (maxGeneratedSpan is null || mappingEndIndex > maxGeneratedSpan.Value.AbsoluteIndex + maxGeneratedSpan.Value.Length) + { + maxGeneratedSpan = mapping.GeneratedSpan; + } + } + } + + // Create a new projected range based on our calculated min/max source spans. + if (minGeneratedSpan is not null && maxGeneratedSpan is not null) + { + var csharpSourceText = codeDocument.GetCSharpSourceText(); + var startRange = minGeneratedSpan.Value.ToRange(csharpSourceText); + var endRange = maxGeneratedSpan.Value.ToRange(csharpSourceText); + + csharpRange = new Range { Start = startRange.Start, End = endRange.End }; + Debug.Assert(csharpRange.Start.CompareTo(csharpRange.End) <= 0, "Range.Start should not be larger than Range.End"); + + return true; + } + + csharpRange = null; + return false; + } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs index 255dfaa29f4..53bd3a863b9 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; -using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -32,7 +31,7 @@ internal sealed class InlayHintService(IRazorDocumentMappingService documentMapp // to C#. Since that doesn't logically match what we want from inlay hints, we instead get the minimum range of mappable // C# to get hints for. We'll filter that later, to remove the sections that can't be mapped back. if (!_documentMappingService.TryMapToGeneratedDocumentRange(csharpDocument, range, out var projectedRange) && - !RazorSemanticTokensInfoService.TryGetMinimalCSharpRange(codeDocument, range, out projectedRange)) + !codeDocument.TryGetMinimalCSharpRange(range, out projectedRange)) { // There's no C# in the range. return null; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs index 769f3f39434..55ffe624c7f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs @@ -190,7 +190,7 @@ private static ImmutableArray CombineSemanticRanges(ImmutableArra // We'll try to call into the mapping service to map to the projected range for us. If that doesn't work, // we'll try to find the minimal range ourselves. if (!_documentMappingService.TryMapToGeneratedDocumentRange(generatedDocument, razorRange, out var csharpRange) && - !TryGetMinimalCSharpRange(codeDocument, razorRange, out csharpRange)) + !codeDocument.TryGetMinimalCSharpRange(razorRange, out csharpRange)) { // There's no C# in the range. return ImmutableArray.Empty; @@ -286,53 +286,6 @@ private static bool ContainsOnlySpacesOrTabs(SourceText razorSource, int startIn return true; } - // Internal for testing only - internal static bool TryGetMinimalCSharpRange(RazorCodeDocument codeDocument, Range razorRange, [NotNullWhen(true)] out Range? csharpRange) - { - SourceSpan? minGeneratedSpan = null; - SourceSpan? maxGeneratedSpan = null; - - var sourceText = codeDocument.GetSourceText(); - var textSpan = razorRange.ToTextSpan(sourceText); - var csharpDoc = codeDocument.GetCSharpDocument(); - - // We want to find the min and max C# source mapping that corresponds with our Razor range. - foreach (var mapping in csharpDoc.SourceMappings) - { - var mappedTextSpan = mapping.OriginalSpan.AsTextSpan(); - - if (textSpan.OverlapsWith(mappedTextSpan)) - { - if (minGeneratedSpan is null || mapping.GeneratedSpan.AbsoluteIndex < minGeneratedSpan.Value.AbsoluteIndex) - { - minGeneratedSpan = mapping.GeneratedSpan; - } - - var mappingEndIndex = mapping.GeneratedSpan.AbsoluteIndex + mapping.GeneratedSpan.Length; - if (maxGeneratedSpan is null || mappingEndIndex > maxGeneratedSpan.Value.AbsoluteIndex + maxGeneratedSpan.Value.Length) - { - maxGeneratedSpan = mapping.GeneratedSpan; - } - } - } - - // Create a new projected range based on our calculated min/max source spans. - if (minGeneratedSpan is not null && maxGeneratedSpan is not null) - { - var csharpSourceText = codeDocument.GetCSharpSourceText(); - var startRange = minGeneratedSpan.Value.ToRange(csharpSourceText); - var endRange = maxGeneratedSpan.Value.ToRange(csharpSourceText); - - csharpRange = new Range { Start = startRange.Start, End = endRange.End }; - Debug.Assert(csharpRange.Start.CompareTo(csharpRange.End) <= 0, "Range.Start should not be larger than Range.End"); - - return true; - } - - csharpRange = null; - return false; - } - private async Task GetMatchingCSharpResponseAsync( IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs index 929419ed5de..411140657f9 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs @@ -891,7 +891,7 @@ public void GetMappedCSharpRanges_MinimalRangeVsSmallDisjointRanges_DisjointRang { // Note that the expected lengths are different on Windows vs. Unix. var expectedCsharpRangeLength = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 970 : 938; - Assert.True(RazorSemanticTokensInfoService.TryGetMinimalCSharpRange(codeDocument, razorRange, out var csharpRange)); + Assert.True(codeDocument.TryGetMinimalCSharpRange(razorRange, out var csharpRange)); var textSpan = csharpRange.ToTextSpan(csharpSourceText); Assert.Equal(expectedCsharpRangeLength, textSpan.Length); } @@ -1139,7 +1139,7 @@ private string GetBaselineFileContents(string baselineFileName) } if (!documentMappingService.TryMapToGeneratedDocumentRange(codeDocument.GetCSharpDocument(), razorRange, out var range) && - !RazorSemanticTokensInfoService.TryGetMinimalCSharpRange(codeDocument, razorRange, out range)) + !codeDocument.TryGetMinimalCSharpRange(razorRange, out range)) { // No C# in the range. return null;