aspnetcore/src/Microsoft.VisualStudio.Lang.../ProjectSystem/DefaultRazorProjectHost.cs

353 lines
13 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.VisualStudio.LanguageServices;
using Microsoft.VisualStudio.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.Properties;
using Item = System.Collections.Generic.KeyValuePair<string, System.Collections.Immutable.IImmutableDictionary<string, string>>;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
// Somewhat similar to https://github.com/dotnet/project-system/blob/fa074d228dcff6dae9e48ce43dd4a3a5aa22e8f0/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs
//
// This class is responsible for intializing the Razor ProjectSnapshotManager for cases where
// MSBuild provides configuration support (>= 2.1).
[AppliesTo("DotNetCoreRazor & DotNetCoreRazorConfiguration")]
[Export(ExportContractNames.Scopes.UnconfiguredProject, typeof(IProjectDynamicLoadComponent))]
internal class DefaultRazorProjectHost : RazorProjectHostBase
{
private IDisposable _subscription;
[ImportingConstructor]
public DefaultRazorProjectHost(
IUnconfiguredProjectCommonServices commonServices,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
: base(commonServices, workspace)
{
}
// Internal for testing
internal DefaultRazorProjectHost(
IUnconfiguredProjectCommonServices commonServices,
Workspace workspace,
ProjectSnapshotManagerBase projectManager)
: base(commonServices, workspace, projectManager)
{
}
protected override async Task InitializeCoreAsync(CancellationToken cancellationToken)
{
await base.InitializeCoreAsync(cancellationToken).ConfigureAwait(false);
// Don't try to evaluate any properties here since the project is still loading and we require access
// to the UI thread to push our updates.
//
// Just subscribe and handle the notification later.
// Don't try to evaluate any properties here since the project is still loading and we require access
// to the UI thread to push our updates.
//
// Just subscribe and handle the notification later.
var receiver = new ActionBlock<IProjectVersionedValue<IProjectSubscriptionUpdate>>(OnProjectChanged);
_subscription = CommonServices.ActiveConfiguredProjectSubscription.JointRuleSource.SourceBlock.LinkTo(
receiver,
initialDataAsNew: true,
suppressVersionOnlyUpdates: true,
ruleNames: new string[]
{
Rules.RazorGeneral.SchemaName,
Rules.RazorConfiguration.SchemaName,
Rules.RazorExtension.SchemaName,
Rules.RazorGenerateWithTargetPath.SchemaName,
});
}
protected override async Task DisposeCoreAsync(bool initialized)
{
await base.DisposeCoreAsync(initialized).ConfigureAwait(false);
if (initialized)
{
_subscription.Dispose();
}
}
// Internal for testing
internal async Task OnProjectChanged(IProjectVersionedValue<IProjectSubscriptionUpdate> update)
{
if (IsDisposing || IsDisposed)
{
return;
}
await CommonServices.TasksService.LoadedProjectAsync(async () =>
{
await ExecuteWithLock(async () =>
{
if (TryGetConfiguration(update.Value.CurrentState, out var configuration))
{
var hostProject = new HostProject(CommonServices.UnconfiguredProject.FullPath, configuration);
// We need to deal with the case where the project was uninitialized, but now
// is valid for Razor. In that case we might have previously seen all of the documents
// but ignored them because the project wasn't active.
//
// So what we do to deal with this, is that we 'remove' all changed and removed items
// and then we 'add' all current items. This allows minimal churn to the PSM, but still
// makes us up to date.
var documents = GetCurrentDocuments(update.Value);
var changedDocuments = GetChangedAndRemovedDocuments(update.Value);
await UpdateAsync(() =>
{
UpdateProjectUnsafe(hostProject);
for (var i = 0; i < changedDocuments.Length; i++)
{
RemoveDocumentUnsafe(changedDocuments[i]);
}
for (var i = 0; i < documents.Length; i++)
{
AddDocumentUnsafe(documents[i]);
}
}).ConfigureAwait(false);
}
else
{
// Ok we can't find a configuration. Let's assume this project isn't using Razor then.
await UpdateAsync(UninitializeProjectUnsafe).ConfigureAwait(false);
}
});
}, registerFaultHandler: true);
}
// Internal for testing
internal static bool TryGetConfiguration(
IImmutableDictionary<string, IProjectRuleSnapshot> state,
out RazorConfiguration configuration)
{
if (!TryGetDefaultConfiguration(state, out var defaultConfiguration))
{
configuration = null;
return false;
}
if (!TryGetLanguageVersion(state, out var languageVersion))
{
configuration = null;
return false;
}
if (!TryGetConfigurationItem(defaultConfiguration, state, out var configurationItem))
{
configuration = null;
return false;
}
if (!TryGetExtensionNames(configurationItem, out var extensionNames))
{
configuration = null;
return false;
}
if (!TryGetExtensions(extensionNames, state, out var extensions))
{
configuration = null;
return false;
}
configuration = new ProjectSystemRazorConfiguration(languageVersion, configurationItem.Key, extensions);
return true;
}
// Internal for testing
internal static bool TryGetDefaultConfiguration(
IImmutableDictionary<string, IProjectRuleSnapshot> state,
out string defaultConfiguration)
{
if (!state.TryGetValue(Rules.RazorGeneral.SchemaName, out var rule))
{
defaultConfiguration = null;
return false;
}
if (!rule.Properties.TryGetValue(Rules.RazorGeneral.RazorDefaultConfigurationProperty, out defaultConfiguration))
{
defaultConfiguration = null;
return false;
}
if (string.IsNullOrEmpty(defaultConfiguration))
{
defaultConfiguration = null;
return false;
}
return true;
}
// Internal for testing
internal static bool TryGetLanguageVersion(
IImmutableDictionary<string, IProjectRuleSnapshot> state,
out RazorLanguageVersion languageVersion)
{
if (!state.TryGetValue(Rules.RazorGeneral.SchemaName, out var rule))
{
languageVersion = null;
return false;
}
if (!rule.Properties.TryGetValue(Rules.RazorGeneral.RazorLangVersionProperty, out var languageVersionValue))
{
languageVersion = null;
return false;
}
if (string.IsNullOrEmpty(languageVersionValue))
{
languageVersion = null;
return false;
}
if (!RazorLanguageVersion.TryParse(languageVersionValue, out languageVersion))
{
languageVersion = RazorLanguageVersion.Latest;
}
return true;
}
// Internal for testing
internal static bool TryGetConfigurationItem(
string configuration,
IImmutableDictionary<string, IProjectRuleSnapshot> state,
out Item configurationItem)
{
if (!state.TryGetValue(Rules.RazorConfiguration.PrimaryDataSourceItemType, out var configurationState))
{
configurationItem = default(Item);
return false;
}
var items = configurationState.Items;
foreach (var item in items)
{
if (item.Key == configuration)
{
configurationItem = item;
return true;
}
}
configurationItem = default(Item);
return false;
}
// Internal for testing
internal static bool TryGetExtensionNames(
Item configurationItem,
out string[] configuredExtensionNames)
{
if (!configurationItem.Value.TryGetValue(Rules.RazorConfiguration.ExtensionsProperty, out var extensionNames))
{
configuredExtensionNames = null;
return false;
}
if (string.IsNullOrEmpty(extensionNames))
{
configuredExtensionNames = null;
return false;
}
configuredExtensionNames = extensionNames.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
return true;
}
// Internal for testing
internal static bool TryGetExtensions(
string[] extensionNames,
IImmutableDictionary<string, IProjectRuleSnapshot> state,
out ProjectSystemRazorExtension[] extensions)
{
if (!state.TryGetValue(Rules.RazorExtension.PrimaryDataSourceItemType, out var rule))
{
extensions = null;
return false;
}
var items = rule.Items;
var extensionList = new List<ProjectSystemRazorExtension>();
foreach (var item in items)
{
var extensionName = item.Key;
if (extensionNames.Contains(extensionName))
{
extensionList.Add(new ProjectSystemRazorExtension(extensionName));
}
}
extensions = extensionList.ToArray();
return true;
}
private HostDocument[] GetCurrentDocuments(IProjectSubscriptionUpdate update)
{
if (!update.CurrentState.TryGetValue(Rules.RazorGenerateWithTargetPath.SchemaName, out var rule))
{
return Array.Empty<HostDocument>();
}
var documents = new List<HostDocument>();
foreach (var kvp in rule.Items)
{
if (kvp.Value.TryGetValue(Rules.RazorGenerateWithTargetPath.TargetPathProperty, out var targetPath) &&
!string.IsNullOrWhiteSpace(kvp.Key) &&
!string.IsNullOrWhiteSpace(targetPath))
{
var filePath = CommonServices.UnconfiguredProject.MakeRooted(kvp.Key);
documents.Add(new HostDocument(filePath, targetPath));
}
}
return documents.ToArray();
}
private HostDocument[] GetChangedAndRemovedDocuments(IProjectSubscriptionUpdate update)
{
if (!update.ProjectChanges.TryGetValue(Rules.RazorGenerateWithTargetPath.SchemaName, out var rule))
{
return Array.Empty<HostDocument>();
}
var documents = new List<HostDocument>();
foreach (var key in rule.Difference.RemovedItems.Concat(rule.Difference.ChangedItems))
{
if (rule.Before.Items.TryGetValue(key, out var value))
{
if (value.TryGetValue(Rules.RazorGenerateWithTargetPath.TargetPathProperty, out var targetPath) &&
!string.IsNullOrWhiteSpace(key) &&
!string.IsNullOrWhiteSpace(targetPath))
{
var filePath = CommonServices.UnconfiguredProject.MakeRooted(key);
documents.Add(new HostDocument(filePath, targetPath));
}
}
}
return documents.ToArray();
}
}
}