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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem 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 Dictionary<ProjectId, ProjectSnapshot> _projects;
private readonly List<WeakReference<DefaultProjectSnapshotListener>> _listeners; 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>(); _projects = new Dictionary<ProjectId, ProjectSnapshot>();
_listeners = new List<WeakReference<DefaultProjectSnapshotListener>>(); _listeners = new List<WeakReference<DefaultProjectSnapshotListener>>();
// Attaching the event handler inside before initialization prevents re-entrancy without for (var i = 0; i < _triggers.Length; i++)
// losing any notifications. {
_workspace.WorkspaceChanged += Workspace_WorkspaceChanged; _triggers[i].Initialize(this);
InitializeSolution(_workspace.CurrentSolution); }
} }
public override IReadOnlyList<ProjectSnapshot> Projects => _projects.Values.ToArray(); public override IReadOnlyList<ProjectSnapshot> Projects => _projects.Values.ToArray();
public override ProjectSnapshot FindProject(string projectPath) public override Workspace Workspace { get; }
{
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 ProjectSnapshotListener Subscribe() public override ProjectSnapshotListener Subscribe()
{ {
@ -55,23 +49,60 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return subscription; 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()) foreach (var kvp in _projects.ToArray())
{ {
_projects.Remove(kvp.Key); _projects.Remove(kvp.Key);
// We need to notify listeners about every project removal.
NotifyListeners(new ProjectChangeEventArgs(kvp.Value, ProjectChangeKind.Removed)); 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) 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Composition; using System.Composition;
using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Host.Mef;
@ -12,6 +13,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
[ExportLanguageServiceFactory(typeof(ProjectSnapshotManager), RazorLanguage.Name)] [ExportLanguageServiceFactory(typeof(ProjectSnapshotManager), RazorLanguage.Name)]
internal class DefaultProjectSnapshotManagerFactory : ILanguageServiceFactory internal class DefaultProjectSnapshotManagerFactory : ILanguageServiceFactory
{ {
private readonly IEnumerable<ProjectSnapshotChangeTrigger> _triggers;
[ImportingConstructor]
public DefaultProjectSnapshotManagerFactory([ImportMany] IEnumerable<ProjectSnapshotChangeTrigger> triggers)
{
_triggers = triggers;
}
public ILanguageService CreateLanguageService(HostLanguageServices languageServices) public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{ {
if (languageServices == null) if (languageServices == null)
@ -19,7 +28,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(languageServices)); 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 IReadOnlyList<ProjectSnapshot> Projects { get; }
public abstract ProjectSnapshot FindProject(string projectPath);
public abstract ProjectSnapshotListener Subscribe(); 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; return;
} }
var project = _projectManager.FindProject(projectPath); var project = _projectManager.GetProjectWithFilePath(projectPath);
var subscription = _projectManager.Subscribe(); var subscription = _projectManager.Subscribe();
subscription.ProjectChanged += Subscription_ProjectStateChanged; subscription.ProjectChanged += Subscription_ProjectStateChanged;

View File

@ -6,9 +6,9 @@ using Xunit;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
public class DefaultProjectStateManagerTest public class WorkspaceProjectSnapshotChangeTriggerTest
{ {
public DefaultProjectStateManagerTest() public WorkspaceProjectSnapshotChangeTriggerTest()
{ {
Workspace = new AdhocWorkspace(); Workspace = new AdhocWorkspace();
EmptySolution = Workspace.CurrentSolution.GetIsolatedSolution(); EmptySolution = Workspace.CurrentSolution.GetIsolatedSolution();
@ -44,12 +44,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void WorkspaceChanged_SolutionEvents_AddsProjectsInSolution(WorkspaceChangeKind kind) public void WorkspaceChanged_SolutionEvents_AddsProjectsInSolution(WorkspaceChangeKind kind)
{ {
// Arrange // 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); var e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
// Act // Act
projectManager.Workspace_WorkspaceChanged(Workspace, e); trigger.Workspace_WorkspaceChanged(Workspace, e);
// Assert // Assert
Assert.Collection( Assert.Collection(
@ -67,16 +68,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void WorkspaceChanged_SolutionEvents_ClearsExistingProjects_AddsProjectsInSolution(WorkspaceChangeKind kind) public void WorkspaceChanged_SolutionEvents_ClearsExistingProjects_AddsProjectsInSolution(WorkspaceChangeKind kind)
{ {
// Arrange // 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. // Initialize with a project. This will get removed.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithOneProject); 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); e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
// Act // Act
projectManager.Workspace_WorkspaceChanged(Workspace, e); trigger.Workspace_WorkspaceChanged(Workspace, e);
// Assert // Assert
Assert.Collection( Assert.Collection(
@ -91,17 +93,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void WorkspaceChanged_ProjectChangeEvents_UpdatesProject(WorkspaceChangeKind kind) public void WorkspaceChanged_ProjectChangeEvents_UpdatesProject(WorkspaceChangeKind kind)
{ {
// Arrange // Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace); var trigger = new WorkspaceProjectSnapshotChangeTrigger();
var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace);
// Initialize with some projects. // Initialize with some projects.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); 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"); var solution = SolutionWithTwoProjects.WithProjectAssemblyName(ProjectNumberOne.Id, "Changed");
e = new WorkspaceChangeEventArgs(kind, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id); e = new WorkspaceChangeEventArgs(kind, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id);
// Act // Act
projectManager.Workspace_WorkspaceChanged(Workspace, e); trigger.Workspace_WorkspaceChanged(Workspace, e);
// Assert // Assert
Assert.Collection( Assert.Collection(
@ -118,17 +121,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void WorkspaceChanged_ProjectRemovedEvent_RemovesProject() public void WorkspaceChanged_ProjectRemovedEvent_RemovesProject()
{ {
// Arrange // Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace); var trigger = new WorkspaceProjectSnapshotChangeTrigger();
var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace);
// Initialize with some projects project. // Initialize with some projects project.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); 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); var solution = SolutionWithTwoProjects.RemoveProject(ProjectNumberOne.Id);
e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectRemoved, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id); e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectRemoved, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id);
// Act // Act
projectManager.Workspace_WorkspaceChanged(Workspace, e); trigger.Workspace_WorkspaceChanged(Workspace, e);
// Assert // Assert
Assert.Collection( Assert.Collection(
@ -140,13 +144,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void WorkspaceChanged_ProjectAddedEvent_AddsProject() public void WorkspaceChanged_ProjectAddedEvent_AddsProject()
{ {
// Arrange // Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace); var trigger = new WorkspaceProjectSnapshotChangeTrigger();
var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace);
var solution = SolutionWithOneProject; var solution = SolutionWithOneProject;
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectAdded, oldSolution: EmptySolution, newSolution: solution, projectId: ProjectNumberThree.Id); var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectAdded, oldSolution: EmptySolution, newSolution: solution, projectId: ProjectNumberThree.Id);
// Act // Act
projectManager.Workspace_WorkspaceChanged(Workspace, e); trigger.Workspace_WorkspaceChanged(Workspace, e);
// Assert // Assert
Assert.Collection( 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
@ -18,8 +19,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{ {
public class DefaultVisualStudioDocumentTrackerFactoryTest : ForegroundDispatcherTestBase public class DefaultVisualStudioDocumentTrackerFactoryTest : ForegroundDispatcherTestBase
{ {
private static IReadOnlyList<ProjectSnapshot> Projects = new List<ProjectSnapshot>();
private ProjectSnapshotManager ProjectManager { get; } = Mock.Of<ProjectSnapshotManager>( 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>()); p.Subscribe() == Mock.Of<ProjectSnapshotListener>());
private TextBufferProjectService ProjectService { get; } = Mock.Of<TextBufferProjectService>( private TextBufferProjectService ProjectService { get; } = Mock.Of<TextBufferProjectService>(