Add support for external project config files in VS4Mac. (#6181)

* Add support for external project config files in VS4Mac.

- Added the ability to extract the intermediate output path in VS4Mac scenarios which is in turn used to inform the language server on where project configuration files (project.razor.vs.json) exist when they're outside of the normal workspace directory.
- Updated test(s).

Part of #6038

* Address code review comments
This commit is contained in:
N. Taylor Mullen 2022-03-18 15:07:16 -07:00 коммит произвёл GitHub
Родитель a1c08e6bfc
Коммит d64b1e93ff
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 207 добавлений и 18 удалений

Просмотреть файл

@ -121,8 +121,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
if (TryGetIntermediateOutputPath(update.Value.CurrentState, out var intermediatePath))
{
var projectRazorJson = Path.Combine(intermediatePath, _languageServerFeatureOptions.ProjectConfigurationFileName);
ProjectConfigurationFilePathStore.Set(hostProject.FilePath, projectRazorJson);
var projectConfigurationFile = Path.Combine(intermediatePath, _languageServerFeatureOptions.ProjectConfigurationFileName);
ProjectConfigurationFilePathStore.Set(hostProject.FilePath, projectConfigurationFile);
}
UpdateProjectUnsafe(hostProject);

Просмотреть файл

@ -147,8 +147,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
if (TryGetIntermediateOutputPath(update.Value.CurrentState, out var intermediatePath))
{
var projectRazorJson = Path.Combine(intermediatePath, _languageServerFeatureOptions.ProjectConfigurationFileName);
ProjectConfigurationFilePathStore.Set(hostProject.FilePath, projectRazorJson);
var projectConfigurationFile = Path.Combine(intermediatePath, _languageServerFeatureOptions.ProjectConfigurationFileName);
ProjectConfigurationFilePathStore.Set(hostProject.FilePath, projectConfigurationFile);
}
UpdateProjectUnsafe(hostProject);

Просмотреть файл

@ -46,7 +46,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
IUnconfiguredProjectCommonServices commonServices!!,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace!!,
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher!!,
ProjectConfigurationFilePathStore projectConfigurationFilePathStore)
ProjectConfigurationFilePathStore projectConfigurationFilePathStore!!)
: base(commonServices.ThreadingService.JoinableTaskContext)
{
CommonServices = commonServices;

Просмотреть файл

@ -20,29 +20,39 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher;
private readonly VisualStudioMacWorkspaceAccessor _workspaceAccessor;
private readonly TextBufferProjectService _projectService;
private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore;
private readonly VSLanguageServerFeatureOptions _languageServerFeatureOptions;
private MacRazorProjectHostBase _razorProjectHost;
public DefaultDotNetProjectHost(
DotNetProject project!!,
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher!!,
VisualStudioMacWorkspaceAccessor workspaceAccessor!!,
TextBufferProjectService projectService!!)
TextBufferProjectService projectService!!,
ProjectConfigurationFilePathStore projectConfigurationFilePathStore!!,
VSLanguageServerFeatureOptions languageServerFeatureOptions!!)
{
_project = project;
_projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher;
_workspaceAccessor = workspaceAccessor;
_projectService = projectService;
_projectConfigurationFilePathStore = projectConfigurationFilePathStore;
_languageServerFeatureOptions = languageServerFeatureOptions;
}
// Internal for testing
internal DefaultDotNetProjectHost(
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher!!,
VisualStudioMacWorkspaceAccessor workspaceAccessor!!,
TextBufferProjectService projectService!!)
TextBufferProjectService projectService!!,
ProjectConfigurationFilePathStore projectConfigurationFilePathStore!!,
VSLanguageServerFeatureOptions languageServerFeatureOptions!!)
{
_projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher;
_workspaceAccessor = workspaceAccessor;
_projectService = projectService;
_projectConfigurationFilePathStore = projectConfigurationFilePathStore;
_languageServerFeatureOptions = languageServerFeatureOptions;
}
public override DotNetProject Project => _project;
@ -87,12 +97,12 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
if (_project.IsCapabilityMatch(ExplicitRazorConfigurationCapability))
{
// SDK >= 2.1
_razorProjectHost = new DefaultMacRazorProjectHost(_project, _projectSnapshotManagerDispatcher, projectSnapshotManager);
_razorProjectHost = new DefaultMacRazorProjectHost(_project, _projectSnapshotManagerDispatcher, projectSnapshotManager, _projectConfigurationFilePathStore, _languageServerFeatureOptions);
return;
}
// We're an older version of Razor at this point, SDK < 2.1
_razorProjectHost = new FallbackMacRazorProjectHost(_project, _projectSnapshotManagerDispatcher, projectSnapshotManager);
_razorProjectHost = new FallbackMacRazorProjectHost(_project, _projectSnapshotManagerDispatcher, projectSnapshotManager, _projectConfigurationFilePathStore, _languageServerFeatureOptions);
}, CancellationToken.None);
}

