File: Preview\PreviewEngine.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_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
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Preview;
 
internal sealed class PreviewEngine : IVsPreviewChangesEngine
{
    private readonly IVsEditorAdaptersFactoryService _editorFactory;
    private readonly Solution _newSolution;
    private readonly Solution _oldSolution;
    private readonly string _topLevelName;
    private readonly Glyph _topLevelGlyph;
    private readonly string _helpString;
    private readonly string _description;
    private readonly string _title;
    private readonly IComponentModel _componentModel;
    private readonly IVsImageService2 _imageService;
 
    private TopLevelChange _topLevelChange;
    private PreviewUpdater _updater;
 
    public Solution FinalSolution { get; private set; }
    public bool ShowCheckBoxes { get; private set; }
 
    public PreviewEngine(string title, string helpString, string description, string topLevelItemName, Glyph topLevelGlyph, Solution newSolution, Solution oldSolution, IComponentModel componentModel, bool showCheckBoxes = true)
        : this(title, helpString, description, topLevelItemName, topLevelGlyph, newSolution, oldSolution, componentModel, null, showCheckBoxes)
    {
    }
 
    public PreviewEngine(
        string title,
        string helpString,
        string description,
        string topLevelItemName,
        Glyph topLevelGlyph,
        Solution newSolution,
        Solution oldSolution,
        IComponentModel componentModel,
        IVsImageService2 imageService,
        bool showCheckBoxes = true)
    {
        _topLevelName = topLevelItemName;
        _topLevelGlyph = topLevelGlyph;
        _title = title ?? throw new ArgumentNullException(nameof(title));
        _helpString = helpString ?? throw new ArgumentNullException(nameof(helpString));
        _description = description ?? throw new ArgumentNullException(nameof(description));
        _newSolution = newSolution.WithMergedLinkedFileChangesAsync(oldSolution, cancellationToken: CancellationToken.None).Result;
        _oldSolution = oldSolution;
        _editorFactory = componentModel.GetService<IVsEditorAdaptersFactoryService>();
        _componentModel = componentModel;
        this.ShowCheckBoxes = showCheckBoxes;
        _imageService = imageService;
    }
 
    public void CloseWorkspace()
    {
        _updater?.CloseWorkspace();
    }
 
    public int ApplyChanges()
    {
        FinalSolution = _topLevelChange.GetUpdatedSolution(applyingChanges: true);
        return VSConstants.S_OK;
    }
 
    public int GetConfirmation(out string pbstrConfirmation)
    {
        pbstrConfirmation = EditorFeaturesResources.Apply2;
        return VSConstants.S_OK;
    }
 
    public int GetDescription(out string pbstrDescription)
    {
        pbstrDescription = _description;
        return VSConstants.S_OK;
    }
 
    public int GetHelpContext(out string pbstrHelpContext)
    {
        pbstrHelpContext = _helpString;
        return VSConstants.S_OK;
    }
 
    public int GetRootChangesList(out object ppIUnknownPreviewChangesList)
    {
        var changes = _newSolution.GetChanges(_oldSolution);
        var projectChanges = changes.GetProjectChanges();
 
        _topLevelChange = new TopLevelChange(_topLevelName, _topLevelGlyph, _newSolution, this);
 
        var builder = ArrayBuilder<AbstractChange>.GetInstance();
 
        // Documents
        // (exclude unchangeable ones if they will be ignored when applied to workspace.)
        var changedDocuments = projectChanges.SelectMany(p => p.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true, _oldSolution.Workspace.IgnoreUnchangeableDocumentsWhenApplyingChanges));
        var addedDocuments = projectChanges.SelectMany(p => p.GetAddedDocuments());
        var removedDocuments = projectChanges.SelectMany(p => p.GetRemovedDocuments());
 
        var allDocumentsWithChanges = new List<DocumentId>();
        allDocumentsWithChanges.AddRange(changedDocuments);
        allDocumentsWithChanges.AddRange(addedDocuments);
        allDocumentsWithChanges.AddRange(removedDocuments);
 
