|
// 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.Diagnostics;
using System.Linq;
using InteractiveHost::Microsoft.CodeAnalysis.Interactive;
using Microsoft.CodeAnalysis.Interactive;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.VisualStudio.InteractiveWindow.Commands;
using Microsoft.VisualStudio.InteractiveWindow.Shell;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Interactive;
internal abstract class VsInteractiveWindowProvider
{
private readonly IVsInteractiveWindowFactory _vsInteractiveWindowFactory;
private readonly SVsServiceProvider _vsServiceProvider;
private readonly VisualStudioWorkspace _vsWorkspace;
private readonly IViewClassifierAggregatorService _classifierAggregator;
private readonly IContentTypeRegistryService _contentTypeRegistry;
// TODO: support multi-instance windows
// single instance of the Interactive Window
private IVsInteractiveWindow _vsInteractiveWindow;
public VsInteractiveWindowProvider(
SVsServiceProvider serviceProvider,
IVsInteractiveWindowFactory interactiveWindowFactory,
IViewClassifierAggregatorService classifierAggregator,
IContentTypeRegistryService contentTypeRegistry,
IInteractiveWindowCommandsFactory commandsFactory,
IInteractiveWindowCommand[] commands,
VisualStudioWorkspace workspace)
{
_vsServiceProvider = serviceProvider;
_classifierAggregator = classifierAggregator;
_contentTypeRegistry = contentTypeRegistry;
_vsWorkspace = workspace;
Commands = GetApplicableCommands(commands, coreContentType: PredefinedInteractiveCommandsContentTypes.InteractiveCommandContentTypeName,
specializedContentType: InteractiveWindowContentTypes.CommandContentTypeName);
_vsInteractiveWindowFactory = interactiveWindowFactory;
CommandsFactory = commandsFactory;
}
protected abstract CSharpInteractiveEvaluator CreateInteractiveEvaluator(
SVsServiceProvider serviceProvider,
IViewClassifierAggregatorService classifierAggregator,
IContentTypeRegistryService contentTypeRegistry,
VisualStudioWorkspace workspace);
protected abstract Guid LanguageServiceGuid { get; }
protected abstract Guid Id { get; }
protected abstract string Title { get; }
protected abstract FunctionId InteractiveWindowFunctionId { get; }
protected IInteractiveWindowCommandsFactory CommandsFactory { get; }
protected ImmutableArray<IInteractiveWindowCommand> Commands { get; }
public void Create(int instanceId)
{
var evaluator = CreateInteractiveEvaluator(_vsServiceProvider, _classifierAggregator, _contentTypeRegistry, _vsWorkspace);
Debug.Assert(_vsInteractiveWindow == null);
// ForceCreate means that the window should be created if the persisted layout indicates that it is visible.
_vsInteractiveWindow = _vsInteractiveWindowFactory.Create(Id, instanceId, Title, evaluator, __VSCREATETOOLWIN.CTW_fForceCreate);
_vsInteractiveWindow.SetLanguage(LanguageServiceGuid, evaluator.ContentType);
if (_vsInteractiveWindow is ToolWindowPane interactiveWindowPane)
{
var defaultPlatform = evaluator.ResetOptions.Platform;
Contract.ThrowIfFalse(defaultPlatform.HasValue);
interactiveWindowPane.Caption = Title + GetFrameworkForTitle(defaultPlatform.Value);
evaluator.OnBeforeReset += platform => interactiveWindowPane.Caption = Title + GetFrameworkForTitle(platform);
}
var window = _vsInteractiveWindow.InteractiveWindow;
window.TextView.Options.SetOptionValue(DefaultTextViewHostOptions.SuggestionMarginId, true);
void closeEventDelegate(object sender, EventArgs e)
{
window.TextView.Closed -= closeEventDelegate;
LogCloseSession(evaluator.SubmissionCount);
evaluator.Dispose();
}
// the tool window now owns the engine:
window.TextView.Closed += closeEventDelegate;
// vsWindow.AutoSaveOptions = true;
// fire and forget:
window.InitializeAsync();
LogSession(LogMessage.Window, LogMessage.Create);
static string GetFrameworkForTitle(InteractiveHostPlatform platform) => platform switch
{
InteractiveHostPlatform.Desktop64 => " (.NET Framework " + ServicesVSResources.Bitness64 + ")",
InteractiveHostPlatform.Desktop32 => " (.NET Framework " + ServicesVSResources.Bitness32 + ")",
InteractiveHostPlatform.Core => " (.NET Core)",
_ => throw ExceptionUtilities.Unreachable()
};
}
public IVsInteractiveWindow Open(int instanceId, bool focus)
{
// TODO: we don't support multi-instance yet
Debug.Assert(instanceId == 0);
if (_vsInteractiveWindow == null)
{
Create(instanceId);
}
_vsInteractiveWindow.Show(focus);
return _vsInteractiveWindow;
}
protected void LogSession(string key, string value)
=> Logger.Log(InteractiveWindowFunctionId, KeyValueLogMessage.Create(m => m.Add(key, value)));
private void LogCloseSession(int languageBufferCount)
{
Logger.Log(InteractiveWindowFunctionId,
KeyValueLogMessage.Create(m =>
{
m.Add(LogMessage.Window, LogMessage.Close);
m.Add(LogMessage.LanguageBufferCount, languageBufferCount);
}));
}
private static ImmutableArray<IInteractiveWindowCommand> GetApplicableCommands(IInteractiveWindowCommand[] commands, string coreContentType, string specializedContentType)
{
// get all commands of coreContentType - generic interactive window commands
var interactiveCommands = commands.Where(
c => c.GetType().GetCustomAttributes(typeof(ContentTypeAttribute), inherit: true).Any(
a => ((ContentTypeAttribute)a).ContentTypes == coreContentType)).ToArray();
// get all commands of specializedContentType - smart C#/VB command implementations
var specializedInteractiveCommands = commands.Where(
c => c.GetType().GetCustomAttributes(typeof(ContentTypeAttribute), inherit: true).Any(
a => ((ContentTypeAttribute)a).ContentTypes == specializedContentType)).ToArray();
// We should choose specialized C#/VB commands over generic core interactive window commands
// Build a map of names and associated core command first
var interactiveCommandMap = new Dictionary<string, int>();
for (var i = 0; i < interactiveCommands.Length; i++)
{
foreach (var name in interactiveCommands[i].Names)
{
interactiveCommandMap.Add(name, i);
}
}
// swap core commands with specialized command if both exist
// Command can have multiple names. We need to compare every name to find match.
foreach (var command in specializedInteractiveCommands)
{
foreach (var name in command.Names)
{
if (interactiveCommandMap.TryGetValue(name, out var value))
{
interactiveCommands[value] = command;
break;
}
}
}
return interactiveCommands.ToImmutableArray();
}
}
|