From 91afef7d525710e03e2d13c979e8d28e4ad223c5 Mon Sep 17 00:00:00 2001 From: Jason Malinowski Date: Wed, 23 Nov 2022 14:13:23 -0800 Subject: [PATCH] Implement a new brokered service for passing project information This defines a new brokered service that is pretty similar to IWorkspaceProjectContext, but more friendly for cross-process communication. It's currently implemented atop IWorkspaceProjectContext so that way the behavior is identical otherwise, but hopefully the project system can move to consuming this one soon enough and we can flatten the implementation. --- eng/targets/Services.props | 1 + .../BrokeredService/WorkspaceProject.cs | 146 ++++++++++++++++++ .../WorkspaceProjectFactoryService.cs | 64 ++++++++ .../CPS/IWorkspaceProjectContext.cs | 2 + src/VisualStudio/Core/Def/RoslynPackage.cs | 10 ++ .../CPSProject_IWorkspaceProjectContext.cs | 2 + ...soft.CodeAnalysis.Remote.Workspaces.csproj | 8 + .../Core/ProjectSystem/IWorkspaceProject.cs | 37 +++++ .../IWorkspaceProjectFactoryService.cs | 24 +++ .../ProjectSystem/MetadataReferenceInfo.cs | 7 + .../Core/ProjectSystem/SourceFileInfo.cs | 9 ++ .../WorkspaceProjectCreationInfo.cs | 9 ++ 12 files changed, 319 insertions(+) create mode 100644 src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs create mode 100644 src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProjectFactoryService.cs create mode 100644 src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProject.cs create mode 100644 src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProjectFactoryService.cs create mode 100644 src/Workspaces/Remote/Core/ProjectSystem/MetadataReferenceInfo.cs create mode 100644 src/Workspaces/Remote/Core/ProjectSystem/SourceFileInfo.cs create mode 100644 src/Workspaces/Remote/Core/ProjectSystem/WorkspaceProjectCreationInfo.cs diff --git a/eng/targets/Services.props b/eng/targets/Services.props index a48040a0c68c5..06fe61e59b733 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -49,5 +49,6 @@ --> + diff --git a/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs new file mode 100644 index 0000000000000..01d0a6ee86e2f --- /dev/null +++ b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Remote.ProjectSystem; +using Roslyn.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem.BrokeredService +{ + internal sealed class WorkspaceProject : IWorkspaceProject + { + // For the sake of the in-proc implementation here, we're going to build this atop IWorkspaceProjectContext so semantics are preserved + // for a few edge cases. Once the project system has moved onto this directly, we can flatten the implementations out. + private readonly IWorkspaceProjectContext _project; + + public WorkspaceProject(IWorkspaceProjectContext project) + { + _project = project; + } + + public ValueTask DisposeAsync() + { + _project.Dispose(); + return ValueTaskFactory.CompletedTask; + } + + public async Task AddAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken cancellationToken) + { + await using var batch = _project.CreateBatchScope().ConfigureAwait(false); + + foreach (var additionalFilePath in additionalFilePaths) + _project.AddAdditionalFile(additionalFilePath); + } + + public async Task RemoveAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken cancellationToken) + { + await using var batch = _project.CreateBatchScope().ConfigureAwait(false); + + foreach (var additionalFilePath in additionalFilePaths) + _project.RemoveAdditionalFile(additionalFilePath); + } + + public async Task AddAnalyzerConfigFilesAsync(IReadOnlyList analyzerConfigPaths, CancellationToken cancellationToken) + { + await using var batch = _project.CreateBatchScope().ConfigureAwait(false); + + foreach (var analyzerConfigPath in analyzerConfigPaths) + _project.AddAnalyzerConfigFile(analyzerConfigPath); + } + public async Task RemoveAnalyzerConfigFilesAsync(IReadOnlyList analyzerConfigPaths, CancellationToken cancellationToken) + { + await using var batch = _project.CreateBatchScope().ConfigureAwait(false); + + foreach (var analyzerConfigPath in analyzerConfigPaths) + _project.RemoveAnalyzerConfigFile(analyzerConfigPath); + } + + public async Task AddAnalyzerReferencesAsync(IReadOnlyList analyzerPaths, CancellationToken cancellationToken) + { + await using var batch = _project.CreateBatchScope().ConfigureAwait(false); + + foreach (var analyzerPath in analyzerPaths) + _project.AddAnalyzerReference(analyzerPath); + } + + public async Task RemoveAnalyzerReferencesAsync(IReadOnlyList analyzerPaths, CancellationToken cancellationToken) + { + await using var batch = _project.CreateBatchScope().ConfigureAwait(false); + + foreach (var analyzerPath in analyzerPaths) + _project.RemoveAnalyzerReference(analyzerPath); + } + + public async Task AddMetadataReferencesAsync(IReadOnlyList metadataReferences, CancellationToken cancellationToken) + { + await using var batch = _project.CreateBatchScope().ConfigureAwait(false); + + foreach (var metadataReference in metadataReferences) + { + _project.AddMetadataReference( + metadataReference.FilePath, + new MetadataReferenceProperties(MetadataImageKind.Assembly, default, metadataReference.EmbedInteropTypes)); + } + } + + public async Task RemoveMetadataReferencesAsync(IReadOnlyList metadataReferences, CancellationToken cancellationToken) + { + await using var batch = _project.CreateBatchScope().ConfigureAwait(false); + + // The existing IWorkspaceProjectContext API here is a bit odd in that it only looks at the file path, and trusts that there aren't two + // references with the same path but different properties. + foreach (var metadataReference in metadataReferences) + _project.RemoveMetadataReference(metadataReference.FilePath); + } + + public async Task AddSourceFilesAsync(IReadOnlyList sourceFiles, CancellationToken cancellationToken) + { + await using var batch = _project.CreateBatchScope().ConfigureAwait(false); + + foreach (var sourceFile in sourceFiles) + { + _project.AddSourceFile( + sourceFile.FilePath, + folderNames: sourceFile.FolderNames); + } + } + public async Task RemoveSourceFilesAsync(IReadOnlyList sourceFiles, CancellationToken cancellationToken) + { + await using var batch = _project.CreateBatchScope().ConfigureAwait(false); + + foreach (var sourceFile in sourceFiles) + _project.RemoveSourceFile(sourceFile); + } + + public async Task SetBuildSystemPropertiesAsync(IReadOnlyDictionary properties, CancellationToken cancellationToken) + { + await using var batch = _project.CreateBatchScope().ConfigureAwait(false); + + foreach (var property in properties) + _project.SetProperty(property.Key, property.Value); + } + + public Task SetCommandLineArgumentsAsync(IReadOnlyList arguments, CancellationToken cancellationToken) + { + _project.SetOptions(arguments.ToImmutableArray()); + return Task.CompletedTask; + } + + public Task SetDisplayNameAsync(string displayName, CancellationToken cancellationToken) + { + _project.DisplayName = displayName; + return Task.CompletedTask; + } + + public Task StartBatchAsync(CancellationToken cancellationToken) + { + return Task.FromResult(_project.CreateBatchScope()); + } + } +} diff --git a/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProjectFactoryService.cs b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProjectFactoryService.cs new file mode 100644 index 0000000000000..98a35c85eeb13 --- /dev/null +++ b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProjectFactoryService.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Remote; +using Microsoft.CodeAnalysis.Remote.ProjectSystem; +using Microsoft.ServiceHub.Framework; + +namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem.BrokeredService +{ + internal class WorkspaceProjectFactoryService : IWorkspaceProjectFactoryService + { + public const string ServiceName = "WorkspaceProjectFactoryService"; + public static readonly ServiceDescriptor ServiceDescriptor = ServiceDescriptor.CreateInProcServiceDescriptor(ServiceDescriptors.ComponentName, ServiceName, suffix: "", ServiceDescriptors.GetFeatureDisplayName); + + private readonly IWorkspaceProjectContextFactory _workspaceProjectContextFactory; + + // For the sake of the in-proc implementation here, we're going to build this atop IWorkspaceProjectContext so semantics are preserved + // for a few edge cases. Once the project system has moved onto this directly, we can flatten the implementations out. + public WorkspaceProjectFactoryService(IWorkspaceProjectContextFactory workspaceProjectContextFactory) + { + _workspaceProjectContextFactory = workspaceProjectContextFactory; + } + + public async Task CreateAndAddProjectAsync(WorkspaceProjectCreationInfo creationInfo, CancellationToken cancellationToken) + { + var project = await _workspaceProjectContextFactory.CreateProjectContextAsync( + Guid.NewGuid(), // TODO: figure out some other side-channel way of communicating this + creationInfo.DisplayName, + creationInfo.Language, + new EvaluationDataShim(creationInfo.BuildSystemProperties), + hostObject: null, // TODO: figure out some other side-channel way of communicating this + cancellationToken).ConfigureAwait(false); + + return new WorkspaceProject(project); + } + + public Task> GetSupportedBuildSystemPropertiesAsync(CancellationToken cancellationToken) + { + return Task.FromResult((IReadOnlyCollection)_workspaceProjectContextFactory.EvaluationItemNames); + } + + private sealed class EvaluationDataShim : EvaluationData + { + private readonly IReadOnlyDictionary _buildSystemProperties; + + public EvaluationDataShim(IReadOnlyDictionary buildSystemProperties) + { + _buildSystemProperties = buildSystemProperties; + } + + public override string GetPropertyValue(string name) + { + return _buildSystemProperties.TryGetValue(name, out var value) ? value : ""; + } + } + } +} diff --git a/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs b/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs index 05e9271ef5c09..83e92eb485a32 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs @@ -9,6 +9,7 @@ using System.Collections.Immutable; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem { @@ -72,6 +73,7 @@ internal interface IWorkspaceProjectContext : IDisposable void RemoveAnalyzerConfigFile(string filePath); void StartBatch(); + IAsyncDisposable CreateBatchScope(); ValueTask EndBatchAsync(); void ReorderSourceFiles(IEnumerable filePaths); diff --git a/src/VisualStudio/Core/Def/RoslynPackage.cs b/src/VisualStudio/Core/Def/RoslynPackage.cs index 130b7e3169592..d4f9140ad1d37 100644 --- a/src/VisualStudio/Core/Def/RoslynPackage.cs +++ b/src/VisualStudio/Core/Def/RoslynPackage.cs @@ -33,9 +33,12 @@ using Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource; using Microsoft.VisualStudio.LanguageServices.Implementation.UnusedReferences; using Microsoft.VisualStudio.LanguageServices.InheritanceMargin; +using Microsoft.VisualStudio.LanguageServices.ProjectSystem; +using Microsoft.VisualStudio.LanguageServices.ProjectSystem.BrokeredService; using Microsoft.VisualStudio.LanguageServices.StackTraceExplorer; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Shell.ServiceBroker; using Microsoft.VisualStudio.TaskStatusCenter; using Microsoft.VisualStudio.TextManager.Interop; using Microsoft.VisualStudio.Threading; @@ -170,6 +173,13 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke // doc events and appropriately map files to/from it and other relevant workspaces (like the // metadata-as-source workspace). await this.ComponentModel.GetService().InitializeAsync(this).ConfigureAwait(false); + + // Proffer in-process service broker services + var serviceBrokerContainer = await this.GetServiceAsync(this.JoinableTaskFactory).ConfigureAwait(false); + + serviceBrokerContainer.Proffer( + WorkspaceProjectFactoryService.ServiceDescriptor, + (_, _, _, _) => ValueTaskFactory.FromResult(new WorkspaceProjectFactoryService(this.ComponentModel.GetService()))); } private async Task LoadOptionPersistersAsync(IComponentModel componentModel, CancellationToken cancellationToken) diff --git a/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs b/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs index ad75a96affecd..60600e4dad016 100644 --- a/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs +++ b/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs @@ -276,5 +276,7 @@ public void AddAnalyzerConfigFile(string filePath) public void RemoveAnalyzerConfigFile(string filePath) => _visualStudioProject.RemoveAnalyzerConfigFile(filePath); + + public IAsyncDisposable CreateBatchScope() => _visualStudioProject.CreateBatchScope(); } } diff --git a/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj b/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj index 89f4227205cf5..7f25e5b44f187 100644 --- a/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj +++ b/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj @@ -73,5 +73,13 @@ + + + + + + + + diff --git a/src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProject.cs b/src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProject.cs new file mode 100644 index 0000000000000..9b0b5e5ceec09 --- /dev/null +++ b/src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProject.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using StreamJsonRpc; + +namespace Microsoft.CodeAnalysis.Remote.ProjectSystem; + +[RpcMarshalable] +internal interface IWorkspaceProject : IAsyncDisposable +{ + Task SetDisplayNameAsync(string displayName, CancellationToken cancellationToken); + + Task SetCommandLineArgumentsAsync(IReadOnlyList arguments, CancellationToken cancellationToken); + Task SetBuildSystemPropertiesAsync(IReadOnlyDictionary properties, CancellationToken cancellationToken); + + Task AddSourceFilesAsync(IReadOnlyList sourceFiles, CancellationToken cancellationToken); + Task RemoveSourceFilesAsync(IReadOnlyList sourceFiles, CancellationToken cancellationToken); + + Task AddMetadataReferencesAsync(IReadOnlyList metadataReferences, CancellationToken cancellationToken); + Task RemoveMetadataReferencesAsync(IReadOnlyList metadataReferences, CancellationToken cancellationToken); + + Task AddAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken cancellationToken); + Task RemoveAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken cancellationToken); + + Task AddAnalyzerReferencesAsync(IReadOnlyList analyzerPaths, CancellationToken cancellationToken); + Task RemoveAnalyzerReferencesAsync(IReadOnlyList analyzerPaths, CancellationToken cancellationToken); + + Task AddAnalyzerConfigFilesAsync(IReadOnlyList analyzerConfigPaths, CancellationToken cancellationToken); + Task RemoveAnalyzerConfigFilesAsync(IReadOnlyList analyzerConfigPaths, CancellationToken cancellationToken); + + Task StartBatchAsync(CancellationToken cancellationToken); +} diff --git a/src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProjectFactoryService.cs b/src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProjectFactoryService.cs new file mode 100644 index 0000000000000..5c0a70b550f63 --- /dev/null +++ b/src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProjectFactoryService.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Remote.ProjectSystem; + +internal interface IWorkspaceProjectFactoryService +{ + Task CreateAndAddProjectAsync(WorkspaceProjectCreationInfo creationInfo, CancellationToken cancellationToken); + + /// + /// Returns the list of properties that are understood by the language service and can be passed to + /// and to + /// . + /// + Task> GetSupportedBuildSystemPropertiesAsync(CancellationToken cancellationToken); +} diff --git a/src/Workspaces/Remote/Core/ProjectSystem/MetadataReferenceInfo.cs b/src/Workspaces/Remote/Core/ProjectSystem/MetadataReferenceInfo.cs new file mode 100644 index 0000000000000..d8d4a827be2ea --- /dev/null +++ b/src/Workspaces/Remote/Core/ProjectSystem/MetadataReferenceInfo.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.Remote.ProjectSystem; + +internal readonly record struct MetadataReferenceInfo(string FilePath, string Aliases, bool EmbedInteropTypes); diff --git a/src/Workspaces/Remote/Core/ProjectSystem/SourceFileInfo.cs b/src/Workspaces/Remote/Core/ProjectSystem/SourceFileInfo.cs new file mode 100644 index 0000000000000..8e0dd477b4dc7 --- /dev/null +++ b/src/Workspaces/Remote/Core/ProjectSystem/SourceFileInfo.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Microsoft.CodeAnalysis.Remote.ProjectSystem; + +internal readonly record struct SourceFileInfo(string FilePath, IReadOnlyList FolderNames); diff --git a/src/Workspaces/Remote/Core/ProjectSystem/WorkspaceProjectCreationInfo.cs b/src/Workspaces/Remote/Core/ProjectSystem/WorkspaceProjectCreationInfo.cs new file mode 100644 index 0000000000000..f81240e4f14a0 --- /dev/null +++ b/src/Workspaces/Remote/Core/ProjectSystem/WorkspaceProjectCreationInfo.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Microsoft.CodeAnalysis.Remote.ProjectSystem; + +internal record WorkspaceProjectCreationInfo(string Language, string DisplayName, string? FilePath, IReadOnlyDictionary BuildSystemProperties);