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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -