diff --git a/benchmarks/Microsoft.AspNetCore.Razor.Performance/Microsoft.AspNetCore.Razor.Performance.csproj b/benchmarks/Microsoft.AspNetCore.Razor.Performance/Microsoft.AspNetCore.Razor.Performance.csproj index e63049a74f..790364d92e 100644 --- a/benchmarks/Microsoft.AspNetCore.Razor.Performance/Microsoft.AspNetCore.Razor.Performance.csproj +++ b/benchmarks/Microsoft.AspNetCore.Razor.Performance/Microsoft.AspNetCore.Razor.Performance.csproj @@ -10,14 +10,12 @@ + - - Shared\RazorDiagnosticJsonConverter.cs - - - Shared\TagHelperDescriptorJsonConverter.cs + + Serialization\%(FileName)%(Extension) diff --git a/benchmarks/Microsoft.AspNetCore.Razor.Performance/TagHelperSerializationBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Razor.Performance/TagHelperSerializationBenchmark.cs index bfee04164f..f1dcabe640 100644 --- a/benchmarks/Microsoft.AspNetCore.Razor.Performance/TagHelperSerializationBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Razor.Performance/TagHelperSerializationBenchmark.cs @@ -7,7 +7,7 @@ using System.IO; using System.Text; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.VisualStudio.LanguageServices.Razor; +using Microsoft.VisualStudio.LanguageServices.Razor.Serialization; using Newtonsoft.Json; namespace Microsoft.AspNetCore.Razor.Performance diff --git a/build/MPack.targets b/build/MPack.targets index fe4fff7f16..54ace6edf4 100644 --- a/build/MPack.targets +++ b/build/MPack.targets @@ -70,7 +70,10 @@ - + + + + diff --git a/build/dependencies.props b/build/dependencies.props index a21fbae048..bb46a4acdc 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -52,6 +52,7 @@ 2.7.0-beta3-62512-06 2.7.0-beta3-62512-06 2.7.0-beta3-62512-06 + 2.7.0-beta3-62512-06 2.7.0-beta3-62512-06 2.7.0-beta3-62512-06 0.8.0 diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ExtensionInitializer.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ExtensionInitializer.cs new file mode 100644 index 0000000000..51f16b552f --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ExtensionInitializer.cs @@ -0,0 +1,29 @@ +// 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.AspNetCore.Mvc.Razor.Extensions.Version1_X +{ + internal class ExtensionInitializer : RazorExtensionInitializer + { + public override void Initialize(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (builder.Configuration.ConfigurationName == "MVC-1.0") + { + RazorExtensions.Register(builder); + } + else if (builder.Configuration.ConfigurationName == "MVC-1.1") + { + RazorExtensions.Register(builder); + RazorExtensions.RegisterViewComponentTagHelpers(builder); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Properties/AssemblyInfo.cs index 8bed236c69..8233c62607 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Properties/AssemblyInfo.cs @@ -2,5 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; +using Microsoft.AspNetCore.Razor.Language; + +[assembly: ProvideRazorExtensionInitializer("MVC-1.0", typeof(ExtensionInitializer))] +[assembly: ProvideRazorExtensionInitializer("MVC-1.1", typeof(ExtensionInitializer))] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs index e8b4f0018c..18bfe99a61 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs @@ -45,6 +45,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X throw new ArgumentNullException(nameof(builder)); } + builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); + builder.Features.Add(new ViewComponentTagHelperPass()); builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); } @@ -88,6 +90,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X EnsureDesignTime(builder); + builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); builder.Features.Add(new ViewComponentTagHelperPass()); builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ViewComponentTypeVisitor.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ViewComponentTypeVisitor.cs index fe70827712..90cb1eedca 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ViewComponentTypeVisitor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ViewComponentTypeVisitor.cs @@ -24,10 +24,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X var symbol = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; if (symbol != null) { - if (string.Equals(symbol.Identity.Name, ViewComponentTypes.Assembly, StringComparison.Ordinal) && - symbol.Identity.Version > ViewComponentTypes.AssemblyVersion) + if (string.Equals(symbol.Identity.Name, ViewComponentTypes.Assembly, StringComparison.Ordinal)) { - enabled = true; + enabled = symbol.Identity.Version >= ViewComponentTypes.AssemblyVersion; break; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs index abf1b447f0..f1ca7455cc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs @@ -61,6 +61,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions InheritsDirective.Register(builder); SectionDirective.Register(builder); + builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); + builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); builder.AddTargetExtension(new TemplateTargetExtension() { diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTypeVisitor.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTypeVisitor.cs index c26779a9c5..9b44634095 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTypeVisitor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTypeVisitor.cs @@ -10,31 +10,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { internal class ViewComponentTypeVisitor : SymbolVisitor { - private static readonly Version SupportedVCTHMvcVersion = new Version(1, 1); - private readonly INamedTypeSymbol _viewComponentAttribute; private readonly INamedTypeSymbol _nonViewComponentAttribute; private readonly List _results; public static ViewComponentTypeVisitor Create(Compilation compilation, List results) { - var enabled = false; - foreach (var reference in compilation.References) - { - var symbol = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; - if (symbol != null) - { - if (string.Equals(symbol.Identity.Name, ViewComponentTypes.Assembly, StringComparison.Ordinal) && - symbol.Identity.Version > ViewComponentTypes.AssemblyVersion) - { - enabled = true; - break; - } - } - } - - var vcAttribute = enabled ? compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute) : null; - var nonVCAttribute = enabled ? compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute) : null; + var vcAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute); + var nonVCAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute); return new ViewComponentTypeVisitor(vcAttribute, nonVCAttribute, results); } 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.AspNetCore.Razor.Tools/DiscoverCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs index c846ef3692..5ff3f07d52 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; using Microsoft.Extensions.CommandLineUtils; -using Microsoft.VisualStudio.LanguageServices.Razor; +using Microsoft.VisualStudio.LanguageServices.Razor.Serialization; using Newtonsoft.Json; namespace Microsoft.AspNetCore.Razor.Tools @@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Razor.Tools } var version = RazorLanguageVersion.Parse(Version.Value()); - var configuration = new RazorConfiguration(version, Configuration.Value(), extensions); + var configuration = RazorConfiguration.Create(version, Configuration.Value(), extensions); var result = ExecuteCore( configuration: configuration, diff --git a/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs index d19c566b76..b1db811079 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs @@ -8,7 +8,7 @@ using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.Extensions.CommandLineUtils; -using Microsoft.VisualStudio.LanguageServices.Razor; +using Microsoft.VisualStudio.LanguageServices.Razor.Serialization; using Newtonsoft.Json; namespace Microsoft.AspNetCore.Razor.Tools @@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.Razor.Tools } var version = RazorLanguageVersion.Parse(Version.Value()); - var configuration = new RazorConfiguration(version, Configuration.Value(), extensions); + var configuration = RazorConfiguration.Create(version, Configuration.Value(), extensions); var result = ExecuteCore( configuration: configuration, @@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Razor.Tools GetVirtualRazorProjectSystem(inputItems), RazorProjectFileSystem.Create(projectDirectory), }); - + var engine = RazorProjectEngine.Create(configuration, compositeFileSystem, b => { b.Features.Add(new StaticTagHelperFeature() { TagHelpers = tagHelpers, }); diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj b/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj index 6aae7e8d50..e67f36e02f 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj +++ b/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj @@ -13,12 +13,12 @@ - - Shared\RazorDiagnosticJsonConverter.cs - - + Shared\TagHelperDescriptorJsonConverter.cs + + Shared\RazorDiagnosticJsonConverter.cs + 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/FallbackProjectEngineFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/FallbackProjectEngineFactory.cs new file mode 100644 index 0000000000..855acedde2 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/FallbackProjectEngineFactory.cs @@ -0,0 +1,37 @@ +// 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.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor +{ + [Export(typeof(IFallbackProjectEngineFactory))] + internal class FallbackProjectEngineFactory : IFallbackProjectEngineFactory + { + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + if (fileSystem == null) + { + throw new ArgumentNullException(nameof(fileSystem)); + } + + // This is a very basic implementation that will provide reasonable support without crashing. + // If the user falls into this situation, ideally they can realize that something is wrong and take + // action. + // + // This has no support for: + // - Tag Helpers + // - Imports + // - Default Imports + // - and will have a very limited set of directives + return RazorProjectEngine.Create(configuration, fileSystem, configure); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/IFallbackProjectEngineFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/IFallbackProjectEngineFactory.cs new file mode 100644 index 0000000000..ccdc1b80c1 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/IFallbackProjectEngineFactory.cs @@ -0,0 +1,10 @@ +// 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 +{ + // Used to create the 'fallback' project engine when we don't have a custom implementation. + internal interface IFallbackProjectEngineFactory : IProjectEngineFactory + { + } +} 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 4edd7817c1..b8646429c6 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs @@ -18,33 +18,62 @@ 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; ComputedVersion = other.ComputedVersion; - Configuration = other.Configuration; + + FilePath = other.FilePath; TagHelpers = other.TagHelpers; + HostProject = hostProject; + 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)); + } + + ComputedVersion = other.ComputedVersion; + + FilePath = other.FilePath; + TagHelpers = other.TagHelpers; + HostProject = other.HostProject; + WorkspaceProject = workspaceProject; + + Version = other.Version.GetNewerVersion(); } private DefaultProjectSnapshot(ProjectSnapshotUpdateContext update, DefaultProjectSnapshot other) @@ -59,16 +88,28 @@ 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; TagHelpers = update.TagHelpers ?? Array.Empty(); + 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 override HostProject HostProject { get; } + + public override bool IsInitialized => WorkspaceProject != null; + + public override VersionStamp Version { get; } + + public override Project WorkspaceProject { get; } public override IReadOnlyList TagHelpers { get; } = Array.Empty(); @@ -77,19 +118,40 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // 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) { @@ -99,7 +161,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return new DefaultProjectSnapshot(update, this); } - public bool HasConfigurationChanged(ProjectSnapshot original) + public bool HasConfigurationChanged(DefaultProjectSnapshot original) { if (original == null) { @@ -119,4 +181,4 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return !Enumerable.SequenceEqual(TagHelpers, original.TagHelpers); } } -} +} \ 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 3534e3789c..ff6584fde5 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs @@ -7,6 +7,22 @@ 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 +33,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 +73,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 +87,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,25 +101,203 @@ 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()); + } + + if (!object.Equals(snapshot.ComputedVersion, original.ComputedVersion)) + { + NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.TagHelpersChanged)); + } + } + } + + public override void HostProjectAdded(HostProject hostProject) + { + if (hostProject == null) + { + throw new ArgumentNullException(nameof(hostProject)); + } + + _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)) + { + 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()); } - // Now we need to know if the changes that we applied are significant. If that's the case then - // we need to notify listeners. - if (snapshot.HasConfigurationChanged(original)) + // 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 HostProjectBuildComplete(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; + + // Notify the background worker so it can trigger tag helper discovery. + NotifyBackgroundWorker(snapshot.CreateUpdateContext()); + } + } + + public override void WorkspaceProjectAdded(Project workspaceProject) + { + if (workspaceProject == null) + { + throw new ArgumentNullException(nameof(workspaceProject)); + } + + _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.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()); + } if (snapshot.HaveTagHelpersChanged(original)) { @@ -161,58 +306,136 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } } - public override void ProjectRemoved(Project underlyingProject) + public override void WorkspaceProjectRemoved(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 snapshot)) - { - _projects.Remove(underlyingProject.Id); - // We need to notify listeners about every project removal. - NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Removed)); + _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 ProjectBuildComplete(Project underlyingProject) + public override void ReportError(Exception exception) { - if (underlyingProject == null) + if (exception == null) { - throw new ArgumentNullException(nameof(underlyingProject)); + throw new ArgumentNullException(nameof(exception)); } - 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; - - // Notify the background worker so it can trigger tag helper discovery. - NotifyBackgroundWorker(underlyingProject); - } + _errorReporter.ReportError(exception); } - public override void ProjectsCleared() + public override void ReportError(Exception exception, ProjectSnapshot project) { - foreach (var kvp in _projects.ToArray()) + if (exception == 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(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 @@ -226,15 +449,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 5a9a3900bb..1b7480f940 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs @@ -9,32 +9,22 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal class DefaultProjectSnapshotWorker : ProjectSnapshotWorker { - private readonly ProjectExtensibilityConfigurationFactory _configurationFactory; private readonly ForegroundDispatcher _foregroundDispatcher; private readonly TagHelperResolver _tagHelperResolver; - public DefaultProjectSnapshotWorker( - ForegroundDispatcher foregroundDispatcher, - ProjectExtensibilityConfigurationFactory configurationFactory, - TagHelperResolver tagHelperResolver) + public DefaultProjectSnapshotWorker(ForegroundDispatcher foregroundDispatcher, TagHelperResolver tagHelperResolver) { if (foregroundDispatcher == null) { throw new ArgumentNullException(nameof(foregroundDispatcher)); } - if (configurationFactory == null) - { - throw new ArgumentNullException(nameof(configurationFactory)); - } - if (tagHelperResolver == null) { throw new ArgumentNullException(nameof(tagHelperResolver)); } _foregroundDispatcher = foregroundDispatcher; - _configurationFactory = configurationFactory; _tagHelperResolver = tagHelperResolver; } @@ -54,16 +44,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return ProjectUpdatesCoreAsync(update); } + protected virtual void OnProcessingUpdate() + { + } + private async Task ProjectUpdatesCoreAsync(object state) { var update = (ProjectSnapshotUpdateContext)state; - // We'll have more things to process here, but for now we're just hardcoding the configuration. + OnProcessingUpdate(); - var configuration = await _configurationFactory.GetConfigurationAsync(update.UnderlyingProject); - update.Configuration = configuration; - - var result = await _tagHelperResolver.GetTagHelpersAsync(update.UnderlyingProject, CancellationToken.None); + var snapshot = new DefaultProjectSnapshot(update.HostProject, update.WorkspaceProject, update.Version); + var result = await _tagHelperResolver.GetTagHelpersAsync(snapshot, CancellationToken.None); update.TagHelpers = result.Descriptors; } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs index ca1946c19e..bd36bf361d 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs @@ -26,10 +26,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - return new DefaultProjectSnapshotWorker( - _foregroundDispatcher, - languageServices.GetRequiredService(), - languageServices.GetRequiredService()); + return new DefaultProjectSnapshotWorker(_foregroundDispatcher, languageServices.GetRequiredService()); } } } 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 5976b72501..538b3cb9fa 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs @@ -9,10 +9,18 @@ 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 IReadOnlyList TagHelpers { get; } + + public abstract VersionStamp Version { get; } + + public abstract Project WorkspaceProject { get; } + + public abstract HostProject HostProject { 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..026b27956f 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs @@ -9,20 +9,28 @@ 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 HostProjectBuildComplete(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, Project project); + public abstract void ReportError(Exception exception, ProjectSnapshot 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..d6299717ca 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 (FilePathComparer.Instance.Equals(filePath, project.FilePath)) { return project; } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs index dec827bdd8..cddb3b08c1 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs @@ -9,20 +9,37 @@ 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 IReadOnlyList TagHelpers { get; set; } + + 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.Workspaces/Properties/AssemblyInfo.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs index a73900fa42..d6cf7bdc7e 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.CompilerServices; - +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Performance, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Workspaces.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Remote.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs index 0388de4671..4defb21947 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs @@ -4,11 +4,20 @@ using System; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.CodeAnalysis.Razor { internal abstract class RazorProjectEngineFactoryService : ILanguageService { - public abstract RazorProjectEngine Create(string projectPath, Action configure); + public abstract IProjectEngineFactory FindFactory(ProjectSnapshot project); + + public abstract IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project); + + public abstract RazorProjectEngine Create(ProjectSnapshot project, Action configure); + + public abstract RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure); + + public abstract RazorProjectEngine Create(string directoryPath, Action configure); } } \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.cs index 21b553cde8..0e8c7b19ef 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.cs @@ -1,6 +1,7 @@ // 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; @@ -8,6 +9,8 @@ namespace Microsoft.CodeAnalysis.Razor { public sealed class TagHelperResolutionResult { + internal static TagHelperResolutionResult Empty = new TagHelperResolutionResult(Array.Empty(), Array.Empty()); + public TagHelperResolutionResult(IReadOnlyList descriptors, IReadOnlyList diagnostics) { Descriptors = descriptors; diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs index d79ed4fab2..b369056f37 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs @@ -1,14 +1,57 @@ // 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.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.CodeAnalysis.Razor { internal abstract class TagHelperResolver : ILanguageService { - public abstract Task GetTagHelpersAsync(Project project, CancellationToken cancellationToken); + public abstract Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default); + + protected virtual async Task GetTagHelpersAsync(ProjectSnapshot project, RazorProjectEngine engine) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (engine == null) + { + throw new ArgumentNullException(nameof(engine)); + } + + if (project.WorkspaceProject == null) + { + return TagHelperResolutionResult.Empty; + } + + var providers = engine.Engine.Features.OfType().ToArray(); + if (providers.Length == 0) + { + return TagHelperResolutionResult.Empty; + } + + var results = new List(); + var context = TagHelperDescriptorProviderContext.Create(results); + + var compilation = await project.WorkspaceProject.GetCompilationAsync().ConfigureAwait(false); + context.SetCompilation(compilation); + + for (var i = 0; i < providers.Length; i++) + { + var provider = providers[i]; + provider.Execute(context); + } + + return new TagHelperResolutionResult(results, Array.Empty()); + } } } 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.CodeAnalysis.Remote.Razor/DefaultTagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/DefaultTagHelperResolver.cs deleted file mode 100644 index 4aca5c8f7e..0000000000 --- a/src/Microsoft.CodeAnalysis.Remote.Razor/DefaultTagHelperResolver.cs +++ /dev/null @@ -1,54 +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.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Razor; - -namespace Microsoft.CodeAnalysis.Remote.Razor -{ - internal class DefaultTagHelperResolver : TagHelperResolver - { - public DefaultTagHelperResolver(bool designTime) - { - DesignTime = designTime; - } - - public bool DesignTime { get; } - - private TagHelperResolutionResult GetTagHelpers(Compilation compilation) - { - var descriptors = new List(); - - var providers = new ITagHelperDescriptorProvider[] - { - new DefaultTagHelperDescriptorProvider() { DesignTime = DesignTime, }, - new ViewComponentTagHelperDescriptorProvider(), - }; - - var results = new List(); - var context = TagHelperDescriptorProviderContext.Create(results); - context.SetCompilation(compilation); - - for (var i = 0; i < providers.Length; i++) - { - var provider = providers[i]; - provider.Execute(context); - } - - var diagnostics = new List(); - var resolutionResult = new TagHelperResolutionResult(results, diagnostics); - - return resolutionResult; - } - - public override async Task GetTagHelpersAsync(Project project, CancellationToken cancellationToken) - { - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - return GetTagHelpers(compilation); - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj b/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj index 7cf58777ef..369dd82020 100644 --- a/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj @@ -7,12 +7,13 @@ - + + Serialization\%(FileName)%(Extension) + - diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs index 930abdee3a..f0bf9bdaf8 100644 --- a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs @@ -10,34 +10,22 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor; -using Microsoft.VisualStudio.LanguageServices.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.CodeAnalysis.Remote.Razor { - internal class RazorLanguageService : ServiceHubServiceBase + internal class RazorLanguageService : RazorServiceBase { public RazorLanguageService(Stream stream, IServiceProvider serviceProvider) - : base(serviceProvider, stream) + : base(stream, serviceProvider) { - Rpc.JsonSerializer.Converters.Add(new RazorDiagnosticJsonConverter()); - - // Due to this issue - https://github.com/dotnet/roslyn/issues/16900#issuecomment-277378950 - // We need to manually start the RPC connection. Otherwise we'd be opting ourselves into - // race condition prone call paths. - Rpc.StartListening(); } - public async Task GetTagHelpersAsync(Guid projectIdBytes, string projectDebugName, CancellationToken cancellationToken = default(CancellationToken)) + public async Task GetTagHelpersAsync(ProjectSnapshotHandle projectHandle, string factoryTypeName, CancellationToken cancellationToken = default) { - var projectId = ProjectId.CreateFromSerialized(projectIdBytes, projectDebugName); + var project = await GetProjectSnapshotAsync(projectHandle, cancellationToken).ConfigureAwait(false); - var solution = await GetSolutionAsync(cancellationToken).ConfigureAwait(false); - var project = solution.GetProject(projectId); - - var resolver = new DefaultTagHelperResolver(designTime: true); - var result = await resolver.GetTagHelpersAsync(project, cancellationToken).ConfigureAwait(false); - - return result; + return await RazorServices.TagHelperResolver.GetTagHelpersAsync(project, factoryTypeName, cancellationToken); } public Task> GetDirectivesAsync(Guid projectIdBytes, string projectDebugName, CancellationToken cancellationToken = default(CancellationToken)) diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs new file mode 100644 index 0000000000..8fdbdd81da --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs @@ -0,0 +1,74 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Remote.Razor +{ + internal abstract class RazorServiceBase : ServiceHubServiceBase + { + public RazorServiceBase(Stream stream, IServiceProvider serviceProvider) + : base(serviceProvider, stream) + { + RazorServices = new RazorServices(); + + Rpc.JsonSerializer.Converters.RegisterRazorConverters(); + + // Due to this issue - https://github.com/dotnet/roslyn/issues/16900#issuecomment-277378950 + // We need to manually start the RPC connection. Otherwise we'd be opting ourselves into + // race condition prone call paths. + Rpc.StartListening(); + } + + protected RazorServices RazorServices { get; } + + protected virtual async Task GetProjectSnapshotAsync(ProjectSnapshotHandle projectHandle, CancellationToken cancellationToken) + { + if (projectHandle == null) + { + throw new ArgumentNullException(nameof(projectHandle)); + } + + var solution = await GetSolutionAsync(cancellationToken).ConfigureAwait(false); + var workspaceProject = solution.GetProject(projectHandle.WorkspaceProjectId); + + return new SerializedProjectSnapshot(projectHandle.FilePath, projectHandle.Configuration, workspaceProject); + } + + private class SerializedProjectSnapshot : ProjectSnapshot + { + public SerializedProjectSnapshot(string filePath, RazorConfiguration configuration, Project workspaceProject) + { + FilePath = filePath; + Configuration = configuration; + HostProject = new HostProject(filePath, configuration); + WorkspaceProject = workspaceProject; + TagHelpers = Array.Empty(); + + IsInitialized = true; + Version = VersionStamp.Default; + } + + public override RazorConfiguration Configuration { get; } + + public override string FilePath { get; } + + public override bool IsInitialized { get; } + + public override VersionStamp Version { get; } + + public override Project WorkspaceProject { get; } + + public override HostProject HostProject { get; } + + public override IReadOnlyList TagHelpers { get; } + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServices.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServices.cs new file mode 100644 index 0000000000..d3a60a400c --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServices.cs @@ -0,0 +1,22 @@ +// 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 +{ + // Provides access to Razor language and workspace services that are avialable in the OOP host. + // + // Since we don't have access to the workspace we only have access to some specific things + // that we can construct directly. + internal class RazorServices + { + public RazorServices() + { + FallbackProjectEngineFactory = new FallbackProjectEngineFactory(); + TagHelperResolver = new RemoteTagHelperResolver(FallbackProjectEngineFactory); + } + + public IFallbackProjectEngineFactory FallbackProjectEngineFactory { get; } + + public RemoteTagHelperResolver TagHelperResolver { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteTagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteTagHelperResolver.cs new file mode 100644 index 0000000000..5c8697db7f --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteTagHelperResolver.cs @@ -0,0 +1,84 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal class RemoteTagHelperResolver : TagHelperResolver + { + private readonly static RazorConfiguration DefaultConfiguration = FallbackRazorConfiguration.MVC_2_0; + + private readonly IFallbackProjectEngineFactory _fallbackFactory; + + public RemoteTagHelperResolver(IFallbackProjectEngineFactory fallbackFactory) + { + if (fallbackFactory == null) + { + throw new ArgumentNullException(nameof(fallbackFactory)); + } + + _fallbackFactory = fallbackFactory; + } + + public override Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task GetTagHelpersAsync(ProjectSnapshot project, string factoryTypeName, CancellationToken cancellationToken = default) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (project.Configuration == null || project.WorkspaceProject == null) + { + return Task.FromResult(TagHelperResolutionResult.Empty); + } + + var engine = CreateProjectEngine(project, factoryTypeName); + return GetTagHelpersAsync(project, engine); + } + + internal RazorProjectEngine CreateProjectEngine(ProjectSnapshot project, string factoryTypeName) + { + // This section is really similar to the code DefaultProjectEngineFactoryService + // but with a few differences that are significant in the remote scenario + // + // Most notably, we are going to find the Tag Helpers using a compilation, and we have + // no editor settings. + Action configure = (b) => + { + b.Features.Add(new DefaultTagHelperDescriptorProvider() { DesignTime = true }); + }; + + // The default configuration currently matches MVC-2.0. Beyond MVC-2.0 we added SDK support for + // properly detecting project versions, so that's a good version to assume when we can't find a + // configuration. + var configuration = project?.Configuration ?? DefaultConfiguration; + + // If there's no factory to handle the configuration then fall back to a very basic configuration. + // + // This will stop a crash from happening in this case (misconfigured project), but will still make + // it obvious to the user that something is wrong. + var factory = CreateFactory(configuration, factoryTypeName) ?? _fallbackFactory; + return factory.Create(configuration, RazorProjectFileSystem.Empty, configure); + } + + private IProjectEngineFactory CreateFactory(RazorConfiguration configuration, string factoryTypeName) + { + if (factoryTypeName == null) + { + return null; + } + + return (IProjectEngineFactory)Activator.CreateInstance(Type.GetType(factoryTypeName, throwOnError: true)); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs index 5d1373729c..a2c64907f7 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs @@ -7,85 +7,177 @@ 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; -using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions; 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; + private readonly Workspace _workspace; + private readonly IFallbackProjectEngineFactory _defaultFactory; + private readonly Lazy[] _customFactories; + private ProjectSnapshotManager _projectManager; - public DefaultProjectEngineFactoryService(ProjectSnapshotManager projectManager) + public DefaultProjectEngineFactoryService( + Workspace workspace, + IFallbackProjectEngineFactory defaultFactory, + Lazy[] customFactories) + { + if (workspace == null) + { + throw new ArgumentNullException(nameof(workspace)); + } + + if (defaultFactory == null) + { + throw new ArgumentNullException(nameof(defaultFactory)); + } + + if (customFactories == null) + { + throw new ArgumentNullException(nameof(customFactories)); + } + + _workspace = workspace; + _defaultFactory = defaultFactory; + _customFactories = customFactories; + } + + // Internal for testing + internal DefaultProjectEngineFactoryService( + ProjectSnapshotManager projectManager, + IFallbackProjectEngineFactory defaultFactory, + Lazy[] customFactories) { if (projectManager == null) { throw new ArgumentNullException(nameof(projectManager)); } + if (defaultFactory == null) + { + throw new ArgumentNullException(nameof(defaultFactory)); + } + + if (customFactories == null) + { + throw new ArgumentNullException(nameof(customFactories)); + } + _projectManager = projectManager; + _defaultFactory = defaultFactory; + _customFactories = customFactories; } - public override RazorProjectEngine Create(string projectPath, Action configure) + public override IProjectEngineFactory FindFactory(ProjectSnapshot project) { - if (projectPath == null) + if (project == null) { - throw new ArgumentNullException(nameof(projectPath)); + throw new ArgumentNullException(nameof(project)); } - // 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 fileSystem = RazorProjectFileSystem.Create(projectPath); - - RazorProjectEngine projectEngine; - if (razorLanguageVersion.Major == 1) - { - projectEngine = RazorProjectEngine.Create(razorConfiguration, fileSystem, b => - { - configure?.Invoke(b); - - Mvc1_X.RazorExtensions.Register(b); - - if (configuration.MvcAssembly.Identity.Version.Minor >= 1) - { - Mvc1_X.RazorExtensions.RegisterViewComponentTagHelpers(b); - } - }); - } - else - { - projectEngine = RazorProjectEngine.Create(razorConfiguration, fileSystem, b => - { - configure?.Invoke(b); - - MvcLatest.RazorExtensions.Register(b); - }); - } - - return projectEngine; + return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: false); } - private ProjectSnapshot FindProject(string directory) + public override IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: true); + } + + public override RazorProjectEngine Create(ProjectSnapshot project, Action configure) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + return CreateCore(project, RazorProjectFileSystem.Create(Path.GetDirectoryName(project.FilePath)), configure); + } + + public override RazorProjectEngine Create(string directoryPath, Action configure) + { + if (directoryPath == null) + { + throw new ArgumentNullException(nameof(directoryPath)); + } + + var project = FindProjectByDirectory(directoryPath); + return CreateCore(project, RazorProjectFileSystem.Create(directoryPath), configure); + } + + public override RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (fileSystem == null) + { + throw new ArgumentNullException(nameof(fileSystem)); + } + + return CreateCore(project, fileSystem, configure); + } + + private RazorProjectEngine CreateCore(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure) + { + // When we're running in the editor, the editor provides a configure delegate that will include + // the editor settings and tag helpers. + // + // This service is only used in process in Visual Studio, and any other callers should provide these + // things also. + configure = configure ?? ((b) => { }); + + // The default configuration currently matches MVC-2.0. Beyond MVC-2.0 we added SDK support for + // properly detecting project versions, so that's a good version to assume when we can't find a + // configuration. + var configuration = project?.Configuration ?? DefaultConfiguration; + + // If there's no factory to handle the configuration then fall back to a very basic configuration. + // + // This will stop a crash from happening in this case (misconfigured project), but will still make + // it obvious to the user that something is wrong. + var factory = SelectFactory(configuration) ?? _defaultFactory; + return factory.Create(configuration, fileSystem, configure); + } + + private IProjectEngineFactory SelectFactory(RazorConfiguration configuration, bool requireSerializable = false) + { + for (var i = 0; i < _customFactories.Length; i++) + { + var factory = _customFactories[i]; + if (string.Equals(configuration.ConfigurationName, factory.Metadata.ConfigurationName)) + { + return requireSerializable && !factory.Metadata.SupportsSerialization ? null : factory.Value; + } + } + + return null; + } + + private ProjectSnapshot FindProjectByDirectory(string directory) { directory = NormalizeDirectoryPath(directory); + if (_projectManager == null) + { + _projectManager = _workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); + } + var projects = _projectManager.Projects; for (var i = 0; i < projects.Count; i++) { var project = projects[i]; - if (project.UnderlyingProject.FilePath != null) + if (project.FilePath != null) { - if (string.Equals(directory, NormalizeDirectoryPath(Path.GetDirectoryName(project.UnderlyingProject.FilePath)), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(directory, NormalizeDirectoryPath(Path.GetDirectoryName(project.FilePath)), StringComparison.OrdinalIgnoreCase)) { return project; } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs index babed97a5c..7f22134479 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs @@ -1,6 +1,10 @@ // 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.Composition; +using System.Linq; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Razor; @@ -11,9 +15,34 @@ namespace Microsoft.VisualStudio.Editor.Razor [ExportLanguageServiceFactory(typeof(RazorProjectEngineFactoryService), RazorLanguage.Name, ServiceLayer.Default)] internal class DefaultProjectEngineFactoryServiceFactory : ILanguageServiceFactory { + private readonly Lazy[] _customFactories; + private readonly IFallbackProjectEngineFactory _fallbackFactory; + + [ImportingConstructor] + public DefaultProjectEngineFactoryServiceFactory( + IFallbackProjectEngineFactory fallbackFactory, + [ImportMany] IEnumerable> customFactories) + { + if (fallbackFactory == null) + { + throw new ArgumentNullException(nameof(fallbackFactory)); + } + + if (customFactories == null) + { + throw new ArgumentNullException(nameof(customFactories)); + } + + _fallbackFactory = fallbackFactory; + _customFactories = customFactories.ToArray(); + } + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - return new DefaultProjectEngineFactoryService(languageServices.GetRequiredService()); + return new DefaultProjectEngineFactoryService( + languageServices.WorkspaceServices.Workspace, + _fallbackFactory, + _customFactories); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs index ab49d1e27d..35b5c26567 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs @@ -2,59 +2,45 @@ // 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.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.VisualStudio.Editor.Razor { internal class DefaultTagHelperResolver : TagHelperResolver { - // Hack for testability. The view component visitor will normally just no op if we're not referencing - // an appropriate version of MVC. - internal bool ForceEnableViewComponentDiscovery { get; set; } + private readonly RazorProjectEngineFactoryService _engineFactory; - public override async Task GetTagHelpersAsync(Project project, CancellationToken cancellationToken) + public DefaultTagHelperResolver(RazorProjectEngineFactoryService engineFactory) + { + if (engineFactory == null) + { + throw new ArgumentNullException(nameof(engineFactory)); + } + + _engineFactory = engineFactory; + } + + public override Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) { if (project == null) { throw new ArgumentNullException(nameof(project)); } - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var result = GetTagHelpers(compilation); - return result; - } - - // Internal for testing - internal TagHelperResolutionResult GetTagHelpers(Compilation compilation) - { - var descriptors = new List(); - - var providers = new ITagHelperDescriptorProvider[] + if (project.Configuration == null || project.WorkspaceProject == null) { - new DefaultTagHelperDescriptorProvider() { DesignTime = true, }, - new ViewComponentTagHelperDescriptorProvider() { ForceEnabled = ForceEnableViewComponentDiscovery }, - }; - - var results = new List(); - var context = TagHelperDescriptorProviderContext.Create(results); - context.SetCompilation(compilation); - - for (var i = 0; i < providers.Length; i++) - { - var provider = providers[i]; - provider.Execute(context); + return Task.FromResult(TagHelperResolutionResult.Empty); } - var diagnostics = new List(); - var resolutionResult = new TagHelperResolutionResult(results, diagnostics); - - return resolutionResult; + var engine = _engineFactory.Create(project, RazorProjectFileSystem.Empty, b => + { + b.Features.Add(new DefaultTagHelperDescriptorProvider() { DesignTime = true, }); + }); + return GetTagHelpersAsync(project, engine); } } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs index 50e2e96179..1a238ad473 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs @@ -14,7 +14,7 @@ namespace Microsoft.VisualStudio.Editor.Razor { public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - return new DefaultTagHelperResolver(); + return new DefaultTagHelperResolver(languageServices.GetRequiredService()); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs index f1b8593f6f..c6e6937b18 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs @@ -91,7 +91,7 @@ namespace Microsoft.VisualStudio.Editor.Razor _textViews = new List(); } - internal override ProjectExtensibilityConfiguration Configuration => _project?.Configuration; + public override RazorConfiguration Configuration => _project?.Configuration; public override EditorSettings EditorSettings => _workspaceEditorSettings.Current; @@ -99,7 +99,7 @@ namespace Microsoft.VisualStudio.Editor.Razor 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; @@ -196,7 +196,7 @@ namespace Microsoft.VisualStudio.Editor.Razor internal 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/LegacyProjectEngineFactory_1_0.cs b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_1_0.cs new file mode 100644 index 0000000000..9b3b92eb89 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_1_0.cs @@ -0,0 +1,32 @@ +// 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.Reflection; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + [ExportCustomProjectEngineFactory("MVC-1.0", SupportsSerialization = true)] + internal class LegacyProjectEngineFactory_1_0 : IProjectEngineFactory + { + private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X"; + + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + // Rewrite the assembly name into a full name just like this one, but with the name of the MVC design time assembly. + var assemblyName = new AssemblyName(typeof(LegacyProjectEngineFactory_1_0).Assembly.FullName); + assemblyName.Name = AssemblyName; + + var extension = new AssemblyExtension(configuration.ConfigurationName, Assembly.Load(assemblyName)); + var initializer = extension.CreateInitializer(); + + return RazorProjectEngine.Create(configuration, fileSystem, b => + { + initializer.Initialize(b); + configure?.Invoke(b); + }); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_1_1.cs b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_1_1.cs new file mode 100644 index 0000000000..d01e6999a8 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_1_1.cs @@ -0,0 +1,32 @@ +// 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.Reflection; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + [ExportCustomProjectEngineFactory("MVC-1.1", SupportsSerialization = true)] + internal class LegacyProjectEngineFactory_1_1 : IProjectEngineFactory + { + private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X"; + + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + // Rewrite the assembly name into a full name just like this one, but with the name of the MVC design time assembly. + var assemblyName = new AssemblyName(typeof(LegacyProjectEngineFactory_1_1).Assembly.FullName); + assemblyName.Name = AssemblyName; + + var extension = new AssemblyExtension(configuration.ConfigurationName, Assembly.Load(assemblyName)); + var initializer = extension.CreateInitializer(); + + return RazorProjectEngine.Create(configuration, fileSystem, b => + { + initializer.Initialize(b); + configure?.Invoke(b); + }); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_0.cs b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_0.cs new file mode 100644 index 0000000000..ebf9e144f5 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_0.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 System.Reflection; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + [ExportCustomProjectEngineFactory("MVC-2.0", SupportsSerialization = true)] + internal class LegacyProjectEngineFactory_2_0 : IProjectEngineFactory + { + private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions"; + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + // Rewrite the assembly name into a full name just like this one, but with the name of the MVC design time assembly. + var assemblyName = new AssemblyName(typeof(LegacyProjectEngineFactory_2_0).Assembly.FullName); + assemblyName.Name = AssemblyName; + + var extension = new AssemblyExtension(configuration.ConfigurationName, Assembly.Load(assemblyName)); + var initializer = extension.CreateInitializer(); + + return RazorProjectEngine.Create(configuration, fileSystem, b => + { + initializer.Initialize(b); + configure?.Invoke(b); + }); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_1.cs b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_1.cs new file mode 100644 index 0000000000..df8d04667c --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_1.cs @@ -0,0 +1,33 @@ +// 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.Reflection; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + // Currently we provide a fixed configuration for 2.1, but this is a point-in-time issue. We plan + // to make the 2.1 configuration more flexible and less hardcoded. + [ExportCustomProjectEngineFactory("MVC-2.1", SupportsSerialization = true)] + internal class LegacyProjectEngineFactory_2_1 : IProjectEngineFactory + { + private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions"; + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + // Rewrite the assembly name into a full name just like this one, but with the name of the MVC design time assembly. + var assemblyName = new AssemblyName(typeof(LegacyProjectEngineFactory_2_1).Assembly.FullName); + assemblyName.Name = AssemblyName; + + var extension = new AssemblyExtension(configuration.ConfigurationName, Assembly.Load(assemblyName)); + var initializer = extension.CreateInitializer(); + + return RazorProjectEngine.Create(configuration, fileSystem, b => + { + initializer.Initialize(b); + configure?.Invoke(b); + }); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj b/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj index 0c367a461e..aa50d39965 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj +++ b/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj @@ -14,8 +14,6 @@ - - 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/OOPTagHelperResolver.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs index 857cce5557..c1e2ae56c7 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Editor.Razor; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -15,53 +16,67 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor internal class OOPTagHelperResolver : TagHelperResolver { private readonly DefaultTagHelperResolver _defaultResolver; + private readonly RazorProjectEngineFactoryService _engineFactory; + private readonly ErrorReporter _errorReporter; private readonly Workspace _workspace; - public OOPTagHelperResolver(Workspace workspace) + public OOPTagHelperResolver(RazorProjectEngineFactoryService engineFactory, ErrorReporter errorReporter, Workspace workspace) { + if (engineFactory == null) + { + throw new ArgumentNullException(nameof(engineFactory)); + } + + if (errorReporter == null) + { + throw new ArgumentNullException(nameof(errorReporter)); + } + if (workspace == null) { throw new ArgumentNullException(nameof(workspace)); } + _engineFactory = engineFactory; + _errorReporter = errorReporter; _workspace = workspace; - _defaultResolver = new DefaultTagHelperResolver(); + + _defaultResolver = new DefaultTagHelperResolver(_engineFactory); } - public override async Task GetTagHelpersAsync(Project project, CancellationToken cancellationToken) + public override async Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) { if (project == null) { throw new ArgumentNullException(nameof(project)); } + if (project.Configuration == null || project.WorkspaceProject == null) + { + return TagHelperResolutionResult.Empty; + } + + // Not every custom factory supports the OOP host. Our priority system should work like this: + // + // 1. Use custom factory out of process + // 2. Use custom factory in process + // 3. Use fallback factory in process + // + // Calling into RazorTemplateEngineFactoryService.Create will accomplish #2 and #3 in one step. + var factory = _engineFactory.FindSerializableFactory(project); + try { TagHelperResolutionResult result = null; - - // We're being defensive here because the OOP host can return null for the client/session/operation - // when it's disconnected (user stops the process). - var client = await RazorLanguageServiceClientFactory.CreateAsync(_workspace, cancellationToken); - if (client != null) + if (factory != null) { - using (var session = await client.CreateSessionAsync(project.Solution)) - { - if (session != null) - { - var jsonObject = await session.InvokeAsync( - "GetTagHelpersAsync", - new object[] { project.Id.Id, "Foo", }, - cancellationToken).ConfigureAwait(false); - - result = GetTagHelperResolutionResult(jsonObject); - } - } + result = await ResolveTagHelpersOutOfProcessAsync(factory, project); } if (result == null) { // Was unable to get tag helpers OOP, fallback to default behavior. - result = await _defaultResolver.GetTagHelpersAsync(project, cancellationToken); + result = await ResolveTagHelpersInProcessAsync(project); } return result; @@ -76,11 +91,61 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor } } - private TagHelperResolutionResult GetTagHelperResolutionResult(JObject jsonObject) + protected virtual async Task ResolveTagHelpersOutOfProcessAsync(IProjectEngineFactory factory, ProjectSnapshot project) + { + // We're being overly defensive here because the OOP host can return null for the client/session/operation + // when it's disconnected (user stops the process). + // + // This will change in the future to an easier to consume API but for VS RTM this is what we have. + try + { + var client = await RazorLanguageServiceClientFactory.CreateAsync(_workspace, CancellationToken.None); + if (client != null) + { + using (var session = await client.CreateSessionAsync(project.WorkspaceProject.Solution)) + { + if (session != null) + { + var args = new object[] + { + Serialize(project), + factory == null ? null : factory.GetType().AssemblyQualifiedName, + }; + + var json = await session.InvokeAsync("GetTagHelpersAsync", args, CancellationToken.None).ConfigureAwait(false); + return Deserialize(json); + } + } + } + } + catch (Exception ex) + { + // We silence exceptions from the OOP host because we don't want to bring down VS for an OOP failure. + // We will retry all failures in process anyway, so if there's a real problem that isn't unique to OOP + // then it will report a crash in VS. + _errorReporter.ReportError(ex, project); + } + + return null; + } + + protected virtual Task ResolveTagHelpersInProcessAsync(ProjectSnapshot project) + { + return _defaultResolver.GetTagHelpersAsync(project); + } + + private JObject Serialize(ProjectSnapshot snapshot) { var serializer = new JsonSerializer(); - serializer.Converters.Add(TagHelperDescriptorJsonConverter.Instance); - serializer.Converters.Add(RazorDiagnosticJsonConverter.Instance); + serializer.Converters.RegisterRazorConverters(); + + return JObject.FromObject(snapshot, serializer); + } + + private TagHelperResolutionResult Deserialize(JObject jsonObject) + { + var serializer = new JsonSerializer(); + serializer.Converters.RegisterRazorConverters(); using (var reader = jsonObject.CreateReader()) { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolverFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolverFactory.cs index d3265f7778..b6b4e93d52 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolverFactory.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolverFactory.cs @@ -14,8 +14,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor { public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - var workspace = languageServices.WorkspaceServices.Workspace; - return new OOPTagHelperResolver(workspace); + return new OOPTagHelperResolver( + languageServices.GetRequiredService(), + languageServices.WorkspaceServices.GetRequiredService(), + languageServices.WorkspaceServices.Workspace); } } } \ No newline at end of file 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..122505b002 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs @@ -0,0 +1,128 @@ +// 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) + { + if (IsDisposing || IsDisposed) + { + return; + } + + await CommonServices.TasksService.LoadedProjectAsync(async () => + { + await ExecuteWithLock(async () => + { + 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); + }); + }, registerFaultHandler: true); + } + } +} \ 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..8e7fb4d493 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs @@ -0,0 +1,146 @@ +// 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 }, + linkOptions: new DataflowLinkOptions() { PropagateCompletion = true }); + } + + protected override async Task DisposeCoreAsync(bool initialized) + { + await base.DisposeCoreAsync(initialized).ConfigureAwait(false); + + if (initialized) + { + _subscription.Dispose(); + } + } + + // Internal for testing + internal async Task OnProjectChanged(IProjectVersionedValue update) + { + if (IsDisposing || IsDisposed) + { + return; + } + + await CommonServices.TasksService.LoadedProjectAsync(async () => + { + await ExecuteWithLock(async () => + { + 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); + }); + }, registerFaultHandler: true); + } + + // 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..f520a6e046 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IUnconfiguredProjectCommonServices.cs @@ -0,0 +1,32 @@ +// 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; } + + IProjectAsynchronousTasksService TasksService { get; } + + IProjectThreadingService ThreadingService { get; } + + 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.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.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.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 index b08397ffb9..cc8f28a6f1 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorProjectProperties.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorProjectProperties.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -31,4 +31,4 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules { } } -} \ No newline at end of file +} 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..d84595a1c3 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/UnconfiguredProjectCommonServices.cs @@ -0,0 +1,96 @@ +// 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( + [Import(ExportContractNames.Scopes.UnconfiguredProject)] IProjectAsynchronousTasksService tasksService, + IProjectThreadingService threadingService, + UnconfiguredProject unconfiguredProject, + IActiveConfiguredProjectSubscriptionService activeConfiguredProjectSubscription, + ActiveConfiguredProject activeConfiguredProject, + ActiveConfiguredProject activeConfiguredProjectAssemblyReferences, + ActiveConfiguredProject activeConfiguredProjectPackageReferences, + ActiveConfiguredProject activeConfiguredProjectRazorProperties) + { + if (tasksService == null) + { + throw new ArgumentNullException(nameof(tasksService)); + } + + 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)); + } + + TasksService = tasksService; + 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 IProjectAsynchronousTasksService TasksService { get; } + + public IProjectThreadingService ThreadingService { get; } + + public UnconfiguredProject UnconfiguredProject { get; } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/JsonConverterCollectionExtensions.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/JsonConverterCollectionExtensions.cs new file mode 100644 index 0000000000..1f008fd63d --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/JsonConverterCollectionExtensions.cs @@ -0,0 +1,27 @@ +// 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.VisualStudio.LanguageServices.Razor.Serialization; +using Newtonsoft.Json; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal static class JsonConverterCollectionExtensions + { + public static void RegisterRazorConverters(this JsonConverterCollection collection) + { + if (collection == null) + { + throw new ArgumentNullException(nameof(collection)); + } + + collection.Add(TagHelperDescriptorJsonConverter.Instance); + collection.Add(RazorDiagnosticJsonConverter.Instance); + collection.Add(RazorExtensionJsonConverter.Instance); + collection.Add(RazorConfigurationJsonConverter.Instance); + collection.Add(ProjectSnapshotJsonConverter.Instance); + collection.Add(ProjectSnapshotHandleJsonConverter.Instance); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandle.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandle.cs new file mode 100644 index 0000000000..d186db2ec8 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandle.cs @@ -0,0 +1,29 @@ +// 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 sealed class ProjectSnapshotHandle + { + public ProjectSnapshotHandle(string filePath, RazorConfiguration configuration, ProjectId workspaceProjectId) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + FilePath = filePath; + Configuration = configuration; + WorkspaceProjectId = workspaceProjectId; + } + + public RazorConfiguration Configuration { get; } + + public string FilePath { get; } + + public ProjectId WorkspaceProjectId { get; } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandleJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandleJsonConverter.cs new file mode 100644 index 0000000000..5b72763912 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandleJsonConverter.cs @@ -0,0 +1,73 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + internal class ProjectSnapshotHandleJsonConverter : JsonConverter + { + public static readonly ProjectSnapshotHandleJsonConverter Instance = new ProjectSnapshotHandleJsonConverter(); + + public override bool CanConvert(Type objectType) + { + return typeof(ProjectSnapshotHandle).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject) + { + return null; + } + + var obj = JObject.Load(reader); + var filePath = obj[nameof(ProjectSnapshotHandle.FilePath)].Value(); + var configuration = obj[nameof(ProjectSnapshotHandle.Configuration)].ToObject(serializer); + + var id = obj[nameof(ProjectSnapshotHandle.WorkspaceProjectId)].Value(); + var workspaceProjectId = id == null ? null : ProjectId.CreateFromSerialized(Guid.Parse(id)); + + return new ProjectSnapshotHandle(filePath, configuration, workspaceProjectId); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var handle = (ProjectSnapshotHandle)value; + + writer.WriteStartObject(); + + writer.WritePropertyName(nameof(ProjectSnapshotHandle.FilePath)); + writer.WriteValue(handle.FilePath); + + if (handle.Configuration == null) + { + writer.WritePropertyName(nameof(ProjectSnapshotHandle.Configuration)); + writer.WriteNull(); + } + else + { + writer.WritePropertyName(nameof(ProjectSnapshotHandle.Configuration)); + serializer.Serialize(writer, handle.Configuration); + } + + if (handle.WorkspaceProjectId == null) + { + writer.WritePropertyName(nameof(ProjectSnapshotHandle.WorkspaceProjectId)); + writer.WriteNull(); + } + else + { + writer.WritePropertyName(nameof(ProjectSnapshotHandle.WorkspaceProjectId)); + writer.WriteValue(handle.WorkspaceProjectId.Id); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotJsonConverter.cs new file mode 100644 index 0000000000..988d42b44d --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotJsonConverter.cs @@ -0,0 +1,40 @@ +// 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.CodeAnalysis.Razor.ProjectSystem; +using Newtonsoft.Json; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + // We can't truly serialize a snapshot because it has access to a Workspace Project\ + // + // Instead we serialize to a ProjectSnapshotHandle and then use that to re-create the snapshot + // inside the remote host. + internal class ProjectSnapshotJsonConverter : JsonConverter + { + public static readonly ProjectSnapshotJsonConverter Instance = new ProjectSnapshotJsonConverter(); + + public override bool CanRead => false; + + public override bool CanWrite => true; + + public override bool CanConvert(Type objectType) + { + return typeof(ProjectSnapshot).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var project = (ProjectSnapshot)value; + var handle = new ProjectSnapshotHandle(project.FilePath, project.Configuration, project.WorkspaceProject?.Id); + + ProjectSnapshotHandleJsonConverter.Instance.WriteJson(writer, handle, serializer); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorConfigurationJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorConfigurationJsonConverter.cs new file mode 100644 index 0000000000..26be8c52b7 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorConfigurationJsonConverter.cs @@ -0,0 +1,53 @@ +// 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 Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + internal class RazorConfigurationJsonConverter : JsonConverter + { + public static readonly RazorConfigurationJsonConverter Instance = new RazorConfigurationJsonConverter(); + + public override bool CanConvert(Type objectType) + { + return typeof(RazorConfiguration).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject) + { + return null; + } + + var obj = JObject.Load(reader); + var configurationName = obj[nameof(RazorConfiguration.ConfigurationName)].Value(); + var languageVersion = obj[nameof(RazorConfiguration.LanguageVersion)].Value(); + var extensions = obj[nameof(RazorConfiguration.Extensions)].ToObject(serializer); + + return RazorConfiguration.Create(RazorLanguageVersion.Parse(languageVersion), configurationName, extensions); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var configuration = (RazorConfiguration)value; + + writer.WriteStartObject(); + + writer.WritePropertyName(nameof(RazorConfiguration.ConfigurationName)); + writer.WriteValue(configuration.ConfigurationName); + + writer.WritePropertyName(nameof(RazorConfiguration.LanguageVersion)); + writer.WriteValue(configuration.LanguageVersion.ToString()); + + writer.WritePropertyName(nameof(RazorConfiguration.Extensions)); + serializer.Serialize(writer, configuration.Extensions); + + writer.WriteEndObject(); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorDiagnosticJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorDiagnosticJsonConverter.cs similarity index 97% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/RazorDiagnosticJsonConverter.cs rename to src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorDiagnosticJsonConverter.cs index 83873949e6..029530c471 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorDiagnosticJsonConverter.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorDiagnosticJsonConverter.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Razor.Language; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization { internal class RazorDiagnosticJsonConverter : JsonConverter { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorExtensionJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorExtensionJsonConverter.cs new file mode 100644 index 0000000000..0d84b8c939 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/RazorExtensionJsonConverter.cs @@ -0,0 +1,45 @@ +// 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 Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + internal class RazorExtensionJsonConverter : JsonConverter + { + public static readonly RazorExtensionJsonConverter Instance = new RazorExtensionJsonConverter(); + + public override bool CanConvert(Type objectType) + { + return typeof(RazorExtension).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject) + { + return null; + } + + var obj = JObject.Load(reader); + var extensionName = obj[nameof(RazorExtension.ExtensionName)].Value(); + + return new SerializedRazorExtension(extensionName); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var extension = (RazorExtension)value; + + writer.WriteStartObject(); + + writer.WritePropertyName(nameof(RazorExtension.ExtensionName)); + writer.WriteValue(extension.ExtensionName); + + writer.WriteEndObject(); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/SerializedRazorExtension.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/SerializedRazorExtension.cs new file mode 100644 index 0000000000..0f0292534f --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/SerializedRazorExtension.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.VisualStudio.LanguageServices.Razor.Serialization +{ + internal class SerializedRazorExtension : RazorExtension + { + public SerializedRazorExtension(string extensionName) + { + if (extensionName == null) + { + throw new ArgumentNullException(nameof(extensionName)); + } + + ExtensionName = extensionName; + } + + public override string ExtensionName { get; } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperDescriptorJsonConverter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/TagHelperDescriptorJsonConverter.cs similarity index 99% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperDescriptorJsonConverter.cs rename to src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/TagHelperDescriptorJsonConverter.cs index 406ea9a85f..271af9e1fd 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperDescriptorJsonConverter.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/TagHelperDescriptorJsonConverter.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Razor.Language; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization { internal class TagHelperDescriptorJsonConverter : JsonConverter { 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/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs index 91297d852b..ae66202b22 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs @@ -85,16 +85,14 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor // This gets called when the project has finished building. public int UpdateProjectCfg_Done(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, int fSuccess, int fCancel) { - var projectName = _projectService.GetProjectName(pHierProj); var projectPath = _projectService.GetProjectPath(pHierProj); // Get the corresponding roslyn project by matching the project name and the project path. - foreach (var project in _projectManager.Workspace.CurrentSolution.Projects) + foreach (var projectSnapshot in _projectManager.Projects) { - if (string.Equals(projectName, project.Name, StringComparison.Ordinal) && - string.Equals(projectPath, project.FilePath, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(projectPath, projectSnapshot.FilePath, StringComparison.OrdinalIgnoreCase)) { - _projectManager.ProjectBuildComplete(project); + _projectManager.HostProjectBuildComplete(projectSnapshot.HostProject); break; } } diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs index e874735740..14f0e7515b 100644 --- a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs @@ -96,16 +96,14 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor return; } - var projectName = _projectService.GetProjectName(projectItem); var projectPath = _projectService.GetProjectPath(projectItem); // Get the corresponding roslyn project by matching the project name and the project path. - foreach (var project in _projectManager.Workspace.CurrentSolution.Projects) + foreach (var projectSnapshot in _projectManager.Projects) { - if (string.Equals(projectName, project.Name, StringComparison.Ordinal) && - string.Equals(projectPath, project.FilePath, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(projectPath, projectSnapshot.FilePath, StringComparison.OrdinalIgnoreCase)) { - _projectManager.ProjectBuildComplete(project); + _projectManager.HostProjectBuildComplete(projectSnapshot.HostProject); break; } } diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Properties/Resources.Designer.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Properties/Resources.Designer.cs index 3e486a9d9e..9b9b6fb28f 100644 --- a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Properties/Resources.Designer.cs @@ -52,6 +52,22 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor internal static string FormatRazorLanguageServiceProjectError(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("RazorLanguageServiceProjectError"), p0); + /// + /// Error encountered from project '{0}': + /// {1} + /// + internal static string RazorLanguageServiceProjectSnapshotError + { + get => GetString("RazorLanguageServiceProjectSnapshotError"); + } + + /// + /// Error encountered from project '{0}': + /// {1} + /// + internal static string FormatRazorLanguageServiceProjectSnapshotError(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("RazorLanguageServiceProjectSnapshotError"), p0, p1); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Resources.resx b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Resources.resx index 9b004fd397..b5fa0a3947 100644 --- a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Resources.resx +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Resources.resx @@ -126,4 +126,8 @@ Razor Language Service error encountered from project '{0}'. + + Error encountered from project '{0}': +{1} + \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/VisualStudioErrorReporter.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/VisualStudioErrorReporter.cs index 5cf0a05c77..6535dfa999 100644 --- a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/VisualStudioErrorReporter.cs +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/VisualStudioErrorReporter.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using MonoDevelop.Core; namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor @@ -36,5 +37,18 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor Resources.FormatRazorLanguageServiceProjectError(project?.Name), exception); } + + public override void ReportError(Exception exception, ProjectSnapshot project) + { + if (exception == null) + { + Debug.Fail("Null exceptions should not be reported."); + return; + } + + LoggingService.LogError( + Resources.FormatRazorLanguageServiceProjectSnapshotError(project?.FilePath, exception), + exception); + } } } 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 e0d04fb0e8..796b1a4e99 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs @@ -2,12 +2,7 @@ // 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 +10,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); Assert.Equal(original.TagHelpers, snapshot.TagHelpers); @@ -37,23 +33,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) + var anotherProject = GetWorkspaceProject("Test1"); + var update = new ProjectSnapshotUpdateContext(original.FilePath, hostProject, anotherProject, original.Version) { - Configuration = Mock.Of(), TagHelpers = Array.Empty(), }; // 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); Assert.Same(update.TagHelpers, snapshot.TagHelpers); } @@ -61,12 +55,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public void HaveTagHelpersChanged_NoUpdatesToTagHelpers_ReturnsFalse() { // Arrange - var underlyingProject = GetProject("Test1"); - var original = new DefaultProjectSnapshot(underlyingProject); + var hostProject = new HostProject("Test1.csproj", RazorConfiguration.Default); + var workspaceProject = GetWorkspaceProject("Test1"); + var original = new DefaultProjectSnapshot(hostProject, workspaceProject); - var anotherProject = GetProject("Test1"); - var update = new ProjectSnapshotUpdateContext(anotherProject); - var snapshot = original.WithProjectChange(update); + var anotherProject = GetWorkspaceProject("Test1"); + var update = new ProjectSnapshotUpdateContext("Test1.csproj", hostProject, anotherProject, VersionStamp.Default); + var snapshot = original.WithComputedUpdate(update); // Act var result = snapshot.HaveTagHelpersChanged(original); @@ -79,11 +74,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public void HaveTagHelpersChanged_TagHelpersUpdated_ReturnsTrue() { // Arrange - var underlyingProject = GetProject("Test1"); - var original = new DefaultProjectSnapshot(underlyingProject); + var hostProject = new HostProject("Test1.csproj", RazorConfiguration.Default); + var workspaceProject = GetWorkspaceProject("Test1"); + var original = new DefaultProjectSnapshot(hostProject, workspaceProject); - var anotherProject = GetProject("Test1"); - var update = new ProjectSnapshotUpdateContext(anotherProject) + var anotherProject = GetWorkspaceProject("Test1"); + var update = new ProjectSnapshotUpdateContext("Test1.csproj", hostProject, anotherProject, VersionStamp.Default) { TagHelpers = new[] { @@ -91,7 +87,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem TagHelperDescriptorBuilder.Create("Two", "TestAssembly").Build(), }, }; - var snapshot = original.WithProjectChange(update); + var snapshot = original.WithComputedUpdate(update); // Act var result = snapshot.HaveTagHelpersChanged(original); @@ -100,7 +96,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem Assert.True(result); } - 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/DefaultImportDocumentManagerIntegrationTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerIntegrationTest.cs index 5494ba8a4a..7a818f8c54 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerIntegrationTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerIntegrationTest.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Moq; @@ -21,7 +23,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var testImportsPath = "C:\\path\\to\\project\\_ViewImports.cshtml"; var tracker = Mock.Of(t => t.FilePath == filePath && t.ProjectPath == projectPath); var anotherTracker = Mock.Of(t => t.FilePath == anotherFilePath && t.ProjectPath == projectPath); - var templateEngineFactoryService = GetProjectEngineFactoryService(); + var projectEngineFactoryService = GetProjectEngineFactoryService(); var fileChangeTracker = new Mock(); fileChangeTracker.Setup(f => f.FilePath).Returns(testImportsPath); var fileChangeTrackerFactory = new Mock(); @@ -36,7 +38,7 @@ namespace Microsoft.VisualStudio.Editor.Razor .Returns(Mock.Of()); var called = false; - var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, templateEngineFactoryService); + var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, projectEngineFactoryService); manager.OnSubscribed(tracker); manager.OnSubscribed(anotherTracker); manager.Changed += (sender, args) => @@ -63,7 +65,18 @@ namespace Microsoft.VisualStudio.Editor.Razor var projectManager = new Mock(); projectManager.Setup(p => p.Projects).Returns(Array.Empty()); - var service = new DefaultProjectEngineFactoryService(projectManager.Object); + var projectEngineFactory = new Mock(); + projectEngineFactory.Setup(s => s.Create(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns>( + (c, fs, b) => RazorProjectEngine.Create( + RazorConfiguration.Default, + fs, + builder => RazorExtensions.Register(builder))); + + var service = new DefaultProjectEngineFactoryService( + projectManager.Object, + projectEngineFactory.Object, + new Lazy[0]); return service; } } diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerTest.cs index c32bbd0af3..0afd8a633a 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultImportDocumentManagerTest.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Moq; @@ -18,7 +20,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var filePath = "C:\\path\\to\\project\\Views\\Home\\file.cshtml"; var projectPath = "C:\\path\\to\\project\\project.csproj"; var tracker = Mock.Of(t => t.FilePath == filePath && t.ProjectPath == projectPath); - var templateEngineFactoryService = GetTemplateEngineFactoryService(); + var projectEngineService = GetProjectEngineFactoryService(); var fileChangeTracker1 = new Mock(); fileChangeTracker1.Setup(f => f.StartListening()).Verifiable(); var fileChangeTrackerFactory = new Mock(); @@ -39,7 +41,7 @@ namespace Microsoft.VisualStudio.Editor.Razor .Returns(fileChangeTracker3.Object) .Verifiable(); - var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, templateEngineFactoryService); + var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, projectEngineService); // Act manager.OnSubscribed(tracker); @@ -58,7 +60,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var filePath = "C:\\path\\to\\project\\file.cshtml"; var projectPath = "C:\\path\\to\\project\\project.csproj"; var tracker = Mock.Of(t => t.FilePath == filePath && t.ProjectPath == projectPath); - var templateEngineFactoryService = GetTemplateEngineFactoryService(); + var projectEngineService = GetProjectEngineFactoryService(); var callCount = 0; var fileChangeTrackerFactory = new Mock(); @@ -67,7 +69,7 @@ namespace Microsoft.VisualStudio.Editor.Razor .Returns(Mock.Of()) .Callback(() => callCount++); - var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, templateEngineFactoryService); + var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, projectEngineService); manager.OnSubscribed(tracker); // Start tracking the import. var anotherFilePath = "C:\\path\\to\\project\\anotherFile.cshtml"; @@ -87,7 +89,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var filePath = "C:\\path\\to\\project\\file.cshtml"; var projectPath = "C:\\path\\to\\project\\project.csproj"; var tracker = Mock.Of(t => t.FilePath == filePath && t.ProjectPath == projectPath); - var templateEngineFactoryService = GetTemplateEngineFactoryService(); + var projectEngineService = GetProjectEngineFactoryService(); var fileChangeTracker = new Mock(); fileChangeTracker.Setup(f => f.StopListening()).Verifiable(); @@ -97,7 +99,7 @@ namespace Microsoft.VisualStudio.Editor.Razor .Returns(fileChangeTracker.Object) .Verifiable(); - var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, templateEngineFactoryService); + var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, projectEngineService); manager.OnSubscribed(tracker); // Start tracking the import. // Act @@ -115,7 +117,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var filePath = "C:\\path\\to\\project\\file.cshtml"; var projectPath = "C:\\path\\to\\project\\project.csproj"; var tracker = Mock.Of(t => t.FilePath == filePath && t.ProjectPath == projectPath); - var templateEngineFactoryService = GetTemplateEngineFactoryService(); + var projectEngineService = GetProjectEngineFactoryService(); var fileChangeTracker = new Mock(); fileChangeTracker @@ -126,7 +128,7 @@ namespace Microsoft.VisualStudio.Editor.Razor .Setup(f => f.Create(It.IsAny())) .Returns(fileChangeTracker.Object); - var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, templateEngineFactoryService); + var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, projectEngineService); manager.OnSubscribed(tracker); // Starts tracking import for the first document. var anotherFilePath = "C:\\path\\to\\project\\anotherFile.cshtml"; @@ -137,12 +139,23 @@ namespace Microsoft.VisualStudio.Editor.Razor manager.OnUnsubscribed(tracker); } - private RazorProjectEngineFactoryService GetTemplateEngineFactoryService() + private RazorProjectEngineFactoryService GetProjectEngineFactoryService() { var projectManager = new Mock(); projectManager.Setup(p => p.Projects).Returns(Array.Empty()); - var service = new DefaultProjectEngineFactoryService(projectManager.Object); + var projectEngineFactory = new Mock(); + projectEngineFactory.Setup(s => s.Create(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns>( + (c, fs, b) => RazorProjectEngine.Create( + RazorConfiguration.Default, + fs, + builder => RazorExtensions.Register(builder))); + + var service = new DefaultProjectEngineFactoryService( + projectManager.Object, + projectEngineFactory.Object, + new Lazy[0]); return service; } } diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs index a9ef8baba3..1afad18177 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectEngineFactoryServiceTest.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Reflection; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; @@ -26,30 +27,67 @@ 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); + + HostProject_For_2_1 = new HostProject( + "/TestPath/SomePath/Test.csproj", + new ProjectSystemRazorConfiguration(RazorLanguageVersion.Version_2_1, "MVC-2.1", Array.Empty())); + HostProject_For_UnknownConfiguration = new HostProject( + "/TestPath/SomePath/Test.csproj", + new ProjectSystemRazorConfiguration(RazorLanguageVersion.Version_2_1, "Blazor-0.1", Array.Empty())); + + + CustomFactories = new Lazy[] + { + new Lazy( + () => new LegacyProjectEngineFactory_1_0(), + typeof(LegacyProjectEngineFactory_1_0).GetCustomAttribute()), + new Lazy( + () => new LegacyProjectEngineFactory_1_1(), + typeof(LegacyProjectEngineFactory_1_1).GetCustomAttribute()), + new Lazy( + () => new LegacyProjectEngineFactory_2_0(), + typeof(LegacyProjectEngineFactory_2_0).GetCustomAttribute()), + new Lazy( + () => new LegacyProjectEngineFactory_2_1(), + typeof(LegacyProjectEngineFactory_2_1).GetCustomAttribute()), + }; + + FallbackFactory = new FallbackProjectEngineFactory(); } - // We don't actually look at the project, we rely on the ProjectStateManager - public Project Project { get; } + private Lazy[] CustomFactories { get; } - public Workspace Workspace { get; } + private IFallbackProjectEngineFactory FallbackFactory { get; } + + private HostProject HostProject_For_1_0 { get; } + + private HostProject HostProject_For_1_1 { get; } + + private HostProject HostProject_For_2_0 { get; } + + private HostProject HostProject_For_2_1 { get; } + + private HostProject HostProject_For_UnknownConfiguration { 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() + public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_1() { // 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_1); + projectManager.WorkspaceProjectAdded(WorkspaceProject); - var factoryService = new DefaultProjectEngineFactoryService(projectManager); + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -59,6 +97,30 @@ namespace Microsoft.VisualStudio.Editor.Razor // Assert Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); + } + + [Fact] + public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_0() + { + // Arrange + var projectManager = new TestProjectSnapshotManager(Workspace); + projectManager.HostProjectAdded(HostProject_For_2_0); + projectManager.WorkspaceProjectAdded(WorkspaceProject); + + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); + + // 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()); Assert.Single(engine.Engine.Features.OfType()); } @@ -68,17 +130,10 @@ 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); + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -88,6 +143,7 @@ namespace Microsoft.VisualStudio.Editor.Razor // Assert Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); } @@ -97,17 +153,10 @@ 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); + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -117,26 +166,41 @@ namespace Microsoft.VisualStudio.Editor.Razor // Assert Assert.Single(engine.Engine.Features.OfType()); - Assert.Single(engine.Engine.Features.OfType()); - Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); } [Fact] - public void Create_HigherMvcVersion_UsesLatest() + public void Create_UnknownProject_UsesVersion2_0() { // Arrange var projectManager = new TestProjectSnapshotManager(Workspace); - projectManager.ProjectAdded(Project); - projectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(Project) + + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); + + // Act + var engine = factoryService.Create("/TestPath/DifferentPath/", b => { - 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")))), + b.Features.Add(new MyCoolNewFeature()); }); - var factoryService = new DefaultProjectEngineFactoryService(projectManager); + // Assert + Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); + } + + [Fact] + public void Create_ForUnknownConfiguration_UsesFallbackFactory() + { + // Arrange + var projectManager = new TestProjectSnapshotManager(Workspace); + projectManager.HostProjectAdded(HostProject_For_UnknownConfiguration); + projectManager.WorkspaceProjectAdded(WorkspaceProject); + + var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); // Act var engine = factoryService.Create("/TestPath/SomePath/", b => @@ -146,49 +210,10 @@ namespace Microsoft.VisualStudio.Editor.Razor // 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() - { - // Arrange - var projectManager = new TestProjectSnapshotManager(Workspace); - - var factoryService = new DefaultProjectEngineFactoryService(projectManager); - - // Act - var engine = factoryService.Create("/TestPath/DifferentPath/", 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_MvcReferenceNotFound_UsesLatest() - { - // Arrange - var projectManager = new TestProjectSnapshotManager(Workspace); - projectManager.ProjectAdded(Project); - - var factoryService = new DefaultProjectEngineFactoryService(projectManager); - - // Act - var engine = factoryService.Create("/TestPath/DifferentPath/", 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()); + Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); } private class MyCoolNewFeature : IRazorEngineFeature diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTagHelperResolverTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTagHelperResolverTest.cs deleted file mode 100644 index 4d872b8b6f..0000000000 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTagHelperResolverTest.cs +++ /dev/null @@ -1,63 +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.Reflection; -using Microsoft.AspNetCore.Razor.TagHelpers; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Xunit; - -namespace Microsoft.VisualStudio.Editor.Razor -{ - public class DefaultTagHelperResolverTest - { - private static readonly Assembly Assembly = typeof(DefaultTagHelperResolverTest).GetTypeInfo().Assembly; - - [Fact] - public void GetTagHelpers_DiscoversViewComponentTagHelpers() - { - // Arrange - var code = @" -public class TestViewComponent -{ - public string Invoke(string foo, string bar) => null; -}"; - var syntaxTree = CSharpSyntaxTree.ParseText(code); - var compilation = TestCompilation.Create(Assembly, syntaxTree); - var tagHelperResolver = new DefaultTagHelperResolver() - { - ForceEnableViewComponentDiscovery = true - }; - - // Act - var result = tagHelperResolver.GetTagHelpers(compilation); - - // Assert - Assert.Empty(result.Diagnostics); - Assert.Equal(1, result.Descriptors.Count); - } - - [Fact] - public void GetTagHelpers_DiscoversTagHelpers() - { - // Arrange - var code = $@" -public class TestTagHelper : {typeof(TagHelper).FullName} -{{ -}}"; - var syntaxTree = CSharpSyntaxTree.ParseText(code); - var compilation = TestCompilation.Create(Assembly, syntaxTree); - var tagHelperResolver = new DefaultTagHelperResolver() - { - ForceEnableViewComponentDiscovery = true - }; - - // Act - var result = tagHelperResolver.GetTagHelpers(compilation); - - // Assert - Assert.Empty(result.Diagnostics); - Assert.Equal(1, result.Descriptors.Count); - } - } -} diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs index c37040a3a3..e0c178f772 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Editor; @@ -64,7 +65,7 @@ namespace Microsoft.VisualStudio.Editor.Razor }); var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, workspace, TextBuffer, ImportDocumentManager); - var projectSnapshot = new DefaultProjectSnapshot(project); + var projectSnapshot = new DefaultProjectSnapshot(new HostProject(project.FilePath, RazorConfiguration.Default), project); var projectChangedArgs = new ProjectChangeEventArgs(projectSnapshot, ProjectChangeKind.Changed); var called = false; @@ -92,7 +93,7 @@ namespace Microsoft.VisualStudio.Editor.Razor }); var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, workspace, TextBuffer, ImportDocumentManager); - var projectSnapshot = new DefaultProjectSnapshot(project); + var projectSnapshot = new DefaultProjectSnapshot(new HostProject(project.FilePath, RazorConfiguration.Default), project); var projectChangedArgs = new ProjectChangeEventArgs(projectSnapshot, ProjectChangeKind.TagHelpersChanged); var called = false; @@ -120,7 +121,7 @@ namespace Microsoft.VisualStudio.Editor.Razor }); var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, workspace, TextBuffer, ImportDocumentManager); - var projectSnapshot = new DefaultProjectSnapshot(project); + var projectSnapshot = new DefaultProjectSnapshot(new HostProject(project.FilePath, RazorConfiguration.Default), project); var projectChangedArgs = new ProjectChangeEventArgs(projectSnapshot, ProjectChangeKind.Changed); var called = false; diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj b/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj index bd3366db65..d7a967f2b8 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj @@ -15,6 +15,8 @@ + + diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj index d182c95d31..1f76acc35b 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj @@ -18,6 +18,8 @@ + + diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/OOPTagHelperResolverTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/OOPTagHelperResolverTest.cs new file mode 100644 index 0000000000..cb9a4e8a6b --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/OOPTagHelperResolverTest.cs @@ -0,0 +1,177 @@ +// 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.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.VisualStudio.Editor.Razor; +using Moq; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + public class OOPTagHelperResolverTest + { + public OOPTagHelperResolverTest() + { + HostProject_For_2_0 = new HostProject("Test.csproj", FallbackRazorConfiguration.MVC_2_0); + HostProject_For_NonSerializableConfiguration = new HostProject( + "Test.csproj", + new ProjectSystemRazorConfiguration(RazorLanguageVersion.Version_2_1, "Blazor-0.1", Array.Empty())); + + CustomFactories = new Lazy[] + { + new Lazy( + () => new LegacyProjectEngineFactory_2_0(), + typeof(LegacyProjectEngineFactory_2_0).GetCustomAttribute()), + + // We don't really use this factory, we just use it to ensure that the call is going to go out of process. + new Lazy( + () => new LegacyProjectEngineFactory_2_1(), + new ExportCustomProjectEngineFactoryAttribute("Blazor-0.1") { SupportsSerialization = false, }), + }; + + FallbackFactory = new FallbackProjectEngineFactory(); + + Workspace = new AdhocWorkspace(); + + var info = ProjectInfo.Create(ProjectId.CreateNewId("Test"), VersionStamp.Default, "Test", "Test", LanguageNames.CSharp, filePath: "Test.csproj"); + WorkspaceProject = Workspace.CurrentSolution.AddProject(info).GetProject(info.Id); + + ErrorReporter = new DefaultErrorReporter(); + ProjectManager = new TestProjectSnapshotManager(Workspace); + EngineFactory = new DefaultProjectEngineFactoryService(ProjectManager, FallbackFactory, CustomFactories); + } + + private ErrorReporter ErrorReporter { get; } + + private RazorProjectEngineFactoryService EngineFactory { get; } + + private Lazy[] CustomFactories { get; } + + private IFallbackProjectEngineFactory FallbackFactory { get; } + + private HostProject HostProject_For_2_0 { get; } + + private HostProject HostProject_For_NonSerializableConfiguration { get; } + + private ProjectSnapshotManagerBase ProjectManager { get; } + + private Project WorkspaceProject { get; } + + private Workspace Workspace { get; } + + [Fact] + public async Task GetTagHelpersAsync_WithNonInitializedProject_Noops() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject_For_2_0); + + var project = ProjectManager.GetProjectWithFilePath("Test.csproj"); + + var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace); + + var result = await resolver.GetTagHelpersAsync(project); + + // Assert + Assert.Same(TagHelperResolutionResult.Empty, result); + } + + [Fact] + public async Task GetTagHelpersAsync_WithSerializableCustomFactory_GoesOutOfProcess() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject_For_2_0); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + + var project = ProjectManager.GetProjectWithFilePath("Test.csproj"); + + var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace) + { + OnResolveOutOfProcess = (f, p) => + { + Assert.Same(CustomFactories[0].Value, f); + Assert.Same(project, p); + + return Task.FromResult(TagHelperResolutionResult.Empty); + }, + }; + + var result = await resolver.GetTagHelpersAsync(project); + + // Assert + Assert.Same(TagHelperResolutionResult.Empty, result); + } + + [Fact] + public async Task GetTagHelpersAsync_WithNonSerializableCustomFactory_StaysInProcess() + { + // Arrange + ProjectManager.HostProjectAdded(HostProject_For_NonSerializableConfiguration); + ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + + var project = ProjectManager.GetProjectWithFilePath("Test.csproj"); + + var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace) + { + OnResolveInProcess = (p) => + { + Assert.Same(project, p); + + return Task.FromResult(TagHelperResolutionResult.Empty); + }, + }; + + var result = await resolver.GetTagHelpersAsync(project); + + // Assert + Assert.Same(TagHelperResolutionResult.Empty, result); + + } + + private class TestTagHelperResolver : OOPTagHelperResolver + { + public TestTagHelperResolver(RazorProjectEngineFactoryService engineFactory, ErrorReporter errorReporter, Workspace workspace) + : base(engineFactory, errorReporter, workspace) + { + } + + public Func> OnResolveOutOfProcess { get; set; } + + public Func> OnResolveInProcess { get; set; } + + protected override Task ResolveTagHelpersOutOfProcessAsync(IProjectEngineFactory factory, ProjectSnapshot project) + { + Assert.NotNull(OnResolveOutOfProcess); + return OnResolveOutOfProcess(factory, project); + } + + protected override Task ResolveTagHelpersInProcessAsync(ProjectSnapshot project) + { + Assert.NotNull(OnResolveInProcess); + return OnResolveInProcess(project); + } + } + private class TestProjectSnapshotManager : DefaultProjectSnapshotManager + { + public TestProjectSnapshotManager(Workspace workspace) + : base( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Enumerable.Empty(), + workspace) + { + } + + protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context) + { + } + } + } +} 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..7333883233 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectSystemServices.cs @@ -0,0 +1,862 @@ +// 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(); + + TasksService = new TestProjectAsynchronousTasksService(ProjectService, UnconfiguredProject, ActiveConfiguredProject); + } + + 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 TestProjectAsynchronousTasksService TasksService { 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; + + IProjectAsynchronousTasksService IUnconfiguredProjectCommonServices.TasksService => TasksService; + + 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 TestProjectAsynchronousTasksService : IProjectAsynchronousTasksService, IProjectContext + { + public CancellationToken UnloadCancellationToken => CancellationToken.None; + + public TestProjectAsynchronousTasksService( + IProjectService projectService, + UnconfiguredProject unconfiguredProject, + ConfiguredProject configuredProject) + { + ProjectService = projectService; + UnconfiguredProject = unconfiguredProject; + ConfiguredProject = configuredProject; + } + + public IProjectService ProjectService { get; } + + public UnconfiguredProject UnconfiguredProject { get; } + + public ConfiguredProject ConfiguredProject { get; } + + public Task DrainCriticalTaskQueueAsync(bool drainCurrentQueueOnly = false, bool throwExceptions = false, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task DrainTaskQueueAsync(bool drainCurrentQueueOnly = false, bool throwExceptions = false, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task DrainTaskQueueAsync(ProjectCriticalOperation operation, bool drainCurrentQueueOnly = false, bool throwExceptions = false, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public bool IsTaskQueueEmpty(ProjectCriticalOperation projectCriticalOperation) + { + throw new NotImplementedException(); + } + + public void RegisterAsyncTask(JoinableTask joinableTask, bool registerFaultHandler = false) + { + } + + public void RegisterAsyncTask(Task task, bool registerFaultHandler = false) + { + } + + public void RegisterAsyncTask(JoinableTask joinableTask, ProjectCriticalOperation operationFlags, bool registerFaultHandler = false) + { + } + + public void RegisterCriticalAsyncTask(JoinableTask joinableTask, bool registerFaultHandler = false) + { + } + } + + 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/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/ProjectSnapshotHandleSerializationTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/ProjectSnapshotHandleSerializationTest.cs new file mode 100644 index 0000000000..347c533f8a --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/ProjectSnapshotHandleSerializationTest.cs @@ -0,0 +1,73 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + public class ProjectSnapshotHandleSerializationTest + { + public ProjectSnapshotHandleSerializationTest() + { + var converters = new JsonConverterCollection(); + converters.RegisterRazorConverters(); + Converters = converters.ToArray(); + } + + public JsonConverter[] Converters { get; } + + [Fact] + public void ProjectSnapshotHandleJsonConverter_Serialization_CanKindaRoundTrip() + { + // Arrange + var snapshot = new ProjectSnapshotHandle( + "Test.csproj", + new ProjectSystemRazorConfiguration( + RazorLanguageVersion.Version_1_1, + "Test", + new[] + { + new ProjectSystemRazorExtension("Test-Extension1"), + new ProjectSystemRazorExtension("Test-Extension2"), + }), + ProjectId.CreateFromSerialized(Guid.NewGuid(), "Test")); + + // Act + var json = JsonConvert.SerializeObject(snapshot, Converters); + var obj = JsonConvert.DeserializeObject(json, Converters); + + // Assert + Assert.Equal(snapshot.FilePath, obj.FilePath); + Assert.Equal(snapshot.Configuration.ConfigurationName, obj.Configuration.ConfigurationName); + Assert.Collection( + snapshot.Configuration.Extensions, + e => Assert.Equal("Test-Extension1", e.ExtensionName), + e => Assert.Equal("Test-Extension2", e.ExtensionName)); + Assert.Equal(snapshot.Configuration.LanguageVersion, obj.Configuration.LanguageVersion); + Assert.Equal(snapshot.WorkspaceProjectId.Id, obj.WorkspaceProjectId.Id); + } + + [Fact] + public void ProjectSnapshotHandleJsonConverter_SerializationWithNulls_CanKindaRoundTrip() + { + // Arrange + var snapshot = new ProjectSnapshotHandle("Test.csproj", null, null); + + // Act + var json = JsonConvert.SerializeObject(snapshot, Converters); + var obj = JsonConvert.DeserializeObject(json, Converters); + + // Assert + Assert.Equal(snapshot.FilePath, obj.FilePath); + Assert.Null(obj.Configuration); + Assert.Null(obj.WorkspaceProjectId); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorConfigurationSerializationTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorConfigurationSerializationTest.cs new file mode 100644 index 0000000000..2b084a8531 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorConfigurationSerializationTest.cs @@ -0,0 +1,50 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + public class RazorConfigurationSerializationTest + { + public RazorConfigurationSerializationTest() + { + var converters = new JsonConverterCollection(); + converters.RegisterRazorConverters(); + Converters = converters.ToArray(); + } + + public JsonConverter[] Converters { get; } + + [Fact] + public void RazorConfigurationJsonConverter_Serialization_CanRoundTrip() + { + // Arrange + var configuration = new ProjectSystemRazorConfiguration( + RazorLanguageVersion.Version_1_1, + "Test", + new[] + { + new ProjectSystemRazorExtension("Test-Extension1"), + new ProjectSystemRazorExtension("Test-Extension2"), + }); + + // Act + var json = JsonConvert.SerializeObject(configuration, Converters); + var obj = JsonConvert.DeserializeObject(json, Converters); + + // Assert + Assert.Equal(configuration.ConfigurationName, obj.ConfigurationName); + Assert.Collection( + configuration.Extensions, + e => Assert.Equal("Test-Extension1", e.ExtensionName), + e => Assert.Equal("Test-Extension2", e.ExtensionName)); + Assert.Equal(configuration.LanguageVersion, obj.LanguageVersion); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorExtensionSerializationTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorExtensionSerializationTest.cs new file mode 100644 index 0000000000..0f93fb5bd7 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/RazorExtensionSerializationTest.cs @@ -0,0 +1,38 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Serialization +{ + public class RazorExtensionSerializationTest + { + public RazorExtensionSerializationTest() + { + var converters = new JsonConverterCollection(); + converters.RegisterRazorConverters(); + Converters = converters.ToArray(); + } + + public JsonConverter[] Converters { get; } + + [Fact] + public void RazorExensionJsonConverter_Serialization_CanRoundTrip() + { + // Arrange + var extension = new ProjectSystemRazorExtension("Test"); + + // Act + var json = JsonConvert.SerializeObject(extension, Converters); + var obj = JsonConvert.DeserializeObject(json, Converters); + + // Assert + Assert.Equal(extension.ExtensionName, obj.ExtensionName); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TagHelperDescriptorSerializationTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/TagHelperDescriptorSerializationTest.cs similarity index 99% rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TagHelperDescriptorSerializationTest.cs rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/TagHelperDescriptorSerializationTest.cs index e8e1967bd8..b19a701506 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TagHelperDescriptorSerializationTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/TagHelperDescriptorSerializationTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.VisualStudio.LanguageServices.Razor.Serialization; using Newtonsoft.Json; using Xunit; diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs index 32e6c03370..726891e0a9 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.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.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Editor.Razor; @@ -40,7 +41,6 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor public void UpdateProjectCfg_Done_KnownProject_Invokes_ProjectBuildComplete() { // Arrange - var expectedProjectName = "Test1"; var expectedProjectPath = "Path/To/Project"; uint cookie; @@ -53,24 +53,23 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor services.Setup(s => s.GetService(It.Is(f => f == typeof(SVsSolutionBuildManager)))).Returns(buildManager.Object); var projectService = new Mock(); - projectService.Setup(p => p.GetProjectName(It.IsAny())).Returns(expectedProjectName); projectService.Setup(p => p.GetProjectPath(It.IsAny())).Returns(expectedProjectPath); - var workspace = TestWorkspace.Create(ws => + var projectSnapshots = new[] { - CreateProjectInWorkspace(ws, expectedProjectName, expectedProjectPath); - CreateProjectInWorkspace(ws, "Test2", "Path/To/AnotherProject"); - }); + Mock.Of(p => p.FilePath == expectedProjectPath && p.HostProject == new HostProject(expectedProjectPath, RazorConfiguration.Default)), + Mock.Of(p => p.FilePath == "Test2.csproj" && p.HostProject == new HostProject("Test2.csproj", RazorConfiguration.Default)), + }; var called = false; var projectManager = new Mock(); - projectManager.SetupGet(p => p.Workspace).Returns(workspace); + projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots); projectManager - .Setup(p => p.ProjectBuildComplete(It.IsAny())) - .Callback(c => + .Setup(p => p.HostProjectBuildComplete(It.IsAny())) + .Callback(c => { called = true; - Assert.Equal(expectedProjectName, c.Name); + Assert.Equal(expectedProjectPath, c.FilePath); }); var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, projectService.Object); @@ -87,7 +86,6 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor public void UpdateProjectCfg_Done_UnknownProject_DoesNotInvoke_ProjectBuildComplete() { // Arrange - var expectedProjectName = "Test1"; var expectedProjectPath = "Path/To/Project"; uint cookie; @@ -100,20 +98,19 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor services.Setup(s => s.GetService(It.Is(f => f == typeof(SVsSolutionBuildManager)))).Returns(buildManager.Object); var projectService = new Mock(); - projectService.Setup(p => p.GetProjectName(It.IsAny())).Returns(expectedProjectName); projectService.Setup(p => p.GetProjectPath(It.IsAny())).Returns(expectedProjectPath); - var workspace = TestWorkspace.Create(ws => + var projectSnapshots = new[] { - CreateProjectInWorkspace(ws, "Test2", "Path/To/AnotherProject"); - CreateProjectInWorkspace(ws, "Test3", "Path/To/DifferenProject"); - }); + Mock.Of(p => p.FilePath == "Path/To/AnotherProject" && p.HostProject == new HostProject("Path/To/AnotherProject", RazorConfiguration.Default)), + Mock.Of(p => p.FilePath == "Path/To/DifferenProject" && p.HostProject == new HostProject("Path/To/DifferenProject", RazorConfiguration.Default)), + }; var projectManager = new Mock(); - projectManager.SetupGet(p => p.Workspace).Returns(workspace); + projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots); projectManager - .Setup(p => p.ProjectBuildComplete(It.IsAny())) - .Callback(c => + .Setup(p => p.HostProjectBuildComplete(It.IsAny())) + .Callback(c => { throw new InvalidOperationException("This should not be called."); }); @@ -124,11 +121,5 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor // Act & Assert - Does not throw trigger.UpdateProjectCfg_Done(Mock.Of(), Mock.Of(), Mock.Of(), 0, 0, 0); } - - private static AdhocWorkspace CreateProjectInWorkspace(AdhocWorkspace workspace, string name, string path) - { - workspace.AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), name, "TestAssembly", LanguageNames.CSharp, filePath: path)); - return workspace; - } } } diff --git a/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.cs b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.cs index b02cb50f04..6fa1e5ae06 100644 --- a/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.cs +++ b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.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.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Editor.Razor; @@ -19,20 +20,19 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor { // Arrange var args = new BuildEventArgs(monitor: null, success: true); - var expectedProjectName = "Test1"; var expectedProjectPath = "Path/To/Project"; - var projectService = CreateProjectService(expectedProjectName, expectedProjectPath); - var workspace = TestWorkspace.Create(ws => + var projectService = CreateProjectService(expectedProjectPath); + var projectSnapshots = new[] { - CreateProjectInWorkspace(ws, expectedProjectName, expectedProjectPath); - CreateProjectInWorkspace(ws, "Test2", "Path/To/AnotherProject"); - }); + Mock.Of(p => p.FilePath == expectedProjectPath && p.HostProject == new HostProject(expectedProjectPath, RazorConfiguration.Default)), + Mock.Of(p => p.FilePath == "Test2.csproj" && p.HostProject == new HostProject("Test2.csproj", RazorConfiguration.Default)), + }; var projectManager = new Mock(MockBehavior.Strict); - projectManager.SetupGet(p => p.Workspace).Returns(workspace); + projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots); projectManager - .Setup(p => p.ProjectBuildComplete(It.IsAny())) - .Callback(c => Assert.Equal(expectedProjectName, c.Name)); + .Setup(p => p.HostProjectBuildComplete(It.IsAny())) + .Callback(c => Assert.Equal(expectedProjectPath, c.FilePath)); var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object); // Act @@ -47,15 +47,15 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor { // Arrange var args = new BuildEventArgs(monitor: null, success: true); - var projectService = CreateProjectService("Test1", "Path/To/Project"); - var workspace = TestWorkspace.Create(ws => + var projectService = CreateProjectService("Path/To/Project"); + var projectSnapshots = new[] { - CreateProjectInWorkspace(ws, "Test2", "Path/To/AnotherProject"); - }); + Mock.Of(p => p.FilePath == "Path/To/AnotherProject" && p.HostProject == new HostProject("Path/To/AnotherProject", RazorConfiguration.Default)), + }; var projectManager = new Mock(); - projectManager.SetupGet(p => p.Workspace).Returns(workspace); + projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots); projectManager - .Setup(p => p.ProjectBuildComplete(It.IsAny())) + .Setup(p => p.HostProjectBuildComplete(It.IsAny())) .Throws(); var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object); @@ -93,10 +93,9 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor trigger.ProjectOperations_EndBuild(null, args); } - private static TextBufferProjectService CreateProjectService(string projectName, string projectPath) + private static TextBufferProjectService CreateProjectService(string projectPath) { var projectService = new Mock(); - projectService.Setup(p => p.GetProjectName(null)).Returns(projectName); projectService.Setup(p => p.GetProjectPath(null)).Returns(projectPath); projectService.Setup(p => p.IsSupportedProject(null)).Returns(true); return projectService.Object; 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 67dc3bf0e1..0a3618e7d9 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj +++ b/tooling/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj @@ -79,7 +79,9 @@ + + @@ -312,4 +314,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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -