Implmement a project system for Razor

This commit is contained in:
Ajay Bhargav Baaskaran 2017-08-28 17:00:24 -07:00 committed by Ryan Nowak
parent 4b68a48f1d
commit 5cb11b9bf4
20 changed files with 571 additions and 52 deletions

View File

@ -0,0 +1,22 @@
// 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 class DefaultProjectSnapshot : ProjectSnapshot
{
public DefaultProjectSnapshot(Project underlyingProject)
{
if (underlyingProject == null)
{
throw new ArgumentNullException(nameof(underlyingProject));
}
UnderlyingProject = underlyingProject;
}
public override Project UnderlyingProject { get; }
}
}

View File

@ -0,0 +1,21 @@
// 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 class DefaultProjectSnapshotListener : ProjectSnapshotListener
{
public override event EventHandler<ProjectChangeEventArgs> ProjectChanged;
internal void Notify(ProjectChangeEventArgs e)
{
var handler = ProjectChanged;
if (handler != null)
{
handler(this, e);
}
}
}
}

View File

@ -0,0 +1,146 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class DefaultProjectSnapshotManager : ProjectSnapshotManager
{
private readonly Workspace _workspace;
private readonly Dictionary<ProjectId, ProjectSnapshot> _projects;
private readonly List<WeakReference<DefaultProjectSnapshotListener>> _listeners;
public DefaultProjectSnapshotManager(Workspace workspace)
{
_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);
}
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 ProjectSnapshotListener Subscribe()
{
var subscription = new DefaultProjectSnapshotListener();
_listeners.Add(new WeakReference<DefaultProjectSnapshotListener>(subscription));
return subscription;
}
private void InitializeSolution(Solution solution)
{
Debug.Assert(solution != null);
foreach (var kvp in _projects.ToArray())
{
_projects.Remove(kvp.Key);
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)
{
for (var i = 0; i < _listeners.Count; i++)
{
if (_listeners[i].TryGetTarget(out var listener))
{
listener.Notify(e);
}
else
{
_listeners.RemoveAt(i--);
}
}
}
// 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

@ -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;
using System.Composition;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
[Shared]
[ExportLanguageServiceFactory(typeof(ProjectSnapshotManager), RazorLanguage.Name)]
internal class DefaultProjectSnapshotManagerFactory : ILanguageServiceFactory
{
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{
if (languageServices == null)
{
throw new ArgumentNullException(nameof(languageServices));
}
return new DefaultProjectSnapshotManager(languageServices.WorkspaceServices.Workspace);
}
}
}

View File

@ -0,0 +1,20 @@
// 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 class ProjectChangeEventArgs : EventArgs
{
public ProjectChangeEventArgs(ProjectSnapshot project, ProjectChangeKind kind)
{
Project = project;
Kind = kind;
}
public ProjectSnapshot Project { get; }
public ProjectChangeKind Kind { get; }
}
}

View File

@ -0,0 +1,12 @@
// 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 enum ProjectChangeKind
{
Added,
Removed,
Changed,
}
}

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 ProjectSnapshot
{
public abstract Project UnderlyingProject { get; }
}
}

View File

@ -0,0 +1,12 @@
// 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 abstract class ProjectSnapshotListener
{
public abstract event EventHandler<ProjectChangeEventArgs> ProjectChanged;
}
}

View File

@ -0,0 +1,17 @@
// 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.Collections.Generic;
using Microsoft.CodeAnalysis.Host;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal abstract class ProjectSnapshotManager : ILanguageService
{
public abstract IReadOnlyList<ProjectSnapshot> Projects { get; }
public abstract ProjectSnapshot FindProject(string projectPath);
public abstract ProjectSnapshotListener Subscribe();
}
}

View File

@ -6,5 +6,6 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Workspaces.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Remote.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LanguageServices.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LanguageServices.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.RazorExtension, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

View File

