File: Preview\DifferenceViewerPreview.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures_3hxu4syd_wpftmp.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// 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.
 
using System;
using System.Windows;
using System.Windows.Interop;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text.Differencing;
using Microsoft.VisualStudio.Text.Operations;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.Preview;
 
internal sealed partial class DifferenceViewerPreview : IDifferenceViewerPreview<IWpfDifferenceViewer>
{
    private const int WM_KEYFIRST = 0x0100;
    private const int WM_KEYLAST = 0x0108;
 
    private readonly IVsFilterKeys2? _filterKeys;
 
    private IWpfDifferenceViewer? _viewer;
    private bool _hasFocus;
    private NavigationalCommandTarget? _editorCommandTarget;
 
    public DifferenceViewerPreview(IWpfDifferenceViewer viewer, IEditorOperationsFactoryService editorOperationsFactoryService)
    {
        Contract.ThrowIfNull(viewer);
        _viewer = viewer;
 
        _viewer.VisualElement.IsKeyboardFocusWithinChanged += OnDifferenceViewerKeyboardFocusWithinChanged;
        _hasFocus = _viewer.VisualElement.IsKeyboardFocusWithin;
 
        var host = _viewer.ViewMode switch
        {
            DifferenceViewMode.Inline => _viewer.InlineHost,
            DifferenceViewMode.LeftViewOnly => _viewer.LeftHost,
            DifferenceViewMode.RightViewOnly => _viewer.RightHost,
            _ => throw ExceptionUtilities.UnexpectedValue(_viewer.ViewMode),
        };
 
        _editorCommandTarget = new NavigationalCommandTarget(host.TextView,
                editorOperationsFactoryService.GetEditorOperations(host.TextView));
 
        _filterKeys = Package.GetGlobalService(typeof(SVsFilterKeys)) as IVsFilterKeys2;
    }
 
    public IWpfDifferenceViewer Viewer
    {
        get
        {
            Contract.ThrowIfNull(_viewer);
            return _viewer;
        }
    }
 
    public void Dispose()
    {
        ThreadHelper.ThrowIfNotOnUIThread();
 
        GC.SuppressFinalize(this);
 
        if (_viewer != null)
        {
            ComponentDispatcher.ThreadFilterMessage -= FilterThreadMessage;
            _viewer.VisualElement.IsKeyboardFocusWithinChanged -= OnDifferenceViewerKeyboardFocusWithinChanged;
 
            if (!_viewer.IsClosed)
                _viewer.Close();
        }
 
        _viewer = null;
        _editorCommandTarget = null;
    }
 
    ~DifferenceViewerPreview()
    {
        // make sure we are not leaking diff viewer
        // we can't close the view from finalizer thread since it must be same
        // thread (owner thread) this UI is created.
        if (Environment.HasShutdownStarted)
        {
            return;
        }
 
        FatalError.ReportAndCatch(new Exception($"Dispose is not called how? viewer state : {_viewer?.IsClosed}"));
    }
 
    private void OnDifferenceViewerKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        _hasFocus = (bool)e.NewValue;
        if (_hasFocus)
        {
            // Hook into WPFs thread message handling so we can handle WM_KEYDOWN messages
            ComponentDispatcher.ThreadFilterMessage += FilterThreadMessage;
        }
        else
        {
            // Unhook from WPF's thread message handling.
            ComponentDispatcher.ThreadFilterMessage -= FilterThreadMessage;
        }
    }
 
    public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
    {
        ThreadHelper.ThrowIfNotOnUIThread();
        if (_hasFocus && _editorCommandTarget != null)
        {
            return _editorCommandTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
        }
 
        return (int)VisualStudio.OLE.Interop.Constants.OLECMDERR_E_UNKNOWNGROUP;
    }
 
    public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
    {
        ThreadHelper.ThrowIfNotOnUIThread();
        if (_hasFocus && _editorCommandTarget != null)
        {
            return _editorCommandTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
        }
 
        return (int)VisualStudio.OLE.Interop.Constants.OLECMDERR_E_UNKNOWNGROUP;
    }
 
    /// <summary>
    /// Preprocess input (keyboard) messages in order to translate them to editor commands if they map. Since we are in a modal dialog
    /// we need to tell the shell to allow pre-translate during a modal loop as well as instructing it to use the editor keyboard scope
    /// even though, as far as the shell knows, there is no editor active.
    /// </summary>
    private void FilterThreadMessage(ref System.Windows.Interop.MSG msg, ref bool handled)
    {
        ThreadHelper.ThrowIfNotOnUIThread();
        if (_filterKeys != null
            && msg.message >= WM_KEYFIRST
            && msg.message <= WM_KEYLAST)
        {
            var oleMSG = new VisualStudio.OLE.Interop.MSG()
            {
                hwnd = msg.hwnd,
                lParam = msg.lParam,
                wParam = msg.wParam,
                message = (uint)msg.message
            };
 
            // Ask the shell to do the command mapping for us and without firing off the command. We need to check if this command is one of the
            // supported commands first before actually firing the command.
            if (ErrorHandler.Succeeded(
                _filterKeys.TranslateAcceleratorEx(
                    [oleMSG],
                    (uint)(__VSTRANSACCELEXFLAGS.VSTAEXF_NoFireCommand | __VSTRANSACCELEXFLAGS.VSTAEXF_UseTextEditorKBScope | __VSTRANSACCELEXFLAGS.VSTAEXF_AllowModalState),
                    0 /*scope count*/,
                    [] /*scopes*/,
                    out var cmdGuid,
                    out var cmdId,
                    out _,
                    out _)))
            {
                // If the command is an allowed command then we fire it.
                if (IsCommandAllowed(cmdGuid, cmdId))
                {
                    var res = _filterKeys.TranslateAcceleratorEx(
                        [oleMSG],
                        (uint)(__VSTRANSACCELEXFLAGS.VSTAEXF_UseTextEditorKBScope | __VSTRANSACCELEXFLAGS.VSTAEXF_AllowModalState),
                        0 /*scope count*/,
                        [] /*scopes*/,
                        out _,
                        out _,
                        out _,
                        out _);
 
                    // We set handled to true if the command was executed, otherwise handled will be false
                    handled = ErrorHandler.Succeeded(res);
                }
            }
        }
    }
 
    /// <summary>
    /// Determines if the command specified is a valid command in Diff preview.
    /// </summary>
    /// <param name="cmdGuid">The command set guid for the command</param>
    /// <param name="cmdId">The command id</param>
    /// <returns>true for the supported commands that are copy, selection, navigation. False otherwise</returns>
    private static bool IsCommandAllowed(Guid cmdGuid, uint cmdId)
    {
        if (cmdGuid == VsMenus.guidStandardCommandSet2K)
        {
            return cmdId == (uint)VSConstants.VSStd2KCmdID.COPY ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.SELECTALL ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.UP ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.UP_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.PAGEUP ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.PAGEUP_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.DOWN ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.DOWN_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.PAGEDN ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.PAGEDN_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.LEFT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.LEFT_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.RIGHT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.RIGHT_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.BOL ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.BOL_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.FIRSTCHAR ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.FIRSTCHAR_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.EOL ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.EOL_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.LASTCHAR ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.LASTCHAR_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.TOPLINE ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.TOPLINE_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.BOTTOMLINE ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.BOTTOMLINE_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.HOME ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.HOME_EXT ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.END ||
                   cmdId == (uint)VSConstants.VSStd2KCmdID.END_EXT;
        }
 
        return false;
    }
}