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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.ProjectSystem;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
Expand Down Expand Up @@ -133,8 +134,9 @@ await ApplyChangeToWorkspaceAsync(w =>
analyzerReferences: w.CurrentSolution.AnalyzerReferences).WithTelemetryId(SolutionTelemetryId);
var newSolution = w.CreateSolution(solutionInfo);

foreach (var project in solutionInfo.Projects)
newSolution = newSolution.AddProject(project);
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projectInfos);
projectInfos.AddRange(solutionInfo.Projects);
newSolution = newSolution.AddProjects(projectInfos);

return newSolution;
}
Expand Down
29 changes: 21 additions & 8 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,17 +345,29 @@ 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.AddProjects"/>
public Solution AddProject(ProjectInfo projectInfo)
=> WithCompilationState(_compilationState.AddProject(projectInfo));
{
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(1, out var projectInfos);
projectInfos.Add(projectInfo);
return AddProjects(projectInfos);
}

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

/// <inheritdoc cref="SolutionCompilationState.RemoveProjects"/>
public Solution RemoveProject(ProjectId projectId)
=> WithCompilationState(_compilationState.RemoveProject(projectId));
{
using var _ = ArrayBuilder<ProjectId>.GetInstance(1, out var projectIds);
projectIds.Add(projectId);
return RemoveProjects(projectIds);
}

/// <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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,42 +316,83 @@ private ImmutableSegmentedDictionary<ProjectId, ICompilationTracker> CreateCompi

public SourceGeneratorExecutionVersionMap SourceGeneratorExecutionVersionMap => _sourceGeneratorExecutionVersionMap;

