File: Interactive\InteractiveEvaluator.cs
Web Access
Project: src\src\EditorFeatures\Core.Wpf\Microsoft.CodeAnalysis.EditorFeatures.Wpf_per0z0nh_wpftmp.csproj (Microsoft.CodeAnalysis.EditorFeatures.Wpf)
// 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.
 
extern alias InteractiveHost;
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.InteractiveWindow;
using Microsoft.VisualStudio.InteractiveWindow.Commands;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;
 
namespace Microsoft.CodeAnalysis.Interactive;
 
using InteractiveHost::Microsoft.CodeAnalysis.Interactive;
 
// TODO: Rename to InteractiveEvaluator https://github.com/dotnet/roslyn/issues/6441
// The code is not specific to C#, but Interactive Window has hardcoded "CSharpInteractiveEvaluator" name.
internal sealed class CSharpInteractiveEvaluator : IResettableInteractiveEvaluator
{
    private const string CommandPrefix = "#";
 
    private readonly InteractiveEvaluatorLanguageInfoProvider _languageInfo;
 
    private readonly IThreadingContext _threadingContext;
    private readonly IViewClassifierAggregatorService _classifierAggregator;
    private readonly IInteractiveWindowCommandsFactory _commandsFactory;
    private readonly ImmutableArray<IInteractiveWindowCommand> _commands;
    private readonly InteractiveWindowWorkspace _workspace;
    private readonly InteractiveSession _session;
 
    private IInteractiveWindow? _lazyInteractiveWindow;
    private IInteractiveWindowCommands? _lazyInteractiveCommands;
 
    #region UI Thread only
 
    /// <remarks>
    /// Submission buffers in the order they were submitted. 
    /// Includes both command buffers as well as language buffers.
    /// Does not include the current buffer unless it has been submitted.
    /// </remarks>
    private readonly List<ITextBuffer> _submittedBuffers = [];
 
    #endregion
 
    public IContentType ContentType { get; }
 
    public InteractiveEvaluatorResetOptions ResetOptions { get; set; }
        = new InteractiveEvaluatorResetOptions(InteractiveHostPlatform.Core);
 
    internal CSharpInteractiveEvaluator(
        IThreadingContext threadingContext,
        IAsynchronousOperationListener listener,
        IContentType contentType,
        HostServices hostServices,
        IViewClassifierAggregatorService classifierAggregator,
        IInteractiveWindowCommandsFactory commandsFactory,
        ImmutableArray<IInteractiveWindowCommand> commands,
        ITextDocumentFactoryService textDocumentFactoryService,
        InteractiveEvaluatorLanguageInfoProvider languageInfo,
        string initialWorkingDirectory)
    {
        Debug.Assert(languageInfo.InteractiveResponseFileName.IndexOfAny([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]) == -1);
 
        _threadingContext = threadingContext;
        ContentType = contentType;
        _languageInfo = languageInfo;
        _classifierAggregator = classifierAggregator;
        _commandsFactory = commandsFactory;
        _commands = commands;
 
        _workspace = new InteractiveWindowWorkspace(hostServices);
 
        _session = new InteractiveSession(
            _workspace,
            listener,
            textDocumentFactoryService,
            languageInfo,
            initialWorkingDirectory);
 
        _session.Host.ProcessInitialized += ProcessInitialized;
    }
 
    public void Dispose()
    {
        _session.Host.ProcessInitialized -= ProcessInitialized;
 
        _session.Dispose();
        _workspace.Dispose();
 
        if (_lazyInteractiveWindow != null)
        {
            _lazyInteractiveWindow.SubmissionBufferAdded -= SubmissionBufferAdded;
        }
    }
 
    private void ProcessInitialized(InteractiveHostPlatformInfo platformInfo, InteractiveHostOptions options, RemoteExecutionResult result)
    {
        // Capture and clear exising submission buffers. Independent of other operations that occur on restart.
        _ = _threadingContext.JoinableTaskFactory.RunAsync(async () =>
        {
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();
            CaptureClassificationSpans();
        });
    }
 
    public IInteractiveWindow? CurrentWindow
    {
        get => _lazyInteractiveWindow;
 
        set
        {
            _threadingContext.ThrowIfNotOnUIThread();
 
            if (_lazyInteractiveWindow != null)
            {
                throw new NotSupportedException(EditorFeaturesWpfResources.The_CurrentWindow_property_may_only_be_assigned_once);
            }
 
            _lazyInteractiveWindow = value ?? throw new ArgumentNullException(nameof(value));
            _workspace.Window = value;
 
            Task.Run(() => _session.Host.SetOutputs(value.OutputWriter, value.ErrorOutputWriter));
 
            value.SubmissionBufferAdded += SubmissionBufferAdded;
            _lazyInteractiveCommands = _commandsFactory.CreateInteractiveCommands(value, CommandPrefix, _commands);
        }
    }
 
    /// <summary>
    /// Invoked before the process is reset. The argument is the value of <see cref="InteractiveHostOptions.Platform"/>.
    /// </summary>
    public event Action<InteractiveHostPlatform>? OnBeforeReset;
 