@ -61,6 +61,17 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
return hierarchy;
}
public override string GetProjectPath(IVsHierarchy hierarchy)
{
if (hierarchy == null)
{
throw new ArgumentNullException(nameof(hierarchy));
}
ErrorHandler.ThrowOnFailure(((IVsProject)hierarchy).GetMkDocument((uint)VSConstants.VSITEMID.Root, out var path), VSConstants.E_NOTIMPL);
return path;
}
public override bool IsSupportedProject(IVsHierarchy hierarchy)
{
if (hierarchy == null)

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
@ -13,17 +14,30 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
internal class DefaultVisualStudioDocumentTracker : VisualStudioDocumentTracker
{
private readonly ProjectSnapshotManager _projectManager;
private readonly TextBufferProjectService _projectService;
private readonly ITextBuffer _textBuffer;
private readonly List<ITextView> _textViews;
private readonly Workspace _workspace;
private bool _isSupportedProject;
private Workspace _workspace;
private ProjectSnapshot _project;
private string _projectPath;
private ProjectSnapshotListener _subscription;
public override event EventHandler ContextChanged;
public DefaultVisualStudioDocumentTracker(TextBufferProjectService projectService, Workspace workspace, ITextBuffer textBuffer)
public DefaultVisualStudioDocumentTracker(
ProjectSnapshotManager projectManager,
TextBufferProjectService projectService,
Workspace workspace,
ITextBuffer textBuffer)
{
if (projectManager == null)
{
throw new ArgumentNullException(nameof(projectManager));
}
if (projectService == null)
{
throw new ArgumentNullException(nameof(projectService));
@ -39,18 +53,19 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
throw new ArgumentNullException(nameof(textBuffer));
}
_projectManager = projectManager;
_projectService = projectService;
_textBuffer = textBuffer;
_workspace = workspace;
_workspace = workspace; // For now we assume that the workspace is the always default VS workspace.
_textViews = new List<ITextView>();
Update();
Initialize();
}
public override bool IsSupportedProject => _isSupportedProject;
public override ProjectId ProjectId => null;
public override Project Project => _project?.UnderlyingProject;
public override ITextBuffer TextBuffer => _textBuffer;
@ -60,49 +75,61 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public override Workspace Workspace => _workspace;
private bool Update()
private void Initialize()
{
// Update is called when the state of any of our surrounding systems changes. Here we want to examine the
// state of the world and then update properties as necessary.
//
// Fundamentally we have a Razor half of the world as as soon as the document is open - and then later
// the C# half of the world will be initialized. This code is in general pretty tolerant of
// unexpected /impossible states.
//
// We also want to successfully shut down when the buffer is renamed to something other .cshtml.
IVsHierarchy project = null;
// We also want to successfully shut down if the buffer is something other than .cshtml.
IVsHierarchy hierarchy = null;
string projectPath = null;
var isSupportedProject = false;
if (_textBuffer.ContentType.IsOfType(RazorLanguage.ContentType) &&
(project = _projectService.GetHierarchy(_textBuffer)) != null)
{
// We expect the document to have a hierarchy even if it's not a real 'project'.
// However the hierarchy can be null when the document is in the process of closing.
isSupportedProject = _projectService.IsSupportedProject(project);
}
// For now we temporarily assume that the workspace is the default VS workspace.
var workspace = _workspace;
var changed = false;
changed |= isSupportedProject == _isSupportedProject;
changed |= workspace == _workspace;
if (changed)
(hierarchy = _projectService.GetHierarchy(_textBuffer)) != null)
{
_isSupportedProject = isSupportedProject;
_workspace = workspace;
projectPath = _projectService.GetProjectPath(hierarchy);
isSupportedProject = _projectService.IsSupportedProject(hierarchy);
}
return changed;
}
if (!isSupportedProject || projectPath == null)
{
return;
}
private void OnContextChanged()
var project = _projectManager.FindProject(projectPath);
var subscription = _projectManager.Subscribe();
subscription.ProjectChanged += Subscription_ProjectStateChanged;
_isSupportedProject = isSupportedProject;
_projectPath = projectPath;
_project = project;
_subscription = subscription;
}
private void OnContextChanged(ProjectSnapshot project)
{
_project = project;
var handler = ContextChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
private void Subscription_ProjectStateChanged(object sender, ProjectChangeEventArgs e)
{
if (_projectPath != null &&
string.Equals(_projectPath, e.Project.UnderlyingProject.FilePath, StringComparison.OrdinalIgnoreCase))
{
OnContextChanged(e.Project);
}
}
}
}

View File

