File: Interactive\VsResetInteractive.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ozsccwvc_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.InteractiveWindow;
using Microsoft.VisualStudio.Shell.Interop;
using Roslyn.Utilities;
using InteractiveHost::Microsoft.CodeAnalysis.Interactive;
using Microsoft.VisualStudio.Utilities;
using Microsoft.CodeAnalysis.Interactive;
using Microsoft.CodeAnalysis.Options;
 
namespace Microsoft.VisualStudio.LanguageServices.Interactive;
 
// TODO: convert to MEF component
internal sealed class VsResetInteractive : ResetInteractive
{
    private readonly VisualStudioWorkspace _workspace;
    private readonly EnvDTE.DTE _dte;
    private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor;
    private readonly IVsMonitorSelection _monitorSelection;
    private readonly IVsSolutionBuildManager _buildManager;
 
    internal VsResetInteractive(
        VisualStudioWorkspace workspace,
        EnvDTE.DTE dte,
        EditorOptionsService editorOptionsService,
        IUIThreadOperationExecutor uiThreadOperationExecutor,
        IVsMonitorSelection monitorSelection,
        IVsSolutionBuildManager buildManager,
        Func<string, string> createReference,
        Func<string, string> createImport)
        : base(editorOptionsService, createReference, createImport)
    {
        _workspace = workspace;
        _dte = dte;
        _uiThreadOperationExecutor = uiThreadOperationExecutor;
        _monitorSelection = monitorSelection;
        _buildManager = buildManager;
    }
 
    /// <summary>
    /// Gets the properties of the currently selected projects necessary for reset.
    /// </summary>
    protected override bool GetProjectProperties(
        out ImmutableArray<string> references,
        out ImmutableArray<string> referenceSearchPaths,
        out ImmutableArray<string> sourceSearchPaths,
        out ImmutableArray<string> projectNamespaces,
        out string projectDirectory,
        out InteractiveHostPlatform? platform)
    {
        var hierarchyPointer = default(IntPtr);
        var selectionContainerPointer = default(IntPtr);
        references = ImmutableArray<string>.Empty;
        referenceSearchPaths = ImmutableArray<string>.Empty;
        sourceSearchPaths = ImmutableArray<string>.Empty;
        projectNamespaces = ImmutableArray<string>.Empty;
        projectDirectory = null;
        platform = null;
 
        try
        {
            Marshal.ThrowExceptionForHR(_monitorSelection.GetCurrentSelection(
                out hierarchyPointer, out var itemid, out var multiItemSelectPointer, out selectionContainerPointer));
 
            if (hierarchyPointer != IntPtr.Zero)
            {
                GetProjectProperties(hierarchyPointer, out references, out referenceSearchPaths, out sourceSearchPaths, out projectNamespaces, out projectDirectory, out platform);
                return true;
            }
        }
        finally
        {
            SafeRelease(hierarchyPointer);
            SafeRelease(selectionContainerPointer);
        }
 
        return false;
    }
 
    private void GetProjectProperties(
        IntPtr hierarchyPointer,
        out ImmutableArray<string> references,
        out ImmutableArray<string> referenceSearchPaths,
        out ImmutableArray<string> sourceSearchPaths,
        out ImmutableArray<string> projectNamespaces,
        out string projectDirectory,
        out InteractiveHostPlatform? platform)
    {
        var hierarchy = (IVsHierarchy)Marshal.GetObjectForIUnknown(hierarchyPointer);
        Marshal.ThrowExceptionForHR(
            hierarchy.GetProperty((uint)VSConstants.VSITEMID.Root, (int)__VSHPROPID.VSHPROPID_ExtObject, out var extensibilityObject));
 
        var dteProject = (EnvDTE.Project)extensibilityObject;
        var vsProject = (VSLangProj.VSProject)dteProject.Object;
        var projectOpt = GetProjectFromHierarchy(hierarchy);
 
        var referencesBuilder = ImmutableArray.CreateBuilder<string>();
        var referenceSearchPathsBuilder = ImmutableArray.CreateBuilder<string>();
        var sourceSearchPathsBuilder = ImmutableArray.CreateBuilder<string>();
        var namespacesToImportBuilder = ImmutableArray.CreateBuilder<string>();
 
        var projectDir = (string)dteProject.Properties.Item("FullPath").Value;
        var outputFileName = (string)dteProject.Properties.Item("OutputFileName").Value;
        var defaultNamespace = (string)dteProject.Properties.Item("DefaultNamespace").Value;
        var targetFrameworkMoniker = (string)dteProject.Properties.Item("TargetFrameworkMoniker").Value;
        var relativeOutputPath = (string)dteProject.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath").Value;
 
        Debug.Assert(!string.IsNullOrEmpty(projectDir));
        Debug.Assert(!string.IsNullOrEmpty(outputFileName));
        Debug.Assert(!string.IsNullOrEmpty(relativeOutputPath));
 
        var scriptsDir = Path.Combine(projectDir, "Scripts");
        var outputDir = Path.Combine(projectDir, relativeOutputPath);
 
        projectDirectory = projectDir;
 
        referenceSearchPathsBuilder.Add(outputDir);
        referenceSearchPathsBuilder.Add(RuntimeEnvironment.GetRuntimeDirectory());
 
        foreach (VSLangProj.Reference reference in vsProject.References)
        {
            var str = GetReferenceString(reference);
            if (str != null)
            {
                referencesBuilder.Add(str);
            }
        }
 
        referencesBuilder.Add(outputFileName);
 
        // TODO (tomat): project Scripts dir
        sourceSearchPathsBuilder.Add(Directory.Exists(scriptsDir) ? scriptsDir : projectDir);
 
        if (!string.IsNullOrEmpty(defaultNamespace))
        {
            namespacesToImportBuilder.Add(defaultNamespace);
        }
 
        references = referencesBuilder.ToImmutableArray();
        referenceSearchPaths = referenceSearchPathsBuilder.ToImmutableArray();
        sourceSearchPaths = sourceSearchPathsBuilder.ToImmutableArray();
        projectNamespaces = namespacesToImportBuilder.ToImmutableArray();
 
        platform = (projectOpt != null) ? GetInteractiveHostPlatform(targetFrameworkMoniker, projectOpt.CompilationOptions.Platform) : null;
    }
 
    internal Project GetProjectFromHierarchy(IVsHierarchy hierarchy)
        => _workspace.CurrentSolution.Projects.FirstOrDefault(proj => _workspace.GetHierarchy(proj.Id) == hierarchy);
 
    private static InteractiveHostPlatform? GetInteractiveHostPlatform(string targetFrameworkMoniker, Platform platform)
    {
        if (targetFrameworkMoniker.StartsWith(".NETCoreApp", StringComparison.OrdinalIgnoreCase) ||
            targetFrameworkMoniker.StartsWith(".NETStandard", StringComparison.OrdinalIgnoreCase))
        {
            return InteractiveHostPlatform.Core;
        }
 
        switch (platform)
        {
            case Platform.Arm:
            case Platform.AnyCpu32BitPreferred:
            case Platform.X86:
                return InteractiveHostPlatform.Desktop32;
 
            case Platform.Itanium:
            case Platform.X64:
            case Platform.Arm64:
                return InteractiveHostPlatform.Desktop64;
 
            default:
                return null;
        }
    }
 
    private static string GetReferenceString(VSLangProj.Reference reference)
    {
        if (!reference.StrongName)
        {
            return reference.Path;
        }
 
        var name = reference.Name;
        if (name == "mscorlib")
        {
            // mscorlib is always loaded
            return null;
        }
 
        return reference.Path;
 
#if TODO // TODO: This shouldn't directly depend on GAC, rather we should have some kind of "reference simplifier".
        var possibleGacNames = GlobalAssemblyCache.GetAssemblyIdentities(name).ToArray();
        if (possibleGacNames.Length == 0)
        {
            // no assembly with simple "name" found in GAC, use path to identify the reference:
            return reference.Path;
        }
 
        string version = reference.Version;
        string culture = reference.Culture;
        string publicKeyToken = reference.PublicKeyToken;
 
        var fullName = string.Concat(
            name,
            ", Version=",
            version,
            ", Culture=",
            (culture == "") ? "neutral" : culture,
            ", PublicKeyToken=",
            publicKeyToken.ToLowerInvariant());
 
        AssemblyIdentity identity;
        if (!AssemblyIdentity.TryParseDisplayName(fullName, out identity))
        {
            // ignore invalid names:
            return null;
        }
 
        var foundEquivalent = false;
        var foundNonEquivalent = false;
        foreach (var possibleGacName in possibleGacNames)
        {
            if (DesktopAssemblyIdentityComparer.Default.ReferenceMatchesDefinition(identity, possibleGacName))
            {
                foundEquivalent = true;
            }
            else
            {
                foundNonEquivalent = true;
            }
 
            if (foundEquivalent && foundNonEquivalent)
            {
                break;
            }
        }
 
        if (!foundEquivalent)
        {
            // The reference name isn't equivalent to any GAC name.
            // The assembly is strong named but not GAC'd, so we need to load it from path:
            return reference.Path;
        }
 
        if (foundNonEquivalent)
        {
            // We found some equivalent assemblies but also some non-equivalent.
            // So simple name doesn't identify the reference uniquely.
            return fullName;
        }
 
        // We found a single simple name match that is equivalent to the given reference.
        // We can use the simple name to load the GAC'd assembly.
        return name;
#endif
    }
 
    private static void SafeRelease(IntPtr pointer)
    {
        if (pointer != IntPtr.Zero)
        {
            Marshal.Release(pointer);
        }
    }
 
    protected override Task<bool> BuildProjectAsync()
    {
        var taskSource = new TaskCompletionSource<bool>();
 
        _ = new VsUpdateSolutionEvents(_buildManager, taskSource);
 
        // Build the project.  When project build is done, set the task source as being done.
        // (Either succeeded, cancelled, or failed).
        _dte.ExecuteCommand("Build.BuildSelection");
 
        return taskSource.Task;
    }
 
    protected override void CancelBuildProject()
        => _dte.ExecuteCommand("Build.Cancel");
 
    protected override IUIThreadOperationExecutor GetUIThreadOperationExecutor()
        => _uiThreadOperationExecutor;
 
    /// <summary>
    /// Return namespaces that can be resolved in the latest interactive compilation.
    /// </summary>
    protected override async Task<IEnumerable<string>> GetNamespacesToImportAsync(IEnumerable<string> namespacesToImport, IInteractiveWindow interactiveWindow)
    {
        var document = interactiveWindow.CurrentLanguageBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
        var compilation = await document.Project.GetCompilationAsync().ConfigureAwait(true);
        return namespacesToImport.Where(ns => compilation.GlobalNamespace.GetQualifiedNamespace(ns) != null);
    }
}