    public int SubmissionCount
        => _session.SubmissionCount;
 
    private IInteractiveWindow GetInteractiveWindow()
        => _lazyInteractiveWindow ?? throw new InvalidOperationException(EditorFeaturesResources.Engine_must_be_attached_to_an_Interactive_Window);
 
    private IInteractiveWindowCommands GetInteractiveCommands()
        => _lazyInteractiveCommands ?? throw new InvalidOperationException(EditorFeaturesResources.Engine_must_be_attached_to_an_Interactive_Window);
 
    /// <summary>
    /// Invoked on UI thread when a new language buffer is created and before it is added to the projection.
    /// </summary>
    private void SubmissionBufferAdded(object sender, SubmissionBufferAddedEventArgs args)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        _session.AddSubmissionProject(args.NewBuffer);
    }
 
    private void CaptureClassificationSpans()
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        var textView = GetInteractiveWindow().TextView;
 
        // Freeze all existing classifications and then clear the list of submission buffers we have.
        foreach (var textBuffer in _submittedBuffers)
        {
            InertClassifierProvider.CaptureExistingClassificationSpans(_classifierAggregator, textView, textBuffer);
        }
 
        _submittedBuffers.Clear();
    }
 
    public bool CanExecuteCode(string text)
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        return _lazyInteractiveCommands?.InCommand == true || _languageInfo.IsCompleteSubmission(text);
    }
 
    /// <summary>
    /// Invoked when the Interactive Window is created.
    /// </summary>
    async Task<ExecutionResult> IInteractiveEvaluator.InitializeAsync()
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        var window = GetInteractiveWindow();
 
        var resetOptions = ResetOptions;
        _session.Host.SetOutputs(window.OutputWriter, window.ErrorOutputWriter);
        var isSuccessful = await _session.ResetAsync(_session.GetHostOptions(initialize: true, resetOptions.Platform)).ConfigureAwait(false);
        return new ExecutionResult(isSuccessful);
    }
 
    /// <summary>
    /// Invoked by the reset toolbar button.
    /// </summary>
    async Task<ExecutionResult> IInteractiveEvaluator.ResetAsync(bool initialize)
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        var window = GetInteractiveWindow();
 
        var resetOptions = ResetOptions;
        Debug.Assert(GetInteractiveCommands().CommandPrefix == CommandPrefix);
        window.AddInput(CommandPrefix + InteractiveWindowResetCommand.GetCommandLine(initialize, resetOptions.Platform));
        window.WriteLine(EditorFeaturesWpfResources.Resetting_execution_engine);
        window.FlushOutput();
 
        var options = _session.GetHostOptions(initialize, resetOptions.Platform);
        OnBeforeReset?.Invoke(options.Platform);
        var isSuccessful = await _session.ResetAsync(options).ConfigureAwait(false);
        return new ExecutionResult(isSuccessful);
    }
 
    /// <summary>
    /// Called on UI thread by the Interactive Window once a code snippet is submitted.
    /// Followed on UI thread by creation of a new language buffer and call to <see cref="SubmissionBufferAdded"/>.
    /// </summary>
    public async Task<ExecutionResult> ExecuteCodeAsync(string text)
    {
        try
        {
            _threadingContext.ThrowIfNotOnUIThread();
 
            var window = GetInteractiveWindow();
            var commands = GetInteractiveCommands();
 
            var currentSubmissionBuffer = window.CurrentLanguageBuffer;
            Contract.ThrowIfNull(currentSubmissionBuffer);
            _submittedBuffers.Add(currentSubmissionBuffer);
 
            if (commands.InCommand)
            {
                // Takes the content of the current language buffer, parses it as a command
                // and returns a task that execute the command, or null if the text doesn't parse.
                var commandTask = commands.TryExecuteCommand();
                if (commandTask != null)
                {
                    return await commandTask.ConfigureAwait(false);
                }
            }
 
            // If process initialization is in progress we will wait with code 
            // execution after the initialization is completed.
            var isSuccessful = await _session.ExecuteCodeAsync(text).ConfigureAwait(false);
            return new ExecutionResult(isSuccessful);
        }
        catch (Exception e) when (FatalError.ReportAndPropagate(e))
        {
            throw ExceptionUtilities.Unreachable();
        }
    }
 
    public void AbortExecution()
    {
        // TODO (https://github.com/dotnet/roslyn/issues/4725)
    }
 
    public string? FormatClipboard()
    {
        // keep the clipboard content as is
        return null;
    }
 
    public string GetPrompt()
    {
        var buffer = GetInteractiveWindow().CurrentLanguageBuffer;
        return buffer != null && buffer.CurrentSnapshot.LineCount > 1
            ? ". "
            : "> ";
    }
 
    public Task SetPathsAsync(ImmutableArray<string> referenceSearchPaths, ImmutableArray<string> sourceSearchPaths, string workingDirectory)
        => _session.SetPathsAsync(referenceSearchPaths, sourceSearchPaths, workingDirectory);
}