Fixes https://github.com/dotnet/razor/issues/10696
Fixes https://github.com/dotnet/razor/issues/10769
Fixes https://github.com/dotnet/razor/issues/10874
Needs Roslyn change: https://github.com/dotnet/roslyn/pull/75102

The actual code change was quite straight foward. The test code was a
bigger pain. Previously in cohosting tests we had two MEF composition,
one for Razor and one for Roslyn. Since Roslyn diagnostics can only run,
or at least start, in devenv, we now need three MEF compositions -
Roslyn devenv, Roslyn OOP and razor OOP - and two workspaces - Roslyn
devenv and Roslyn OOP - and our previous MEF infrastructure really
didn't handle this well, as evidenced by the fact that this PR also
fixes https://github.com/dotnet/razor/issues/10874

Now our MEF infra is simpler: We still cache catalogs etc. statically
the save time, but we create an `ExportProvider` as needed and expect
consumers to dispose of it like any other `IDisposable`, though there is
also here a test fixture to share the editor composition in the
formatting tests so we don't pay a huge penalty in terms of test run
time.

Commit-at-a-time if you want to read the story from beginning to end,
but it should be reasonably okay to just review as a whole too.
This commit is contained in:
David Wengier 2024-09-18 14:11:37 +10:00 коммит произвёл GitHub
Родитель 966d7628b9 fe5dc7beae
Коммит 4b3e4096e8
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
76 изменённых файлов: 882 добавлений и 712 удалений

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

@ -11,82 +11,82 @@
<Sha>08649fed58d668737a54913f7d4c649a8da5dc6e</Sha>
<SourceBuild RepoName="source-build-reference-packages" ManagedOnly="true" />
</Dependency>
<Dependency Name="Microsoft.Net.Compilers.Toolset" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.Net.Compilers.Toolset" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CommonLanguageServerProtocol.Framework" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CommonLanguageServerProtocol.Framework" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.ExternalAccess.Razor" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.ExternalAccess.Razor" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.Common" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.Common" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.CSharp" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.CSharp" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.CSharp.EditorFeatures" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.CSharp.EditorFeatures" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.CSharp.Features" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.CSharp.Features" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.EditorFeatures" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.EditorFeatures" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.EditorFeatures.Common" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.EditorFeatures.Common" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.EditorFeatures.Text" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.EditorFeatures.Text" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.EditorFeatures.Wpf" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.EditorFeatures.Wpf" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.Remote.ServiceHub" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.Remote.ServiceHub" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.VisualStudio.LanguageServices" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.VisualStudio.LanguageServices" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.Test.Utilities" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.CodeAnalysis.Test.Utilities" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
</Dependency>
<!-- Intermediate is necessary for source build. -->
<Dependency Name="Microsoft.SourceBuild.Intermediate.roslyn" Version="4.12.0-3.24454.5">
<Dependency Name="Microsoft.SourceBuild.Intermediate.roslyn" Version="4.12.0-3.24466.4">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>9f86520c46f67d2a8a59af189f8fd87e35c574bb</Sha>
<Sha>7b7951aa13c50ad768538e58ed3805898b058928</Sha>
<SourceBuild RepoName="roslyn" ManagedOnly="true" />
</Dependency>
</ProductDependencies>

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

@ -53,25 +53,25 @@
<MicrosoftSourceBuildIntermediatearcadePackageVersion>9.0.0-beta.24453.1</MicrosoftSourceBuildIntermediatearcadePackageVersion>
<MicrosoftDotNetXliffTasksPackageVersion>1.0.0-beta.23475.1</MicrosoftDotNetXliffTasksPackageVersion>
<MicrosoftSourceBuildIntermediatexlifftasksPackageVersion>1.0.0-beta.23475.1</MicrosoftSourceBuildIntermediatexlifftasksPackageVersion>
<MicrosoftNetCompilersToolsetPackageVersion>4.12.0-3.24454.5</MicrosoftNetCompilersToolsetPackageVersion>
<MicrosoftCommonLanguageServerProtocolFrameworkPackageVersion>4.12.0-3.24454.5</MicrosoftCommonLanguageServerProtocolFrameworkPackageVersion>
<MicrosoftCodeAnalysisExternalAccessRazorPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisExternalAccessRazorPackageVersion>
<MicrosoftCodeAnalysisCommonPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisCommonPackageVersion>
<MicrosoftCodeAnalysisCSharpPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisCSharpPackageVersion>
<MicrosoftCodeAnalysisCSharpEditorFeaturesPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisCSharpEditorFeaturesPackageVersion>
<MicrosoftCodeAnalysisCSharpFeaturesPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisCSharpFeaturesPackageVersion>
<MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>
<MicrosoftCodeAnalysisEditorFeaturesPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisEditorFeaturesPackageVersion>
<MicrosoftCodeAnalysisEditorFeaturesCommonPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisEditorFeaturesCommonPackageVersion>
<MicrosoftCodeAnalysisEditorFeaturesTextPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisEditorFeaturesTextPackageVersion>
<MicrosoftCodeAnalysisEditorFeaturesWpfPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisEditorFeaturesWpfPackageVersion>
<MicrosoftCodeAnalysisRemoteServiceHubPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisRemoteServiceHubPackageVersion>
<MicrosoftCodeAnalysisTestUtilitiesPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisTestUtilitiesPackageVersion>
<MicrosoftCodeAnalysisVisualBasicWorkspacesPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisVisualBasicWorkspacesPackageVersion>
<MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>
<MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion>4.12.0-3.24454.5</MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion>
<MicrosoftSourceBuildIntermediateroslynPackageVersion>4.12.0-3.24454.5</MicrosoftSourceBuildIntermediateroslynPackageVersion>
<MicrosoftVisualStudioLanguageServicesPackageVersion>4.12.0-3.24454.5</MicrosoftVisualStudioLanguageServicesPackageVersion>
<MicrosoftNetCompilersToolsetPackageVersion>4.12.0-3.24466.4</MicrosoftNetCompilersToolsetPackageVersion>
<MicrosoftCommonLanguageServerProtocolFrameworkPackageVersion>4.12.0-3.24466.4</MicrosoftCommonLanguageServerProtocolFrameworkPackageVersion>
<MicrosoftCodeAnalysisExternalAccessRazorPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisExternalAccessRazorPackageVersion>
<MicrosoftCodeAnalysisCommonPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisCommonPackageVersion>
<MicrosoftCodeAnalysisCSharpPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisCSharpPackageVersion>
<MicrosoftCodeAnalysisCSharpEditorFeaturesPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisCSharpEditorFeaturesPackageVersion>
<MicrosoftCodeAnalysisCSharpFeaturesPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisCSharpFeaturesPackageVersion>
<MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>
<MicrosoftCodeAnalysisEditorFeaturesPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisEditorFeaturesPackageVersion>
<MicrosoftCodeAnalysisEditorFeaturesCommonPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisEditorFeaturesCommonPackageVersion>
<MicrosoftCodeAnalysisEditorFeaturesTextPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisEditorFeaturesTextPackageVersion>
<MicrosoftCodeAnalysisEditorFeaturesWpfPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisEditorFeaturesWpfPackageVersion>
<MicrosoftCodeAnalysisRemoteServiceHubPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisRemoteServiceHubPackageVersion>
<MicrosoftCodeAnalysisTestUtilitiesPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisTestUtilitiesPackageVersion>
<MicrosoftCodeAnalysisVisualBasicWorkspacesPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisVisualBasicWorkspacesPackageVersion>
<MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>
<MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion>4.12.0-3.24466.4</MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion>
<MicrosoftSourceBuildIntermediateroslynPackageVersion>4.12.0-3.24466.4</MicrosoftSourceBuildIntermediateroslynPackageVersion>
<MicrosoftVisualStudioLanguageServicesPackageVersion>4.12.0-3.24466.4</MicrosoftVisualStudioLanguageServicesPackageVersion>
<!--
Exception - Microsoft.Extensions.ObjectPool and System.Collections.Immutable packages are not updated by automation,
but are present in Version.Details.xml for source-build PVP flow. See the comment in Version.Details.xml for more information.

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

@ -32,5 +32,6 @@
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Formatting" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteFormattingService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.GoToImplementation" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteGoToImplementationService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.SpellCheck" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteSpellCheckService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Diagnostics" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteDiagnosticsService+Factory" />
</ItemGroup>
</Project>

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

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics;
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.CodeAnalysis.Razor.Diagnostics;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;

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

@ -43,6 +43,9 @@
<Compile Include="..\..\test\Microsoft.AspNetCore.Razor.Test.Common.Tooling\Workspaces\TestWorkspaceServices.cs">
<Link>TestServices\%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="..\..\test\Microsoft.AspNetCore.Razor.Test.Common.Tooling\Mef\ExportProviderExtensions.cs">
<Link>TestServices\%(FileName)%(Extension)</Link>
</Compile>
<None Remove=".gitignore" />
<None Remove="BenchmarkDotNet.Artifacts\**" />

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

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Telemetry;
using Microsoft.CodeAnalysis.Razor.Diagnostics;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Protocol.Diagnostics;
@ -72,7 +73,9 @@ internal class DocumentPullDiagnosticsEndpoint : IRazorRequestHandler<VSInternal
return null;
}
var razorDiagnostics = await GetRazorDiagnosticsAsync(documentContext, cancellationToken).ConfigureAwait(false);
var documentSnapshot = documentContext.Snapshot;
var razorDiagnostics = await GetRazorDiagnosticsAsync(documentSnapshot).ConfigureAwait(false);
var (csharpDiagnostics, htmlDiagnostics) = await GetHtmlCSharpDiagnosticsAsync(documentContext, correlationId, cancellationToken).ConfigureAwait(false);
@ -96,7 +99,7 @@ internal class DocumentPullDiagnosticsEndpoint : IRazorRequestHandler<VSInternal
{
if (report.Diagnostics is not null)
{
var mappedDiagnostics = await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.CSharp, report.Diagnostics, documentContext, cancellationToken).ConfigureAwait(false);
var mappedDiagnostics = await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.CSharp, report.Diagnostics, documentSnapshot).ConfigureAwait(false);
report.Diagnostics = mappedDiagnostics;
}
@ -110,7 +113,7 @@ internal class DocumentPullDiagnosticsEndpoint : IRazorRequestHandler<VSInternal
{
if (report.Diagnostics is not null)
{
var mappedDiagnostics = await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.Html, report.Diagnostics, documentContext, cancellationToken).ConfigureAwait(false);
var mappedDiagnostics = await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.Html, report.Diagnostics, documentSnapshot).ConfigureAwait(false);
report.Diagnostics = mappedDiagnostics;
}
@ -121,10 +124,10 @@ internal class DocumentPullDiagnosticsEndpoint : IRazorRequestHandler<VSInternal
return allDiagnostics.ToArray();
}
private static async Task<VSInternalDiagnosticReport[]?> GetRazorDiagnosticsAsync(DocumentContext documentContext, CancellationToken cancellationToken)
private static async Task<VSInternalDiagnosticReport[]?> GetRazorDiagnosticsAsync(IDocumentSnapshot documentSnapshot)
{
var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
var sourceText = codeDocument.Source.Text;
var csharpDocument = codeDocument.GetCSharpDocument();
var diagnostics = csharpDocument.Diagnostics;
@ -133,7 +136,7 @@ internal class DocumentPullDiagnosticsEndpoint : IRazorRequestHandler<VSInternal
return null;
}
var convertedDiagnostics = RazorDiagnosticConverter.Convert(diagnostics, sourceText, documentContext.Snapshot);
var convertedDiagnostics = RazorDiagnosticConverter.Convert(diagnostics, sourceText, documentSnapshot);
return
[

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

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Diagnostics;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
@ -187,7 +188,7 @@ internal partial class RazorDiagnosticsPublisher : IDocumentProcessedListener, I
if (_documentContextFactory.Value.TryCreate(delegatedParams.TextDocument.Uri, projectContext: null, out var documentContext))
{
return await _translateDiagnosticsService.Value
.TranslateAsync(RazorLanguageKind.CSharp, fullDiagnostics.Items, documentContext, token)
.TranslateAsync(RazorLanguageKind.CSharp, fullDiagnostics.Items, documentContext.Snapshot)
.ConfigureAwait(false);
}
}

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

@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.SpellCheck;
using Microsoft.AspNetCore.Razor.LanguageServer.Tooltip;
using Microsoft.AspNetCore.Razor.ProjectEngineHost;
using Microsoft.CodeAnalysis.Razor.Completion;
using Microsoft.CodeAnalysis.Razor.Diagnostics;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;

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

