Added TagHelper discovery to Razor project systen

This commit is contained in:
Ajay Bhargav Baaskaran 2017-11-08 18:54:49 -08:00
parent 90120f6a3b
commit 0a76ad7017
19 changed files with 553 additions and 32 deletions

View File

@ -2,6 +2,9 @@
// 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.Linq;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
@ -41,6 +44,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
ComputedVersion = other.ComputedVersion;
Configuration = other.Configuration;
TagHelpers = other.TagHelpers;
}
private DefaultProjectSnapshot(ProjectSnapshotUpdateContext update, DefaultProjectSnapshot other)
@ -59,12 +63,15 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
ComputedVersion = update.UnderlyingProject.Version;
Configuration = update.Configuration;
TagHelpers = update.TagHelpers ?? Array.Empty<TagHelperDescriptor>();
}
public override ProjectExtensibilityConfiguration Configuration { get; }
public override Project UnderlyingProject { get; }
public override IReadOnlyList<TagHelperDescriptor> TagHelpers { get; } = Array.Empty<TagHelperDescriptor>();
// This is the version that the computed state is based on.
public VersionStamp? ComputedVersion { get; set; }
@ -92,7 +99,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return new DefaultProjectSnapshot(update, this);
}
public bool HasChangesComparedTo(ProjectSnapshot original)
public bool HasConfigurationChanged(ProjectSnapshot original)
{
if (original == null)
{
@ -101,5 +108,15 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return !object.Equals(Configuration, original.Configuration);
}
public bool HaveTagHelpersChanged(ProjectSnapshot original)
{
if (original == null)
{
throw new ArgumentNullException(nameof(original));
}
return !Enumerable.SequenceEqual(TagHelpers, original.TagHelpers);
}
}
}

View File

@ -149,10 +149,15 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Now we need to know if the changes that we applied are significant. If that's the case then
// we need to notify listeners.
if (snapshot.HasChangesComparedTo(original))
if (snapshot.HasConfigurationChanged(original))
{
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
}
if (snapshot.HaveTagHelpersChanged(original))
{
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.TagHelpersChanged));
}
}
}
@ -172,6 +177,25 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
}
public override void ProjectBuildComplete(Project underlyingProject)
{
if (underlyingProject == null)
{
throw new ArgumentNullException(nameof(underlyingProject));
}
if (_projects.TryGetValue(underlyingProject.Id, out var original))
{
// Doing an update to the project should keep computed values, but mark the project as dirty if the
// underlying project is newer.
var snapshot = original.WithProjectChange(underlyingProject);
_projects[underlyingProject.Id] = snapshot;
// Notify the background worker so it can trigger tag helper discovery.
NotifyBackgroundWorker(underlyingProject);
}
}
public override void ProjectsCleared()
{
foreach (var kvp in _projects.ToArray())

View File

@ -11,10 +11,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
private readonly ProjectExtensibilityConfigurationFactory _configurationFactory;
private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly TagHelperResolver _tagHelperResolver;
public DefaultProjectSnapshotWorker(
ForegroundDispatcher foregroundDispatcher,
ProjectExtensibilityConfigurationFactory configurationFactory)
ProjectExtensibilityConfigurationFactory configurationFactory,
TagHelperResolver tagHelperResolver)
{
if (foregroundDispatcher == null)
{
@ -26,8 +28,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(configurationFactory));
}
if (tagHelperResolver == null)
{
throw new ArgumentNullException(nameof(tagHelperResolver));
}
_foregroundDispatcher = foregroundDispatcher;
_configurationFactory = configurationFactory;
_tagHelperResolver = tagHelperResolver;
}
public override Task ProcessUpdateAsync(ProjectSnapshotUpdateContext update, CancellationToken cancellationToken = default(CancellationToken))
@ -54,6 +62,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var configuration = await _configurationFactory.GetConfigurationAsync(update.UnderlyingProject);
update.Configuration = configuration;
var result = await _tagHelperResolver.GetTagHelpersAsync(update.UnderlyingProject);
update.TagHelpers = result.Descriptors;
}
}
}

View File

@ -15,7 +15,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
return new DefaultProjectSnapshotWorker(
languageServices.WorkspaceServices.GetRequiredService<ForegroundDispatcher>(),
languageServices.GetRequiredService<ProjectExtensibilityConfigurationFactory>());
languageServices.GetRequiredService<ProjectExtensibilityConfigurationFactory>(),
languageServices.GetRequiredService<TagHelperResolver>());
}
}
}

