From 61245c99888a1f727523740eca940e04773ce0aa Mon Sep 17 00:00:00 2001 From: Artur Spychaj Date: Wed, 20 Jan 2016 10:16:03 -0800 Subject: [PATCH] Extract ResetInteractive base class ResetInteractive base class is not coupled to VS and therefore is much easier to test. --- .../Interactive/ResetInteractive.cs | 137 +++++++++++++++++ .../Core/InteractiveEditorFeatures.csproj | 3 +- ...ractiveEditorFeaturesResources.Designer.cs | 18 +++ .../InteractiveEditorFeaturesResources.resx | 6 + .../Core/Def/ServicesVSResources.Designer.cs | 18 --- .../Core/Def/ServicesVSResources.resx | 6 - .../Interactive/VsInteractiveWindowPackage.cs | 13 +- ...etInteractive.cs => VsResetInteractive.cs} | 141 +++++++----------- .../VisualStudioInteractiveServices.csproj | 2 +- 9 files changed, 225 insertions(+), 119 deletions(-) create mode 100644 src/Interactive/EditorFeatures/Core/Extensibility/Interactive/ResetInteractive.cs rename src/VisualStudio/InteractiveServices/Interactive/{ResetInteractive.cs => VsResetInteractive.cs} (59%) diff --git a/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/ResetInteractive.cs b/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/ResetInteractive.cs new file mode 100644 index 00000000000..6d8e9eed9b9 --- /dev/null +++ b/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/ResetInteractive.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft. 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.Linq; +using System.Threading.Tasks; +using Roslyn.Utilities; +using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Interactive; +using Microsoft.VisualStudio.InteractiveWindow; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Editor; + +namespace Microsoft.VisualStudio.LanguageServices.Interactive +{ + /// + /// ResetInteractive class that implements base functionality for reset interactive command. + /// Does not depend on VS classes to make it easier to stub out and test. + /// + internal abstract class ResetInteractive + { + private readonly Func _createReference; + + private readonly Func _createImport; + + internal event EventHandler ExecutionCompleted; + + internal ResetInteractive(Func createReference, Func createImport) + { + _createReference = createReference; + _createImport = createImport; + } + + internal Task Execute(IInteractiveWindow interactiveWindow, string title) + { + ImmutableArray references, referenceSearchPaths, sourceSearchPaths, namespacesToImport; + string projectDirectory; + + if (GetProjectProperties(out references, out referenceSearchPaths, out sourceSearchPaths, out namespacesToImport, out projectDirectory)) + { + // Now, we're going to do a bunch of async operations. So create a wait + // indicator so the user knows something is happening, and also so they cancel. + var waitIndicator = GetWaitIndicator(); + var waitContext = waitIndicator.StartWait(title, InteractiveEditorFeaturesResources.BuildingProject, allowCancel: true); + + var resetInteractiveTask = ResetInteractiveAsync( + interactiveWindow, + references, + referenceSearchPaths, + sourceSearchPaths, + namespacesToImport, + projectDirectory, + waitContext); + + // Once we're done resetting, dismiss the wait indicator and focus the REPL window. + return resetInteractiveTask.SafeContinueWith( + _ => + { + waitContext.Dispose(); + ExecutionCompleted?.Invoke(this, new EventArgs()); + }, + TaskScheduler.FromCurrentSynchronizationContext()); + } + + return Task.CompletedTask; + } + + private async Task ResetInteractiveAsync( + IInteractiveWindow interactiveWindow, + ImmutableArray referencePaths, + ImmutableArray referenceSearchPaths, + ImmutableArray sourceSearchPaths, + ImmutableArray namespacesToImport, + string projectDirectory, + IWaitContext waitContext) + { + // First, open the repl window. + IInteractiveEvaluator evaluator = interactiveWindow.Evaluator; + + // If the user hits the cancel button on the wait indicator, then we want to stop the + // build. + waitContext.CancellationToken.Register(() => + CancelBuildProject(), useSynchronizationContext: true); + + // First, start a build. + // If the build fails do not reset the REPL. + var builtSuccessfully = await BuildProject().ConfigureAwait(true); + if (!builtSuccessfully) + { + return; + } + + // Then reset the REPL + waitContext.Message = InteractiveEditorFeaturesResources.ResettingInteractive; + await interactiveWindow.Operations.ResetAsync(initialize: true).ConfigureAwait(true); + + // TODO: load context from an rsp file. + + // Now send the reference paths we've collected to the repl. + // The SetPathsAsync method is not available through an Interface. + // Execute the method only if the cast to a concrete InteractiveEvaluator succeeds. + InteractiveEvaluator interactiveEvaluator = evaluator as InteractiveEvaluator; + if (interactiveEvaluator != null) + { + await interactiveEvaluator.SetPathsAsync(referenceSearchPaths, sourceSearchPaths, projectDirectory).ConfigureAwait(true); + } + + await interactiveWindow.SubmitAsync(new[] + { + referencePaths.Select(_createReference).Join("\r\n"), + namespacesToImport.Select(_createImport).Join("\r\n") + }).ConfigureAwait(true); + } + + /// + /// Gets the properties of the currently selected projects necessary for reset. + /// + protected abstract bool GetProjectProperties( + out ImmutableArray references, + out ImmutableArray referenceSearchPaths, + out ImmutableArray sourceSearchPaths, + out ImmutableArray namespacesToImport, + out string projectDirectory); + + /// + /// A method that should trigger an async project build. + /// + /// Whether or not the build was successful. + protected abstract Task BuildProject(); + + /// + /// A method that should trigger a project cancellation. + /// + protected abstract void CancelBuildProject(); + + protected abstract IWaitIndicator GetWaitIndicator(); + } +} diff --git a/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj b/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj index 99706d0b218..c9ce0690638 100644 --- a/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj +++ b/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj @@ -107,7 +107,7 @@ - + @@ -134,6 +134,7 @@ + diff --git a/src/Interactive/EditorFeatures/Core/InteractiveEditorFeaturesResources.Designer.cs b/src/Interactive/EditorFeatures/Core/InteractiveEditorFeaturesResources.Designer.cs index 1cd9ecdd93f..4bbe7f1c2eb 100644 --- a/src/Interactive/EditorFeatures/Core/InteractiveEditorFeaturesResources.Designer.cs +++ b/src/Interactive/EditorFeatures/Core/InteractiveEditorFeaturesResources.Designer.cs @@ -60,6 +60,15 @@ internal class InteractiveEditorFeaturesResources { } } + /// + /// Looks up a localized string similar to Building Project. + /// + internal static string BuildingProject { + get { + return ResourceManager.GetString("BuildingProject", resourceCulture); + } + } + /// /// Looks up a localized string similar to Print a list of referenced assemblies.. /// @@ -105,6 +114,15 @@ internal class InteractiveEditorFeaturesResources { } } + /// + /// Looks up a localized string similar to Resetting Interactive. + /// + internal static string ResettingInteractive { + get { + return ResourceManager.GetString("ResettingInteractive", resourceCulture); + } + } + /// /// Looks up a localized string similar to The CurrentWindow property may only be assigned once.. /// diff --git a/src/Interactive/EditorFeatures/Core/InteractiveEditorFeaturesResources.resx b/src/Interactive/EditorFeatures/Core/InteractiveEditorFeaturesResources.resx index fa0f4268930..e49a2d5fee7 100644 --- a/src/Interactive/EditorFeatures/Core/InteractiveEditorFeaturesResources.resx +++ b/src/Interactive/EditorFeatures/Core/InteractiveEditorFeaturesResources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Building Project + Print a list of referenced assemblies. @@ -132,6 +135,9 @@ Resetting execution engine. + + Resetting Interactive + The CurrentWindow property may only be assigned once. diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs b/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs index 16adccc86aa..2c477adbc08 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs +++ b/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs @@ -105,15 +105,6 @@ internal class ServicesVSResources { } } - /// - /// Looks up a localized string similar to Building Project. - /// - internal static string BuildingProject { - get { - return ResourceManager.GetString("BuildingProject", resourceCulture); - } - } - /// /// Looks up a localized string similar to C#/VB Build Table Data Source. /// @@ -1002,15 +993,6 @@ internal class ServicesVSResources { } } - /// - /// Looks up a localized string similar to Resetting Interactive. - /// - internal static string ResettingInteractive { - get { - return ResourceManager.GetString("ResettingInteractive", resourceCulture); - } - } - /// /// Looks up a localized string similar to Resolving breakpoint location.... /// diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.resx b/src/VisualStudio/Core/Def/ServicesVSResources.resx index 68348ab8f11..602ce878bf1 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.resx +++ b/src/VisualStudio/Core/Def/ServicesVSResources.resx @@ -135,12 +135,6 @@ File name must have the "{0}" extension. - - Building Project - - - Resetting Interactive - Debugger diff --git a/src/VisualStudio/InteractiveServices/Interactive/VsInteractiveWindowPackage.cs b/src/VisualStudio/InteractiveServices/Interactive/VsInteractiveWindowPackage.cs index 7f7c9ac1b3e..a3a36561e24 100644 --- a/src/VisualStudio/InteractiveServices/Interactive/VsInteractiveWindowPackage.cs +++ b/src/VisualStudio/InteractiveServices/Interactive/VsInteractiveWindowPackage.cs @@ -85,7 +85,7 @@ private void InitializeResetInteractiveFromProjectCommand(OleMenuCommandService var resetInteractiveFromProjectCommand = new OleMenuCommand( (sender, args) => { - var resetInteractive = new ResetInteractive( + var resetInteractive = new VsResetInteractive( (DTE)this.GetService(typeof(SDTE)), _componentModel, (IVsMonitorSelection)this.GetService(typeof(SVsShellMonitorSelection)), @@ -95,7 +95,16 @@ private void InitializeResetInteractiveFromProjectCommand(OleMenuCommandService var vsInteractiveWindow = _interactiveWindowProvider.Open(instanceId: 0, focus: true); - resetInteractive.Execute(vsInteractiveWindow, LanguageName + " Interactive"); + EventHandler focusWindow = null; + focusWindow = (s, e) => + { + // We have to set focus to the Interactive Window *after* the wait indicator is dismissed. + vsInteractiveWindow.Show(focus: true); + resetInteractive.ExecutionCompleted -= focusWindow; + }; + + resetInteractive.Execute(vsInteractiveWindow.InteractiveWindow, LanguageName + " Interactive"); + resetInteractive.ExecutionCompleted += focusWindow; }, GetResetInteractiveFromProjectCommandID()); diff --git a/src/VisualStudio/InteractiveServices/Interactive/ResetInteractive.cs b/src/VisualStudio/InteractiveServices/Interactive/VsResetInteractive.cs similarity index 59% rename from src/VisualStudio/InteractiveServices/Interactive/ResetInteractive.cs rename to src/VisualStudio/InteractiveServices/Interactive/VsResetInteractive.cs index 426783ddd4e..9d368cd372f 100644 --- a/src/VisualStudio/InteractiveServices/Interactive/ResetInteractive.cs +++ b/src/VisualStudio/InteractiveServices/Interactive/VsResetInteractive.cs @@ -7,49 +7,51 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; -using core::Roslyn.Utilities; using EnvDTE; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editor.Host; -using Microsoft.CodeAnalysis.Editor.Interactive; -using Microsoft.VisualStudio; using Microsoft.VisualStudio.ComponentModelHost; -using Microsoft.VisualStudio.LanguageServices; using Microsoft.VisualStudio.Shell.Interop; -using Microsoft.VisualStudio.InteractiveWindow; -using Microsoft.VisualStudio.InteractiveWindow.Shell; using VSLangProj; using Project = EnvDTE.Project; using System.Collections.Immutable; namespace Microsoft.VisualStudio.LanguageServices.Interactive { - internal sealed class ResetInteractive + internal sealed class VsResetInteractive : ResetInteractive { private readonly DTE _dte; private readonly IComponentModel _componentModel; private readonly IVsMonitorSelection _monitorSelection; private readonly IVsSolutionBuildManager _buildManager; - private readonly Func _createReference; - private readonly Func _createImport; - internal ResetInteractive(DTE dte, IComponentModel componentModel, IVsMonitorSelection monitorSelection, IVsSolutionBuildManager buildManager, Func createReference, Func createImport) + internal VsResetInteractive(DTE dte, IComponentModel componentModel, IVsMonitorSelection monitorSelection, IVsSolutionBuildManager buildManager, Func createReference, Func createImport) + : base(createReference, createImport) { _dte = dte; _componentModel = componentModel; _monitorSelection = monitorSelection; _buildManager = buildManager; - _createReference = createReference; - _createImport = createImport; } - internal void Execute(IVsInteractiveWindow vsInteractiveWindow, string title) + /// + /// Gets the properties of the currently selected projects necessary for reset. + /// + protected override bool GetProjectProperties( + out ImmutableArray references, + out ImmutableArray referenceSearchPaths, + out ImmutableArray sourceSearchPaths, + out ImmutableArray namespacesToImport, + out string projectDirectory) { var hierarchyPointer = default(IntPtr); var selectionContainerPointer = default(IntPtr); + references = ImmutableArray.Empty; + referenceSearchPaths = ImmutableArray.Empty; + sourceSearchPaths = ImmutableArray.Empty; + namespacesToImport = ImmutableArray.Empty; + projectDirectory = null; try { @@ -60,34 +62,8 @@ internal void Execute(IVsInteractiveWindow vsInteractiveWindow, string title) if (hierarchyPointer != IntPtr.Zero) { - List references, referenceSearchPaths, sourceSearchPaths, namespacesToImport; - string projectDirectory; GetProjectProperties(hierarchyPointer, out references, out referenceSearchPaths, out sourceSearchPaths, out namespacesToImport, out projectDirectory); - - // Now, we're going to do a bunch of async operations. So create a wait - // indicator so the user knows something is happening, and also so they cancel. - var waitIndicator = _componentModel.GetService(); - var waitContext = waitIndicator.StartWait(title, ServicesVSResources.BuildingProject, allowCancel: true); - - var resetInteractiveTask = ResetInteractiveAsync( - vsInteractiveWindow, - references.ToImmutableArray(), - referenceSearchPaths.ToImmutableArray(), - sourceSearchPaths.ToImmutableArray(), - namespacesToImport.ToImmutableArray(), - projectDirectory, - waitContext); - - // Once we're done resetting, dismiss the wait indicator and focus the REPL window. - resetInteractiveTask.SafeContinueWith( - _ => - { - waitContext.Dispose(); - - // We have to set focus to the Interactive Window *after* the wait indicator is dismissed. - vsInteractiveWindow.Show(focus: true); - }, - TaskScheduler.FromCurrentSynchronizationContext()); + return true; } } finally @@ -95,48 +71,16 @@ internal void Execute(IVsInteractiveWindow vsInteractiveWindow, string title) SafeRelease(hierarchyPointer); SafeRelease(selectionContainerPointer); } - } - - private async Task ResetInteractiveAsync( - IVsInteractiveWindow vsInteractiveWindow, - ImmutableArray referencePaths, - ImmutableArray referenceSearchPaths, - ImmutableArray sourceSearchPaths, - ImmutableArray namespacesToImport, - string projectDirectory, - IWaitContext waitContext) - { - // First, open the repl window. - var engine = (InteractiveEvaluator)vsInteractiveWindow.InteractiveWindow.Evaluator; - - // If the user hits the cancel button on the wait indicator, then we want to stop the - // build. - waitContext.CancellationToken.Register(() => - _dte.ExecuteCommand("Build.Cancel"), useSynchronizationContext: true); - - // First, start a build - await BuildProject().ConfigureAwait(true); - - // Then reset the REPL - waitContext.Message = ServicesVSResources.ResettingInteractive; - await vsInteractiveWindow.InteractiveWindow.Operations.ResetAsync(initialize: false).ConfigureAwait(true); - - // Now send the reference paths we've collected to the repl. - await engine.SetPathsAsync(referenceSearchPaths, sourceSearchPaths, projectDirectory).ConfigureAwait(true); - await vsInteractiveWindow.InteractiveWindow.SubmitAsync(new[] - { - referencePaths.Select(_createReference).Join("\r\n"), - namespacesToImport.Select(_createImport).Join("\r\n") - }).ConfigureAwait(true); + return false; } private static void GetProjectProperties( IntPtr hierarchyPointer, - out List references, - out List referenceSearchPaths, - out List sourceSearchPaths, - out List namespacesToImport, + out ImmutableArray references, + out ImmutableArray referenceSearchPaths, + out ImmutableArray sourceSearchPaths, + out ImmutableArray namespacesToImport, out string projectDirectory) { var hierarchy = (IVsHierarchy)Marshal.GetObjectForIUnknown(hierarchyPointer); @@ -148,10 +92,10 @@ internal void Execute(IVsInteractiveWindow vsInteractiveWindow, string title) var project = (Project)extensibilityObject; var vsProject = (VSProject)project.Object; - references = new List(); - referenceSearchPaths = new List(); - sourceSearchPaths = new List(); - namespacesToImport = new List(); + var referencesBuilder = ImmutableArray.CreateBuilder(); + var referenceSearchPathsBuilder = ImmutableArray.CreateBuilder(); + var sourceSearchPathsBuilder = ImmutableArray.CreateBuilder(); + var namespacesToImportBuilder = ImmutableArray.CreateBuilder(); var projectDir = (string)project.Properties.Item("FullPath").Value; var outputFileName = (string)project.Properties.Item("OutputFileName").Value; @@ -167,27 +111,32 @@ internal void Execute(IVsInteractiveWindow vsInteractiveWindow, string title) projectDirectory = projectDir; - referenceSearchPaths.Add(outputDir); - referenceSearchPaths.Add(RuntimeEnvironment.GetRuntimeDirectory()); + referenceSearchPathsBuilder.Add(outputDir); + referenceSearchPathsBuilder.Add(RuntimeEnvironment.GetRuntimeDirectory()); foreach (Reference reference in vsProject.References) { var str = GetReferenceString(reference); if (str != null) { - references.Add(str); + referencesBuilder.Add(str); } } - references.Add(outputFileName); + referencesBuilder.Add(outputFileName); // TODO (tomat): project Scripts dir - sourceSearchPaths.Add(Directory.Exists(scriptsDir) ? scriptsDir : projectDir); + sourceSearchPathsBuilder.Add(Directory.Exists(scriptsDir) ? scriptsDir : projectDir); if (!string.IsNullOrEmpty(defaultNamespace)) { - namespacesToImport.Add(defaultNamespace); + namespacesToImportBuilder.Add(defaultNamespace); } + + references = referencesBuilder.ToImmutableArray(); + referenceSearchPaths = referenceSearchPathsBuilder.ToImmutableArray(); + sourceSearchPaths = sourceSearchPathsBuilder.ToImmutableArray(); + namespacesToImport = namespacesToImportBuilder.ToImmutableArray(); } private static string GetReferenceString(Reference reference) @@ -262,7 +211,7 @@ private static string GetReferenceString(Reference reference) if (foundNonEquivalent) { - // We found some equivalent assemblies but also some non-equivalent. + // We found some equivalent assemblies but also some non-equivalent. // So simple name doesn't identify the reference uniquely. return fullName; } @@ -281,7 +230,7 @@ private static void SafeRelease(IntPtr pointer) } } - private Task BuildProject() + protected override Task BuildProject() { var taskSource = new TaskCompletionSource(); @@ -293,5 +242,15 @@ private Task BuildProject() return taskSource.Task; } + + protected override void CancelBuildProject() + { + _dte.ExecuteCommand("Build.Cancel"); + } + + protected override IWaitIndicator GetWaitIndicator() + { + return _componentModel.GetService(); + } } } diff --git a/src/VisualStudio/InteractiveServices/VisualStudioInteractiveServices.csproj b/src/VisualStudio/InteractiveServices/VisualStudioInteractiveServices.csproj index 6a44672266e..ce2916204cd 100644 --- a/src/VisualStudio/InteractiveServices/VisualStudioInteractiveServices.csproj +++ b/src/VisualStudio/InteractiveServices/VisualStudioInteractiveServices.csproj @@ -200,7 +200,7 @@ - + -- GitLab