Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bulk add/remove project capability to the workspace. #72957

Merged
merged 16 commits into from
Apr 9, 2024
Merged
17 changes: 11 additions & 6 deletions src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
Expand Down Expand Up @@ -344,18 +345,22 @@ public Project AddProject(string name, string assemblyName, string language)
public Solution AddProject(ProjectId projectId, string name, string assemblyName, string language)
=> this.AddProject(ProjectInfo.Create(projectId, VersionStamp.Create(), name, assemblyName, language));

/// <summary>
/// Create a new solution instance that includes a project with the specified project information.
/// </summary>
/// <inheritdoc cref="SolutionCompilationState.AddProject"/>
public Solution AddProject(ProjectInfo projectInfo)
=> WithCompilationState(_compilationState.AddProject(projectInfo));

/// <summary>
/// Create a new solution instance without the project specified.
/// </summary>
/// <inheritdoc cref="SolutionCompilationState.AddProjects"/>
internal Solution AddProjects(ArrayBuilder<ProjectInfo> projectInfo)
=> WithCompilationState(_compilationState.AddProjects(projectInfo));

/// <inheritdoc cref="SolutionCompilationState.RemoveProject"/>
public Solution RemoveProject(ProjectId projectId)
=> WithCompilationState(_compilationState.RemoveProject(projectId));

/// <inheritdoc cref="SolutionCompilationState.RemoveProjects"/>
internal Solution RemoveProjects(ArrayBuilder<ProjectId> projectIds)
=> WithCompilationState(_compilationState.RemoveProjects(projectIds));

/// <summary>
/// Creates a new solution instance with the project specified updated to have the new
/// assembly name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,47 @@ public SolutionCompilationState AddProject(ProjectInfo projectInfo)
sourceGeneratorExecutionVersionMap: sourceGeneratorExecutionVersionMap);
}

/// <inheritdoc cref="SolutionState.AddProjects(ArrayBuilder{ProjectInfo})"/>
public SolutionCompilationState AddProjects(ArrayBuilder<ProjectInfo> projectInfos)
{
if (projectInfos.Count == 0)
return this;

if (projectInfos.Count == 1)
return AddProject(projectInfos.First());

var newSolutionState = this.SolutionState.AddProjects(projectInfos);

// When adding a project, we might add a project that an *existing* project now has a reference to. That's
// because we allow existing projects to have 'dangling' project references. As such, we have to ensure we do
// not reuse compilation trackers for any of those projects.
using var _ = PooledHashSet<ProjectId>.GetInstance(out var dependentProjects);
var newDependencyGraph = newSolutionState.GetProjectDependencyGraph();
foreach (var projectInfo in projectInfos)
dependentProjects.AddRange(newDependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(projectInfo.Id));

var newTrackerMap = CreateCompilationTrackerMap(
static (projectId, dependentProjects) => !dependentProjects.Contains(projectId),
dependentProjects,
// We don't need to do anything here. Compilation trackers are created on demand. So we'll just keep the
// tracker map as-is, and have the trackers for these new projects be created when needed.
modifyNewTrackerInfo: static (_, _) => { }, argModifyNewTrackerInfo: default(VoidResult),
skipEmptyCallback: true);

var versionMapBuilder = _sourceGeneratorExecutionVersionMap.Map.ToBuilder();
foreach (var projectInfo in projectInfos)
{
if (RemoteSupportedLanguages.IsSupported(projectInfo.Language))
versionMapBuilder.Add(projectInfo.Id, new());
}

var sourceGeneratorExecutionVersionMap = new SourceGeneratorExecutionVersionMap(versionMapBuilder.ToImmutable());
return Branch(
newSolutionState,
projectIdToTrackerMap: newTrackerMap,
sourceGeneratorExecutionVersionMap: sourceGeneratorExecutionVersionMap);
}

/// <inheritdoc cref="SolutionState.RemoveProject(ProjectId)"/>
public SolutionCompilationState RemoveProject(ProjectId projectId)
{
Expand All @@ -359,6 +400,55 @@ public SolutionCompilationState RemoveProject(ProjectId projectId)
sourceGeneratorExecutionVersionMap: new(versionMapBuilder.ToImmutable()));
}