        // Additional Documents
        var changedAdditionalDocuments = projectChanges.SelectMany(p => p.GetChangedAdditionalDocuments());
        var addedAdditionalDocuments = projectChanges.SelectMany(p => p.GetAddedAdditionalDocuments());
        var removedAdditionalDocuments = projectChanges.SelectMany(p => p.GetRemovedAdditionalDocuments());
 
        allDocumentsWithChanges.AddRange(changedAdditionalDocuments);
        allDocumentsWithChanges.AddRange(addedAdditionalDocuments);
        allDocumentsWithChanges.AddRange(removedAdditionalDocuments);
 
        // AnalyzerConfig Documents
        var changedAnalyzerConfigDocuments = projectChanges.SelectMany(p => p.GetChangedAnalyzerConfigDocuments());
        var addedAnalyzerConfigDocuments = projectChanges.SelectMany(p => p.GetAddedAnalyzerConfigDocuments());
        var removedAnalyzerConfigDocuments = projectChanges.SelectMany(p => p.GetRemovedAnalyzerConfigDocuments());
 
        allDocumentsWithChanges.AddRange(changedAnalyzerConfigDocuments);
        allDocumentsWithChanges.AddRange(addedAnalyzerConfigDocuments);
        allDocumentsWithChanges.AddRange(removedAnalyzerConfigDocuments);
 
        AppendFileChanges(allDocumentsWithChanges, builder);
 
        // References (metadata/project/analyzer)
        ReferenceChange.AppendReferenceChanges(projectChanges, this, builder);
 
        _topLevelChange.Children = builder.Count == 0 ? ChangeList.Empty : new ChangeList(builder.ToArray());
        ppIUnknownPreviewChangesList = _topLevelChange.Children.Changes.Length == 0 ? new ChangeList(new[] { new NoChange(this) }) : new ChangeList(new[] { _topLevelChange });
 
        if (_topLevelChange.Children.Changes.Length == 0)
        {
            this.ShowCheckBoxes = false;
        }
 