Просмотреть файл

@ -13,6 +13,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using MonoDevelop.Projects;
using MonoDevelop.Projects.MSBuild;
@ -27,15 +28,18 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
private const string RazorConfigurationItemTypeExtensionsProperty = "Extensions";
private const string RootNamespaceProperty = "RootNamespace";
private const string ContentItemType = "Content";
private readonly VSLanguageServerFeatureOptions _languageServerFeatureOptions;
private IReadOnlyList<string> _currentRazorFilePaths = Array.Empty<string>();
public DefaultMacRazorProjectHost(
DotNetProject project,
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher,
ProjectSnapshotManagerBase projectSnapshotManager)
: base(project, projectSnapshotManagerDispatcher, projectSnapshotManager)
ProjectSnapshotManagerBase projectSnapshotManager,
ProjectConfigurationFilePathStore projectConfigurationFilePathStore,
VSLanguageServerFeatureOptions languageServerFeatureOptions)
: base(project, projectSnapshotManagerDispatcher, projectSnapshotManager, projectConfigurationFilePathStore)
{
_languageServerFeatureOptions = languageServerFeatureOptions;
}
protected override async Task OnProjectChangedAsync()
@ -45,6 +49,12 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
var projectProperties = DotNetProject.MSBuildProject.EvaluatedProperties;
var projectItems = DotNetProject.MSBuildProject.EvaluatedItems;
if (TryGetIntermediateOutputPath(projectProperties, out var intermediatePath))
{
var projectConfigurationFile = Path.Combine(intermediatePath, _languageServerFeatureOptions.ProjectConfigurationFileName);
ProjectConfigurationFilePathStore.Set(DotNetProject.FileName.FullPath, projectConfigurationFile);
}
if (TryGetConfiguration(projectProperties, projectItems, out var configuration))
{
TryGetRootNamespace(projectProperties, out var rootNamespace);

Просмотреть файл

@ -6,6 +6,7 @@
using System;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using MonoDevelop.Projects;
@ -18,21 +19,27 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher;
private readonly VisualStudioMacWorkspaceAccessor _workspaceAccessor;
private readonly TextBufferProjectService _projectService;
private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore;
private readonly VSLanguageServerFeatureOptions _languageServerFeatureOptions;
[ImportingConstructor]
public DotNetProjectHostFactory(
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher!!,
VisualStudioMacWorkspaceAccessor workspaceAccessor!!,
TextBufferProjectService projectService!!)
TextBufferProjectService projectService!!,
ProjectConfigurationFilePathStore projectConfigurationFilePathStore!!,
VSLanguageServerFeatureOptions languageServerFeatureOptions!!)
{
_projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher;
_workspaceAccessor = workspaceAccessor;
_projectService = projectService;
_projectConfigurationFilePathStore = projectConfigurationFilePathStore;
_languageServerFeatureOptions = languageServerFeatureOptions;
}
public DotNetProjectHost Create(DotNetProject project!!)
{
var projectHost = new DefaultDotNetProjectHost(project, _projectSnapshotManagerDispatcher, _workspaceAccessor, _projectService);
var projectHost = new DefaultDotNetProjectHost(project, _projectSnapshotManagerDispatcher, _workspaceAccessor, _projectService, _projectConfigurationFilePathStore, _languageServerFeatureOptions);
return projectHost;
}
}

Просмотреть файл

