Refactor project snapshot manager

Splits the 'trigger' out from the change manager. The next change will
add more functionality to DPSMBase.
This commit is contained in:
Ryan Nowak 2017-09-06 22:28:24 -07:00
parent 1806d26e9a
commit 82866d9442
10 changed files with 233 additions and 110 deletions

View File

@ -3,49 +3,43 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class DefaultProjectSnapshotManager : ProjectSnapshotManager
internal class DefaultProjectSnapshotManager : ProjectSnapshotManagerBase
{
private readonly Workspace _workspace;
private readonly ProjectSnapshotChangeTrigger[] _triggers;
private readonly Dictionary<ProjectId, ProjectSnapshot> _projects;
private readonly List<WeakReference<DefaultProjectSnapshotListener>> _listeners;
public DefaultProjectSnapshotManager(Workspace workspace)
public DefaultProjectSnapshotManager(IEnumerable<ProjectSnapshotChangeTrigger> triggers, Workspace workspace)
{
_workspace = workspace;
if (triggers == null)
{
throw new ArgumentNullException(nameof(triggers));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_triggers = triggers.ToArray();
Workspace = workspace;
_projects = new Dictionary<ProjectId, ProjectSnapshot>();
_listeners = new List<WeakReference<DefaultProjectSnapshotListener>>();
// Attaching the event handler inside before initialization prevents re-entrancy without
// losing any notifications.
_workspace.WorkspaceChanged += Workspace_WorkspaceChanged;
InitializeSolution(_workspace.CurrentSolution);
for (var i = 0; i < _triggers.Length; i++)
{
_triggers[i].Initialize(this);
}
}
public override IReadOnlyList<ProjectSnapshot> Projects => _projects.Values.ToArray();
public override ProjectSnapshot FindProject(string projectPath)
{
if (projectPath == null)
{
throw new ArgumentNullException(nameof(projectPath));
}
foreach (var project in _projects.Values)
{
if (string.Equals(projectPath, project.UnderlyingProject.FilePath, StringComparison.OrdinalIgnoreCase))
{
return project;
}
}
return null;
}
public override Workspace Workspace { get; }
public override ProjectSnapshotListener Subscribe()
{
@ -55,23 +49,60 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return subscription;
}
private void InitializeSolution(Solution solution)
public override void ProjectAdded(Project underlyingProject)
{
Debug.Assert(solution != null);
if (underlyingProject == null)
{
throw new ArgumentNullException(nameof(underlyingProject));
}
var snapshot = new DefaultProjectSnapshot(underlyingProject);
_projects[underlyingProject.Id] = snapshot;
// We need to notify listeners about every project add.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Added));
}
public override void ProjectChanged(Project underlyingProject)
{
if (underlyingProject == null)
{
throw new ArgumentNullException(nameof(underlyingProject));
}
// For now we don't have any state associated with the project so we can just construct a new snapshot.
var snapshot = new DefaultProjectSnapshot(underlyingProject);
_projects[underlyingProject.Id] = snapshot;
// There's no need to notify listeners about project changes because we don't have any state.
// This will change when we implement extensibility support.
}
public override void ProjectRemoved(Project underlyingProject)
{
if (underlyingProject == null)
{
throw new ArgumentNullException(nameof(underlyingProject));
}
if (_projects.TryGetValue(underlyingProject.Id, out var snapshot))
{
_projects.Remove(underlyingProject.Id);
// We need to notify listeners about every project removal.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Removed));
}
}
public override void ProjectsCleared()
{
foreach (var kvp in _projects.ToArray())
{
_projects.Remove(kvp.Key);
// We need to notify listeners about every project removal.
NotifyListeners(new ProjectChangeEventArgs(kvp.Value, ProjectChangeKind.Removed));
}
foreach (var project in solution.Projects)
{
var projectState = new DefaultProjectSnapshot(project);
_projects[project.Id] = projectState;
NotifyListeners(new ProjectChangeEventArgs(projectState, ProjectChangeKind.Added));
}
}
private void NotifyListeners(ProjectChangeEventArgs e)
@ -88,59 +119,5 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
}
}
// Internal for testing
internal void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
{
Project underlyingProject;
ProjectSnapshot snapshot;
switch (e.Kind)
{
case WorkspaceChangeKind.ProjectAdded:
{
underlyingProject = e.NewSolution.GetProject(e.ProjectId);
Debug.Assert(underlyingProject != null);
snapshot = new DefaultProjectSnapshot(underlyingProject);
_projects[e.ProjectId] = snapshot;
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Added));
break;
}
case WorkspaceChangeKind.ProjectChanged:
case WorkspaceChangeKind.ProjectReloaded:
{
underlyingProject = e.NewSolution.GetProject(e.ProjectId);
Debug.Assert(underlyingProject != null);
snapshot = new DefaultProjectSnapshot(underlyingProject);
_projects[e.ProjectId] = snapshot;
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
break;
}
case WorkspaceChangeKind.ProjectRemoved:
{
// We're being extra defensive here to avoid crashes.
if (_projects.TryGetValue(e.ProjectId, out snapshot))
{
_projects.Remove(e.ProjectId);
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Removed));
}
break;
}
case WorkspaceChangeKind.SolutionAdded:
case WorkspaceChangeKind.SolutionChanged:
case WorkspaceChangeKind.SolutionCleared:
case WorkspaceChangeKind.SolutionReloaded:
case WorkspaceChangeKind.SolutionRemoved:
InitializeSolution(e.NewSolution);
break;
}
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Composition;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
@ -12,6 +13,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
[ExportLanguageServiceFactory(typeof(ProjectSnapshotManager), RazorLanguage.Name)]
internal class DefaultProjectSnapshotManagerFactory : ILanguageServiceFactory
{
private readonly IEnumerable<ProjectSnapshotChangeTrigger> _triggers;
[ImportingConstructor]
public DefaultProjectSnapshotManagerFactory([ImportMany] IEnumerable<ProjectSnapshotChangeTrigger> triggers)
{
_triggers = triggers;
}
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{
if (languageServices == null)
@ -19,7 +28,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(languageServices));
}
return new DefaultProjectSnapshotManager(languageServices.WorkspaceServices.Workspace);
return new DefaultProjectSnapshotManager(_triggers, languageServices.WorkspaceServices.Workspace);
}
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal abstract class ProjectSnapshotChangeTrigger
{
public abstract void Initialize(ProjectSnapshotManagerBase projectManager);
}
}