View File

@ -2,6 +2,8 @@
// 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 Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
@ -10,5 +12,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public abstract ProjectExtensibilityConfiguration Configuration { get; }
public abstract Project UnderlyingProject { get; }
public abstract IReadOnlyList<TagHelperDescriptor> TagHelpers { get; }
}
}

View File

@ -12,6 +12,5 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public abstract event EventHandler<ProjectChangeEventArgs> Changed;
public abstract IReadOnlyList<ProjectSnapshot> Projects { get; }
}
}

View File

@ -17,6 +17,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public abstract void ProjectRemoved(Project underlyingProject);
public abstract void ProjectBuildComplete(Project underlyingProject);
public abstract void ProjectsCleared();
public abstract void ReportError(Exception exception);

View File

@ -2,6 +2,8 @@
// 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 Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
@ -20,5 +22,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public Project UnderlyingProject { get; }
public ProjectExtensibilityConfiguration Configuration { get; set; }
public IReadOnlyList<TagHelperDescriptor> TagHelpers { get; set; }
}
}

View File

@ -74,11 +74,11 @@ namespace Microsoft.VisualStudio.Editor.Razor
_textViews = new List<ITextView>();
}
internal override ProjectExtensibilityConfiguration Configuration => _project.Configuration;
internal override ProjectExtensibilityConfiguration Configuration => _project?.Configuration;
public override EditorSettings EditorSettings => _editorSettingsManager.Current;
public override IReadOnlyList<TagHelperDescriptor> TagHelpers => Array.Empty<TagHelperDescriptor>();
public override IReadOnlyList<TagHelperDescriptor> TagHelpers => _project?.TagHelpers ?? Array.Empty<TagHelperDescriptor>();
public override bool IsSupportedProject => _isSupportedProject;
@ -167,7 +167,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e)
// Internal for testing
internal void ProjectManager_Changed(object sender, ProjectChangeEventArgs e)
{
if (_projectPath != null &&
string.Equals(_projectPath, e.Project.UnderlyingProject.FilePath, StringComparison.OrdinalIgnoreCase))

View File

@ -387,7 +387,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
private void ConfigureTemplateEngine(IRazorEngineBuilder builder)
{
builder.Features.Add(new VisualStudioParserOptionsFeature(_documentTracker.EditorSettings));
builder.Features.Add(new VisualStudioTagHelperFeature(TextBuffer));
builder.Features.Add(new VisualStudioTagHelperFeature(_documentTracker.TagHelpers));
}
private class VisualStudioParserOptionsFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature
@ -408,29 +408,20 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
/// <summary>
/// This class will cease to be useful once we control TagHelper discovery. For now, it delegates discovery
/// to ITagHelperFeature's that exist on the text buffer.
/// </summary>
private class VisualStudioTagHelperFeature : ITagHelperFeature
{
private readonly ITextBuffer _textBuffer;
private readonly IReadOnlyList<TagHelperDescriptor> _tagHelpers;
public VisualStudioTagHelperFeature(ITextBuffer textBuffer)
public VisualStudioTagHelperFeature(IReadOnlyList<TagHelperDescriptor> tagHelpers)
{
_textBuffer = textBuffer;
_tagHelpers = tagHelpers;
}
public RazorEngine Engine { get; set; }
public IReadOnlyList<TagHelperDescriptor> GetDescriptors()
{
if (_textBuffer.Properties.TryGetProperty(typeof(ITagHelperFeature), out ITagHelperFeature feature))
{
return feature.GetDescriptors();
}
return Array.Empty<TagHelperDescriptor>();
return _tagHelpers;
}
}

View File

@ -12,5 +12,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
public abstract bool IsSupportedProject(object project);
public abstract string GetProjectPath(object project);
public abstract string GetProjectName(object project);
}
}

View File

@ -11,12 +11,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
[ExportLanguageServiceFactory(typeof(TagHelperResolver), RazorLanguage.Name, ServiceLayer.Default)]
internal class DefaultTagHelperResolverFactory : ILanguageServiceFactory
{
[Import]
public VisualStudioWorkspace Workspace { get; set; }
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{
return new DefaultTagHelperResolver(Workspace.Services.GetRequiredService<ErrorReporter>(), Workspace);
var workspace = languageServices.WorkspaceServices.Workspace;
return new DefaultTagHelperResolver(workspace.Services.GetRequiredService<ErrorReporter>(), workspace);
}
}
}