@ -11,6 +11,7 @@ using System.Reflection.PortableExecutable;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using MonoDevelop.Projects;
using AssemblyReference = MonoDevelop.Projects.AssemblyReference;
@ -19,13 +20,17 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
internal class FallbackMacRazorProjectHost : MacRazorProjectHostBase
{
private const string MvcAssemblyFileName = "Microsoft.AspNetCore.Mvc.Razor.dll";
private readonly VSLanguageServerFeatureOptions _languageServerFeatureOptions;
public FallbackMacRazorProjectHost(
DotNetProject project,
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher,
ProjectSnapshotManagerBase projectSnapshotManager)
: base(project, projectSnapshotManagerDispatcher, projectSnapshotManager)
ProjectSnapshotManagerBase projectSnapshotManager,
ProjectConfigurationFilePathStore projectConfigurationFilePathStore,
VSLanguageServerFeatureOptions languageServerFeatureOptions)
: base(project, projectSnapshotManagerDispatcher, projectSnapshotManager, projectConfigurationFilePathStore)
{
_languageServerFeatureOptions = languageServerFeatureOptions;
}
protected override async Task OnProjectChangedAsync()
@ -34,6 +39,13 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
{
var referencedAssemblies = await DotNetProject.GetReferencedAssemblies(ConfigurationSelector.Default);
var mvcReference = referencedAssemblies.FirstOrDefault(IsMvcAssembly);
var projectProperties = DotNetProject.MSBuildProject.EvaluatedProperties;
if (TryGetIntermediateOutputPath(projectProperties, out var intermediatePath))
{
var projectConfigurationFile = Path.Combine(intermediatePath, _languageServerFeatureOptions.ProjectConfigurationFileName);
ProjectConfigurationFilePathStore.Set(DotNetProject.FileName.FullPath, projectConfigurationFile);
}
if (mvcReference is null)
{

Просмотреть файл

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -13,6 +14,7 @@ using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Threading;
using MonoDevelop.Projects;
using MonoDevelop.Projects.MSBuild;
namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
{
@ -20,8 +22,12 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
{
// References changes are always triggered when project changes happen.
private const string ProjectChangedHint = "References";
private const string BaseIntermediateOutputPathPropertyName = "BaseIntermediateOutputPath";
private const string IntermediateOutputPathPropertyName = "IntermediateOutputPath";
private const string MSBuildProjectDirectoryPropertyName = "MSBuildProjectDirectory";
private bool _batchingProjectChanges;
protected readonly ProjectConfigurationFilePathStore ProjectConfigurationFilePathStore;
private readonly ProjectSnapshotManagerBase _projectSnapshotManager;
private readonly AsyncSemaphore _onProjectChangedInnerSemaphore;
private readonly AsyncSemaphore _projectChangedSemaphore;
@ -30,11 +36,13 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
public MacRazorProjectHostBase(
DotNetProject project!!,
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher!!,
ProjectSnapshotManagerBase projectSnapshotManager!!)
ProjectSnapshotManagerBase projectSnapshotManager!!,
ProjectConfigurationFilePathStore projectConfigurationFilePathStore!!)
{
DotNetProject = project;
ProjectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher;
_projectSnapshotManager = projectSnapshotManager;
ProjectConfigurationFilePathStore = projectConfigurationFilePathStore;
_onProjectChangedInnerSemaphore = new AsyncSemaphore(initialCount: 1);
_projectChangedSemaphore = new AsyncSemaphore(initialCount: 1);
_currentDocuments = new Dictionary<string, HostDocument>(FilePathComparer.Instance);
@ -138,6 +146,7 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
else if (HostProject != null && newHostProject is null)
{
_projectSnapshotManager.ProjectRemoved(HostProject);
ProjectConfigurationFilePathStore.Remove(HostProject.FilePath);
}
else
{
@ -170,5 +179,81 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
_currentDocuments.Remove(filePath);
}
}
// Internal for testing
internal static bool TryGetIntermediateOutputPath(
IMSBuildEvaluatedPropertyCollection projectProperties,
out string path)
{
if (!projectProperties.HasProperty(BaseIntermediateOutputPathPropertyName))
{
path = null;
return false;
}
if (!projectProperties.HasProperty(IntermediateOutputPathPropertyName))
{
path = null;
return false;
}
var baseIntermediateOutputPathValue = projectProperties.GetValue(BaseIntermediateOutputPathPropertyName);
if (string.IsNullOrEmpty(baseIntermediateOutputPathValue))
{
path = null;
return false;
}
var intermediateOutputPathValue = projectProperties.GetValue(IntermediateOutputPathPropertyName);
if (string.IsNullOrEmpty(intermediateOutputPathValue))
{
path = null;
return false;
}
var basePath = new DirectoryInfo(baseIntermediateOutputPathValue).Parent;
var joinedPath = Path.Combine(basePath.FullName, intermediateOutputPathValue);
if (!Directory.Exists(joinedPath))
{
// The directory doesn't exist for the currently executing application.
// This can occur in Razor class library scenarios because:
// 1. Razor class libraries base intermediate path is not absolute. Meaning instead of C:/project/obj it returns /obj.
// 2. Our `new DirectoryInfo(...).Parent` call above is forgiving so if the path passed to it isn't absolute (Razor class library scenario) it utilizes Directory.GetCurrentDirectory where
// in this case would be the C:/Windows/System path
// Because of the above two issues the joinedPath ends up looking like "C:\WINDOWS\system32\obj\Debug\netstandard2.0\" which doesn't actually exist and of course isn't writeable. The end-user effect of this
// quirk means that you don't get any component completions for Razor class libraries because we're unable to capture their project state information.
//
// To workaround these inconsistencies with Razor class libraries we fall back to the MSBuildProjectDirectory and build what we think is the intermediate output path.
joinedPath = ResolveFallbackIntermediateOutputPath(projectProperties, intermediateOutputPathValue);
if (joinedPath is null)
{
// Still couldn't resolve a valid directory.
path = null;
return false;
}
}
path = joinedPath;
return true;
}
private static string ResolveFallbackIntermediateOutputPath(IMSBuildEvaluatedPropertyCollection projectProperties, string intermediateOutputPathValue)
{
if (!projectProperties.HasProperty(MSBuildProjectDirectoryPropertyName))
{
// Can't resolve the project, bail.
return null;
}
var projectDirectory = projectProperties.GetValue(MSBuildProjectDirectoryPropertyName);
var joinedPath = Path.Combine(projectDirectory, intermediateOutputPathValue);
if (!Directory.Exists(joinedPath))
{
return null;
}
return joinedPath;
}
}
}

Просмотреть файл

@ -21,7 +21,9 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
var dotNetProjectHost = new DefaultDotNetProjectHost(
Dispatcher,
Mock.Of<VisualStudioMacWorkspaceAccessor>(MockBehavior.Strict),
projectService.Object);
projectService.Object,
TestProjectConfigurationFilePathStore.Instance,
TestVSLanguageServerFeatureOptions.Instance);
// Act & Assert
dotNetProjectHost.UpdateRazorHostProject();

