From a7cd940cd7800ce5e791ac9033d9a495f9944cf3 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 3 Sep 2024 16:21:10 +1000 Subject: [PATCH 1/5] Cohost go to implementation --- eng/targets/Services.props | 1 + .../RazorLanguageServer.cs | 4 +- .../IRemoteGoToImplementationService.cs | 19 ++ .../Remote/RazorServices.cs | 1 + .../RemoteGoToDefinitionService.cs | 15 +- .../RemoteGoToImplementationService.cs | 111 ++++++++++++ .../RazorDocumentServiceBase.cs | 33 +++- .../CohostGoToImplementationEndpoint.cs | 153 ++++++++++++++++ .../CohostGoToImplementationEndpointTest.cs | 166 ++++++++++++++++++ 9 files changed, 484 insertions(+), 19 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteGoToImplementationService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToImplementation/RemoteGoToImplementationService.cs create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToImplementationEndpoint.cs create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs diff --git a/eng/targets/Services.props b/eng/targets/Services.props index b2476eab94a..07c6ab44565 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -29,5 +29,6 @@ + diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index d3162a0e5b1..f1034646716 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -184,10 +184,10 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption // Transient because it should only be used once and I'm hoping it doesn't stick around. services.AddTransient(sp => sp.GetRequiredService()); - services.AddHandlerWithCapabilities(); - if (!featureOptions.UseRazorCohostServer) { + services.AddHandlerWithCapabilities(); + services.AddSingleton(); services.AddHandlerWithCapabilities(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteGoToImplementationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteGoToImplementationService.cs new file mode 100644 index 00000000000..41311404578 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteGoToImplementationService.cs @@ -0,0 +1,19 @@ +// 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 RoslynLocation = Roslyn.LanguageServer.Protocol.Location; +using RoslynPosition = Roslyn.LanguageServer.Protocol.Position; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +internal interface IRemoteGoToImplementationService : IRemoteJsonService +{ + ValueTask> GetImplementationAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId razorDocumentId, + RoslynPosition position, + 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 558b737d4c4..1a86184e8a2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -32,6 +32,7 @@ internal static class RazorServices (typeof(IRemoteInlayHintService), null), (typeof(IRemoteDocumentSymbolService), null), (typeof(IRemoteRenameService), null), + (typeof(IRemoteGoToImplementationService), null), ]; private const string ComponentName = "Razor"; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs index 58d6abd3e0a..6169c994349 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs @@ -58,20 +58,7 @@ protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs arg return NoFurtherHandling; } - var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex); - - if (positionInfo.LanguageKind == RazorLanguageKind.Html) - { - // Sometimes Html can actually be mapped to C#, like for example component attributes, which map to - // C# properties, even though they appear entirely in a Html context. Since remapping is pretty cheap - // it's easier to just try mapping, and see what happens, rather than checking for specific syntax nodes. - if (DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out VsPosition? csharpPosition, out _)) - { - // We're just gonna pretend this mapped perfectly normally onto C#. Moving this logic to the actual position info - // calculating code is possible, but could have untold effects, so opt-in is better (for now?) - positionInfo = positionInfo with { LanguageKind = RazorLanguageKind.CSharp, Position = csharpPosition }; - } - } + var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: true); if (positionInfo.LanguageKind is RazorLanguageKind.Html or RazorLanguageKind.Razor) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToImplementation/RemoteGoToImplementationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToImplementation/RemoteGoToImplementationService.cs new file mode 100644 index 00000000000..7121cd6dbf0 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToImplementation/RemoteGoToImplementationService.cs @@ -0,0 +1,111 @@ +// 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.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Remote.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Roslyn.LanguageServer.Protocol; +using static Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; +using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; +using RoslynLocation = Roslyn.LanguageServer.Protocol.Location; +using RoslynPosition = Roslyn.LanguageServer.Protocol.Position; +using VsPosition = Microsoft.VisualStudio.LanguageServer.Protocol.Position; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +internal sealed class RemoteGoToImplementationService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteGoToImplementationService +{ + internal sealed class Factory : FactoryBase + { + protected override IRemoteGoToImplementationService CreateService(in ServiceArgs args) + => new RemoteGoToImplementationService(in args); + } + + protected override IDocumentPositionInfoStrategy DocumentPositionInfoStrategy => PreferAttributeNameDocumentPositionInfoStrategy.Instance; + + public ValueTask> GetImplementationAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + RoslynPosition position, + CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + documentId, + context => GetImplementationAsync(context, position, cancellationToken), + cancellationToken); + + private async ValueTask> GetImplementationAsync( + RemoteDocumentContext context, + RoslynPosition position, + CancellationToken cancellationToken) + { + var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + if (!codeDocument.Source.Text.TryGetAbsoluteIndex(position, out var hostDocumentIndex)) + { + return NoFurtherHandling; + } + + var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: true); + + if (positionInfo.LanguageKind is RazorLanguageKind.Razor) + { + return NoFurtherHandling; + } + + if (positionInfo.LanguageKind is RazorLanguageKind.Html) + { + return CallHtml; + } + + if (!DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out var mappedPosition, out _)) + { + // If we can't map to the generated C# file, we're done. + return NoFurtherHandling; + } + + // Finally, call into C#. + var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false); + + var locations = await ExternalHandlers.GoToImplementation + .FindImplementationsAsync( + generatedDocument, + mappedPosition, + supportsVisualStudioExtensions: true, + cancellationToken) + .ConfigureAwait(false); + + if (locations is null and not []) + { + // C# didn't return anything, so we're done. + return NoFurtherHandling; + } + + // Map the C# locations back to the Razor file. + using var mappedLocations = new PooledArrayBuilder(locations.Length); + + foreach (var location in locations) + { + var (uri, range) = location; + + var (mappedDocumentUri, mappedRange) = await DocumentMappingService + .MapToHostDocumentUriAndRangeAsync(context.Snapshot, uri, range.ToLinePositionSpan(), cancellationToken) + .ConfigureAwait(false); + + var mappedLocation = RoslynLspFactory.CreateLocation(mappedDocumentUri, mappedRange); + + mappedLocations.Add(mappedLocation); + } + + return Results(mappedLocations.ToArray()); + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorDocumentServiceBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorDocumentServiceBase.cs index 9327d9931e7..557fc66d271 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorDocumentServiceBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorDocumentServiceBase.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -24,11 +25,34 @@ internal abstract class RazorDocumentServiceBase(in ServiceArgs args) : RazorBro protected virtual IDocumentPositionInfoStrategy DocumentPositionInfoStrategy { get; } = DefaultDocumentPositionInfoStrategy.Instance; protected DocumentPositionInfo GetPositionInfo(RazorCodeDocument codeDocument, int hostDocumentIndex) + => GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: false); + + protected DocumentPositionInfo GetPositionInfo(RazorCodeDocument codeDocument, int hostDocumentIndex, bool preferCSharpOverHtml) { - return DocumentPositionInfoStrategy.GetPositionInfo(DocumentMappingService, codeDocument, hostDocumentIndex); + var positionInfo = DocumentPositionInfoStrategy.GetPositionInfo(DocumentMappingService, codeDocument, hostDocumentIndex); + + if (preferCSharpOverHtml && positionInfo.LanguageKind == RazorLanguageKind.Html) + { + // Sometimes Html can actually be mapped to C#, like for example component attributes, which map to + // C# properties, even though they appear entirely in a Html context. Since remapping is pretty cheap + // it's easier to just try mapping, and see what happens, rather than checking for specific syntax nodes. + if (DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), positionInfo.HostDocumentIndex, out VsPosition? csharpPosition, out _)) + { + // We're just gonna pretend this mapped perfectly normally onto C#. Moving this logic to the actual position info + // calculating code is possible, but could have untold effects, so opt-in is better (for now?) + + // TODO: Not using a with operator here because it doesn't work in OOP for some reason. + positionInfo = new DocumentPositionInfo(RazorLanguageKind.CSharp, csharpPosition, positionInfo.HostDocumentIndex); + } + } + + return positionInfo; } protected bool TryGetDocumentPositionInfo(RazorCodeDocument codeDocument, RoslynPosition position, out DocumentPositionInfo positionInfo) + => TryGetDocumentPositionInfo(codeDocument, position, preferCSharpOverHtml: false, out positionInfo); + + protected bool TryGetDocumentPositionInfo(RazorCodeDocument codeDocument, RoslynPosition position, bool preferCSharpOverHtml, out DocumentPositionInfo positionInfo) { if (!codeDocument.Source.Text.TryGetAbsoluteIndex(position, out var hostDocumentIndex)) { @@ -36,11 +60,14 @@ protected bool TryGetDocumentPositionInfo(RazorCodeDocument codeDocument, Roslyn return false; } - positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex); + positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml); return true; } protected bool TryGetDocumentPositionInfo(RazorCodeDocument codeDocument, VsPosition position, out DocumentPositionInfo positionInfo) + => TryGetDocumentPositionInfo(codeDocument, position, preferCSharpOverHtml: false, out positionInfo); + + protected bool TryGetDocumentPositionInfo(RazorCodeDocument codeDocument, VsPosition position, bool preferCSharpOverHtml, out DocumentPositionInfo positionInfo) { if (!codeDocument.Source.Text.TryGetAbsoluteIndex(position, out var hostDocumentIndex)) { @@ -48,7 +75,7 @@ protected bool TryGetDocumentPositionInfo(RazorCodeDocument codeDocument, VsPosi return false; } - positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex); + positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml); return true; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToImplementationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToImplementationEndpoint.cs new file mode 100644 index 00000000000..be8e197bbbd --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToImplementationEndpoint.cs @@ -0,0 +1,153 @@ +// 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; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using RoslynLspFactory = Roslyn.LanguageServer.Protocol.RoslynLspFactory; +using RoslynLspLocation = Roslyn.LanguageServer.Protocol.Location; +using VsLspLocation = Microsoft.VisualStudio.LanguageServer.Protocol.Location; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +#pragma warning disable RS0030 // Do not use banned APIs +[Shared] +[CohostEndpoint(Methods.TextDocumentImplementationName)] +[Export(typeof(IDynamicRegistrationProvider))] +[ExportCohostStatelessLspService(typeof(CohostGoToImplementationEndpoint))] +[method: ImportingConstructor] +#pragma warning restore RS0030 // Do not use banned APIs +internal sealed class CohostGoToImplementationEndpoint( + IRemoteServiceInvoker remoteServiceInvoker, + IHtmlDocumentSynchronizer htmlDocumentSynchronizer, + LSPRequestInvoker requestInvoker, + IFilePathService filePathService) + : AbstractRazorCohostDocumentRequestHandler?>, IDynamicRegistrationProvider +{ + private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker; + private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer; + private readonly LSPRequestInvoker _requestInvoker = requestInvoker; + private readonly IFilePathService _filePathService = filePathService; + + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => true; + + public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + { + if (clientCapabilities.TextDocument?.Implementation?.DynamicRegistration == true) + { + return new Registration + { + Method = Methods.TextDocumentImplementationName, + RegisterOptions = new ImplementationOptions() + }; + } + + return null; + } + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(TextDocumentPositionParams request) + => request.TextDocument.ToRazorTextDocumentIdentifier(); + + protected override Task?> HandleRequestAsync(TextDocumentPositionParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync( + request, + context.TextDocument.AssumeNotNull(), + cancellationToken); + + private async Task?> HandleRequestAsync(TextDocumentPositionParams request, TextDocument razorDocument, CancellationToken cancellationToken) + { + var position = RoslynLspFactory.CreatePosition(request.Position.ToLinePosition()); + + var response = await _remoteServiceInvoker + .TryInvokeAsync>( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => + service.GetImplementationAsync(solutionInfo, razorDocument.Id, position, cancellationToken), + cancellationToken) + .ConfigureAwait(false); + + if (response.Result is RoslynLspLocation[] locations) + { + return locations; + } + + if (response.StopHandling) + { + return null; + } + + return await GetHtmlImplementationsAsync(request, razorDocument, cancellationToken).ConfigureAwait(false); + } + + private async Task?> GetHtmlImplementationsAsync(TextDocumentPositionParams request, TextDocument razorDocument, CancellationToken cancellationToken) + { + var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false); + if (htmlDocument is null) + { + return null; + } + + request.TextDocument = request.TextDocument.WithUri(htmlDocument.Uri); + + var result = await _requestInvoker + .ReinvokeRequestOnServerAsync?>( + htmlDocument.Buffer, + Methods.TextDocumentImplementationName, + RazorLSPConstants.HtmlLanguageServerName, + request, + cancellationToken) + .ConfigureAwait(false); + + if (result is not { Response: { } response }) + { + return null; + } + + if (response.TryGetFirst(out var locations)) + { + foreach (var location in locations) + { + RemapVirtualHtmlUri(location); + } + + return locations; + } + else if (response.TryGetSecond(out var referenceItems)) + { + foreach (var referenceItem in referenceItems) + { + RemapVirtualHtmlUri(referenceItem.Location); + } + + return referenceItems; + } + + return null; + } + + private void RemapVirtualHtmlUri(VsLspLocation location) + { + if (_filePathService.IsVirtualHtmlFile(location.Uri)) + { + location.Uri = _filePathService.GetRazorDocumentUri(location.Uri); + } + } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(CohostGoToImplementationEndpoint instance) + { + public Task?> HandleRequestAsync( + TextDocumentPositionParams request, TextDocument razorDocument, CancellationToken cancellationToken) + => instance.HandleRequestAsync(request, razorDocument, cancellationToken); + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs new file mode 100644 index 00000000000..170ca71d6a6 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs @@ -0,0 +1,166 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Remote.Razor; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Xunit; +using Xunit.Abstractions; +using LspLocation = Microsoft.VisualStudio.LanguageServer.Protocol.Location; +using RoslynLspExtensions = Roslyn.LanguageServer.Protocol.RoslynLspExtensions; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +public class CohostGoToImplementationEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +{ + [Fact] + public async Task CSharp_Method() + { + var input = """ +
+ + @{ + var x = Ge$$tX(); + } + + @code + { + void [|GetX|]() + { + } + } + """; + + await VerifyCSharpGoToImplementationAsync(input); + } + + [Fact] + public async Task CSharp_Field() + { + var input = """ +
+ + @{ + var x = GetX(); + } + + @code + { + private string [|_name|]; + + string GetX() + { + return _na$$me; + } + } + """; + + await VerifyCSharpGoToImplementationAsync(input); + } + + [Fact] + public async Task CSharp_Multiple() + { + var input = """ +
+ + @code + { + class [|Base|] { } + class [|Derived1|] : Base { } + class [|Derived2|] : Base { } + + void M(Ba$$se b) + { + } + } + """; + + await VerifyCSharpGoToImplementationAsync(input); + } + + [Fact] + public async Task Html() + { + // This really just validates Uri remapping, the actual response is largely arbitrary + + TestCode input = """ +
+ + + """; + + var document = CreateProjectAndRazorDocument(input.Text); + var inputText = await document.GetTextAsync(DisposalToken); + + var htmlResponse = new SumType?(new LspLocation[] + { + new LspLocation + { + Uri = new Uri(document.CreateUri(), document.Name + FeatureOptions.HtmlVirtualDocumentSuffix), + Range = inputText.GetRange(input.Span), + }, + }); + + var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentImplementationName, htmlResponse)]); + + await VerifyGoToImplementationResultAsync(input, document, requestInvoker); + } + + private async Task VerifyCSharpGoToImplementationAsync(TestCode input) + { + var document = CreateProjectAndRazorDocument(input.Text); + + var requestInvoker = new TestLSPRequestInvoker(); + + await VerifyGoToImplementationResultAsync(input, document, requestInvoker); + } + + private async Task VerifyGoToImplementationResultAsync(TestCode input, TextDocument document, TestLSPRequestInvoker requestInvoker) + { + var inputText = await document.GetTextAsync(DisposalToken); + + var filePathService = new RemoteFilePathService(FeatureOptions); + var endpoint = new CohostGoToImplementationEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker, filePathService); + + var position = inputText.GetPosition(input.Position); + var textDocumentPositionParams = new TextDocumentPositionParams + { + Position = position, + TextDocument = new TextDocumentIdentifier { Uri = document.CreateUri() }, + }; + + var result = await endpoint.GetTestAccessor().HandleRequestAsync(textDocumentPositionParams, document, DisposalToken); + + if (result.Value.TryGetFirst(out var roslynLocations)) + { + var expected = input.Spans.Select(s => inputText.GetRange(s).ToLinePositionSpan()).OrderBy(r => r.Start.Line).ToArray(); + var actual = roslynLocations.Select(l => RoslynLspExtensions.ToLinePositionSpan(l.Range)).OrderBy(r => r.Start.Line).ToArray(); + Assert.Equal(expected, actual); + + Assert.All(roslynLocations, l => l.Uri.Equals(document.CreateUri())); + } + else if (result.Value.TryGetSecond(out var vsLocations)) + { + var expected = input.Spans.Select(s => inputText.GetRange(s).ToLinePositionSpan()).OrderBy(r => r.Start.Line).ToArray(); + var actual = vsLocations.Select(l => l.Range.ToLinePositionSpan()).OrderBy(r => r.Start.Line).ToArray(); + Assert.Equal(expected, actual); + + Assert.All(vsLocations, l => l.Uri.Equals(document.CreateUri())); + } + else + { + Assert.Fail($"Unsupported result type: {result.Value.GetType()}"); + } + } +} From 84c2983392827a137e6b55be48f03a7a0dceb6b8 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 4 Sep 2024 11:21:13 +1000 Subject: [PATCH 2/5] Whitespace --- .../CohostGoToImplementationEndpointTest.cs | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs index 170ca71d6a6..4ee5dab2c8d 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs @@ -23,19 +23,19 @@ public class CohostGoToImplementationEndpointTest(ITestOutputHelper testOutputHe public async Task CSharp_Method() { var input = """ -
+
- @{ - var x = Ge$$tX(); - } + @{ + var x = Ge$$tX(); + } - @code + @code + { + void [|GetX|]() { - void [|GetX|]() - { - } } - """; + } + """; await VerifyCSharpGoToImplementationAsync(input); } @@ -44,22 +44,22 @@ public async Task CSharp_Method() public async Task CSharp_Field() { var input = """ -
+
- @{ - var x = GetX(); - } + @{ + var x = GetX(); + } - @code - { - private string [|_name|]; + @code + { + private string [|_name|]; - string GetX() - { - return _na$$me; - } + string GetX() + { + return _na$$me; } - """; + } + """; await VerifyCSharpGoToImplementationAsync(input); } @@ -68,19 +68,19 @@ string GetX() public async Task CSharp_Multiple() { var input = """ -
+
- @code - { - class [|Base|] { } - class [|Derived1|] : Base { } - class [|Derived2|] : Base { } + @code + { + class [|Base|] { } + class [|Derived1|] : Base { } + class [|Derived2|] : Base { } - void M(Ba$$se b) - { - } + void M(Ba$$se b) + { } - """; + } + """; await VerifyCSharpGoToImplementationAsync(input); } @@ -91,14 +91,14 @@ public async Task Html() // This really just validates Uri remapping, the actual response is largely arbitrary TestCode input = """ -
- - - """; +
+ + + """; var document = CreateProjectAndRazorDocument(input.Text); var inputText = await document.GetTextAsync(DisposalToken); From 6e121618bb0645e2f0598278bd35c1b3e30434b9 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 6 Sep 2024 11:51:26 +1000 Subject: [PATCH 3/5] Bump to real Roslyn version --- 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 613a3732666..3c4f6ac8505 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -11,82 +11,82 @@ 9ae78a4e6412926d19ba97cfed159bf9de70b538 - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb diff --git a/eng/Versions.props b/eng/Versions.props index c4680833301..c5cbdff6fd3 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -53,25 +53,25 @@ 9.0.0-beta.24426.3 1.0.0-beta.23475.1 1.0.0-beta.23475.1 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5