View File

@ -100,5 +100,21 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
return false;
}
public override string GetProjectName(object project)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
var hierarchy = (IVsHierarchy)project;
if (ErrorHandler.Failed(hierarchy.GetProperty((uint)VSConstants.VSITEMID.Root, (int)__VSHPROPID.VSHPROPID_Name, out var name)))
{
return null;
}
return (string)name;
}
}
}

View File

@ -0,0 +1,94 @@
// 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.ComponentModel.Composition;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Editor.Razor;
using System.Runtime.InteropServices;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
[Export(typeof(ProjectSnapshotChangeTrigger))]
internal class VsSolutionUpdatesProjectSnapshotChangeTrigger : ProjectSnapshotChangeTrigger, IVsUpdateSolutionEvents2
{
private readonly IServiceProvider _services;
private readonly TextBufferProjectService _projectService;
private ProjectSnapshotManagerBase _projectManager;
[ImportingConstructor]
public VsSolutionUpdatesProjectSnapshotChangeTrigger(
[Import(typeof(SVsServiceProvider))] IServiceProvider services,
TextBufferProjectService projectService)
{
_services = services;
_projectService = projectService;
}
public override void Initialize(ProjectSnapshotManagerBase projectManager)
{
_projectManager = projectManager;
// Attach the event sink to solution update events.
var solutionBuildManager = _services.GetService(typeof(SVsSolutionBuildManager)) as IVsSolutionBuildManager;
if (solutionBuildManager != null)
{
// We expect this to be called only once. So we don't need to Unadvise.
var hr = solutionBuildManager.AdviseUpdateSolutionEvents(this, out var cookie);
Marshal.ThrowExceptionForHR(hr);
}
}
public int UpdateSolution_Begin(ref int pfCancelUpdate)
{
return VSConstants.S_OK;
}
public int UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand)
{
return VSConstants.S_OK;
}
public int UpdateSolution_StartUpdate(ref int pfCancelUpdate)
{
return VSConstants.S_OK;
}
public int UpdateSolution_Cancel()
{
return VSConstants.S_OK;
}
public int OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy)
{
return VSConstants.S_OK;
}
public int UpdateProjectCfg_Begin(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, ref int pfCancel)
{
return VSConstants.S_OK;
}
public int UpdateProjectCfg_Done(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, int fSuccess, int fCancel)
{
var projectName = _projectService.GetProjectName(pHierProj);
var projectPath = _projectService.GetProjectPath(pHierProj);
// Get the corresponding roslyn project by matching the project name and the project path.
foreach (var project in _projectManager.Workspace.CurrentSolution.Projects)
{
if (string.Equals(projectName, project.Name, StringComparison.Ordinal) &&
string.Equals(projectPath, project.FilePath, StringComparison.OrdinalIgnoreCase))
{
_projectManager.ProjectBuildComplete(project);
break;
}
}
return VSConstants.S_OK;
}
}
}

View File