Просмотреть файл

@ -0,0 +1,41 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
{
internal class TestProjectConfigurationFilePathStore : ProjectConfigurationFilePathStore
{
public static readonly TestProjectConfigurationFilePathStore Instance = new();
private TestProjectConfigurationFilePathStore()
{
}
public override event EventHandler<ProjectConfigurationFilePathChangedEventArgs>? Changed;
public override IReadOnlyDictionary<string, string> GetMappings()
{
throw new NotImplementedException();
}
public override void Remove(string projectFilePath)
{
Changed?.Invoke(this, new ProjectConfigurationFilePathChangedEventArgs(projectFilePath, configurationFilePath: null));
}
public override void Set(string projectFilePath, string configurationFilePath)
{
Changed?.Invoke(this, new ProjectConfigurationFilePathChangedEventArgs(projectFilePath, configurationFilePath));
}
public override bool TryGet(string projectFilePath, out string? configurationFilePath)
{
configurationFilePath = null;
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.Editor.Razor;
using Moq;
namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.ProjectSystem
{
internal class TestVSLanguageServerFeatureOptions : VSLanguageServerFeatureOptions
{
#pragma warning disable CS0618 // Type or member is obsolete
public static readonly TestVSLanguageServerFeatureOptions Instance = new();
#pragma warning restore CS0618 // Type or member is obsolete
[Obsolete("Use static Instance member")]
public TestVSLanguageServerFeatureOptions() : base(Mock.Of<LSPEditorFeatureDetector>(MockBehavior.Strict))
{
}
}
}