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