This commit is contained in:
David Wengier 2024-11-21 17:39:36 +11:00
Родитель f3aaba93ee
Коммит e347343f19
6 изменённых файлов: 455 добавлений и 0 удалений

Просмотреть файл

@ -36,5 +36,6 @@
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Hover" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteHoverService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Completion" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteCompletionService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.CodeActions" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteCodeActionsService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.FindAllReferences" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteFindAllReferencesService+Factory" />
</ItemGroup>
</Project>

Просмотреть файл

@ -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 Roslyn.LanguageServer.Protocol;
using LspLocation = Roslyn.LanguageServer.Protocol.Location;
namespace Microsoft.CodeAnalysis.Razor.Remote;
internal interface IRemoteFindAllReferencesService : IRemoteJsonService
{
ValueTask<RemoteResponse<SumType<VSInternalReferenceItem, LspLocation>[]?>> FindAllReferencesAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId razorDocumentId,
Position position,
CancellationToken cancellationToken);
}

Просмотреть файл

@ -39,6 +39,7 @@ internal static class RazorServices
(typeof(IRemoteDiagnosticsService), null),
(typeof(IRemoteCompletionService), null),
(typeof(IRemoteCodeActionsService), null),
(typeof(IRemoteFindAllReferencesService), null),
];
private const string ComponentName = "Razor";

Просмотреть файл

@ -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.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 Roslyn.LanguageServer.Protocol;
using static Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Roslyn.LanguageServer.Protocol.SumType<Roslyn.LanguageServer.Protocol.VSInternalReferenceItem, Roslyn.LanguageServer.Protocol.Location>[]?>;
using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
using LspLocation = Roslyn.LanguageServer.Protocol.Location;
using VsLspExtensions = Microsoft.VisualStudio.LanguageServer.Protocol.VsLspExtensions;
namespace Microsoft.CodeAnalysis.Remote.Razor;
internal sealed class RemoteFindAllReferencesService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteFindAllReferencesService
{
internal sealed class Factory : FactoryBase<IRemoteFindAllReferencesService>
{
protected override IRemoteFindAllReferencesService CreateService(in ServiceArgs args)
=> new RemoteFindAllReferencesService(in args);
}
private readonly IClientCapabilitiesService _clientCapabilitiesService = args.ExportProvider.GetExportedValue<IClientCapabilitiesService>();
protected override IDocumentPositionInfoStrategy DocumentPositionInfoStrategy => PreferAttributeNameDocumentPositionInfoStrategy.Instance;
public ValueTask<RemoteResponse<SumType<VSInternalReferenceItem, LspLocation>[]?>> FindAllReferencesAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId documentId,
Position position,
CancellationToken cancellationToken)
=> RunServiceAsync(
solutionInfo,
documentId,
context => GetImplementationAsync(context, position, cancellationToken),
cancellationToken);
private async ValueTask<RemoteResponse<SumType<VSInternalReferenceItem, LspLocation>[]?>> GetImplementationAsync(
RemoteDocumentContext context,
Position 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 not RazorLanguageKind.CSharp)
{
return NoFurtherHandling;
}
// Finally, call into C#.
var generatedDocument = await context.Snapshot
.GetGeneratedDocumentAsync(cancellationToken)
.ConfigureAwait(false);
var results = await ExternalHandlers.FindAllReferences
.FindReferencesAsync(
RemoteWorkspaceAccessor.GetWorkspace(),
generatedDocument,
VsLspExtensions.ToLinePosition(positionInfo.Position),
_clientCapabilitiesService.ClientCapabilities.SupportsVisualStudioExtensions,
cancellationToken)
.ConfigureAwait(false);
if (results is null and not [])
{
// C# didn't return anything, so we're done.
return NoFurtherHandling;
}
// Map the C# locations back to the Razor file.
foreach (var result in results)
{
var location = result.TryGetFirst(out var referenceItem)
? referenceItem.Location
: result.Second;
var (mappedUri, mappedRange) = await DocumentMappingService.MapToHostDocumentUriAndRangeAsync(context.Snapshot, location.Uri, location.Range.ToLinePositionSpan(), cancellationToken).ConfigureAwait(false);
if (referenceItem is not null)
{
// Indicates the reference item is directly available in the code
referenceItem.Origin = VSInternalItemOrigin.Exact;
// If we're going to change the Uri, then also override the file paths
if (mappedUri != location.Uri)
{
referenceItem.DisplayPath = mappedUri.AbsolutePath;
referenceItem.DocumentName = mappedUri.AbsolutePath;
}
}
location.Uri = mappedUri;
location.Range = mappedRange.ToRange();
}
return Results(results);
}
}

Просмотреть файл

@ -0,0 +1,83 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
using Microsoft.CodeAnalysis.Razor.Remote;
using Roslyn.LanguageServer.Protocol;
using LspLocation = Roslyn.LanguageServer.Protocol.Location;
using VSLSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
#pragma warning disable RS0030 // Do not use banned APIs
[Shared]
[CohostEndpoint(Methods.TextDocumentReferencesName)]
[Export(typeof(IDynamicRegistrationProvider))]
[ExportCohostStatelessLspService(typeof(CohostFindAllReferencesEndpoint))]
[method: ImportingConstructor]
#pragma warning restore RS0030 // Do not use banned APIs
internal sealed class CohostFindAllReferencesEndpoint(
IRemoteServiceInvoker remoteServiceInvoker)
: AbstractRazorCohostDocumentRequestHandler<ReferenceParams, SumType<VSInternalReferenceItem, LspLocation>[]?>, IDynamicRegistrationProvider
{
private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
protected override bool MutatesSolutionState => false;
protected override bool RequiresLSPSolution => true;
public ImmutableArray<VSLSP.Registration> GetRegistrations(VSLSP.VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext)
{
if (clientCapabilities.TextDocument?.References?.DynamicRegistration == true)
{
return [new VSLSP.Registration
{
Method = Methods.TextDocumentReferencesName,
RegisterOptions = new VSLSP.ReferenceRegistrationOptions()
}];
}
return [];
}
protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(ReferenceParams request)
=> request.TextDocument.ToRazorTextDocumentIdentifier();
protected override Task<SumType<VSInternalReferenceItem, LspLocation>[]?> HandleRequestAsync(ReferenceParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
=> HandleRequestAsync(
context.TextDocument.AssumeNotNull(),
request.Position,
cancellationToken);
private async Task<SumType<VSInternalReferenceItem, LspLocation>[]?> HandleRequestAsync(TextDocument razorDocument, Position position, CancellationToken cancellationToken)
{
var response = await _remoteServiceInvoker
.TryInvokeAsync<IRemoteFindAllReferencesService, RemoteResponse<SumType<VSInternalReferenceItem, LspLocation>[]?>>(
razorDocument.Project.Solution,
(service, solutionInfo, cancellationToken) =>
service.FindAllReferencesAsync(solutionInfo, razorDocument.Id, position, cancellationToken),
cancellationToken)
.ConfigureAwait(false);
if (response.Result is SumType<VSInternalReferenceItem, LspLocation>[] results)
{
return results;
}
return null;
}
internal TestAccessor GetTestAccessor() => new(this);
internal readonly struct TestAccessor(CohostFindAllReferencesEndpoint instance)
{
public Task<SumType<VSInternalReferenceItem, LspLocation>[]?> HandleRequestAsync(TextDocument razorDocument, Position position, CancellationToken cancellationToken)
=> instance.HandleRequestAsync(razorDocument, position, cancellationToken);
}
}

Просмотреть файл

@ -0,0 +1,240 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Text;
using Roslyn.LanguageServer.Protocol;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
public class CohostFindAllReferencesEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper)
{
[Theory]
[CombinatorialData]
public Task FindCSharpMember(bool supportsVSExtensions)
=> VerifyFindAllReferencesAsync("""
@{
string M()
{
return [|MyName|];
}
}
<p>@[|MyName|]</p>
@code {
private const string [|$$MyName|] = "David";
}
""",
supportsVSExtensions);
[Theory]
[CombinatorialData]
public async Task ComponentAttribute(bool supportsVSExtensions)
{
TestCode input = """
<SurveyPrompt [|Ti$$tle|]="InputValue" />
""";
TestCode surveyPrompt = """
@namespace SomeProject
<div></div>
@code
{
[Parameter]
public string [|Title|] { get; set; }
}
""";
TestCode surveyPromptGeneratedCode = """
// <auto-generated/>
#pragma warning disable 1591
namespace SomeProject
{
#line default
using global::System;
using global::System.Collections.Generic;
using global::System.Linq;
using global::System.Threading.Tasks;
#nullable restore
#line 1 "c:\users\example\src\SomeProject\_Imports.razor"
using Microsoft.AspNetCore.Components;
#nullable disable
#nullable restore
#line 2 "c:\users\example\src\SomeProject\_Imports.razor"
using Microsoft.AspNetCore.Components.Authorization;
#nullable disable
#nullable restore
#line 3 "c:\users\example\src\SomeProject\_Imports.razor"
using Microsoft.AspNetCore.Components.Forms;
#nullable disable
#nullable restore
#line 4 "c:\users\example\src\SomeProject\_Imports.razor"
using Microsoft.AspNetCore.Components.Routing;
#nullable disable
#nullable restore
#line 5 "c:\users\example\src\SomeProject\_Imports.razor"
using Microsoft.AspNetCore.Components.Web;
#line default
#line hidden
#nullable disable
#nullable restore
public partial class SurveyPrompt : global::Microsoft.AspNetCore.Components.ComponentBase
#nullable disable
{
#pragma warning disable 219
private void __RazorDirectiveTokenHelpers__() {
((global::System.Action)(() => {
#nullable restore
#line 1 "c:\users\example\src\SomeProject\SurveyPrompt.razor"
global::System.Object __typeHelper = nameof(SomeProject);
#line default
#line hidden
#nullable disable
}
))();
}
#pragma warning restore 219
#pragma warning disable 0414
private static object __o = null;
#pragma warning restore 0414
#pragma warning disable 1998
protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
{
}
#pragma warning restore 1998
#nullable restore
#line 6 "c:\users\example\src\SomeProject\SurveyPrompt.razor"
[Parameter]
public string Title { get; set; }
#line default
#line hidden
#nullable disable
}
}
#pragma warning restore 1591
""";
await VerifyFindAllReferencesAsync(input, supportsVSExtensions,
(FilePath("SurveyPrompt.razor"), surveyPrompt),
(FilePath("SurveyPrompt.razor.g.cs"), surveyPromptGeneratedCode));
}
[Theory]
[CombinatorialData]
public async Task OtherCSharpFile(bool supportsVSExtensions)
{
TestCode input = """
@code
{
public void M()
{
var x = new OtherClass();
x.[|D$$ate|].ToString();
}
}
""";
TestCode otherClass = """
using System;
namespace SomeProject;
public class OtherClass
{
public DateTime [|Date|] => DateTime.Now;
}
""";
await VerifyFindAllReferencesAsync(input, supportsVSExtensions,
(FilePath("OtherClass.cs"), otherClass));
}
private async Task VerifyFindAllReferencesAsync(TestCode input, bool supportsVSExtensions, params (string fileName, TestCode testCode)[]? additionalFiles)
{
UpdateClientLSPInitializationOptions(c =>
{
c.ClientCapabilities.SupportsVisualStudioExtensions = supportsVSExtensions;
return c;
});
var document = await CreateProjectAndRazorDocumentAsync(input.Text, additionalFiles: additionalFiles.Select(f => (f.fileName, f.testCode.Text)).ToArray());
var inputText = await document.GetTextAsync(DisposalToken);
var position = inputText.GetPosition(input.Position);
var endpoint = new CohostFindAllReferencesEndpoint(RemoteServiceInvoker);
var textDocumentPositionParams = new TextDocumentPositionParams
{
Position = position,
TextDocument = new TextDocumentIdentifier { Uri = document.CreateUri() },
};
var results = await endpoint.GetTestAccessor().HandleRequestAsync(document, position, DisposalToken);
Assumes.NotNull(results);
var totalSpans = input.Spans.Length + additionalFiles.Sum(f => f.testCode.TryGetNamedSpans("", out var spans) ? spans.Length : 0);
Assert.Equal(totalSpans, results.Length);
var razorDocumentUri = document.CreateUri();
foreach (var result in results)
{
if (result.TryGetFirst(out var referenceItem))
{
Assert.True(supportsVSExtensions);
if (referenceItem.DisplayPath is not null)
{
Assert.False(referenceItem.DisplayPath.EndsWith(".g.cs"));
}
if (referenceItem.DocumentName is not null)
{
Assert.False(referenceItem.DocumentName.EndsWith(".g.cs"));
}
}
else
{
Assert.False(supportsVSExtensions);
}
}
foreach (var location in results.Select(GetLocation))
{
if (razorDocumentUri.Equals(location.Uri))
{
Assert.Single(input.Spans.Where(s => inputText.GetRange(s).Equals(location.Range)));
}
else
{
var additionalFile = Assert.Single(additionalFiles.Where(f => FilePathNormalizingComparer.Instance.Equals(f.fileName, location.Uri.AbsolutePath)));
var text = SourceText.From(additionalFile.testCode.Text);
Assert.Single(additionalFile.testCode.Spans.Where(s => text.GetRange(s).Equals(location.Range)));
}
}
}
private static Location GetLocation(SumType<VSInternalReferenceItem, Location> r)
{
return r.TryGetFirst(out var refItem)
? refItem.Location
: r.Second;
}
}