From e90920af7f43ce453175ae1032cd7a7e1ca447a0 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 9 Jul 2024 18:13:22 +1000 Subject: [PATCH 1/8] Allow tests to intercept a static method call --- .../IBrokeredServiceInterceptor.cs | 20 +++++++++++++++++++ .../RazorServiceBase.cs | 6 ++++-- 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/IBrokeredServiceInterceptor.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/IBrokeredServiceInterceptor.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/IBrokeredServiceInterceptor.cs new file mode 100644 index 00000000000..a2a3c23a48b --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/IBrokeredServiceInterceptor.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Api; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +/// +/// An abstraction to avoid calling the static helper defined in Roslyn +/// +internal interface IBrokeredServiceInterceptor +{ + ValueTask RunServiceAsync(Func implementation, CancellationToken cancellationToken); + + ValueTask RunServiceAsync(RazorPinnedSolutionInfoWrapper solutionInfo, Func> implementation, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs index 28f1d6accbb..35855fe0acb 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs @@ -13,17 +13,19 @@ namespace Microsoft.CodeAnalysis.Remote.Razor; internal abstract class RazorServiceBase : IDisposable { private readonly ServiceBrokerClient _serviceBrokerClient; + private readonly IBrokeredServiceInterceptor? _brokeredServiceInterceptor; public RazorServiceBase(IServiceBroker serviceBroker) { + _brokeredServiceInterceptor = serviceBroker as IBrokeredServiceInterceptor; _serviceBrokerClient = new ServiceBrokerClient(serviceBroker, joinableTaskFactory: null); } protected ValueTask RunServiceAsync(Func implementation, CancellationToken cancellationToken) - => RazorBrokeredServiceImplementation.RunServiceAsync(implementation, cancellationToken); + => _brokeredServiceInterceptor?.RunServiceAsync(implementation, cancellationToken) ?? RazorBrokeredServiceImplementation.RunServiceAsync(implementation, cancellationToken); protected ValueTask RunServiceAsync(RazorPinnedSolutionInfoWrapper solutionInfo, Func> implementation, CancellationToken cancellationToken) - => RazorBrokeredServiceImplementation.RunServiceAsync(solutionInfo, _serviceBrokerClient, implementation, cancellationToken); + => _brokeredServiceInterceptor?.RunServiceAsync(solutionInfo, implementation, cancellationToken) ?? RazorBrokeredServiceImplementation.RunServiceAsync(solutionInfo, _serviceBrokerClient, implementation, cancellationToken); public void Dispose() { From e0373016d88a504bd13b029d84ec2862a9d06971 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 9 Jul 2024 18:13:39 +1000 Subject: [PATCH 2/8] Allow tests to call into the endpoint --- .../CohostLinkedEditingRangeEndpoint.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs index a05924cdf51..d78eaa7a225 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; using Microsoft.CodeAnalysis.Razor.LinkedEditingRange; using Microsoft.CodeAnalysis.Razor.Logging; @@ -53,14 +54,15 @@ internal class CohostLinkedEditingRangeEndpoint(IRemoteServiceProvider remoteSer protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(LinkedEditingRangeParams request) => request.TextDocument.ToRazorTextDocumentIdentifier(); - protected override async Task HandleRequestAsync(LinkedEditingRangeParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) - { - var razorDocument = context.TextDocument.AssumeNotNull(); + protected override Task HandleRequestAsync(LinkedEditingRangeParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken); + private async Task HandleRequestAsync(LinkedEditingRangeParams request, TextDocument razorDocument, CancellationToken cancellationToken) + { var linkedRanges = await _remoteServiceProvider.TryInvokeAsync( - razorDocument.Project.Solution, - (service, solutionInfo, cancellationToken) => service.GetRangesAsync(solutionInfo, razorDocument.Id, request.Position.ToLinePosition(), cancellationToken), - cancellationToken).ConfigureAwait(false); + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => service.GetRangesAsync(solutionInfo, razorDocument.Id, request.Position.ToLinePosition(), cancellationToken), + cancellationToken).ConfigureAwait(false); if (linkedRanges is [{ } span1, { } span2]) { @@ -73,4 +75,12 @@ internal class CohostLinkedEditingRangeEndpoint(IRemoteServiceProvider remoteSer return null; } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(CohostLinkedEditingRangeEndpoint instance) + { + public Task HandleRequestAsync(LinkedEditingRangeParams request, TextDocument razorDocument, CancellationToken cancellationToken) + => instance.HandleRequestAsync(request, razorDocument, cancellationToken); + } } From 93b8b84bda5228b214c6cc999c44991a1595bf61 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 9 Jul 2024 18:13:56 +1000 Subject: [PATCH 3/8] Add tests and test infra --- ...Microsoft.CodeAnalysis.Remote.Razor.csproj | 1 + .../Cohost/CohostLinkedEditingRangeTest.cs | 81 +++++++++++++++++++ .../Cohost/CohostTestBase.cs | 57 +++++++++++++ .../Cohost/InterceptingServiceBroker.cs | 38 +++++++++ .../ShortCircuitingRemoteServiceProvider.cs | 73 +++++++++++++++++ .../Cohost/TestTraceSourceProvider.cs | 52 ++++++++++++ ...lStudio.LanguageServices.Razor.Test.csproj | 1 + 7 files changed, 303 insertions(+) create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeTest.cs create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/InterceptingServiceBroker.cs create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/ShortCircuitingRemoteServiceProvider.cs create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestTraceSourceProvider.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj index e56c17ac547..3357fd1eb79 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj @@ -54,6 +54,7 @@ + diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeTest.cs new file mode 100644 index 00000000000..0420d4d9b67 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeTest.cs @@ -0,0 +1,81 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.LinkedEditingRange; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Razor.LanguageClient.Cohost; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost; + +public class CohostLinkedEditingRangeTest(ITestOutputHelper testOutputHelper) : CohostTestBase(testOutputHelper) +{ + [Fact] + public async Task StartTag() + { + var input = """ + This is a Razor document. + + <[|$$div|]> + Here is some content. + + + The end. + """; + + await VerifyLinkedEditingRangeAsync(input); + } + + [Fact] + public async Task EndTag() + { + var input = """ + This is a Razor document. + + <[|div|]> + Here is some content. + + + The end. + """; + + await VerifyLinkedEditingRangeAsync(input); + } + + private async Task VerifyLinkedEditingRangeAsync(string input) + { + TestFileMarkupParser.GetPositionAndSpans(input, out input, out int cursorPosition, out ImmutableArray spans); + var document = CreateRazorDocument(input); + var sourceText = await document.GetTextAsync(DisposalToken); + sourceText.GetLineAndOffset(cursorPosition, out var lineIndex, out var characterIndex); + + var endpoint = new CohostLinkedEditingRangeEndpoint(RemoteServiceProvider, LoggerFactory); + + var request = new LinkedEditingRangeParams() + { + TextDocument = new TextDocumentIdentifier() + { + Uri = document.CreateUri() + }, + Position = new Position() + { + Line = lineIndex, + Character = characterIndex + } + }; + + var result = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, DisposalToken); + + Assert.NotNull(result); + Assert.Equal(LinkedEditingRangeHelper.WordPattern, result.WordPattern); + Assert.Equal(spans[0], result.Ranges[0].ToTextSpan(sourceText)); + Assert.Equal(spans[1], result.Ranges[1].ToTextSpan(sourceText)); + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs new file mode 100644 index 00000000000..d4a7baff69a --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Text; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost; + +public class CohostTestBase(ITestOutputHelper testOutputHelper) : WorkspaceTestBase(testOutputHelper) +{ + private readonly ITestOutputHelper _testOutputHelper = testOutputHelper; + + private IRemoteServiceProvider? _remoteServiceProvider; + + internal IRemoteServiceProvider RemoteServiceProvider => _remoteServiceProvider.AssumeNotNull(); + + protected override Task InitializeAsync() + { + _remoteServiceProvider = new ShortCircuitingRemoteServiceProvider(_testOutputHelper); + + return base.InitializeAsync(); + } + + protected TextDocument CreateRazorDocument(string input) + { + var hostProject = TestProjectData.SomeProject; + var hostDocument = TestProjectData.SomeProjectComponentFile1; + + var sourceText = SourceText.From(input); + + var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( + ProjectId.CreateNewId(Path.GetFileNameWithoutExtension(hostProject.FilePath)), + VersionStamp.Create(), + Path.GetFileNameWithoutExtension(hostDocument.FilePath), + Path.GetFileNameWithoutExtension(hostDocument.FilePath), + LanguageNames.CSharp, + hostDocument.FilePath)); + + solution = solution.AddAdditionalDocument( + DocumentId.CreateNewId(solution.ProjectIds.Single(), hostDocument.FilePath), + hostDocument.FilePath, + sourceText, + filePath: hostDocument.FilePath); + + var document = solution.Projects.Single().AdditionalDocuments.Single(); + + return document; + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/InterceptingServiceBroker.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/InterceptingServiceBroker.cs new file mode 100644 index 00000000000..0cee8db4187 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/InterceptingServiceBroker.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Remote.Razor; +using Microsoft.ServiceHub.Framework; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost; + +internal class InterceptingServiceBroker(Solution solution) : IServiceBroker, IBrokeredServiceInterceptor +{ + public event EventHandler? AvailabilityChanged { add { } remove { } } + + public ValueTask GetPipeAsync(ServiceMoniker serviceMoniker, ServiceActivationOptions options = default, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask GetProxyAsync(ServiceRpcDescriptor serviceDescriptor, ServiceActivationOptions options = default, CancellationToken cancellationToken = default) where T : class + { + throw new NotImplementedException(); + } + + public ValueTask RunServiceAsync(Func implementation, CancellationToken cancellationToken) + { + return implementation(cancellationToken); + } + + public ValueTask RunServiceAsync(RazorPinnedSolutionInfoWrapper solutionInfo, Func> implementation, CancellationToken cancellationToken) + { + return implementation(solution); + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/ShortCircuitingRemoteServiceProvider.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/ShortCircuitingRemoteServiceProvider.cs new file mode 100644 index 00000000000..5c5f8dde01f --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/ShortCircuitingRemoteServiceProvider.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Remote.Razor; +using Microsoft.ServiceHub.Framework; +using Nerdbank.Streams; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost; + +/// +/// An implementation of IRemoteServiceProvider that doesn't actually do anything remote, but rather directly calls service methods +/// +internal class ShortCircuitingRemoteServiceProvider(ITestOutputHelper testOutputHelper) : IRemoteServiceProvider +{ + private static Dictionary _factoryMap = BuildFactoryMap(); + + private readonly IServiceProvider _serviceProvider = new TestTraceSourceProvider(testOutputHelper); + + private static Dictionary BuildFactoryMap() + { + var result = new Dictionary(); + + foreach (var type in typeof(RazorServiceFactoryBase<>).Assembly.GetTypes()) + { + if (!type.IsAbstract && + typeof(IServiceHubServiceFactory).IsAssignableFrom(type)) + { + Debug.Assert(type.BaseType.GetGenericTypeDefinition() == typeof(RazorServiceFactoryBase<>)); + + var genericType = type.BaseType.GetGenericArguments().FirstOrDefault(); + if (genericType != null) + { + // ServiceHub requires a parameterless constructor, so we can safely rely on it existing too + var factory = (IServiceHubServiceFactory)Activator.CreateInstance(type); + result.Add(genericType, factory); + } + } + } + + return result; + } + + public async ValueTask TryInvokeAsync(Solution solution, Func> invocation, CancellationToken cancellationToken, [CallerFilePath] string? callerFilePath = null, [CallerMemberName] string? callerMemberName = null) where TService : class + { + Assert.True(_factoryMap.TryGetValue(typeof(TService), out var factory)); + + var testServiceBroker = new InterceptingServiceBroker(solution); + + // We don't ever use this stream, because we never really use ServiceHub, but going through its factory method means the + // remote services under test are using their full MEF composition etc. so we get excellent coverage. + var (stream, _) = FullDuplexStream.CreatePair(); + var service = (TService)await factory.CreateAsync(stream, _serviceProvider, serviceActivationOptions: default, testServiceBroker, authorizationServiceClient: default!); + + // This is never used, we short-circuited things by passing the solution direct to the InterceptingServiceBroker + var solutionInfo = new RazorPinnedSolutionInfoWrapper(); + + testOutputHelper.WriteLine($"Pretend OOP call for {typeof(TService).Name}, invocation: {Path.GetFileNameWithoutExtension(callerFilePath)}.{callerMemberName}"); + return await invocation(service, solutionInfo, cancellationToken); + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestTraceSourceProvider.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestTraceSourceProvider.cs new file mode 100644 index 00000000000..b61b1a61d3c --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestTraceSourceProvider.cs @@ -0,0 +1,52 @@ +// 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 Xunit.Abstractions; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost; + +/// +/// An implementation of IServiceProvider that only provides a TraceSource, that writes to test output +/// +internal class TestTraceSourceProvider(ITestOutputHelper testOutputHelper) : IServiceProvider +{ + public object GetService(Type serviceType) + { + if (serviceType == typeof(TraceSource)) + { + return new TestOutputTraceSource(testOutputHelper); + } + + throw new NotImplementedException(); + } + + private class TestOutputTraceSource : TraceSource + { + public TestOutputTraceSource(ITestOutputHelper testOutputHelper) + : base("OOP", SourceLevels.All) + { + Listeners.Add(new TestOutputTraceListener(testOutputHelper)); + } + + private class TestOutputTraceListener(ITestOutputHelper testOutputHelper) : TraceListener + { + public override void Write(string message) + { + // ITestOutputHelper doesn't have a Write method, but all we lose is some extra ServiceHub details like log level + } + + public override void WriteLine(string message) + { + // Ignore some specific ServiceHub noise, since we're not using ServiceHub anyway + if (message.StartsWith("Added local RPC method") || message == "Listening started.") + { + return; + } + + testOutputHelper.WriteLine(message); + } + } + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj index ace810bf8ac..264ae60a47e 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj @@ -24,6 +24,7 @@ + From b3711a86d7a26803f4e8baa725377b33ec63dc67 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 9 Jul 2024 18:14:01 +1000 Subject: [PATCH 4/8] Minor fix --- .../Remote/RemoteServiceProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceProvider.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceProvider.cs index 7f3a4c438f7..77361646138 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceProvider.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceProvider.cs @@ -71,7 +71,7 @@ internal sealed class RemoteServiceProvider( catch (Exception ex) when (ex is not OperationCanceledException) { var approximateCallingClassName = Path.GetFileNameWithoutExtension(callerFilePath); - _logger.LogError(ex, $"Error calling remote method for {typeof(TService).Name} service, invocation: ${approximateCallingClassName}.{callerMemberName}"); + _logger.LogError(ex, $"Error calling remote method for {typeof(TService).Name} service, invocation: {approximateCallingClassName}.{callerMemberName}"); _telemetryReporter.ReportFault(ex, "Exception calling remote method for {service}, invocation: {class}.{method}", typeof(TService).FullName, approximateCallingClassName, callerMemberName); return default; } From c467559911b97942bb9054fc05bd0c03d9f0b450 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 9 Jul 2024 18:21:40 +1000 Subject: [PATCH 5/8] Whitespace --- .../Cohost/CohostLinkedEditingRangeEndpoint.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs index d78eaa7a225..a288d8c4883 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs @@ -60,9 +60,9 @@ internal class CohostLinkedEditingRangeEndpoint(IRemoteServiceProvider remoteSer private async Task HandleRequestAsync(LinkedEditingRangeParams request, TextDocument razorDocument, CancellationToken cancellationToken) { var linkedRanges = await _remoteServiceProvider.TryInvokeAsync( - razorDocument.Project.Solution, - (service, solutionInfo, cancellationToken) => service.GetRangesAsync(solutionInfo, razorDocument.Id, request.Position.ToLinePosition(), cancellationToken), - cancellationToken).ConfigureAwait(false); + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => service.GetRangesAsync(solutionInfo, razorDocument.Id, request.Position.ToLinePosition(), cancellationToken), + cancellationToken).ConfigureAwait(false); if (linkedRanges is [{ } span1, { } span2]) { From 82d6cf65090d3d3d2af42718bb3edc119b73c858 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 10 Jul 2024 11:05:34 +1000 Subject: [PATCH 6/8] PR Feedback --- .../ToolingTestBase.cs | 7 +++++++ .../Cohost/CohostTestBase.cs | 9 +++------ .../Cohost/InterceptingServiceBroker.cs | 3 ++- .../ShortCircuitingRemoteServiceProvider.cs | 15 ++++++++++----- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ToolingTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ToolingTestBase.cs index 1cac6d4b4ff..8dad3a87185 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ToolingTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ToolingTestBase.cs @@ -70,6 +70,12 @@ static ToolingTestBase() /// internal ILoggerFactory LoggerFactory { get; } + /// + /// An that the currently running test can use to write + /// though using is probably preferred. + /// + internal ITestOutputHelper TestOutputHelper { get; } + private ILogger? _logger; /// @@ -86,6 +92,7 @@ protected ToolingTestBase(ITestOutputHelper testOutput) _disposalTokenSource = new(); DisposalToken = _disposalTokenSource.Token; + TestOutputHelper = testOutput; LoggerFactory = new TestOutputLoggerFactory(testOutput); // Give this thread a name, so it's easier to find in the VS Threads window. diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs index d4a7baff69a..1ee26c68cf4 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.IO; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Test.Common; @@ -14,17 +13,15 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost; -public class CohostTestBase(ITestOutputHelper testOutputHelper) : WorkspaceTestBase(testOutputHelper) +public abstract class CohostTestBase(ITestOutputHelper testOutputHelper) : WorkspaceTestBase(testOutputHelper) { - private readonly ITestOutputHelper _testOutputHelper = testOutputHelper; - private IRemoteServiceProvider? _remoteServiceProvider; - internal IRemoteServiceProvider RemoteServiceProvider => _remoteServiceProvider.AssumeNotNull(); + private protected IRemoteServiceProvider RemoteServiceProvider => _remoteServiceProvider.AssumeNotNull(); protected override Task InitializeAsync() { - _remoteServiceProvider = new ShortCircuitingRemoteServiceProvider(_testOutputHelper); + _remoteServiceProvider = new ShortCircuitingRemoteServiceProvider(TestOutputHelper); return base.InitializeAsync(); } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/InterceptingServiceBroker.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/InterceptingServiceBroker.cs index 0cee8db4187..364f0effc00 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/InterceptingServiceBroker.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/InterceptingServiceBroker.cs @@ -21,7 +21,8 @@ public event EventHandler? AvailabilityChanged throw new NotImplementedException(); } - public ValueTask GetProxyAsync(ServiceRpcDescriptor serviceDescriptor, ServiceActivationOptions options = default, CancellationToken cancellationToken = default) where T : class + public ValueTask GetProxyAsync(ServiceRpcDescriptor serviceDescriptor, ServiceActivationOptions options = default, CancellationToken cancellationToken = default) + where T : class { throw new NotImplementedException(); } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/ShortCircuitingRemoteServiceProvider.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/ShortCircuitingRemoteServiceProvider.cs index 5c5f8dde01f..e49bb65bbed 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/ShortCircuitingRemoteServiceProvider.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/ShortCircuitingRemoteServiceProvider.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -25,7 +24,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost; /// internal class ShortCircuitingRemoteServiceProvider(ITestOutputHelper testOutputHelper) : IRemoteServiceProvider { - private static Dictionary _factoryMap = BuildFactoryMap(); + private static readonly Dictionary s_factoryMap = BuildFactoryMap(); private readonly IServiceProvider _serviceProvider = new TestTraceSourceProvider(testOutputHelper); @@ -38,7 +37,7 @@ private static Dictionary BuildFactoryMap() if (!type.IsAbstract && typeof(IServiceHubServiceFactory).IsAssignableFrom(type)) { - Debug.Assert(type.BaseType.GetGenericTypeDefinition() == typeof(RazorServiceFactoryBase<>)); + Assert.Equal(typeof(RazorServiceFactoryBase<>), type.BaseType.GetGenericTypeDefinition()); var genericType = type.BaseType.GetGenericArguments().FirstOrDefault(); if (genericType != null) @@ -53,9 +52,15 @@ private static Dictionary BuildFactoryMap() return result; } - public async ValueTask TryInvokeAsync(Solution solution, Func> invocation, CancellationToken cancellationToken, [CallerFilePath] string? callerFilePath = null, [CallerMemberName] string? callerMemberName = null) where TService : class + public async ValueTask TryInvokeAsync( + Solution solution, + Func> invocation, + CancellationToken cancellationToken, + [CallerFilePath] string? callerFilePath = null, + [CallerMemberName] string? callerMemberName = null) + where TService : class { - Assert.True(_factoryMap.TryGetValue(typeof(TService), out var factory)); + Assert.True(s_factoryMap.TryGetValue(typeof(TService), out var factory)); var testServiceBroker = new InterceptingServiceBroker(solution); From b92a394c94789aa02b78e4f6beb588a00d0b3a46 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 10 Jul 2024 11:05:42 +1000 Subject: [PATCH 7/8] Improve create document method --- .../Cohost/CohostTestBase.cs | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs index 1ee26c68cf4..cc0800edf62 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTestBase.cs @@ -26,29 +26,28 @@ protected override Task InitializeAsync() return base.InitializeAsync(); } - protected TextDocument CreateRazorDocument(string input) + protected TextDocument CreateRazorDocument(string contents) { - var hostProject = TestProjectData.SomeProject; - var hostDocument = TestProjectData.SomeProjectComponentFile1; - - var sourceText = SourceText.From(input); + var projectFilePath = TestProjectData.SomeProject.FilePath; + var documentFilePath = TestProjectData.SomeProjectComponentFile1.FilePath; + var projectName = Path.GetFileNameWithoutExtension(projectFilePath); + var projectId = ProjectId.CreateNewId(debugName: projectName); + var documentId = DocumentId.CreateNewId(projectId, debugName: documentFilePath); var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( - ProjectId.CreateNewId(Path.GetFileNameWithoutExtension(hostProject.FilePath)), + projectId, VersionStamp.Create(), - Path.GetFileNameWithoutExtension(hostDocument.FilePath), - Path.GetFileNameWithoutExtension(hostDocument.FilePath), + name: projectName, + assemblyName: projectName, LanguageNames.CSharp, - hostDocument.FilePath)); + documentFilePath)); solution = solution.AddAdditionalDocument( - DocumentId.CreateNewId(solution.ProjectIds.Single(), hostDocument.FilePath), - hostDocument.FilePath, - sourceText, - filePath: hostDocument.FilePath); - - var document = solution.Projects.Single().AdditionalDocuments.Single(); + documentId, + documentFilePath, + SourceText.From(contents), + filePath: documentFilePath); - return document; + return solution.GetAdditionalDocument(documentId).AssumeNotNull(); } } From a1d07e0d89eefc33b7fc957a5215bce9becc08a1 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 10 Jul 2024 11:17:15 +1000 Subject: [PATCH 8/8] Expand tests to match the existing ones --- .../Cohost/CohostLinkedEditingRangeTest.cs | 111 ++++++++++++++++-- 1 file changed, 103 insertions(+), 8 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeTest.cs index 0420d4d9b67..b84ad7a36ee 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeTest.cs @@ -17,13 +17,16 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost; public class CohostLinkedEditingRangeTest(ITestOutputHelper testOutputHelper) : CohostTestBase(testOutputHelper) { - [Fact] - public async Task StartTag() + [Theory] + [InlineData("$$div")] + [InlineData("di$$v")] + [InlineData("div$$")] + public async Task Html_StartTag(string startTagAndCursorLocation) { - var input = """ + var input = $""" This is a Razor document. - <[|$$div|]> + <[|{startTagAndCursorLocation}|]> Here is some content. @@ -33,15 +36,101 @@ The end. await VerifyLinkedEditingRangeAsync(input); } - [Fact] - public async Task EndTag() + [Theory] + [InlineData("$$div")] + [InlineData("di$$v")] + [InlineData("div$$")] + public async Task Html_EndTag(string endTagAndCursorLocation) { - var input = """ + var input = $""" This is a Razor document. <[|div|]> Here is some content. - + + + The end. + """; + + await VerifyLinkedEditingRangeAsync(input); + } + + [Fact] + public async Task Html_EndTag_BeforeSlash() + { + var input = $""" + This is a Razor document. + +
+ Here is some content. + <$$/div> + + The end. + """; + + await VerifyLinkedEditingRangeAsync(input); + } + + [Fact] + public async Task Html_NotATag() + { + var input = $""" + This is a $$Razor document. + +
+ Here is some content. +
+ + The end. + """; + + await VerifyLinkedEditingRangeAsync(input); + } + + [Fact] + public async Task Html_NestedTags_Outer() + { + var input = $""" + This is a Razor document. + + <[|d$$iv|]> +
+ Here is some content. +
+ + + The end. + """; + + await VerifyLinkedEditingRangeAsync(input); + } + + [Fact] + public async Task Html_NestedTags_Inner() + { + var input = $""" + This is a Razor document. + +
+ <[|d$$iv|]> + Here is some content. + +
+ + The end. + """; + + await VerifyLinkedEditingRangeAsync(input); + } + + [Fact] + public async Task Html_SelfClosingTag() + { + var input = $""" + This is a Razor document. + + + Here is some content. The end. """; @@ -73,6 +162,12 @@ private async Task VerifyLinkedEditingRangeAsync(string input) var result = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, DisposalToken); + if (spans.Length == 0) + { + Assert.Null(result); + return; + } + Assert.NotNull(result); Assert.Equal(LinkedEditingRangeHelper.WordPattern, result.WordPattern); Assert.Equal(spans[0], result.Ranges[0].ToTextSpan(sourceText));