/// <inheritdoc cref="SolutionState.AddProject(ProjectInfo)"/>
public SolutionCompilationState AddProject(ProjectInfo projectInfo)
/// <inheritdoc cref="SolutionState.AddProjects(ArrayBuilder{ProjectInfo})"/>
public SolutionCompilationState AddProjects(ArrayBuilder<ProjectInfo> projectInfos)
{
var newSolutionState = this.SolutionState.AddProject(projectInfo);
var newTrackerMap = CreateCompilationTrackerMap(projectInfo.Id, newSolutionState.GetProjectDependencyGraph(), static (_, _) => { }, /* unused */ 0, skipEmptyCallback: true);
if (projectInfos.Count == 0)
return this;

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

var sourceGeneratorExecutionVersionMap = _sourceGeneratorExecutionVersionMap;
if (RemoteSupportedLanguages.IsSupported(projectInfo.Language))
// 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)
{
var versionMapBuilder = _sourceGeneratorExecutionVersionMap.Map.ToBuilder();
versionMapBuilder.Add(projectInfo.Id, new());
sourceGeneratorExecutionVersionMap = new(versionMapBuilder.ToImmutable());
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)
/// <inheritdoc cref="SolutionState.RemoveProjects"/>
public SolutionCompilationState RemoveProjects(ArrayBuilder<ProjectId> projectIds)
{
var newSolutionState = this.SolutionState.RemoveProject(projectId);
if (projectIds.Count == 0)
return this;

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

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 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(
projectId,
newSolutionState.GetProjectDependencyGraph(),
static (trackerMap, projectId) =>
// 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) =>
{
trackerMap.Remove(projectId);
foreach (var projectId in projectIds)
trackerMap.Remove(projectId);
},
projectId,
projectIds,
skipEmptyCallback: true);

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

return this.Branch(
newSolutionState,
Expand Down
158 changes: 94 additions & 64 deletions src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs
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 @@ -288,98 +289,127 @@ public ProjectState GetRequiredProjectState(ProjectId projectId)
return result;
}

private SolutionState AddProject(ProjectState projectState)
/// <summary>
/// Create a new solution instance that includes projects with the specified project information.
/// </summary>
public SolutionState AddProjects(ArrayBuilder<ProjectInfo> projectInfos)
{
var projectId = projectState.Id;
Contract.ThrowIfTrue(projectInfos.HasDuplicates(static p => p.Id), "Duplicate ProjectId provided");

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

var newProjectIds = ProjectIds.ToImmutableArray().Add(projectId);
var newStateMap = _projectIdToProjectStateMap.Add(projectId, projectState);
using var _ = ArrayBuilder<ProjectState>.GetInstance(projectInfos.Count, out var projectStates);
foreach (var projectInfo in projectInfos)
projectStates.Add(CreateProjectState(projectInfo));

var newDependencyGraph = _dependencyGraph
.WithAdditionalProject(projectId)
.WithAdditionalProjectReferences(projectId, projectState.ProjectReferences);
return AddProjects(projectStates);

// It's possible that another project already in newStateMap has a reference to this project that we're adding, since we allow
// dangling references like that. If so, we'll need to link those in too.
foreach (var newState in newStateMap)
ProjectState CreateProjectState(ProjectInfo projectInfo)
{
foreach (var projectReference in newState.Value.ProjectReferences)
{
if (projectReference.ProjectId == projectId)
{
newDependencyGraph = newDependencyGraph.WithAdditionalProjectReferences(
newState.Key, [projectReference]);
if (projectInfo == null)
throw new ArgumentNullException(nameof(projectInfo));

break;
}
}
}
var projectId = projectInfo.Id;

return Branch(
solutionAttributes: newSolutionAttributes,
projectIds: newProjectIds,
idToProjectStateMap: newStateMap,
dependencyGraph: newDependencyGraph);
}
var language = projectInfo.Language;
if (language == null)
throw new ArgumentNullException(nameof(language));

/// <summary>
/// Create a new solution instance that includes a project with the specified project information.
/// </summary>
public SolutionState AddProject(ProjectInfo projectInfo)
{
if (projectInfo == null)
{
throw new ArgumentNullException(nameof(projectInfo));
}
var displayName = projectInfo.Name;
if (displayName == null)
throw new ArgumentNullException(nameof(displayName));

var projectId = projectInfo.Id;
CheckNotContainsProject(projectId);

var language = projectInfo.Language;
if (language == null)
{
throw new ArgumentNullException(nameof(language));
var languageServices = Services.GetLanguageServices(language);
if (languageServices == null)
throw new ArgumentException(string.Format(WorkspacesResources.The_language_0_is_not_supported, language));

var newProject = new ProjectState(languageServices, projectInfo);
return newProject;
}

var displayName = projectInfo.Name;
if (displayName == null)
SolutionState AddProjects(ArrayBuilder<ProjectState> projectStates)
{
throw new ArgumentNullException(nameof(displayName));
}
// changed project list so, increment version.
var newSolutionAttributes = _solutionAttributes.With(version: Version.GetNewerVersion());

CheckNotContainsProject(projectId);
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();

var languageServices = Services.GetLanguageServices(language);
if (languageServices == null)
{
throw new ArgumentException(string.Format(WorkspacesResources.The_language_0_is_not_supported, language));
}
newProjectIdsBuilder.AddRange(ProjectIds);

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

var newProject = new ProjectState(languageServices, projectInfo);
var newProjectIds = newProjectIdsBuilder.ToBoxedImmutableArray();
var newStateMap = newStateMapBuilder.ToImmutable();

return this.AddProject(newProject);
// TODO: it would be nice to update these graphs without so much forking.
var newDependencyGraph = _dependencyGraph;
foreach (var projectState in projectStates)
{
var projectId = projectState.Id;
newDependencyGraph = newDependencyGraph
.WithAdditionalProject(projectId)
.WithAdditionalProjectReferences(projectId, projectState.ProjectReferences);
}

// It's possible that another project already in newStateMap has a reference to this project that we're adding,
// since we allow dangling references like that. If so, we'll need to link those in too.
foreach (var (projectId, newState) in newStateMap)
{
foreach (var projectReference in newState.ProjectReferences)
{
if (addedProjectIds.Contains(projectReference.ProjectId))
newDependencyGraph = newDependencyGraph.WithAdditionalProjectReferences(projectId, [projectReference]);
}
}

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

/// <summary>
/// Create a new solution instance without the project specified.
/// Create a new solution instance without the projects specified.
/// </summary>
public SolutionState RemoveProject(ProjectId projectId)
public SolutionState RemoveProjects(ArrayBuilder<ProjectId> projectIds)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
Contract.ThrowIfTrue(projectIds.HasDuplicates(), "Duplicate ProjectId provided");

CheckContainsProject(projectId);
if (projectIds.Count == 0)
return this;

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

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

var newProjectIds = ProjectIds.ToImmutableArray().Remove(projectId);
var newStateMap = _projectIdToProjectStateMap.Remove(projectId);
var newDependencyGraph = _dependencyGraph.WithProjectRemoved(projectId);
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();

// Note: it would be nice to not cause N forks of the dependency graph here.
var newDependencyGraph = _dependencyGraph;
foreach (var projectId in projectIds)
newDependencyGraph = newDependencyGraph.WithProjectRemoved(projectId);

return this.Branch(
solutionAttributes: newSolutionAttributes,
Expand Down
Loading
Loading