From 1d602d12057296b1cd265e836c1eb3ea6531520f Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Fri, 2 Mar 2018 13:53:00 -0800 Subject: [PATCH] Add host project system for VS4Mac. - Tied into VS4Macs ProjectExtensions in order to bootstrap our Razor world. - We currently watch all DotNet projects with the expectation that they're the only ones that can potentially turn into Razor compatible projects. - Added a fallback Razor project host which is used for pre-Razor SDK Razor versions (< 2.1). - Added a default Razor project host which consumes all MSBuild data from the users packages and sets up the Razor world accordingly. - Had to modify some existing contracts to work better with new expectations. one of these was the VS4Mac specific Workspace accessor; essentially we needed to be able to lookup a workspace from a solution. - Some of our previous expectations about addins were wrong (not being able to directly reference your libraries). To avoid using reflection to bootstrap our types I tried out directly referencing our libraries and all worked fine. - Refactored the DefaultRazorProjectHost in windows (since we had to in Mac) for testing purposes. #2081 --- build/dependencies.props | 2 +- .../Properties/AssemblyInfo.cs | 1 + .../ProjectSystem/DefaultRazorProjectHost.cs | 206 +++++-- ...efaultVisualStudioMacWorkspaceAccessor.cs} | 17 +- .../Editor/DefaultTextBufferProjectService.cs | 16 +- .../ProjectSystem/DefaultDotNetProjectHost.cs | 155 +++++ .../ProjectSystem/DefaultRazorProjectHost.cs | 180 ++++++ .../ProjectSystem/DotNetProjectHost.cs | 14 + .../ProjectSystem/DotNetProjectHostFactory.cs | 57 ++ .../ProjectSystem/FallbackRazorProjectHost.cs | 99 +++ .../ProjectSystem/RazorProjectHostBase.cs | 169 +++++ .../Properties/AssemblyInfo.cs | 1 + .../VisualStudioMacWorkspaceAccessor.cs | 14 + .../DefaultRazorProjectHostTest.cs | 575 +++++++++++++++++- .../DefaultDotNetProjectHostTest.cs | 34 ++ .../DefaultRazorProjectHostTest.cs | 470 ++++++++++++++ ...ltVisualStudioMacWorkspaceAccessorTest.cs} | 4 +- .../FallbackRazorProjectHostTest.cs | 62 ++ ...dio.Mac.LanguageServices.Razor.Test.csproj | 4 + .../xunit.runner.json | 4 + ...crosoft.VisualStudio.Mac.RazorAddin.csproj | 11 +- .../Properties/_Manifest.addin.xml | 7 +- .../RazorProjectExtension.cs | 59 ++ 23 files changed, 2106 insertions(+), 55 deletions(-) rename src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/{DefaultVisualStudioWorkspaceAccessor.cs => DefaultVisualStudioMacWorkspaceAccessor.cs} (81%) create mode 100644 src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DefaultDotNetProjectHost.cs create mode 100644 src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs create mode 100644 src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DotNetProjectHost.cs create mode 100644 src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DotNetProjectHostFactory.cs create mode 100644 src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs create mode 100644 src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs create mode 100644 src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/VisualStudioMacWorkspaceAccessor.cs create mode 100644 test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultDotNetProjectHostTest.cs create mode 100644 test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultRazorProjectHostTest.cs rename test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/{DefaultVisualStudioWorkspaceAccessorTest.cs => DefaultVisualStudioMacWorkspaceAccessorTest.cs} (86%) create mode 100644 test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/FallbackRazorProjectHostTest.cs create mode 100644 test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/xunit.runner.json create mode 100644 tooling/Microsoft.VisualStudio.Mac.RazorAddin/RazorProjectExtension.cs diff --git a/build/dependencies.props b/build/dependencies.props index e92d72528f..dca2d39e6b 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -36,7 +36,7 @@ 9.0.30729 7.10.6071 15.6.161-preview - 1.3.7 + 1.3.8 1.0.1 4.7.49 2.0.1 diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs index d6cf7bdc7e..e70ec9920f 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Performance, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Mac.RazorAddin, 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.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs index 122505b002..5e45f9821f 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.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 System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel.Composition; using System.Linq; using System.Threading; @@ -10,6 +12,9 @@ using System.Threading.Tasks.Dataflow; using Microsoft.AspNetCore.Razor.Language; using Microsoft.VisualStudio.LanguageServices; using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.Properties; +using ProjectState = System.Collections.Immutable.IImmutableDictionary; +using ProjectStateItem = System.Collections.Generic.KeyValuePair>; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { @@ -82,47 +87,180 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { 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 (TryGetConfiguration(update.Value.CurrentState, out var configuration)) { - 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(); + var hostProject = new HostProject(CommonServices.UnconfiguredProject.FullPath, configuration); + await UpdateProjectUnsafeAsync(hostProject).ConfigureAwait(false); } - - if (configuration == null) + else { - // Ok we can't find a language version. Let's assume this project isn't using Razor then. + // Ok we can't find a configuration. 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); } + + // Internal for testing + internal static bool TryGetConfiguration( + ProjectState projectState, + out RazorConfiguration configuration) + { + if (!TryGetDefaultConfiguration(projectState, out var defaultConfiguration)) + { + configuration = null; + return false; + } + + if (!TryGetLanguageVersion(projectState, out var languageVersion)) + { + configuration = null; + return false; + } + + if (!TryGetConfigurationItem(defaultConfiguration, projectState, out var configurationItem)) + { + configuration = null; + return false; + } + + if (!TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames)) + { + configuration = null; + return false; + } + + if (!TryGetExtensions(configuredExtensionNames, projectState, out var extensions)) + { + configuration = null; + return false; + } + + configuration = new ProjectSystemRazorConfiguration(languageVersion, configurationItem.Key, extensions); + return true; + } + + + // Internal for testing + internal static bool TryGetDefaultConfiguration(ProjectState projectState, out string defaultConfiguration) + { + if (!projectState.TryGetValue(Rules.RazorGeneral.SchemaName, out var rule)) + { + defaultConfiguration = null; + return false; + } + + if (!rule.Properties.TryGetValue(Rules.RazorGeneral.RazorDefaultConfigurationProperty, out defaultConfiguration)) + { + defaultConfiguration = null; + return false; + } + + if (string.IsNullOrEmpty(defaultConfiguration)) + { + defaultConfiguration = null; + return false; + } + + return true; + } + + // Internal for testing + internal static bool TryGetLanguageVersion(ProjectState projectState, out RazorLanguageVersion languageVersion) + { + if (!projectState.TryGetValue(Rules.RazorGeneral.SchemaName, out var rule)) + { + languageVersion = null; + return false; + } + + if (!rule.Properties.TryGetValue(Rules.RazorGeneral.RazorLangVersionProperty, out var languageVersionValue)) + { + languageVersion = null; + return false; + } + + if (string.IsNullOrEmpty(languageVersionValue)) + { + languageVersion = null; + return false; + } + + if (!RazorLanguageVersion.TryParse(languageVersionValue, out languageVersion)) + { + languageVersion = RazorLanguageVersion.Latest; + } + + return true; + } + + // Internal for testing + internal static bool TryGetConfigurationItem( + string configuration, + ProjectState projectState, + out ProjectStateItem configurationItem) + { + if (!projectState.TryGetValue(Rules.RazorConfiguration.PrimaryDataSourceItemType, out var configurationState)) + { + configurationItem = default(ProjectStateItem); + return false; + } + + var razorConfigurationItems = configurationState.Items; + foreach (var item in razorConfigurationItems) + { + if (item.Key == configuration) + { + configurationItem = item; + return true; + } + } + + configurationItem = default(ProjectStateItem); + return false; + } + + // Internal for testing + internal static bool TryGetConfiguredExtensionNames(ProjectStateItem configurationItem, out string[] configuredExtensionNames) + { + if (!configurationItem.Value.TryGetValue(Rules.RazorConfiguration.ExtensionsProperty, out var extensionNamesValue)) + { + configuredExtensionNames = null; + return false; + } + + if (string.IsNullOrEmpty(extensionNamesValue)) + { + configuredExtensionNames = null; + return false; + } + + configuredExtensionNames = extensionNamesValue.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + return true; + } + + // Internal for testing + internal static bool TryGetExtensions(string[] configuredExtensionNames, ProjectState projectState, out ProjectSystemRazorExtension[] extensions) + { + if (!projectState.TryGetValue(Rules.RazorExtension.PrimaryDataSourceItemType, out var extensionState)) + { + extensions = null; + return false; + } + + var extensionItems = extensionState.Items; + var extensionList = new List(); + foreach (var item in extensionItems) + { + var extensionName = item.Key; + if (configuredExtensionNames.Contains(extensionName)) + { + extensionList.Add(new ProjectSystemRazorExtension(extensionName)); + } + } + + extensions = extensionList.ToArray(); + return true; + } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/DefaultVisualStudioWorkspaceAccessor.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/DefaultVisualStudioMacWorkspaceAccessor.cs similarity index 81% rename from src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/DefaultVisualStudioWorkspaceAccessor.cs rename to src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/DefaultVisualStudioMacWorkspaceAccessor.cs index aa634ab507..eeac75968a 100644 --- a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/DefaultVisualStudioWorkspaceAccessor.cs +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/DefaultVisualStudioMacWorkspaceAccessor.cs @@ -13,12 +13,13 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor { [System.Composition.Shared] [Export(typeof(VisualStudioWorkspaceAccessor))] - internal class DefaultVisualStudioWorkspaceAccessor : VisualStudioWorkspaceAccessor + [Export(typeof(VisualStudioMacWorkspaceAccessor))] + internal class DefaultVisualStudioMacWorkspaceAccessor : VisualStudioMacWorkspaceAccessor { private readonly TextBufferProjectService _projectService; [ImportingConstructor] - public DefaultVisualStudioWorkspaceAccessor(TextBufferProjectService projectService) + public DefaultVisualStudioMacWorkspaceAccessor(TextBufferProjectService projectService) { if (projectService == null) { @@ -56,7 +57,17 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor return false; } - workspace = TypeSystemService.GetWorkspace(hostSolution); + return TryGetWorkspace(hostSolution, out workspace); + } + + public override bool TryGetWorkspace(Solution solution, out Workspace workspace) + { + if (solution == null) + { + throw new ArgumentNullException(nameof(solution)); + } + + workspace = TypeSystemService.GetWorkspace(solution); // Workspace cannot be null at this point. If TypeSystemService.GetWorkspace isn't able to find a corresponding // workspace it returns an empty workspace. Therefore, in order to see if we have a valid workspace we need to diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs index 64696bf571..02a4541958 100644 --- a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs @@ -17,6 +17,7 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.Editor [Export(typeof(TextBufferProjectService))] internal class DefaultTextBufferProjectService : TextBufferProjectService { + private const string DotNetCoreRazorCapability = "DotNetCoreRazor | AspNetCore"; private readonly ITextDocumentFactoryService _documentFactory; [ImportingConstructor] @@ -73,7 +74,20 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.Editor } // VisualStudio for Mac only supports ASP.NET Core Razor. - public override bool IsSupportedProject(object project) => project is DotNetProject; + public override bool IsSupportedProject(object project) + { + if (!(project is DotNetProject dotNetProject)) + { + return false; + } + + if (!dotNetProject.IsCapabilityMatch(DotNetCoreRazorCapability)) + { + return false; + } + + return true; + } public override string GetProjectName(object project) { diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DefaultDotNetProjectHost.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DefaultDotNetProjectHost.cs new file mode 100644 index 0000000000..f66d2774fb --- /dev/null +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DefaultDotNetProjectHost.cs @@ -0,0 +1,155 @@ +// 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; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.VisualStudio.Editor.Razor; +using MonoDevelop.Projects; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem +{ + internal class DefaultDotNetProjectHost : DotNetProjectHost + { + private const string ExplicitRazorConfigurationCapability = "DotNetCoreRazorConfiguration"; + + private readonly DotNetProject _project; + private readonly ForegroundDispatcher _foregroundDispatcher; + private readonly VisualStudioMacWorkspaceAccessor _workspaceAccessor; + private readonly TextBufferProjectService _projectService; + private RazorProjectHostBase _razorProjectHost; + + public DefaultDotNetProjectHost( + DotNetProject project, + ForegroundDispatcher foregroundDispatcher, + VisualStudioMacWorkspaceAccessor workspaceAccessor, + TextBufferProjectService projectService) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (workspaceAccessor == null) + { + throw new ArgumentNullException(nameof(workspaceAccessor)); + } + + if (projectService == null) + { + throw new ArgumentNullException(nameof(projectService)); + } + + _project = project; + _foregroundDispatcher = foregroundDispatcher; + _workspaceAccessor = workspaceAccessor; + _projectService = projectService; + } + + // Internal for testing + internal DefaultDotNetProjectHost( + ForegroundDispatcher foregroundDispatcher, + VisualStudioMacWorkspaceAccessor workspaceAccessor, + TextBufferProjectService projectService) + { + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (workspaceAccessor == null) + { + throw new ArgumentNullException(nameof(workspaceAccessor)); + } + + if (projectService == null) + { + throw new ArgumentNullException(nameof(projectService)); + } + + _foregroundDispatcher = foregroundDispatcher; + _workspaceAccessor = workspaceAccessor; + _projectService = projectService; + } + + public override DotNetProject Project => _project; + + public override void Subscribe() + { + _foregroundDispatcher.AssertForegroundThread(); + + UpdateRazorHostProject(); + + _project.ProjectCapabilitiesChanged += Project_ProjectCapabilitiesChanged; + _project.Disposing += Project_Disposing; + } + + private void Project_Disposing(object sender, EventArgs e) + { + _foregroundDispatcher.AssertForegroundThread(); + + _project.ProjectCapabilitiesChanged -= Project_ProjectCapabilitiesChanged; + _project.Disposing -= Project_Disposing; + + DetatchCurrentRazorProjectHost(); + } + + private void Project_ProjectCapabilitiesChanged(object sender, EventArgs e) => UpdateRazorHostProject(); + + // Internal for testing + internal void UpdateRazorHostProject() + { + _foregroundDispatcher.AssertForegroundThread(); + + DetatchCurrentRazorProjectHost(); + + if (!_projectService.IsSupportedProject(_project)) + { + // Not a Razor compatible project. + return; + } + + if (!TryGetProjectSnapshotManager(out var projectSnapshotManager)) + { + // Could not get a ProjectSnapshotManager for the current project. + return; + } + + if (_project.IsCapabilityMatch(ExplicitRazorConfigurationCapability)) + { + // SDK >= 2.1 + _razorProjectHost = new DefaultRazorProjectHost(_project, _foregroundDispatcher, projectSnapshotManager); + return; + } + + // We're an older version of Razor at this point, SDK < 2.1 + _razorProjectHost = new FallbackRazorProjectHost(_project, _foregroundDispatcher, projectSnapshotManager); + } + + private bool TryGetProjectSnapshotManager(out ProjectSnapshotManagerBase projectSnapshotManagerBase) + { + if (!_workspaceAccessor.TryGetWorkspace(_project.ParentSolution, out var workspace)) + { + // Could not locate workspace for razor project. Project is most likely tearing down. + projectSnapshotManagerBase = null; + return false; + } + + var languageService = workspace.Services.GetLanguageServices(RazorLanguage.Name); + projectSnapshotManagerBase = (ProjectSnapshotManagerBase)languageService.GetRequiredService(); + + return true; + } + + private void DetatchCurrentRazorProjectHost() + { + _razorProjectHost?.Detatch(); + _razorProjectHost = null; + } + } +} diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs new file mode 100644 index 0000000000..587c7f7b9e --- /dev/null +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs @@ -0,0 +1,180 @@ +// 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.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using MonoDevelop.Projects; +using MonoDevelop.Projects.MSBuild; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem +{ + internal class DefaultRazorProjectHost : RazorProjectHostBase + { + private const string RazorLangVersionProperty = "RazorLangVersion"; + private const string RazorDefaultConfigurationProperty = "RazorDefaultConfiguration"; + private const string RazorExtensionItemType = "RazorExtension"; + private const string RazorConfigurationItemType = "RazorConfiguration"; + private const string RazorConfigurationItemTypeExtensionsProperty = "Extensions"; + + public DefaultRazorProjectHost( + DotNetProject project, + ForegroundDispatcher foregroundDispatcher, + ProjectSnapshotManagerBase projectSnapshotManager) + : base(project, foregroundDispatcher, projectSnapshotManager) + { + } + + protected override async Task OnProjectChangedAsync() + { + ForegroundDispatcher.AssertBackgroundThread(); + + await ExecuteWithLockAsync(async () => + { + var projectProperties = DotNetProject.MSBuildProject.EvaluatedProperties; + var projectItems = DotNetProject.MSBuildProject.EvaluatedItems; + + if (TryGetConfiguration(projectProperties, projectItems, out var configuration)) + { + var hostProject = new HostProject(DotNetProject.FileName.FullPath, configuration); + await UpdateHostProjectUnsafeAsync(hostProject).ConfigureAwait(false); + } + else + { + // Ok we can't find a configuration. Let's assume this project isn't using Razor then. + await UpdateHostProjectUnsafeAsync(null).ConfigureAwait(false); + } + }); + } + + // Internal for testing + internal static bool TryGetConfiguration( + IMSBuildEvaluatedPropertyCollection projectProperties, + IEnumerable projectItems, + out RazorConfiguration configuration) + { + if (!TryGetDefaultConfiguration(projectProperties, out var defaultConfiguration)) + { + configuration = null; + return false; + } + + if (!TryGetLanguageVersion(projectProperties, out var languageVersion)) + { + configuration = null; + return false; + } + + if (!TryGetConfigurationItem(defaultConfiguration, projectItems, out var configurationItem)) + { + configuration = null; + return false; + } + + if (!TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames)) + { + configuration = null; + return false; + } + + var extensions = GetExtensions(configuredExtensionNames, projectItems); + configuration = new ProjectSystemRazorConfiguration(languageVersion, configurationItem.Include, extensions); + return true; + } + + + // Internal for testing + internal static bool TryGetDefaultConfiguration(IMSBuildEvaluatedPropertyCollection projectProperties, out string defaultConfiguration) + { + defaultConfiguration = projectProperties.GetValue(RazorDefaultConfigurationProperty); + if (string.IsNullOrEmpty(defaultConfiguration)) + { + defaultConfiguration = null; + return false; + } + + return true; + } + + // Internal for testing + internal static bool TryGetLanguageVersion(IMSBuildEvaluatedPropertyCollection projectProperties, out RazorLanguageVersion languageVersion) + { + var languageVersionValue = projectProperties.GetValue(RazorLangVersionProperty); + if (string.IsNullOrEmpty(languageVersionValue)) + { + languageVersion = null; + return false; + } + + if (!RazorLanguageVersion.TryParse(languageVersionValue, out languageVersion)) + { + languageVersion = RazorLanguageVersion.Latest; + } + + return true; + } + + // Internal for testing + internal static bool TryGetConfigurationItem( + string configuration, + IEnumerable projectItems, + out IMSBuildItemEvaluated configurationItem) + { + foreach (var item in projectItems) + { + if (item.Name == RazorConfigurationItemType && item.Include == configuration) + { + configurationItem = item; + return true; + } + } + + configurationItem = null; + return false; + } + + // Internal for testing + internal static bool TryGetConfiguredExtensionNames(IMSBuildItemEvaluated configurationItem, out string[] configuredExtensionNames) + { + var extensionNamesValue = configurationItem.Metadata.GetValue(RazorConfigurationItemTypeExtensionsProperty); + + if (string.IsNullOrEmpty(extensionNamesValue)) + { + configuredExtensionNames = null; + return false; + } + + configuredExtensionNames = extensionNamesValue.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + return true; + } + + // Internal for testing + internal static ProjectSystemRazorExtension[] GetExtensions( + string[] configuredExtensionNames, + IEnumerable projectItems) + { + var extensions = new List(); + + foreach (var item in projectItems) + { + if (item.Name != RazorExtensionItemType) + { + // Not a RazorExtension + continue; + } + + var extensionName = item.Include; + if (configuredExtensionNames.Contains(extensionName)) + { + extensions.Add(new ProjectSystemRazorExtension(extensionName)); + } + } + + return extensions.ToArray(); + } + } +} diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DotNetProjectHost.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DotNetProjectHost.cs new file mode 100644 index 0000000000..d5933ef90f --- /dev/null +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DotNetProjectHost.cs @@ -0,0 +1,14 @@ +// 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 MonoDevelop.Projects; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem +{ + internal abstract class DotNetProjectHost + { + public abstract DotNetProject Project { get; } + + public abstract void Subscribe(); + } +} diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DotNetProjectHostFactory.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DotNetProjectHostFactory.cs new file mode 100644 index 0000000000..9755b2dc19 --- /dev/null +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/DotNetProjectHostFactory.cs @@ -0,0 +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.ComponentModel.Composition; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.VisualStudio.Editor.Razor; +using MonoDevelop.Projects; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem +{ + [System.Composition.Shared] + [Export(typeof(DotNetProjectHostFactory))] + internal class DotNetProjectHostFactory + { + private readonly ForegroundDispatcher _foregroundDispatcher; + private readonly VisualStudioMacWorkspaceAccessor _workspaceAccessor; + private readonly TextBufferProjectService _projectService; + + [ImportingConstructor] + public DotNetProjectHostFactory( + ForegroundDispatcher foregroundDispatcher, + VisualStudioMacWorkspaceAccessor workspaceAccessor, + TextBufferProjectService projectService) + { + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (workspaceAccessor == null) + { + throw new ArgumentNullException(nameof(workspaceAccessor)); + } + + if (projectService == null) + { + throw new ArgumentNullException(nameof(projectService)); + } + + _foregroundDispatcher = foregroundDispatcher; + _workspaceAccessor = workspaceAccessor; + _projectService = projectService; + } + + public DotNetProjectHost Create(DotNetProject project) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + var projectHost = new DefaultDotNetProjectHost(project, _foregroundDispatcher, _workspaceAccessor, _projectService); + return projectHost; + } + } +} diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs new file mode 100644 index 0000000000..170dd51ca2 --- /dev/null +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs @@ -0,0 +1,99 @@ +// 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.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using MonoDevelop.Projects; +using AssemblyReference = MonoDevelop.Projects.AssemblyReference; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem +{ + internal class FallbackRazorProjectHost : RazorProjectHostBase + { + private const string MvcAssemblyFileName = "Microsoft.AspNetCore.Mvc.Razor.dll"; + + public FallbackRazorProjectHost( + DotNetProject project, + ForegroundDispatcher foregroundDispatcher, + ProjectSnapshotManagerBase projectSnapshotManager) + : base(project, foregroundDispatcher, projectSnapshotManager) + { + } + + protected override async Task OnProjectChangedAsync() + { + ForegroundDispatcher.AssertBackgroundThread(); + + await ExecuteWithLockAsync(async () => + { + var referencedAssemblies = await DotNetProject.GetReferencedAssemblies(ConfigurationSelector.Default); + var mvcReference = referencedAssemblies.FirstOrDefault(IsMvcAssembly); + + if (mvcReference == null) + { + // Ok we can't find an MVC version. Let's assume this project isn't using Razor then. + await UpdateHostProjectUnsafeAsync(null).ConfigureAwait(false); + return; + } + + var version = GetAssemblyVersion(mvcReference.FilePath); + if (version == null) + { + // Ok we can't find an MVC version. Let's assume this project isn't using Razor then. + await UpdateHostProjectUnsafeAsync(null).ConfigureAwait(false); + return; + } + + var configuration = FallbackRazorConfiguration.SelectConfiguration(version); + var hostProject = new HostProject(DotNetProject.FileName.FullPath, configuration); + await UpdateHostProjectUnsafeAsync(hostProject).ConfigureAwait(false); + }); + } + + // Internal for testing + internal static bool IsMvcAssembly(AssemblyReference reference) + { + var fileName = reference?.FilePath.FileName; + + if (string.IsNullOrEmpty(fileName)) + { + return false; + } + + if (string.Equals(reference.FilePath.FileName, MvcAssemblyFileName, StringComparison.OrdinalIgnoreCase)) + { + // Mvc assembly + return true; + } + + return false; + } + + private static Version GetAssemblyVersion(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; + } + } + } +} diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs new file mode 100644 index 0000000000..c6afacad93 --- /dev/null +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs @@ -0,0 +1,169 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.VisualStudio.Threading; +using MonoDevelop.Projects; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem +{ + internal abstract class RazorProjectHostBase + { + // References changes are always triggered when project changes happen. + private const string ProjectChangedHint = "References"; + + private bool _batchingProjectChanges; + private readonly DotNetProject _dotNetProject; + private readonly ForegroundDispatcher _foregroundDispatcher; + private readonly ProjectSnapshotManagerBase _projectSnapshotManager; + private readonly AsyncSemaphore _onProjectChangedInnerSemaphore; + private readonly AsyncSemaphore _projectChangedSemaphore; + private HostProject _currentHostProject; + + public RazorProjectHostBase( + DotNetProject project, + ForegroundDispatcher foregroundDispatcher, + ProjectSnapshotManagerBase projectSnapshotManager) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (projectSnapshotManager == null) + { + throw new ArgumentNullException(nameof(projectSnapshotManager)); + } + + _dotNetProject = project; + _foregroundDispatcher = foregroundDispatcher; + _projectSnapshotManager = projectSnapshotManager; + _onProjectChangedInnerSemaphore = new AsyncSemaphore(initialCount: 1); + _projectChangedSemaphore = new AsyncSemaphore(initialCount: 1); + + AttachToProject(); + } + + public DotNetProject DotNetProject => _dotNetProject; + + public HostProject HostProject => _currentHostProject; + + protected ForegroundDispatcher ForegroundDispatcher => _foregroundDispatcher; + + public void Detatch() + { + _foregroundDispatcher.AssertForegroundThread(); + + DotNetProject.Modified -= DotNetProject_Modified; + + UpdateHostProjectForeground(null); + } + + protected abstract Task OnProjectChangedAsync(); + + // Protected virtual for testing + protected virtual void AttachToProject() + { + ForegroundDispatcher.AssertForegroundThread(); + + DotNetProject.Modified += DotNetProject_Modified; + + // Trigger the initial update to the project. + _batchingProjectChanges = true; + Task.Factory.StartNew(ProjectChangedBackgroundAsync, null, CancellationToken.None, TaskCreationOptions.None, ForegroundDispatcher.BackgroundScheduler); + } + + // Must be called inside the lock. + protected async Task UpdateHostProjectUnsafeAsync(HostProject newHostProject) + { + _foregroundDispatcher.AssertBackgroundThread(); + + await Task.Factory.StartNew(UpdateHostProjectForeground, newHostProject, CancellationToken.None, TaskCreationOptions.None, ForegroundDispatcher.ForegroundScheduler); + } + + protected async Task ExecuteWithLockAsync(Func func) + { + using (await _projectChangedSemaphore.EnterAsync().ConfigureAwait(false)) + { + await func().ConfigureAwait(false); + } + } + + private async Task ProjectChangedBackgroundAsync(object state) + { + ForegroundDispatcher.AssertBackgroundThread(); + + _batchingProjectChanges = false; + + // Ensure ordering, typically we'll only have 1 background thread in flight at a time. However, + // between this line and the one prior another background thread could have also entered this + // method. This is here to protect against us changing the order of project changed events. + using (await _onProjectChangedInnerSemaphore.EnterAsync().ConfigureAwait(false)) + { + await OnProjectChangedAsync(); + } + } + + private void DotNetProject_Modified(object sender, SolutionItemModifiedEventArgs args) + { + if (args == null) + { + throw new ArgumentNullException(nameof(args)); + } + + ForegroundDispatcher.AssertForegroundThread(); + + if (_batchingProjectChanges) + { + // Already waiting to recompute host project, no need to do any more work to determine if we're dirty. + return; + } + + var projectChanged = args.Any(arg => string.Equals(arg.Hint, ProjectChangedHint, StringComparison.Ordinal)); + if (projectChanged) + { + // This method can be spammed for tons of project change events but all we really care about is "are we dirty?". + // Therefore, we re-dispatch here to allow any remaining project change events to fire and to then only have 1 host + // project change trigger; this way we don't spam our own system with re-configure calls. + _batchingProjectChanges = true; + Task.Factory.StartNew(ProjectChangedBackgroundAsync, null, CancellationToken.None, TaskCreationOptions.None, ForegroundDispatcher.BackgroundScheduler); + } + } + + private void UpdateHostProjectForeground(object state) + { + _foregroundDispatcher.AssertForegroundThread(); + + var newHostProject = (HostProject)state; + + if (_currentHostProject == null && newHostProject == null) + { + // This is a no-op. This project isn't using Razor. + } + else if (_currentHostProject == null && newHostProject != null) + { + _projectSnapshotManager.HostProjectAdded(newHostProject); + } + else if (_currentHostProject != null && newHostProject == null) + { + _projectSnapshotManager.HostProjectRemoved(HostProject); + } + else + { + _projectSnapshotManager.HostProjectChanged(newHostProject); + } + + _currentHostProject = newHostProject; + } + } +} diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Properties/AssemblyInfo.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Properties/AssemblyInfo.cs index d49ca03713..e8a39ba34e 100644 --- a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Properties/AssemblyInfo.cs +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Properties/AssemblyInfo.cs @@ -3,5 +3,6 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Mac.RazorAddin, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/VisualStudioMacWorkspaceAccessor.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/VisualStudioMacWorkspaceAccessor.cs new file mode 100644 index 0000000000..9056f3864d --- /dev/null +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/VisualStudioMacWorkspaceAccessor.cs @@ -0,0 +1,14 @@ +// 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.Editor.Razor; +using MonoDevelop.Projects; +using Workspace = Microsoft.CodeAnalysis.Workspace; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor +{ + internal abstract class VisualStudioMacWorkspaceAccessor : VisualStudioWorkspaceAccessor + { + public abstract bool TryGetWorkspace(Solution solution, out Workspace workspace); + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultRazorProjectHostTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultRazorProjectHostTest.cs index 6234bfb557..b0c0322f12 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultRazorProjectHostTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultRazorProjectHostTest.cs @@ -3,12 +3,16 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.VisualStudio.LanguageServices.Razor; using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.Properties; using Moq; using Xunit; +using ProjectStateItem = System.Collections.Generic.KeyValuePair>; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { @@ -24,6 +28,571 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private Workspace Workspace { get; } + [Fact] + public void TryGetDefaultConfiguration_FailsIfNoRule() + { + // Arrange + var projectState = new Dictionary().ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetDefaultConfiguration(projectState, out var defaultConfiguration); + + // Assert + Assert.False(result); + Assert.Null(defaultConfiguration); + } + + [Fact] + public void TryGetDefaultConfiguration_FailsIfNoConfiguration() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary()) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetDefaultConfiguration(projectState, out var defaultConfiguration); + + // Assert + Assert.False(result); + Assert.Null(defaultConfiguration); + } + + [Fact] + public void TryGetDefaultConfiguration_FailsIfEmptyConfiguration() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties( + Rules.RazorGeneral.SchemaName, + new Dictionary() + { + [Rules.RazorGeneral.RazorDefaultConfigurationProperty] = string.Empty + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetDefaultConfiguration(projectState, out var defaultConfiguration); + + // Assert + Assert.False(result); + Assert.Null(defaultConfiguration); + } + + [Fact] + public void TryGetDefaultConfiguration_SucceedsWithValidConfiguration() + { + // Arrange + var expectedConfiguration = "Razor-13.37"; + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties( + Rules.RazorGeneral.SchemaName, + new Dictionary() + { + [Rules.RazorGeneral.RazorDefaultConfigurationProperty] = expectedConfiguration + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetDefaultConfiguration(projectState, out var defaultConfiguration); + + // Assert + Assert.True(result); + Assert.Equal(expectedConfiguration, defaultConfiguration); + } + + [Fact] + public void TryGetLanguageVersion_FailsIfNoRule() + { + // Arrange + var projectState = new Dictionary().ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetLanguageVersion(projectState, out var languageVersion); + + // Assert + Assert.False(result); + Assert.Null(languageVersion); + } + + [Fact] + public void TryGetLanguageVersion_FailsIfNoLanguageVersion() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary()) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetLanguageVersion(projectState, out var languageVersion); + + // Assert + Assert.False(result); + Assert.Null(languageVersion); + } + + [Fact] + public void TryGetLanguageVersion_FailsIfEmptyLanguageVersion() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties( + Rules.RazorGeneral.SchemaName, + new Dictionary() + { + [Rules.RazorGeneral.RazorLangVersionProperty] = string.Empty + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetLanguageVersion(projectState, out var languageVersion); + + // Assert + Assert.False(result); + Assert.Null(languageVersion); + } + + [Fact] + public void TryGetLanguageVersion_SucceedsWithValidLanguageVersion() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties( + Rules.RazorGeneral.SchemaName, + new Dictionary() + { + [Rules.RazorGeneral.RazorLangVersionProperty] = "1.0" + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetLanguageVersion(projectState, out var languageVersion); + + // Assert + Assert.True(result); + Assert.Same(RazorLanguageVersion.Version_1_0, languageVersion); + } + + [Fact] + public void TryGetLanguageVersion_SucceedsWithUnknownLanguageVersion_DefaultsToLatest() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties( + Rules.RazorGeneral.SchemaName, + new Dictionary() + { + [Rules.RazorGeneral.RazorLangVersionProperty] = "13.37" + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetLanguageVersion(projectState, out var languageVersion); + + // Assert + Assert.True(result); + Assert.Same(RazorLanguageVersion.Latest, languageVersion); + } + + [Fact] + public void TryGetConfigurationItem_FailsNoRazorConfigurationRule() + { + // Arrange + var projectState = new Dictionary().ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetConfigurationItem("Razor-13.37", projectState, out var configurationItem); + + // Assert + Assert.False(result); + } + + [Fact] + public void TryGetConfigurationItem_FailsNoRazorConfigurationItems() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorConfiguration.SchemaName] = TestProjectRuleSnapshot.CreateItems( + Rules.RazorConfiguration.SchemaName, + new Dictionary>()) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetConfigurationItem("Razor-13.37", projectState, out var configurationItem); + + // Assert + Assert.False(result); + } + + [Fact] + public void TryGetConfigurationItem_FailsNoMatchingRazorConfigurationItems() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorConfiguration.SchemaName] = TestProjectRuleSnapshot.CreateItems( + Rules.RazorConfiguration.SchemaName, + new Dictionary>() + { + ["Razor-10.0"] = new Dictionary(), + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetConfigurationItem("Razor-13.37", projectState, out var configurationItem); + + // Assert + Assert.False(result); + } + + [Fact] + public void TryGetConfigurationItem_SucceedsForMatchingConfigurationItem() + { + // Arrange + var expectedConfiguration = "Razor-13.37"; + var expectedConfigurationValue = new Dictionary() + { + [Rules.RazorConfiguration.ExtensionsProperty] = "SomeExtension" + }; + var projectState = new Dictionary() + { + [Rules.RazorConfiguration.SchemaName] = TestProjectRuleSnapshot.CreateItems( + Rules.RazorConfiguration.SchemaName, + new Dictionary>() + { + [expectedConfiguration] = expectedConfigurationValue + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetConfigurationItem(expectedConfiguration, projectState, out var configurationItem); + + // Assert + Assert.True(result); + Assert.Equal(expectedConfiguration, configurationItem.Key); + Assert.True(Enumerable.SequenceEqual(expectedConfigurationValue, configurationItem.Value)); + } + + [Fact] + public void TryGetConfiguredExtensionNames_FailsIfNoExtensions() + { + // Arrange + var extensions = new Dictionary().ToImmutableDictionary(); + var configurationItem = new ProjectStateItem(Rules.RazorConfiguration.SchemaName, extensions); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionnames); + + // Assert + Assert.False(result); + Assert.Null(configuredExtensionnames); + } + + [Fact] + public void TryGetConfiguredExtensionNames_FailsIfEmptyExtensions() + { + // Arrange + var extensions = new Dictionary() + { + [Rules.RazorConfiguration.ExtensionsProperty] = string.Empty + }.ToImmutableDictionary(); + var configurationItem = new ProjectStateItem(Rules.RazorConfiguration.SchemaName, extensions); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames); + + // Assert + Assert.False(result); + Assert.Null(configuredExtensionNames); + } + + [Fact] + public void TryGetConfiguredExtensionNames_SucceedsIfSingleExtension() + { + // Arrange + var expectedExtensionName = "SomeExtensionName"; + var extensions = new Dictionary() + { + [Rules.RazorConfiguration.ExtensionsProperty] = expectedExtensionName + }.ToImmutableDictionary(); + var configurationItem = new ProjectStateItem(Rules.RazorConfiguration.SchemaName, extensions); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames); + + // Assert + Assert.True(result); + var extensionName = Assert.Single(configuredExtensionNames); + Assert.Equal(expectedExtensionName, extensionName); + } + + [Fact] + public void TryGetConfiguredExtensionNames_SucceedsIfMultipleExtensions() + { + // Arrange + var extensions = new Dictionary() + { + [Rules.RazorConfiguration.ExtensionsProperty] = "SomeExtensionName;SomeOtherExtensionName" + }.ToImmutableDictionary(); + var configurationItem = new ProjectStateItem(Rules.RazorConfiguration.SchemaName, extensions); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames); + + // Assert + Assert.True(result); + Assert.Collection( + configuredExtensionNames, + name => Assert.Equal("SomeExtensionName", name), + name => Assert.Equal("SomeOtherExtensionName", name)); + } + + [Fact] + public void TryGetExtensions_NoExtensions() + { + // Arrange + var projectState = new Dictionary().ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetExtensions(new[] { "Extension1", "Extension2" }, projectState, out var extensions); + + // Assert + Assert.False(result); + Assert.Null(extensions); + } + + [Fact] + public void TryGetExtensions_SucceedsWithUnConfiguredExtensionTypes() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorExtension.PrimaryDataSourceItemType] = TestProjectRuleSnapshot.CreateItems( + Rules.RazorExtension.PrimaryDataSourceItemType, + new Dictionary>() + { + ["UnconfiguredExtensionName"] = new Dictionary() + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetExtensions(new[] { "Extension1", "Extension2" }, projectState, out var extensions); + + // Assert + Assert.True(result); + Assert.Empty(extensions); + } + + [Fact] + public void TryGetExtensions_SucceedsWithSomeConfiguredExtensions() + { + // Arrange + var expectedExtension1Name = "Extension1"; + var expectedExtension2Name = "Extension2"; + var projectState = new Dictionary() + { + [Rules.RazorExtension.PrimaryDataSourceItemType] = TestProjectRuleSnapshot.CreateItems( + Rules.RazorExtension.PrimaryDataSourceItemType, + new Dictionary>() + { + ["UnconfiguredExtensionName"] = new Dictionary(), + [expectedExtension1Name] = new Dictionary(), + [expectedExtension2Name] = new Dictionary(), + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetExtensions(new[] { expectedExtension1Name, expectedExtension2Name }, projectState, out var extensions); + + // Assert + Assert.True(result); + Assert.Collection( + extensions, + extension => Assert.Equal(expectedExtension2Name, extension.ExtensionName), + extension => Assert.Equal(expectedExtension1Name, extension.ExtensionName)); + } + + [Fact] + public void TryGetConfiguration_FailsIfNoDefaultConfiguration() + { + // Arrange + var projectState = new Dictionary().ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguration(projectState, out var configuration); + + // Assert + Assert.False(result); + Assert.Null(configuration); + } + + [Fact] + public void TryGetConfiguration_FailsIfNoLanguageVersion() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties( + Rules.RazorGeneral.SchemaName, + new Dictionary() + { + [Rules.RazorGeneral.RazorDefaultConfigurationProperty] = "13.37" + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguration(projectState, out var configuration); + + // Assert + Assert.False(result); + Assert.Null(configuration); + } + + [Fact] + public void TryGetConfiguration_FailsIfNoConfigurationItems() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties( + Rules.RazorGeneral.SchemaName, + new Dictionary() + { + [Rules.RazorGeneral.RazorDefaultConfigurationProperty] = "13.37", + [Rules.RazorGeneral.RazorLangVersionProperty] = "1.0", + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguration(projectState, out var configuration); + + // Assert + Assert.False(result); + Assert.Null(configuration); + } + + [Fact] + public void TryGetConfiguration_FailsIfNoConfiguredExtensionNames() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties( + Rules.RazorGeneral.SchemaName, + new Dictionary() + { + [Rules.RazorGeneral.RazorDefaultConfigurationProperty] = "13.37", + [Rules.RazorGeneral.RazorLangVersionProperty] = "1.0", + }), + [Rules.RazorConfiguration.SchemaName] = TestProjectRuleSnapshot.CreateItems( + Rules.RazorConfiguration.SchemaName, + new Dictionary>() + { + ["Razor-13.37"] = new Dictionary() + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguration(projectState, out var configuration); + + // Assert + Assert.False(result); + Assert.Null(configuration); + } + + [Fact] + public void TryGetConfiguration_FailsIfNoExtensions() + { + // Arrange + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties( + Rules.RazorGeneral.SchemaName, + new Dictionary() + { + [Rules.RazorGeneral.RazorDefaultConfigurationProperty] = "13.37", + [Rules.RazorGeneral.RazorLangVersionProperty] = "1.0", + }), + [Rules.RazorConfiguration.SchemaName] = TestProjectRuleSnapshot.CreateItems( + Rules.RazorConfiguration.SchemaName, + new Dictionary>() + { + ["SomeExtension"] = new Dictionary() + { + ["Extensions"] = "Razor-13.37" + } + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguration(projectState, out var configuration); + + // Assert + Assert.False(result); + Assert.Null(configuration); + } + + // This is more of an integration test but is here to test the overall flow/functionality + [Fact] + public void TryGetConfiguration_SucceedsWithAllPreRequisites() + { + // Arrange + var expectedLanguageVersion = RazorLanguageVersion.Version_1_0; + var expectedConfigurationName = "Razor-Test"; + var expectedExtension1Name = "Extension1"; + var expectedExtension2Name = "Extension2"; + var projectState = new Dictionary() + { + [Rules.RazorGeneral.SchemaName] = TestProjectRuleSnapshot.CreateProperties( + Rules.RazorGeneral.SchemaName, + new Dictionary() + { + [Rules.RazorGeneral.RazorDefaultConfigurationProperty] = expectedConfigurationName, + [Rules.RazorGeneral.RazorLangVersionProperty] = "1.0", + }), + [Rules.RazorConfiguration.SchemaName] = TestProjectRuleSnapshot.CreateItems( + Rules.RazorConfiguration.SchemaName, + new Dictionary>() + { + ["UnconfiguredRazorConfiguration"] = new Dictionary() + { + ["Extensions"] = "Razor-9.0" + }, + [expectedConfigurationName] = new Dictionary() + { + ["Extensions"] = expectedExtension1Name + ";" + expectedExtension2Name + } + }), + [Rules.RazorExtension.PrimaryDataSourceItemType] = TestProjectRuleSnapshot.CreateItems( + Rules.RazorExtension.PrimaryDataSourceItemType, + new Dictionary>() + { + [expectedExtension1Name] = new Dictionary(), + [expectedExtension2Name] = new Dictionary(), + }) + }.ToImmutableDictionary(); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguration(projectState, out var configuration); + + // Assert + Assert.True(result); + Assert.Equal(expectedLanguageVersion, configuration.LanguageVersion); + Assert.Equal(expectedConfigurationName, configuration.ConfigurationName); + Assert.Collection( + configuration.Extensions, + extension => Assert.Equal(expectedExtension2Name, extension.ExtensionName), + extension => Assert.Equal(expectedExtension1Name, extension.ExtensionName)); + } + [ForegroundFact] public async Task DefaultRazorProjectHost_ForegroundThread_CreateAndDispose_Succeeds() { @@ -232,8 +801,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem 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)); + e => Assert.Equal("Another-Thing", e.ExtensionName), + e => Assert.Equal("MVC-2.0", e.ExtensionName)); await Task.Run(async () => await host.DisposeAsync()); Assert.Empty(ProjectManager.Projects); @@ -443,7 +1012,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private class TestProjectSnapshotManager : DefaultProjectSnapshotManager { - public TestProjectSnapshotManager(ForegroundDispatcher dispatcher, Workspace workspace) + public TestProjectSnapshotManager(ForegroundDispatcher dispatcher, Workspace workspace) : base(dispatcher, Mock.Of(), Mock.Of(), Array.Empty(), workspace) { } diff --git a/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultDotNetProjectHostTest.cs b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultDotNetProjectHostTest.cs new file mode 100644 index 0000000000..b2a55f7cf4 --- /dev/null +++ b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultDotNetProjectHostTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.VisualStudio.Editor.Razor; +using Moq; +using Xunit; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem +{ + public class DefaultDotNetProjectHostTest : ForegroundDispatcherTestBase + { + [Fact] + public void UpdateRazorHostProject_UnsupportedProjectNoops() + { + // Arrange + var projectService = new Mock(); + projectService.Setup(p => p.IsSupportedProject(It.IsAny())) + .Returns(false); + var dotNetProjectHost = new DefaultDotNetProjectHost( + Dispatcher, + Mock.Of(), + projectService.Object); + + // Act & Assert + dotNetProjectHost.UpdateRazorHostProject(); + } + + // ------------------------------------------------------------------------------------------- + // Purposefully do not have any more tests here because that would involve mocking MonoDevelop + // types. The default constructors for the Solution / DotNetProject MonoDevelop types change + // static classes (they assume they're being created in an IDE). + // ------------------------------------------------------------------------------------------- + } +} diff --git a/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultRazorProjectHostTest.cs b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultRazorProjectHostTest.cs new file mode 100644 index 0000000000..4a02ccad17 --- /dev/null +++ b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultRazorProjectHostTest.cs @@ -0,0 +1,470 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using MonoDevelop.Projects.MSBuild; +using Xunit; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem +{ + public class DefaultRazorProjectHostTest + { + [Fact] + public void TryGetDefaultConfiguration_FailsIfNoConfiguration() + { + // Arrange + var projectProperties = new MSBuildPropertyGroup(); + + // Act + var result = DefaultRazorProjectHost.TryGetDefaultConfiguration(projectProperties, out var defaultConfiguration); + + // Assert + Assert.False(result); + Assert.Null(defaultConfiguration); + } + + [Fact] + public void TryGetDefaultConfiguration_FailsIfEmptyConfiguration() + { + // Arrange + var projectProperties = new MSBuildPropertyGroup(); + projectProperties.SetValue("RazorDefaultConfiguration", string.Empty); + + // Act + var result = DefaultRazorProjectHost.TryGetDefaultConfiguration(projectProperties, out var defaultConfiguration); + + // Assert + Assert.False(result); + Assert.Null(defaultConfiguration); + } + + [Fact] + public void TryGetDefaultConfiguration_SucceedsWithValidConfiguration() + { + // Arrange + var expectedConfiguration = "Razor-13.37"; + var projectProperties = new MSBuildPropertyGroup(); + projectProperties.SetValue("RazorDefaultConfiguration", expectedConfiguration); + + // Act + var result = DefaultRazorProjectHost.TryGetDefaultConfiguration(projectProperties, out var defaultConfiguration); + + // Assert + Assert.True(result); + Assert.Equal(expectedConfiguration, defaultConfiguration); + } + + [Fact] + public void TryGetLanguageVersion_FailsIfNoLanguageVersion() + { + // Arrange + var projectProperties = new MSBuildPropertyGroup(); + + // Act + var result = DefaultRazorProjectHost.TryGetLanguageVersion(projectProperties, out var languageVersion); + + // Assert + Assert.False(result); + Assert.Null(languageVersion); + } + + [Fact] + public void TryGetLanguageVersion_FailsIfEmptyLanguageVersion() + { + // Arrange + var projectProperties = new MSBuildPropertyGroup(); + projectProperties.SetValue("RazorLangVersion", string.Empty); + + // Act + var result = DefaultRazorProjectHost.TryGetLanguageVersion(projectProperties, out var languageVersion); + + // Assert + Assert.False(result); + Assert.Null(languageVersion); + } + + [Fact] + public void TryGetLanguageVersion_SucceedsWithValidLanguageVersion() + { + // Arrange + var projectProperties = new MSBuildPropertyGroup(); + projectProperties.SetValue("RazorLangVersion", "1.0"); + + // Act + var result = DefaultRazorProjectHost.TryGetLanguageVersion(projectProperties, out var languageVersion); + + // Assert + Assert.True(result); + Assert.Same(RazorLanguageVersion.Version_1_0, languageVersion); + } + + [Fact] + public void TryGetLanguageVersion_SucceedsWithUnknownLanguageVersion_DefaultsToLatest() + { + // Arrange + var projectProperties = new MSBuildPropertyGroup(); + projectProperties.SetValue("RazorLangVersion", "13.37"); + + // Act + var result = DefaultRazorProjectHost.TryGetLanguageVersion(projectProperties, out var languageVersion); + + // Assert + Assert.True(result); + Assert.Same(RazorLanguageVersion.Latest, languageVersion); + } + + [Fact] + public void TryGetConfigurationItem_FailsNoRazorConfigurationItems() + { + // Arrange + var projectItems = Enumerable.Empty(); + + // Act + var result = DefaultRazorProjectHost.TryGetConfigurationItem("Razor-13.37", projectItems, out var configurationItem); + + // Assert + Assert.False(result); + Assert.Null(configurationItem); + } + + [Fact] + public void TryGetConfigurationItem_FailsNoMatchingRazorConfigurationItems() + { + // Arrange + var projectItems = new IMSBuildItemEvaluated[] + { + new TestMSBuildItem("RazorConfiguration") + { + Include = "Razor-10.0", + } + }; + + // Act + var result = DefaultRazorProjectHost.TryGetConfigurationItem("Razor-13.37", projectItems, out var configurationItem); + + // Assert + Assert.False(result); + Assert.Null(configurationItem); + } + + [Fact] + public void TryGetConfigurationItem_SucceedsForMatchingConfigurationItem() + { + // Arrange + var expectedConfiguration = "Razor-13.37"; + var expectedConfigurationItem = new TestMSBuildItem("RazorConfiguration") + { + Include = expectedConfiguration, + }; + var projectItems = new IMSBuildItemEvaluated[] + { + new TestMSBuildItem("RazorConfiguration") + { + Include = "Razor-10.0-DoesNotMatch", + }, + expectedConfigurationItem + }; + + // Act + var result = DefaultRazorProjectHost.TryGetConfigurationItem(expectedConfiguration, projectItems, out var configurationItem); + + // Assert + Assert.True(result); + Assert.Same(expectedConfigurationItem, configurationItem); + } + + [Fact] + public void TryGetConfiguredExtensionNames_FailsIfNoExtensions() + { + // Arrange + var configurationItem = new TestMSBuildItem("RazorConfiguration"); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionnames); + + // Assert + Assert.False(result); + Assert.Null(configuredExtensionnames); + } + + [Fact] + public void TryGetConfiguredExtensionNames_FailsIfEmptyExtensions() + { + // Arrange + var configurationItem = new TestMSBuildItem("RazorConfiguration"); + configurationItem.TestMetadata.SetValue("Extensions", string.Empty); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames); + + // Assert + Assert.False(result); + Assert.Null(configuredExtensionNames); + } + + [Fact] + public void TryGetConfiguredExtensionNames_SucceedsIfSingleExtension() + { + // Arrange + var expectedExtensionName = "SomeExtensionName"; + var configurationItem = new TestMSBuildItem("RazorConfiguration"); + configurationItem.TestMetadata.SetValue("Extensions", expectedExtensionName); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames); + + // Assert + Assert.True(result); + var extensionName = Assert.Single(configuredExtensionNames); + Assert.Equal(expectedExtensionName, extensionName); + } + + [Fact] + public void TryGetConfiguredExtensionNames_SucceedsIfMultipleExtensions() + { + // Arrange + var configurationItem = new TestMSBuildItem("RazorConfiguration"); + configurationItem.TestMetadata.SetValue("Extensions", "SomeExtensionName;SomeOtherExtensionName"); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames); + + // Assert + Assert.True(result); + Assert.Collection( + configuredExtensionNames, + name => Assert.Equal("SomeExtensionName", name), + name => Assert.Equal("SomeOtherExtensionName", name)); + } + + [Fact] + public void GetExtensions_NoExtensionTypes_ReturnsEmptyArray() + { + // Arrange + var projectItems = new IMSBuildItemEvaluated[] + { + new TestMSBuildItem("NotAnExtension") + { + Include = "Extension1", + }, + }; + + // Act + var extensions = DefaultRazorProjectHost.GetExtensions(new[] { "Extension1", "Extension2" }, projectItems); + + // Assert + Assert.Empty(extensions); + } + + [Fact] + public void GetExtensions_UnConfiguredExtensionTypes_ReturnsEmptyArray() + { + // Arrange + var projectItems = new IMSBuildItemEvaluated[] + { + new TestMSBuildItem("RazorExtension") + { + Include = "UnconfiguredExtensionName", + }, + }; + + // Act + var extensions = DefaultRazorProjectHost.GetExtensions(new[] { "Extension1", "Extension2" }, projectItems); + + // Assert + Assert.Empty(extensions); + } + + [Fact] + public void GetExtensions_SomeConfiguredExtensions_ReturnsConfiguredExtensions() + { + // Arrange + var expectedExtension1Name = "Extension1"; + var expectedExtension2Name = "Extension2"; + var projectItems = new IMSBuildItemEvaluated[] + { + new TestMSBuildItem("RazorExtension") + { + Include = "UnconfiguredExtensionName", + }, + new TestMSBuildItem("RazorExtension") + { + Include = expectedExtension1Name, + }, + new TestMSBuildItem("RazorExtension") + { + Include = expectedExtension2Name, + }, + }; + + // Act + var extensions = DefaultRazorProjectHost.GetExtensions(new[] { expectedExtension1Name, expectedExtension2Name }, projectItems); + + // Assert + Assert.Collection( + extensions, + extension => Assert.Equal(expectedExtension1Name, extension.ExtensionName), + extension => Assert.Equal(expectedExtension2Name, extension.ExtensionName)); + } + + [Fact] + public void TryGetConfiguration_FailsIfNoDefaultConfiguration() + { + // Arrange + var projectProperties = new MSBuildPropertyGroup(); + var projectItems = new IMSBuildItemEvaluated[0]; + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguration(projectProperties, projectItems, out var configuration); + + // Assert + Assert.False(result); + Assert.Null(configuration); + } + + [Fact] + public void TryGetConfiguration_FailsIfNoLanguageVersion() + { + // Arrange + var projectProperties = new MSBuildPropertyGroup(); + projectProperties.SetValue("RazorDefaultConfiguration", "Razor-13.37"); + var projectItems = new IMSBuildItemEvaluated[0]; + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguration(projectProperties, projectItems, out var configuration); + + // Assert + Assert.False(result); + Assert.Null(configuration); + } + + [Fact] + public void TryGetConfiguration_FailsIfNoConfigurationItems() + { + // Arrange + var projectProperties = new MSBuildPropertyGroup(); + projectProperties.SetValue("RazorDefaultConfiguration", "Razor-13.37"); + projectProperties.SetValue("RazorLangVersion", "1.0"); + var projectItems = new IMSBuildItemEvaluated[0]; + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguration(projectProperties, projectItems, out var configuration); + + // Assert + Assert.False(result); + Assert.Null(configuration); + } + + [Fact] + public void TryGetConfiguration_FailsIfNoConfiguredExtensionNames() + { + // Arrange + var projectProperties = new MSBuildPropertyGroup(); + projectProperties.SetValue("RazorDefaultConfiguration", "Razor-13.37"); + projectProperties.SetValue("RazorLangVersion", "1.0"); + var projectItems = new IMSBuildItemEvaluated[] + { + new TestMSBuildItem("RazorConfiguration") + { + Include = "Razor-13.37", + }, + }; + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguration(projectProperties, projectItems, out var configuration); + + // Assert + Assert.False(result); + Assert.Null(configuration); + } + + // This is more of an integration test but is here to test the overall flow/functionality + [Fact] + public void TryGetConfiguration_SucceedsWithAllPreRequisites() + { + // Arrange + var expectedLanguageVersion = RazorLanguageVersion.Version_1_0; + var expectedConfigurationName = "Razor-Test"; + var expectedExtension1Name = "Extension1"; + var expectedExtension2Name = "Extension2"; + var expectedRazorConfigurationItem = new TestMSBuildItem("RazorConfiguration") + { + Include = expectedConfigurationName, + }; + expectedRazorConfigurationItem.TestMetadata.SetValue("Extensions", "Extension1;Extension2"); + var projectItems = new IMSBuildItemEvaluated[] + { + new TestMSBuildItem("RazorConfiguration") + { + Include = "UnconfiguredRazorConfiguration", + }, + new TestMSBuildItem("RazorExtension") + { + Include = "UnconfiguredExtensionName", + }, + new TestMSBuildItem("RazorExtension") + { + Include = expectedExtension1Name, + }, + new TestMSBuildItem("RazorExtension") + { + Include = expectedExtension2Name, + }, + expectedRazorConfigurationItem, + }; + var projectProperties = new MSBuildPropertyGroup(); + projectProperties.SetValue("RazorDefaultConfiguration", expectedConfigurationName); + projectProperties.SetValue("RazorLangVersion", "1.0"); + + // Act + var result = DefaultRazorProjectHost.TryGetConfiguration(projectProperties, projectItems, out var configuration); + + // Assert + Assert.True(result); + Assert.Equal(expectedLanguageVersion, configuration.LanguageVersion); + Assert.Equal(expectedConfigurationName, configuration.ConfigurationName); + Assert.Collection( + configuration.Extensions, + extension => Assert.Equal(expectedExtension1Name, extension.ExtensionName), + extension => Assert.Equal(expectedExtension2Name, extension.ExtensionName)); + } + + private class TestMSBuildItem : IMSBuildItemEvaluated + { + private readonly MSBuildPropertyGroup _metadata; + private readonly string _name; + private string _include; + + public TestMSBuildItem(string name) + { + _name = name; + _metadata = new MSBuildPropertyGroup(); + } + + public string Name => _name; + + public string Include + { + get => _include; + set => _include = value; + } + + public MSBuildPropertyGroup TestMetadata => _metadata; + + public IMSBuildPropertyGroupEvaluated Metadata => _metadata; + + public string Condition => throw new System.NotImplementedException(); + + public bool IsImported => throw new System.NotImplementedException(); + + + public string UnevaluatedInclude => throw new System.NotImplementedException(); + + public MSBuildItem SourceItem => throw new System.NotImplementedException(); + + public IEnumerable SourceItems => throw new System.NotImplementedException(); + } + } +} diff --git a/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultVisualStudioWorkspaceAccessorTest.cs b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultVisualStudioMacWorkspaceAccessorTest.cs similarity index 86% rename from test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultVisualStudioWorkspaceAccessorTest.cs rename to test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultVisualStudioMacWorkspaceAccessorTest.cs index 551c25c6e8..bafd757836 100644 --- a/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultVisualStudioWorkspaceAccessorTest.cs +++ b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/DefaultVisualStudioMacWorkspaceAccessorTest.cs @@ -8,13 +8,13 @@ using Xunit; namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor { - public class DefaultVisualStudioWorkspaceAccessorTest + public class DefaultVisualStudioMacWorkspaceAccessorTest { [Fact] public void TryGetWorkspace_NoHostProject_ReturnsFalse() { // Arrange - var workspaceAccessor = new DefaultVisualStudioWorkspaceAccessor(Mock.Of()); + var workspaceAccessor = new DefaultVisualStudioMacWorkspaceAccessor(Mock.Of()); var textBuffer = Mock.Of(); // Act diff --git a/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/FallbackRazorProjectHostTest.cs b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/FallbackRazorProjectHostTest.cs new file mode 100644 index 0000000000..99bc334438 --- /dev/null +++ b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/FallbackRazorProjectHostTest.cs @@ -0,0 +1,62 @@ +// 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 MonoDevelop.Core; +using MonoDevelop.Projects; +using Xunit; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem +{ + public class FallbackRazorProjectHostTest + { + [Theory] + [InlineData(null)] + [InlineData("")] + public void IsMvcAssembly_FailsIfNullOrEmptyFilePath(string filePath) + { + // Arrange + var assemblyFilePath = new FilePath(filePath); + var assemblyReference = new AssemblyReference(assemblyFilePath); + + // Act + var result = FallbackRazorProjectHost.IsMvcAssembly(assemblyReference); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsMvcAssembly_FailsIfNotMvc() + { + // Arrange + var assemblyFilePath = new FilePath("C:/Path/To/Assembly.dll"); + var assemblyReference = new AssemblyReference(assemblyFilePath); + + // Act + var result = FallbackRazorProjectHost.IsMvcAssembly(assemblyReference); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsMvcAssembly_SucceedsIfMvc() + { + // Arrange + var assemblyFilePath = new FilePath("C:/Path/To/Microsoft.AspNetCore.Mvc.Razor.dll"); + var assemblyReference = new AssemblyReference(assemblyFilePath); + + // Act + var result = FallbackRazorProjectHost.IsMvcAssembly(assemblyReference); + + // Assert + Assert.True(result); + } + + // ------------------------------------------------------------------------------------------- + // Purposefully do not have any more tests here because that would involve mocking MonoDevelop + // types. The default constructors for the Solution / DotNetProject MonoDevelop types change + // static classes (they assume they're being created in an IDE). + // ------------------------------------------------------------------------------------------- + } +} diff --git a/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test.csproj b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test.csproj index 4cf489bc13..3ab920e61c 100644 --- a/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test.csproj +++ b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test.csproj @@ -4,6 +4,10 @@ net461 + + + + diff --git a/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/xunit.runner.json b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/xunit.runner.json new file mode 100644 index 0000000000..c04bb61fe6 --- /dev/null +++ b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "methodDisplay": "method", + "shadowCopy": false +} \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.Mac.RazorAddin/Microsoft.VisualStudio.Mac.RazorAddin.csproj b/tooling/Microsoft.VisualStudio.Mac.RazorAddin/Microsoft.VisualStudio.Mac.RazorAddin.csproj index e7e41ae731..9f6462e3a7 100644 --- a/tooling/Microsoft.VisualStudio.Mac.RazorAddin/Microsoft.VisualStudio.Mac.RazorAddin.csproj +++ b/tooling/Microsoft.VisualStudio.Mac.RazorAddin/Microsoft.VisualStudio.Mac.RazorAddin.csproj @@ -42,15 +42,6 @@ - - - false - true - Content - Build - + diff --git a/tooling/Microsoft.VisualStudio.Mac.RazorAddin/Properties/_Manifest.addin.xml b/tooling/Microsoft.VisualStudio.Mac.RazorAddin/Properties/_Manifest.addin.xml index 3d47fdc295..0c81e6caf6 100644 --- a/tooling/Microsoft.VisualStudio.Mac.RazorAddin/Properties/_Manifest.addin.xml +++ b/tooling/Microsoft.VisualStudio.Mac.RazorAddin/Properties/_Manifest.addin.xml @@ -19,5 +19,10 @@ - + + + + + + diff --git a/tooling/Microsoft.VisualStudio.Mac.RazorAddin/RazorProjectExtension.cs b/tooling/Microsoft.VisualStudio.Mac.RazorAddin/RazorProjectExtension.cs new file mode 100644 index 0000000000..2c69a1d298 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.Mac.RazorAddin/RazorProjectExtension.cs @@ -0,0 +1,59 @@ +// 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.CodeAnalysis.Razor; +using Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem; +using MonoDevelop.Ide.Composition; +using MonoDevelop.Ide.TypeSystem; +using MonoDevelop.Projects; + +namespace Microsoft.VisualStudio.Mac.RazorAddin +{ + internal class RazorProjectExtension : ProjectExtension + { + private readonly object _lock = new object(); + private readonly ForegroundDispatcher _foregroundDispatcher; + + public RazorProjectExtension() + { + _foregroundDispatcher = CompositionManager.GetExportedValue(); + } + + protected override void OnBoundToSolution() + { + if (!(Project is DotNetProject dotNetProject)) + { + return; + } + + DotNetProjectHost projectHost; + lock (_lock) + { + if (Project.ExtendedProperties.Contains(typeof(DotNetProjectHost))) + { + // Already have a project host. + return; + } + + var projectHostFactory = CompositionManager.GetExportedValue(); + projectHost = projectHostFactory.Create(dotNetProject); + Project.ExtendedProperties[typeof(DotNetProjectHost)] = projectHost; + } + + // Once a workspace is created for the solution we'll setup our project host for the current project. The Razor world + // shares a lifetime with the workspace (as Roslyn services) so we need to ensure it exists prior to wiring the host + // world to the Roslyn world. + TypeSystemService.GetWorkspaceAsync(Project.ParentSolution).ContinueWith(task => + { + if (task.IsFaulted || task.IsCanceled) + { + // We only want to act if we could properly retrieve the workspace. + return; + } + + projectHost.Subscribe(); + }, + _foregroundDispatcher.ForegroundScheduler); + } + } +}