@ -8,9 +8,9 @@ using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Projection;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
@ -21,6 +21,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
[Export(typeof(VisualStudioDocumentTrackerFactory))]
internal class DefaultVisualStudioDocumentTrackerFactory : VisualStudioDocumentTrackerFactory, IWpfTextViewConnectionListener
{
private readonly ProjectSnapshotManager _projectManager;
private readonly TextBufferProjectService _projectService;
private readonly Workspace _workspace;
@ -41,6 +42,34 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
_projectService = projectService;
_workspace = workspace;
_projectManager = workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<ProjectSnapshotManager>();
}
// This is only for testing. We want to avoid using the actual Roslyn GetService methods in unit tests.
internal DefaultVisualStudioDocumentTrackerFactory(
ProjectSnapshotManager projectManager,
TextBufferProjectService projectService,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (projectManager == null)
{
throw new ArgumentNullException(nameof(projectManager));
}
if (projectService == null)
{
throw new ArgumentNullException(nameof(projectService));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_projectManager = projectManager;
_projectService = projectService;
_workspace = workspace;
}
public Workspace Workspace => _workspace;
@ -96,7 +125,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
DefaultVisualStudioDocumentTracker tracker;
if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker))
{
tracker = new DefaultVisualStudioDocumentTracker(_projectService, _workspace, textBuffer);
tracker = new DefaultVisualStudioDocumentTracker(_projectManager, _projectService, _workspace, textBuffer);
textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
}

View File

@ -11,5 +11,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public abstract IVsHierarchy GetHierarchy(ITextBuffer textBuffer);
public abstract bool IsSupportedProject(IVsHierarchy hierarchy);
public abstract string GetProjectPath(IVsHierarchy hierarchy);
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public abstract bool IsSupportedProject { get; }
public abstract ProjectId ProjectId { get; }
public abstract Project Project { get; }
public abstract Workspace Workspace { get; }

View File

@ -18,8 +18,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces"/>
<PackageReference Include="Microsoft.Extensions.DependencyModel" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="xunit" />
</ItemGroup>

View File

@ -0,0 +1,157 @@
// 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.Linq;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public class DefaultProjectStateManagerTest
{
public DefaultProjectStateManagerTest()
{
Workspace = new AdhocWorkspace();
EmptySolution = Workspace.CurrentSolution.GetIsolatedSolution();
ProjectNumberOne = Workspace.CurrentSolution.AddProject("One", "One", LanguageNames.CSharp);
ProjectNumberTwo = ProjectNumberOne.Solution.AddProject("Two", "Two", LanguageNames.CSharp);
SolutionWithTwoProjects = ProjectNumberTwo.Solution;
ProjectNumberThree = EmptySolution.GetIsolatedSolution().AddProject("Three", "Three", LanguageNames.CSharp);
SolutionWithOneProject = ProjectNumberThree.Solution;
}
private Solution EmptySolution { get; }
private Solution SolutionWithOneProject { get; }
private Solution SolutionWithTwoProjects { get; }
private Project ProjectNumberOne { get; }
private Project ProjectNumberTwo { get; }
private Project ProjectNumberThree { get; }
private Workspace Workspace { get; }
[Theory]
[InlineData(WorkspaceChangeKind.SolutionAdded)]
[InlineData(WorkspaceChangeKind.SolutionChanged)]
[InlineData(WorkspaceChangeKind.SolutionCleared)]
[InlineData(WorkspaceChangeKind.SolutionReloaded)]
[InlineData(WorkspaceChangeKind.SolutionRemoved)]
public void WorkspaceChanged_SolutionEvents_AddsProjectsInSolution(WorkspaceChangeKind kind)
{
// Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace);
var e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
// Act
projectManager.Workspace_WorkspaceChanged(Workspace, e);
// Assert
Assert.Collection(
projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name),
p => Assert.Equal(ProjectNumberOne.Id, p.UnderlyingProject.Id),
p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id));
}
[Theory]
[InlineData(WorkspaceChangeKind.SolutionAdded)]
[InlineData(WorkspaceChangeKind.SolutionChanged)]
[InlineData(WorkspaceChangeKind.SolutionCleared)]
[InlineData(WorkspaceChangeKind.SolutionReloaded)]
[InlineData(WorkspaceChangeKind.SolutionRemoved)]
public void WorkspaceChanged_SolutionEvents_ClearsExistingProjects_AddsProjectsInSolution(WorkspaceChangeKind kind)
{
// Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace);
// Initialize with a project. This will get removed.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithOneProject);
projectManager.Workspace_WorkspaceChanged(Workspace, e);
e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
// Act
projectManager.Workspace_WorkspaceChanged(Workspace, e);
// Assert
Assert.Collection(
projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name),
p => Assert.Equal(ProjectNumberOne.Id, p.UnderlyingProject.Id),
p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id));
}
[Theory]
[InlineData(WorkspaceChangeKind.ProjectChanged)]
[InlineData(WorkspaceChangeKind.ProjectReloaded)]
public void WorkspaceChanged_ProjectChangeEvents_UpdatesProject(WorkspaceChangeKind kind)
{
// Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace);
// Initialize with some projects.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
projectManager.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);
// Assert
Assert.Collection(
projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name),
p =>
{
Assert.Equal(ProjectNumberOne.Id, p.UnderlyingProject.Id);
Assert.Equal("Changed", p.UnderlyingProject.AssemblyName);
},
p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id));
}
[Fact]
public void WorkspaceChanged_ProjectRemovedEvent_RemovesProject()
{
// Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace);
// Initialize with some projects project.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
projectManager.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);
// Assert
Assert.Collection(
projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name),
p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id));
}
[Fact]
public void WorkspaceChanged_ProjectAddedEvent_AddsProject()
{
// Arrange
var projectManager = new DefaultProjectSnapshotManager(Workspace);
var solution = SolutionWithOneProject;
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectAdded, oldSolution: EmptySolution, newSolution: solution, projectId: ProjectNumberThree.Id);
// Act
projectManager.Workspace_WorkspaceChanged(Workspace, e);
// Assert
Assert.Collection(
projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name),
p => Assert.Equal(ProjectNumberThree.Id, p.UnderlyingProject.Id));
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.ObjectModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
@ -17,9 +18,13 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
public class DefaultVisualStudioDocumentTrackerFactoryTest
{
private ProjectSnapshotManager ProjectManager { get; } = Mock.Of<ProjectSnapshotManager>(
p => p.FindProject(It.IsAny<string>()) == Mock.Of<ProjectSnapshot>() &&
p.Subscribe() == Mock.Of<ProjectSnapshotListener>());
private TextBufferProjectService ProjectService { get; } = Mock.Of<TextBufferProjectService>(
s => s.GetHierarchy(It.IsAny<ITextBuffer>()) == Mock.Of<IVsHierarchy>() &&
s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == true);
s => s.GetHierarchy(It.IsAny<ITextBuffer>()) == Mock.Of<IVsHierarchy>() &&
s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == true);
private Workspace Workspace { get; } = new AdhocWorkspace();
@ -31,7 +36,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void SubjectBuffersConnected_ForNonRazorTextBuffer_DoesNothing()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
@ -51,7 +56,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void SubjectBuffersConnected_ForRazorTextBufferWithoutTracker_CreatesTrackerAndTracksTextView()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
@ -73,7 +78,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void SubjectBuffersConnected_ForRazorTextBufferWithoutTracker_CreatesTrackerAndTracksTextView_ForMultipleBuffers()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
@ -103,7 +108,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void SubjectBuffersConnected_ForRazorTextBufferWithTracker_DoesNotAddDuplicateTextViewEntry()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
@ -113,7 +118,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]);
var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
@ -129,7 +134,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void SubjectBuffersConnected_ForRazorTextBufferWithTracker_AddsEntryForADifferentTextView()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace);
var textView1 = Mock.Of<IWpfTextView>();
var textView2 = Mock.Of<IWpfTextView>();
@ -140,7 +145,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]);
var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView1);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
@ -156,7 +161,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void SubjectBuffersDisconnected_ForAnyTextBufferWithTracker_RemovesTextView()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace);
var textView1 = Mock.Of<IWpfTextView>();
var textView2 = Mock.Of<IWpfTextView>();
@ -168,12 +173,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]);
var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView1);
tracker.TextViewsInternal.Add(textView2);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[1]);
tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[1]);
tracker.TextViewsInternal.Add(textView1);
tracker.TextViewsInternal.Add(textView2);
buffers[1].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
@ -193,7 +198,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void SubjectBuffersDisconnected_ForAnyTextBufferWithoutTracker_DoesNothing()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
@ -213,7 +218,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void GetTracker_ForRazorTextBufferWithTracker_ReturnsTheFirstTracker()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace);
var buffers = new Collection<ITextBuffer>()
{
@ -225,7 +230,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
var textView = Mock.Of<IWpfTextView>(v => v.BufferGraph == bufferGraph);
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]);
var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
@ -240,7 +245,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void GetTracker_WithoutRazorBuffer_ReturnsNull()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace);
var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace);
var buffers = new Collection<ITextBuffer>();

View File

@ -23,6 +23,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.analyzers" />

View File

@ -40,7 +40,7 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
}
}
public ProjectId ProjectId => _documentTracker.ProjectId;
public ProjectId ProjectId => _documentTracker.Project?.Id;
public Workspace Workspace => _documentTracker.Workspace;
@ -55,7 +55,6 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
public TagHelperCompletionService TagHelperCompletionService => RazorLanguageServices?.GetRequiredService<TagHelperCompletionService>();
public TagHelperFactsService TagHelperFactsService => RazorLanguageServices?.GetRequiredService<TagHelperFactsService>();
}
}