File: Preview\DifferenceViewerPreview.cs
Web Access
Project: src\src\EditorFeatures\Core.Wpf\Microsoft.CodeAnalysis.EditorFeatures.Wpf_0tct2hz0_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.
 
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;
using Roslyn.Utilities;
 
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;
        }
    }
}