|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
extern alias InteractiveHost;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using InteractiveHost::Microsoft.CodeAnalysis.Interactive;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.InteractiveWindow;
using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Interactive
{
/// <summary>
/// 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.
/// </summary>
internal abstract class ResetInteractive
{
private readonly Func<string, string> _createReference;
private readonly Func<string, string> _createImport;
private readonly EditorOptionsService _editorOptionsService;
internal event EventHandler ExecutionCompleted;
internal ResetInteractive(EditorOptionsService editorOptionsService, Func<string, string> createReference, Func<string, string> createImport)
{
_editorOptionsService = editorOptionsService;
_createReference = createReference;
_createImport = createImport;
}
internal async Task ExecuteAsync(IInteractiveWindow interactiveWindow, string title)
{
if (GetProjectProperties(out var references, out var referenceSearchPaths, out var sourceSearchPaths, out var projectNamespaces, out var projectDirectory, out var platform))
{
try
{
// 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 uiThreadOperationExecutor = GetUIThreadOperationExecutor();
using var context = uiThreadOperationExecutor.BeginExecute(title, EditorFeaturesWpfResources.Building_Project, allowCancellation: true, showProgress: false);
// We want to come back onto the calling context to dismiss the wait indicator and to notify about
// execution completion.
await ResetInteractiveAsync(
interactiveWindow,
references,
referenceSearchPaths,
sourceSearchPaths,
projectNamespaces,
projectDirectory,
platform,
context).ConfigureAwait(true);
}
finally
{
// Once we're done resetting focus the REPL window.
ExecutionCompleted?.Invoke(this, new EventArgs());
}
}
}
private async Task ResetInteractiveAsync(
IInteractiveWindow interactiveWindow,
ImmutableArray<string> referencePaths,
ImmutableArray<string> referenceSearchPaths,
ImmutableArray<string> sourceSearchPaths,
ImmutableArray<string> projectNamespaces,
string projectDirectory,
InteractiveHostPlatform? platform,
IUIThreadOperationContext uiThreadOperationContext)
{
// First, open the repl window.
var evaluator = (IResettableInteractiveEvaluator)interactiveWindow.Evaluator;
// If the user hits the cancel button on the wait indicator, then we want to stop the
// build.
using (uiThreadOperationContext.UserCancellationToken.Register(() =>
CancelBuildProject(), useSynchronizationContext: true))
{
// First, start a build.
// If the build fails do not reset the REPL.
var builtSuccessfully = await BuildProjectAsync().ConfigureAwait(true);
if (!builtSuccessfully)
{
return;
}
}
// Then reset the REPL
using var scope = uiThreadOperationContext.AddScope(allowCancellation: true, EditorFeaturesWpfResources.Resetting_Interactive);
evaluator.ResetOptions = new InteractiveEvaluatorResetOptions(platform);
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.
await evaluator.SetPathsAsync(referenceSearchPaths, sourceSearchPaths, projectDirectory).ConfigureAwait(true);
var editorOptions = _editorOptionsService.Factory.GetOptions(interactiveWindow.CurrentLanguageBuffer);
var importReferencesCommand = referencePaths.Select(_createReference);
await interactiveWindow.SubmitAsync(importReferencesCommand).ConfigureAwait(true);
// Project's default namespace might be different from namespace used within project.
// Filter out namespace imports that do not exist in interactive compilation.
var namespacesToImport = await GetNamespacesToImportAsync(projectNamespaces, interactiveWindow).ConfigureAwait(true);
var importNamespacesCommand = namespacesToImport.Select(_createImport).Join(editorOptions.GetNewLineCharacter());
if (!string.IsNullOrWhiteSpace(importNamespacesCommand))
{
await interactiveWindow.SubmitAsync([importNamespacesCommand]).ConfigureAwait(true);
}
}
protected abstract Task<IEnumerable<string>> GetNamespacesToImportAsync(IEnumerable<string> namespacesToImport, IInteractiveWindow interactiveWindow);
/// <summary>
/// Gets the properties of the currently selected projects necessary for reset.
/// </summary>
protected abstract bool GetProjectProperties(
out ImmutableArray<string> references,
out ImmutableArray<string> referenceSearchPaths,
out ImmutableArray<string> sourceSearchPaths,
out ImmutableArray<string> projectNamespaces,
out string projectDirectory,
out InteractiveHostPlatform? platform);
/// <summary>
/// A method that should trigger an async project build.
/// </summary>
/// <returns>Whether or not the build was successful.</returns>
protected abstract Task<bool> BuildProjectAsync();
/// <summary>
/// A method that should trigger a project cancellation.
/// </summary>
protected abstract void CancelBuildProject();
protected abstract IUIThreadOperationExecutor GetUIThreadOperationExecutor();
}
}
|