        return VSConstants.S_OK;
    }
 
    private void AppendFileChanges(IEnumerable<DocumentId> changedDocuments, ArrayBuilder<AbstractChange> builder)
    {
        // Avoid showing linked changes to linked files multiple times.
        var linkedDocumentIds = new HashSet<DocumentId>();
 
        var orderedChangedDocuments = changedDocuments.GroupBy(d => d.ProjectId).OrderByDescending(g => g.Count()).Flatten();
 
        foreach (var documentId in orderedChangedDocuments)
        {
            if (linkedDocumentIds.Contains(documentId))
            {
                continue;
            }
 
            var left = _oldSolution.GetTextDocument(documentId);
            var right = _newSolution.GetTextDocument(documentId);
 
            if (left is Document leftDocument)
            {
                linkedDocumentIds.AddRange(leftDocument.GetLinkedDocumentIds());
            }
            else if (right is Document rightDocument)
            {
                // Added document.
                linkedDocumentIds.AddRange(rightDocument.GetLinkedDocumentIds());
            }
 
            var fileChange = new FileChange(left, right, _componentModel, _topLevelChange, this, _imageService);
            if (fileChange.Children.Changes.Length > 0)
            {
                builder.Add(fileChange);
            }
        }
    }
 
    public int GetTextViewDescription(out string pbstrTextViewDescription)
    {
        pbstrTextViewDescription = EditorFeaturesResources.Preview_Code_Changes_colon;
        return VSConstants.S_OK;
    }
 
    public int GetTitle(out string pbstrTitle)
    {
        pbstrTitle = _title;
        return VSConstants.S_OK;
    }
 
    public int GetWarning(out string pbstrWarning, out int ppcwlWarningLevel)
    {
        pbstrWarning = null;
        ppcwlWarningLevel = 0;
        return VSConstants.E_NOTIMPL;
    }
 
    public void UpdatePreview(DocumentId documentId, SpanChange spanSource)
    {
        var updatedSolution = _topLevelChange.GetUpdatedSolution(applyingChanges: false);
        var document = updatedSolution.GetTextDocument(documentId);
        if (document != null)
        {
            _updater.UpdateView(document, spanSource);
        }
    }
 
    // We don't get a TexView until they call OnRequestChanges on a child.
    // However, once they've called it once, it's always the same TextView.
    public void SetTextView(object textView)
    {
        _updater ??= new PreviewUpdater(EnsureTextViewIsInitialized(textView));
    }
 
    private ITextView EnsureTextViewIsInitialized(object previewTextView)
    {
        // We pass in a regular ITextView in tests
        if (previewTextView is not null and ITextView)
        {
            return (ITextView)previewTextView;
        }
 
        var adapter = (IVsTextView)previewTextView;
        var textView = _editorFactory.GetWpfTextView(adapter);
 
        if (textView == null)
        {
            EditBufferToInitialize(adapter);
            textView = _editorFactory.GetWpfTextView(adapter);
        }
 
        UpdateTextViewOptions(textView);
 
        return textView;
    }
 
    private static void UpdateTextViewOptions(IWpfTextView textView)
    {
        // Do not show the IndentationCharacterMargin, which controls spaces vs. tabs etc.
        textView.Options.SetOptionValue(DefaultTextViewHostOptions.IndentationCharacterMarginOptionId, false);
 
        // Do not show LineEndingMargin, which determines EOL and EOF settings.
        textView.Options.SetOptionValue(DefaultTextViewHostOptions.LineEndingMarginOptionId, false);
 
        // Do not show the "no issues found" health indicator for previews. 
        textView.Options.SetOptionValue(DefaultTextViewHostOptions.EnableFileHealthIndicatorOptionId, false);
    }
 
    // When the dialog is first instantiated, the IVsTextView it contains may 
    // not have been initialized, which will prevent us from using an 
    // EditorAdaptersFactoryService to get the ITextView. If we edit the IVsTextView,
    // it will initialize and we can proceed.
    private static void EditBufferToInitialize(IVsTextView adapter)
    {
        if (adapter == null)
        {
            return;
        }
 
        var newText = "";
        var newTextPtr = Marshal.StringToHGlobalAuto(newText);
 
        try
        {
            Marshal.ThrowExceptionForHR(adapter.GetBuffer(out var lines));
            Marshal.ThrowExceptionForHR(lines.GetLastLineIndex(out _, out var piLineIndex));
            Marshal.ThrowExceptionForHR(lines.GetLengthOfLine(piLineIndex, out var piLineLength));
 
            Microsoft.VisualStudio.TextManager.Interop.TextSpan[] changes = null;
 
            piLineLength = piLineLength > 0 ? piLineLength - 1 : 0;
 
            Marshal.ThrowExceptionForHR(lines.ReplaceLines(0, 0, piLineIndex, piLineLength, newTextPtr, newText.Length, changes));
        }
        finally
        {
            Marshal.FreeHGlobal(newTextPtr);
        }
    }
 
    private class NoChange : AbstractChange
    {
        public NoChange(PreviewEngine engine) : base(engine)
        {
        }
 
        public override int GetText(out VSTREETEXTOPTIONS tto, out string ppszText)
        {
            tto = VSTREETEXTOPTIONS.TTO_DEFAULT;
            ppszText = ServicesVSResources.No_Changes;
            return VSConstants.S_OK;
        }
 
        public override int GetTipText(out VSTREETOOLTIPTYPE eTipType, out string ppszText)
        {
            eTipType = VSTREETOOLTIPTYPE.TIPTYPE_DEFAULT;
            ppszText = null;
            return VSConstants.E_FAIL;
        }
 
        public override int CanRecurse => 0;
        public override int IsExpandable => 0;
 
        internal override void GetDisplayData(VSTREEDISPLAYDATA[] pData)
            => pData[0].Image = pData[0].SelectedImage = (ushort)StandardGlyphGroup.GlyphInformation;
 
        public override int OnRequestSource(object pIUnknownTextView)
            => VSConstants.S_OK;
 
        public override void UpdatePreview()
        {
        }
    }
}