@ -128,7 +128,6 @@ internal partial class RazorLanguageServer : SystemTextJsonLanguageServer<RazorR
services.AddLifeCycleServices(this, _clientConnection, _lspServerActivationTracker);
services.AddDiagnosticServices();
services.AddSemanticTokensServices(featureOptions);
services.AddDocumentManagementServices(featureOptions);
services.AddCompletionServices();
@ -140,6 +139,9 @@ internal partial class RazorLanguageServer : SystemTextJsonLanguageServer<RazorR
if (!featureOptions.UseRazorCohostServer)
{
// Diagnostics
services.AddDiagnosticServices();
// Auto insert
services.AddSingleton<IOnAutoInsertProvider, CloseTextTagOnAutoInsertProvider>();
services.AddSingleton<IOnAutoInsertProvider, AutoClosingTagOnAutoInsertProvider>();

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

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Razor.LanguageServer;
namespace Microsoft.CodeAnalysis.Razor.Diagnostics;
// Note: This type should be kept in sync with WTE's ErrorCodes.cs
internal static class CSSErrorCodes

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

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Razor.LanguageServer;
namespace Microsoft.CodeAnalysis.Razor.Diagnostics;
// Note: This type should be kept in sync with WTE's ErrorCodes.cs
internal static class HtmlErrorCodes

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

@ -2,15 +2,17 @@
// 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.Globalization;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic;
using LspDiagnosticSeverity = Microsoft.VisualStudio.LanguageServer.Protocol.DiagnosticSeverity;
using LspRange = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics;
namespace Microsoft.CodeAnalysis.Razor.Diagnostics;
internal static class RazorDiagnosticConverter
{
@ -46,9 +48,9 @@ internal static class RazorDiagnosticConverter
}];
}
internal static Diagnostic[] Convert(IReadOnlyList<RazorDiagnostic> diagnostics, SourceText sourceText, IDocumentSnapshot documentSnapshot)
internal static LspDiagnostic[] Convert(ImmutableArray<RazorDiagnostic> diagnostics, SourceText sourceText, IDocumentSnapshot documentSnapshot)
{
var convertedDiagnostics = new Diagnostic[diagnostics.Count];
var convertedDiagnostics = new LspDiagnostic[diagnostics.Length];
var i = 0;
foreach (var diagnostic in diagnostics)
@ -60,18 +62,18 @@ internal static class RazorDiagnosticConverter
}
// Internal for testing
internal static DiagnosticSeverity ConvertSeverity(RazorDiagnosticSeverity severity)
internal static LspDiagnosticSeverity ConvertSeverity(RazorDiagnosticSeverity severity)
{
return severity switch
{
RazorDiagnosticSeverity.Error => DiagnosticSeverity.Error,
RazorDiagnosticSeverity.Warning => DiagnosticSeverity.Warning,
_ => DiagnosticSeverity.Information,
RazorDiagnosticSeverity.Error => LspDiagnosticSeverity.Error,
RazorDiagnosticSeverity.Warning => LspDiagnosticSeverity.Warning,
_ => LspDiagnosticSeverity.Information,
};
}
// Internal for testing
internal static Range? ConvertSpanToRange(SourceSpan sourceSpan, SourceText sourceText)
internal static LspRange? ConvertSpanToRange(SourceSpan sourceSpan, SourceText sourceText)
{
if (sourceSpan == SourceSpan.Undefined)
{

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

@ -6,7 +6,6 @@ using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Syntax;
@ -18,12 +17,14 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Diagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic;
using DiagnosticSeverity = Microsoft.VisualStudio.LanguageServer.Protocol.DiagnosticSeverity;
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic;
using LspDiagnosticSeverity = Microsoft.VisualStudio.LanguageServer.Protocol.DiagnosticSeverity;
using LspRange = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics;
namespace Microsoft.CodeAnalysis.Razor.Diagnostics;
using RazorDiagnosticFactory = AspNetCore.Razor.Language.RazorDiagnosticFactory;
using SyntaxNode = AspNetCore.Razor.Language.Syntax.SyntaxNode;
/// <summary>
/// Contains several methods for mapping and filtering Razor and C# diagnostics. It allows for
@ -48,16 +49,14 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
/// </summary>
/// <param name="diagnosticKind">The `RazorLanguageKind` of the `Diagnostic` objects included in `diagnostics`.</param>
/// <param name="diagnostics">An array of `Diagnostic` objects to translate.</param>
/// <param name="documentContext">The `DocumentContext` for the code document associated with the diagnostics.</param>
/// <param name="cancellationToken">A `CancellationToken` to observe while waiting for the task to complete.</param>
/// <param name="documentSnapshot">The `DocumentContext` for the code document associated with the diagnostics.</param>
/// <returns>An array of translated diagnostics</returns>
internal async Task<Diagnostic[]> TranslateAsync(
internal async Task<LspDiagnostic[]> TranslateAsync(
RazorLanguageKind diagnosticKind,
Diagnostic[] diagnostics,
DocumentContext documentContext,
CancellationToken cancellationToken)
LspDiagnostic[] diagnostics,
IDocumentSnapshot documentSnapshot)
{
var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
if (codeDocument.IsUnsupported() != false)
{
_logger.LogInformation($"Unsupported code document.");
@ -78,20 +77,20 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
var mappedDiagnostics = MapDiagnostics(
diagnosticKind,
filteredDiagnostics,
documentContext.Snapshot,
documentSnapshot,
codeDocument);
return mappedDiagnostics;
}
private Diagnostic[] FilterCSharpDiagnostics(Diagnostic[] unmappedDiagnostics, RazorCodeDocument codeDocument)
private LspDiagnostic[] FilterCSharpDiagnostics(LspDiagnostic[] unmappedDiagnostics, RazorCodeDocument codeDocument)
{
return unmappedDiagnostics.Where(d =>
!ShouldFilterCSharpDiagnosticBasedOnErrorCode(d, codeDocument)).ToArray();
}
private static Diagnostic[] FilterHTMLDiagnostics(
Diagnostic[] unmappedDiagnostics,
private static LspDiagnostic[] FilterHTMLDiagnostics(
LspDiagnostic[] unmappedDiagnostics,
RazorCodeDocument codeDocument)
{
var syntaxTree = codeDocument.GetSyntaxTree();
@ -110,14 +109,14 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
return filteredDiagnostics;
}
private Diagnostic[] MapDiagnostics(
private LspDiagnostic[] MapDiagnostics(
RazorLanguageKind languageKind,
Diagnostic[] diagnostics,
LspDiagnostic[] diagnostics,
IDocumentSnapshot documentSnapshot,
RazorCodeDocument codeDocument)
{
var projects = RazorDiagnosticConverter.GetProjectInformation(documentSnapshot);
using var mappedDiagnostics = new PooledArrayBuilder<Diagnostic>();
using var mappedDiagnostics = new PooledArrayBuilder<LspDiagnostic>();
foreach (var diagnostic in diagnostics)
{
@ -146,7 +145,7 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
}
private static bool InCSharpLiteral(
Diagnostic d,
LspDiagnostic d,
SourceText sourceText,
RazorSyntaxTree syntaxTree)
{
@ -177,7 +176,7 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
or SyntaxKind.CSharpEphemeralTextLiteral;
}
private static bool AppliesToTagHelperTagName(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
private static bool AppliesToTagHelperTagName(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
{
// Goal of this method is to filter diagnostics that touch TagHelper tag names. Reason being is TagHelpers can output anything. Meaning
// If you have a TagHelper like:
@ -214,7 +213,7 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
return true;
}
private static bool ShouldFilterHtmlDiagnosticBasedOnErrorCode(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
private static bool ShouldFilterHtmlDiagnosticBasedOnErrorCode(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
{
if (!diagnostic.Code.HasValue)
{
@ -235,7 +234,7 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
_ => false,
};
static bool IsCSharpInStyleBlock(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
static bool IsCSharpInStyleBlock(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
{
// C# in a style block causes diagnostics because the HTML background document replaces C# with "~"
var owner = syntaxTree.FindInnermostNode(sourceText, diagnostic.Range.Start);
@ -253,7 +252,7 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
// Ideally this would be solved instead by not emitting the "!" at the HTML backing file,
// but we don't currently have a system to accomplish that
static bool IsAnyFilteredTooFewElementsError(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
static bool IsAnyFilteredTooFewElementsError(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
{
var owner = syntaxTree.FindInnermostNode(sourceText, diagnostic.Range.Start);
if (owner is null)
@ -282,7 +281,7 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
// Ideally this would be solved instead by not emitting the "!" at the HTML backing file,
// but we don't currently have a system to accomplish that
static bool IsHtmlWithBangAndMatchingTags(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
static bool IsHtmlWithBangAndMatchingTags(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
{
var owner = syntaxTree.FindInnermostNode(sourceText, diagnostic.Range.Start);
if (owner is null)
@ -306,11 +305,11 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
return haveBang && namesEquivalent;
}
static bool IsAnyFilteredInvalidNestingError(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
static bool IsAnyFilteredInvalidNestingError(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
=> IsInvalidNestingWarningWithinComponent(diagnostic, sourceText, syntaxTree) ||
IsInvalidNestingFromBody(diagnostic, sourceText, syntaxTree);
static bool IsInvalidNestingWarningWithinComponent(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
static bool IsInvalidNestingWarningWithinComponent(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
{
var owner = syntaxTree.FindInnermostNode(sourceText, diagnostic.Range.Start);
if (owner is null)
@ -325,7 +324,7 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
// Ideally this would be solved instead by not emitting the "!" at the HTML backing file,
// but we don't currently have a system to accomplish that
static bool IsInvalidNestingFromBody(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
static bool IsInvalidNestingFromBody(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
{
var owner = syntaxTree.FindInnermostNode(sourceText, diagnostic.Range.Start);
if (owner is null)
@ -350,7 +349,7 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
}
private static bool InAttributeContainingCSharp(
Diagnostic diagnostic,
LspDiagnostic diagnostic,
SourceText sourceText,
RazorSyntaxTree syntaxTree,
Dictionary<TextSpan, bool> processedAttributes)
@ -400,7 +399,7 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
}
}
private bool ShouldFilterCSharpDiagnosticBasedOnErrorCode(Diagnostic diagnostic, RazorCodeDocument codeDocument)
private bool ShouldFilterCSharpDiagnosticBasedOnErrorCode(LspDiagnostic diagnostic, RazorCodeDocument codeDocument)
{
if (diagnostic.Code is not { } code ||
!code.TryGetSecond(out var str) ||
@ -413,10 +412,10 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
{
"CS1525" => ShouldIgnoreCS1525(diagnostic, codeDocument),
_ => s_cSharpDiagnosticsToIgnore.Contains(str) &&
diagnostic.Severity != DiagnosticSeverity.Error
diagnostic.Severity != LspDiagnosticSeverity.Error
};
bool ShouldIgnoreCS1525(Diagnostic diagnostic, RazorCodeDocument codeDocument)
bool ShouldIgnoreCS1525(LspDiagnostic diagnostic, RazorCodeDocument codeDocument)
{
if (CheckIfDocumentHasRazorDiagnostic(codeDocument, RazorDiagnosticFactory.TagHelper_EmptyBoundAttribute.Id) &&
TryGetOriginalDiagnosticRange(diagnostic, codeDocument, out var originalRange) &&
@ -440,7 +439,7 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
return codeDocument.GetSyntaxTree().Diagnostics.Any(razorDiagnosticCode, static (d, code) => d.Id == code);
}
private bool TryGetOriginalDiagnosticRange(Diagnostic diagnostic, RazorCodeDocument codeDocument, [NotNullWhen(true)] out Range? originalRange)
private bool TryGetOriginalDiagnosticRange(LspDiagnostic diagnostic, RazorCodeDocument codeDocument, [NotNullWhen(true)] out LspRange? originalRange)
{
if (IsRudeEditDiagnostic(diagnostic))
{
@ -460,7 +459,7 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
{
// Couldn't remap the range correctly.
// If this isn't an `Error` Severity Diagnostic we can discard it.
if (diagnostic.Severity != DiagnosticSeverity.Error)
if (diagnostic.Severity != LspDiagnosticSeverity.Error)
{
return false;
}
@ -474,14 +473,14 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document
return true;
}
private static bool IsRudeEditDiagnostic(Diagnostic diagnostic)
private static bool IsRudeEditDiagnostic(LspDiagnostic diagnostic)
{
return diagnostic.Code.HasValue &&
diagnostic.Code.Value.TryGetSecond(out var str) &&
str.StartsWith("ENC");
}
private bool TryRemapRudeEditRange(Range diagnosticRange, RazorCodeDocument codeDocument, [NotNullWhen(true)] out Range? remappedRange)
private bool TryRemapRudeEditRange(LspRange diagnosticRange, RazorCodeDocument codeDocument, [NotNullWhen(true)] out LspRange? remappedRange)
{
// This is a rude edit diagnostic that has already been mapped to the Razor document. The mapping isn't absolutely correct though,
// it's based on the runtime code generation of the Razor document therefore we need to re-map the already mapped diagnostic in a

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

@ -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.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using RoslynLspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic;
namespace Microsoft.CodeAnalysis.Razor.Remote;
internal interface IRemoteDiagnosticsService : IRemoteJsonService
{
ValueTask<ImmutableArray<RoslynLspDiagnostic>> GetDiagnosticsAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId documentId,
RoslynLspDiagnostic[] csharpDiagnostics,
RoslynLspDiagnostic[] htmlDiagnostics,
CancellationToken cancellationToken);
}

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

@ -35,6 +35,7 @@ internal static class RazorServices
(typeof(IRemoteDocumentSymbolService), null),
(typeof(IRemoteRenameService), null),
(typeof(IRemoteGoToImplementationService), null),
(typeof(IRemoteDiagnosticsService), null),
];
private const string ComponentName = "Razor";

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

@ -0,0 +1,56 @@
// 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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.Diagnostics;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic;
namespace Microsoft.CodeAnalysis.Remote.Razor;
internal sealed class RemoteDiagnosticsService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteDiagnosticsService
{
internal sealed class Factory : FactoryBase<IRemoteDiagnosticsService>
{
protected override IRemoteDiagnosticsService CreateService(in ServiceArgs args)
=> new RemoteDiagnosticsService(in args);
}
private readonly RazorTranslateDiagnosticsService _translateDiagnosticsService = args.ExportProvider.GetExportedValue<RazorTranslateDiagnosticsService>();
public ValueTask<ImmutableArray<LspDiagnostic>> GetDiagnosticsAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId documentId,
LspDiagnostic[] csharpDiagnostics,
LspDiagnostic[] htmlDiagnostics,
CancellationToken cancellationToken)
=> RunServiceAsync(
solutionInfo,
documentId,
context => GetDiagnosticsAsync(context, csharpDiagnostics, htmlDiagnostics, cancellationToken),
cancellationToken);
private async ValueTask<ImmutableArray<LspDiagnostic>> GetDiagnosticsAsync(
RemoteDocumentContext context,
LspDiagnostic[] csharpDiagnostics,
LspDiagnostic[] htmlDiagnostics,
CancellationToken cancellationToken)
{
// We've got C# and Html, lets get Razor diagnostics
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
// Yes, CSharpDocument.Documents are the Razor diagnostics. Don't ask.
var razorDiagnostics = codeDocument.GetCSharpDocument().Diagnostics;
return [
.. RazorDiagnosticConverter.Convert(razorDiagnostics, codeDocument.Source.Text, context.Snapshot),
.. await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.CSharp, csharpDiagnostics, context.Snapshot),
.. await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.Html, htmlDiagnostics, context.Snapshot)
];
}
}

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

@ -0,0 +1,17 @@
// 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 Microsoft.CodeAnalysis.Razor.Diagnostics;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Logging;
namespace Microsoft.CodeAnalysis.Remote.Razor.Diagnostics;
[Export(typeof(RazorTranslateDiagnosticsService)), Shared]
[method: ImportingConstructor]
internal sealed class RemoteRazorTranslateDiagnosticsService(
IDocumentMappingService documentMappingService,
ILoggerFactory loggerFactory) : RazorTranslateDiagnosticsService(documentMappingService, loggerFactory)
{
}

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

@ -0,0 +1,204 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
#pragma warning disable RS0030 // Do not use banned APIs
[Shared]
[CohostEndpoint(VSInternalMethods.DocumentPullDiagnosticName)]
[Export(typeof(IDynamicRegistrationProvider))]
[ExportCohostStatelessLspService(typeof(CohostDocumentPullDiagnosticsEndpoint))]
[method: ImportingConstructor]
#pragma warning restore RS0030 // Do not use banned APIs
internal class CohostDocumentPullDiagnosticsEndpoint(
IRemoteServiceInvoker remoteServiceInvoker,
IHtmlDocumentSynchronizer htmlDocumentSynchronizer,
LSPRequestInvoker requestInvoker,
IFilePathService filePathService,
ILoggerFactory loggerFactory)
: AbstractRazorCohostDocumentRequestHandler<VSInternalDocumentDiagnosticsParams, VSInternalDiagnosticReport[]?>, IDynamicRegistrationProvider
{
private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer;
private readonly LSPRequestInvoker _requestInvoker = requestInvoker;
private readonly IFilePathService _filePathService = filePathService;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CohostDocumentPullDiagnosticsEndpoint>();
protected override bool MutatesSolutionState => false;
protected override bool RequiresLSPSolution => true;
public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext)
{
// TODO: if (clientCapabilities.TextDocument?.Diagnostic?.DynamicRegistration is true)
{
return new Registration()
{
Method = VSInternalMethods.DocumentPullDiagnosticName,
RegisterOptions = new VSInternalDiagnosticRegistrationOptions()
{
DocumentSelector = filter,
DiagnosticKinds = [VSInternalDiagnosticKind.Syntax]
}
};
}
// return null;
}
protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(VSInternalDocumentDiagnosticsParams request)
=> request.TextDocument?.ToRazorTextDocumentIdentifier();
protected override Task<VSInternalDiagnosticReport[]?> HandleRequestAsync(VSInternalDocumentDiagnosticsParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
=> HandleRequestAsync(context.TextDocument.AssumeNotNull(), cancellationToken);
private async Task<VSInternalDiagnosticReport[]?> HandleRequestAsync(TextDocument razorDocument, CancellationToken cancellationToken)
{
// Diagnostics is a little different, because Roslyn is not designed to run diagnostics in OOP. Their system will transition to OOP
// as it needs, but we have to start here in devenv. This is not as big a problem as it sounds, specifically for diagnostics, because
// we only need to tell Roslyn the document we need diagnostics for. If we had to map positions or ranges etc. it would be worse
// because we'd have to transition to our OOP to find out that info, then back here to get the diagnostics, then back to OOP to process.
_logger.LogDebug($"Getting diagnostics for {razorDocument.FilePath}");
var csharpTask = GetCSharpDiagnosticsAsync(razorDocument, cancellationToken);
var htmlTask = GetHtmlDiagnosticsAsync(razorDocument, cancellationToken);
try
{
await Task.WhenAll(htmlTask, csharpTask).ConfigureAwait(false);
}
catch (Exception e)
{
if (e is not OperationCanceledException)
{
_logger.LogError(e, $"Exception thrown in PullDiagnostic delegation");
throw;
}
}
var csharpDiagnostics = await csharpTask.ConfigureAwait(false);
var htmlDiagnostics = await htmlTask.ConfigureAwait(false);
_logger.LogDebug($"Calling OOP with the {csharpDiagnostics.Length} C# and {htmlDiagnostics.Length} Html diagnostics");
var diagnostics = await _remoteServiceInvoker.TryInvokeAsync<IRemoteDiagnosticsService, ImmutableArray<LspDiagnostic>>(
razorDocument.Project.Solution,
(service, solutionInfo, cancellationToken) => service.GetDiagnosticsAsync(solutionInfo, razorDocument.Id, csharpDiagnostics, htmlDiagnostics, cancellationToken),
cancellationToken).ConfigureAwait(false);
if (diagnostics.IsDefaultOrEmpty)
{
return null;
}
_logger.LogDebug($"Reporting {diagnostics.Length} diagnostics back to the client");
return
[
new()
{
Diagnostics = diagnostics.ToArray(),
ResultId = Guid.NewGuid().ToString()
}
];
}
private async Task<LspDiagnostic[]> GetCSharpDiagnosticsAsync(TextDocument razorDocument, CancellationToken cancellationToken)
{
// TODO: This code will not work when the source generator is hooked up.
// How do we get the source generated C# document without OOP? Can we reverse engineer a file path?
var projectKey = razorDocument.Project.ToProjectKey();
var csharpFilePath = _filePathService.GetRazorCSharpFilePath(projectKey, razorDocument.FilePath.AssumeNotNull());
// We put the project Id in the generated document path, so there can only be one document
if (razorDocument.Project.Solution.GetDocumentIdsWithFilePath(csharpFilePath) is not [{ } generatedDocumentId] ||
razorDocument.Project.GetDocument(generatedDocumentId) is not { } generatedDocument)
{
return [];
}
_logger.LogDebug($"Getting C# diagnostics for {generatedDocument.FilePath}");
var csharpDiagnostics = await ExternalHandlers.Diagnostics.GetDocumentDiagnosticsAsync(generatedDocument, supportsVisualStudioExtensions: true, cancellationToken).ConfigureAwait(false);
// This is, to say the least, not ideal. In future we're going to normalize on to Roslyn LSP types, and this can go.
var options = new JsonSerializerOptions();
foreach (var converter in RazorServiceDescriptorsWrapper.GetLspConverters())
{
options.Converters.Add(converter);
}
if (JsonSerializer.Deserialize<LspDiagnostic[]>(JsonSerializer.SerializeToDocument(csharpDiagnostics), options) is not { } convertedDiagnostics)
{
return [];
}
return convertedDiagnostics;
}
private async Task<LspDiagnostic[]> GetHtmlDiagnosticsAsync(TextDocument razorDocument, CancellationToken cancellationToken)
{
var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false);
if (htmlDocument is null)
{
return [];
}
var diagnosticsParams = new VSInternalDocumentDiagnosticsParams
{
TextDocument = new TextDocumentIdentifier { Uri = htmlDocument.Uri }
};
_logger.LogDebug($"Getting Html diagnostics for {htmlDocument.Uri}");
var result = await _requestInvoker.ReinvokeRequestOnServerAsync<VSInternalDocumentDiagnosticsParams, VSInternalDiagnosticReport[]?>(
htmlDocument.Buffer,
VSInternalMethods.DocumentPullDiagnosticName,
RazorLSPConstants.HtmlLanguageServerName,
diagnosticsParams,
cancellationToken).ConfigureAwait(false);
if (result?.Response is null)
{
return [];
}
using var allDiagnostics = new PooledArrayBuilder<LspDiagnostic>();
foreach (var report in result.Response)
{
if (report.Diagnostics is not null)
{
allDiagnostics.AddRange(report.Diagnostics);
}
}
return allDiagnostics.ToArray();
}
internal TestAccessor GetTestAccessor() => new(this);
internal readonly struct TestAccessor(CohostDocumentPullDiagnosticsEndpoint instance)
{
public Task<VSInternalDiagnosticReport[]?> HandleRequestAsync(TextDocument razorDocument, CancellationToken cancellationToken)
=> instance.HandleRequestAsync(razorDocument, cancellationToken);
}
}

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

@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
using Microsoft.AspNetCore.Razor.LanguageServer.Test;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
@ -26,7 +25,6 @@ using Xunit.Sdk;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation;
[UseExportProvider]
public class DelegatedCompletionItemResolverTest : LanguageServerTestBase
{
private readonly VSInternalClientCapabilities _clientCapabilities;

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

@ -10,7 +10,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Testing;
@ -21,7 +20,6 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation;
[UseExportProvider]
public class DelegatedCompletionListProviderTest : LanguageServerTestBase
{
private readonly TestDelegatedCompletionListProvider _provider;

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

@ -9,6 +9,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
using Microsoft.CodeAnalysis.Razor.Diagnostics;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;

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

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.AspNetCore.Razor.Test.Common.Workspaces;
using Microsoft.CodeAnalysis.Razor.Diagnostics;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Moq;
using Xunit;

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

@ -6,6 +6,7 @@
using System.Globalization;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.CodeAnalysis.Razor.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Xunit;

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

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem;
using Microsoft.AspNetCore.Razor.Test.Common.Workspaces;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.Diagnostics;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;

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

@ -11,7 +11,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Testing;
@ -23,7 +22,6 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.DocumentHighlighting;
[UseExportProvider]
public class DocumentHighlightEndpointTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput)
{
[Fact]

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

@ -4,12 +4,15 @@
#nullable disable
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
public class CSharpStatementBlockOnTypeFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput)
[Collection(HtmlFormattingCollection.Name)]
public class CSharpStatementBlockOnTypeFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput)
: FormattingTestBase(fixture.Service, testOutput)
{
[Fact]
public async Task CloseCurly_IfBlock_SingleLineAsync()

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

@ -4,12 +4,15 @@
#nullable disable
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
public class CodeActionFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput)
[Collection(HtmlFormattingCollection.Name)]
public class CodeActionFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput)
: FormattingTestBase(fixture.Service, testOutput)
{
[Fact]
public async Task AddDebuggerDisplay()

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

@ -5,12 +5,15 @@ using System.Collections.Immutable;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
public class CodeDirectiveFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput)
[Collection(HtmlFormattingCollection.Name)]
public class CodeDirectiveFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput)
: FormattingTestBase(fixture.Service, testOutput)
{
internal override bool UseTwoPhaseCompilation => true;

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

@ -5,12 +5,15 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
public class CodeDirectiveOnTypeFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput)
[Collection(HtmlFormattingCollection.Name)]
public class CodeDirectiveOnTypeFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput)
: FormattingTestBase(fixture.Service, testOutput)
{
[Fact]
public async Task FormatsIfStatementInComponent()

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

@ -18,8 +18,9 @@ using Newtonsoft.Json.Linq;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
internal class FormattingLanguageServerClient(ILoggerFactory loggerFactory) : IClientConnection
internal class FormattingLanguageServerClient(HtmlFormattingService htmlFormattingService, ILoggerFactory loggerFactory) : IClientConnection
{
private readonly HtmlFormattingService _htmlFormattingService = htmlFormattingService;
private readonly Dictionary<string, RazorCodeDocument> _documents = [];
private readonly ILoggerFactory _loggerFactory = loggerFactory;
@ -36,7 +37,7 @@ internal class FormattingLanguageServerClient(ILoggerFactory loggerFactory) : IC
{
var generatedHtml = GetGeneratedHtml(@params.TextDocument.Uri);
var edits = await HtmlFormatting.GetOnTypeFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Position, @params.Options.InsertSpaces, @params.Options.TabSize);
var edits = await _htmlFormattingService.GetOnTypeFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Position, @params.Options.InsertSpaces, @params.Options.TabSize);
return new()
{
@ -48,7 +49,7 @@ internal class FormattingLanguageServerClient(ILoggerFactory loggerFactory) : IC
{
var generatedHtml = GetGeneratedHtml(@params.TextDocument.Uri);
var edits = await HtmlFormatting.GetDocumentFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Options.InsertSpaces, @params.Options.TabSize);
var edits = await _htmlFormattingService.GetDocumentFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Options.InsertSpaces, @params.Options.TabSize);
return new()
{

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

@ -34,10 +34,14 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
public class FormattingTestBase : RazorToolingIntegrationTestBase
{
public FormattingTestBase(ITestOutputHelper testOutput)
private readonly HtmlFormattingService _htmlFormattingService;
internal FormattingTestBase(HtmlFormattingService htmlFormattingService, ITestOutputHelper testOutput)
: base(testOutput)
{
ITestOnlyLoggerExtensions.TestOnlyLoggingEnabled = true;
_htmlFormattingService = htmlFormattingService;
}
private protected async Task RunFormattingTestAsync(
@ -94,7 +98,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, razorLSPOptions);
var documentContext = new DocumentContext(uri, documentSnapshot, projectContext: null);
var client = new FormattingLanguageServerClient(LoggerFactory);
var client = new FormattingLanguageServerClient(_htmlFormattingService, LoggerFactory);
client.AddCodeDocument(codeDocument);
var htmlFormatter = new HtmlFormatter(client);
@ -159,7 +163,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
}
else
{
var client = new FormattingLanguageServerClient(LoggerFactory);
var client = new FormattingLanguageServerClient(_htmlFormattingService, LoggerFactory);
client.AddCodeDocument(codeDocument);
var htmlFormatter = new HtmlFormatter(client);
@ -168,7 +172,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
}
// Assert
var edited = razorSourceText.WithChanges( changes);
var edited = razorSourceText.WithChanges(changes);
var actual = edited.ToString();
AssertEx.EqualOrDiff(expected, actual);

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

@ -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.CodeAnalysis.Razor.Formatting;
using Xunit;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
[CollectionDefinition(Name)]
public class HtmlFormattingCollection : ICollectionFixture<HtmlFormattingFixture>
{
public const string Name = nameof(HtmlFormattingCollection);
}

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

@ -14,7 +14,9 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
public class HtmlFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput)
[Collection(HtmlFormattingCollection.Name)]
public class HtmlFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput)
: FormattingTestBase(fixture.Service, testOutput)
{
internal override bool UseTwoPhaseCompilation => true;

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

@ -7,12 +7,14 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
public class RazorFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput)
[Collection(HtmlFormattingCollection.Name)]
public class RazorFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput) : FormattingTestBase(fixture.Service, testOutput)
{
[Fact]
public async Task Section_BraceOnNextLine()

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

@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Tooltip;
using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -29,7 +28,6 @@ using static Microsoft.AspNetCore.Razor.LanguageServer.Tooltip.DefaultVSLSPTagHe
namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Hover;
[UseExportProvider]
public class HoverServiceTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput)
{
private static VSInternalClientCapabilities CreateMarkDownCapabilities()

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

@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.LanguageServer.MapCode;
using Microsoft.AspNetCore.Razor.Telemetry;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Testing;
@ -22,7 +21,6 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.MapCode;
[UseExportProvider]
public class MapCodeTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput)
{
private const string RazorFilePath = "C:/path/to/file.razor";

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

@ -7,7 +7,6 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Rename;
using Microsoft.CodeAnalysis.Razor.Workspaces;
@ -19,7 +18,6 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Refactoring;
[UseExportProvider]
public class RenameEndpointDelegationTest(ITestOutputHelper testOutput) : SingleServerDelegatingEndpointTestBase(testOutput)
{
[Fact]

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

@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -33,7 +32,6 @@ using static Microsoft.AspNetCore.Razor.Language.CommonMetadata;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Refactoring;
[UseExportProvider]
public class RenameEndpointTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput)
{
private static readonly string s_project1BasePath = PathUtilities.CreateRootedPath("First");

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

@ -19,7 +19,6 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
@ -35,7 +34,6 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic;
[UseExportProvider]
public partial class SemanticTokensTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput)
{
private readonly Mock<IClientConnection> _clientConnection = new(MockBehavior.Strict);

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

@ -7,7 +7,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -19,7 +18,6 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.LanguageServer;
[UseExportProvider]
public abstract partial class SingleServerDelegatingEndpointTestBase(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput)
{
private protected IDocumentContextFactory? DocumentContextFactory { get; private set; }

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

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
public class HtmlFormattingFixture : IDisposable
{
private readonly HtmlFormattingService _htmlFormattingService = new();
internal HtmlFormattingService Service => _htmlFormattingService;
public void Dispose()
{
_htmlFormattingService.Dispose();
}
}

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

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Composition;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Utilities;
@ -17,9 +18,21 @@ using Microsoft.WebTools.Languages.Shared.ContentTypes;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
internal static class HtmlFormatting
internal sealed class HtmlFormattingService : IDisposable
{
public static Task<TextEdit[]?> GetDocumentFormattingEditsAsync(ILoggerFactory loggerFactory, Uri uri, string generatedHtml, bool insertSpaces, int tabSize)
private ExportProvider? _exportProvider;
private ExportProvider ExportProvider => _exportProvider ?? (_exportProvider = TestComposition.Editor.ExportProviderFactory.CreateExportProvider());
public void Dispose()
{
if (_exportProvider is not null)
{
_exportProvider.Dispose();
}
}
public Task<TextEdit[]?> GetDocumentFormattingEditsAsync(ILoggerFactory loggerFactory, Uri uri, string generatedHtml, bool insertSpaces, int tabSize)
{
var request = $$"""
{
@ -37,7 +50,7 @@ internal static class HtmlFormatting
return CallWebToolsApplyFormattedEditsHandlerAsync(loggerFactory, request, uri, generatedHtml);
}
public static Task<TextEdit[]?> GetOnTypeFormattingEditsAsync(ILoggerFactory loggerFactory, Uri uri, string generatedHtml, Position position, bool insertSpaces, int tabSize)
public Task<TextEdit[]?> GetOnTypeFormattingEditsAsync(ILoggerFactory loggerFactory, Uri uri, string generatedHtml, Position position, bool insertSpaces, int tabSize)
{
var generatedHtmlSource = SourceText.From(generatedHtml, Encoding.UTF8);
var absoluteIndex = generatedHtmlSource.GetRequiredAbsoluteIndex(position);
@ -64,10 +77,9 @@ internal static class HtmlFormatting
return CallWebToolsApplyFormattedEditsHandlerAsync(loggerFactory, request, uri, generatedHtml);
}
private static async Task<TextEdit[]?> CallWebToolsApplyFormattedEditsHandlerAsync(ILoggerFactory loggerFactory, string serializedValue, Uri documentUri, string generatedHtml)
private async Task<TextEdit[]?> CallWebToolsApplyFormattedEditsHandlerAsync(ILoggerFactory loggerFactory, string serializedValue, Uri documentUri, string generatedHtml)
{
var exportProvider = TestComposition.Editor.ExportProviderFactory.CreateExportProvider();
var contentTypeService = exportProvider.GetExportedValue<IContentTypeRegistryService>();
var contentTypeService = ExportProvider.GetExportedValue<IContentTypeRegistryService>();
lock (contentTypeService)
{
@ -77,7 +89,7 @@ internal static class HtmlFormatting
}
}
var textBufferFactoryService = (ITextBufferFactoryService3)exportProvider.GetExportedValue<ITextBufferFactoryService>();
var textBufferFactoryService = (ITextBufferFactoryService3)ExportProvider.GetExportedValue<ITextBufferFactoryService>();
var bufferManager = WebTools.BufferManager.New(contentTypeService, textBufferFactoryService, []);
var logger = loggerFactory.GetOrCreateLogger("ApplyFormattedEditsHandler");
var applyFormatEditsHandler = WebTools.ApplyFormatEditsHandler.New(textBufferFactoryService, bufferManager, logger);

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

@ -12,7 +12,6 @@ using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Razor;
@ -22,7 +21,6 @@ using Xunit.Sdk;
namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests;
[UseExportProvider]
public class RazorToolingIntegrationTestBase : ToolingTestBase
{
internal const string ArbitraryWindowsPath = "x:\\dir\\subdir\\Test";

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

@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
public sealed class CSharpTestLspServer : IAsyncDisposable
{
private readonly AdhocWorkspace _testWorkspace;
private readonly IRazorLanguageServerTarget _languageServer;
private readonly ExportProvider _exportProvider;
private readonly JsonRpc _clientRpc;
private readonly JsonRpc _serverRpc;
@ -41,6 +41,7 @@ public sealed class CSharpTestLspServer : IAsyncDisposable
CancellationToken cancellationToken)
{
_testWorkspace = testWorkspace;
_exportProvider = exportProvider;
_cancellationToken = cancellationToken;
var (clientStream, serverStream) = FullDuplexStream.CreatePair();
@ -67,7 +68,7 @@ public sealed class CSharpTestLspServer : IAsyncDisposable
_clientRpc.StartListening();
_languageServer = CreateLanguageServer(_serverRpc, _serverMessageFormatter.JsonSerializerOptions, testWorkspace, languageServerFactory, exportProvider, serverCapabilities);
_ = CreateLanguageServer(_serverRpc, _serverMessageFormatter.JsonSerializerOptions, testWorkspace, languageServerFactory, exportProvider, serverCapabilities);
static SystemTextJsonFormatter CreateSystemTextJsonMessageFormatter(AbstractRazorLanguageServerFactoryWrapper languageServerFactory)
{
@ -146,6 +147,7 @@ public sealed class CSharpTestLspServer : IAsyncDisposable
public async ValueTask DisposeAsync()
{
_testWorkspace.Dispose();
_exportProvider.Dispose();
_clientRpc.Dispose();
_clientMessageFormatter.Dispose();

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

@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -14,9 +13,7 @@ using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.AspNetCore.Razor.Test.Common.Workspaces;
using Microsoft.AspNetCore.Razor.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
@ -109,8 +106,7 @@ internal static class CSharpTestLspServerHelpers
IRazorSpanMappingService razorSpanMappingService,
bool multiTargetProject)
{
var hostServices = MefHostServices.Create(exportProvider.AsCompositionContext());
var workspace = TestWorkspace.Create(hostServices);
var workspace = TestWorkspace.CreateWithDiagnosticAnalyzers(exportProvider);
// Add project and solution to workspace
var projectInfoNet60 = ProjectInfo.Create(
@ -135,14 +131,10 @@ internal static class CSharpTestLspServerHelpers
? [projectInfoNet60, projectInfoNet80]
: [projectInfoNet80];
var solutionInfo = SolutionInfo.Create(
id: SolutionId.CreateNewId("TestSolution"),
version: VersionStamp.Default,
projects: projectInfos);
workspace.AddSolution(solutionInfo);
AddAnalyzersToWorkspace(workspace, exportProvider);
foreach (var projectInfo in projectInfos)
{
workspace.AddProject(projectInfo);
}
// Add document to workspace. We use an IVT method to create the DocumentInfo variable because there's
// a special constructor in Roslyn that will help identify the document as belonging to Razor.
@ -172,30 +164,6 @@ internal static class CSharpTestLspServerHelpers
return workspace;
}
private static void AddAnalyzersToWorkspace(Workspace workspace, ExportProvider exportProvider)
{
var analyzerLoader = RazorTestAnalyzerLoader.CreateAnalyzerAssemblyLoader();
var analyzerPaths = new DirectoryInfo(AppContext.BaseDirectory).GetFiles("*.dll")
.Where(f => f.Name.StartsWith("Microsoft.CodeAnalysis.", StringComparison.Ordinal) && !f.Name.Contains("LanguageServer") && !f.Name.Contains("Test.Utilities"))
.Select(f => f.FullName)
.ToImmutableArray();
var references = new List<AnalyzerFileReference>();
foreach (var analyzerPath in analyzerPaths)
{
if (File.Exists(analyzerPath))
{
references.Add(new AnalyzerFileReference(analyzerPath, analyzerLoader));
}
}
workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences(references));
// Make sure Roslyn is producing diagnostics for our workspace
var razorTestAnalyzerLoader = exportProvider.GetExportedValue<RazorTestAnalyzerLoader>();
razorTestAnalyzerLoader.InitializeDiagnosticsServices(workspace);
}
private record CSharpFile(Uri DocumentUri, SourceText CSharpSourceText);
private class EmptyMappingService : IRazorSpanMappingService

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

@ -2,14 +2,10 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Composition;
namespace Microsoft.AspNetCore.Razor.Test.Common.Mef;
@ -18,60 +14,6 @@ public static class ExportProviderCache
{
private static readonly PartDiscovery s_partDiscovery = CreatePartDiscovery(Resolver.DefaultInstance);
private static readonly TestComposition s_defaultHostExportProviderComposition = TestComposition.Empty
.AddAssemblies(MefHostServices.DefaultAssemblies);
private static readonly ConcurrentDictionary<string, Scope> s_scopes = new();
private const string DefaultScope = "default";
private static readonly object s_lock = new();
internal static bool Enabled { get; private set; }
internal static ExportProvider[] ExportProvidersForCleanup
{
get
{
var scopes = s_scopes.Values.ToArray();
var defaultScope = scopes.Where(scope => scope.Name == DefaultScope);
var allButDefault = scopes.Where(scope => scope.Name != DefaultScope);
// Make sure to return the default scope as the last element
return allButDefault.Concat(defaultScope)
.Where(scope => scope._currentExportProvider is { })
.Select(scope => scope._currentExportProvider!)
.ToArray();
}
}
internal static void SetEnabled_OnlyUseExportProviderAttributeCanCall(bool value)
{
lock (s_lock)
{
Enabled = value;
if (!Enabled)
{
foreach (var scope in s_scopes.Values.ToArray())
{
scope.Clear();
}
}
}
}
/// <summary>
/// Use to create <see cref="IExportProviderFactory"/> for default instances of <see cref="MefHostServices"/>.
/// </summary>
public static IExportProviderFactory GetOrCreateExportProviderFactory(IEnumerable<Assembly> assemblies)
{
if (assemblies is ImmutableArray<Assembly> assembliesArray &&
assembliesArray == MefHostServices.DefaultAssemblies)
{
return s_defaultHostExportProviderComposition.ExportProviderFactory;
}
return CreateExportProviderFactory(CreateAssemblyCatalog(assemblies), scopeName: DefaultScope);
}
public static ComposableCatalog CreateAssemblyCatalog(IEnumerable<Assembly> assemblies, Resolver? resolver = null)
{
var discovery = resolver is null ? s_partDiscovery : CreatePartDiscovery(resolver);
@ -125,150 +67,47 @@ public static class ExportProviderCache
}
}
public static IExportProviderFactory CreateExportProviderFactory(ComposableCatalog catalog, string? scopeName = null)
public static IExportProviderFactory CreateExportProviderFactory(ComposableCatalog catalog)
{
var scope = s_scopes.GetOrAdd(scopeName ?? DefaultScope, scopeName => new Scope(scopeName));
var configuration = CompositionConfiguration.Create(catalog.WithCompositionService());
ValidateConfiguration(configuration);
var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration);
var exportProviderFactory = runtimeComposition.CreateExportProviderFactory();
return new SingleExportProviderFactory(scope, catalog, configuration, exportProviderFactory);
return exportProviderFactory;
}
private sealed class SingleExportProviderFactory : IExportProviderFactory
private static void ValidateConfiguration(CompositionConfiguration configuration)
{
private readonly Scope _scope;
private readonly ComposableCatalog _catalog;
private readonly CompositionConfiguration _configuration;
private readonly IExportProviderFactory _exportProviderFactory;
public SingleExportProviderFactory(Scope scope, ComposableCatalog catalog, CompositionConfiguration configuration, IExportProviderFactory exportProviderFactory)
foreach (var errorCollection in configuration.CompositionErrors)
{
_scope = scope;
_catalog = catalog;
_configuration = configuration;
_exportProviderFactory = exportProviderFactory;
}
private ExportProvider GetOrCreateExportProvider()
{
if (!Enabled)
foreach (var error in errorCollection)
{
// The [UseExportProvider] attribute on tests ensures that the pre- and post-conditions of methods
// in this type are met during test conditions.
throw new InvalidOperationException($"{nameof(ExportProviderCache)} may only be used from tests marked with {nameof(UseExportProviderAttribute)}");
}
var expectedCatalog = Interlocked.CompareExchange(ref _scope._expectedCatalog, _catalog, null) ?? _catalog;
RequireForSingleExportProvider(expectedCatalog == _catalog);
var expected = _scope._expectedProviderForCatalog;
if (expected is null)
{
foreach (var errorCollection in _configuration.CompositionErrors)
foreach (var part in error.Parts)
{
foreach (var error in errorCollection)
foreach (var pair in part.SatisfyingExports)
{
foreach (var part in error.Parts)
var (importBinding, exportBindings) = (pair.Key, pair.Value);
if (exportBindings.Count <= 1)
{
foreach (var pair in part.SatisfyingExports)
{
var (importBinding, exportBindings) = (pair.Key, pair.Value);
if (exportBindings.Count <= 1)
{
// Ignore composition errors for missing parts
continue;
}
// Ignore composition errors for missing parts
continue;
}
if (importBinding.ImportDefinition.Cardinality != ImportCardinality.ZeroOrMore)
{
// This failure occurs when a binding fails because multiple exports were
// provided but only a single one (at most) is expected. This typically occurs
// when a test ExportProvider is created with a mock implementation without
// first removing a value provided by default.
throw new InvalidOperationException(
"Failed to construct the MEF catalog for testing. Multiple exports were found for a part for which only one export is expected:" + Environment.NewLine
+ error.Message);
}
}
if (importBinding.ImportDefinition.Cardinality != ImportCardinality.ZeroOrMore)
{
// This failure occurs when a binding fails because multiple exports were
// provided but only a single one (at most) is expected. This typically occurs
// when a test ExportProvider is created with a mock implementation without
// first removing a value provided by default.
throw new InvalidOperationException(
"Failed to construct the MEF catalog for testing. Multiple exports were found for a part for which only one export is expected:" + Environment.NewLine
+ error.Message);
}
}
}
expected = _exportProviderFactory.CreateExportProvider();
expected = Interlocked.CompareExchange(ref _scope._expectedProviderForCatalog, expected, null) ?? expected;
Interlocked.CompareExchange(ref _scope._currentExportProvider, expected, null);
}
var exportProvider = _scope._currentExportProvider;
RequireForSingleExportProvider(exportProvider == expected);
return exportProvider!;
}
ExportProvider IExportProviderFactory.CreateExportProvider()
{
// Currently this implementation deviates from the typical behavior of IExportProviderFactory. For the
// duration of a single test, an instance of SingleExportProviderFactory will continue returning the
// same ExportProvider instance each time this method is called.
//
// It may be clearer to refactor the implementation to only allow one call to CreateExportProvider in
// the context of a single test. https://github.com/dotnet/roslyn/issues/25863
lock (s_lock)
{
return GetOrCreateExportProvider();
}
}
private void RequireForSingleExportProvider(bool condition)
{
if (!condition)
{
// The ExportProvider provides services that act as singleton instances in the context of an
// application (this include cases of multiple exports, where the 'singleton' is the list of all
// exports matching the contract). When reasoning about the behavior of test code, it is valuable to
// know service instances will be used in a consistent manner throughout the execution of a test,
// regardless of whether they are passed as arguments or obtained through requests to the
// ExportProvider.
//
// Restricting a test to a single ExportProvider guarantees that objects that *look* like singletons
// will *behave* like singletons for the duration of the test. Each test is expected to create and
// use its ExportProvider in a consistent manner.
//
// A test that validates remote services is allowed to create a couple of ExportProviders:
// one for local workspace and the other for the remote one.
//
// When this exception is thrown by a test, it typically means one of the following occurred:
//
// * A test failed to pass an ExportProvider via an optional argument to a method, resulting in the
// method attempting to create a default ExportProvider which did not match the one assigned to
// the test.
// * A test attempted to perform multiple test sequences in the context of a single test method,
// rather than break up the test into distinct tests for each case.
// * A test referenced different predefined ExportProvider instances within the context of a test.
// Each test is expected to use the same ExportProvider throughout the test.
throw new InvalidOperationException($"Only one {_scope.Name} {nameof(ExportProvider)} can be created in the context of a single test.");
}
}
}
private sealed class Scope
{
public readonly string Name;
public ExportProvider? _currentExportProvider;
public ComposableCatalog? _expectedCatalog;
public ExportProvider? _expectedProviderForCatalog;
public Scope(string name)
{
Name = name;
}
public void Clear()
{
_currentExportProvider = null;
_expectedCatalog = null;
_expectedProviderForCatalog = null;
}
}

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

@ -3,12 +3,12 @@
using System;
using System.Collections.Generic;
using System.Composition.Hosting.Core;
using System.Composition;
using System.Composition.Hosting.Core;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.VisualStudio.Composition;
using System.Reflection;
using Microsoft.VisualStudio.Composition;
namespace Microsoft.AspNetCore.Razor.Test.Common.Mef;
@ -31,43 +31,55 @@ internal static class ExportProviderExtensions
public override bool TryGetExport(CompositionContract contract, [NotNullWhen(true)] out object? export)
{
var importMany = contract.MetadataConstraints.Contains(new KeyValuePair<string, object>("IsImportMany", true));
var (contractType, metadataType) = GetContractType(contract.ContractType, importMany);
var (contractType, metadataType, isLazy) = GetContractType(contract.ContractType, importMany);
if (metadataType != null)
var method = (metadataType, isLazy) switch
{
var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods()
where method.Name == nameof(ExportProvider.GetExports)
where method.IsGenericMethod && method.GetGenericArguments().Length == 2
where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string)
select method).Single();
var parameterizedMethod = methodInfo.MakeGenericMethod(contractType, metadataType);
export = parameterizedMethod.Invoke(_exportProvider, new[] { contract.ContractName });
Assumes.NotNull(export);
}
else
(not null, true) => GetExportProviderGenericMethod(nameof(ExportProvider.GetExports), contractType, metadataType),
(null, true) => GetExportProviderGenericMethod(nameof(ExportProvider.GetExports), contractType),
(null, false) => GetExportProviderGenericMethod(nameof(ExportProvider.GetExportedValues), contractType),
_ => null
};
if (method is null)
{
var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods()
where method.Name == nameof(ExportProvider.GetExports)
where method.IsGenericMethod && method.GetGenericArguments().Length == 1
where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string)
select method).Single();
var parameterizedMethod = methodInfo.MakeGenericMethod(contractType);
export = parameterizedMethod.Invoke(_exportProvider, new[] { contract.ContractName });
Assumes.NotNull(export);
export = null;
return false;
}
export = method.Invoke(_exportProvider, [contract.ContractName]);
Assumes.NotNull(export);
return true;
static MethodInfo GetExportProviderGenericMethod(string methodName, params Type[] typeArguments)
{
var methodInfo = (from method in typeof(ExportProvider).GetTypeInfo().GetMethods()
where method.Name == methodName
where method.IsGenericMethod && method.GetGenericArguments().Length == typeArguments.Length
where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string)
select method).Single();
return methodInfo.MakeGenericMethod(typeArguments);
}
}
private static (Type exportType, Type? metadataType) GetContractType(Type contractType, bool importMany)
private static (Type exportType, Type? metadataType, bool isLazy) GetContractType(Type contractType, bool importMany)
{
if (importMany && contractType.IsConstructedGenericType)
if (importMany)
{
if (contractType.GetGenericTypeDefinition() == typeof(IList<>)
|| contractType.GetGenericTypeDefinition() == typeof(ICollection<>)
|| contractType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
if (contractType.IsConstructedGenericType)
{
contractType = contractType.GenericTypeArguments[0];
if (contractType.GetGenericTypeDefinition() == typeof(IList<>)
|| contractType.GetGenericTypeDefinition() == typeof(ICollection<>)
|| contractType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
contractType = contractType.GenericTypeArguments[0];
}
}
else if (contractType.IsArray)
{
contractType = contractType.GetElementType().AssumeNotNull();
}
}
@ -75,11 +87,11 @@ internal static class ExportProviderExtensions
{
if (contractType.GetGenericTypeDefinition() == typeof(Lazy<>))
{
return (contractType.GenericTypeArguments[0], null);
return (contractType.GenericTypeArguments[0], null, true);
}
else if (contractType.GetGenericTypeDefinition() == typeof(Lazy<,>))
{
return (contractType.GenericTypeArguments[0], contractType.GenericTypeArguments[1]);
return (contractType.GenericTypeArguments[0], contractType.GenericTypeArguments[1], true);
}
else
{
@ -87,7 +99,7 @@ internal static class ExportProviderExtensions
}
}
throw new NotSupportedException();
return (contractType, null, false);
}
}
}

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

@ -22,8 +22,7 @@ public sealed partial class TestComposition
public static readonly TestComposition Empty = new(
ImmutableHashSet<Assembly>.Empty,
ImmutableHashSet<Type>.Empty,
ImmutableHashSet<Type>.Empty,
scope: null);
ImmutableHashSet<Type>.Empty);
public static readonly TestComposition Roslyn = Empty
.AddAssemblies(MefHostServices.DefaultAssemblies)
@ -102,32 +101,17 @@ public sealed partial class TestComposition
/// </summary>
public readonly ImmutableHashSet<Type> Parts;
/// <summary>
/// The scope in which to create the export provider, or <see langword="null"/> to use the default scope.
/// </summary>
public readonly string? Scope;
private readonly Lazy<IExportProviderFactory> _exportProviderFactory;
private TestComposition(ImmutableHashSet<Assembly> assemblies, ImmutableHashSet<Type> parts, ImmutableHashSet<Type> excludedPartTypes, string? scope)
private TestComposition(ImmutableHashSet<Assembly> assemblies, ImmutableHashSet<Type> parts, ImmutableHashSet<Type> excludedPartTypes)
{
Assemblies = assemblies;
Parts = parts;
ExcludedPartTypes = excludedPartTypes;
Scope = scope;
_exportProviderFactory = new Lazy<IExportProviderFactory>(GetOrCreateFactory);
}
#if false
/// <summary>
/// Returns a new instance of <see cref="HostServices"/> for the composition. This will either be a MEF composition or VS MEF composition host,
/// depending on what layer the composition is for. Editor Features and VS layers use VS MEF composition while anything else uses System.Composition.
/// </summary>
public HostServices GetHostServices()
=> VisualStudioMefHostServices.Create(ExportProviderFactory.CreateExportProvider());
#endif
/// <summary>
/// VS MEF <see cref="ExportProvider"/>.
/// </summary>
@ -145,7 +129,7 @@ public HostServices GetHostServices()
}
}
var newFactory = ExportProviderCache.CreateExportProviderFactory(GetCatalog(), Scope);
var newFactory = ExportProviderCache.CreateExportProviderFactory(GetCatalog());
lock (s_factoryCache)
{
@ -215,7 +199,7 @@ public HostServices GetHostServices()
var testAssembly = assemblies.FirstOrDefault(IsTestAssembly);
Verify.Operation(testAssembly is null, $"Test assemblies are not allowed in test composition: {testAssembly}. Specify explicit test parts instead.");
return new TestComposition(assemblies, Parts, ExcludedPartTypes, Scope);
return new TestComposition(assemblies, Parts, ExcludedPartTypes);
static bool IsTestAssembly(Assembly assembly)
{
@ -230,13 +214,10 @@ public HostServices GetHostServices()
}
public TestComposition WithParts(ImmutableHashSet<Type> parts)
=> parts == Parts ? this : new TestComposition(Assemblies, parts, ExcludedPartTypes, Scope);
=> parts == Parts ? this : new TestComposition(Assemblies, parts, ExcludedPartTypes);
public TestComposition WithExcludedPartTypes(ImmutableHashSet<Type> excludedPartTypes)
=> excludedPartTypes == ExcludedPartTypes ? this : new TestComposition(Assemblies, Parts, excludedPartTypes, Scope);
public TestComposition WithScope(string? scope)
=> scope == Scope ? this : new TestComposition(Assemblies, Parts, ExcludedPartTypes, scope);
=> excludedPartTypes == ExcludedPartTypes ? this : new TestComposition(Assemblies, Parts, excludedPartTypes);
/// <summary>
/// Use for VS MEF composition troubleshooting.

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

@ -1,232 +0,0 @@
// 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.Reflection;
using Microsoft.CodeAnalysis.Host;
using Microsoft.VisualStudio.Composition;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Razor.Test.Common.Mef;
/// <summary>
/// This attribute supports tests that need to use a MEF container (<see cref="ExportProvider"/>) directly or
/// indirectly during the test sequence. It ensures production code uniformly handles the export provider created
/// during a test, and cleans up the state before the test completes.
/// </summary>
/// <remarks>
/// <para>This attribute serves several important functions for tests that use state variables which are otherwise
/// shared at runtime:</para>
/// <list type="bullet">
/// <item>Ensures <see cref="HostServices"/> implementations all use the same <see cref="ExportProvider"/>, which is
/// the one created by the test.</item>
/// <item>Clears static cached values in production code holding instances of <see cref="HostServices"/>, or any
/// object obtained from it or one of its related interfaces such as <see cref="HostLanguageServices"/>.</item>
/// <item>Isolates tests by waiting for asynchronous operations to complete before a test is considered
/// complete.</item>
/// <item>When required, provides a separate <see cref="ExportProvider"/> for the <see cref="T:RemoteWorkspace"/>
/// executing in the test process. If this provider is created during testing, it is cleaned up with the primary
/// export provider during test teardown.</item>
/// </list>
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class UseExportProviderAttribute : BeforeAfterTestAttribute
{
/// <summary>
/// Asynchronous operations are expected to be cancelled at the end of the test that started them. Operations
/// cancelled by the test are cleaned up immediately. The remaining operations are given an opportunity to run
/// to completion. If this timeout is exceeded by the asynchronous operations running after a test completes,
/// the test is failed.
/// </summary>
private static readonly TimeSpan s_cleanupTimeout = TimeSpan.FromMinutes(1);
#if false
private MefHostServices? _hostServices;
#endif
public override void Before(MethodInfo? methodUnderTest)
{
#if false
MefHostServices.TestAccessor.HookServiceCreation(CreateMefHostServices);
// make sure we enable this for all unit tests
AsynchronousOperationListenerProvider.Enable(enable: true, diagnostics: true);
#endif
ExportProviderCache.SetEnabled_OnlyUseExportProviderAttributeCanCall(true);
}
/// <summary>
/// To the extent reasonably possible, this method resets the state of the test environment to the same state as
/// it started, ensuring that tests running in sequence cannot influence the outcome of later tests.
/// </summary>
/// <remarks>
/// <para>The test cleanup runs in two primary steps:</para>
/// <list type="number">
/// <item>Waiting for asynchronous operations started by the test to complete.</item>
/// <item>Disposing of mutable resources created by the test.</item>
/// <item>Clearing static state variables related to the use of MEF during a test.</item>
/// </list>
/// </remarks>
public override void After(MethodInfo? methodUnderTest)
{
try
{
foreach (var exportProvider in ExportProviderCache.ExportProvidersForCleanup)
{
DisposeExportProvider(exportProvider);
}
}
finally
{
#if false
// Replace hooks with ones that always throw exceptions. These hooks detect cases where code executing
// after the end of a test attempts to create an ExportProvider.
MefHostServices.TestAccessor.HookServiceCreation(DenyMefHostServicesCreationBetweenTests);
#endif
// Reset static state variables.
#if false
_hostServices = null;
#endif
ExportProviderCache.SetEnabled_OnlyUseExportProviderAttributeCanCall(false);
}
}
private static void DisposeExportProvider(ExportProvider? exportProvider)
{
if (exportProvider is null)
{
return;
}
// Dispose of the export provider, including calling Dispose for any IDisposable services created during the test.
using var _ = exportProvider;
#if false
if (exportProvider.GetExportedValues<IAsynchronousOperationListenerProvider>().SingleOrDefault() is { } listenerProvider)
{
if (exportProvider.GetExportedValues<IThreadingContext>().SingleOrDefault()?.HasMainThread ?? false)
{
// Immediately clear items from the foreground notification service for which cancellation is
// requested. This service maintains a queue separately from Tasks, and work items scheduled for
// execution after a delay are not immediately purged when cancellation is requested. This code
// instructs the service to walk the list of queued work items and immediately cancel and purge any
// which are already cancelled.
var foregroundNotificationService = exportProvider.GetExportedValues<IForegroundNotificationService>().SingleOrDefault() as ForegroundNotificationService;
foregroundNotificationService?.ReleaseCancelledItems();
}
// Verify the synchronization context was not used incorrectly
var testExportJoinableTaskContext = exportProvider.GetExportedValues<TestExportJoinableTaskContext>().SingleOrDefault();
var denyExecutionSynchronizationContext = testExportJoinableTaskContext?.SynchronizationContext as TestExportJoinableTaskContext.DenyExecutionSynchronizationContext;
// Join remaining operations with a timeout
using (var timeoutTokenSource = new CancellationTokenSource(s_cleanupTimeout))
{
if (denyExecutionSynchronizationContext is object)
{
// Immediately cancel the test if the synchronization context is improperly used
denyExecutionSynchronizationContext.InvalidSwitch += delegate { timeoutTokenSource.CancelAfter(0); };
denyExecutionSynchronizationContext.ThrowIfSwitchOccurred();
}
try
{
// This attribute cleans up the in-process and out-of-process export providers separately, so we
// don't need to provide a workspace when waiting for operations to complete.
var waiter = ((AsynchronousOperationListenerProvider)listenerProvider).WaitAllDispatcherOperationAndTasksAsync(workspace: null);
waiter.JoinUsingDispatcher(timeoutTokenSource.Token);
}
catch (OperationCanceledException ex) when (timeoutTokenSource.IsCancellationRequested)
{
// If the failure was caused by an invalid thread change, throw that exception
denyExecutionSynchronizationContext?.ThrowIfSwitchOccurred();
var messageBuilder = new StringBuilder("Failed to clean up listeners in a timely manner.");
foreach (var token in ((AsynchronousOperationListenerProvider)listenerProvider).GetTokens())
{
messageBuilder.AppendLine().Append($" {token}");
}
throw new TimeoutException(messageBuilder.ToString(), ex);
}
}
denyExecutionSynchronizationContext?.ThrowIfSwitchOccurred();
foreach (var testErrorHandler in exportProvider.GetExportedValues<ITestErrorHandler>())
{
var exceptions = testErrorHandler.Exceptions;
if (exceptions.Count > 0)
{
throw new AggregateException("Tests threw unexpected exceptions", exceptions);
}
}
}
#endif
}
#if false
private MefHostServices CreateMefHostServices(IEnumerable<Assembly> assemblies)
{
ExportProvider exportProvider;
if (assemblies is ImmutableArray<Assembly> array &&
array == MefHostServices.DefaultAssemblies &&
ExportProviderCache.LocalExportProviderForCleanup != null)
{
if (_hostServices != null)
{
return _hostServices;
}
exportProvider = ExportProviderCache.LocalExportProviderForCleanup;
}
else
{
exportProvider = ExportProviderCache.GetOrCreateExportProviderFactory(assemblies).CreateExportProvider();
}
Interlocked.CompareExchange(
ref _hostServices,
new ExportProviderMefHostServices(exportProvider),
null);
return _hostServices;
}
private static MefHostServices DenyMefHostServicesCreationBetweenTests(IEnumerable<Assembly> assemblies)
{
// If you hit this, one of three situations occurred:
//
// 1. A test method that uses ExportProvider is not marked with UseExportProviderAttribute (can also be
// applied to the containing type or a base type.
// 2. A test attempted to create an ExportProvider during the test cleanup operations after the
// ExportProvider was already disposed.
// 3. A test attempted to use an ExportProvider in the constructor of the test, or during the initialization
// of a field in the test class.
throw new InvalidOperationException("Cannot create host services after test tear down.");
}
private class ExportProviderMefHostServices : MefHostServices, IMefHostExportProvider
{
private readonly VisualStudioMefHostServices _vsHostServices;
public ExportProviderMefHostServices(ExportProvider exportProvider)
: base(new ContainerConfiguration().CreateContainer())
{
_vsHostServices = VisualStudioMefHostServices.Create(exportProvider);
}
protected internal override HostWorkspaceServices CreateWorkspaceServices(Workspace workspace)
=> _vsHostServices.CreateWorkspaceServices(workspace);
IEnumerable<Lazy<TExtension, TMetadata>> IMefHostExportProvider.GetExports<TExtension, TMetadata>()
=> _vsHostServices.GetExports<TExtension, TMetadata>();
IEnumerable<Lazy<TExtension>> IMefHostExportProvider.GetExports<TExtension>()
=> _vsHostServices.GetExports<TExtension>();
}
#endif
}

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

@ -89,10 +89,6 @@
<InternalsVisibleTo Include="rzls" Key="$(RazorKey)" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\Benchmarking\" />
</ItemGroup>
<Import Condition="'$(TargetFramework)' != '$(DefaultNetFxTargetFramework)'" Project="..\..\..\Shared\Microsoft.AspNetCore.Razor.Serialization.Json\Microsoft.AspNetCore.Razor.Serialization.Json.projitems" Label="Shared" />
</Project>

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

@ -47,6 +47,9 @@ internal readonly struct TestCode
public ImmutableArray<TextSpan> Spans
=> GetNamedSpans(string.Empty);
public ImmutableDictionary<string, ImmutableArray<TextSpan>> NamedSpans
=> _nameToSpanMap;
public ImmutableArray<TextSpan> GetNamedSpans(string name)
=> _nameToSpanMap[name];

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

@ -2,8 +2,17 @@
// 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.IO;
using System.Linq;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Composition;
namespace Microsoft.AspNetCore.Razor.Test.Common.Workspaces;
@ -14,6 +23,17 @@ public static class TestWorkspace
public static Workspace Create(Action<AdhocWorkspace>? configure = null)
=> Create(services: null, configure: configure);
public static AdhocWorkspace CreateWithDiagnosticAnalyzers(ExportProvider exportProvider)
{
var hostServices = MefHostServices.Create(exportProvider.AsCompositionContext());
var workspace = Create(hostServices);
AddAnalyzersToWorkspace(workspace, exportProvider);
return workspace;
}
public static AdhocWorkspace Create(HostServices? services, Action<AdhocWorkspace>? configure = null)
{
lock (s_workspaceLock)
@ -27,4 +47,24 @@ public static class TestWorkspace
return workspace;
}
}
private static void AddAnalyzersToWorkspace(Workspace workspace, ExportProvider exportProvider)
{
var analyzerLoader = RazorTestAnalyzerLoader.CreateAnalyzerAssemblyLoader();
var analyzerPaths = new DirectoryInfo(AppContext.BaseDirectory).GetFiles("*.dll")
.Where(f => f.Name.StartsWith("Microsoft.CodeAnalysis.", StringComparison.Ordinal) && !f.Name.Contains("LanguageServer") && !f.Name.Contains("Test.Utilities"))
.Select(f => f.FullName)
.ToImmutableArray();
var references = new List<AnalyzerFileReference>();
foreach (var analyzerPath in analyzerPaths)
{
if (File.Exists(analyzerPath))
{
references.Add(new AnalyzerFileReference(analyzerPath, analyzerLoader));
}
}
workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences(references));
}
}

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

@ -5,7 +5,6 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.Formatting;
@ -19,8 +18,9 @@ using Xunit.Abstractions;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
[UseExportProvider]
public class CohostDocumentFormattingEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper)
[Collection(HtmlFormattingCollection.Name)]
public class CohostDocumentFormattingEndpointTest(HtmlFormattingFixture htmlFormattingFixture, ITestOutputHelper testOutputHelper)
: CohostEndpointTestBase(testOutputHelper)
{
// All of the formatting tests in the language server exercise the formatting engine and cover various edge cases
// and provide regression prevention. The tests here are not exhaustive, but they validate the the cohost endpoints
@ -108,7 +108,7 @@ public class CohostDocumentFormattingEndpointTest(ITestOutputHelper testOutputHe
private async Task VerifyDocumentFormattingAsync(string input, string expected)
{
var document = CreateProjectAndRazorDocument(input);
var document = await CreateProjectAndRazorDocumentAsync(input);
var inputText = await document.GetTextAsync(DisposalToken);
var htmlDocumentPublisher = new HtmlDocumentPublisher(RemoteServiceInvoker, StrictMock.Of<TrackingLSPDocumentManager>(), StrictMock.Of<JoinableTaskContext>(), LoggerFactory);
@ -116,7 +116,7 @@ public class CohostDocumentFormattingEndpointTest(ITestOutputHelper testOutputHe
Assert.NotNull(generatedHtml);
var uri = new Uri(document.CreateUri(), $"{document.FilePath}{FeatureOptions.HtmlVirtualDocumentSuffix}");
var htmlEdits = await HtmlFormatting.GetDocumentFormattingEditsAsync(LoggerFactory, uri, generatedHtml, insertSpaces: true, tabSize: 4);
var htmlEdits = await htmlFormattingFixture.Service.GetDocumentFormattingEditsAsync(LoggerFactory, uri, generatedHtml, insertSpaces: true, tabSize: 4);
var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentFormattingName, htmlEdits)]);

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

@ -147,7 +147,7 @@ public class CohostDocumentHighlightEndpointTest(ITestOutputHelper testOutputHel
private async Task VerifyDocumentHighlightsAsync(string input, DocumentHighlight[]? htmlResponse = null)
{
TestFileMarkupParser.GetPositionAndSpans(input, out var source, out int cursorPosition, out ImmutableArray<TextSpan> spans);
var document = CreateProjectAndRazorDocument(source);
var document = await CreateProjectAndRazorDocumentAsync(source);
var inputText = await document.GetTextAsync(DisposalToken);
var position = inputText.GetPosition(cursorPosition);

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

@ -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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
public class CohostDocumentPullDiagnosticsTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper)
{
[Fact]
public Task CSharp()
=> VerifyDiagnosticsAsync("""
<div></div>
@code
{
public void IJustMetYou()
{
{|CS0103:CallMeMaybe|}();
}
}
""");
[Fact]
public Task Razor()
=> VerifyDiagnosticsAsync("""
<div>
{|RZ10012:<NonExistentComponent />|}
</div>
""");
[Fact]
public Task Html()
{
TestCode input = """
<div>
{|HTM1337:<not_a_tag />|}
</div>
""";
return VerifyDiagnosticsAsync(input,
htmlResponse: [new VSInternalDiagnosticReport
{
Diagnostics =
[
new Diagnostic
{
Code = "HTM1337",
Range = SourceText.From(input.Text).GetRange(input.NamedSpans.First().Value.First())
}
]
}]);
}
[Fact]
public Task CombinedAndNestedDiagnostics()
=> VerifyDiagnosticsAsync("""
@using System.Threading.Tasks;
<div>
{|RZ10012:<NonExistentComponent />|}
@code
{
public void IJustMetYou()
{
{|CS0103:CallMeMaybe|}();
}
}
<div>
@{
{|CS4033:await Task.{|CS1501:Delay|}()|};
}
{|RZ9980:<p>|}
</div>
</div>
""");
private async Task VerifyDiagnosticsAsync(TestCode input, VSInternalDiagnosticReport[]? htmlResponse = null)
{
var document = await CreateProjectAndRazorDocumentAsync(input.Text, createSeparateRemoteAndLocalWorkspaces: true);
var inputText = await document.GetTextAsync(DisposalToken);
var requestInvoker = new TestLSPRequestInvoker([(VSInternalMethods.DocumentPullDiagnosticName, htmlResponse)]);
var endpoint = new CohostDocumentPullDiagnosticsEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker, FilePathService, LoggerFactory);
var result = await endpoint.GetTestAccessor().HandleRequestAsync(document, DisposalToken);
var markers = result!.SelectMany(d => d.Diagnostics.AssumeNotNull()).SelectMany(d =>
new[] {
(index: inputText.GetTextSpan(d.Range).Start, text: $"{{|{d.Code!.Value.Second}:"),
(index: inputText.GetTextSpan(d.Range).End, text:"|}")
});
var testOutput = input.Text;
foreach (var (index, text) in markers.OrderByDescending(i => i.index))
{
testOutput = testOutput.Insert(index, text);
}
AssertEx.EqualOrDiff(input.OriginalInput, testOutput);
}
}

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

@ -57,7 +57,7 @@ public class CohostDocumentSpellCheckEndpointTest(ITestOutputHelper testOutputHe
private async Task VerifySemanticTokensAsync(TestCode input)
{
var document = CreateProjectAndRazorDocument(input.Text);
var document = await CreateProjectAndRazorDocumentAsync(input.Text);
var sourceText = await document.GetTextAsync(DisposalToken);
var endpoint = new CohostDocumentSpellCheckEndpoint(RemoteServiceInvoker);

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

@ -69,7 +69,7 @@ public class CohostDocumentSymbolEndpointTest(ITestOutputHelper testOutput) : Co
private async Task VerifyDocumentSymbolsAsync(string input, bool hierarchical = false)
{
TestFileMarkupParser.GetSpans(input, out input, out ImmutableDictionary<string, ImmutableArray<TextSpan>> spansDict);
var document = CreateProjectAndRazorDocument(input);
var document = await CreateProjectAndRazorDocumentAsync(input);
var endpoint = new CohostDocumentSymbolEndpoint(RemoteServiceInvoker);

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

@ -9,10 +9,13 @@ using Basic.Reference.Assemblies;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.AspNetCore.Razor.Test.Common.Workspaces;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Remote.Razor;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Composition;
using Xunit.Abstractions;
@ -68,7 +71,7 @@ public abstract class CohostEndpointTestBase(ITestOutputHelper testOutputHelper)
FeatureOptions.SetOptions(_clientInitializationOptions);
}
protected TextDocument CreateProjectAndRazorDocument(string contents, string? fileKind = null, (string fileName, string contents)[]? additionalFiles = null)
protected Task<TextDocument> CreateProjectAndRazorDocumentAsync(string contents, string? fileKind = null, (string fileName, string contents)[]? additionalFiles = null, bool createSeparateRemoteAndLocalWorkspaces = false)
{
// Using IsLegacy means null == component, so easier for test authors
var isComponent = !FileKinds.IsLegacy(fileKind);
@ -82,6 +85,56 @@ public abstract class CohostEndpointTestBase(ITestOutputHelper testOutputHelper)
var projectId = ProjectId.CreateNewId(debugName: projectName);
var documentId = DocumentId.CreateNewId(projectId, debugName: documentFilePath);
var remoteWorkspace = RemoteWorkspaceAccessor.GetWorkspace();
var remoteDocument = CreateProjectAndRazorDocument(remoteWorkspace, projectId, projectName, documentId, documentFilePath, contents, additionalFiles);
if (createSeparateRemoteAndLocalWorkspaces)
{
// Usually its fine to just use the remote workspace, but sometimes we need to also have things available in the
// "devenv" side of Roslyn, which is a different workspace with a different set of services. We don't have any
// actual solution syncing set up for testing, and don't really use a service broker, but since we also would
// expect to never make changes to a workspace, it should be fine to simply create duplicated solutions as part
// of test setup.
return CreateLocalProjectAndRazorDocumentAsync(remoteDocument.Project.Solution, projectId, projectName, documentId, documentFilePath, contents, additionalFiles);
}
// If we're just creating one workspace, then its the remote one and we just return the remote document
// and assume that the endpoint under test doesn't need to do anything on the devenv side. This makes it
// easier for tests to mutate solutions
return Task.FromResult(remoteDocument);
}
private async Task<TextDocument> CreateLocalProjectAndRazorDocumentAsync(Solution remoteSolution, ProjectId projectId, string projectName, DocumentId documentId, string documentFilePath, string contents, (string fileName, string contents)[]? additionalFiles)
{
var exportProvider = TestComposition.Roslyn.ExportProviderFactory.CreateExportProvider();
AddDisposable(exportProvider);
var workspace = TestWorkspace.CreateWithDiagnosticAnalyzers(exportProvider);
AddDisposable(workspace);
var razorDocument = CreateProjectAndRazorDocument(workspace, projectId, projectName, documentId, documentFilePath, contents, additionalFiles);
// Until the source generator is hooked up, the workspace representing "local" projects doesn't have anything
// to actually compile the Razor to C#, so we just do it now at creation
var solution = razorDocument.Project.Solution;
// We're cheating a bit here and using the remote export provider to get something to do the compilation
var factory = _exportProvider.AssumeNotNull().GetExportedValue<DocumentSnapshotFactory>();
var snapshot = factory.GetOrCreate(razorDocument);
// Compile the Razor file
var codeDocument = await snapshot.GetGeneratedOutputAsync(false);
// Update the generated doc contents
var generatedDocumentIds = solution.GetDocumentIdsWithFilePath(documentFilePath + CSharpVirtualDocumentSuffix);
solution = solution.WithDocumentText(generatedDocumentIds, codeDocument.GetCSharpSourceText());
razorDocument = solution.GetAdditionalDocument(documentId).AssumeNotNull();
// If we're creating remote and local workspaces, then we'll return the local document, and have to allow
// the remote service invoker to map from the local solution to the remote one.
RemoteServiceInvoker.MapSolutionIdToRemote(razorDocument.Project.Solution.Id, remoteSolution);
return razorDocument;
}
private static TextDocument CreateProjectAndRazorDocument(CodeAnalysis.Workspace workspace, ProjectId projectId, string projectName, DocumentId documentId, string documentFilePath, string contents, (string fileName, string contents)[]? additionalFiles)
{
var projectInfo = ProjectInfo
.Create(
projectId,
@ -93,9 +146,6 @@ public abstract class CohostEndpointTestBase(ITestOutputHelper testOutputHelper)
.WithDefaultNamespace(TestProjectData.SomeProject.RootNamespace)
.WithMetadataReferences(AspNet80.ReferenceInfos.All.Select(r => r.Reference));
// Importantly, we use Roslyn's remote workspace here so that when our OOP services call into Roslyn, their code
// will be able to access their services.
var workspace = RemoteWorkspaceAccessor.GetWorkspace();
var solution = workspace.CurrentSolution.AddProject(projectInfo);
solution = solution

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

@ -7,7 +7,6 @@ using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
@ -218,7 +217,7 @@ public class CohostFoldingRangeEndpointTest(ITestOutputHelper testOutputHelper)
private async Task VerifyFoldingRangesAsync(string input, string? fileKind = null)
{
TestFileMarkupParser.GetSpans(input, out var source, out ImmutableDictionary<string, ImmutableArray<TextSpan>> spans);
var document = CreateProjectAndRazorDocument(source, fileKind);
var document = await CreateProjectAndRazorDocumentAsync(source, fileKind);
var inputText = await document.GetTextAsync(DisposalToken);
var htmlSpans = spans.GetValueOrDefault("html").NullToEmpty();

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

@ -8,7 +8,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Remote.Razor;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Xunit;
@ -313,7 +312,7 @@ public class CohostGoToDefinitionEndpointTest(ITestOutputHelper testOutputHelper
</script>
""";
var document = CreateProjectAndRazorDocument(input.Text);
var document = await CreateProjectAndRazorDocumentAsync(input.Text);
var inputText = await document.GetTextAsync(DisposalToken);
var htmlResponse = new SumType<Location, Location[], DocumentLink[]>?(new Location[]
@ -333,7 +332,7 @@ public class CohostGoToDefinitionEndpointTest(ITestOutputHelper testOutputHelper
private async Task VerifyGoToDefinitionAsync(TestCode input, string? fileKind = null, SumType<Location, Location[], DocumentLink[]>? htmlResponse = null)
{
var document = CreateProjectAndRazorDocument(input.Text, fileKind);
var document = await CreateProjectAndRazorDocumentAsync(input.Text, fileKind);
var result = await GetGoToDefinitionResultAsync(document, input, htmlResponse);
Assumes.NotNull(result);
@ -349,11 +348,11 @@ public class CohostGoToDefinitionEndpointTest(ITestOutputHelper testOutputHelper
Assert.Equal(document.CreateUri(), location.Uri);
}
private Task<SumType<RoslynLocation, RoslynLocation[], RoslynDocumentLink[]>?> GetGoToDefinitionResultAsync(
private async Task<SumType<RoslynLocation, RoslynLocation[], RoslynDocumentLink[]>?> GetGoToDefinitionResultAsync(
TestCode input, string? fileKind = null, params (string fileName, string contents)[]? additionalFiles)
{
var document = CreateProjectAndRazorDocument(input.Text, fileKind, additionalFiles);
return GetGoToDefinitionResultAsync(document, input, htmlResponse: null);
var document = await CreateProjectAndRazorDocumentAsync(input.Text, fileKind, additionalFiles);
return await GetGoToDefinitionResultAsync(document, input, htmlResponse: null);
}
private async Task<SumType<RoslynLocation, RoslynLocation[], RoslynDocumentLink[]>?> GetGoToDefinitionResultAsync(

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

@ -100,7 +100,7 @@ public class CohostGoToImplementationEndpointTest(ITestOutputHelper testOutputHe
</script>
""";
var document = CreateProjectAndRazorDocument(input.Text);
var document = await CreateProjectAndRazorDocumentAsync(input.Text);
var inputText = await document.GetTextAsync(DisposalToken);
var htmlResponse = new SumType<LspLocation[], VSInternalReferenceItem[]>?(new LspLocation[]
@ -119,7 +119,7 @@ public class CohostGoToImplementationEndpointTest(ITestOutputHelper testOutputHe
private async Task VerifyCSharpGoToImplementationAsync(TestCode input)
{
var document = CreateProjectAndRazorDocument(input.Text);
var document = await CreateProjectAndRazorDocumentAsync(input.Text);
var requestInvoker = new TestLSPRequestInvoker();

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

@ -131,7 +131,7 @@ public class CohostInlayHintEndpointTest(ITestOutputHelper testOutputHelper) : C
private async Task VerifyInlayHintsAsync(string input, Dictionary<string, string> toolTipMap, string output, bool displayAllOverride = false)
{
TestFileMarkupParser.GetSpans(input, out input, out ImmutableDictionary<string, ImmutableArray<TextSpan>> spansDict);
var document = CreateProjectAndRazorDocument(input);
var document = await CreateProjectAndRazorDocumentAsync(input);
var inputText = await document.GetTextAsync(DisposalToken);
var endpoint = new CohostInlayHintEndpoint(RemoteServiceInvoker);

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

@ -159,7 +159,7 @@ public class CohostLinkedEditingRangeEndpointTest(ITestOutputHelper testOutputHe
private async Task VerifyLinkedEditingRangeAsync(string input)
{
TestFileMarkupParser.GetPositionAndSpans(input, out input, out int cursorPosition, out ImmutableArray<TextSpan> spans);
var document = CreateProjectAndRazorDocument(input);
var document = await CreateProjectAndRazorDocumentAsync(input);
var sourceText = await document.GetTextAsync(DisposalToken);
var endpoint = new CohostLinkedEditingRangeEndpoint(RemoteServiceInvoker);

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

@ -196,7 +196,7 @@ public class CohostOnAutoInsertEndpointTest(ITestOutputHelper testOutputHelper)
bool formatOnType = true,
bool autoClosingTags = true)
{
var document = CreateProjectAndRazorDocument(input.Text);
var document = await CreateProjectAndRazorDocumentAsync(input.Text);
var sourceText = await document.GetTextAsync(DisposalToken);
var clientSettingsManager = new ClientSettingsManager([], null, null);

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

@ -5,7 +5,6 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.Formatting;
@ -19,8 +18,9 @@ using Xunit.Abstractions;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
[UseExportProvider]
public class CohostOnTypeFormattingEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper)
[Collection(HtmlFormattingCollection.Name)]
public class CohostOnTypeFormattingEndpointTest(HtmlFormattingFixture htmlFormattingFixture, ITestOutputHelper testOutputHelper)
: CohostEndpointTestBase(testOutputHelper)
{
[Fact]
public async Task InvalidTrigger()
@ -103,7 +103,7 @@ public class CohostOnTypeFormattingEndpointTest(ITestOutputHelper testOutputHelp
private async Task VerifyOnTypeFormattingAsync(TestCode input, string expected, char triggerCharacter, bool html = false)
{
var document = CreateProjectAndRazorDocument(input.Text);
var document = await CreateProjectAndRazorDocumentAsync(input.Text);
var inputText = await document.GetTextAsync(DisposalToken);
var position = inputText.GetPosition(input.Position);
@ -115,7 +115,7 @@ public class CohostOnTypeFormattingEndpointTest(ITestOutputHelper testOutputHelp
Assert.NotNull(generatedHtml);
var uri = new Uri(document.CreateUri(), $"{document.FilePath}{FeatureOptions.HtmlVirtualDocumentSuffix}");
var htmlEdits = await HtmlFormatting.GetOnTypeFormattingEditsAsync(LoggerFactory, uri, generatedHtml, position, insertSpaces: true, tabSize: 4);
var htmlEdits = await htmlFormattingFixture.Service.GetOnTypeFormattingEditsAsync(LoggerFactory, uri, generatedHtml, position, insertSpaces: true, tabSize: 4);
requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentOnTypeFormattingName, htmlEdits)]);
}

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

@ -5,7 +5,6 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.Formatting;
@ -19,8 +18,9 @@ using Xunit.Abstractions;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
[UseExportProvider]
public class CohostRangeFormattingEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper)
[Collection(HtmlFormattingCollection.Name)]
public class CohostRangeFormattingEndpointTest(HtmlFormattingFixture htmlFormattingFixture, ITestOutputHelper testOutputHelper)
: CohostEndpointTestBase(testOutputHelper)
{
[Fact]
public Task RangeFormatting()
@ -102,7 +102,7 @@ public class CohostRangeFormattingEndpointTest(ITestOutputHelper testOutputHelpe
private async Task VerifyRangeFormattingAsync(TestCode input, string expected)
{
var document = CreateProjectAndRazorDocument(input.Text);
var document = await CreateProjectAndRazorDocumentAsync(input.Text);
var inputText = await document.GetTextAsync(DisposalToken);
var htmlDocumentPublisher = new HtmlDocumentPublisher(RemoteServiceInvoker, StrictMock.Of<TrackingLSPDocumentManager>(), StrictMock.Of<JoinableTaskContext>(), LoggerFactory);
@ -110,7 +110,7 @@ public class CohostRangeFormattingEndpointTest(ITestOutputHelper testOutputHelpe
Assert.NotNull(generatedHtml);
var uri = new Uri(document.CreateUri(), $"{document.FilePath}{FeatureOptions.HtmlVirtualDocumentSuffix}");
var htmlEdits = await HtmlFormatting.GetDocumentFormattingEditsAsync(LoggerFactory, uri, generatedHtml, insertSpaces: true, tabSize: 4);
var htmlEdits = await htmlFormattingFixture.Service.GetDocumentFormattingEditsAsync(LoggerFactory, uri, generatedHtml, insertSpaces: true, tabSize: 4);
var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentFormattingName, htmlEdits)]);

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

@ -198,7 +198,7 @@ public class CohostRenameEndpointTest(ITestOutputHelper testOutputHelper) : Coho
private async Task VerifyRenamesAsync(string input, string newName, string expected, string? fileKind = null, (string fileName, string contents)[]? additionalFiles = null, (string oldName, string newName)[]? renames = null)
{
TestFileMarkupParser.GetPosition(input, out var source, out var cursorPosition);
var document = CreateProjectAndRazorDocument(source, fileKind, additionalFiles);
var document = await CreateProjectAndRazorDocumentAsync(source, fileKind, additionalFiles);
var inputText = await document.GetTextAsync(DisposalToken);
var position = inputText.GetPosition(cursorPosition);

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

@ -90,7 +90,7 @@ public class CohostSemanticTokensRangeEndpointTest(ITestOutputHelper testOutputH
private async Task VerifySemanticTokensAsync(string input, bool colorBackground, bool precise, string? fileKind = null, [CallerMemberName] string? testName = null)
{
var document = CreateProjectAndRazorDocument(input, fileKind);
var document = await CreateProjectAndRazorDocumentAsync(input, fileKind);
var sourceText = await document.GetTextAsync(DisposalToken);
var legend = TestRazorSemanticTokensLegendService.Instance;

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

@ -91,7 +91,7 @@ public class CohostSignatureHelpEndpointTest(ITestOutputHelper testOutputHelper)
private async Task VerifySignatureHelpAsync(string input, string expected, bool autoListParams = true, SignatureHelpTriggerKind? triggerKind = null)
{
TestFileMarkupParser.GetPosition(input, out input, out var cursorPosition);
var document = CreateProjectAndRazorDocument(input);
var document = await CreateProjectAndRazorDocumentAsync(input);
var sourceText = await document.GetTextAsync(DisposalToken);
var clientSettingsManager = new ClientSettingsManager([], null, null);

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

@ -55,7 +55,7 @@ public class CohostTextPresentationEndpointTest(ITestOutputHelper testOutputHelp
private async Task VerifyUriPresentationAsync(string input, string text, string? expected, WorkspaceEdit? htmlResponse = null)
{
TestFileMarkupParser.GetSpan(input, out input, out var span);
var document = CreateProjectAndRazorDocument(input);
var document = await CreateProjectAndRazorDocumentAsync(input);
var sourceText = await document.GetTextAsync(DisposalToken);
var requestInvoker = new TestLSPRequestInvoker([(VSInternalMethods.TextDocumentTextPresentationName, htmlResponse)]);

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

@ -286,7 +286,7 @@ public class CohostUriPresentationEndpointTest(ITestOutputHelper testOutputHelpe
private async Task VerifyUriPresentationAsync(string input, Uri[] uris, string? expected, WorkspaceEdit? htmlResponse = null, (string fileName, string contents)[]? additionalFiles = null)
{
TestFileMarkupParser.GetSpan(input, out input, out var span);
var document = CreateProjectAndRazorDocument(input, additionalFiles: additionalFiles);
var document = await CreateProjectAndRazorDocumentAsync(input, additionalFiles: additionalFiles);
var sourceText = await document.GetTextAsync(DisposalToken);
var requestInvoker = new TestLSPRequestInvoker([(VSInternalMethods.TextDocumentUriPresentationName, htmlResponse)]);

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

@ -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.CodeAnalysis.Razor.Formatting;
using Xunit;
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
[CollectionDefinition(Name)]
public class HtmlFormattingCollection : ICollectionFixture<HtmlFormattingFixture>
{
public const string Name = nameof(HtmlFormattingCollection);
}

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

@ -76,7 +76,7 @@ public class RazorComponentDefinitionServiceTest(ITestOutputHelper testOutputHel
private async Task VerifyDefinitionAsync(TestCode input, TestCode expectedDocument, params (string fileName, string contents)[]? additionalFiles)
{
var document = CreateProjectAndRazorDocument(input.Text, FileKinds.Component, additionalFiles);
var document = await CreateProjectAndRazorDocumentAsync(input.Text, FileKinds.Component, additionalFiles);
var service = OOPExportProvider.GetExportedValue<IRazorComponentDefinitionService>();
var documentSnapshotFactory = OOPExportProvider.GetExportedValue<DocumentSnapshotFactory>();

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

@ -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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
@ -14,6 +15,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
internal sealed class TestBrokeredServiceInterceptor : IRazorBrokeredServiceInterceptor
{
private readonly TestSolutionStore _solutionStore = new();
private readonly Dictionary<SolutionId, Solution> _localToRemoteSolutionMap = [];
public Task<RazorPinnedSolutionInfoWrapper> GetSolutionInfoAsync(Solution solution, CancellationToken cancellationToken)
=> _solutionStore.AddAsync(solution, cancellationToken);
@ -32,6 +34,18 @@ internal sealed class TestBrokeredServiceInterceptor : IRazorBrokeredServiceInte
Assert.NotNull(solution);
// Rather than actually syncing assets, we just let the test author directly map from a local solution
// to a remote solution;
if (_localToRemoteSolutionMap.TryGetValue(solution.Id, out var remoteSolution))
{
solution = remoteSolution;
}
return implementation(solution);
}
internal void MapSolutionIdToRemote(SolutionId localSolutionId, Solution remoteSolution)
{
_localToRemoteSolutionMap.Add(localSolutionId, remoteSolution);
}
}

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

@ -57,6 +57,11 @@ internal sealed class TestRemoteServiceInvoker(
return await invocation(service, solutionInfo, cancellationToken);
}
public void MapSolutionIdToRemote(SolutionId localSolutionId, Solution remoteSolution)
{
_serviceInterceptor.MapSolutionIdToRemote(localSolutionId, remoteSolution);
}
public void Dispose()
{
_reentrantSemaphore.Dispose();

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

@ -111,7 +111,8 @@ internal class TestLSPRequestInvoker : LSPRequestInvoker
return new ReinvocationResponse<TOut>(languageClientName: RazorLSPConstants.RazorCSharpLanguageServerName, result);
}
if (_htmlResponses.TryGetValue(method, out var response))
if (_htmlResponses is not null &&
_htmlResponses.TryGetValue(method, out var response))
{
return new ReinvocationResponse<TOut>(languageClientName: "html", (TOut)response);
}