View File

@ -10,8 +10,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public abstract IReadOnlyList<ProjectSnapshot> Projects { get; }
public abstract ProjectSnapshot FindProject(string projectPath);
public abstract ProjectSnapshotListener Subscribe();
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal abstract class ProjectSnapshotManagerBase : ProjectSnapshotManager
{
public abstract Workspace Workspace { get; }
public abstract void ProjectAdded(Project underlyingProject);
public abstract void ProjectChanged(Project underlyingProject);
public abstract void ProjectRemoved(Project underlyingProject);
public abstract void ProjectsCleared();
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal static class ProjectSnapshotManagerExtensions
{
public static ProjectSnapshot GetProjectWithFilePath(this ProjectSnapshotManager snapshotManager, string filePath)
{
var projects = snapshotManager.Projects;
for (var i = 0; i< projects.Count; i++)
{
var project = projects[i];
if (string.Equals(filePath, project.UnderlyingProject.FilePath, StringComparison.OrdinalIgnoreCase))
{
return project;
}
}
return null;
}
}
}

View File

@ -0,0 +1,78 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Composition;
using System.Diagnostics;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
[Export(typeof(ProjectSnapshotChangeTrigger))]
internal class WorkspaceProjectSnapshotChangeTrigger : ProjectSnapshotChangeTrigger
{
private ProjectSnapshotManagerBase _projectManager;
public override void Initialize(ProjectSnapshotManagerBase projectManager)
{
_projectManager = projectManager;
_projectManager.Workspace.WorkspaceChanged += Workspace_WorkspaceChanged;
InitializeSolution(_projectManager.Workspace.CurrentSolution);
}
private void InitializeSolution(Solution solution)
{
Debug.Assert(solution != null);
_projectManager.ProjectsCleared();
foreach (var project in solution.Projects)
{
_projectManager.ProjectAdded(project);
}
}
// Internal for testing
internal void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
{
Project underlyingProject;
switch (e.Kind)
{
case WorkspaceChangeKind.ProjectAdded:
{
underlyingProject = e.NewSolution.GetProject(e.ProjectId);
Debug.Assert(underlyingProject != null);
_projectManager.ProjectAdded(underlyingProject);
break;
}
case WorkspaceChangeKind.ProjectChanged:
case WorkspaceChangeKind.ProjectReloaded:
{
underlyingProject = e.NewSolution.GetProject(e.ProjectId);
Debug.Assert(underlyingProject != null);
_projectManager.ProjectChanged(underlyingProject);
break;
}
case WorkspaceChangeKind.ProjectRemoved:
{
underlyingProject = e.OldSolution.GetProject(e.ProjectId);
Debug.Assert(underlyingProject != null);
_projectManager.ProjectRemoved(underlyingProject);
break;
}
case WorkspaceChangeKind.SolutionAdded:
case WorkspaceChangeKind.SolutionChanged:
case WorkspaceChangeKind.SolutionCleared:
case WorkspaceChangeKind.SolutionReloaded:
case WorkspaceChangeKind.SolutionRemoved:
InitializeSolution(e.NewSolution);
break;
}
}
}
}