/// <inheritdoc cref="SolutionState.RemoveProjects"/>
public SolutionCompilationState RemoveProjects(ArrayBuilder<ProjectId> projectIds)
{
if (projectIds.Count == 0)
return this;

if (projectIds.Count == 1)
return RemoveProject(projectIds.First());

var originalDependencyGraph = this.SolutionState.GetProjectDependencyGraph();
using var _ = PooledHashSet<ProjectId>.GetInstance(out var dependentProjects);

// Determine the set of projects that depend on the projects being removed.
foreach (var projectId in projectIds)
{
foreach (var dependentProject in originalDependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(projectId))
dependentProjects.Add(dependentProject);
}

// Now go and remove the projects from teh solution-state itself.
var newSolutionState = this.SolutionState.RemoveProjects(projectIds);

// Now for each compilation tracker.
// 1. remove the compilation tracker if we're removing the project.
// 2. fork teh compilation tracker if it depended on a removed project.
// 3. do nothing for the rest.
var newTrackerMap = CreateCompilationTrackerMap(
// Can reuse the compilation tracker for a project, unless it is some project that had a dependency on one
// of the projects removed.
static (projectId, dependentProjects) => !dependentProjects.Contains(projectId),
dependentProjects,
static (trackerMap, projectIds) =>
{
foreach (var projectId in projectIds)
trackerMap.Remove(projectId);
},
projectIds,
skipEmptyCallback: true);

var versionMapBuilder = _sourceGeneratorExecutionVersionMap.Map.ToBuilder();
foreach (var projectId in projectIds)
versionMapBuilder.Remove(projectId);

return this.Branch(
newSolutionState,
projectIdToTrackerMap: newTrackerMap,
sourceGeneratorExecutionVersionMap: new(versionMapBuilder.ToImmutable()));
}

/// <inheritdoc cref="SolutionState.WithProjectAssemblyName"/>
public SolutionCompilationState WithProjectAssemblyName(
ProjectId projectId, string assemblyName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
Expand Down Expand Up @@ -325,10 +326,62 @@ private SolutionState AddProject(ProjectState projectState)
dependencyGraph: newDependencyGraph);
}

private SolutionState AddProjects(ArrayBuilder<ProjectState> projectStates)
{
if (projectStates.Count == 0)
return this;

if (projectStates.Count == 1)
return AddProject(projectStates.First());
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved

// changed project list so, increment version.
var newSolutionAttributes = _solutionAttributes.With(version: Version.GetNewerVersion());

using var _1 = ArrayBuilder<ProjectId>.GetInstance(ProjectIds.Count + projectStates.Count, out var newProjectIdsBuilder);
using var _2 = PooledHashSet<ProjectId>.GetInstance(out var addedProjectIds);
var newStateMapBuilder = _projectIdToProjectStateMap.ToBuilder();

newProjectIdsBuilder.AddRange(ProjectIds);

foreach (var projectState in projectStates)
{
addedProjectIds.Add(projectState.Id);
newProjectIdsBuilder.Add(projectState.Id);
newStateMapBuilder.Add(projectState.Id, projectState);
}

var newProjectIds = newProjectIdsBuilder.ToBoxedImmutableArray();
var newStateMap = newStateMapBuilder.ToImmutable();

var newDependencyGraph = CreateDependencyGraph(newProjectIds, newStateMap);

return Branch(
solutionAttributes: newSolutionAttributes,
projectIds: newProjectIds,
idToProjectStateMap: newStateMap,
dependencyGraph: newDependencyGraph);
}

/// <summary>
/// Create a new solution instance that includes a project with the specified project information.
/// </summary>
public SolutionState AddProject(ProjectInfo projectInfo)
=> this.AddProject(CreateProjectState(projectInfo));

/// <summary>
/// Create a new solution instance that includes projects with the specified project information.
/// </summary>
public SolutionState AddProjects(ArrayBuilder<ProjectInfo> projectInfos)
{
using var _ = ArrayBuilder<ProjectState>.GetInstance(projectInfos.Count, out var projectStates);
foreach (var projectInfo in projectInfos)
projectStates.Add(CreateProjectState(projectInfo));

Contract.ThrowIfTrue(projectInfos.Select(i => i.Id).Distinct().Count() != projectInfos.Count);
return this.AddProjects(projectStates);
}

private ProjectState CreateProjectState(ProjectInfo projectInfo)
{
if (projectInfo == null)
{
Expand Down Expand Up @@ -358,8 +411,7 @@ public SolutionState AddProject(ProjectInfo projectInfo)
}

var newProject = new ProjectState(languageServices, projectInfo);

return this.AddProject(newProject);
return newProject;
}

/// <summary>
Expand Down Expand Up @@ -388,6 +440,42 @@ public SolutionState RemoveProject(ProjectId projectId)
dependencyGraph: newDependencyGraph);
}

