From 4b93741610ad4df73a1fcde4106931f261d35b6c Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Mon, 18 Dec 2017 15:59:22 -0800 Subject: [PATCH] Add Mac project build change trigger. - Added a Mac specific implementation of the project build change trigger. It applies to all types of project builds so we need to do a little extra filtering to ensure that we're not operating on a non-ASP.NET Core project. - Added tests to validate that the project build event fires correctly. #1851 --- .../Editor/DefaultTextBufferProjectService.cs | 2 +- .../ProjectBuildChangeTrigger.cs | 111 ++++++++++++++++++ ...UpdatesProjectSnapshotChangeTriggerTest.cs | 0 .../ProjectBuildChangeTriggerTest.cs | 107 +++++++++++++++++ 4 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs rename test/Microsoft.VisualStudio.LanguageServices.Razor.Test/{ProjectSystem => }/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs (100%) create mode 100644 test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.cs diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs index 01db0a8864..dddeed3e74 100644 --- a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs @@ -81,7 +81,7 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor.Editor } // VisualStudio for Mac only supports ASP.NET Core Razor. - public override bool IsSupportedProject(object project) => true; + public override bool IsSupportedProject(object project) => project is DotNetProject; public override string GetProjectName(object project) { diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs new file mode 100644 index 0000000000..88a112c84c --- /dev/null +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs @@ -0,0 +1,111 @@ +// 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; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.VisualStudio.Editor.Razor; +using MonoDevelop.Ide; +using MonoDevelop.Projects; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor +{ + [Export(typeof(ProjectSnapshotChangeTrigger))] + internal class ProjectBuildChangeTrigger : ProjectSnapshotChangeTrigger + { + private readonly TextBufferProjectService _projectService; + private readonly ForegroundDispatcher _foregroundDispatcher; + private ProjectSnapshotManagerBase _projectManager; + + [ImportingConstructor] + public ProjectBuildChangeTrigger(VisualStudioWorkspaceAccessor workspaceAccessor) + { + if (workspaceAccessor == null) + { + throw new ArgumentNullException(nameof(workspaceAccessor)); + } + + _foregroundDispatcher = workspaceAccessor.Workspace.Services.GetRequiredService(); + + var languageServices = workspaceAccessor.Workspace.Services.GetLanguageServices(RazorLanguage.Name); + _projectService = languageServices.GetRequiredService(); + } + + // Internal for testing + internal ProjectBuildChangeTrigger( + ForegroundDispatcher foregroundDispatcher, + TextBufferProjectService projectService, + ProjectSnapshotManagerBase projectManager) + { + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (projectService == null) + { + throw new ArgumentNullException(nameof(projectService)); + } + + if (projectManager == null) + { + throw new ArgumentNullException(nameof(projectManager)); + } + + _foregroundDispatcher = foregroundDispatcher; + _projectService = projectService; + _projectManager = projectManager; + } + + public override void Initialize(ProjectSnapshotManagerBase projectManager) + { + if (projectManager == null) + { + throw new ArgumentNullException(nameof(projectManager)); + } + + _projectManager = projectManager; + + IdeApp.ProjectOperations.EndBuild += ProjectOperations_EndBuild; + } + + // Internal for testing + internal void ProjectOperations_EndBuild(object sender, BuildEventArgs args) + { + if (args == null) + { + throw new ArgumentNullException(nameof(args)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + if (!args.Success) + { + // Build failed + return; + } + + var projectItem = args.SolutionItem; + if (!_projectService.IsSupportedProject(projectItem)) + { + // We're hooked into all build events, it's possible to get called with an unsupported project item type. + return; + } + + var projectName = _projectService.GetProjectName(projectItem); + var projectPath = _projectService.GetProjectPath(projectItem); + + // 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; + } + } + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs similarity index 100% rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs diff --git a/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.cs b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.cs new file mode 100644 index 0000000000..d2e4ec838c --- /dev/null +++ b/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.cs @@ -0,0 +1,107 @@ +// 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 MonoDevelop.Projects; +using Moq; +using Xunit; +using Project = Microsoft.CodeAnalysis.Project; + +namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor +{ + public class ProjectBuildChangeTriggerTest : ForegroundDispatcherTestBase + { + [ForegroundFact] + public void ProjectOperations_EndBuild_Invokes_ProjectBuildComplete() + { + // Arrange + var args = new BuildEventArgs(monitor: null, success: true); + var expectedProjectName = "Test1"; + var expectedProjectPath = "Path/To/Project"; + var projectService = CreateProjectService(expectedProjectName, expectedProjectPath); + var workspace = new AdhocWorkspace(); + CreateProjectInWorkspace(workspace, expectedProjectName, expectedProjectPath); + CreateProjectInWorkspace(workspace, "Test2", "Path/To/AnotherProject"); + + var projectManager = new Mock(MockBehavior.Strict); + projectManager.SetupGet(p => p.Workspace).Returns(workspace); + projectManager + .Setup(p => p.ProjectBuildComplete(It.IsAny())) + .Callback(c => Assert.Equal(expectedProjectName, c.Name)); + var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object); + + // Act + trigger.ProjectOperations_EndBuild(null, args); + + // Assert + projectManager.VerifyAll(); + } + + [ForegroundFact] + public void ProjectOperations_EndBuild_UntrackedProject_Noops() + { + // Arrange + var args = new BuildEventArgs(monitor: null, success: true); + var projectService = CreateProjectService("Test1", "Path/To/Project"); + var workspace = new AdhocWorkspace(); + CreateProjectInWorkspace(workspace, "Test2", "Path/To/AnotherProject"); + var projectManager = new Mock(); + projectManager.SetupGet(p => p.Workspace).Returns(workspace); + projectManager + .Setup(p => p.ProjectBuildComplete(It.IsAny())) + .Throws(); + var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object); + + // Act & Assert + trigger.ProjectOperations_EndBuild(null, args); + } + + [ForegroundFact] + public void ProjectOperations_EndBuild_BuildFailed_Noops() + { + // Arrange + var args = new BuildEventArgs(monitor: null, success: false); + var projectService = new Mock(); + projectService.Setup(p => p.IsSupportedProject(null)).Throws(); + var projectManager = new Mock(); + projectManager.SetupGet(p => p.Workspace).Throws(); + var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService.Object, projectManager.Object); + + // Act & Assert + trigger.ProjectOperations_EndBuild(null, args); + } + + [ForegroundFact] + public void ProjectOperations_EndBuild_UnsupportedProject_Noops() + { + // Arrange + var args = new BuildEventArgs(monitor: null, success: true); + var projectService = new Mock(); + projectService.Setup(p => p.IsSupportedProject(null)).Returns(false); + var projectManager = new Mock(); + projectManager.SetupGet(p => p.Workspace).Throws(); + var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService.Object, projectManager.Object); + + // Act & Assert + trigger.ProjectOperations_EndBuild(null, args); + } + + private static TextBufferProjectService CreateProjectService(string projectName, string projectPath) + { + var projectService = new Mock(); + projectService.Setup(p => p.GetProjectName(null)).Returns(projectName); + projectService.Setup(p => p.GetProjectPath(null)).Returns(projectPath); + projectService.Setup(p => p.IsSupportedProject(null)).Returns(true); + return projectService.Object; + } + + 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; + } + } +}