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);