@ -209,6 +209,38 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.False(ProjectManager.WorkerStarted);
}
[Fact]
public void ProjectBuildComplete_KnownProject_NotifiesBackgroundWorker()
{
// Arrange
var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp);
ProjectManager.ProjectAdded(project);
ProjectManager.Reset();
// Act
ProjectManager.ProjectBuildComplete(project);
// Assert
Assert.False(ProjectManager.ListenersNotified);
Assert.True(ProjectManager.WorkerStarted);
}
[Fact]
public void ProjectBuildComplete_IgnoresUnknownProject()
{
// Arrange
var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp);
// Act
ProjectManager.ProjectBuildComplete(project);
// Assert
Assert.Empty(ProjectManager.Projects);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
}
[Fact]
public void ProjectRemoved_RemovesProject_NotifiesListeners_DoesNotStartBackgroundWorker()
{

View File

@ -0,0 +1,109 @@
// 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.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Moq;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public class DefaultProjectSnapshotTest
{
[Fact]
public void WithProjectChange_WithProject_CreatesSnapshot_UpdatesUnderlyingProject()
{
// Arrange
var underlyingProject = GetProject("Test1");
var original = new DefaultProjectSnapshot(underlyingProject);
var anotherProject = GetProject("Test1");
// Act
var snapshot = original.WithProjectChange(anotherProject);
// Assert
Assert.Same(anotherProject, snapshot.UnderlyingProject);
Assert.Equal(original.ComputedVersion, snapshot.ComputedVersion);
Assert.Equal(original.Configuration, snapshot.Configuration);
Assert.Equal(original.TagHelpers, snapshot.TagHelpers);
}
[Fact]
public void WithProjectChange_WithProject_CreatesSnapshot_UpdatesValues()
{
// Arrange
var underlyingProject = GetProject("Test1");
var original = new DefaultProjectSnapshot(underlyingProject);
var anotherProject = GetProject("Test1");
var update = new ProjectSnapshotUpdateContext(anotherProject)
{
Configuration = Mock.Of<ProjectExtensibilityConfiguration>(),
TagHelpers = Array.Empty<TagHelperDescriptor>(),
};
// Act
var snapshot = original.WithProjectChange(update);
// Assert
Assert.Same(original.UnderlyingProject, snapshot.UnderlyingProject);
Assert.Equal(update.UnderlyingProject.Version, snapshot.ComputedVersion);
Assert.Same(update.Configuration, snapshot.Configuration);
Assert.Same(update.TagHelpers, snapshot.TagHelpers);
}
[Fact]
public void HaveTagHelpersChanged_NoUpdatesToTagHelpers_ReturnsFalse()
{
// Arrange
var underlyingProject = GetProject("Test1");
var original = new DefaultProjectSnapshot(underlyingProject);
var anotherProject = GetProject("Test1");
var update = new ProjectSnapshotUpdateContext(anotherProject);
var snapshot = original.WithProjectChange(update);
// Act
var result = snapshot.HaveTagHelpersChanged(original);
// Assert
Assert.False(result);
}
[Fact]
public void HaveTagHelpersChanged_TagHelpersUpdated_ReturnsTrue()
{
// Arrange
var underlyingProject = GetProject("Test1");
var original = new DefaultProjectSnapshot(underlyingProject);
var anotherProject = GetProject("Test1");
var update = new ProjectSnapshotUpdateContext(anotherProject)
{
TagHelpers = new[]
{
TagHelperDescriptorBuilder.Create("One", "TestAssembly").Build(),
TagHelperDescriptorBuilder.Create("Two", "TestAssembly").Build(),
},
};
var snapshot = original.WithProjectChange(update);
// Act
var result = snapshot.HaveTagHelpersChanged(original);
// Assert
Assert.True(result);
}
private Project GetProject(string name)
{
var project = new AdhocWorkspace().AddProject(name, LanguageNames.CSharp);
return project;
}
}
}

View File

