зеркало из https://github.com/dotnet/razor.git
Create new project to allow hosting the RazorProjectHost with a Roslyn workspace and minimal dependencies
This is the first part of a larger simplification, but wanted to limit the code moves for now.
This commit is contained in:
Родитель
7e7d499920
Коммит
c2af8ff1c8
20
Razor.sln
20
Razor.sln
|
@ -172,6 +172,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.ExternalAccess.OmniSharp", "src\Razor\src\Microsoft.AspNetCore.Razor.ExternalAccess.OmniSharp\Microsoft.AspNetCore.Razor.ExternalAccess.OmniSharp.csproj", "{3E2B6DF5-524F-4909-8A66-7F8C6383620A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace", "src\Razor\src\Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace\Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.csproj", "{2223B8FD-D98A-47BE-94A9-6A3A6B8557B8}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.ProjectEngineHost", "src\Razor\src\Microsoft.AspNetCore.Razor.ProjectEngineHost\Microsoft.AspNetCore.Razor.ProjectEngineHost.csproj", "{2FB4801C-A083-4F08-A4FB-C4910985DE31}"
|
||||
EndProject
|
||||
Global
|
||||
|
@ -718,6 +720,22 @@ Global
|
|||
{3E2B6DF5-524F-4909-8A66-7F8C6383620A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3E2B6DF5-524F-4909-8A66-7F8C6383620A}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3E2B6DF5-524F-4909-8A66-7F8C6383620A}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{2223B8FD-D98A-47BE-94A9-6A3A6B8557B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2223B8FD-D98A-47BE-94A9-6A3A6B8557B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2223B8FD-D98A-47BE-94A9-6A3A6B8557B8}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2223B8FD-D98A-47BE-94A9-6A3A6B8557B8}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2223B8FD-D98A-47BE-94A9-6A3A6B8557B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2223B8FD-D98A-47BE-94A9-6A3A6B8557B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2223B8FD-D98A-47BE-94A9-6A3A6B8557B8}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2223B8FD-D98A-47BE-94A9-6A3A6B8557B8}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{2FB4801C-A083-4F08-A4FB-C4910985DE31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2FB4801C-A083-4F08-A4FB-C4910985DE31}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2FB4801C-A083-4F08-A4FB-C4910985DE31}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2FB4801C-A083-4F08-A4FB-C4910985DE31}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2FB4801C-A083-4F08-A4FB-C4910985DE31}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2FB4801C-A083-4F08-A4FB-C4910985DE31}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2FB4801C-A083-4F08-A4FB-C4910985DE31}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2FB4801C-A083-4F08-A4FB-C4910985DE31}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -797,6 +815,8 @@ Global
|
|||
{A9F9B5E5-C5C2-4860-BE56-038C70ADBAC9} = {FB7C870E-A173-4F75-BE63-4EF39C79A759}
|
||||
{7400A168-2552-49C7-93E3-D4DAA90C216F} = {C2C98051-0F39-47F2-80B6-E72B29159F2C}
|
||||
{3E2B6DF5-524F-4909-8A66-7F8C6383620A} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
|
||||
{2223B8FD-D98A-47BE-94A9-6A3A6B8557B8} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
|
||||
{2FB4801C-A083-4F08-A4FB-C4910985DE31} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {0035341D-175A-4D05-95E6-F1C2785A1E26}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace;
|
||||
|
||||
// Copied from https://github.com/dotnet/project-system/blob/e4db47666e0a49f6c38e701f8630dbc31380fb64/src/Microsoft.VisualStudio.ProjectSystem.Managed/Threading/Tasks/CancellationSeries.cs
|
||||
|
||||
internal sealed class CancellationSeries : IDisposable
|
||||
{
|
||||
private CancellationTokenSource? _cts = new();
|
||||
|
||||
private readonly CancellationToken _superToken;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="CancellationSeries"/>.
|
||||
/// </summary>
|
||||
/// <param name="token">An optional cancellation token that, when cancelled, cancels the last
|
||||
/// issued token and causes any subsequent tokens to be issued in a cancelled state.</param>
|
||||
public CancellationSeries(CancellationToken token)
|
||||
{
|
||||
_superToken = token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the next <see cref="CancellationToken"/> in the series, ensuring the last issued
|
||||
/// token (if any) is cancelled first.
|
||||
/// </summary>
|
||||
/// <param name="token">An optional cancellation token that, when cancelled, cancels the
|
||||
/// returned token.</param>
|
||||
/// <returns>
|
||||
/// A cancellation token that will be cancelled when either:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="CreateNext"/> is called again</item>
|
||||
/// <item>The token passed to this method (if any) is cancelled</item>
|
||||
/// <item>The token passed to the constructor (if any) is cancelled</item>
|
||||
/// <item><see cref="Dispose"/> is called</item>
|
||||
/// </list>
|
||||
/// </returns>
|
||||
/// <exception cref="ObjectDisposedException">This object has been disposed.</exception>
|
||||
public CancellationToken CreateNext(CancellationToken token)
|
||||
{
|
||||
var nextSource = CancellationTokenSource.CreateLinkedTokenSource(token, _superToken);
|
||||
|
||||
// Obtain the token before exchange, as otherwise the CTS may be cancelled before
|
||||
// we request the Token, which will result in an ObjectDisposedException.
|
||||
// This way we would return a cancelled token, which is reasonable.
|
||||
var nextToken = nextSource.Token;
|
||||
|
||||
var priorSource = Interlocked.Exchange(ref _cts, nextSource);
|
||||
|
||||
if (priorSource is null)
|
||||
{
|
||||
nextSource.Dispose();
|
||||
|
||||
throw new ObjectDisposedException(nameof(CancellationSeries));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
priorSource.Cancel();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// A registered action on the token may throw, which would surface here.
|
||||
// Ensure we always dispose the prior CTS.
|
||||
priorSource.Dispose();
|
||||
}
|
||||
|
||||
return nextToken;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
var source = Interlocked.Exchange(ref _cts, null);
|
||||
|
||||
if (source is null)
|
||||
{
|
||||
// Already disposed
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
source.Cancel();
|
||||
}
|
||||
finally
|
||||
{
|
||||
source.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisCSharpPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="$(MicrosoftCodeAnalysisWorkspacesCommonPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Compiler\Microsoft.CodeAnalysis.Razor\src\Microsoft.CodeAnalysis.Razor.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Razor.ProjectEngineHost\Microsoft.AspNetCore.Razor.ProjectEngineHost.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1 @@
|
|||
#nullable enable
|
|
@ -0,0 +1 @@
|
|||
#nullable enable
|
|
@ -0,0 +1,215 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.ProjectEngineHost;
|
||||
using Microsoft.AspNetCore.Razor.ProjectEngineHost.Serialization;
|
||||
using Microsoft.AspNetCore.Razor.Telemetry;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace;
|
||||
|
||||
internal static class RazorProjectJsonSerializer
|
||||
{
|
||||
private static readonly JsonSerializer s_serializer;
|
||||
private static readonly EmptyProjectEngineFactory s_fallbackProjectEngineFactory;
|
||||
private static readonly StringComparison s_stringComparison;
|
||||
|
||||
static RazorProjectJsonSerializer()
|
||||
{
|
||||
s_serializer = new JsonSerializer()
|
||||
{
|
||||
Formatting = Formatting.Indented
|
||||
};
|
||||
|
||||
s_serializer.Converters.RegisterProjectSerializerConverters();
|
||||
|
||||
s_fallbackProjectEngineFactory = new EmptyProjectEngineFactory();
|
||||
s_stringComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||
? StringComparison.Ordinal
|
||||
: StringComparison.OrdinalIgnoreCase;
|
||||
}
|
||||
|
||||
public static async Task SerializeAsync(Project project, string projectRazorJsonFileName, CancellationToken cancellationToken)
|
||||
{
|
||||
var projectPath = Path.GetDirectoryName(project.FilePath);
|
||||
if (projectPath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var intermediateOutputPath = Path.GetDirectoryName(project.CompilationOutputInfo.AssemblyPath);
|
||||
if (intermediateOutputPath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// First, lets get the documents, because if there aren't any, we can skip out early
|
||||
var documents = GetDocuments(project, projectPath);
|
||||
|
||||
// Not a razor project
|
||||
if (documents.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var csharpLanguageVersion = (project.ParseOptions as CSharpParseOptions)?.LanguageVersion ?? LanguageVersion.Default;
|
||||
|
||||
var options = project.AnalyzerOptions.AnalyzerConfigOptionsProvider;
|
||||
var configuration = ComputeRazorConfigurationOptions(options, out var defaultNamespace);
|
||||
|
||||
var fileSystem = RazorProjectFileSystem.Create(projectPath);
|
||||
|
||||
var defaultConfigure = (RazorProjectEngineBuilder builder) =>
|
||||
{
|
||||
if (defaultNamespace is not null)
|
||||
{
|
||||
builder.SetRootNamespace(defaultNamespace);
|
||||
}
|
||||
|
||||
builder.SetCSharpLanguageVersion(csharpLanguageVersion);
|
||||
builder.SetSupportLocalizedComponentNames(); // ProjectState in MS.CA.Razor.Workspaces does this, so I'm doing it too!
|
||||
};
|
||||
|
||||
var engine = DefaultProjectEngineFactory.Create(
|
||||
configuration,
|
||||
fileSystem: fileSystem,
|
||||
configure: defaultConfigure,
|
||||
fallback: s_fallbackProjectEngineFactory,
|
||||
factories: ProjectEngineFactories.Factories);
|
||||
|
||||
var resolver = new CompilationTagHelperResolver(NoOpTelemetryReporter.Instance);
|
||||
var tagHelpers = await resolver.GetTagHelpersAsync(project, engine, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var projectWorkspaceState = new ProjectWorkspaceState(
|
||||
tagHelpers: tagHelpers.Descriptors!,
|
||||
csharpLanguageVersion: csharpLanguageVersion);
|
||||
|
||||
var jsonFilePath = Path.Combine(intermediateOutputPath, projectRazorJsonFileName);
|
||||
|
||||
var projectRazorJson = new ProjectRazorJson(
|
||||
serializedOriginFilePath: jsonFilePath,
|
||||
filePath: project.FilePath!,
|
||||
configuration: configuration,
|
||||
rootNamespace: defaultNamespace,
|
||||
projectWorkspaceState: projectWorkspaceState,
|
||||
documents: documents);
|
||||
|
||||
WriteJsonFile(jsonFilePath, projectRazorJson);
|
||||
}
|
||||
|
||||
private static RazorConfiguration ComputeRazorConfigurationOptions(AnalyzerConfigOptionsProvider options, out string defaultNamespace)
|
||||
{
|
||||
// See RazorSourceGenerator.RazorProviders.cs
|
||||
|
||||
var globalOptions = options.GlobalOptions;
|
||||
|
||||
globalOptions.TryGetValue("build_property.RazorConfiguration", out var configurationName);
|
||||
|
||||
configurationName ??= "MVC-3.0"; // TODO: Source generator uses "default" here??
|
||||
|
||||
globalOptions.TryGetValue("build_property.RootNamespace", out var rootNamespace);
|
||||
|
||||
if (!globalOptions.TryGetValue("build_property.RazorLangVersion", out var razorLanguageVersionString) ||
|
||||
!RazorLanguageVersion.TryParse(razorLanguageVersionString, out var razorLanguageVersion))
|
||||
{
|
||||
razorLanguageVersion = RazorLanguageVersion.Latest;
|
||||
}
|
||||
|
||||
var razorConfiguration = RazorConfiguration.Create(razorLanguageVersion, configurationName, Enumerable.Empty<RazorExtension>(), useConsolidatedMvcViews: true);
|
||||
|
||||
defaultNamespace = rootNamespace ?? "ASP"; // TODO: Source generator does this. Do we want it?
|
||||
|
||||
return razorConfiguration;
|
||||
}
|
||||
|
||||
private static void WriteJsonFile(string publishFilePath, ProjectRazorJson projectRazorJson)
|
||||
{
|
||||
// We need to avoid having an incomplete file at any point, but our
|
||||
// project configuration is large enough that it will be written as multiple operations.
|
||||
var tempFilePath = string.Concat(publishFilePath, ".temp");
|
||||
var tempFileInfo = new FileInfo(tempFilePath);
|
||||
|
||||
if (tempFileInfo.Exists)
|
||||
{
|
||||
// This could be caused by failures during serialization or early process termination.
|
||||
tempFileInfo.Delete();
|
||||
}
|
||||
|
||||
// This needs to be in explicit brackets because the operation needs to be completed
|
||||
// by the time we move the temp file into its place
|
||||
using (var writer = tempFileInfo.CreateText())
|
||||
{
|
||||
s_serializer.Serialize(writer, projectRazorJson);
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(publishFilePath);
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
fileInfo.Delete();
|
||||
}
|
||||
|
||||
File.Move(tempFileInfo.FullName, publishFilePath);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<DocumentSnapshotHandle> GetDocuments(Project project, string projectPath)
|
||||
{
|
||||
var documents = new List<DocumentSnapshotHandle>(project.DocumentIds.Count);
|
||||
|
||||
var normalizedProjectPath = FilePathNormalizer.NormalizeDirectory(projectPath);
|
||||
|
||||
// We go through additional documents, because that's where the razor files will be
|
||||
// We could alternatively go through the Documents and look for our virtual C# documents, that the dynamic file info
|
||||
// would have added
|
||||
foreach (var document in project.AdditionalDocuments)
|
||||
{
|
||||
if (document.FilePath is not null &&
|
||||
TryGetFileKind(document.FilePath, out var kind))
|
||||
{
|
||||
documents.Add(new DocumentSnapshotHandle(document.FilePath, GetTargetPath(document.FilePath, normalizedProjectPath), kind));
|
||||
}
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
private static string GetTargetPath(string documentFilePath, string normalizedProjectPath)
|
||||
{
|
||||
var targetFilePath = FilePathNormalizer.Normalize(documentFilePath);
|
||||
if (targetFilePath.StartsWith(normalizedProjectPath, s_stringComparison))
|
||||
{
|
||||
// Make relative
|
||||
targetFilePath = documentFilePath.Substring(normalizedProjectPath.Length);
|
||||
}
|
||||
|
||||
// Representing all of our host documents with a re-normalized target path to workaround GetRelatedDocument limitations.
|
||||
var normalizedTargetFilePath = targetFilePath.Replace('/', '\\').TrimStart('\\');
|
||||
|
||||
return normalizedTargetFilePath;
|
||||
}
|
||||
|
||||
private static bool TryGetFileKind(string? filePath, [NotNullWhen(true)] out string? fileKind)
|
||||
{
|
||||
var extension = Path.GetExtension(filePath);
|
||||
|
||||
if (string.Equals(extension, ".cshtml", s_stringComparison))
|
||||
{
|
||||
fileKind = FileKinds.Legacy;
|
||||
return true;
|
||||
}
|
||||
else if (string.Equals(extension, ".razor", s_stringComparison))
|
||||
{
|
||||
fileKind = FileKinds.GetComponentFileKindFromFilePath(filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
fileKind = null;
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace;
|
||||
|
||||
public class RazorWorkspaceListener : IDisposable
|
||||
{
|
||||
private static readonly TimeSpan s_debounceTime = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
private string? _projectRazorJsonFileName;
|
||||
private readonly Dictionary<string, TaskDelayScheduler> _workQueues;
|
||||
private readonly object _gate = new();
|
||||
|
||||
public RazorWorkspaceListener()
|
||||
{
|
||||
var comparer = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||
? StringComparer.Ordinal
|
||||
: StringComparer.OrdinalIgnoreCase;
|
||||
_workQueues = new Dictionary<string, TaskDelayScheduler>(comparer);
|
||||
}
|
||||
|
||||
public void EnsureInitialized(Workspace workspace, string projectRazorJsonFileName)
|
||||
{
|
||||
// Make sure we don't hook up the event handler multiple times
|
||||
if (_projectRazorJsonFileName is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_projectRazorJsonFileName = projectRazorJsonFileName;
|
||||
workspace.WorkspaceChanged += Workspace_WorkspaceChanged;
|
||||
}
|
||||
|
||||
private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs e)
|
||||
{
|
||||
switch (e.Kind)
|
||||
{
|
||||
case WorkspaceChangeKind.SolutionAdded:
|
||||
case WorkspaceChangeKind.SolutionChanged:
|
||||
case WorkspaceChangeKind.SolutionReloaded:
|
||||
foreach (var project in e.NewSolution.Projects)
|
||||
{
|
||||
EnqueueUpdate(project);
|
||||
}
|
||||
|
||||
break;
|
||||
case WorkspaceChangeKind.ProjectRemoved:
|
||||
RemoveProject(e.OldSolution.GetProject(e.ProjectId));
|
||||
break;
|
||||
case WorkspaceChangeKind.ProjectAdded:
|
||||
case WorkspaceChangeKind.ProjectChanged:
|
||||
case WorkspaceChangeKind.ProjectReloaded:
|
||||
case WorkspaceChangeKind.DocumentAdded:
|
||||
case WorkspaceChangeKind.DocumentRemoved:
|
||||
case WorkspaceChangeKind.DocumentReloaded:
|
||||
case WorkspaceChangeKind.DocumentChanged:
|
||||
case WorkspaceChangeKind.AdditionalDocumentAdded:
|
||||
case WorkspaceChangeKind.AdditionalDocumentRemoved:
|
||||
case WorkspaceChangeKind.AdditionalDocumentReloaded:
|
||||
case WorkspaceChangeKind.AdditionalDocumentChanged:
|
||||
case WorkspaceChangeKind.DocumentInfoChanged:
|
||||
case WorkspaceChangeKind.AnalyzerConfigDocumentAdded:
|
||||
case WorkspaceChangeKind.AnalyzerConfigDocumentRemoved:
|
||||
case WorkspaceChangeKind.AnalyzerConfigDocumentReloaded:
|
||||
case WorkspaceChangeKind.AnalyzerConfigDocumentChanged:
|
||||
EnqueueUpdate(e.NewSolution.GetProject(e.ProjectId));
|
||||
break;
|
||||
case WorkspaceChangeKind.SolutionCleared:
|
||||
case WorkspaceChangeKind.SolutionRemoved:
|
||||
foreach (var project in e.OldSolution.Projects)
|
||||
{
|
||||
RemoveProject(project);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveProject(Project? project)
|
||||
{
|
||||
if (project?.FilePath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TaskDelayScheduler? scheduler;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_workQueues.TryGetValue(project.FilePath, out scheduler))
|
||||
{
|
||||
_workQueues.Remove(project.FilePath);
|
||||
}
|
||||
}
|
||||
|
||||
scheduler?.Dispose();
|
||||
}
|
||||
|
||||
private void EnqueueUpdate(Project? project)
|
||||
{
|
||||
if (_projectRazorJsonFileName is null ||
|
||||
project is not
|
||||
{
|
||||
FilePath: not null,
|
||||
Language: LanguageNames.CSharp
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TaskDelayScheduler? scheduler;
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_workQueues.TryGetValue(project.FilePath, out scheduler))
|
||||
{
|
||||
scheduler = new TaskDelayScheduler(s_debounceTime, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
scheduler.ScheduleAsyncTask(ct => RazorProjectJsonSerializer.SerializeAsync(project, _projectRazorJsonFileName, ct), CancellationToken.None);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
foreach (var kvp in _workQueues)
|
||||
{
|
||||
kvp.Value.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// 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.ExternalAccess.RoslynWorkspace;
|
||||
|
||||
// Copied from https://github.com/dotnet/project-system/blob/e4db47666e0a49f6c38e701f8630dbc31380fb64/src/Microsoft.VisualStudio.ProjectSystem.Managed/Threading/Tasks/TaskDelayScheduler.cs
|
||||
// (with changes to remove JTF concepts)
|
||||
|
||||
/// <summary>
|
||||
/// Helper class which allows a task to be scheduled to run after some delay, but if a new task
|
||||
/// is scheduled before the delay runs out, the previous task is cancelled.
|
||||
/// </summary>
|
||||
internal sealed class TaskDelayScheduler
|
||||
{
|
||||
private readonly TimeSpan _taskDelayTime;
|
||||
private readonly CancellationSeries _cancellationSeries;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the TaskDelayScheduler. If an originalSourceToken is passed, it will be linked to the PendingUpdateTokenSource so
|
||||
/// that cancelling that token will also flow through and cancel a pending update.
|
||||
/// </summary>
|
||||
public TaskDelayScheduler(TimeSpan taskDelayTime, CancellationToken originalSourceToken)
|
||||
{
|
||||
_taskDelayTime = taskDelayTime;
|
||||
_cancellationSeries = new CancellationSeries(originalSourceToken);
|
||||
}
|
||||
|
||||
public void ScheduleAsyncTask(Func<CancellationToken, Task> operation, CancellationToken token)
|
||||
{
|
||||
var nextToken = _cancellationSeries.CreateNext(token);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (nextToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(_taskDelayTime, nextToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await operation(nextToken);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any pending tasks and disposes this object.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellationSeries.Dispose();
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче