From 0a76ad70172f29721173b07ef2707bc558a4c367 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Wed, 8 Nov 2017 18:54:49 -0800 Subject: [PATCH] Added TagHelper discovery to Razor project systen --- .../ProjectSystem/DefaultProjectSnapshot.cs | 19 ++- .../DefaultProjectSnapshotManager.cs | 26 +++- .../DefaultProjectSnapshotWorker.cs | 13 +- .../DefaultProjectSnapshotWorkerFactory.cs | 3 +- .../ProjectSystem/ProjectSnapshot.cs | 4 + .../ProjectSystem/ProjectSnapshotManager.cs | 1 - .../ProjectSnapshotManagerBase.cs | 2 + .../ProjectSnapshotUpdateContext.cs | 4 + .../DefaultVisualStudioDocumentTracker.cs | 7 +- .../DefaultVisualStudioRazorParser.cs | 19 +-- .../TextBufferProjectService.cs | 2 + .../DefaultTagHelperResolverFactory.cs | 6 +- .../Editor/DefaultTextBufferProjectService.cs | 16 +++ ...tionUpdatesProjectSnapshotChangeTrigger.cs | 94 +++++++++++++ .../DefaultProjectSnapshotManagerTest.cs | 32 +++++ .../DefaultProjectSnapshotTest.cs | 109 +++++++++++++++ .../DefaultVisualStudioDocumentTrackerTest.cs | 72 ++++++++++ .../DefaultProjectSnapshotWorkerTest.cs | 26 +++- ...UpdatesProjectSnapshotChangeTriggerTest.cs | 130 ++++++++++++++++++ 19 files changed, 553 insertions(+), 32 deletions(-) create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs create mode 100644 test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs index ad43248fab..4edd7817c1 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs @@ -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(); } public override ProjectExtensibilityConfiguration Configuration { get; } public override Project UnderlyingProject { get; } + public override IReadOnlyList TagHelpers { get; } = Array.Empty(); + // 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); + } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs index e8f9ff1de3..6011052bad 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs @@ -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()) diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs index 78efdfbf04..fde6e33cb2 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs @@ -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; } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs index 40a5b3f5f4..c2dd1f4052 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs @@ -15,7 +15,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { return new DefaultProjectSnapshotWorker( languageServices.WorkspaceServices.GetRequiredService(), - languageServices.GetRequiredService()); + languageServices.GetRequiredService(), + languageServices.GetRequiredService()); } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs index 0a54f9b4f4..5976b72501 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs @@ -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 TagHelpers { get; } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs index d5a4c7a9ea..b9e39e00b1 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs @@ -12,6 +12,5 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public abstract event EventHandler Changed; public abstract IReadOnlyList Projects { get; } - } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs index 1e036b76f6..4dd392076b 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs @@ -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); diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs index bce7e39b3a..dec827bdd8 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs @@ -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 TagHelpers { get; set; } } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs index 3fb3896ca5..07d4d4ac74 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs @@ -74,11 +74,11 @@ namespace Microsoft.VisualStudio.Editor.Razor _textViews = new List(); } - internal override ProjectExtensibilityConfiguration Configuration => _project.Configuration; + internal override ProjectExtensibilityConfiguration Configuration => _project?.Configuration; public override EditorSettings EditorSettings => _editorSettingsManager.Current; - public override IReadOnlyList TagHelpers => Array.Empty(); + public override IReadOnlyList TagHelpers => _project?.TagHelpers ?? Array.Empty(); 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)) diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs index 99a7163ba8..f480dec0a2 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs @@ -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 } } - /// - /// 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. - /// private class VisualStudioTagHelperFeature : ITagHelperFeature { - private readonly ITextBuffer _textBuffer; + private readonly IReadOnlyList _tagHelpers; - public VisualStudioTagHelperFeature(ITextBuffer textBuffer) + public VisualStudioTagHelperFeature(IReadOnlyList tagHelpers) { - _textBuffer = textBuffer; + _tagHelpers = tagHelpers; } public RazorEngine Engine { get; set; } public IReadOnlyList GetDescriptors() { - if (_textBuffer.Properties.TryGetProperty(typeof(ITagHelperFeature), out ITagHelperFeature feature)) - { - return feature.GetDescriptors(); - } - - return Array.Empty(); + return _tagHelpers; } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/TextBufferProjectService.cs b/src/Microsoft.VisualStudio.Editor.Razor/TextBufferProjectService.cs index 84d5fab5d1..30ba31714c 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/TextBufferProjectService.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/TextBufferProjectService.cs @@ -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); } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs index f59d049bfa..d17beb9151 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs @@ -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(), Workspace); + var workspace = languageServices.WorkspaceServices.Workspace; + return new DefaultTagHelperResolver(workspace.Services.GetRequiredService(), workspace); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs index 3d8089af01..c60d748c3c 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs @@ -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; + } } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs new file mode 100644 index 0000000000..bc5f0d569b --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs @@ -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; + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs index 53e4b476c0..3edbef29ca 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs @@ -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() { diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs new file mode 100644 index 0000000000..be14178ae9 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs @@ -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(), + TagHelpers = Array.Empty(), + }; + + // 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; + } + } +} diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs index 6bc12c3af3..ac9c752069 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs @@ -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() { diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotWorkerTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotWorkerTest.cs index 1ab2f64753..8434f4fbb7 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotWorkerTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotWorkerTest.cs @@ -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(); - ConfigurationFactory = Mock.Of(f => f.GetConfigurationAsync(It.IsAny(), default(CancellationToken)) == CompletionSource.Task); + ConfigurationCompletionSource = new TaskCompletionSource(); + TagHelpersCompletionSource = new TaskCompletionSource(); + ConfigurationFactory = Mock.Of(f => f.GetConfigurationAsync(It.IsAny(), default(CancellationToken)) == ConfigurationCompletionSource.Task); + TagHelperResolver = Mock.Of(f => f.GetTagHelpersAsync(It.IsAny(), default(CancellationToken)) == TagHelpersCompletionSource.Task); } private Project Project { get; } private ProjectExtensibilityConfigurationFactory ConfigurationFactory { get; } - private TaskCompletionSource CompletionSource { get; } + private TagHelperResolver TagHelperResolver { get; } + + private TaskCompletionSource ConfigurationCompletionSource { get; } + + private TaskCompletionSource 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(); + var tagHelpers = Array.Empty(); + var tagHelperResolutionResult = new TagHelperResolutionResult(tagHelpers, Array.Empty()); // 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); } } } diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs new file mode 100644 index 0000000000..86ed4dae0d --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs @@ -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(MockBehavior.Strict); + buildManager + .Setup(b => b.AdviseUpdateSolutionEvents(It.IsAny(), out cookie)) + .Returns(VSConstants.S_OK) + .Verifiable(); + + var services = new Mock(); + services.Setup(s => s.GetService(It.Is(f => f == typeof(SVsSolutionBuildManager)))).Returns(buildManager.Object); + + var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, Mock.Of()); + + // Act + trigger.Initialize(Mock.Of()); + + // 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(MockBehavior.Strict); + buildManager + .Setup(b => b.AdviseUpdateSolutionEvents(It.IsAny(), out cookie)) + .Returns(VSConstants.S_OK); + + var services = new Mock(); + services.Setup(s => s.GetService(It.Is(f => f == typeof(SVsSolutionBuildManager)))).Returns(buildManager.Object); + + var projectService = new Mock(); + projectService.Setup(p => p.GetProjectName(It.IsAny())).Returns(expectedProjectName); + projectService.Setup(p => p.GetProjectPath(It.IsAny())).Returns(expectedProjectPath); + + var workspace = new AdhocWorkspace(); + CreateProjectInWorkspace(workspace, expectedProjectName, expectedProjectPath); + CreateProjectInWorkspace(workspace, "Test2", "Path/To/AnotherProject"); + + var called = false; + var projectManager = new Mock(); + projectManager.SetupGet(p => p.Workspace).Returns(workspace); + projectManager + .Setup(p => p.ProjectBuildComplete(It.IsAny())) + .Callback(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(), Mock.Of(), Mock.Of(), 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(MockBehavior.Strict); + buildManager + .Setup(b => b.AdviseUpdateSolutionEvents(It.IsAny(), out cookie)) + .Returns(VSConstants.S_OK); + + var services = new Mock(); + services.Setup(s => s.GetService(It.Is(f => f == typeof(SVsSolutionBuildManager)))).Returns(buildManager.Object); + + var projectService = new Mock(); + projectService.Setup(p => p.GetProjectName(It.IsAny())).Returns(expectedProjectName); + projectService.Setup(p => p.GetProjectPath(It.IsAny())).Returns(expectedProjectPath); + + var workspace = new AdhocWorkspace(); + CreateProjectInWorkspace(workspace, "Test2", "Path/To/AnotherProject"); + CreateProjectInWorkspace(workspace, "Test3", "Path/To/DifferenProject"); + + var projectManager = new Mock(); + projectManager.SetupGet(p => p.Workspace).Returns(workspace); + projectManager + .Setup(p => p.ProjectBuildComplete(It.IsAny())) + .Callback(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(), Mock.Of(), Mock.Of(), 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; + } + } +}