@ -38,6 +38,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var called = false;
documentTracker.ContextChanged += (sender, args) =>
{
Assert.Equal(ContextChangeKind.EditorSettingsChanged, args.Kind);
called = true;
Assert.Equal(ContextChangeKind.EditorSettingsChanged, args.Kind);
};
@ -49,6 +50,77 @@ namespace Microsoft.VisualStudio.Editor.Razor
Assert.True(called);
}
[Fact]
public void ProjectManager_Changed_ProjectChanged_TriggersContextChanged()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
var project = new AdhocWorkspace().AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), "Test1", "TestAssembly", LanguageNames.CSharp, filePath: "C:/Some/Path/TestProject.csproj"));
var projectSnapshot = new DefaultProjectSnapshot(project);
var projectChangedArgs = new ProjectChangeEventArgs(projectSnapshot, ProjectChangeKind.Changed);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
{
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
called = true;
};
// Act
documentTracker.ProjectManager_Changed(null, projectChangedArgs);
// Assert
Assert.True(called);
}
[Fact]
public void ProjectManager_Changed_TagHelpersChanged_TriggersContextChanged()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
var project = new AdhocWorkspace().AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), "Test1", "TestAssembly", LanguageNames.CSharp, filePath: "C:/Some/Path/TestProject.csproj"));
var projectSnapshot = new DefaultProjectSnapshot(project);
var projectChangedArgs = new ProjectChangeEventArgs(projectSnapshot, ProjectChangeKind.TagHelpersChanged);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
{
Assert.Equal(ContextChangeKind.TagHelpersChanged, args.Kind);
called = true;
};
// Act
documentTracker.ProjectManager_Changed(null, projectChangedArgs);
// Assert
Assert.True(called);
}
[Fact]
public void ProjectManager_Changed_IgnoresUnknownProject()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
var project = new AdhocWorkspace().AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), "Test1", "TestAssembly", LanguageNames.CSharp, filePath: "C:/Some/Other/Path/TestProject.csproj"));
var projectSnapshot = new DefaultProjectSnapshot(project);
var projectChangedArgs = new ProjectChangeEventArgs(projectSnapshot, ProjectChangeKind.Changed);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
{
called = true;
};
// Act
documentTracker.ProjectManager_Changed(null, projectChangedArgs);
// Assert
Assert.False(called);
}
[Fact]
public void Subscribe_SetsSupportedProjectAndTriggersContextChanged()
{

View File

@ -1,8 +1,11 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Moq;
using Xunit;
@ -14,40 +17,51 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
Project = new AdhocWorkspace().AddProject("Test1", LanguageNames.CSharp);
CompletionSource = new TaskCompletionSource<ProjectExtensibilityConfiguration>();
ConfigurationFactory = Mock.Of<ProjectExtensibilityConfigurationFactory>(f => f.GetConfigurationAsync(It.IsAny<Project>(), default(CancellationToken)) == CompletionSource.Task);
ConfigurationCompletionSource = new TaskCompletionSource<ProjectExtensibilityConfiguration>();
TagHelpersCompletionSource = new TaskCompletionSource<TagHelperResolutionResult>();
ConfigurationFactory = Mock.Of<ProjectExtensibilityConfigurationFactory>(f => f.GetConfigurationAsync(It.IsAny<Project>(), default(CancellationToken)) == ConfigurationCompletionSource.Task);
TagHelperResolver = Mock.Of<TagHelperResolver>(f => f.GetTagHelpersAsync(It.IsAny<Project>(), default(CancellationToken)) == TagHelpersCompletionSource.Task);
}
private Project Project { get; }
private ProjectExtensibilityConfigurationFactory ConfigurationFactory { get; }
private TaskCompletionSource<ProjectExtensibilityConfiguration> CompletionSource { get; }
private TagHelperResolver TagHelperResolver { get; }
private TaskCompletionSource<ProjectExtensibilityConfiguration> ConfigurationCompletionSource { get; }
private TaskCompletionSource<TagHelperResolutionResult> TagHelpersCompletionSource { get; }
[ForegroundFact]
public async Task ProcessUpdateAsync_DoesntBlockForegroundThread()
{
// Arrange
var worker = new DefaultProjectSnapshotWorker(Dispatcher, ConfigurationFactory);
var worker = new DefaultProjectSnapshotWorker(Dispatcher, ConfigurationFactory, TagHelperResolver);
var context = new ProjectSnapshotUpdateContext(Project);
var configuration = Mock.Of<ProjectExtensibilityConfiguration>();
var tagHelpers = Array.Empty<TagHelperDescriptor>();
var tagHelperResolutionResult = new TagHelperResolutionResult(tagHelpers, Array.Empty<RazorDiagnostic>());
// Act 1 -- We want to verify that this doesn't block the main thread
var task = worker.ProcessUpdateAsync(context);
// Assert 1
//
// We haven't let the background task proceed yet, so this is still null.
// We haven't let the background task proceed yet, so these are still null.
Assert.Null(context.Configuration);
Assert.Null(context.TagHelpers);
// Act 2 - Ok let's go
CompletionSource.SetResult(configuration);
ConfigurationCompletionSource.SetResult(configuration);
TagHelpersCompletionSource.SetResult(tagHelperResolutionResult);
await task;
// Assert 2
Assert.Same(configuration, context.Configuration);
Assert.Same(tagHelpers, context.TagHelpers);
}
}
}

View File

