From 5008c7803c71574061bae539db2bf722df7aae77 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 2 Feb 2018 18:13:16 -0800 Subject: [PATCH] Add a project system Step 1: Add HostProject This is a somewhat complex addition to the ProjectSnapshotManager. Now that we accept updates from the underlying IDE project system we need to coordinate those with the Workspace. This means that ProjectSnapshot itself now also has a version concept. Step 2: Introduce a new project system based on CPS We use project capabilities defined by the Razor SDK to determine whether to rely on MSBuild evaluation to detect the configuration or whether to fallback to assembly-based detection. Step 3: Flow RazorConfiguration everywhere We use now expose the RazorConfiguration to the language service and editor. This means that we no longer need to detect the project's configuration asynchronously, it happens much faster now. --- build/dependencies.props | 3 + .../RazorConfiguration.cs | 37 +- .../DefaultErrorReporter.cs | 23 +- .../ErrorReporter.cs | 5 +- ...rojectExtensibilityConfigurationFactory.cs | 142 --- ...xtensibilityConfigurationFactoryFactory.cs | 25 - .../ProjectSystem/DefaultProjectSnapshot.cs | 119 ++- .../DefaultProjectSnapshotManager.cs | 380 ++++++-- .../DefaultProjectSnapshotWorker.cs | 22 +- .../DefaultProjectSnapshotWorkerFactory.cs | 4 +- .../FallbackRazorConfiguration.cs | 78 ++ .../ProjectSystem/FallbackRazorExtension.cs | 23 + .../ProjectSystem/HostProject.cs | 31 + .../MvcExtensibilityConfiguration.cs | 86 -- .../ProjectExtensibilityConfiguration.cs | 31 - ...rojectExtensibilityConfigurationFactory.cs | 14 - .../ProjectExtensibilityConfigurationKind.cs | 14 - .../ProjectSystem/ProjectSnapshot.cs | 13 +- .../ProjectSnapshotManagerBase.cs | 24 +- .../ProjectSnapshotManagerExtensions.cs | 2 +- .../ProjectSnapshotUpdateContext.cs | 29 +- .../ProjectSnapshotWorkerQueue.cs | 19 +- .../ProjectSystemRazorConfiguration.cs | 43 + .../ProjectSystemRazorExtension.cs | 23 + .../WorkspaceProjectSnapshotChangeTrigger.cs | 39 +- .../FilePathComparer.cs | 30 + .../DefaultProjectEngineFactoryService.cs | 24 +- .../DefaultVisualStudioDocumentTracker.cs | 6 +- .../VisualStudioDocumentTracker.cs | 3 +- ...VisualStudio.LanguageServices.Razor.csproj | 82 +- .../ProjectSystem/DefaultRazorProjectHost.cs | 125 +++ .../ProjectSystem/FallbackRazorProjectHost.cs | 142 +++ .../IUnconfiguredProjectCommonServices.cs | 88 ++ .../ManageProjectSystemSchema.cs | 16 + .../ProjectSystem/RazorProjectHostBase.cs | 190 ++++ .../ProjectSystem/Rules/RazorConfiguration.cs | 212 +++++ .../Rules/RazorConfiguration.xaml | 29 + .../ProjectSystem/Rules/RazorExtension.cs | 235 +++++ .../ProjectSystem/Rules/RazorExtension.xaml | 37 + .../ProjectSystem/Rules/RazorGeneral.cs | 235 +++++ .../ProjectSystem/Rules/RazorGeneral.xaml | 36 + .../Rules/RazorProjectProperties.cs | 34 + .../UnconfiguredProjectCommonServices.cs | 30 + .../VisualStudioErrorReporter.cs | 23 +- ....CodeAnalysis.Razor.Workspaces.Test.csproj | 1 + ...ctExtensibilityConfigurationFactoryTest.cs | 245 ------ .../DefaultProjectSnapshotManagerTest.cs | 331 ------- .../DefaultProjectSnapshotTest.cs | 40 +- ...rkspaceProjectSnapshotChangeTriggerTest.cs | 112 ++- .../DefaultProjectEngineFactoryServiceTest.cs | 83 +- .../DefaultProjectSnapshotManagerTest.cs | 815 ++++++++++++++++++ .../DefaultRazorProjectHostTest.cs | 456 ++++++++++ .../FallbackRazorProjectHostTest.cs | 373 ++++++++ .../ProjectSnapshotWorkerQueueTest.cs | 98 ++- .../ProjectSystem/TestAssemblyReference.cs | 68 ++ .../TestProjectChangeDescription.cs | 24 + .../ProjectSystem/TestProjectRuleSnapshot.cs | 61 ++ .../TestProjectSystemServices.cs | 799 +++++++++++++++++ .../ProjectSystem/TestPropertyData.cs | 18 + .../RazorDocumentInfoViewModel.cs | 2 +- ...crosoft.VisualStudio.RazorExtension.csproj | 4 +- .../RazorInfo/ProjectInfoViewModel.cs | 11 - .../RazorInfo/ProjectSnapshotViewModel.cs | 36 + .../RazorInfo/ProjectViewModel.cs | 26 +- .../RazorInfo/PropertyViewModel.cs | 21 + .../RazorInfo/RazorInfoToolWindow.cs | 124 ++- .../RazorInfo/RazorInfoToolWindowControl.xaml | 424 +++++---- .../RazorInfo/RazorInfoViewModel.cs | 18 +- 68 files changed, 5497 insertions(+), 1499 deletions(-) delete mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactory.cs delete mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryFactory.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/FallbackRazorConfiguration.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/FallbackRazorExtension.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostProject.cs delete mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/MvcExtensibilityConfiguration.cs delete mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfiguration.cs delete mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationFactory.cs delete mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationKind.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSystemRazorConfiguration.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSystemRazorExtension.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor/FilePathComparer.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IUnconfiguredProjectCommonServices.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManageProjectSystemSchema.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.xaml create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.xaml create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.xaml create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorProjectProperties.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/UnconfiguredProjectCommonServices.cs delete mode 100644 test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryTest.cs delete mode 100644 test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultRazorProjectHostTest.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackRazorProjectHostTest.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestAssemblyReference.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectChangeDescription.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectRuleSnapshot.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectSystemServices.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestPropertyData.cs create mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectSnapshotViewModel.cs create mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/PropertyViewModel.cs diff --git a/build/dependencies.props b/build/dependencies.props index bd52ff645b..93d4967dee 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -8,6 +8,8 @@ 2.1.0-preview2-30077 2.1.0-preview2-30106 2.1.0-preview2-30106 + 15.3.409 + 15.3.409 2.4.0 2.4.0 2.1.0-preview2-30106 @@ -46,6 +48,7 @@ 2.6.0-beta1-62023-02 2.6.0-beta1-62023-02 2.6.0-beta1-62023-02 + 2.6.0-beta1-62023-02 2.6.0-beta1-62023-02 2.6.0-beta1-62023-02 2.6.0-beta1-62023-02 diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs index f68db61f22..e8ef287c0c 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs @@ -7,15 +7,15 @@ using System.Linq; namespace Microsoft.AspNetCore.Razor.Language { - public sealed class RazorConfiguration + public abstract class RazorConfiguration { - public static readonly RazorConfiguration Default = new RazorConfiguration( + public static readonly RazorConfiguration Default = new DefaultRazorConfiguration( RazorLanguageVersion.Latest, "unnamed", Array.Empty()); - public RazorConfiguration( - RazorLanguageVersion languageVersion, + public static RazorConfiguration Create( + RazorLanguageVersion languageVersion, string configurationName, IEnumerable extensions) { @@ -34,15 +34,32 @@ namespace Microsoft.AspNetCore.Razor.Language throw new ArgumentNullException(nameof(extensions)); } - LanguageVersion = languageVersion; - ConfigurationName = configurationName; - Extensions = extensions.ToArray(); + return new DefaultRazorConfiguration(languageVersion, configurationName, extensions.ToArray()); } - public string ConfigurationName { get; } + public abstract string ConfigurationName { get; } - public IReadOnlyList Extensions { get; } + public abstract IReadOnlyList Extensions { get; } - public RazorLanguageVersion LanguageVersion { get; } + public abstract RazorLanguageVersion LanguageVersion { get; } + + private class DefaultRazorConfiguration : RazorConfiguration + { + public DefaultRazorConfiguration( + RazorLanguageVersion languageVersion, + string configurationName, + RazorExtension[] extensions) + { + LanguageVersion = languageVersion; + ConfigurationName = configurationName; + Extensions = extensions; + } + + public override string ConfigurationName { get; } + + public override IReadOnlyList Extensions { get; } + + public override RazorLanguageVersion LanguageVersion { get; } + } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultErrorReporter.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultErrorReporter.cs index 664434a674..ff200966e8 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultErrorReporter.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultErrorReporter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.CodeAnalysis.Razor { @@ -9,11 +10,31 @@ namespace Microsoft.CodeAnalysis.Razor { public override void ReportError(Exception exception) { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + // Do nothing. } - public override void ReportError(Exception exception, Project project) + public override void ReportError(Exception exception, ProjectSnapshot project) { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + + // Do nothing. + } + + public override void ReportError(Exception exception, Project workspaceProject) + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + // Do nothing. } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ErrorReporter.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ErrorReporter.cs index 03bc44f61c..4f0b0dab81 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ErrorReporter.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ErrorReporter.cs @@ -3,13 +3,16 @@ using System; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.CodeAnalysis.Razor { internal abstract class ErrorReporter : IWorkspaceService { public abstract void ReportError(Exception exception); + + public abstract void ReportError(Exception exception, ProjectSnapshot project); - public abstract void ReportError(Exception exception, Project project); + public abstract void ReportError(Exception exception, Project workspaceProject); } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactory.cs deleted file mode 100644 index d1c21ff88f..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactory.cs +++ /dev/null @@ -1,142 +0,0 @@ -// 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.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - // This is hardcoded for now. A more complete design would fan out to a list of providers. - internal class DefaultProjectExtensibilityConfigurationFactory : ProjectExtensibilityConfigurationFactory - { - private const string MvcAssemblyName = "Microsoft.AspNetCore.Mvc.Razor"; - private const string RazorV1AssemblyName = "Microsoft.AspNetCore.Razor"; - private const string RazorV2AssemblyName = "Microsoft.AspNetCore.Razor.Language"; - - // Using MaxValue here so that we ignore patch and build numbers. We only want to compare major/minor. - private static readonly Version MaxSupportedRazorVersion = new Version(2, 0, Int32.MaxValue, Int32.MaxValue); - private static readonly Version MaxSupportedMvcVersion = new Version(2, 0, Int32.MaxValue, Int32.MaxValue); - - private static readonly Version DefaultRazorVersion = new Version(2, 0, 0, 0); - private static readonly Version DefaultMvcVersion = new Version(2, 0, 0, 0); - - public async override Task GetConfigurationAsync(Project project, CancellationToken cancellationToken = default(CancellationToken)) - { - if (project == null) - { - throw new ArgumentNullException(nameof(project)); - } - - var compilation = await project.GetCompilationAsync(cancellationToken); - return GetConfiguration(compilation.ReferencedAssemblyNames); - } - - // internal/separate for testing. - internal ProjectExtensibilityConfiguration GetConfiguration(IEnumerable references) - { - // Avoiding ToDictionary here because we don't want a crash if there is a duplicate name. - var assemblies = new Dictionary(); - foreach (var assembly in references) - { - assemblies[assembly.Name] = assembly; - } - - // First we look for the V2+ Razor Assembly. If we find this then its version is the correct Razor version. - AssemblyIdentity razorAssembly; - if (assemblies.TryGetValue(RazorV2AssemblyName, out razorAssembly)) - { - if (razorAssembly.Version == null || razorAssembly.Version > MaxSupportedRazorVersion) - { - // This is a newer Razor version than we know, treat it as a fallback case. - razorAssembly = null; - } - } - else if (assemblies.TryGetValue(RazorV1AssemblyName, out razorAssembly)) - { - // This assembly only counts as the 'Razor' assembly if it's a version lower than 2.0.0. - if (razorAssembly.Version == null || razorAssembly.Version >= new Version(2, 0, 0, 0)) - { - razorAssembly = null; - } - } - - AssemblyIdentity mvcAssembly; - if (assemblies.TryGetValue(MvcAssemblyName, out mvcAssembly)) - { - if (mvcAssembly.Version == null || mvcAssembly.Version > MaxSupportedMvcVersion) - { - // This is a newer MVC version than we know, treat it as a fallback case. - mvcAssembly = null; - } - } - - RazorLanguageVersion languageVersion = null; - if (razorAssembly != null && mvcAssembly != null) - { - languageVersion = GetLanguageVersion(razorAssembly); - - // This means we've definitely found a supported Razor version and an MVC version. - return new MvcExtensibilityConfiguration( - languageVersion, - ProjectExtensibilityConfigurationKind.ApproximateMatch, - new ProjectExtensibilityAssembly(razorAssembly), - new ProjectExtensibilityAssembly(mvcAssembly)); - } - - // If we get here it means we didn't find everything, so we have to guess. - if (razorAssembly == null || razorAssembly.Version == null) - { - razorAssembly = new AssemblyIdentity(RazorV2AssemblyName, DefaultRazorVersion); - } - - if (mvcAssembly == null || mvcAssembly.Version == null) - { - mvcAssembly = new AssemblyIdentity(MvcAssemblyName, DefaultMvcVersion); - } - - if (languageVersion == null) - { - languageVersion = GetLanguageVersion(razorAssembly); - } - - return new MvcExtensibilityConfiguration( - languageVersion, - ProjectExtensibilityConfigurationKind.Fallback, - new ProjectExtensibilityAssembly(razorAssembly), - new ProjectExtensibilityAssembly(mvcAssembly)); - } - - // Internal for testing - internal static RazorLanguageVersion GetLanguageVersion(AssemblyIdentity razorAssembly) - { - // This is inferred from the assembly for now, the Razor language version will eventually flow from MSBuild. - - var razorAssemblyVersion = razorAssembly.Version; - if (razorAssemblyVersion.Major == 1) - { - if (razorAssemblyVersion.Minor >= 1) - { - return RazorLanguageVersion.Version_1_1; - } - - return RazorLanguageVersion.Version_1_0; - } - - if (razorAssemblyVersion.Major == 2) - { - if (razorAssemblyVersion.Minor >= 1) - { - return RazorLanguageVersion.Version_2_1; - } - - return RazorLanguageVersion.Version_2_0; - } - - // Couldn't determine version based off of assembly, fallback to latest. - return RazorLanguageVersion.Latest; - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryFactory.cs deleted file mode 100644 index 93d7400ee3..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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.Composition; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - [Shared] - [ExportLanguageServiceFactory(typeof(ProjectExtensibilityConfigurationFactory), RazorLanguage.Name)] - internal class DefaultProjectExtensibilityConfigurationFactoryFactory : ILanguageServiceFactory - { - public ILanguageService CreateLanguageService(HostLanguageServices languageServices) - { - if (languageServices == null) - { - throw new ArgumentNullException(nameof(languageServices)); - } - - return new DefaultProjectExtensibilityConfigurationFactory(); - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs index afbd0028f3..20d3864e4e 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs @@ -2,8 +2,6 @@ // 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.Linq; using Microsoft.AspNetCore.Razor.Language; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem @@ -18,32 +16,60 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // at once. internal class DefaultProjectSnapshot : ProjectSnapshot { - public DefaultProjectSnapshot(Project underlyingProject) + public DefaultProjectSnapshot(HostProject hostProject, Project workspaceProject, VersionStamp? version = null) { - if (underlyingProject == null) + if (hostProject == null) { - throw new ArgumentNullException(nameof(underlyingProject)); + throw new ArgumentNullException(nameof(hostProject)); } - UnderlyingProject = underlyingProject; + HostProject = hostProject; + WorkspaceProject = workspaceProject; // Might be null + + FilePath = hostProject.FilePath; + Version = version ?? VersionStamp.Default; } - private DefaultProjectSnapshot(Project underlyingProject, DefaultProjectSnapshot other) + private DefaultProjectSnapshot(HostProject hostProject, DefaultProjectSnapshot other) { - if (underlyingProject == null) + if (hostProject == null) { - throw new ArgumentNullException(nameof(underlyingProject)); + throw new ArgumentNullException(nameof(hostProject)); } if (other == null) { throw new ArgumentNullException(nameof(other)); } - - UnderlyingProject = underlyingProject; + + HostProject = hostProject; ComputedVersion = other.ComputedVersion; - Configuration = other.Configuration; + FilePath = other.FilePath; + WorkspaceProject = other.WorkspaceProject; + + Version = other.Version.GetNewerVersion(); + } + + private DefaultProjectSnapshot(Project workspaceProject, DefaultProjectSnapshot other) + { + if (workspaceProject == null) + { + throw new ArgumentNullException(nameof(workspaceProject)); + } + + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + WorkspaceProject = workspaceProject; + + ComputedVersion = other.ComputedVersion; + FilePath = other.FilePath; + HostProject = other.HostProject; + + Version = other.Version.GetNewerVersion(); } private DefaultProjectSnapshot(ProjectSnapshotUpdateContext update, DefaultProjectSnapshot other) @@ -58,34 +84,67 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(other)); } - UnderlyingProject = other.UnderlyingProject; + ComputedVersion = update.Version; - ComputedVersion = update.UnderlyingProject.Version; - Configuration = update.Configuration; + FilePath = other.FilePath; + HostProject = other.HostProject; + WorkspaceProject = other.WorkspaceProject; + + // This doesn't represent a new version of the underlying data. Keep the same version. + Version = other.Version; } - public override ProjectExtensibilityConfiguration Configuration { get; } + public override RazorConfiguration Configuration => HostProject.Configuration; - public override Project UnderlyingProject { get; } + public override string FilePath { get; } + + public HostProject HostProject { get; } + + public override bool IsInitialized => WorkspaceProject != null; + + public override VersionStamp Version { get; } + + public override Project WorkspaceProject { get; } // This is the version that the computed state is based on. public VersionStamp? ComputedVersion { get; set; } // We know the project is dirty if we don't have a computed result, or it was computed for a different version. // Since the PSM updates the snapshots synchronously, the snapshot can never be older than the computed state. - public bool IsDirty => ComputedVersion == null || ComputedVersion.Value != UnderlyingProject.Version; + public bool IsDirty => ComputedVersion == null || ComputedVersion.Value != Version; - public DefaultProjectSnapshot WithProjectChange(Project project) + public ProjectSnapshotUpdateContext CreateUpdateContext() { - if (project == null) - { - throw new ArgumentNullException(nameof(project)); - } - - return new DefaultProjectSnapshot(project, this); + return new ProjectSnapshotUpdateContext(FilePath, HostProject, WorkspaceProject, Version); } - public DefaultProjectSnapshot WithProjectChange(ProjectSnapshotUpdateContext update) + public DefaultProjectSnapshot WithHostProject(HostProject hostProject) + { + if (hostProject == null) + { + throw new ArgumentNullException(nameof(hostProject)); + } + + return new DefaultProjectSnapshot(hostProject, this); + } + + public DefaultProjectSnapshot RemoveWorkspaceProject() + { + // We want to get rid of all of the computed state since it's not really valid. + return new DefaultProjectSnapshot(HostProject, null, Version.GetNewerVersion()); + } + + public DefaultProjectSnapshot WithWorkspaceProject(Project workspaceProject) + { + if (workspaceProject == null) + { + throw new ArgumentNullException(nameof(workspaceProject)); + } + + return new DefaultProjectSnapshot(workspaceProject, this); + } + + public DefaultProjectSnapshot WithComputedUpdate(ProjectSnapshotUpdateContext update) { if (update == null) { @@ -95,14 +154,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return new DefaultProjectSnapshot(update, this); } - public bool HasConfigurationChanged(ProjectSnapshot original) + public bool HasConfigurationChanged(DefaultProjectSnapshot original) { if (original == null) { throw new ArgumentNullException(nameof(original)); } - return !object.Equals(Configuration, original.Configuration); + // We don't have any computed state right now, so treat all background updates as + // significant. + return !object.Equals(ComputedVersion, original.ComputedVersion); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs index 32c3ae4681..b8e041fe76 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs @@ -3,10 +3,27 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { + // The implementation of project snapshot manager abstracts over the Roslyn Project (WorkspaceProject) + // and information from the host's underlying project system (HostProject), to provide a unified and + // immutable view of the underlying project systems. + // + // The HostProject support all of the configuration that the Razor SDK exposes via the project system + // (language version, extensions, named configuration). + // + // The WorkspaceProject is needed to support our use of Roslyn Compilations for Tag Helpers and other + // C# based constructs. + // + // The implementation will create a ProjectSnapshot for each HostProject. Put another way, when we + // see a WorkspaceProject get created, we only care if we already have a HostProject for the same + // filepath. + // + // Our underlying HostProject infrastructure currently does not handle multiple TFMs (project with + // $(TargetFrameworks), so we just bind to the first WorkspaceProject we see for each HostProject. internal class DefaultProjectSnapshotManager : ProjectSnapshotManagerBase { public override event EventHandler Changed; @@ -17,8 +34,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private readonly ProjectSnapshotWorkerQueue _workerQueue; private readonly ProjectSnapshotWorker _worker; - private readonly Dictionary _projects; - + private readonly Dictionary _projects; + public DefaultProjectSnapshotManager( ForegroundDispatcher foregroundDispatcher, ErrorReporter errorReporter, @@ -57,7 +74,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _triggers = triggers.ToArray(); Workspace = workspace; - _projects = new Dictionary(); + _projects = new Dictionary(FilePathComparer.Instance); + _workerQueue = new ProjectSnapshotWorkerQueue(_foregroundDispatcher, this, worker); for (var i = 0; i < _triggers.Length; i++) @@ -70,63 +88,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { get { + _foregroundDispatcher.AssertForegroundThread(); return _projects.Values.ToArray(); } } - public DefaultProjectSnapshot FindProject(ProjectId id) - { - if (id == null) - { - throw new ArgumentNullException(nameof(id)); - } - - _projects.TryGetValue(id, out var project); - return project; - } - public override Workspace Workspace { get; } - public override void ProjectAdded(Project underlyingProject) - { - if (underlyingProject == null) - { - throw new ArgumentNullException(nameof(underlyingProject)); - } - - var snapshot = new DefaultProjectSnapshot(underlyingProject); - _projects[underlyingProject.Id] = snapshot; - - // New projects always start dirty, need to compute state in the background. - NotifyBackgroundWorker(snapshot.UnderlyingProject); - - // We need to notify listeners about every project add. - NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Added)); - } - - public override void ProjectChanged(Project underlyingProject) - { - if (underlyingProject == null) - { - throw new ArgumentNullException(nameof(underlyingProject)); - } - - if (_projects.TryGetValue(underlyingProject.Id, out var original)) - { - // Doing an update to the project should keep computed values, but mark the project as dirty if the - // underlying project is newer. - var snapshot = original.WithProjectChange(underlyingProject); - _projects[underlyingProject.Id] = snapshot; - - if (snapshot.IsDirty) - { - // We don't need to notify listeners yet because we don't have any **new** computed state. However we do - // need to trigger the background work to asynchronously compute the effect of the updates. - NotifyBackgroundWorker(snapshot.UnderlyingProject); - } - } - } - public override void ProjectUpdated(ProjectSnapshotUpdateContext update) { if (update == null) @@ -134,17 +102,25 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(update)); } - if (_projects.TryGetValue(update.UnderlyingProject.Id, out var original)) + _foregroundDispatcher.AssertForegroundThread(); + + if (_projects.TryGetValue(update.WorkspaceProject.FilePath, out var original)) { + if (!original.IsInitialized) + { + // If the project has been uninitialized, just ignore the update. + return; + } + // This is an update to the project's computed values, so everything should be overwritten - var snapshot = original.WithProjectChange(update); - _projects[update.UnderlyingProject.Id] = snapshot; + var snapshot = original.WithComputedUpdate(update); + _projects[update.WorkspaceProject.FilePath] = snapshot; if (snapshot.IsDirty) { // It's possible that the snapshot can still be dirty if we got a project update while computing state in // the background. We need to trigger the background work to asynchronously compute the effect of the updates. - NotifyBackgroundWorker(snapshot.UnderlyingProject); + NotifyBackgroundWorker(snapshot.CreateUpdateContext()); } // Now we need to know if the changes that we applied are significant. If that's the case then @@ -156,58 +132,288 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } } - public override void ProjectRemoved(Project underlyingProject) + public override void HostProjectAdded(HostProject hostProject) { - if (underlyingProject == null) + if (hostProject == null) { - throw new ArgumentNullException(nameof(underlyingProject)); + throw new ArgumentNullException(nameof(hostProject)); } - - if (_projects.TryGetValue(underlyingProject.Id, out var snapshot)) + + _foregroundDispatcher.AssertForegroundThread(); + + // We don't expect to see a HostProject initialized multiple times for the same path. Just ignore it. + if (_projects.ContainsKey(hostProject.FilePath)) { - _projects.Remove(underlyingProject.Id); + return; + } + + // It's possible that Workspace has already created a project for this, but it's not deterministic + // So if possible find a WorkspaceProject. + var workspaceProject = GetWorkspaceProject(hostProject.FilePath); + + var snapshot = new DefaultProjectSnapshot(hostProject, workspaceProject); + _projects[hostProject.FilePath] = snapshot; + + if (snapshot.IsInitialized && snapshot.IsDirty) + { + // Start computing background state if the project is fully initialized. + NotifyBackgroundWorker(snapshot.CreateUpdateContext()); + } + + // We need to notify listeners about every project add. + NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Added)); + } + + public override void HostProjectChanged(HostProject hostProject) + { + if (hostProject == null) + { + throw new ArgumentNullException(nameof(hostProject)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + if (_projects.TryGetValue(hostProject.FilePath, out var original)) + { + // Doing an update to the project should keep computed values, but mark the project as dirty if the + // underlying project is newer. + var snapshot = original.WithHostProject(hostProject); + _projects[hostProject.FilePath] = snapshot; + + if (snapshot.IsInitialized && snapshot.IsDirty) + { + // Start computing background state if the project is fully initialized. + NotifyBackgroundWorker(snapshot.CreateUpdateContext()); + } + + // Notify listeners right away because if the HostProject changes then it's likely that the Razor + // configuration changed. + NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed)); + } + } + + public override void HostProjectRemoved(HostProject hostProject) + { + if (hostProject == null) + { + throw new ArgumentNullException(nameof(hostProject)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + if (_projects.TryGetValue(hostProject.FilePath, out var snapshot)) + { + _projects.Remove(hostProject.FilePath); // We need to notify listeners about every project removal. NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Removed)); } } - public override void ProjectBuildComplete(Project underlyingProject) + public override void WorkspaceProjectAdded(Project workspaceProject) { - if (underlyingProject == null) + if (workspaceProject == null) { - throw new ArgumentNullException(nameof(underlyingProject)); + throw new ArgumentNullException(nameof(workspaceProject)); } - if (_projects.TryGetValue(underlyingProject.Id, out var original)) + _foregroundDispatcher.AssertForegroundThread(); + + if (!IsSupportedWorkspaceProject(workspaceProject)) + { + return; + } + + // The WorkspaceProject initialization never triggers a "Project Add" from out point of view, we + // only care if the new WorkspaceProject matches an existing HostProject. + if (_projects.TryGetValue(workspaceProject.FilePath, out var original)) + { + // If this is a multi-targeting project then we are only interested in a single workspace project. If we already + // found one in the past just ignore this one. + if (original.WorkspaceProject == null) + { + var snapshot = original.WithWorkspaceProject(workspaceProject); + _projects[workspaceProject.FilePath] = snapshot; + + if (snapshot.IsInitialized && snapshot.IsDirty) + { + // We don't need to notify listeners yet because we don't have any **new** computed state. + // + // However we do need to trigger the background work to asynchronously compute the effect of the updates. + NotifyBackgroundWorker(snapshot.CreateUpdateContext()); + } + + // Notify listeners right away since WorkspaceProject was just added, the project is now initialized. + NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed)); + } + } + } + + public override void WorkspaceProjectChanged(Project workspaceProject) + { + if (workspaceProject == null) + { + throw new ArgumentNullException(nameof(workspaceProject)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + if (!IsSupportedWorkspaceProject(workspaceProject)) + { + return; + } + + // We also need to check the projectId here. If this is a multi-targeting project then we are only interested + // in a single workspace project. Just use the one that showed up first. + if (_projects.TryGetValue(workspaceProject.FilePath, out var original) && + (original.WorkspaceProject == null || + original.WorkspaceProject.Id == workspaceProject.Id)) { // Doing an update to the project should keep computed values, but mark the project as dirty if the // underlying project is newer. - var snapshot = original.WithProjectChange(underlyingProject); - _projects[underlyingProject.Id] = snapshot; + var snapshot = original.WithWorkspaceProject(workspaceProject); + _projects[workspaceProject.FilePath] = snapshot; - // Notify the background worker so it can trigger tag helper discovery. - NotifyBackgroundWorker(underlyingProject); + if (snapshot.IsInitialized && snapshot.IsDirty) + { + // We don't need to notify listeners yet because we don't have any **new** computed state. However we do + // need to trigger the background work to asynchronously compute the effect of the updates. + NotifyBackgroundWorker(snapshot.CreateUpdateContext()); + } } } - public override void ProjectsCleared() + public override void WorkspaceProjectRemoved(Project workspaceProject) { - foreach (var kvp in _projects.ToArray()) + if (workspaceProject == null) { - _projects.Remove(kvp.Key); - - // We need to notify listeners about every project removal. - NotifyListeners(new ProjectChangeEventArgs(kvp.Value, ProjectChangeKind.Removed)); + throw new ArgumentNullException(nameof(workspaceProject)); } + + _foregroundDispatcher.AssertForegroundThread(); + + if (!IsSupportedWorkspaceProject(workspaceProject)) + { + return; + } + + if (_projects.TryGetValue(workspaceProject.FilePath, out var original)) + { + // We also need to check the projectId here. If this is a multi-targeting project then we are only interested + // in a single workspace project. Make sure the WorkspaceProject we're using is the one that's being removed. + if (original.WorkspaceProject?.Id != workspaceProject.Id) + { + return; + } + + + DefaultProjectSnapshot snapshot; + + // So if the WorkspaceProject got removed, we should double check to make sure that there aren't others + // hanging around. This could happen if a project is multi-targeting and one of the TFMs is removed. + var otherWorkspaceProject = GetWorkspaceProject(workspaceProject.FilePath); + if (otherWorkspaceProject != null && otherWorkspaceProject.Id != workspaceProject.Id) + { + // OK there's another WorkspaceProject, use that. + // + // Doing an update to the project should keep computed values, but mark the project as dirty if the + // underlying project is newer. + snapshot = original.WithWorkspaceProject(otherWorkspaceProject); + _projects[workspaceProject.FilePath] = snapshot; + + if (snapshot.IsInitialized && snapshot.IsDirty) + { + // We don't need to notify listeners yet because we don't have any **new** computed state. However we do + // need to trigger the background work to asynchronously compute the effect of the updates. + NotifyBackgroundWorker(snapshot.CreateUpdateContext()); + } + + // Notify listeners of a change because it's a different WorkspaceProject. + NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed)); + + return; + } + + snapshot = original.RemoveWorkspaceProject(); + _projects[workspaceProject.FilePath] = snapshot; + + // Notify listeners of a change because we've removed computed state. + NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed)); + } + } + + public override void ReportError(Exception exception) + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + + _errorReporter.ReportError(exception); + } + + public override void ReportError(Exception exception, ProjectSnapshot project) + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + + _errorReporter.ReportError(exception, project); + } + + public override void ReportError(Exception exception, HostProject hostProject) + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + + var project = hostProject?.FilePath == null ? null : this.GetProjectWithFilePath(hostProject.FilePath); + _errorReporter.ReportError(exception, project); + } + + public override void ReportError(Exception exception, Project workspaceProject) + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + + _errorReporter.ReportError(exception, workspaceProject); + } + + // We're only interested in CSharp projects that have a FilePath. We rely on the FilePath to + // unify the Workspace Project with our HostProject concept. + private bool IsSupportedWorkspaceProject(Project workspaceProject) => workspaceProject.Language == LanguageNames.CSharp && workspaceProject.FilePath != null; + + private Project GetWorkspaceProject(string filePath) + { + var solution = Workspace.CurrentSolution; + if (solution == null) + { + return null; + } + + foreach (var workspaceProject in solution.Projects) + { + if (IsSupportedWorkspaceProject(workspaceProject) && + FilePathComparer.Instance.Equals(filePath, workspaceProject.FilePath)) + { + // We don't try to handle mulitple TFMs anwhere in Razor, just take the first WorkspaceProject that is a match. + return workspaceProject; + } + } + + return null; } // virtual so it can be overridden in tests - protected virtual void NotifyBackgroundWorker(Project project) + protected virtual void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context) { _foregroundDispatcher.AssertForegroundThread(); - - _workerQueue.Enqueue(project); + + _workerQueue.Enqueue(context); } // virtual so it can be overridden in tests @@ -221,15 +427,5 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem handler(this, e); } } - - public override void ReportError(Exception exception) - { - _errorReporter.ReportError(exception); - } - - public override void ReportError(Exception exception, Project project) - { - _errorReporter.ReportError(exception, project); - } } -} +} \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs index 8a40c50f40..fea034a6b4 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs @@ -9,25 +9,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal class DefaultProjectSnapshotWorker : ProjectSnapshotWorker { - private readonly ProjectExtensibilityConfigurationFactory _configurationFactory; private readonly ForegroundDispatcher _foregroundDispatcher; - public DefaultProjectSnapshotWorker( - ForegroundDispatcher foregroundDispatcher, - ProjectExtensibilityConfigurationFactory configurationFactory) + public DefaultProjectSnapshotWorker(ForegroundDispatcher foregroundDispatcher) { if (foregroundDispatcher == null) { throw new ArgumentNullException(nameof(foregroundDispatcher)); } - if (configurationFactory == null) - { - throw new ArgumentNullException(nameof(configurationFactory)); - } - _foregroundDispatcher = foregroundDispatcher; - _configurationFactory = configurationFactory; } public override Task ProcessUpdateAsync(ProjectSnapshotUpdateContext update, CancellationToken cancellationToken = default(CancellationToken)) @@ -46,14 +37,15 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return ProjectUpdatesCoreAsync(update); } - private async Task ProjectUpdatesCoreAsync(object state) + protected virtual void OnProcessingUpdate() { - var update = (ProjectSnapshotUpdateContext)state; + } - // We'll have more things to process here, but for now we're just hardcoding the configuration. + private Task ProjectUpdatesCoreAsync(object state) + { + OnProcessingUpdate(); - var configuration = await _configurationFactory.GetConfigurationAsync(update.UnderlyingProject); - update.Configuration = configuration; + return Task.CompletedTask; } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs index 321d7dfa7a..91317bf29d 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs @@ -26,9 +26,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - return new DefaultProjectSnapshotWorker( - _foregroundDispatcher, - languageServices.GetRequiredService()); + return new DefaultProjectSnapshotWorker(_foregroundDispatcher); } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/FallbackRazorConfiguration.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/FallbackRazorConfiguration.cs new file mode 100644 index 0000000000..707125158a --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/FallbackRazorConfiguration.cs @@ -0,0 +1,78 @@ +// 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 Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class FallbackRazorConfiguration : RazorConfiguration + { + public static readonly RazorConfiguration MVC_1_0 = new FallbackRazorConfiguration( + RazorLanguageVersion.Version_1_0, + "MVC-1.0", + new[] { new FallbackRazorExtension("MVC-1.0"), }); + + public static readonly RazorConfiguration MVC_1_1 = new FallbackRazorConfiguration( + RazorLanguageVersion.Version_1_1, + "MVC-1.1", + new[] { new FallbackRazorExtension("MVC-1.1"), }); + + public static readonly RazorConfiguration MVC_2_0 = new FallbackRazorConfiguration( + RazorLanguageVersion.Version_2_0, + "MVC-2.0", + new[] { new FallbackRazorExtension("MVC-2.0"), }); + + public static RazorConfiguration SelectConfiguration(Version version) + { + if (version.Major == 1 && version.Minor == 0) + { + return MVC_1_0; + } + else if (version.Major == 1 && version.Minor == 1) + { + return MVC_1_1; + } + else if (version.Major == 2 && version.Minor == 0) + { + return MVC_2_0; + } + else + { + return MVC_2_0; + } + } + + public FallbackRazorConfiguration( + RazorLanguageVersion languageVersion, + string configurationName, + RazorExtension[] extensions) + { + if (languageVersion == null) + { + throw new ArgumentNullException(nameof(languageVersion)); + } + + if (configurationName == null) + { + throw new ArgumentNullException(nameof(configurationName)); + } + + if (extensions == null) + { + throw new ArgumentNullException(nameof(extensions)); + } + + LanguageVersion = languageVersion; + ConfigurationName = configurationName; + Extensions = extensions; + } + + public override string ConfigurationName { get; } + + public override IReadOnlyList Extensions { get; } + + public override RazorLanguageVersion LanguageVersion { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/FallbackRazorExtension.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/FallbackRazorExtension.cs new file mode 100644 index 0000000000..5080b0705d --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/FallbackRazorExtension.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class FallbackRazorExtension : RazorExtension + { + public FallbackRazorExtension(string extensionName) + { + if (extensionName == null) + { + throw new ArgumentNullException(nameof(extensionName)); + } + + ExtensionName = extensionName; + } + + public override string ExtensionName { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostProject.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostProject.cs new file mode 100644 index 0000000000..cf3c1524b6 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostProject.cs @@ -0,0 +1,31 @@ +// 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 Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class HostProject + { + public HostProject(string projectFilePath, RazorConfiguration razorConfiguration) + { + if (projectFilePath == null) + { + throw new ArgumentNullException(nameof(projectFilePath)); + } + + if (razorConfiguration == null) + { + throw new ArgumentNullException(nameof(razorConfiguration)); + } + + FilePath = projectFilePath; + Configuration = razorConfiguration; + } + + public RazorConfiguration Configuration { get; } + + public string FilePath { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/MvcExtensibilityConfiguration.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/MvcExtensibilityConfiguration.cs deleted file mode 100644 index dfe2b88904..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/MvcExtensibilityConfiguration.cs +++ /dev/null @@ -1,86 +0,0 @@ -// 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.Linq; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.Extensions.Internal; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal class MvcExtensibilityConfiguration : ProjectExtensibilityConfiguration - { - public MvcExtensibilityConfiguration( - RazorLanguageVersion languageVersion, - ProjectExtensibilityConfigurationKind kind, - ProjectExtensibilityAssembly razorAssembly, - ProjectExtensibilityAssembly mvcAssembly) - { - if (razorAssembly == null) - { - throw new ArgumentNullException(nameof(razorAssembly)); - } - - if (mvcAssembly == null) - { - throw new ArgumentNullException(nameof(mvcAssembly)); - } - - Kind = kind; - RazorAssembly = razorAssembly; - MvcAssembly = mvcAssembly; - LanguageVersion = languageVersion; - - Assemblies = new[] { RazorAssembly, MvcAssembly, }; - } - - public override IReadOnlyList Assemblies { get; } - - // MVC: '2.0.0' (fallback) | Razor Language '2.0.0' - // or - // MVC: '2.1.3' | Razor Language '2.1.3' - public override string DisplayName => $"MVC: {MvcAssembly.Identity.Version.ToString(3)}" + (Kind == ProjectExtensibilityConfigurationKind.Fallback? " (fallback)" : string.Empty) + " | " + LanguageVersion; - - public override ProjectExtensibilityConfigurationKind Kind { get; } - - public override ProjectExtensibilityAssembly RazorAssembly { get; } - - public override RazorLanguageVersion LanguageVersion { get; } - - public ProjectExtensibilityAssembly MvcAssembly { get; } - - public override bool Equals(ProjectExtensibilityConfiguration other) - { - if (other == null) - { - return false; - } - - // We're intentionally ignoring the 'Kind' here. That's mostly for diagnostics and doesn't influence any behavior. - return LanguageVersion == other.LanguageVersion && - Enumerable.SequenceEqual( - Assemblies.OrderBy(a => a.Identity.Name).Select(a => a.Identity), - other.Assemblies.OrderBy(a => a.Identity.Name).Select(a => a.Identity), - AssemblyIdentityEqualityComparer.NameAndVersion); - } - - public override int GetHashCode() - { - var hash = new HashCodeCombiner(); - foreach (var assembly in Assemblies.OrderBy(a => a.Identity.Name)) - { - hash.Add(assembly); - } - - hash.Add(LanguageVersion); - - return hash; - } - - public override string ToString() - { - return DisplayName; - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfiguration.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfiguration.cs deleted file mode 100644 index a868c4781d..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfiguration.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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 Microsoft.AspNetCore.Razor.Language; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal abstract class ProjectExtensibilityConfiguration : IEquatable - { - public abstract IReadOnlyList Assemblies { get; } - - public abstract string DisplayName { get; } - - public abstract ProjectExtensibilityConfigurationKind Kind { get; } - - public abstract ProjectExtensibilityAssembly RazorAssembly { get; } - - public abstract RazorLanguageVersion LanguageVersion { get; } - - public abstract bool Equals(ProjectExtensibilityConfiguration other); - - public abstract override int GetHashCode(); - - public override bool Equals(object obj) - { - return Equals(obj as ProjectExtensibilityConfiguration); - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationFactory.cs deleted file mode 100644 index 71d929dd29..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal abstract class ProjectExtensibilityConfigurationFactory : ILanguageService - { - public abstract Task GetConfigurationAsync(Project project, CancellationToken cancellationToken = default(CancellationToken)); - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationKind.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationKind.cs deleted file mode 100644 index 0efc5e5e37..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationKind.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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. - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - /// - /// Describes how closely the configuration of Razor tooling matches the actual project dependencies. - /// - internal enum ProjectExtensibilityConfigurationKind - { - ApproximateMatch, - Fallback, - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs index f28e477d5d..b32a917ecd 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs @@ -1,15 +1,20 @@ // 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.Collections.Generic; using Microsoft.AspNetCore.Razor.Language; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal abstract class ProjectSnapshot { - public abstract ProjectExtensibilityConfiguration Configuration { get; } + public abstract RazorConfiguration Configuration { get; } - public abstract Project UnderlyingProject { get; } + public abstract string FilePath { get; } + + public abstract bool IsInitialized { get; } + + public abstract VersionStamp Version { get; } + + public abstract Project WorkspaceProject { get; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs index 4dd392076b..b156868fa0 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs @@ -9,20 +9,26 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { public abstract Workspace Workspace { get; } - public abstract void ProjectAdded(Project underlyingProject); - - public abstract void ProjectChanged(Project underlyingProject); - public abstract void ProjectUpdated(ProjectSnapshotUpdateContext update); - public abstract void ProjectRemoved(Project underlyingProject); + public abstract void HostProjectAdded(HostProject hostProject); - public abstract void ProjectBuildComplete(Project underlyingProject); + public abstract void HostProjectChanged(HostProject hostProject); - public abstract void ProjectsCleared(); + public abstract void HostProjectRemoved(HostProject hostProject); + + public abstract void WorkspaceProjectAdded(Project workspaceProject); + + public abstract void WorkspaceProjectChanged(Project workspaceProject); + + public abstract void WorkspaceProjectRemoved(Project workspaceProject); public abstract void ReportError(Exception exception); + + public abstract void ReportError(Exception exception, ProjectSnapshot project); - public abstract void ReportError(Exception exception, Project project); + public abstract void ReportError(Exception exception, HostProject hostProject); + + public abstract void ReportError(Exception exception, Project workspaceProject); } -} +} \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerExtensions.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerExtensions.cs index c926021eb0..579a42ffcb 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerExtensions.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerExtensions.cs @@ -13,7 +13,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem for (var i = 0; i< projects.Count; i++) { var project = projects[i]; - if (string.Equals(filePath, project.UnderlyingProject.FilePath, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(filePath, project.WorkspaceProject.FilePath, StringComparison.OrdinalIgnoreCase)) { return project; } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs index 83357e9968..23cc5066da 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs @@ -9,18 +9,35 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal class ProjectSnapshotUpdateContext { - public ProjectSnapshotUpdateContext(Project underlyingProject) + public ProjectSnapshotUpdateContext(string filePath, HostProject hostProject, Project workspaceProject, VersionStamp version) { - if (underlyingProject == null) + if (filePath == null) { - throw new ArgumentNullException(nameof(underlyingProject)); + throw new ArgumentNullException(nameof(filePath)); } - UnderlyingProject = underlyingProject; + if (hostProject == null) + { + throw new ArgumentNullException(nameof(hostProject)); + } + + if (workspaceProject == null) + { + throw new ArgumentNullException(nameof(workspaceProject)); + } + + FilePath = filePath; + HostProject = hostProject; + WorkspaceProject = workspaceProject; + Version = version; } - public Project UnderlyingProject { get; } + public string FilePath { get; } - public ProjectExtensibilityConfiguration Configuration { get; set; } + public HostProject HostProject { get; } + + public Project WorkspaceProject { get; } + + public VersionStamp Version { get; } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorkerQueue.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorkerQueue.cs index 0dcbe91284..69c0062f05 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorkerQueue.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorkerQueue.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -16,7 +15,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private readonly DefaultProjectSnapshotManager _projectManager; private readonly ProjectSnapshotWorker _projectWorker; - private readonly Dictionary _projects; + private readonly Dictionary _projects; private Timer _timer; public ProjectSnapshotWorkerQueue(ForegroundDispatcher foregroundDispatcher, DefaultProjectSnapshotManager projectManager, ProjectSnapshotWorker projectWorker) @@ -40,7 +39,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _projectManager = projectManager; _projectWorker = projectWorker; - _projects = new Dictionary(); + _projects = new Dictionary(FilePathComparer.Instance); } public bool HasPendingNotifications @@ -93,11 +92,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } } - public void Enqueue(Project project) + public void Enqueue(ProjectSnapshotUpdateContext context) { - if (project == null) + if (context == null) { - throw new ArgumentNullException(); + throw new ArgumentNullException(nameof(context)); } _foregroundDispatcher.AssertForegroundThread(); @@ -106,7 +105,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // We only want to store the last 'seen' version of any given project. That way when we pick one to process // it's always the best version to use. - _projects[project.Id] = project; + _projects[context.FilePath] = context; StartWorker(); } @@ -133,7 +132,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem OnStartingBackgroundWork(); - Project[] work; + ProjectSnapshotUpdateContext[] work; lock (_projects) { work = _projects.Values.ToArray(); @@ -145,7 +144,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { try { - updates[i] = (new ProjectSnapshotUpdateContext(work[i]), null); + updates[i] = (work[i], null); await _projectWorker.ProcessUpdateAsync(updates[i].context); } catch (Exception projectException) @@ -196,7 +195,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } else { - _projectManager.ReportError(update.exception, update.context?.UnderlyingProject); + _projectManager.ReportError(update.exception, update.context?.WorkspaceProject); } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSystemRazorConfiguration.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSystemRazorConfiguration.cs new file mode 100644 index 0000000000..43dfebe864 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSystemRazorConfiguration.cs @@ -0,0 +1,43 @@ +// 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 Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class ProjectSystemRazorConfiguration : RazorConfiguration + { + public ProjectSystemRazorConfiguration( + RazorLanguageVersion languageVersion, + string configurationName, + RazorExtension[] extensions) + { + if (languageVersion == null) + { + throw new ArgumentNullException(nameof(languageVersion)); + } + + if (configurationName == null) + { + throw new ArgumentNullException(nameof(configurationName)); + } + + if (extensions == null) + { + throw new ArgumentNullException(nameof(extensions)); + } + + LanguageVersion = languageVersion; + ConfigurationName = configurationName; + Extensions = extensions; + } + + public override string ConfigurationName { get; } + + public override IReadOnlyList Extensions { get; } + + public override RazorLanguageVersion LanguageVersion { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSystemRazorExtension.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSystemRazorExtension.cs new file mode 100644 index 0000000000..77f742c563 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSystemRazorExtension.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class ProjectSystemRazorExtension : RazorExtension + { + public ProjectSystemRazorExtension(string extensionName) + { + if (extensionName == null) + { + throw new ArgumentNullException(nameof(extensionName)); + } + + ExtensionName = extensionName; + } + + public override string ExtensionName { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs index 1769da0197..fb2deeec32 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs @@ -23,51 +23,43 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { Debug.Assert(solution != null); - _projectManager.ProjectsCleared(); - foreach (var project in solution.Projects) { - if (project.Language == LanguageNames.CSharp) - { - _projectManager.ProjectAdded(project); - } + _projectManager.WorkspaceProjectAdded(project); } } // Internal for testing internal void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e) { - Project underlyingProject; + Project project; switch (e.Kind) { case WorkspaceChangeKind.ProjectAdded: { - underlyingProject = e.NewSolution.GetProject(e.ProjectId); - Debug.Assert(underlyingProject != null); + project = e.NewSolution.GetProject(e.ProjectId); + Debug.Assert(project != null); - if (underlyingProject.Language == LanguageNames.CSharp) - { - _projectManager.ProjectAdded(underlyingProject); - } + _projectManager.WorkspaceProjectAdded(project); break; } case WorkspaceChangeKind.ProjectChanged: case WorkspaceChangeKind.ProjectReloaded: { - underlyingProject = e.NewSolution.GetProject(e.ProjectId); - Debug.Assert(underlyingProject != null); + project = e.NewSolution.GetProject(e.ProjectId); + Debug.Assert(project != null); - _projectManager.ProjectChanged(underlyingProject); + _projectManager.WorkspaceProjectChanged(project); break; } case WorkspaceChangeKind.ProjectRemoved: { - underlyingProject = e.OldSolution.GetProject(e.ProjectId); - Debug.Assert(underlyingProject != null); + project = e.OldSolution.GetProject(e.ProjectId); + Debug.Assert(project != null); - _projectManager.ProjectRemoved(underlyingProject); + _projectManager.WorkspaceProjectRemoved(project); break; } @@ -76,6 +68,15 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem case WorkspaceChangeKind.SolutionCleared: case WorkspaceChangeKind.SolutionReloaded: case WorkspaceChangeKind.SolutionRemoved: + + if (e.OldSolution != null) + { + foreach (var p in e.OldSolution.Projects) + { + _projectManager.WorkspaceProjectRemoved(p); + } + } + InitializeSolution(e.NewSolution); break; } diff --git a/src/Microsoft.CodeAnalysis.Razor/FilePathComparer.cs b/src/Microsoft.CodeAnalysis.Razor/FilePathComparer.cs new file mode 100644 index 0000000000..a0ca3cb9a3 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor/FilePathComparer.cs @@ -0,0 +1,30 @@ +// 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.Runtime.InteropServices; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal static class FilePathComparer + { + private static StringComparer _instance; + + public static StringComparer Instance + { + get + { + if (_instance == null && RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + _instance = StringComparer.Ordinal; + } + else if (_instance == null) + { + _instance = StringComparer.OrdinalIgnoreCase; + } + + return _instance; + } + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs index f97abf0a3b..4f949d7ecd 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs @@ -4,7 +4,6 @@ using System; using System.IO; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; @@ -14,11 +13,7 @@ namespace Microsoft.VisualStudio.Editor.Razor { internal class DefaultProjectEngineFactoryService : RazorProjectEngineFactoryService { - private readonly static MvcExtensibilityConfiguration DefaultConfiguration = new MvcExtensibilityConfiguration( - RazorLanguageVersion.Version_2_0, - ProjectExtensibilityConfigurationKind.Fallback, - new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version("2.0.0.0"))), - new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0")))); + private readonly static RazorConfiguration DefaultConfiguration = FallbackRazorConfiguration.MVC_2_0; private readonly ProjectSnapshotManager _projectManager; @@ -41,22 +36,19 @@ namespace Microsoft.VisualStudio.Editor.Razor // In 15.5 we expect projectPath to be a directory, NOT the path to the csproj. var project = FindProject(projectPath); - var configuration = (project?.Configuration as MvcExtensibilityConfiguration) ?? DefaultConfiguration; - var razorLanguageVersion = configuration.LanguageVersion; - - var razorConfiguration = new RazorConfiguration(razorLanguageVersion, "unnamed", Array.Empty()); + var configuration = project?.Configuration ?? DefaultConfiguration; var fileSystem = RazorProjectFileSystem.Create(projectPath); RazorProjectEngine projectEngine; - if (razorLanguageVersion.Major == 1) + if (configuration.LanguageVersion.Major == 1) { - projectEngine = RazorProjectEngine.Create(razorConfiguration, fileSystem, b => + projectEngine = RazorProjectEngine.Create(configuration, fileSystem, b => { configure?.Invoke(b); Mvc1_X.RazorExtensions.Register(b); - if (configuration.MvcAssembly.Identity.Version.Minor >= 1) + if (configuration.LanguageVersion.Minor >= 1) { Mvc1_X.RazorExtensions.RegisterViewComponentTagHelpers(b); } @@ -64,7 +56,7 @@ namespace Microsoft.VisualStudio.Editor.Razor } else { - projectEngine = RazorProjectEngine.Create(razorConfiguration, fileSystem, b => + projectEngine = RazorProjectEngine.Create(configuration, fileSystem, b => { configure?.Invoke(b); @@ -83,9 +75,9 @@ namespace Microsoft.VisualStudio.Editor.Razor for (var i = 0; i < projects.Count; i++) { var project = projects[i]; - if (project.UnderlyingProject.FilePath != null) + if (project.WorkspaceProject?.FilePath != null) { - if (string.Equals(directory, NormalizeDirectoryPath(Path.GetDirectoryName(project.UnderlyingProject.FilePath)), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(directory, NormalizeDirectoryPath(Path.GetDirectoryName(project.WorkspaceProject.FilePath)), StringComparison.OrdinalIgnoreCase)) { return project; } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs index eb771205d1..f55ee5f70e 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs @@ -77,7 +77,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor _textViews = new List(); } - internal override ProjectExtensibilityConfiguration Configuration => _project.Configuration; + public override RazorConfiguration Configuration => _project.Configuration; public override EditorSettings EditorSettings => _editorSettingsManager.Current; @@ -85,7 +85,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public override bool IsSupportedProject => _isSupportedProject; - public override Project Project => _workspace.CurrentSolution.GetProject(_project.UnderlyingProject.Id); + public override Project Project => _workspace.CurrentSolution.GetProject(_project.WorkspaceProject.Id); public override ITextBuffer TextBuffer => _textBuffer; @@ -206,7 +206,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) { if (_projectPath != null && - string.Equals(_projectPath, e.Project.UnderlyingProject.FilePath, StringComparison.OrdinalIgnoreCase)) + string.Equals(_projectPath, e.Project.FilePath, StringComparison.OrdinalIgnoreCase)) { if (e.Kind == ProjectChangeKind.TagHelpersChanged) { diff --git a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs index efd1016bd1..7cde4da716 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.Editor; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -16,7 +15,7 @@ namespace Microsoft.VisualStudio.Editor.Razor { public abstract event EventHandler ContextChanged; - internal abstract ProjectExtensibilityConfiguration Configuration { get; } + public abstract RazorConfiguration Configuration { get; } public abstract EditorSettings EditorSettings { get; } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj b/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj index 5f058e97e1..d2dcdd7ecc 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj @@ -11,6 +11,8 @@ + + @@ -26,7 +28,7 @@ - + @@ -35,4 +37,82 @@ + + + + + + + + + + + + RazorConfiguration.xaml + + + RazorExtension.xaml + + + RazorGeneral.xaml + + + XamlRuleToCode:RazorConfiguration.xaml + + + XamlRuleToCode:RazorExtension.xaml + + + XamlRuleToCode:RazorGeneral.xaml + + + + + Designer + MSBuild:GenerateRuleSourceFromXaml + Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules + RazorProjectProperties + + ProjectSystem\Rules\ + + + Designer + MSBuild:GenerateRuleSourceFromXaml + Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules + RazorProjectProperties + ProjectSystem\Rules\ + + + Designer + MSBuild:GenerateRuleSourceFromXaml + Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules + RazorProjectProperties + ProjectSystem\Rules\ + + + RazorGeneral.xaml + + + RazorGeneral.xaml + + + RazorGeneral.xaml + + + + + + + + + diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs new file mode 100644 index 0000000000..f68ae2cd11 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs @@ -0,0 +1,125 @@ +// 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.ComponentModel.Composition; +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; + +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 }); + } + + 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) + { + await ExecuteWithLock(async () => + { + if (IsDisposing || IsDisposed) + { + return; + } + + var languageVersion = update.Value.CurrentState[Rules.RazorGeneral.SchemaName].Properties[Rules.RazorGeneral.RazorLangVersionProperty]; + var defaultConfiguration = update.Value.CurrentState[Rules.RazorGeneral.SchemaName].Properties[Rules.RazorGeneral.RazorDefaultConfigurationProperty]; + + RazorConfiguration configuration = null; + if (!string.IsNullOrEmpty(languageVersion) && !string.IsNullOrEmpty(defaultConfiguration)) + { + if (!RazorLanguageVersion.TryParse(languageVersion, out var parsedVersion)) + { + parsedVersion = RazorLanguageVersion.Latest; + } + + var extensions = update.Value.CurrentState[Rules.RazorExtension.PrimaryDataSourceItemType].Items.Select(e => + { + return new ProjectSystemRazorExtension(e.Key); + }).ToArray(); + + var configurations = update.Value.CurrentState[Rules.RazorConfiguration.PrimaryDataSourceItemType].Items.Select(c => + { + var includedExtensions = c.Value[Rules.RazorConfiguration.ExtensionsProperty] + .Split(';') + .Select(name => extensions.Where(e => e.ExtensionName == name).FirstOrDefault()) + .Where(e => e != null) + .ToArray(); + + return new ProjectSystemRazorConfiguration(parsedVersion, c.Key, includedExtensions); + }).ToArray(); + + configuration = configurations.Where(c => c.ConfigurationName == defaultConfiguration).FirstOrDefault(); + } + + if (configuration == null) + { + // Ok we can't find a language version. Let's assume this project isn't using Razor then. + await UpdateProjectUnsafeAsync(null).ConfigureAwait(false); + return; + } + + var hostProject = new HostProject(CommonServices.UnconfiguredProject.FullPath, configuration); + await UpdateProjectUnsafeAsync(hostProject).ConfigureAwait(false); + }); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs new file mode 100644 index 0000000000..60dd780e51 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs @@ -0,0 +1,142 @@ +// 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.ComponentModel.Composition; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Microsoft.VisualStudio.LanguageServices; +using Microsoft.VisualStudio.ProjectSystem; +using ResolvedCompilationReference = Microsoft.CodeAnalysis.Razor.ProjectSystem.ManageProjectSystemSchema.ResolvedCompilationReference; + +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 does not provides configuration support (SDK < 2.1). + [AppliesTo("(DotNetCoreRazor | DotNetCoreWeb) & !DotNetCoreRazorConfiguration")] + [Export(ExportContractNames.Scopes.UnconfiguredProject, typeof(IProjectDynamicLoadComponent))] + internal class FallbackRazorProjectHost : RazorProjectHostBase + { + private const string MvcAssemblyName = "Microsoft.AspNetCore.Mvc.Razor"; + private const string MvcAssemblyFileName = "Microsoft.AspNetCore.Mvc.Razor.dll"; + + private IDisposable _subscription; + + [ImportingConstructor] + public FallbackRazorProjectHost( + IUnconfiguredProjectCommonServices commonServices, + [Import(typeof(VisualStudioWorkspace))] Workspace workspace) + : base(commonServices, workspace) + { + } + + // Internal for testing + internal FallbackRazorProjectHost( + 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. + var receiver = new ActionBlock>(OnProjectChanged); + _subscription = CommonServices.ActiveConfiguredProjectSubscription.JointRuleSource.SourceBlock.LinkTo( + receiver, + initialDataAsNew: true, + suppressVersionOnlyUpdates: true, + ruleNames: new string[] { ResolvedCompilationReference.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) + { + await ExecuteWithLock(async () => + { + if (IsDisposing || IsDisposed) + { + return; + } + + string mvcReferenceFullPath = null; + var references = update.Value.CurrentState[ResolvedCompilationReference.SchemaName].Items; + foreach (var reference in references) + { + if (reference.Key.EndsWith(MvcAssemblyFileName, StringComparison.OrdinalIgnoreCase)) + { + mvcReferenceFullPath = reference.Key; + break; + } + } + + if (mvcReferenceFullPath == null) + { + // Ok we can't find an MVC version. Let's assume this project isn't using Razor then. + await UpdateProjectUnsafeAsync(null).ConfigureAwait(false); + return; + } + + var version = GetAssemblyVersion(mvcReferenceFullPath); + if (version == null) + { + // Ok we can't find an MVC version. Let's assume this project isn't using Razor then. + await UpdateProjectUnsafeAsync(null).ConfigureAwait(false); + return; + } + + var configuration = FallbackRazorConfiguration.SelectConfiguration(version); + var hostProject = new HostProject(CommonServices.UnconfiguredProject.FullPath, configuration); + await UpdateProjectUnsafeAsync(hostProject).ConfigureAwait(false); + }); + } + + // virtual for overriding in tests + protected virtual Version GetAssemblyVersion(string filePath) + { + return ReadAssemblyVersion(filePath); + } + + private static Version ReadAssemblyVersion(string filePath) + { + try + { + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) + using (var reader = new PEReader(stream)) + { + var metadataReader = reader.GetMetadataReader(); + + var assemblyDefinition = metadataReader.GetAssemblyDefinition(); + return assemblyDefinition.Version; + } + } + catch + { + // We're purposely silencing any kinds of I/O exceptions here, just in case something wacky is going on. + return null; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IUnconfiguredProjectCommonServices.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IUnconfiguredProjectCommonServices.cs new file mode 100644 index 0000000000..1e98e9c732 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IUnconfiguredProjectCommonServices.cs @@ -0,0 +1,88 @@ +// 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.ComponentModel.Composition; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.References; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + [Export(typeof(IUnconfiguredProjectCommonServices))] + internal class UnconfiguredProjectCommonServices : IUnconfiguredProjectCommonServices + { + private readonly ActiveConfiguredProject _activeConfiguredProject; + private readonly ActiveConfiguredProject _activeConfiguredProjectAssemblyReferences; + private readonly ActiveConfiguredProject _activeConfiguredProjectPackageReferences; + private readonly ActiveConfiguredProject _activeConfiguredProjectProperties; + + [ImportingConstructor] + public UnconfiguredProjectCommonServices( + IProjectThreadingService threadingService, + UnconfiguredProject unconfiguredProject, + IActiveConfiguredProjectSubscriptionService activeConfiguredProjectSubscription, + ActiveConfiguredProject activeConfiguredProject, + ActiveConfiguredProject activeConfiguredProjectAssemblyReferences, + ActiveConfiguredProject activeConfiguredProjectPackageReferences, + ActiveConfiguredProject activeConfiguredProjectRazorProperties) + { + if (threadingService == null) + { + throw new ArgumentNullException(nameof(threadingService)); + } + + if (unconfiguredProject == null) + { + throw new ArgumentNullException(nameof(unconfiguredProject)); + } + + if (activeConfiguredProjectSubscription == null) + { + throw new ArgumentNullException(nameof(ActiveConfiguredProjectSubscription)); + } + + if (activeConfiguredProject == null) + { + throw new ArgumentNullException(nameof(activeConfiguredProject)); + } + + if (activeConfiguredProjectAssemblyReferences == null) + { + throw new ArgumentNullException(nameof(activeConfiguredProjectAssemblyReferences)); + } + + if (activeConfiguredProjectPackageReferences == null) + { + throw new ArgumentNullException(nameof(activeConfiguredProjectPackageReferences)); + } + + if (activeConfiguredProjectRazorProperties == null) + { + throw new ArgumentNullException(nameof(activeConfiguredProjectRazorProperties)); + } + + ThreadingService = threadingService; + UnconfiguredProject = unconfiguredProject; + ActiveConfiguredProjectSubscription = activeConfiguredProjectSubscription; + _activeConfiguredProject = activeConfiguredProject; + _activeConfiguredProjectAssemblyReferences = activeConfiguredProjectAssemblyReferences; + _activeConfiguredProjectPackageReferences = activeConfiguredProjectPackageReferences; + _activeConfiguredProjectProperties = activeConfiguredProjectRazorProperties; + } + + public ConfiguredProject ActiveConfiguredProject => _activeConfiguredProject.Value; + + public IAssemblyReferencesService ActiveConfiguredProjectAssemblyReferences => _activeConfiguredProjectAssemblyReferences.Value; + + public IPackageReferencesService ActiveConfiguredProjectPackageReferences => _activeConfiguredProjectPackageReferences.Value; + + public Rules.RazorProjectProperties ActiveConfiguredProjectRazorProperties => _activeConfiguredProjectProperties.Value; + + public IActiveConfiguredProjectSubscriptionService ActiveConfiguredProjectSubscription { get; } + + + public IProjectThreadingService ThreadingService { get; } + + public UnconfiguredProject UnconfiguredProject { get; } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManageProjectSystemSchema.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManageProjectSystemSchema.cs new file mode 100644 index 0000000000..79138c8ac6 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManageProjectSystemSchema.cs @@ -0,0 +1,16 @@ +// 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. + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + // Well-Known Schema and property names defined by the ManagedProjectSystem + internal static class ManageProjectSystemSchema + { + public static class ResolvedCompilationReference + { + public static readonly string SchemaName = "ResolvedCompilationReference"; + + public static readonly string ItemName = "ResolvedCompilationReference"; + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs new file mode 100644 index 0000000000..5419eb73af --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs @@ -0,0 +1,190 @@ +// 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.ComponentModel.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.LanguageServices; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal abstract class RazorProjectHostBase : OnceInitializedOnceDisposedAsync, IProjectDynamicLoadComponent + { + private readonly Workspace _workspace; + private readonly AsyncSemaphore _lock; + + private ProjectSnapshotManagerBase _projectManager; + private HostProject _current; + + public RazorProjectHostBase( + IUnconfiguredProjectCommonServices commonServices, + [Import(typeof(VisualStudioWorkspace))] Workspace workspace) + : base(commonServices.ThreadingService.JoinableTaskContext) + { + if (commonServices == null) + { + throw new ArgumentNullException(nameof(commonServices)); + } + + if (workspace == null) + { + throw new ArgumentNullException(nameof(workspace)); + } + + CommonServices = commonServices; + _workspace = workspace; + + _lock = new AsyncSemaphore(initialCount: 1); + } + + // Internal for testing + protected RazorProjectHostBase( + IUnconfiguredProjectCommonServices commonServices, + Workspace workspace, + ProjectSnapshotManagerBase projectManager) + : base(commonServices.ThreadingService.JoinableTaskContext) + { + if (commonServices == null) + { + throw new ArgumentNullException(nameof(commonServices)); + } + + if (workspace == null) + { + throw new ArgumentNullException(nameof(workspace)); + } + + if (projectManager == null) + { + throw new ArgumentNullException(nameof(projectManager)); + } + + CommonServices = commonServices; + _workspace = workspace; + _projectManager = projectManager; + + _lock = new AsyncSemaphore(initialCount: 1); + } + + protected IUnconfiguredProjectCommonServices CommonServices { get; } + + // internal for tests. The product will call through the IProjectDynamicLoadComponent interface. + internal Task LoadAsync() + { + return InitializeAsync(); + } + + protected override Task InitializeCoreAsync(CancellationToken cancellationToken) + { + CommonServices.UnconfiguredProject.ProjectRenaming += UnconfiguredProject_ProjectRenaming; + + return Task.CompletedTask; + } + + protected override async Task DisposeCoreAsync(bool initialized) + { + if (initialized) + { + CommonServices.UnconfiguredProject.ProjectRenaming -= UnconfiguredProject_ProjectRenaming; + + await ExecuteWithLock(async () => + { + if (_current != null) + { + await UpdateProjectUnsafeAsync(null).ConfigureAwait(false); + } + }); + } + } + + // Internal for tests + internal async Task OnProjectRenamingAsync() + { + // When a project gets renamed we expect any rules watched by the derived class to fire. + // + // However, the project snapshot manager uses the project Fullpath as the key. We want to just + // reinitialize the HostProject with the same configuration and settings here, but the updated + // FilePath. + await ExecuteWithLock(async () => + { + if (_current != null) + { + var old = _current; + await UpdateProjectUnsafeAsync(null).ConfigureAwait(false); + + var filePath = CommonServices.UnconfiguredProject.FullPath; + await UpdateProjectUnsafeAsync(new HostProject(filePath, old.Configuration)).ConfigureAwait(false); + } + }); + } + + // Should only be called from the UI thread. + private ProjectSnapshotManagerBase GetProjectManager() + { + CommonServices.ThreadingService.VerifyOnUIThread(); + + if (_projectManager == null) + { + _projectManager = (ProjectSnapshotManagerBase)_workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); + } + + return _projectManager; + } + + // Must be called inside the lock. + protected async Task UpdateProjectUnsafeAsync(HostProject project) + { + await CommonServices.ThreadingService.SwitchToUIThread(); + var projectManager = GetProjectManager(); + + if (_current == null && project == null) + { + // This is a no-op. This project isn't using Razor. + } + else if (_current == null && project != null) + { + projectManager.HostProjectAdded(project); + } + else if (_current != null && project == null) + { + projectManager.HostProjectRemoved(_current); + } + else + { + projectManager.HostProjectChanged(project); + } + + _current = project; + } + + protected async Task ExecuteWithLock(Func func) + { + using (JoinableCollection.Join()) + { + using (await _lock.EnterAsync().ConfigureAwait(false)) + { + var task = JoinableFactory.RunAsync(func); + await task.Task.ConfigureAwait(false); + } + } + } + + Task IProjectDynamicLoadComponent.LoadAsync() + { + return InitializeAsync(); + } + + Task IProjectDynamicLoadComponent.UnloadAsync() + { + return DisposeAsync(); + } + + private async Task UnconfiguredProject_ProjectRenaming(object sender, ProjectRenamedEventArgs args) + { + await OnProjectRenamingAsync().ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.cs new file mode 100644 index 0000000000..85912aed47 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.cs @@ -0,0 +1,212 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules { + + + internal partial class RazorConfiguration { + + /// Backing field for deserialized rule.. + private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule; + + /// The name of the schema to look for at runtime to fulfill property access. + internal const string SchemaName = "RazorConfiguration"; + + /// The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceItemType = "RazorConfiguration"; + + /// The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceLabel = ""; + + /// Razor Extensions (The "Extensions" property). + internal const string ExtensionsProperty = "Extensions"; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule; + + /// Backing field for the file name of the rule property. + private string file; + + /// Backing field for the ItemType property. + private string itemType; + + /// Backing field for the ItemName property. + private string itemName; + + /// Configured Project + private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject; + + /// The dictionary of named catalogs. + private System.Collections.Immutable.IImmutableDictionary catalogs; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule; + + /// Thread locking object + private object locker = new object(); + + /// Initializes a new instance of the RazorConfiguration class. + internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) { + this.rule = rule; + } + + /// Initializes a new instance of the RazorConfiguration class. + internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, string file, string itemType, string itemName) : + this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) { + if ((configuredProject == null)) { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.catalogs = catalogs; + this.file = file; + this.itemType = itemType; + this.itemName = itemName; + } + + /// Initializes a new instance of the RazorConfiguration class. + internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) : + this(rule) { + if ((rule == null)) { + throw new System.ArgumentNullException("rule"); + } + if ((configuredProject == null)) { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.rule = rule; + this.file = this.rule.File; + this.itemType = this.rule.ItemType; + this.itemName = this.rule.ItemName; + } + + /// Initializes a new instance of the RazorConfiguration class. + internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) : + this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) { + } + + /// Initializes a new instance of the RazorConfiguration class that assumes a project context (neither property sheet nor items). + internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs) : + this(configuredProject, catalogs, "Project", null, null, null) { + } + + /// Gets the IRule used to get and set properties. + public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule { + get { + return this.rule; + } + } + + /// Razor Extensions + internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty Extensions { + get { + Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; + if ((localRule == null)) { + localRule = this.GeneratedFallbackRule; + } + if ((localRule == null)) { + return null; + } + Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(ExtensionsProperty))); + if (((property == null) + && (this.GeneratedFallbackRule != null))) { + localRule = this.GeneratedFallbackRule; + property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(ExtensionsProperty))); + } + return property; + } + } + + /// Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule { + get { + if (((this.fallbackRule == null) + && (this.configuredProject != null))) { + System.Threading.Monitor.Enter(this.locker); + try { + if ((this.fallbackRule == null)) { + this.InitializeFallbackRule(); + } + } + finally { + System.Threading.Monitor.Exit(this.locker); + } + } + return this.fallbackRule; + } + } + + private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) { + if ((catalog == null)) { + return null; + } + return catalog.BindToContext(SchemaName, file, itemType, itemName); + } + + private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) { + if ((propertiesContext.IsProjectFile == true)) { + return null; + } + else { + return propertiesContext.File; + } + } + + private void InitializeFallbackRule() { + if ((this.configuredProject == null)) { + return; + } + Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorConfiguration.deserializedFallbackRule; + if ((unboundRule == null)) { + System.IO.Stream xamlStream = null; + System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); + try { + xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorConfiguration.xaml"); + Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream))); + System.Collections.Generic.IEnumerator ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator(); + for ( + ; ((unboundRule == null) + && ruleEnumerator.MoveNext()); + ) { + Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current)); + if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) { + unboundRule = t; + unboundRule.Name = "843bc0bc-5265-4864-9b06-f5d5503b0484"; + RazorConfiguration.deserializedFallbackRule = unboundRule; + } + } + } + finally { + if ((xamlStream != null)) { + ((System.IDisposable)(xamlStream)).Dispose(); + } + } + } + this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext"); + Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext"); + this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName); + } + } + + internal partial class RazorProjectProperties { + + private static System.Func>, object, RazorConfiguration> CreateRazorConfigurationPropertiesDelegate = new System.Func>, object, RazorConfiguration>(CreateRazorConfigurationProperties); + + private static RazorConfiguration CreateRazorConfigurationProperties(System.Threading.Tasks.Task> namedCatalogs, object state) { + RazorProjectProperties that = ((RazorProjectProperties)(state)); + return new RazorConfiguration(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName); + } + + /// Gets the strongly-typed property accessor used to get and set Configuration Properties properties. + internal System.Threading.Tasks.Task GetRazorConfigurationPropertiesAsync() { + System.Threading.Tasks.Task> namedCatalogsTask = this.GetNamedCatalogsAsync(); + return namedCatalogsTask.ContinueWith(CreateRazorConfigurationPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.xaml b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.xaml new file mode 100644 index 0000000000..9632054a75 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.cs new file mode 100644 index 0000000000..f73cc3fdb1 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.cs @@ -0,0 +1,235 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules { + + + internal partial class RazorExtension { + + /// Backing field for deserialized rule.. + private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule; + + /// The name of the schema to look for at runtime to fulfill property access. + internal const string SchemaName = "RazorExtension"; + + /// The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceItemType = "RazorExtension"; + + /// The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceLabel = ""; + + /// Razor Extension Assembly Name (The "AssemblyName" property). + internal const string AssemblyNameProperty = "AssemblyName"; + + /// Razor Extension Assembly File Path (The "AssemblyFilePath" property). + internal const string AssemblyFilePathProperty = "AssemblyFilePath"; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule; + + /// Backing field for the file name of the rule property. + private string file; + + /// Backing field for the ItemType property. + private string itemType; + + /// Backing field for the ItemName property. + private string itemName; + + /// Configured Project + private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject; + + /// The dictionary of named catalogs. + private System.Collections.Immutable.IImmutableDictionary catalogs; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule; + + /// Thread locking object + private object locker = new object(); + + /// Initializes a new instance of the RazorExtension class. + internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) { + this.rule = rule; + } + + /// Initializes a new instance of the RazorExtension class. + internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, string file, string itemType, string itemName) : + this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) { + if ((configuredProject == null)) { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.catalogs = catalogs; + this.file = file; + this.itemType = itemType; + this.itemName = itemName; + } + + /// Initializes a new instance of the RazorExtension class. + internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) : + this(rule) { + if ((rule == null)) { + throw new System.ArgumentNullException("rule"); + } + if ((configuredProject == null)) { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.rule = rule; + this.file = this.rule.File; + this.itemType = this.rule.ItemType; + this.itemName = this.rule.ItemName; + } + + /// Initializes a new instance of the RazorExtension class. + internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) : + this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) { + } + + /// Initializes a new instance of the RazorExtension class that assumes a project context (neither property sheet nor items). + internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs) : + this(configuredProject, catalogs, "Project", null, null, null) { + } + + /// Gets the IRule used to get and set properties. + public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule { + get { + return this.rule; + } + } + + /// Razor Extension Assembly Name + internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty AssemblyName { + get { + Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; + if ((localRule == null)) { + localRule = this.GeneratedFallbackRule; + } + if ((localRule == null)) { + return null; + } + Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyNameProperty))); + if (((property == null) + && (this.GeneratedFallbackRule != null))) { + localRule = this.GeneratedFallbackRule; + property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyNameProperty))); + } + return property; + } + } + + /// Razor Extension Assembly File Path + internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty AssemblyFilePath { + get { + Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; + if ((localRule == null)) { + localRule = this.GeneratedFallbackRule; + } + if ((localRule == null)) { + return null; + } + Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyFilePathProperty))); + if (((property == null) + && (this.GeneratedFallbackRule != null))) { + localRule = this.GeneratedFallbackRule; + property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyFilePathProperty))); + } + return property; + } + } + + /// Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule { + get { + if (((this.fallbackRule == null) + && (this.configuredProject != null))) { + System.Threading.Monitor.Enter(this.locker); + try { + if ((this.fallbackRule == null)) { + this.InitializeFallbackRule(); + } + } + finally { + System.Threading.Monitor.Exit(this.locker); + } + } + return this.fallbackRule; + } + } + + private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) { + if ((catalog == null)) { + return null; + } + return catalog.BindToContext(SchemaName, file, itemType, itemName); + } + + private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) { + if ((propertiesContext.IsProjectFile == true)) { + return null; + } + else { + return propertiesContext.File; + } + } + + private void InitializeFallbackRule() { + if ((this.configuredProject == null)) { + return; + } + Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorExtension.deserializedFallbackRule; + if ((unboundRule == null)) { + System.IO.Stream xamlStream = null; + System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); + try { + xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorExtension.xaml"); + Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream))); + System.Collections.Generic.IEnumerator ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator(); + for ( + ; ((unboundRule == null) + && ruleEnumerator.MoveNext()); + ) { + Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current)); + if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) { + unboundRule = t; + unboundRule.Name = "10b26d1c-5ab7-4ca2-ab64-182fe18d53dd"; + RazorExtension.deserializedFallbackRule = unboundRule; + } + } + } + finally { + if ((xamlStream != null)) { + ((System.IDisposable)(xamlStream)).Dispose(); + } + } + } + this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext"); + Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext"); + this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName); + } + } + + internal partial class RazorProjectProperties { + + private static System.Func>, object, RazorExtension> CreateRazorExtensionPropertiesDelegate = new System.Func>, object, RazorExtension>(CreateRazorExtensionProperties); + + private static RazorExtension CreateRazorExtensionProperties(System.Threading.Tasks.Task> namedCatalogs, object state) { + RazorProjectProperties that = ((RazorProjectProperties)(state)); + return new RazorExtension(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName); + } + + /// Gets the strongly-typed property accessor used to get and set Extension Properties properties. + internal System.Threading.Tasks.Task GetRazorExtensionPropertiesAsync() { + System.Threading.Tasks.Task> namedCatalogsTask = this.GetNamedCatalogsAsync(); + return namedCatalogsTask.ContinueWith(CreateRazorExtensionPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.xaml b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.xaml new file mode 100644 index 0000000000..f68eef7107 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.cs new file mode 100644 index 0000000000..f28d4a90c3 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.cs @@ -0,0 +1,235 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules { + + + internal partial class RazorGeneral { + + /// Backing field for deserialized rule.. + private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule; + + /// The name of the schema to look for at runtime to fulfill property access. + internal const string SchemaName = "RazorGeneral"; + + /// The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceItemType = null; + + /// The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceLabel = ""; + + /// Razor Language Version (The "RazorLangVersion" property). + internal const string RazorLangVersionProperty = "RazorLangVersion"; + + /// Razor Configuration Name (The "RazorDefaultConfiguration" property). + internal const string RazorDefaultConfigurationProperty = "RazorDefaultConfiguration"; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule; + + /// Backing field for the file name of the rule property. + private string file; + + /// Backing field for the ItemType property. + private string itemType; + + /// Backing field for the ItemName property. + private string itemName; + + /// Configured Project + private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject; + + /// The dictionary of named catalogs. + private System.Collections.Immutable.IImmutableDictionary catalogs; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule; + + /// Thread locking object + private object locker = new object(); + + /// Initializes a new instance of the RazorGeneral class. + internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) { + this.rule = rule; + } + + /// Initializes a new instance of the RazorGeneral class. + internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, string file, string itemType, string itemName) : + this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) { + if ((configuredProject == null)) { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.catalogs = catalogs; + this.file = file; + this.itemType = itemType; + this.itemName = itemName; + } + + /// Initializes a new instance of the RazorGeneral class. + internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) : + this(rule) { + if ((rule == null)) { + throw new System.ArgumentNullException("rule"); + } + if ((configuredProject == null)) { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.rule = rule; + this.file = this.rule.File; + this.itemType = this.rule.ItemType; + this.itemName = this.rule.ItemName; + } + + /// Initializes a new instance of the RazorGeneral class. + internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) : + this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) { + } + + /// Initializes a new instance of the RazorGeneral class that assumes a project context (neither property sheet nor items). + internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs) : + this(configuredProject, catalogs, "Project", null, null, null) { + } + + /// Gets the IRule used to get and set properties. + public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule { + get { + return this.rule; + } + } + + /// Razor Language Version + internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty RazorLangVersion { + get { + Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; + if ((localRule == null)) { + localRule = this.GeneratedFallbackRule; + } + if ((localRule == null)) { + return null; + } + Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorLangVersionProperty))); + if (((property == null) + && (this.GeneratedFallbackRule != null))) { + localRule = this.GeneratedFallbackRule; + property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorLangVersionProperty))); + } + return property; + } + } + + /// Razor Configuration Name + internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty RazorDefaultConfiguration { + get { + Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; + if ((localRule == null)) { + localRule = this.GeneratedFallbackRule; + } + if ((localRule == null)) { + return null; + } + Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorDefaultConfigurationProperty))); + if (((property == null) + && (this.GeneratedFallbackRule != null))) { + localRule = this.GeneratedFallbackRule; + property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorDefaultConfigurationProperty))); + } + return property; + } + } + + /// Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule { + get { + if (((this.fallbackRule == null) + && (this.configuredProject != null))) { + System.Threading.Monitor.Enter(this.locker); + try { + if ((this.fallbackRule == null)) { + this.InitializeFallbackRule(); + } + } + finally { + System.Threading.Monitor.Exit(this.locker); + } + } + return this.fallbackRule; + } + } + + private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) { + if ((catalog == null)) { + return null; + } + return catalog.BindToContext(SchemaName, file, itemType, itemName); + } + + private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) { + if ((propertiesContext.IsProjectFile == true)) { + return null; + } + else { + return propertiesContext.File; + } + } + + private void InitializeFallbackRule() { + if ((this.configuredProject == null)) { + return; + } + Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorGeneral.deserializedFallbackRule; + if ((unboundRule == null)) { + System.IO.Stream xamlStream = null; + System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); + try { + xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorGeneral.xaml"); + Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream))); + System.Collections.Generic.IEnumerator ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator(); + for ( + ; ((unboundRule == null) + && ruleEnumerator.MoveNext()); + ) { + Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current)); + if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) { + unboundRule = t; + unboundRule.Name = "15acc140-184e-44be-a4d3-62505276a0bb"; + RazorGeneral.deserializedFallbackRule = unboundRule; + } + } + } + finally { + if ((xamlStream != null)) { + ((System.IDisposable)(xamlStream)).Dispose(); + } + } + } + this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext"); + Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext"); + this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName); + } + } + + internal partial class RazorProjectProperties { + + private static System.Func>, object, RazorGeneral> CreateRazorGeneralPropertiesDelegate = new System.Func>, object, RazorGeneral>(CreateRazorGeneralProperties); + + private static RazorGeneral CreateRazorGeneralProperties(System.Threading.Tasks.Task> namedCatalogs, object state) { + RazorProjectProperties that = ((RazorProjectProperties)(state)); + return new RazorGeneral(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName); + } + + /// Gets the strongly-typed property accessor used to get and set Razor Properties properties. + internal System.Threading.Tasks.Task GetRazorGeneralPropertiesAsync() { + System.Threading.Tasks.Task> namedCatalogsTask = this.GetNamedCatalogsAsync(); + return namedCatalogsTask.ContinueWith(CreateRazorGeneralPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.xaml b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.xaml new file mode 100644 index 0000000000..a2e5150160 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.xaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorProjectProperties.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorProjectProperties.cs new file mode 100644 index 0000000000..cc8f28a6f1 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorProjectProperties.cs @@ -0,0 +1,34 @@ +// 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.ComponentModel.Composition; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.Properties; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules +{ + [Export] + internal partial class RazorProjectProperties : StronglyTypedPropertyAccess + { + [ImportingConstructor] + public RazorProjectProperties(ConfiguredProject configuredProject) + : base(configuredProject) + { + } + + public RazorProjectProperties(ConfiguredProject configuredProject, UnconfiguredProject unconfiguredProject) + : base(configuredProject, unconfiguredProject) + { + } + + public RazorProjectProperties(ConfiguredProject configuredProject, IProjectPropertiesContext projectPropertiesContext) + : base(configuredProject, projectPropertiesContext) + { + } + + public RazorProjectProperties(ConfiguredProject configuredProject, string file, string itemType, string itemName) + : base(configuredProject, file, itemType, itemName) + { + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/UnconfiguredProjectCommonServices.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/UnconfiguredProjectCommonServices.cs new file mode 100644 index 0000000000..9e235012d2 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/UnconfiguredProjectCommonServices.cs @@ -0,0 +1,30 @@ +// 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 Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.References; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + // This defines the set of services that we frequently need for working with UnconfiguredProject. + // + // We're following a somewhat common pattern for code that uses CPS. It's really easy to end up + // relying on service location inside CPS, which can be hard to test. This approach makes it easy + // for us to build reusable mocks instead. + internal interface IUnconfiguredProjectCommonServices + { + ConfiguredProject ActiveConfiguredProject { get; } + + IAssemblyReferencesService ActiveConfiguredProjectAssemblyReferences { get; } + + IPackageReferencesService ActiveConfiguredProjectPackageReferences { get; } + + Rules.RazorProjectProperties ActiveConfiguredProjectRazorProperties { get; } + + IActiveConfiguredProjectSubscriptionService ActiveConfiguredProjectSubscription { get; } + + IProjectThreadingService ThreadingService { get; } + + UnconfiguredProject UnconfiguredProject { get; } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioErrorReporter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioErrorReporter.cs index 8a847f5a3a..766494cf1b 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioErrorReporter.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioErrorReporter.cs @@ -4,6 +4,7 @@ using System; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Shell.Interop; namespace Microsoft.VisualStudio.LanguageServices.Razor @@ -40,7 +41,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor } } - public override void ReportError(Exception exception, Project project) + public override void ReportError(Exception exception, ProjectSnapshot project) { if (exception == null) { @@ -53,7 +54,25 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor var hr = activityLog.LogEntry( (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR, "Razor Language Services", - $"Error encountered from project '{project?.Name}':{Environment.NewLine}{exception}"); + $"Error encountered from project '{project?.FilePath}':{Environment.NewLine}{exception}"); + ErrorHandler.ThrowOnFailure(hr); + } + } + + public override void ReportError(Exception exception, Project workspaceProject) + { + if (exception == null) + { + return; + } + + var activityLog = GetActivityLog(); + if (activityLog != null) + { + var hr = activityLog.LogEntry( + (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR, + "Razor Language Services", + $"Error encountered from project '{workspaceProject?.Name}' '{workspaceProject?.FilePath}':{Environment.NewLine}{exception}"); ErrorHandler.ThrowOnFailure(hr); } } diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj index d1ad2a9fab..1967009f1f 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj @@ -19,6 +19,7 @@ + diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryTest.cs deleted file mode 100644 index 633ac9f797..0000000000 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryTest.cs +++ /dev/null @@ -1,245 +0,0 @@ -// 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 Microsoft.AspNetCore.Razor.Language; -using Xunit; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - public class DefaultProjectExtensibilityConfigurationFactoryTest - { - public static TheoryData LanguageVersionMappingData - { - get - { - return new TheoryData - { - { new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("1.0.0.0")), RazorLanguageVersion.Version_1_0 }, - { new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("1.1.0.0")), RazorLanguageVersion.Version_1_1 }, - { new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("2.0.0.0")), RazorLanguageVersion.Version_2_0 }, - { new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("2.1.0.0")), RazorLanguageVersion.Version_2_1 }, - }; - } - } - - [Theory] - [MemberData(nameof(LanguageVersionMappingData))] - public void GetLanguageVersion_MapsExactVersionsCorrectly(AssemblyIdentity assemblyIdentity, RazorLanguageVersion expectedVersion) - { - // Act - var languageVersion = DefaultProjectExtensibilityConfigurationFactory.GetLanguageVersion(assemblyIdentity); - - // Assert - Assert.Same(expectedVersion, languageVersion); - } - - [Fact] - public void GetLanguageVersion_MapsFuture_1_0_VersionsCorrectly() - { - // Arrange - var assemblyIdentity = new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("1.3.0.0")); - - // Act - var languageVersion = DefaultProjectExtensibilityConfigurationFactory.GetLanguageVersion(assemblyIdentity); - - // Assert - Assert.Same(RazorLanguageVersion.Version_1_1, languageVersion); - } - - [Fact] - public void GetLanguageVersion_MapsFuture_2_0_VersionsCorrectly() - { - // Arrange - var assemblyIdentity = new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("2.3.0.0")); - - // Act - var languageVersion = DefaultProjectExtensibilityConfigurationFactory.GetLanguageVersion(assemblyIdentity); - - // Assert - Assert.Same(RazorLanguageVersion.Latest, languageVersion); - } - - [Theory] - [InlineData("1.0.0.0", "1.0.0.0")] - [InlineData("1.1.0.0", "1.1.0.0")] - [InlineData("2.0.0.0", "2.0.0.0")] - [InlineData("2.0.2.0", "2.0.2.0")] - public void GetConfiguration_FindsSupportedConfiguration_ForNewRazor(string razorVersion, string mvcVersion) - { - // Arrange - var references = new AssemblyIdentity[] - { - new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version(razorVersion)), - new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version(mvcVersion)), - }; - - var factory = new DefaultProjectExtensibilityConfigurationFactory(); - - // Act - var result = factory.GetConfiguration(references); - - // Assert - var configuration = Assert.IsType(result); - Assert.Equal(ProjectExtensibilityConfigurationKind.ApproximateMatch, configuration.Kind); - Assert.Equal(razorVersion, configuration.RazorAssembly.Identity.Version.ToString()); - Assert.Equal(mvcVersion, configuration.MvcAssembly.Identity.Version.ToString()); - } - - [Theory] - [InlineData("1.0.0.0", "1.0.0.0")] - [InlineData("1.1.0.0", "1.1.0.0")] - [InlineData("1.9.9.9", "2.0.0.0")] // MVC version is ignored - public void GetConfiguration_FindsSupportedConfiguration_ForOldRazor(string razorVersion, string mvcVersion) - { - // Arrange - var references = new AssemblyIdentity[] - { - new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version(razorVersion)), - new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version(mvcVersion)), - }; - - var factory = new DefaultProjectExtensibilityConfigurationFactory(); - - // Act - var result = factory.GetConfiguration(references); - - // Assert - var configuration = Assert.IsType(result); - Assert.Equal(ProjectExtensibilityConfigurationKind.ApproximateMatch, configuration.Kind); - Assert.Equal(razorVersion, configuration.RazorAssembly.Identity.Version.ToString()); - Assert.Equal(mvcVersion, configuration.MvcAssembly.Identity.Version.ToString()); - } - - [Fact] - public void GetConfiguration_RazorVersion_NewAssemblyWinsOverOld() - { - // Arrange - var references = new AssemblyIdentity[] - { - new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("1.0.0.0")), - new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version("2.0.0.0")), - new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0")), - }; - - var factory = new DefaultProjectExtensibilityConfigurationFactory(); - - // Act - var result = factory.GetConfiguration(references); - - // Assert - var configuration = Assert.IsType(result); - Assert.Equal(ProjectExtensibilityConfigurationKind.ApproximateMatch, configuration.Kind); - Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); - Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); - } - - [Fact] - public void GetConfiguration_RazorVersion_OldAssemblyIgnoredPastV1() - { - // Arrange - var references = new AssemblyIdentity[] - { - new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("2.0.0.0")), - new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0")), - }; - - var factory = new DefaultProjectExtensibilityConfigurationFactory(); - - // Act - var result = factory.GetConfiguration(references); - - // Assert - var configuration = Assert.IsType(result); - Assert.Equal(ProjectExtensibilityConfigurationKind.Fallback, configuration.Kind); - Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); - Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); - } - - [Fact] - public void GetConfiguration_NoRazorVersion_ChoosesDefault() - { - // Arrange - var references = new AssemblyIdentity[] - { - new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0")), - }; - - var factory = new DefaultProjectExtensibilityConfigurationFactory(); - - // Act - var result = factory.GetConfiguration(references); - - // Assert - var configuration = Assert.IsType(result); - Assert.Equal(ProjectExtensibilityConfigurationKind.Fallback, configuration.Kind); - Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); - Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); - } - - [Fact] - public void GetConfiguration_UnsupportedRazorVersion_ChoosesDefault() - { - // Arrange - var references = new AssemblyIdentity[] - { - new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version("3.0.0.0")), - new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0")), - }; - - var factory = new DefaultProjectExtensibilityConfigurationFactory(); - - // Act - var result = factory.GetConfiguration(references); - - // Assert - var configuration = Assert.IsType(result); - Assert.Equal(ProjectExtensibilityConfigurationKind.Fallback, configuration.Kind); - Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); - Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); - } - - [Fact] - public void GetConfiguration_NoMvcVersion_ChoosesDefault() - { - // Arrange - var references = new AssemblyIdentity[] - { - new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version("2.0.0.0")), - }; - - var factory = new DefaultProjectExtensibilityConfigurationFactory(); - - // Act - var result = factory.GetConfiguration(references); - - // Assert - var configuration = Assert.IsType(result); - Assert.Equal(ProjectExtensibilityConfigurationKind.Fallback, configuration.Kind); - Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); - Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); - } - - [Fact] - public void GetConfiguration_UnsupportedMvcVersion_ChoosesDefault() - { - // Arrange - var references = new AssemblyIdentity[] - { - new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version("2.0.0.0")), - new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("3.0.0.0")), - }; - - var factory = new DefaultProjectExtensibilityConfigurationFactory(); - - // Act - var result = factory.GetConfiguration(references); - - // Assert - var configuration = Assert.IsType(result); - Assert.Equal(ProjectExtensibilityConfigurationKind.Fallback, configuration.Kind); - Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); - Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); - } - } -} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs deleted file mode 100644 index eda57ab8ba..0000000000 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs +++ /dev/null @@ -1,331 +0,0 @@ -// 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.Collections.Generic; -using System.Linq; -using Moq; -using Xunit; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - public class DefaultProjectSnapshotManagerTest - { - public DefaultProjectSnapshotManagerTest() - { - Workspace = TestWorkspace.Create(); - ProjectManager = new TestProjectSnapshotManager(Enumerable.Empty(), Workspace); - } - - private TestProjectSnapshotManager ProjectManager { get; } - - private Workspace Workspace { get; } - - [Fact] - public void ProjectAdded_AddsProject_NotifiesListeners_AndStartsBackgroundWorker() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - - // Act - ProjectManager.ProjectAdded(project); - - // Assert - var snapshot = ProjectManager.GetSnapshot(project.Id); - Assert.True(snapshot.IsDirty); - - Assert.True(ProjectManager.ListenersNotified); - Assert.True(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectChanged_MadeDirty_RetainsComputedState_NotifiesListeners_AndStartsBackgroundWorker() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - ProjectManager.ProjectAdded(project); - ProjectManager.Reset(); - - // Adding some computed state - var configuration = Mock.Of(); - ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); - ProjectManager.Reset(); - - project = project.WithAssemblyName("Test1"); // Simulate a project change - - // Act - ProjectManager.ProjectChanged(project); - - // Assert - var snapshot = ProjectManager.GetSnapshot(project.Id); - Assert.True(snapshot.IsDirty); - Assert.Same(configuration, snapshot.Configuration); - - Assert.False(ProjectManager.ListenersNotified); - Assert.True(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectChanged_BackgroundUpdate_MadeClean_WithSignificantChanges_NotifiesListeners_AndDoesNotStartBackgroundWorker() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - ProjectManager.ProjectAdded(project); - ProjectManager.Reset(); - - var configuration = Mock.Of(); - - // Act - ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); - - // Assert - var snapshot = ProjectManager.GetSnapshot(project.Id); - Assert.False(snapshot.IsDirty); - Assert.Same(configuration, snapshot.Configuration); - - Assert.True(ProjectManager.ListenersNotified); - Assert.False(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectChanged_BackgroundUpdate_MadeClean_WithoutSignificantChanges_NotifiesListeners_AndDoesNotStartBackgroundWorker() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - ProjectManager.ProjectAdded(project); - ProjectManager.Reset(); - - var configuration = Mock.Of(); - ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); - ProjectManager.Reset(); - - project = project.WithAssemblyName("Test1"); // Simulate a project change - ProjectManager.ProjectChanged(project); - ProjectManager.Reset(); - - // Act - ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); - - // Assert - var snapshot = ProjectManager.GetSnapshot(project.Id); - Assert.False(snapshot.IsDirty); - Assert.Same(configuration, snapshot.Configuration); - - Assert.False(ProjectManager.ListenersNotified); - Assert.False(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectChanged_BackgroundUpdate_StillDirty_WithSignificantChanges_NotifiesListeners_AndStartsBackgroundWorker() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - ProjectManager.ProjectAdded(project); - ProjectManager.Reset(); - - var configuration = Mock.Of(); - - // Compute an update for "Test" - var update = new ProjectSnapshotUpdateContext(project) { Configuration = configuration }; - - project = project.WithAssemblyName("Test1"); // Simulate a project change - ProjectManager.ProjectChanged(project); - ProjectManager.Reset(); - - // Act - ProjectManager.ProjectUpdated(update); - - // Assert - var snapshot = ProjectManager.GetSnapshot(project.Id); - Assert.True(snapshot.IsDirty); - Assert.Same(configuration, snapshot.Configuration); - - Assert.True(ProjectManager.ListenersNotified); - Assert.True(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectChanged_BackgroundUpdate_StillDirty_WithoutSignificantChanges_NotifiesListeners_AndStartsBackgroundWorker() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - ProjectManager.ProjectAdded(project); - ProjectManager.Reset(); - - var configuration = Mock.Of(); - ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); - - project = project.WithAssemblyName("Test1"); // Simulate a project change - ProjectManager.ProjectChanged(project); - ProjectManager.Reset(); - - // Compute an update for "Test1" - var update = new ProjectSnapshotUpdateContext(project) { Configuration = configuration }; - - project = project.WithAssemblyName("Test2"); // Simulate a project change - ProjectManager.ProjectChanged(project); - ProjectManager.Reset(); - - // Act - ProjectManager.ProjectUpdated(update); // Still dirty because the project changed while computing the update - - // Assert - var snapshot = ProjectManager.GetSnapshot(project.Id); - Assert.True(snapshot.IsDirty); - Assert.Same(configuration, snapshot.Configuration); - - Assert.False(ProjectManager.ListenersNotified); - Assert.True(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectChanged_IgnoresUnknownProject() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - - // Act - ProjectManager.ProjectChanged(project); - - // Assert - Assert.Empty(ProjectManager.Projects); - - Assert.False(ProjectManager.ListenersNotified); - Assert.False(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectChanged_WithComputedState_IgnoresUnknownProject() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - - // Act - ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project)); - - // Assert - Assert.Empty(ProjectManager.Projects); - - Assert.False(ProjectManager.ListenersNotified); - Assert.False(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectBuildComplete_KnownProject_NotifiesBackgroundWorker() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - ProjectManager.ProjectAdded(project); - ProjectManager.Reset(); - - // Act - ProjectManager.ProjectBuildComplete(project); - - // Assert - Assert.False(ProjectManager.ListenersNotified); - Assert.True(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectBuildComplete_IgnoresUnknownProject() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - - // Act - ProjectManager.ProjectBuildComplete(project); - - // Assert - Assert.Empty(ProjectManager.Projects); - - Assert.False(ProjectManager.ListenersNotified); - Assert.False(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectRemoved_RemovesProject_NotifiesListeners_DoesNotStartBackgroundWorker() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - - ProjectManager.ProjectAdded(project); - ProjectManager.Reset(); - - // Act - ProjectManager.ProjectRemoved(project); - - // Assert - Assert.Empty(ProjectManager.Projects); - - Assert.True(ProjectManager.ListenersNotified); - Assert.False(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectRemoved_IgnoresUnknownProject() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - - // Act - ProjectManager.ProjectRemoved(project); - - // Assert - Assert.Empty(ProjectManager.Projects); - - Assert.False(ProjectManager.ListenersNotified); - Assert.False(ProjectManager.WorkerStarted); - } - - [Fact] - public void ProjectsCleared_RemovesProject_NotifiesListeners_DoesNotStartBackgroundWorker() - { - // Arrange - var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); - - ProjectManager.ProjectAdded(project); - ProjectManager.Reset(); - - // Act - ProjectManager.ProjectsCleared(); - - // Assert - Assert.Empty(ProjectManager.Projects); - - Assert.True(ProjectManager.ListenersNotified); - Assert.False(ProjectManager.WorkerStarted); - } - - private class TestProjectSnapshotManager : DefaultProjectSnapshotManager - { - public TestProjectSnapshotManager(IEnumerable triggers, Workspace workspace) - : base(Mock.Of(), Mock.Of(), Mock.Of(), triggers, workspace) - { - } - - public bool ListenersNotified { get; private set; } - - public bool WorkerStarted { get; private set; } - - public DefaultProjectSnapshot GetSnapshot(ProjectId id) - { - return Projects.Cast().FirstOrDefault(s => s.UnderlyingProject.Id == id); - } - - public void Reset() - { - ListenersNotified = false; - WorkerStarted = false; - } - - protected override void NotifyListeners(ProjectChangeEventArgs e) - { - ListenersNotified = true; - } - - protected override void NotifyBackgroundWorker(Project project) - { - WorkerStarted = true; - } - } - } -} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs index 2a72f46a6c..4a491971a8 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs @@ -1,13 +1,6 @@ // 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.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Moq; using Xunit; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem @@ -15,19 +8,20 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public class DefaultProjectSnapshotTest { [Fact] - public void WithProjectChange_WithProject_CreatesSnapshot_UpdatesUnderlyingProject() + public void WithWorkspaceProject_CreatesSnapshot_UpdatesUnderlyingProject() { // Arrange - var underlyingProject = GetProject("Test1"); - var original = new DefaultProjectSnapshot(underlyingProject); + var hostProject = new HostProject("Test.cshtml", FallbackRazorConfiguration.MVC_2_0); + var workspaceProject = GetWorkspaceProject("Test1"); + var original = new DefaultProjectSnapshot(hostProject, workspaceProject); - var anotherProject = GetProject("Test1"); + var anotherProject = GetWorkspaceProject("Test1"); // Act - var snapshot = original.WithProjectChange(anotherProject); + var snapshot = original.WithWorkspaceProject(anotherProject); // Assert - Assert.Same(anotherProject, snapshot.UnderlyingProject); + Assert.Same(anotherProject, snapshot.WorkspaceProject); Assert.Equal(original.ComputedVersion, snapshot.ComputedVersion); Assert.Equal(original.Configuration, snapshot.Configuration); } @@ -36,25 +30,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public void WithProjectChange_WithProject_CreatesSnapshot_UpdatesValues() { // Arrange - var underlyingProject = GetProject("Test1"); - var original = new DefaultProjectSnapshot(underlyingProject); + var hostProject = new HostProject("Test.cshtml", FallbackRazorConfiguration.MVC_2_0); + var workspaceProject = GetWorkspaceProject("Test1"); + var original = new DefaultProjectSnapshot(hostProject, workspaceProject); - var anotherProject = GetProject("Test1"); - var update = new ProjectSnapshotUpdateContext(anotherProject) - { - Configuration = Mock.Of(), - }; + var anotherProject = GetWorkspaceProject("Test1"); + var update = new ProjectSnapshotUpdateContext(original.FilePath, hostProject, anotherProject, original.Version); // Act - var snapshot = original.WithProjectChange(update); + var snapshot = original.WithComputedUpdate(update); // Assert - Assert.Same(original.UnderlyingProject, snapshot.UnderlyingProject); - Assert.Equal(update.UnderlyingProject.Version, snapshot.ComputedVersion); - Assert.Same(update.Configuration, snapshot.Configuration); + Assert.Same(original.WorkspaceProject, snapshot.WorkspaceProject); } - private Project GetProject(string name) + private Project GetWorkspaceProject(string name) { Project project = null; TestWorkspace.Create(workspace => diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs index 293f6e8bf0..86872c08ea 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs @@ -14,33 +14,53 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { public WorkspaceProjectSnapshotChangeTriggerTest() { - Solution emptySolution = null; - Project project1 = null; - Project project2 = null; - Project project3 = null; - Solution solutionWithTwoProjects = null; - Solution solutionWithOneProject = null; + Workspace = TestWorkspace.Create(); + EmptySolution = Workspace.CurrentSolution.GetIsolatedSolution(); - Workspace = TestWorkspace.Create(ws => - { - emptySolution = ws.CurrentSolution.GetIsolatedSolution(); - project1 = ws.CurrentSolution.AddProject("One", "One", LanguageNames.CSharp); - project2 = project1.Solution.AddProject("Two", "Two", LanguageNames.CSharp); - solutionWithTwoProjects = project2.Solution; + var projectId1 = ProjectId.CreateNewId("One"); + var projectId2 = ProjectId.CreateNewId("Two"); + var projectId3 = ProjectId.CreateNewId("Three"); - project3 = emptySolution.GetIsolatedSolution().AddProject("Three", "Three", LanguageNames.CSharp); - solutionWithOneProject = project3.Solution; - }); + SolutionWithTwoProjects = Workspace.CurrentSolution + .AddProject(ProjectInfo.Create( + projectId1, + VersionStamp.Default, + "One", + "One", + LanguageNames.CSharp, + filePath: "One.csproj")) + .AddProject(ProjectInfo.Create( + projectId2, + VersionStamp.Default, + "Two", + "Two", + LanguageNames.CSharp, + filePath: "Two.csproj")); - EmptySolution = emptySolution; - ProjectNumberOne = project1; - ProjectNumberTwo = project2; - ProjectNumberThree = project3; - SolutionWithTwoProjects = solutionWithTwoProjects; - SolutionWithOneProject = solutionWithOneProject; + SolutionWithOneProject = EmptySolution.GetIsolatedSolution() + .AddProject(ProjectInfo.Create( + projectId3, + VersionStamp.Default, + "Three", + "Three", + LanguageNames.CSharp, + filePath: "Three.csproj")); + ProjectNumberOne = SolutionWithTwoProjects.GetProject(projectId1); + ProjectNumberTwo = SolutionWithTwoProjects.GetProject(projectId2); + ProjectNumberThree = SolutionWithOneProject.GetProject(projectId3); + + HostProjectOne = new HostProject("One.csproj", FallbackRazorConfiguration.MVC_1_1); + HostProjectTwo = new HostProject("Two.csproj", FallbackRazorConfiguration.MVC_1_1); + HostProjectThree = new HostProject("Three.csproj", FallbackRazorConfiguration.MVC_1_1); } + private HostProject HostProjectOne { get; } + + private HostProject HostProjectTwo { get; } + + private HostProject HostProjectThree { get; } + private Solution EmptySolution { get; } private Solution SolutionWithOneProject { get; } @@ -66,7 +86,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Arrange var trigger = new WorkspaceProjectSnapshotChangeTrigger(); var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); - + projectManager.HostProjectAdded(HostProjectOne); + projectManager.HostProjectAdded(HostProjectTwo); + var e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); // Act @@ -74,9 +96,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Assert Assert.Collection( - projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name), - p => Assert.Equal(ProjectNumberOne.Id, p.UnderlyingProject.Id), - p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id)); + projectManager.Projects.OrderBy(p => p.WorkspaceProject.Name), + p => Assert.Equal(ProjectNumberOne.Id, p.WorkspaceProject.Id), + p => Assert.Equal(ProjectNumberTwo.Id, p.WorkspaceProject.Id)); } [Theory] @@ -90,21 +112,25 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Arrange var trigger = new WorkspaceProjectSnapshotChangeTrigger(); var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); + projectManager.HostProjectAdded(HostProjectOne); + projectManager.HostProjectAdded(HostProjectTwo); + projectManager.HostProjectAdded(HostProjectThree); // Initialize with a project. This will get removed. var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithOneProject); trigger.Workspace_WorkspaceChanged(Workspace, e); - e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); + e = new WorkspaceChangeEventArgs(kind, oldSolution: SolutionWithOneProject, newSolution: SolutionWithTwoProjects); // Act trigger.Workspace_WorkspaceChanged(Workspace, e); // Assert Assert.Collection( - projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name), - p => Assert.Equal(ProjectNumberOne.Id, p.UnderlyingProject.Id), - p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id)); + projectManager.Projects.OrderBy(p => p.WorkspaceProject?.Name), + p => Assert.Null(p.WorkspaceProject), + p => Assert.Equal(ProjectNumberOne.Id, p.WorkspaceProject.Id), + p => Assert.Equal(ProjectNumberTwo.Id, p.WorkspaceProject.Id)); } [Theory] @@ -115,6 +141,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Arrange var trigger = new WorkspaceProjectSnapshotChangeTrigger(); var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); + projectManager.HostProjectAdded(HostProjectOne); + projectManager.HostProjectAdded(HostProjectTwo); // Initialize with some projects. var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); @@ -128,13 +156,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Assert Assert.Collection( - projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name), + projectManager.Projects.OrderBy(p => p.WorkspaceProject.Name), p => { - Assert.Equal(ProjectNumberOne.Id, p.UnderlyingProject.Id); - Assert.Equal("Changed", p.UnderlyingProject.AssemblyName); + Assert.Equal(ProjectNumberOne.Id, p.WorkspaceProject.Id); + Assert.Equal("Changed", p.WorkspaceProject.AssemblyName); }, - p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id)); + p => Assert.Equal(ProjectNumberTwo.Id, p.WorkspaceProject.Id)); } [Fact] @@ -143,6 +171,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Arrange var trigger = new WorkspaceProjectSnapshotChangeTrigger(); var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); + projectManager.HostProjectAdded(HostProjectOne); + projectManager.HostProjectAdded(HostProjectTwo); // Initialize with some projects project. var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); @@ -156,8 +186,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Assert Assert.Collection( - projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name), - p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id)); + projectManager.Projects.OrderBy(p => p.WorkspaceProject?.Name), + p => Assert.Null(p.WorkspaceProject), + p => Assert.Equal(ProjectNumberTwo.Id, p.WorkspaceProject.Id)); } [Fact] @@ -166,6 +197,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Arrange var trigger = new WorkspaceProjectSnapshotChangeTrigger(); var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); + projectManager.HostProjectAdded(HostProjectThree); var solution = SolutionWithOneProject; var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectAdded, oldSolution: EmptySolution, newSolution: solution, projectId: ProjectNumberThree.Id); @@ -175,19 +207,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Assert Assert.Collection( - projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name), - p => Assert.Equal(ProjectNumberThree.Id, p.UnderlyingProject.Id)); + projectManager.Projects.OrderBy(p => p.WorkspaceProject.Name), + p => Assert.Equal(ProjectNumberThree.Id, p.WorkspaceProject.Id)); } private class TestProjectSnapshotManager : DefaultProjectSnapshotManager { - public TestProjectSnapshotManager(IEnumerable triggers, Workspace workspace) + public TestProjectSnapshotManager(IEnumerable triggers, Workspace workspace) : base(Mock.Of(), Mock.Of(), new TestProjectSnapshotWorker(), triggers, workspace) { } - protected override void NotifyBackgroundWorker(Project project) + protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context) { + Assert.NotNull(context.HostProject); + Assert.NotNull(context.WorkspaceProject); } } diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs index 74ede949ac..12ecf74efe 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs @@ -26,28 +26,31 @@ namespace Microsoft.VisualStudio.Editor.Razor project = workspace.CurrentSolution.AddProject(info).GetProject(info.Id); }); - Project = project; + WorkspaceProject = project; + + HostProject_For_1_0 = new HostProject("/TestPath/SomePath/Test.csproj", FallbackRazorConfiguration.MVC_1_0); + HostProject_For_1_1 = new HostProject("/TestPath/SomePath/Test.csproj", FallbackRazorConfiguration.MVC_1_1); + HostProject_For_2_0 = new HostProject("/TestPath/SomePath/Test.csproj", FallbackRazorConfiguration.MVC_2_0); } - // We don't actually look at the project, we rely on the ProjectStateManager - public Project Project { get; } + private HostProject HostProject_For_1_0 { get; } - public Workspace Workspace { get; } + private HostProject HostProject_For_1_1 { get; } + + private HostProject HostProject_For_2_0 { get; } + + // We don't actually look at the project, we rely on the ProjectStateManager + private Project WorkspaceProject { get; } + + private Workspace Workspace { get; } [Fact] public void Create_CreatesTemplateEngine_ForLatest() { // Arrange var projectManager = new TestProjectSnapshotManager(Workspace); - projectManager.ProjectAdded(Project); - projectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(Project) - { - Configuration = new MvcExtensibilityConfiguration( - RazorLanguageVersion.Version_2_0, - ProjectExtensibilityConfigurationKind.ApproximateMatch, - new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0"))), - new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("2.0.0.0")))), - }); + projectManager.HostProjectAdded(HostProject_For_2_0); + projectManager.WorkspaceProjectAdded(WorkspaceProject); var factoryService = new DefaultProjectEngineFactoryService(projectManager); @@ -68,15 +71,8 @@ namespace Microsoft.VisualStudio.Editor.Razor { // Arrange var projectManager = new TestProjectSnapshotManager(Workspace); - projectManager.ProjectAdded(Project); - projectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(Project) - { - Configuration = new MvcExtensibilityConfiguration( - RazorLanguageVersion.Version_1_1, - ProjectExtensibilityConfigurationKind.ApproximateMatch, - new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("1.1.3.0"))), - new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("1.1.3.0")))), - }); + projectManager.HostProjectAdded(HostProject_For_1_1); + projectManager.WorkspaceProjectAdded(WorkspaceProject); var factoryService = new DefaultProjectEngineFactoryService(projectManager); @@ -97,15 +93,8 @@ namespace Microsoft.VisualStudio.Editor.Razor { // Arrange var projectManager = new TestProjectSnapshotManager(Workspace); - projectManager.ProjectAdded(Project); - projectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(Project) - { - Configuration = new MvcExtensibilityConfiguration( - RazorLanguageVersion.Version_1_0, - ProjectExtensibilityConfigurationKind.ApproximateMatch, - new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("1.0.0.0"))), - new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("1.0.0.0")))), - }); + projectManager.HostProjectAdded(HostProject_For_1_0); + projectManager.WorkspaceProjectAdded(WorkspaceProject); var factoryService = new DefaultProjectEngineFactoryService(projectManager); @@ -121,35 +110,6 @@ namespace Microsoft.VisualStudio.Editor.Razor Assert.Empty(engine.Engine.Features.OfType()); } - [Fact] - public void Create_HigherMvcVersion_UsesLatest() - { - // Arrange - var projectManager = new TestProjectSnapshotManager(Workspace); - projectManager.ProjectAdded(Project); - projectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(Project) - { - Configuration = new MvcExtensibilityConfiguration( - RazorLanguageVersion.Latest, - ProjectExtensibilityConfigurationKind.ApproximateMatch, - new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("3.0.0.0"))), - new ProjectExtensibilityAssembly(new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("3.0.0.0")))), - }); - - var factoryService = new DefaultProjectEngineFactoryService(projectManager); - - // Act - var engine = factoryService.Create("/TestPath/SomePath/", b => - { - b.Features.Add(new MyCoolNewFeature()); - }); - - // Assert - Assert.Single(engine.Engine.Features.OfType()); - Assert.Single(engine.Engine.Features.OfType()); - Assert.Single(engine.Engine.Features.OfType()); - } - [Fact] public void Create_UnknownProjectPath_UsesLatest() { @@ -175,7 +135,8 @@ namespace Microsoft.VisualStudio.Editor.Razor { // Arrange var projectManager = new TestProjectSnapshotManager(Workspace); - projectManager.ProjectAdded(Project); + projectManager.HostProjectAdded(HostProject_For_2_0); + projectManager.WorkspaceProjectAdded(WorkspaceProject); var factoryService = new DefaultProjectEngineFactoryService(projectManager); diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs new file mode 100644 index 0000000000..ef13312831 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs @@ -0,0 +1,815 @@ +// 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.Collections.Generic; +using System.Linq; +using Moq; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + public class DefaultProjectSnapshotManagerTest : ForegroundDispatcherTestBase + { + public DefaultProjectSnapshotManagerTest() + { + HostProject = new HostProject("Test.csproj", FallbackRazorConfiguration.MVC_2_0); + + Workspace = TestWorkspace.Create(); + ProjectManager = new TestProjectSnapshotManager(Dispatcher, Enumerable.Empty(), Workspace); + + var projectId = ProjectId.CreateNewId("Test"); + var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( + projectId, + VersionStamp.Default, + "Test", + "Test", + LanguageNames.CSharp, + "Test.csproj")); + WorkspaceProject = solution.GetProject(projectId); + + var vbProjectId = ProjectId.CreateNewId("VB"); + solution = solution.AddProject(ProjectInfo.Create( + vbProjectId, + VersionStamp.Default, + "VB", + "VB", + LanguageNames.VisualBasic, + "VB.vbproj")); + VBWorkspaceProject = solution.GetProject(vbProjectId); + + var projectWithoutFilePathId = ProjectId.CreateNewId("NoFile"); + solution = solution.AddProject(ProjectInfo.Create( + projectWithoutFilePathId, + VersionStamp.Default, + "NoFile", + "NoFile", + LanguageNames.CSharp)); + WorkspaceProjectWithoutFilePath = solution.GetProject(projectWithoutFilePathId); + + // Approximates a project with multi-targeting + var projectIdWithDifferentTfm = ProjectId.CreateNewId("TestWithDifferentTfm"); + solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( + projectIdWithDifferentTfm, + VersionStamp.Default, + "Test (Different TFM)", + "Test", + LanguageNames.CSharp, + "Test.csproj")); + WorkspaceProjectWithDifferentTfm = solution.GetProject(projectIdWithDifferentTfm); + } + + private HostProject HostProject { get; } + + private Project WorkspaceProject { get; } + + private Project WorkspaceProjectWithDifferentTfm { get; } + + private Project WorkspaceProjectWithoutFilePath { get; } + + private Project VBWorkspaceProject { get; } + + private TestProjectSnapshotManager ProjectManager { get; } + + private Workspace Workspace { get; } + + [ForegroundFact] + public void HostProjectAdded_WithoutWorkspaceProject_NotifiesListeners() + { + // Arrange + + // Act + ProjectManager.HostProjectAdded(HostProject); + + // Assert + var snapshot = ProjectManager.GetSnapshot(HostProject); + Assert.True(snapshot.IsDirty); + Assert.False(snapshot.IsInitialized); + + Assert.True(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void HostProjectAdded_FindsWorkspaceProject_NotifiesListeners_AndStartsBackgroundWorker() + { + // Arrange + Assert.True(Workspace.TryApplyChanges(WorkspaceProject.Solution)); + + // Act + ProjectManager.HostProjectAdded(HostProject); + + // Assert + var snapshot = ProjectManager.GetSnapshot(HostProject); + Assert.True(snapshot.IsDirty); + Assert.True(snapshot.IsInitialized); + + Assert.True(ProjectManager.ListenersNotified); + Assert.True(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void HostProjectChanged_WithoutWorkspaceProject_NotifiesListeners_AndDoesNotStartBackgroundWorker() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.Reset(); + + var project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_0); // Simulate a project change + + // Act + ProjectManager.HostProjectChanged(project); + + // Assert + var snapshot = ProjectManager.GetSnapshot(HostProject); + Assert.True(snapshot.IsDirty); + Assert.False(snapshot.IsInitialized); + + Assert.True(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void HostProjectChanged_WithWorkspaceProject_RetainsComputedState_NotifiesListeners_AndStartsBackgroundWorker() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Adding some computed state + var snapshot = ProjectManager.GetSnapshot(HostProject); + var updateContext = snapshot.CreateUpdateContext(); + ProjectManager.ProjectUpdated(updateContext); + ProjectManager.Reset(); + + var project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_0); // Simulate a project change + + // Act + ProjectManager.HostProjectChanged(project); + + // Assert + snapshot = ProjectManager.GetSnapshot(project); + Assert.True(snapshot.IsDirty); + Assert.True(snapshot.IsInitialized); + + Assert.True(ProjectManager.ListenersNotified); + Assert.True(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void HostProjectChanged_IgnoresUnknownProject() + { + // Arrange + + // Act + ProjectManager.HostProjectChanged(HostProject); + + // Assert + Assert.Empty(ProjectManager.Projects); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void HostProjectRemoved_RemovesProject_NotifiesListeners() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.Reset(); + + // Act + ProjectManager.HostProjectRemoved(HostProject); + + // Assert + Assert.Empty(ProjectManager.Projects); + + Assert.True(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void ProjectUpdated_WithComputedState_IgnoresUnknownProject() + { + // Arrange + + // Act + ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext("Test", HostProject, WorkspaceProject, VersionStamp.Default)); + + // Assert + Assert.Empty(ProjectManager.Projects); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void ProjectUpdated_WhenHostProjectChanged_MadeClean_NotifiesListeners_AndDoesNotStartBackgroundWorker() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + var project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_0); // Simulate a project change + ProjectManager.HostProjectChanged(project); + ProjectManager.Reset(); + + // Generate the update + var snapshot = ProjectManager.GetSnapshot(HostProject); + var updateContext = snapshot.CreateUpdateContext(); + + // Act + ProjectManager.ProjectUpdated(updateContext); + + // Assert + snapshot = ProjectManager.GetSnapshot(project); + Assert.False(snapshot.IsDirty); + + Assert.True(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void ProjectUpdated_WhenWorkspaceProjectChanged_MadeClean_NotifiesListeners_AndDoesNotStartBackgroundWorker() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change + ProjectManager.WorkspaceProjectChanged(project); + ProjectManager.Reset(); + + // Generate the update + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + var updateContext = snapshot.CreateUpdateContext(); + + // Act + ProjectManager.ProjectUpdated(updateContext); + + // Assert + snapshot = ProjectManager.GetSnapshot(project); + Assert.False(snapshot.IsDirty); + + Assert.True(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void ProjectUpdated_WhenHostProjectChanged_StillDirty_WithSignificantChanges_NotifiesListeners_AndStartsBackgroundWorker() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Generate the update + var snapshot = ProjectManager.GetSnapshot(HostProject); + var updateContext = snapshot.CreateUpdateContext(); + + var project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_0); // Simulate a project change + ProjectManager.HostProjectChanged(project); + ProjectManager.Reset(); + + // Act + ProjectManager.ProjectUpdated(updateContext); + + // Assert + snapshot = ProjectManager.GetSnapshot(project); + Assert.True(snapshot.IsDirty); + + Assert.True(ProjectManager.ListenersNotified); + Assert.True(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectChanged_BackgroundUpdate_StillDirty_WithSignificantChanges_NotifiesListeners_AndStartsBackgroundWorker() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Generate the update + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + var updateContext = snapshot.CreateUpdateContext(); + + var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change + ProjectManager.WorkspaceProjectChanged(project); + ProjectManager.Reset(); + + // Act + ProjectManager.ProjectUpdated(updateContext); + + // Assert + snapshot = ProjectManager.GetSnapshot(project); + Assert.True(snapshot.IsDirty); + + Assert.True(ProjectManager.ListenersNotified); + Assert.True(ProjectManager.WorkerStarted); + } + + [Fact(Skip = "We no longer have any background-computed state")] + public void ProjectUpdated_WhenHostProjectChanged_StillDirty_WithoutSignificantChanges_DoesNotNotifyListeners_AndStartsBackgroundWorker() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Generate an update based on the original state + var snapshot = ProjectManager.GetSnapshot(HostProject); + var updateContext = snapshot.CreateUpdateContext(); + ProjectManager.ProjectUpdated(updateContext); + ProjectManager.Reset(); + + var project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_0); // Simulate a project change + ProjectManager.HostProjectChanged(project); + ProjectManager.Reset(); + + // Now start computing another update + snapshot = ProjectManager.GetSnapshot(HostProject); + updateContext = snapshot.CreateUpdateContext(); + + project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_1); // Simulate a project change + ProjectManager.HostProjectChanged(project); + ProjectManager.Reset(); + + // Act + ProjectManager.ProjectUpdated(updateContext); // Still dirty because the project changed while computing the update + + // Assert + snapshot = ProjectManager.GetSnapshot(project); + Assert.True(snapshot.IsDirty); + + Assert.False(ProjectManager.ListenersNotified); + Assert.True(ProjectManager.WorkerStarted); + } + + [Fact(Skip = "We no longer have any background-computed state")] + public void ProjectUpdated_WhenWorkspaceProjectChanged_StillDirty_WithoutSignificantChanges_DoesNotNotifyListeners_AndStartsBackgroundWorker() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Generate an update based on the original state + var snapshot = ProjectManager.GetSnapshot(HostProject); + var updateContext = snapshot.CreateUpdateContext(); + ProjectManager.ProjectUpdated(updateContext); + ProjectManager.Reset(); + + var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change + ProjectManager.WorkspaceProjectChanged(project); + ProjectManager.Reset(); + + // Now start computing another update + snapshot = ProjectManager.GetSnapshot(HostProject); + updateContext = snapshot.CreateUpdateContext(); + + project = project.WithAssemblyName("Test2"); // Simulate a project change + ProjectManager.WorkspaceProjectChanged(project); + ProjectManager.Reset(); + + // Act + ProjectManager.ProjectUpdated(updateContext); // Still dirty because the project changed while computing the update + + // Assert + snapshot = ProjectManager.GetSnapshot(project); + Assert.True(snapshot.IsDirty); + + Assert.False(ProjectManager.ListenersNotified); + Assert.True(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void ProjectUpdated_WhenHostProjectRemoved_DiscardsUpdate() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Generate the update + var snapshot = ProjectManager.GetSnapshot(HostProject); + var updateContext = snapshot.CreateUpdateContext(); + + ProjectManager.HostProjectRemoved(HostProject); + ProjectManager.Reset(); + + // Act + ProjectManager.ProjectUpdated(updateContext); + + // Assert + snapshot = ProjectManager.GetSnapshot(HostProject); + Assert.Null(snapshot); + } + + [ForegroundFact] + public void ProjectUpdated_WhenWorkspaceProjectRemoved_DiscardsUpdate() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Generate the update + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + var updateContext = snapshot.CreateUpdateContext(); + + ProjectManager.WorkspaceProjectRemoved(WorkspaceProject); + ProjectManager.Reset(); + + // Act + ProjectManager.ProjectUpdated(updateContext); + + // Assert + snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.True(snapshot.IsDirty); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void ProjectUpdated_BackgroundUpdate_MadeClean_WithSignificantChanges_NotifiesListeners_AndDoesNotStartBackgroundWorker() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Generate the update + var snapshot = ProjectManager.GetSnapshot(HostProject); + var updateContext = snapshot.CreateUpdateContext(); + + // Act + ProjectManager.ProjectUpdated(updateContext); + + // Assert + snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.False(snapshot.IsDirty); + + Assert.True(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectAdded_WithoutHostProject_IgnoresWorkspaceProject() + { + // Arrange + + // Act + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + + // Assert + Assert.Empty(ProjectManager.Projects); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectAdded_IgnoresNonCSharpProject() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.Reset(); + + // Act + ProjectManager.WorkspaceProjectAdded(VBWorkspaceProject); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.False(snapshot.IsInitialized); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectAdded_IgnoresSecondProjectWithSameFilePath() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Act + ProjectManager.WorkspaceProjectAdded(WorkspaceProjectWithDifferentTfm); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.Same(WorkspaceProject, snapshot.WorkspaceProject); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectAdded_IgnoresProjectWithoutFilePath() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.Reset(); + + // Act + ProjectManager.WorkspaceProjectAdded(WorkspaceProjectWithoutFilePath); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.False(snapshot.IsInitialized); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectAdded_WithHostProject_NotifiesListenters_AndStartsBackgroundWorker() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.Reset(); + + // Act + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.True(snapshot.IsDirty); + Assert.True(snapshot.IsInitialized); + + Assert.True(ProjectManager.ListenersNotified); + Assert.True(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectChanged_WithoutHostProject_IgnoresWorkspaceProject() + { + // Arrange + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change + + // Act + ProjectManager.WorkspaceProjectChanged(project); + + // Assert + Assert.Empty(ProjectManager.Projects); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectChanged_IgnoresNonCSharpProject() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(VBWorkspaceProject); + ProjectManager.Reset(); + + var project = VBWorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change + + // Act + ProjectManager.WorkspaceProjectChanged(project); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.False(snapshot.IsInitialized); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + + [ForegroundFact] + public void WorkspaceProjectChanged_IgnoresProjectWithoutFilePath() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProjectWithoutFilePath); + ProjectManager.Reset(); + + var project = WorkspaceProjectWithoutFilePath.WithAssemblyName("Test1"); // Simulate a project change + + // Act + ProjectManager.WorkspaceProjectChanged(project); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.False(snapshot.IsInitialized); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectChanged_IgnoresSecondProjectWithSameFilePath() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Act + ProjectManager.WorkspaceProjectChanged(WorkspaceProjectWithDifferentTfm); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.Same(WorkspaceProject, snapshot.WorkspaceProject); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectChanged_MadeDirty_RetainsComputedState_NotifiesListeners_AndStartsBackgroundWorker() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Generate the update + var snapshot = ProjectManager.GetSnapshot(HostProject); + var updateContext = snapshot.CreateUpdateContext(); + ProjectManager.ProjectUpdated(updateContext); + ProjectManager.Reset(); + + var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change + + // Act + ProjectManager.WorkspaceProjectChanged(project); + + // Assert + snapshot = ProjectManager.GetSnapshot(project); + Assert.True(snapshot.IsDirty); + + Assert.False(ProjectManager.ListenersNotified); + Assert.True(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectRemoved_WithHostProject_DoesNotRemoveProject() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Act + ProjectManager.WorkspaceProjectRemoved(WorkspaceProject); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.True(snapshot.IsDirty); + Assert.False(snapshot.IsInitialized); + + Assert.True(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectRemoved_WithHostProject_FallsBackToSecondProject() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Sets up a solution where the which has WorkspaceProjectWithDifferentTfm but not WorkspaceProject + // This will enable us to fall back and find the WorkspaceProjectWithDifferentTfm + Assert.True(Workspace.TryApplyChanges(WorkspaceProjectWithDifferentTfm.Solution)); + + // Act + ProjectManager.WorkspaceProjectRemoved(WorkspaceProject); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.True(snapshot.IsDirty); + Assert.True(snapshot.IsInitialized); + Assert.Equal(WorkspaceProjectWithDifferentTfm.Id, snapshot.WorkspaceProject.Id); + + Assert.True(ProjectManager.ListenersNotified); + Assert.True(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectRemoved_IgnoresSecondProjectWithSameFilePath() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.Reset(); + + // Act + ProjectManager.WorkspaceProjectRemoved(WorkspaceProjectWithDifferentTfm); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.Same(WorkspaceProject, snapshot.WorkspaceProject); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectRemoved_IgnoresNonCSharpProject() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(VBWorkspaceProject); + ProjectManager.Reset(); + + // Act + ProjectManager.WorkspaceProjectRemoved(VBWorkspaceProject); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.False(snapshot.IsInitialized); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectRemoved_IgnoresProjectWithoutFilePath() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject); + ProjectManager.WorkspaceProjectAdded(WorkspaceProjectWithoutFilePath); + ProjectManager.Reset(); + + // Act + ProjectManager.WorkspaceProjectRemoved(WorkspaceProjectWithoutFilePath); + + // Assert + var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); + Assert.False(snapshot.IsInitialized); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + [ForegroundFact] + public void WorkspaceProjectRemoved_IgnoresUnknownProject() + { + // Arrange + + // Act + ProjectManager.WorkspaceProjectRemoved(WorkspaceProject); + + // Assert + Assert.Empty(ProjectManager.Projects); + + Assert.False(ProjectManager.ListenersNotified); + Assert.False(ProjectManager.WorkerStarted); + } + + private class TestProjectSnapshotManager : DefaultProjectSnapshotManager + { + public TestProjectSnapshotManager(ForegroundDispatcher dispatcher, IEnumerable triggers, Workspace workspace) + : base(dispatcher, Mock.Of(), Mock.Of(), triggers, workspace) + { + } + + public bool ListenersNotified { get; private set; } + + public bool WorkerStarted { get; private set; } + + public DefaultProjectSnapshot GetSnapshot(HostProject hostProject) + { + return Projects.Cast().FirstOrDefault(s => s.FilePath == hostProject.FilePath); + } + + public DefaultProjectSnapshot GetSnapshot(Project workspaceProject) + { + return Projects.Cast().FirstOrDefault(s => s.FilePath == workspaceProject.FilePath); + } + + public void Reset() + { + ListenersNotified = false; + WorkerStarted = false; + } + + protected override void NotifyListeners(ProjectChangeEventArgs e) + { + ListenersNotified = true; + } + + protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context) + { + Assert.NotNull(context.HostProject); + Assert.NotNull(context.WorkspaceProject); + + WorkerStarted = true; + } + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultRazorProjectHostTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultRazorProjectHostTest.cs new file mode 100644 index 0000000000..6234bfb557 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultRazorProjectHostTest.cs @@ -0,0 +1,456 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.VisualStudio.LanguageServices.Razor; +using Microsoft.VisualStudio.ProjectSystem; +using Moq; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + public class DefaultRazorProjectHostTest : ForegroundDispatcherTestBase + { + public DefaultRazorProjectHostTest() + { + Workspace = new AdhocWorkspace(); + ProjectManager = new TestProjectSnapshotManager(Dispatcher, Workspace); + } + + private TestProjectSnapshotManager ProjectManager { get; } + + private Workspace Workspace { get; } + + [ForegroundFact] + public async Task DefaultRazorProjectHost_ForegroundThread_CreateAndDispose_Succeeds() + { + // Arrange + var services = new TestProjectSystemServices("Test.csproj"); + var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager); + + // Act & Assert + await host.LoadAsync(); + Assert.Empty(ProjectManager.Projects); + + await host.DisposeAsync(); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task DefaultRazorProjectHost_BackgroundThread_CreateAndDispose_Succeeds() + { + // Arrange + var services = new TestProjectSystemServices("Test.csproj"); + var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager); + + // Act & Assert + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectChanged_ReadsProperties_InitializesProject() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = Rules.RazorGeneral.SchemaName, + After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary() + { + { Rules.RazorGeneral.RazorLangVersionProperty, "2.1" }, + { Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1" }, + }), + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorConfiguration.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary>() + { + { "MVC-2.1", new Dictionary() { { "Extensions", "MVC-2.1;Another-Thing" }, } }, + }) + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorExtension.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary>() + { + { "MVC-2.1", new Dictionary(){ } }, + { "Another-Thing", new Dictionary(){ } }, + }) + } + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager); + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert + var snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + + Assert.Equal(RazorLanguageVersion.Version_2_1, snapshot.Configuration.LanguageVersion); + Assert.Equal("MVC-2.1", snapshot.Configuration.ConfigurationName); + Assert.Collection( + snapshot.Configuration.Extensions, + e => Assert.Equal("MVC-2.1", e.ExtensionName), + e => Assert.Equal("Another-Thing", e.ExtensionName)); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectChanged_NoVersionFound_DoesNotIniatializeProject() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = Rules.RazorGeneral.SchemaName, + After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary() + { + { Rules.RazorGeneral.RazorLangVersionProperty, "" }, + { Rules.RazorGeneral.RazorDefaultConfigurationProperty, "" }, + }), + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorConfiguration.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary>() + { + }) + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorExtension.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary>() + { + }) + } + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager); + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert + Assert.Empty(ProjectManager.Projects); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectChanged_UpdateProject_Succeeds() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = Rules.RazorGeneral.SchemaName, + After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary() + { + { Rules.RazorGeneral.RazorLangVersionProperty, "2.1" }, + { Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1" }, + }), + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorConfiguration.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary>() + { + { "MVC-2.1", new Dictionary() { { "Extensions", "MVC-2.1;Another-Thing" }, } }, + }) + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorExtension.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary>() + { + { "MVC-2.1", new Dictionary(){ } }, + { "Another-Thing", new Dictionary(){ } }, + }) + } + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager); + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act - 1 + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 1 + var snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + + Assert.Equal(RazorLanguageVersion.Version_2_1, snapshot.Configuration.LanguageVersion); + Assert.Equal("MVC-2.1", snapshot.Configuration.ConfigurationName); + Assert.Collection( + snapshot.Configuration.Extensions, + e => Assert.Equal("MVC-2.1", e.ExtensionName), + e => Assert.Equal("Another-Thing", e.ExtensionName)); + + // Act - 2 + changes[0].After.SetProperty(Rules.RazorGeneral.RazorLangVersionProperty, "2.0"); + changes[0].After.SetProperty(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.0"); + changes[1].After.SetItem("MVC-2.0", new Dictionary() { { "Extensions", "MVC-2.0;Another-Thing" }, }); + changes[2].After.SetItem("MVC-2.0", new Dictionary()); + + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 2 + snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + + Assert.Equal(RazorLanguageVersion.Version_2_0, snapshot.Configuration.LanguageVersion); + Assert.Equal("MVC-2.0", snapshot.Configuration.ConfigurationName); + Assert.Collection( + snapshot.Configuration.Extensions, + e => Assert.Equal("MVC-2.0", e.ExtensionName), + e => Assert.Equal("Another-Thing", e.ExtensionName)); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectChanged_VersionRemoved_DeinitializesProject() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = Rules.RazorGeneral.SchemaName, + After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary() + { + { Rules.RazorGeneral.RazorLangVersionProperty, "2.1" }, + { Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1" }, + }), + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorConfiguration.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary>() + { + { "MVC-2.1", new Dictionary() { { "Extensions", "MVC-2.1;Another-Thing" }, } }, + }) + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorExtension.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary>() + { + { "MVC-2.1", new Dictionary(){ } }, + { "Another-Thing", new Dictionary(){ } }, + }) + } + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager); + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act - 1 + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 1 + var snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + + Assert.Equal(RazorLanguageVersion.Version_2_1, snapshot.Configuration.LanguageVersion); + Assert.Equal("MVC-2.1", snapshot.Configuration.ConfigurationName); + Assert.Collection( + snapshot.Configuration.Extensions, + e => Assert.Equal("MVC-2.1", e.ExtensionName), + e => Assert.Equal("Another-Thing", e.ExtensionName)); + + // Act - 2 + changes[0].After.SetProperty(Rules.RazorGeneral.RazorLangVersionProperty, ""); + changes[0].After.SetProperty(Rules.RazorGeneral.RazorDefaultConfigurationProperty, ""); + + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 2 + Assert.Empty(ProjectManager.Projects); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectChanged_AfterDispose_IgnoresUpdate() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = Rules.RazorGeneral.SchemaName, + After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary() + { + { Rules.RazorGeneral.RazorLangVersionProperty, "2.1" }, + { Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1" }, + }), + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorConfiguration.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary>() + { + { "MVC-2.1", new Dictionary() { { "Extensions", "MVC-2.1;Another-Thing" }, } }, + }) + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorExtension.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary>() + { + { "MVC-2.1", new Dictionary(){ } }, + { "Another-Thing", new Dictionary(){ } }, + }) + } + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager); + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act - 1 + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 1 + var snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + + Assert.Equal(RazorLanguageVersion.Version_2_1, snapshot.Configuration.LanguageVersion); + Assert.Equal("MVC-2.1", snapshot.Configuration.ConfigurationName); + Assert.Collection( + snapshot.Configuration.Extensions, + e => Assert.Equal("MVC-2.1", e.ExtensionName), + e => Assert.Equal("Another-Thing", e.ExtensionName)); + + // Act - 2 + await Task.Run(async () => await host.DisposeAsync()); + + // Assert - 2 + Assert.Empty(ProjectManager.Projects); + + // Act - 3 + changes[0].After.SetProperty(Rules.RazorGeneral.RazorLangVersionProperty, "2.0"); + changes[0].After.SetProperty(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.0"); + changes[1].After.SetItem("MVC-2.0", new Dictionary() { { "Extensions", "MVC-2.0;Another-Thing" }, }); + + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 3 + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectRenamed_RemovesHostProject_CopiesConfiguration() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = Rules.RazorGeneral.SchemaName, + After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary() + { + { Rules.RazorGeneral.RazorLangVersionProperty, "2.1" }, + { Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1" }, + }), + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorConfiguration.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary>() + { + { "MVC-2.1", new Dictionary() { { "Extensions", "MVC-2.1;Another-Thing" }, } }, + }) + }, + new TestProjectChangeDescription() + { + RuleName = Rules.RazorExtension.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary>() + { + { "MVC-2.1", new Dictionary(){ } }, + { "Another-Thing", new Dictionary(){ } }, + }) + } + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager); + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act - 1 + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 1 + var snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + Assert.Same("MVC-2.1", snapshot.Configuration.ConfigurationName); + + // Act - 2 + services.UnconfiguredProject.FullPath = "Test2.csproj"; + await Task.Run(async () => await host.OnProjectRenamingAsync()); + + // Assert - 1 + snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test2.csproj", snapshot.FilePath); + Assert.Same("MVC-2.1", snapshot.Configuration.ConfigurationName); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + private class TestProjectSnapshotManager : DefaultProjectSnapshotManager + { + public TestProjectSnapshotManager(ForegroundDispatcher dispatcher, Workspace workspace) + : base(dispatcher, Mock.Of(), Mock.Of(), Array.Empty(), workspace) + { + } + + protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context) + { + } + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackRazorProjectHostTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackRazorProjectHostTest.cs new file mode 100644 index 0000000000..04c910800b --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackRazorProjectHostTest.cs @@ -0,0 +1,373 @@ +// 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.Threading.Tasks; +using Microsoft.VisualStudio.LanguageServices.Razor; +using Microsoft.VisualStudio.ProjectSystem; +using Moq; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + public class FallbackRazorProjectHostTest : ForegroundDispatcherTestBase + { + public FallbackRazorProjectHostTest() + { + Workspace = new AdhocWorkspace(); + ProjectManager = new TestProjectSnapshotManager(Dispatcher, Workspace); + } + + private TestProjectSnapshotManager ProjectManager { get; } + + private Workspace Workspace { get; } + + [ForegroundFact] + public async Task FallbackRazorProjectHost_ForegroundThread_CreateAndDispose_Succeeds() + { + // Arrange + var services = new TestProjectSystemServices("Test.csproj"); + var host = new TestFallbackRazorProjectHost(services, Workspace, ProjectManager); + + // Act & Assert + await host.LoadAsync(); + Assert.Empty(ProjectManager.Projects); + + await host.DisposeAsync(); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task FallbackRazorProjectHost_BackgroundThread_CreateAndDispose_Succeeds() + { + // Arrange + var services = new TestProjectSystemServices("Test.csproj"); + var host = new TestFallbackRazorProjectHost(services, Workspace, ProjectManager); + + // Act & Assert + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectChanged_ReadsProperties_InitializesProject() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary>() + { + { "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary() }, + }), + }, + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new TestFallbackRazorProjectHost(services, Workspace, ProjectManager) + { + AssemblyVersion = new Version(2, 0), // Mock for reading the assembly's version + }; + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert + var snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + Assert.Same(FallbackRazorConfiguration.MVC_2_0, snapshot.Configuration); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectChanged_NoAssemblyFound_DoesNotIniatializeProject() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary>() + { + }), + }, + + }; + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new TestFallbackRazorProjectHost(services, Workspace, ProjectManager); + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert + Assert.Empty(ProjectManager.Projects); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectChanged_AssemblyFoundButCannotReadVersion_DoesNotIniatializeProject() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary>() + { + { "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary() }, + }), + }, + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new TestFallbackRazorProjectHost(services, Workspace, ProjectManager); + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert + Assert.Empty(ProjectManager.Projects); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectChanged_UpdateProject_Succeeds() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary>() + { + { "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary() }, + }), + }, + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new TestFallbackRazorProjectHost(services, Workspace, ProjectManager) + { + AssemblyVersion = new Version(2, 0), + }; + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act - 1 + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 1 + var snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + Assert.Same(FallbackRazorConfiguration.MVC_2_0, snapshot.Configuration); + + // Act - 2 + host.AssemblyVersion = new Version(1, 0); + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 2 + snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + Assert.Same(FallbackRazorConfiguration.MVC_1_0, snapshot.Configuration); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectChanged_VersionRemoved_DeinitializesProject() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary>() + { + { "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary() }, + }), + }, + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new TestFallbackRazorProjectHost(services, Workspace, ProjectManager) + { + AssemblyVersion = new Version(2, 0), + }; + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act - 1 + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 1 + var snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + Assert.Same(FallbackRazorConfiguration.MVC_2_0, snapshot.Configuration); + + // Act - 2 + host.AssemblyVersion= null; + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 2 + Assert.Empty(ProjectManager.Projects); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectChanged_AfterDispose_IgnoresUpdate() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary>() + { + { "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary() }, + }), + }, + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new TestFallbackRazorProjectHost(services, Workspace, ProjectManager) + { + AssemblyVersion = new Version(2, 0), + }; + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act - 1 + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 1 + var snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + Assert.Same(FallbackRazorConfiguration.MVC_2_0, snapshot.Configuration); + + // Act - 2 + await Task.Run(async () => await host.DisposeAsync()); + + // Assert - 2 + Assert.Empty(ProjectManager.Projects); + + // Act - 3 + host.AssemblyVersion = new Version(1, 1); + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 3 + Assert.Empty(ProjectManager.Projects); + } + + [ForegroundFact] + public async Task OnProjectRenamed_RemovesHostProject_CopiesConfiguration() + { + // Arrange + var changes = new TestProjectChangeDescription[] + { + new TestProjectChangeDescription() + { + RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, + After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary>() + { + { "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary() }, + }), + }, + }; + + var services = new TestProjectSystemServices("Test.csproj"); + + var host = new TestFallbackRazorProjectHost(services, Workspace, ProjectManager) + { + AssemblyVersion = new Version(2, 0), // Mock for reading the assembly's version + }; + + await Task.Run(async () => await host.LoadAsync()); + Assert.Empty(ProjectManager.Projects); + + // Act - 1 + await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes))); + + // Assert - 1 + var snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test.csproj", snapshot.FilePath); + Assert.Same(FallbackRazorConfiguration.MVC_2_0, snapshot.Configuration); + + // Act - 2 + services.UnconfiguredProject.FullPath = "Test2.csproj"; + await Task.Run(async () => await host.OnProjectRenamingAsync()); + + // Assert - 1 + snapshot = Assert.Single(ProjectManager.Projects); + Assert.Equal("Test2.csproj", snapshot.FilePath); + Assert.Same(FallbackRazorConfiguration.MVC_2_0, snapshot.Configuration); + + await Task.Run(async () => await host.DisposeAsync()); + Assert.Empty(ProjectManager.Projects); + } + + private class TestFallbackRazorProjectHost : FallbackRazorProjectHost + { + internal TestFallbackRazorProjectHost(IUnconfiguredProjectCommonServices commonServices, Workspace workspace, ProjectSnapshotManagerBase projectManager) + : base(commonServices, workspace, projectManager) + { + } + + public Version AssemblyVersion { get; set; } + + protected override Version GetAssemblyVersion(string filePath) + { + return AssemblyVersion; + } + } + + private class TestProjectSnapshotManager : DefaultProjectSnapshotManager + { + public TestProjectSnapshotManager(ForegroundDispatcher dispatcher, Workspace workspace) + : base(dispatcher, Mock.Of(), Mock.Of(), Array.Empty(), workspace) + { + } + + protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context) + { + } + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotWorkerQueueTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotWorkerQueueTest.cs index f4129b8bd6..70ff58db57 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotWorkerQueueTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotWorkerQueueTest.cs @@ -16,30 +16,54 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { public ProjectSnapshotWorkerQueueTest() { - Project project1 = null; - Project project2 = null; + HostProject1 = new HostProject("Test1.csproj", FallbackRazorConfiguration.MVC_1_0); + HostProject2 = new HostProject("Test2.csproj", FallbackRazorConfiguration.MVC_1_0); - Workspace = TestWorkspace.Create(workspace => - { - project1 = workspace.CurrentSolution.AddProject("Test1", "Test1", LanguageNames.CSharp); - project2 = workspace.CurrentSolution.AddProject("Test2", "Test2", LanguageNames.CSharp); - }); + Workspace = TestWorkspace.Create(); - Project1 = project1; - Project2 = project2; + var projectId1 = ProjectId.CreateNewId("Test1"); + var projectId2 = ProjectId.CreateNewId("Test2"); + + var solution = Workspace.CurrentSolution + .AddProject(ProjectInfo.Create( + projectId1, + VersionStamp.Default, + "Test1", + "Test1", + LanguageNames.CSharp, + "Test1.csproj")) + .AddProject(ProjectInfo.Create( + projectId2, + VersionStamp.Default, + "Test2", + "Test2", + LanguageNames.CSharp, + "Test2.csproj")); ; + + WorkspaceProject1 = solution.GetProject(projectId1); + WorkspaceProject2 = solution.GetProject(projectId2); } - public Project Project1 { get; } + private HostProject HostProject1 { get; } - public Project Project2 { get; } + private HostProject HostProject2 { get; } - public Workspace Workspace { get; } + private Project WorkspaceProject1 { get; } + + private Project WorkspaceProject2 { get; } + + private Workspace Workspace { get; } [ForegroundFact] public async Task Queue_ProcessesNotifications_AndGoesBackToSleep() { // Arrange var projectManager = new TestProjectSnapshotManager(Dispatcher, Workspace); + projectManager.HostProjectAdded(HostProject1); + projectManager.HostProjectAdded(HostProject2); + projectManager.WorkspaceProjectAdded(WorkspaceProject1); + projectManager.WorkspaceProjectAdded(WorkspaceProject2); + var projectWorker = new TestProjectSnapshotWorker(); var queue = new ProjectSnapshotWorkerQueue(Dispatcher, projectManager, projectWorker) @@ -51,10 +75,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem }; // Act & Assert - queue.Enqueue(Project1); + queue.Enqueue(projectManager.GetSnapshot(HostProject1).CreateUpdateContext()); - Assert.True(queue.IsScheduledOrRunning); - Assert.True(queue.HasPendingNotifications); + Assert.True(queue.IsScheduledOrRunning, "Queue should be scheduled during Enqueue"); + Assert.True(queue.HasPendingNotifications, "Queue should have a notification created during Enqueue"); // Allow the background work to proceed. queue.BlockBackgroundWorkStart.Set(); @@ -62,8 +86,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Get off the foreground thread and allow the updates to flow through. await Task.Run(() => queue.NotifyForegroundWorkFinish.Wait(TimeSpan.FromSeconds(1))); - Assert.False(queue.IsScheduledOrRunning); - Assert.False(queue.HasPendingNotifications); + Assert.False(queue.IsScheduledOrRunning, "Queue should not have restarted"); + Assert.False(queue.HasPendingNotifications, "Queue should have processed all notifications"); } [ForegroundFact] @@ -71,6 +95,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // Arrange var projectManager = new TestProjectSnapshotManager(Dispatcher, Workspace); + projectManager.HostProjectAdded(HostProject1); + projectManager.HostProjectAdded(HostProject2); + projectManager.WorkspaceProjectAdded(WorkspaceProject1); + projectManager.WorkspaceProjectAdded(WorkspaceProject2); + var projectWorker = new TestProjectSnapshotWorker(); var queue = new ProjectSnapshotWorkerQueue(Dispatcher, projectManager, projectWorker) @@ -82,20 +111,20 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem }; // Act & Assert - queue.Enqueue(Project1); + queue.Enqueue(projectManager.GetSnapshot(HostProject1).CreateUpdateContext()); - Assert.True(queue.IsScheduledOrRunning); - Assert.True(queue.HasPendingNotifications); + Assert.True(queue.IsScheduledOrRunning, "Queue should be scheduled during Enqueue"); + Assert.True(queue.HasPendingNotifications, "Queue should have a notification created during Enqueue"); // Allow the background work to proceed. queue.BlockBackgroundWorkStart.Set(); queue.NotifyBackgroundWorkFinish.Wait(); // Block the foreground thread so we can queue another notification. - Assert.True(queue.IsScheduledOrRunning); - Assert.False(queue.HasPendingNotifications); + Assert.True(queue.IsScheduledOrRunning, "Worker should be processing now"); + Assert.False(queue.HasPendingNotifications, "Worker should have taken all notifications"); - queue.Enqueue(Project2); + queue.Enqueue(projectManager.GetSnapshot(HostProject2).CreateUpdateContext()); Assert.True(queue.HasPendingNotifications); // Now we should see the worker restart when it finishes. @@ -106,17 +135,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem queue.NotifyForegroundWorkFinish.Reset(); // It should start running again right away. - Assert.True(queue.IsScheduledOrRunning); - Assert.True(queue.HasPendingNotifications); + Assert.True(queue.IsScheduledOrRunning, "Queue should be scheduled during Enqueue"); + Assert.True(queue.HasPendingNotifications, "Queue should have a notification created during Enqueue"); // Allow the background work to proceed. queue.BlockBackgroundWorkStart.Set(); // Get off the foreground thread and allow the updates to flow through. await Task.Run(() => queue.NotifyForegroundWorkFinish.Wait(TimeSpan.FromSeconds(1))); - - Assert.False(queue.IsScheduledOrRunning); - Assert.False(queue.HasPendingNotifications); + + Assert.False(queue.IsScheduledOrRunning, "Queue should not have restarted"); + Assert.False(queue.HasPendingNotifications, "Queue should have processed all notifications"); } private class TestProjectSnapshotManager : DefaultProjectSnapshotManager @@ -126,17 +155,24 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { } - public DefaultProjectSnapshot GetSnapshot(ProjectId id) + public DefaultProjectSnapshot GetSnapshot(HostProject hostProject) { - return Projects.Cast().FirstOrDefault(s => s.UnderlyingProject.Id == id); + return Projects.Cast().FirstOrDefault(s => s.FilePath == hostProject.FilePath); + } + + public DefaultProjectSnapshot GetSnapshot(Project workspaceProject) + { + return Projects.Cast().FirstOrDefault(s => s.FilePath == workspaceProject.FilePath); } protected override void NotifyListeners(ProjectChangeEventArgs e) { } - protected override void NotifyBackgroundWorker(Project project) + protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context) { + Assert.NotNull(context.HostProject); + Assert.NotNull(context.WorkspaceProject); } } diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestAssemblyReference.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestAssemblyReference.cs new file mode 100644 index 0000000000..b80f5e85ba --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestAssemblyReference.cs @@ -0,0 +1,68 @@ +// 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.Reflection; +using System.Threading.Tasks; +using Microsoft.VisualStudio.ProjectSystem.Properties; + +namespace Microsoft.VisualStudio.ProjectSystem.References +{ + internal class TestAssemblyReference : IAssemblyReference + { + public AssemblyName AssemblyName { get; set; } + + public string FullPath { get; set; } + + public IProjectProperties Metadata => throw new System.NotImplementedException(); + + public Task GetAssemblyNameAsync() + { + return Task.FromResult(AssemblyName); + } + + public Task GetCopyLocalAsync() + { + throw new System.NotImplementedException(); + } + + public Task GetCopyLocalSatelliteAssembliesAsync() + { + throw new System.NotImplementedException(); + } + + public Task GetDescriptionAsync() + { + throw new System.NotImplementedException(); + } + + public Task GetFullPathAsync() + { + return Task.FromResult(FullPath); + } + + public Task GetNameAsync() + { + throw new System.NotImplementedException(); + } + + public Task GetReferenceOutputAssemblyAsync() + { + throw new System.NotImplementedException(); + } + + public Task GetRequiredTargetFrameworkAsync() + { + throw new System.NotImplementedException(); + } + + public Task GetSpecificVersionAsync() + { + throw new System.NotImplementedException(); + } + + public Task IsWinMDFileAsync() + { + throw new System.NotImplementedException(); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectChangeDescription.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectChangeDescription.cs new file mode 100644 index 0000000000..a6aa3b21d5 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectChangeDescription.cs @@ -0,0 +1,24 @@ +// 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 Microsoft.VisualStudio.ProjectSystem.Properties; + +namespace Microsoft.VisualStudio.ProjectSystem +{ + internal class TestProjectChangeDescription : IProjectChangeDescription + { + public string RuleName { get; set; } + + public TestProjectRuleSnapshot Before { get; set; } + + public IProjectChangeDiff Difference { get; set; } + + public TestProjectRuleSnapshot After { get; set; } + + IProjectRuleSnapshot IProjectChangeDescription.Before => Before; + + IProjectChangeDiff IProjectChangeDescription.Difference => Difference; + + IProjectRuleSnapshot IProjectChangeDescription.After => After; + } +} \ No newline at end of file diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectRuleSnapshot.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectRuleSnapshot.cs new file mode 100644 index 0000000000..4239470863 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectRuleSnapshot.cs @@ -0,0 +1,61 @@ +// 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 Microsoft.VisualStudio.ProjectSystem.Properties; + +namespace Microsoft.VisualStudio.ProjectSystem +{ + internal class TestProjectRuleSnapshot : IProjectRuleSnapshot + { + public static TestProjectRuleSnapshot CreateProperties(string ruleName, Dictionary properties) + { + return new TestProjectRuleSnapshot( + ruleName, + items: ImmutableDictionary>.Empty, + properties: properties.ToImmutableDictionary(), + dataSourceVersions: ImmutableDictionary.Empty); + } + + public static TestProjectRuleSnapshot CreateItems(string ruleName, Dictionary> items) + { + return new TestProjectRuleSnapshot( + ruleName, + items: items.ToImmutableDictionary(kvp => kvp.Key, kvp => (IImmutableDictionary)kvp.Value.ToImmutableDictionary()), + properties: ImmutableDictionary.Empty, + dataSourceVersions: ImmutableDictionary.Empty); + } + + public TestProjectRuleSnapshot( + string ruleName, + IImmutableDictionary> items, + IImmutableDictionary properties, + IImmutableDictionary dataSourceVersions) + { + RuleName = ruleName; + Items = items; + Properties = properties; + DataSourceVersions = dataSourceVersions; + } + + public void SetProperty(string key, string value) + { + Properties = Properties.SetItem(key, value); + } + + public void SetItem(string key, Dictionary values) + { + Items = Items.SetItem(key, values.ToImmutableDictionary()); + } + + public string RuleName { get; } + + public IImmutableDictionary> Items { get; set; } + + public IImmutableDictionary Properties { get; set; } + + public IImmutableDictionary DataSourceVersions { get; } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectSystemServices.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectSystemServices.cs new file mode 100644 index 0000000000..161600f625 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectSystemServices.cs @@ -0,0 +1,799 @@ +// 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.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using System.Xml; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.XamlTypes; +using Microsoft.VisualStudio.Composition; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.Build; +using Microsoft.VisualStudio.ProjectSystem.Properties; +using Microsoft.VisualStudio.ProjectSystem.References; +using Microsoft.VisualStudio.Threading; +using Moq; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class TestProjectSystemServices : IUnconfiguredProjectCommonServices + { + public TestProjectSystemServices(string fullPath, params TestPropertyData[] data) + { + ProjectService = new TestProjectService(); + ThreadingService = ProjectService.Services.ThreadingPolicy; + + UnconfiguredProject = new TestUnconfiguredProject(ProjectService, fullPath); + ProjectService.LoadedUnconfiguredProjects.Add(UnconfiguredProject); + + ActiveConfiguredProject = new TestConfiguredProject(UnconfiguredProject, data); + UnconfiguredProject.LoadedConfiguredProjects.Add(ActiveConfiguredProject); + + ActiveConfiguredProjectAssemblyReferences = new TestAssemblyReferencesService(); + ActiveConfiguredProjectRazorProperties = new Rules.RazorProjectProperties(ActiveConfiguredProject, UnconfiguredProject); + ActiveConfiguredProjectSubscription = new TestActiveConfiguredProjectSubscriptionService(); + } + + public TestProjectServices Services { get; } + + public TestProjectService ProjectService { get; } + + public TestUnconfiguredProject UnconfiguredProject { get; } + + public TestConfiguredProject ActiveConfiguredProject { get; } + + public TestAssemblyReferencesService ActiveConfiguredProjectAssemblyReferences { get; } + + public Rules.RazorProjectProperties ActiveConfiguredProjectRazorProperties { get; } + + public TestActiveConfiguredProjectSubscriptionService ActiveConfiguredProjectSubscription { get; } + + public TestThreadingService ThreadingService { get; } + + ConfiguredProject IUnconfiguredProjectCommonServices.ActiveConfiguredProject => ActiveConfiguredProject; + + IAssemblyReferencesService IUnconfiguredProjectCommonServices.ActiveConfiguredProjectAssemblyReferences => ActiveConfiguredProjectAssemblyReferences; + + IPackageReferencesService IUnconfiguredProjectCommonServices.ActiveConfiguredProjectPackageReferences => throw new NotImplementedException(); + + Rules.RazorProjectProperties IUnconfiguredProjectCommonServices.ActiveConfiguredProjectRazorProperties => ActiveConfiguredProjectRazorProperties; + + IActiveConfiguredProjectSubscriptionService IUnconfiguredProjectCommonServices.ActiveConfiguredProjectSubscription => ActiveConfiguredProjectSubscription; + + IProjectThreadingService IUnconfiguredProjectCommonServices.ThreadingService => ThreadingService; + + UnconfiguredProject IUnconfiguredProjectCommonServices.UnconfiguredProject => UnconfiguredProject; + + public IProjectVersionedValue CreateUpdate(params TestProjectChangeDescription[] descriptions) + { + return new ProjectVersionedValue( + value: new ProjectSubscriptionUpdate( + projectChanges: descriptions.ToImmutableDictionary(d => d.RuleName, d => (IProjectChangeDescription)d), + projectConfiguration: ActiveConfiguredProject.ProjectConfiguration), + dataSourceVersions: ImmutableDictionary.Empty); + } + + public class TestProjectServices : IProjectServices + { + public TestProjectServices(TestProjectService projectService) + { + ProjectService = projectService; + ThreadingPolicy = new TestThreadingService(); + } + + public TestProjectService ProjectService { get; } + + public TestThreadingService ThreadingPolicy { get; } + + IProjectLockService IProjectServices.ProjectLockService => throw new NotImplementedException(); + + IProjectThreadingService IProjectServices.ThreadingPolicy => ThreadingPolicy; + + IProjectFaultHandlerService IProjectServices.FaultHandler => throw new NotImplementedException(); + + IProjectReloader IProjectServices.ProjectReloader => throw new NotImplementedException(); + + ExportProvider IProjectCommonServices.ExportProvider => throw new NotImplementedException(); + + IProjectDataSourceRegistry IProjectCommonServices.DataSourceRegistry => throw new NotImplementedException(); + + IProjectService IProjectCommonServices.ProjectService => ProjectService; + + IProjectCapabilitiesScope IProjectCommonServices.Capabilities => throw new NotImplementedException(); + } + + public class TestProjectService : IProjectService + { + public TestProjectService() + { + LoadedUnconfiguredProjects = new List(); + Services = new TestProjectServices(this); + } + + public List LoadedUnconfiguredProjects { get; } + + public TestProjectServices Services { get; } + + IEnumerable IProjectService.LoadedUnconfiguredProjects => throw new NotImplementedException(); + + IProjectServices IProjectService.Services => Services; + + IProjectCapabilitiesScope IProjectService.Capabilities => throw new NotImplementedException(); + + Task IProjectService.LoadProjectAsync(string projectLocation, IImmutableSet projectCapabilities) + { + throw new NotImplementedException(); + } + + Task IProjectService.LoadProjectAsync(XmlReader reader, IImmutableSet projectCapabilities) + { + throw new NotImplementedException(); + } + + Task IProjectService.LoadProjectAsync(string projectLocation, bool delayAutoLoad, IImmutableSet projectCapabilities) + { + throw new NotImplementedException(); + } + + Task IProjectService.UnloadProjectAsync(UnconfiguredProject project) + { + throw new NotImplementedException(); + } + } + + public class TestUnconfiguredProject : UnconfiguredProject + { + public TestUnconfiguredProject(TestProjectService projectService, string fullPath) + { + ProjectService = projectService; + FullPath = fullPath; + + LoadedConfiguredProjects = new List(); + } + + public TestProjectService ProjectService { get; } + + public string FullPath { get; set; } + + public List LoadedConfiguredProjects { get; } + + string UnconfiguredProject.FullPath => FullPath; + bool UnconfiguredProject.RequiresReloadForExternalFileChange => throw new NotImplementedException(); + + IProjectCapabilitiesScope UnconfiguredProject.Capabilities => throw new NotImplementedException(); + + IProjectService UnconfiguredProject.ProjectService => ProjectService; + + IUnconfiguredProjectServices UnconfiguredProject.Services => throw new NotImplementedException(); + + IEnumerable UnconfiguredProject.LoadedConfiguredProjects => LoadedConfiguredProjects; + + bool UnconfiguredProject.IsLoading => throw new NotImplementedException(); + + event AsyncEventHandler UnconfiguredProject.ProjectUnloading + { + add + { + throw new NotImplementedException(); + } + + remove + { + throw new NotImplementedException(); + } + } + + event AsyncEventHandler UnconfiguredProject.ProjectRenaming + { + add + { + } + + remove + { + } + } + + event AsyncEventHandler UnconfiguredProject.ProjectRenamedOnWriter + { + add + { + throw new NotImplementedException(); + } + + remove + { + throw new NotImplementedException(); + } + } + + event AsyncEventHandler UnconfiguredProject.ProjectRenamed + { + add + { + throw new NotImplementedException(); + } + + remove + { + throw new NotImplementedException(); + } + } + + Task UnconfiguredProject.CanRenameAsync(string newFilePath) + { + throw new NotImplementedException(); + } + + Task UnconfiguredProject.GetFileEncodingAsync() + { + throw new NotImplementedException(); + } + + Task UnconfiguredProject.GetIsDirtyAsync() + { + throw new NotImplementedException(); + } + + Task UnconfiguredProject.GetSuggestedConfiguredProjectAsync() + { + throw new NotImplementedException(); + } + + Task UnconfiguredProject.LoadConfiguredProjectAsync(string name, IImmutableDictionary configurationProperties) + { + throw new NotImplementedException(); + } + + Task UnconfiguredProject.LoadConfiguredProjectAsync(ProjectConfiguration projectConfiguration) + { + throw new NotImplementedException(); + } + + Task UnconfiguredProject.ReloadAsync(bool immediately) + { + throw new NotImplementedException(); + } + + Task UnconfiguredProject.RenameAsync(string newFilePath) + { + throw new NotImplementedException(); + } + + Task UnconfiguredProject.SaveAsync(string filePath) + { + throw new NotImplementedException(); + } + + Task UnconfiguredProject.SaveCopyAsync(string filePath, Encoding fileEncoding) + { + throw new NotImplementedException(); + } + + Task UnconfiguredProject.SaveUserFileAsync() + { + throw new NotImplementedException(); + } + + Task UnconfiguredProject.SetFileEncodingAsync(Encoding value) + { + throw new NotImplementedException(); + } + } + + public class TestConfiguredProject : ConfiguredProject + { + public TestConfiguredProject(TestUnconfiguredProject unconfiguredProject, TestPropertyData[] data) + { + UnconfiguredProject = unconfiguredProject; + Services = new TestConfiguredProjectServices(this, data); + + ProjectConfiguration = new StandardProjectConfiguration( + "Debug|AnyCPU", + ImmutableDictionary.Empty.Add("Configuration", "Debug").Add("Platform", "AnyCPU")); + } + + public TestUnconfiguredProject UnconfiguredProject { get; } + + public ProjectConfiguration ProjectConfiguration { get; } + + public TestConfiguredProjectServices Services { get; } + + IComparable ConfiguredProject.ProjectVersion => throw new NotImplementedException(); + + IReceivableSourceBlock ConfiguredProject.ProjectVersionBlock => throw new NotImplementedException(); + + ProjectConfiguration ConfiguredProject.ProjectConfiguration => ProjectConfiguration; + + IProjectCapabilitiesScope ConfiguredProject.Capabilities => throw new NotImplementedException(); + + UnconfiguredProject ConfiguredProject.UnconfiguredProject => UnconfiguredProject; + + IConfiguredProjectServices ConfiguredProject.Services => Services; + + event AsyncEventHandler ConfiguredProject.ProjectUnloading + { + add + { + throw new NotImplementedException(); + } + + remove + { + throw new NotImplementedException(); + } + } + + event EventHandler ConfiguredProject.ProjectChanged + { + add + { + throw new NotImplementedException(); + } + + remove + { + throw new NotImplementedException(); + } + } + + event EventHandler ConfiguredProject.ProjectChangedSynchronous + { + add + { + throw new NotImplementedException(); + } + + remove + { + throw new NotImplementedException(); + } + } + + void ConfiguredProject.NotifyProjectChange() + { + throw new NotImplementedException(); + } + } + + public class TestConfiguredProjectServices : IConfiguredProjectServices + { + public TestConfiguredProjectServices(TestConfiguredProject configuredProject, TestPropertyData[] data) + { + ConfiguredProject = configuredProject; + + AdditionalRuleDefinitions = new TestAdditionalRuleDefinitionsService(); + PropertyPagesCatalog = new TestPropertyPagesCatalogProvider(new TestPropertyPagesCatalog(data)); + } + + public TestConfiguredProject ConfiguredProject { get; } + + public TestAdditionalRuleDefinitionsService AdditionalRuleDefinitions { get; } + + public TestPropertyPagesCatalogProvider PropertyPagesCatalog { get; } + + IOutputGroupsService IConfiguredProjectServices.OutputGroups => throw new NotImplementedException(); + + IBuildProject IConfiguredProjectServices.Build => throw new NotImplementedException(); + + IBuildSupport IConfiguredProjectServices.BuildSupport => throw new NotImplementedException(); + + IAssemblyReferencesService IConfiguredProjectServices.AssemblyReferences => throw new NotImplementedException(); + + IComReferencesService IConfiguredProjectServices.ComReferences => throw new NotImplementedException(); + + ISdkReferencesService IConfiguredProjectServices.SdkReferences => throw new NotImplementedException(); + + IPackageReferencesService IConfiguredProjectServices.PackageReferences => throw new NotImplementedException(); + + IWinRTReferencesService IConfiguredProjectServices.WinRTReferences => throw new NotImplementedException(); + + IBuildDependencyProjectReferencesService IConfiguredProjectServices.ProjectReferences => throw new NotImplementedException(); + + IProjectItemProvider IConfiguredProjectServices.SourceItems => throw new NotImplementedException(); + + IProjectPropertiesProvider IConfiguredProjectServices.ProjectPropertiesProvider => throw new NotImplementedException(); + + IProjectPropertiesProvider IConfiguredProjectServices.UserPropertiesProvider => throw new NotImplementedException(); + + IProjectAsynchronousTasksService IConfiguredProjectServices.ProjectAsynchronousTasks => throw new NotImplementedException(); + + IAdditionalRuleDefinitionsService IConfiguredProjectServices.AdditionalRuleDefinitions => AdditionalRuleDefinitions; + + IPropertyPagesCatalogProvider IConfiguredProjectServices.PropertyPagesCatalog => PropertyPagesCatalog; + + IProjectSubscriptionService IConfiguredProjectServices.ProjectSubscription => throw new NotImplementedException(); + + IProjectSnapshotService IConfiguredProjectServices.ProjectSnapshotService => throw new NotImplementedException(); + + object IConfiguredProjectServices.HostObject => throw new NotImplementedException(); + + ExportProvider IProjectCommonServices.ExportProvider => throw new NotImplementedException(); + + IProjectDataSourceRegistry IProjectCommonServices.DataSourceRegistry => throw new NotImplementedException(); + + IProjectService IProjectCommonServices.ProjectService => ConfiguredProject.UnconfiguredProject.ProjectService; + + IProjectCapabilitiesScope IProjectCommonServices.Capabilities => throw new NotImplementedException(); + } + + public class TestAdditionalRuleDefinitionsService : IAdditionalRuleDefinitionsService + { + IProjectVersionedValue IAdditionalRuleDefinitionsService.AdditionalRuleDefinitions => throw new NotImplementedException(); + + IReceivableSourceBlock> IProjectValueDataSource.SourceBlock => throw new NotImplementedException(); + + ISourceBlock> IProjectValueDataSource.SourceBlock => throw new NotImplementedException(); + + NamedIdentity IProjectValueDataSource.DataSourceKey => throw new NotImplementedException(); + + IComparable IProjectValueDataSource.DataSourceVersion => throw new NotImplementedException(); + + bool IAdditionalRuleDefinitionsService.AddRuleDefinition(string path, string context) + { + return false; + } + + bool IAdditionalRuleDefinitionsService.AddRuleDefinition(Rule rule, string context) + { + return false; + } + + IDisposable IJoinableProjectValueDataSource.Join() + { + throw new NotImplementedException(); + } + + bool IAdditionalRuleDefinitionsService.RemoveRuleDefinition(string path) + { + return false; + } + + bool IAdditionalRuleDefinitionsService.RemoveRuleDefinition(Rule rule) + { + return false; + } + } + + public class TestPropertyPagesCatalogProvider : IPropertyPagesCatalogProvider + { + public TestPropertyPagesCatalogProvider(TestPropertyPagesCatalog catalog) + { + Catalog = catalog; + CatalogsByContext = new Dictionary() + { + { "Project", catalog }, + }; + } + + public TestPropertyPagesCatalog Catalog { get; } + + public Dictionary CatalogsByContext { get; } + + public IReceivableSourceBlock> SourceBlock => throw new NotImplementedException(); + + public NamedIdentity DataSourceKey => throw new NotImplementedException(); + + public IComparable DataSourceVersion => throw new NotImplementedException(); + + ISourceBlock> IProjectValueDataSource.SourceBlock => throw new NotImplementedException(); + + public Task GetCatalogAsync(string name, CancellationToken cancellationToken = default) + { + return Task.FromResult(CatalogsByContext[name]); + } + + public Task> GetCatalogsAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult>(CatalogsByContext.ToImmutableDictionary()); + } + + public IPropertyPagesCatalog GetMemoryOnlyCatalog(string context) + { + return Catalog; + } + + public IDisposable Join() + { + throw new NotImplementedException(); + } + } + + public class TestActiveConfiguredProjectSubscriptionService : IActiveConfiguredProjectSubscriptionService + { + public TestActiveConfiguredProjectSubscriptionService() + { + JointRuleBlock = new BufferBlock>(); + JointRuleSource = new TestProjectValueDataSource(JointRuleBlock); + } + + public BufferBlock> JointRuleBlock { get; } + + public TestProjectValueDataSource JointRuleSource { get; } + + IReceivableSourceBlock> IProjectSubscriptionService.ProjectBlock => throw new NotImplementedException(); + + IProjectValueDataSource IProjectSubscriptionService.ProjectSource => throw new NotImplementedException(); + + IProjectValueDataSource IProjectSubscriptionService.ImportTreeSource => throw new NotImplementedException(); + + IProjectValueDataSource IProjectSubscriptionService.SharedFoldersSource => throw new NotImplementedException(); + + IProjectValueDataSource> IProjectSubscriptionService.OutputGroupsSource => throw new NotImplementedException(); + + IReceivableSourceBlock> IProjectSubscriptionService.ProjectCatalogBlock => throw new NotImplementedException(); + + IProjectValueDataSource IProjectSubscriptionService.ProjectCatalogSource => throw new NotImplementedException(); + + IReceivableSourceBlock> IProjectSubscriptionService.ProjectRuleBlock => throw new NotImplementedException(); + + IProjectValueDataSource IProjectSubscriptionService.ProjectRuleSource => throw new NotImplementedException(); + + IReceivableSourceBlock> IProjectSubscriptionService.ProjectBuildRuleBlock => throw new NotImplementedException(); + + IProjectValueDataSource IProjectSubscriptionService.ProjectBuildRuleSource => throw new NotImplementedException(); + + ISourceBlock> IProjectSubscriptionService.JointRuleBlock => JointRuleBlock; + + IProjectValueDataSource IProjectSubscriptionService.JointRuleSource => JointRuleSource; + + IReceivableSourceBlock> IProjectSubscriptionService.SourceItemsRuleBlock => throw new NotImplementedException(); + + IProjectValueDataSource IProjectSubscriptionService.SourceItemsRuleSource => throw new NotImplementedException(); + + IReceivableSourceBlock>> IProjectSubscriptionService.SourceItemRuleNamesBlock => throw new NotImplementedException(); + + IProjectValueDataSource> IProjectSubscriptionService.SourceItemRuleNamesSource => throw new NotImplementedException(); + } + + public class TestProjectValueDataSource : IProjectValueDataSource + { + public TestProjectValueDataSource(BufferBlock> sourceBlock) + { + SourceBlock = sourceBlock; + } + + public BufferBlock> SourceBlock { get; } + + IReceivableSourceBlock> IProjectValueDataSource.SourceBlock => SourceBlock; + + ISourceBlock> IProjectValueDataSource.SourceBlock => throw new NotImplementedException(); + + NamedIdentity IProjectValueDataSource.DataSourceKey => throw new NotImplementedException(); + + IComparable IProjectValueDataSource.DataSourceVersion => throw new NotImplementedException(); + + IDisposable IJoinableProjectValueDataSource.Join() + { + throw new NotImplementedException(); + } + } + + public class TestPropertyPagesCatalog : IPropertyPagesCatalog + { + private readonly Dictionary _data; + + public TestPropertyPagesCatalog(TestPropertyData[] data) + { + _data = new Dictionary(); + foreach (var category in data.GroupBy(p => p.Category)) + { + _data.Add( + category.Key, + CreateRule(category.Select(property => CreateProperty(property.PropertyName, property.Value, property.SetValues)))); + } + } + + private static IRule CreateRule(IEnumerable properties) + { + var rule = new Mock(); + rule + .Setup(o => o.GetProperty(It.IsAny())) + .Returns((string propertyName) => + { + + return properties.FirstOrDefault(p => p.Name == propertyName); + }); + + return rule.Object; + } + + private static IProperty CreateProperty(string name, object value, List setValues = null) + { + var property = new Mock(); + property.SetupGet(o => o.Name) + .Returns(name); + + property.Setup(o => o.GetValueAsync()) + .ReturnsAsync(value); + + property.As().Setup(p => p.GetEvaluatedValueAtEndAsync()).ReturnsAsync(value.ToString()); + property.As().Setup(p => p.GetEvaluatedValueAsync()).ReturnsAsync(value.ToString()); + + if (setValues != null) + { + property + .Setup(p => p.SetValueAsync(It.IsAny())) + .Callback(obj => setValues.Add(obj)) + .Returns(() => Task.CompletedTask); + } + + return property.Object; + } + + IRule IPropertyPagesCatalog.BindToContext(string schemaName, string file, string itemType, string itemName) + { + _data.TryGetValue(schemaName, out var value); + return value; + } + + IRule IPropertyPagesCatalog.BindToContext(string schemaName, IProjectPropertiesContext context) + { + throw new NotImplementedException(); + } + + IRule IPropertyPagesCatalog.BindToContext(string schemaName, ProjectInstance projectInstance, string itemType, string itemName) + { + throw new NotImplementedException(); + } + + IRule IPropertyPagesCatalog.BindToContext(string schemaName, ProjectInstance projectInstance, ITaskItem taskItem) + { + throw new NotImplementedException(); + } + + IReadOnlyCollection IPropertyPagesCatalog.GetProjectLevelPropertyPagesSchemas() + { + throw new NotImplementedException(); + } + + IReadOnlyCollection IPropertyPagesCatalog.GetPropertyPagesSchemas() + { + throw new NotImplementedException(); + } + + IReadOnlyCollection IPropertyPagesCatalog.GetPropertyPagesSchemas(string itemType) + { + throw new NotImplementedException(); + } + + IReadOnlyCollection IPropertyPagesCatalog.GetPropertyPagesSchemas(IEnumerable paths) + { + throw new NotImplementedException(); + } + + Rule IPropertyPagesCatalog.GetSchema(string schemaName) + { + throw new NotImplementedException(); + } + } + + public class TestAssemblyReferencesService : IAssemblyReferencesService + { + public TestAssemblyReferencesService() + { + ResolvedReferences = new List(); + } + + public List ResolvedReferences { get; } + + Task> IAssemblyReferencesService.AddAsync(AssemblyName assemblyName, string assemblyPath) + { + throw new NotImplementedException(); + } + + Task IAssemblyReferencesService.CanResolveAsync(AssemblyName assemblyName, string assemblyPath) + { + throw new NotImplementedException(); + } + + Task IAssemblyReferencesService.ContainsAsync(AssemblyName assemblyName, string assemblyPath) + { + throw new NotImplementedException(); + } + + Task IAssemblyReferencesService.GetResolvedReferenceAsync(AssemblyName assemblyName, string assemblyPath) + { + throw new NotImplementedException(); + } + + Task IResolvableReferencesService.GetResolvedReferenceAsync(IUnresolvedAssemblyReference unresolvedReference) + { + throw new NotImplementedException(); + } + + Task> IResolvableReferencesService.GetResolvedReferencesAsync() + { + return Task.FromResult>(ResolvedReferences.ToImmutableHashSet()); + } + + Task IAssemblyReferencesService.GetUnresolvedReferenceAsync(AssemblyName assemblyName, string assemblyPath) + { + throw new NotImplementedException(); + } + + Task IResolvableReferencesService.GetUnresolvedReferenceAsync(IAssemblyReference resolvedReference) + { + throw new NotImplementedException(); + } + + Task> IResolvableReferencesService.GetUnresolvedReferencesAsync() + { + throw new NotImplementedException(); + } + + Task IAssemblyReferencesService.RemoveAsync(AssemblyName assemblyName, string assemblyPath) + { + throw new NotImplementedException(); + } + + Task IResolvableReferencesService.RemoveAsync(IUnresolvedAssemblyReference reference) + { + throw new NotImplementedException(); + } + + Task IResolvableReferencesService.RemoveAsync(IEnumerable references) + { + throw new NotImplementedException(); + } + } + + public class TestThreadingService : IProjectThreadingService + { + public TestThreadingService() + { + JoinableTaskContext = new JoinableTaskContextNode(new JoinableTaskContext()); + JoinableTaskFactory = new JoinableTaskFactory(JoinableTaskContext.Context); + } + + public JoinableTaskContextNode JoinableTaskContext { get; } + + public JoinableTaskFactory JoinableTaskFactory { get; } + + public bool IsOnMainThread => throw new NotImplementedException(); + + public void ExecuteSynchronously(Func asyncAction) + { + asyncAction().GetAwaiter().GetResult(); + } + + public T ExecuteSynchronously(Func> asyncAction) + { + return asyncAction().GetAwaiter().GetResult(); + } + + public void Fork( + Func asyncAction, + JoinableTaskFactory factory = null, + UnconfiguredProject unconfiguredProject = null, + ConfiguredProject configuredProject = null, + ErrorReportSettings watsonReportSettings = null, + ProjectFaultSeverity faultSeverity = ProjectFaultSeverity.Recoverable, + ForkOptions options = ForkOptions.Default) + { + throw new NotImplementedException(); + } + + public IDisposable SuppressProjectExecutionContext() + { + throw new NotImplementedException(); + } + + public void VerifyOnUIThread() + { + if (!JoinableTaskContext.IsOnMainThread) + { + throw new InvalidOperationException("This isn't the main thread."); + } + } + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestPropertyData.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestPropertyData.cs new file mode 100644 index 0000000000..c2b3fb24c5 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestPropertyData.cs @@ -0,0 +1,18 @@ +// 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.Collections.Generic; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + public class TestPropertyData + { + public string Category { get; set; } + + public string PropertyName { get; set; } + + public object Value { get; set; } + + public List SetValues { get; set; } + } +} diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs index 8e3c53d614..36073681dc 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs @@ -23,7 +23,7 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo _documentTracker = documentTracker; } - public string Configuration => _documentTracker.Configuration?.DisplayName; + public string Configuration => _documentTracker.Configuration?.ConfigurationName; public bool IsSupportedDocument => _documentTracker.IsSupportedProject; diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj b/tooling/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj index bcfa2e6662..e5ac2eaea6 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj +++ b/tooling/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj @@ -79,7 +79,9 @@ + + @@ -296,4 +298,4 @@ - + \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectInfoViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectInfoViewModel.cs index 7c01538db6..98aa5b709b 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectInfoViewModel.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectInfoViewModel.cs @@ -8,21 +8,10 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo { public class ProjectInfoViewModel : NotifyPropertyChanged { - private ObservableCollection _assemblies; private ObservableCollection _directives; private ObservableCollection _documents; private ObservableCollection _tagHelpers; - public ObservableCollection Assemblies - { - get { return _assemblies; } - set - { - _assemblies = value; - OnPropertyChanged(); - } - } - public ObservableCollection Directives { get { return _directives; } diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectSnapshotViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectSnapshotViewModel.cs new file mode 100644 index 0000000000..c95157cec1 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectSnapshotViewModel.cs @@ -0,0 +1,36 @@ +// 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. + +#if RAZOR_EXTENSION_DEVELOPER_MODE +using System.Collections.ObjectModel; +using System.IO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.VisualStudio.RazorExtension.RazorInfo +{ + public class ProjectSnapshotViewModel : NotifyPropertyChanged + { + internal ProjectSnapshotViewModel(ProjectSnapshot project) + { + Project = project; + + Id = project.WorkspaceProject?.Id; + Properties = new ObservableCollection() + { + new PropertyViewModel("Razor Language Version", project.Configuration?.LanguageVersion.ToString()), + new PropertyViewModel("Configuration Name", $"{project.Configuration?.ConfigurationName} ({project.Configuration?.GetType().Name ?? "unknown"})"), + new PropertyViewModel("Workspace Project", project.WorkspaceProject?.Name) + }; + } + + internal ProjectSnapshot Project { get; } + + public string Name => Path.GetFileNameWithoutExtension(Project.FilePath); + + public ProjectId Id { get; } + + public ObservableCollection Properties { get; } + } +} +#endif \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectViewModel.cs index 1c649e6f0a..1d1442c05f 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectViewModel.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectViewModel.cs @@ -2,21 +2,35 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #if RAZOR_EXTENSION_DEVELOPER_MODE -using Microsoft.CodeAnalysis; +using System.IO; namespace Microsoft.VisualStudio.RazorExtension.RazorInfo { public class ProjectViewModel : NotifyPropertyChanged { - public ProjectViewModel(Project project) + private ProjectSnapshotViewModel _snapshot; + + internal ProjectViewModel(string filePath) { - Id = project.Id; - Name = project.Name; + FilePath = filePath; } + + public string FilePath { get; } - public string Name { get; } + public string Name => Path.GetFileNameWithoutExtension(FilePath); - public ProjectId Id { get; } + public bool HasSnapshot => Snapshot != null; + + public ProjectSnapshotViewModel Snapshot + { + get => _snapshot; + set + { + _snapshot = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSnapshot)); + } + } } } #endif \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/PropertyViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/PropertyViewModel.cs new file mode 100644 index 0000000000..e7586847ac --- /dev/null +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/PropertyViewModel.cs @@ -0,0 +1,21 @@ +// 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. + +#if RAZOR_EXTENSION_DEVELOPER_MODE + +namespace Microsoft.VisualStudio.RazorExtension.RazorInfo +{ + public class PropertyViewModel : NotifyPropertyChanged + { + internal PropertyViewModel(string name, string value) + { + Name = name; + Value = value; + } + + public string Name { get; } + + public string Value { get; } + } +} +#endif \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindow.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindow.cs index 57fa70f4ec..8b405550fd 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindow.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindow.cs @@ -4,7 +4,6 @@ #if RAZOR_EXTENSION_DEVELOPER_MODE using System; using System.Runtime.InteropServices; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.ComponentModelHost; @@ -18,16 +17,22 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo [Guid("079e9499-d150-40af-8876-3047f7942c2a")] public class RazorInfoToolWindow : ToolWindowPane { - private ProjectExtensibilityConfigurationFactory _configurationFactory; private IRazorEngineDocumentGenerator _documentGenerator; private IRazorEngineDirectiveResolver _directiveResolver; + private ProjectSnapshotManager _projectManager; private TagHelperResolver _tagHelperResolver; private VisualStudioWorkspace _workspace; public RazorInfoToolWindow() : base(null) { - this.Caption = "Razor Info"; - this.Content = new RazorInfoToolWindowControl(); + Caption = "Razor Info"; + Content = new RazorInfoToolWindowControl(); + } + + private RazorInfoViewModel DataContext + { + get => (RazorInfoViewModel)((RazorInfoToolWindowControl)Content).DataContext; + set => ((RazorInfoToolWindowControl)Content).DataContext = value; } protected override void Initialize() @@ -35,16 +40,28 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo base.Initialize(); var componentModel = (IComponentModel)GetService(typeof(SComponentModel)); + _workspace = componentModel.GetService(); - _configurationFactory = componentModel.GetService(); _documentGenerator = componentModel.GetService(); _directiveResolver = componentModel.GetService(); - _tagHelperResolver = componentModel.GetService(); + _tagHelperResolver = _workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); - _workspace = componentModel.GetService(); - _workspace.WorkspaceChanged += Workspace_WorkspaceChanged; + _projectManager = _workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); + _projectManager.Changed += ProjectManager_Changed; - Reset(_workspace.CurrentSolution); + DataContext = new RazorInfoViewModel(this, _workspace, _projectManager, _directiveResolver, _tagHelperResolver, _documentGenerator, OnException); + foreach (var project in _projectManager.Projects) + { + DataContext.Projects.Add(new ProjectViewModel(project.FilePath) + { + Snapshot = new ProjectSnapshotViewModel(project), + }); + } + + if (DataContext.Projects.Count > 0) + { + DataContext.CurrentProject = DataContext.Projects[0]; + } } protected override void Dispose(bool disposing) @@ -53,28 +70,69 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo if (disposing) { - _workspace.WorkspaceChanged -= Workspace_WorkspaceChanged; + _projectManager.Changed -= ProjectManager_Changed; } } - private void Reset(Solution solution) + private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) { - if (solution == null) + switch (e.Kind) { - ((RazorInfoToolWindowControl)this.Content).DataContext = null; - return; - } + case ProjectChangeKind.Added: + { + var added = new ProjectViewModel(e.Project.FilePath) + { + Snapshot = new ProjectSnapshotViewModel(e.Project), + }; - var viewModel = new RazorInfoViewModel(this, _workspace, _configurationFactory, _directiveResolver, _tagHelperResolver, _documentGenerator, OnException); - foreach (var project in solution.Projects) - { - if (project.Language == LanguageNames.CSharp) - { - viewModel.Projects.Add(new ProjectViewModel(project)); - } - } + DataContext.Projects.Add(added); - ((RazorInfoToolWindowControl)this.Content).DataContext = viewModel; + if (DataContext.Projects.Count == 1) + { + DataContext.CurrentProject = added; + } + break; + } + + case ProjectChangeKind.Removed: + { + ProjectViewModel removed = null; + for (var i = DataContext.Projects.Count - 1; i >= 0; i--) + { + var project = DataContext.Projects[i]; + if (project.FilePath == e.Project.FilePath) + { + removed = project; + DataContext.Projects.RemoveAt(i); + break; + } + } + + if (DataContext.CurrentProject == removed) + { + DataContext.CurrentProject = null; + } + + break; + } + + case ProjectChangeKind.Changed: + { + ProjectViewModel changed = null; + for (var i = DataContext.Projects.Count - 1; i >= 0; i--) + { + var project = DataContext.Projects[i]; + if (project.FilePath == e.Project.FilePath) + { + changed = project; + changed.Snapshot = new ProjectSnapshotViewModel(e.Project); + break; + } + } + + break; + } + } } private void OnException(Exception ex) @@ -87,24 +145,6 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST); } - - private void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e) - { - switch (e.Kind) - { - case WorkspaceChangeKind.ProjectAdded: - case WorkspaceChangeKind.ProjectChanged: - case WorkspaceChangeKind.ProjectReloaded: - case WorkspaceChangeKind.ProjectRemoved: - case WorkspaceChangeKind.SolutionAdded: - case WorkspaceChangeKind.SolutionChanged: - case WorkspaceChangeKind.SolutionCleared: - case WorkspaceChangeKind.SolutionReloaded: - case WorkspaceChangeKind.SolutionRemoved: - Reset(e.NewSolution); - break; - } - } } } #endif diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindowControl.xaml b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindowControl.xaml index d4178dda0c..3044ba25c7 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindowControl.xaml +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindowControl.xaml @@ -1,162 +1,266 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -