// 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>; 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>(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 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 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 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 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 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 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(); 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(); } var documents = new List(); 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(); } var documents = new List(); 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(); } } }