/// <summary>
/// Create a new solution instance without the projects specified.
/// </summary>
public SolutionState RemoveProjects(ArrayBuilder<ProjectId> projectIds)
{
if (projectIds.Count == 0)
return this;

if (projectIds.Count == 1)
return RemoveProject(projectIds.First());

foreach (var projectId in projectIds)
CheckContainsProject(projectId);

// changed project list so, increment version.
var newSolutionAttributes = _solutionAttributes.With(version: this.Version.GetNewerVersion());

using var _ = PooledHashSet<ProjectId>.GetInstance(out var projectIdsSet);
projectIdsSet.AddRange(projectIds);

var newProjectIds = ProjectIds.Where(p => !projectIdsSet.Contains(p)).ToBoxedImmutableArray();

var newStateMapBuilder = _projectIdToProjectStateMap.ToBuilder();
foreach (var projectId in projectIds)
newStateMapBuilder.Remove(projectId);
var newStateMap = newStateMapBuilder.ToImmutable();

var newDependencyGraph = CreateDependencyGraph(newProjectIds, newStateMap);

return this.Branch(
solutionAttributes: newSolutionAttributes,
projectIds: newProjectIds,
idToProjectStateMap: newStateMap,
dependencyGraph: newDependencyGraph);
}

/// <summary>
/// Creates a new solution instance with the project specified updated to have the new
/// assembly name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ICSharpCode.Decompiler.Solution;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
Expand Down Expand Up @@ -245,13 +246,15 @@ private async Task<Solution> UpdateProjectsAsync(
{
// Note: it's common to see a whole lot of project-infos change. So attempt to collect that in one go
// if we can.
using var _ = PooledHashSet<Checksum>.GetInstance(out var projectInfoChecksums);
using var _1 = PooledHashSet<Checksum>.GetInstance(out var projectInfoChecksums);
foreach (var (projectId, newProjectChecksums) in newProjectIdToStateChecksums)
projectInfoChecksums.Add(newProjectChecksums.Info);

await _assetProvider.GetAssetsAsync<ProjectInfo.ProjectAttributes, VoidResult>(
assetPath: AssetPath.SolutionAndTopLevelProjectsOnly, projectInfoChecksums, callback: null, arg: default, cancellationToken).ConfigureAwait(false);

using var _2 = ArrayBuilder<ProjectInfo>.GetInstance(out var projectInfos);

// added project
foreach (var (projectId, newProjectChecksums) in newProjectIdToStateChecksums)
{
Expand All @@ -262,10 +265,12 @@ private async Task<Solution> UpdateProjectsAsync(

await _assetProvider.SynchronizeProjectAssetsAsync(newProjectChecksums, cancellationToken).ConfigureAwait(false);
var projectInfo = await _assetProvider.CreateProjectInfoAsync(projectId, newProjectChecksums.Checksum, cancellationToken).ConfigureAwait(false);
solution = solution.AddProject(projectInfo);
projectInfos.Add(projectInfo);
}
}

solution = solution.AddProjects(projectInfos);

// remove all project references from projects that changed. this ensures exceptions will not occur for
// cyclic references during an incremental update.
foreach (var (projectId, newProjectChecksums) in newProjectIdToStateChecksums)
Expand All @@ -279,17 +284,21 @@ private async Task<Solution> UpdateProjectsAsync(
}
}

using var _3 = ArrayBuilder<ProjectId>.GetInstance(out var projectsToRemove);

// removed project
foreach (var (projectId, _) in oldProjectIdToStateChecksums)
{
if (!newProjectIdToStateChecksums.ContainsKey(projectId))
{
// Should never be removing projects during cone syncing.
Contract.ThrowIfTrue(isConeSync);
solution = solution.RemoveProject(projectId);
projectsToRemove.Add(projectId);
}
}

solution = solution.RemoveProjects(projectsToRemove);

// changed project
foreach (var (projectId, newProjectChecksums) in newProjectIdToStateChecksums)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.PooledObjects;

namespace Roslyn.Utilities;

Expand All @@ -26,6 +27,22 @@ public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T>? v
}
}

public static void AddRange<T>(this ICollection<T> collection, ArrayBuilder<T>? values)
{
if (collection == null)
{
throw new ArgumentNullException(nameof(collection));
}

if (values != null)
{
foreach (var item in values)
{
collection.Add(item);
}
}
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
}

public static void AddRange<T>(this ICollection<T> collection, HashSet<T>? values)
{
if (collection == null)
Expand Down
Loading