View File

@ -101,7 +101,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
return;
}
var project = _projectManager.FindProject(projectPath);
var project = _projectManager.GetProjectWithFilePath(projectPath);
var subscription = _projectManager.Subscribe();
subscription.ProjectChanged += Subscription_ProjectStateChanged;

View File

@ -6,9 +6,9 @@ using Xunit;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public class DefaultProjectStateManagerTest
public class WorkspaceProjectSnapshotChangeTriggerTest
{
public DefaultProjectStateManagerTest()
public WorkspaceProjectSnapshotChangeTriggerTest()
{
Workspace = new AdhocWorkspace();
EmptySolution = Workspace.CurrentSolution.GetIsolatedSolution();
@ -44,12 +44,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void WorkspaceChanged_SolutionEvents_AddsProjectsInSolution(WorkspaceChangeKind kind)
{
// Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace);
var trigger = new WorkspaceProjectSnapshotChangeTrigger();
var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace);
var e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
// Act
projectManager.Workspace_WorkspaceChanged(Workspace, e);
trigger.Workspace_WorkspaceChanged(Workspace, e);
// Assert
Assert.Collection(
@ -67,16 +68,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void WorkspaceChanged_SolutionEvents_ClearsExistingProjects_AddsProjectsInSolution(WorkspaceChangeKind kind)
{
// Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace);
var trigger = new WorkspaceProjectSnapshotChangeTrigger();
var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace);
// Initialize with a project. This will get removed.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithOneProject);
projectManager.Workspace_WorkspaceChanged(Workspace, e);
trigger.Workspace_WorkspaceChanged(Workspace, e);
e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
// Act
projectManager.Workspace_WorkspaceChanged(Workspace, e);
trigger.Workspace_WorkspaceChanged(Workspace, e);
// Assert
Assert.Collection(
@ -91,17 +93,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void WorkspaceChanged_ProjectChangeEvents_UpdatesProject(WorkspaceChangeKind kind)
{
// Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace);
var trigger = new WorkspaceProjectSnapshotChangeTrigger();
var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace);
// Initialize with some projects.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
projectManager.Workspace_WorkspaceChanged(Workspace, e);
trigger.Workspace_WorkspaceChanged(Workspace, e);
var solution = SolutionWithTwoProjects.WithProjectAssemblyName(ProjectNumberOne.Id, "Changed");
e = new WorkspaceChangeEventArgs(kind, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id);
// Act
projectManager.Workspace_WorkspaceChanged(Workspace, e);
trigger.Workspace_WorkspaceChanged(Workspace, e);
// Assert
Assert.Collection(
@ -118,17 +121,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void WorkspaceChanged_ProjectRemovedEvent_RemovesProject()
{
// Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace);
var trigger = new WorkspaceProjectSnapshotChangeTrigger();
var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace);
// Initialize with some projects project.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
projectManager.Workspace_WorkspaceChanged(Workspace, e);
trigger.Workspace_WorkspaceChanged(Workspace, e);
var solution = SolutionWithTwoProjects.RemoveProject(ProjectNumberOne.Id);
e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectRemoved, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id);
// Act
projectManager.Workspace_WorkspaceChanged(Workspace, e);
trigger.Workspace_WorkspaceChanged(Workspace, e);
// Assert
Assert.Collection(
@ -140,13 +144,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void WorkspaceChanged_ProjectAddedEvent_AddsProject()
{
// Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace);
var trigger = new WorkspaceProjectSnapshotChangeTrigger();
var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace);
var solution = SolutionWithOneProject;
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectAdded, oldSolution: EmptySolution, newSolution: solution, projectId: ProjectNumberThree.Id);
// Act
projectManager.Workspace_WorkspaceChanged(Workspace, e);
trigger.Workspace_WorkspaceChanged(Workspace, e);
// Assert
Assert.Collection(

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
@ -18,8 +19,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
public class DefaultVisualStudioDocumentTrackerFactoryTest : ForegroundDispatcherTestBase
{
private static IReadOnlyList<ProjectSnapshot> Projects = new List<ProjectSnapshot>();
private ProjectSnapshotManager ProjectManager { get; } = Mock.Of<ProjectSnapshotManager>(
p => p.FindProject(It.IsAny<string>()) == Mock.Of<ProjectSnapshot>() &&
p => p.Projects == Projects &&
p.Subscribe() == Mock.Of<ProjectSnapshotListener>());
private TextBufferProjectService ProjectService { get; } = Mock.Of<TextBufferProjectService>(