@ -0,0 +1,130 @@
// 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Shell.Interop;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public class VsSolutionUpdatesProjectSnapshotChangeTriggerTest
{
[Fact]
public void Initialize_AttachesEventSink()
{
// Arrange
uint cookie;
var buildManager = new Mock<IVsSolutionBuildManager>(MockBehavior.Strict);
buildManager
.Setup(b => b.AdviseUpdateSolutionEvents(It.IsAny<VsSolutionUpdatesProjectSnapshotChangeTrigger>(), out cookie))
.Returns(VSConstants.S_OK)
.Verifiable();
var services = new Mock<IServiceProvider>();
services.Setup(s => s.GetService(It.Is<Type>(f => f == typeof(SVsSolutionBuildManager)))).Returns(buildManager.Object);
var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, Mock.Of<TextBufferProjectService>());
// Act
trigger.Initialize(Mock.Of<ProjectSnapshotManagerBase>());
// Assert
buildManager.Verify();
}
[Fact]
public void UpdateProjectCfg_Done_KnownProject_Invokes_ProjectBuildComplete()
{
// Arrange
var expectedProjectName = "Test1";
var expectedProjectPath = "Path/To/Project";
uint cookie;
var buildManager = new Mock<IVsSolutionBuildManager>(MockBehavior.Strict);
buildManager
.Setup(b => b.AdviseUpdateSolutionEvents(It.IsAny<VsSolutionUpdatesProjectSnapshotChangeTrigger>(), out cookie))
.Returns(VSConstants.S_OK);
var services = new Mock<IServiceProvider>();
services.Setup(s => s.GetService(It.Is<Type>(f => f == typeof(SVsSolutionBuildManager)))).Returns(buildManager.Object);
var projectService = new Mock<TextBufferProjectService>();
projectService.Setup(p => p.GetProjectName(It.IsAny<IVsHierarchy>())).Returns(expectedProjectName);
projectService.Setup(p => p.GetProjectPath(It.IsAny<IVsHierarchy>())).Returns(expectedProjectPath);
var workspace = new AdhocWorkspace();
CreateProjectInWorkspace(workspace, expectedProjectName, expectedProjectPath);
CreateProjectInWorkspace(workspace, "Test2", "Path/To/AnotherProject");
var called = false;
var projectManager = new Mock<ProjectSnapshotManagerBase>();
projectManager.SetupGet(p => p.Workspace).Returns(workspace);
projectManager
.Setup(p => p.ProjectBuildComplete(It.IsAny<Project>()))
.Callback<Project>(c =>
{
called = true;
Assert.Equal(expectedProjectName, c.Name);
});
var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, projectService.Object);
trigger.Initialize(projectManager.Object);
// Act
trigger.UpdateProjectCfg_Done(Mock.Of<IVsHierarchy>(), Mock.Of<IVsCfg>(), Mock.Of<IVsCfg>(), 0, 0, 0);
// Assert
Assert.True(called);
}
[Fact]
public void UpdateProjectCfg_Done_UnknownProject_DoesNotInvoke_ProjectBuildComplete()
{
// Arrange
var expectedProjectName = "Test1";
var expectedProjectPath = "Path/To/Project";
uint cookie;
var buildManager = new Mock<IVsSolutionBuildManager>(MockBehavior.Strict);
buildManager
.Setup(b => b.AdviseUpdateSolutionEvents(It.IsAny<VsSolutionUpdatesProjectSnapshotChangeTrigger>(), out cookie))
.Returns(VSConstants.S_OK);
var services = new Mock<IServiceProvider>();
services.Setup(s => s.GetService(It.Is<Type>(f => f == typeof(SVsSolutionBuildManager)))).Returns(buildManager.Object);
var projectService = new Mock<TextBufferProjectService>();
projectService.Setup(p => p.GetProjectName(It.IsAny<IVsHierarchy>())).Returns(expectedProjectName);
projectService.Setup(p => p.GetProjectPath(It.IsAny<IVsHierarchy>())).Returns(expectedProjectPath);
var workspace = new AdhocWorkspace();
CreateProjectInWorkspace(workspace, "Test2", "Path/To/AnotherProject");
CreateProjectInWorkspace(workspace, "Test3", "Path/To/DifferenProject");
var projectManager = new Mock<ProjectSnapshotManagerBase>();
projectManager.SetupGet(p => p.Workspace).Returns(workspace);
projectManager
.Setup(p => p.ProjectBuildComplete(It.IsAny<Project>()))
.Callback<Project>(c =>
{
throw new InvalidOperationException("This should not be called.");
});
var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, projectService.Object);
trigger.Initialize(projectManager.Object);
// Act & Assert - Does not throw
trigger.UpdateProjectCfg_Done(Mock.Of<IVsHierarchy>(), Mock.Of<IVsCfg>(), Mock.Of<IVsCfg>(), 0, 0, 0);
}
private static AdhocWorkspace CreateProjectInWorkspace(AdhocWorkspace workspace, string name, string path)
{
workspace.AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), name, "TestAssembly", LanguageNames.CSharp, filePath: path));
return workspace;
}
}
}