diff --git a/src/Features/Core/Portable/Experimentation/KeybindingResetOptions.cs b/src/Features/Core/Portable/Experimentation/KeybindingResetOptions.cs new file mode 100644 index 0000000000000000000000000000000000000000..dd9be0e93a06aed07e9e983ff6022196930ee7da --- /dev/null +++ b/src/Features/Core/Portable/Experimentation/KeybindingResetOptions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Options; + +namespace Microsoft.CodeAnalysis.Experimentation +{ + internal static class KeybindingResetOptions + { + private const string LocalRegistryPath = @"Roslyn\Internal\KeybindingsStatus\"; + + public static readonly Option ReSharperStatus = new Option(nameof(KeybindingResetOptions), + nameof(ReSharperStatus), defaultValue: Experimentation.ReSharperStatus.NotInstalledOrDisabled, + storageLocations: new LocalUserProfileStorageLocation(LocalRegistryPath + nameof(ReSharperStatus))); + + public static readonly Option NeedsReset = new Option(nameof(KeybindingResetOptions), + nameof(NeedsReset), defaultValue: false, + storageLocations: new LocalUserProfileStorageLocation(LocalRegistryPath + nameof(NeedsReset))); + + public static readonly Option NeverShowAgain = new Option(nameof(KeybindingResetOptions), + nameof(NeverShowAgain), defaultValue: false, + storageLocations: new LocalUserProfileStorageLocation(LocalRegistryPath + nameof(NeverShowAgain))); + } +} diff --git a/src/Features/Core/Portable/Experimentation/ReSharperStatus.cs b/src/Features/Core/Portable/Experimentation/ReSharperStatus.cs new file mode 100644 index 0000000000000000000000000000000000000000..080da66c8b51859b98567939b86c6937b729c142 --- /dev/null +++ b/src/Features/Core/Portable/Experimentation/ReSharperStatus.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Experimentation +{ + internal enum ReSharperStatus + { + /// + /// Disabled in the extension manager or not installed. + /// + NotInstalledOrDisabled, + /// + /// ReSharper is suspended. Package is loaded, but is not actually performing actions. + /// + Suspended, + /// + /// ReSharper is running. + /// + Enabled + } +} diff --git a/src/VisualStudio/Core/Def/Experimentation/IExperiment.cs b/src/VisualStudio/Core/Def/Experimentation/IExperiment.cs new file mode 100644 index 0000000000000000000000000000000000000000..968bbb378ebde141d3723d48692f3f2b2eb13c7a --- /dev/null +++ b/src/VisualStudio/Core/Def/Experimentation/IExperiment.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.LanguageServices.Experimentation +{ + internal interface IExperiment + { + Task Initialize(); + } +} diff --git a/src/VisualStudio/Core/Def/Implementation/Experimentation/KeybindingResetDetector.cs b/src/VisualStudio/Core/Def/Implementation/Experimentation/KeybindingResetDetector.cs new file mode 100644 index 0000000000000000000000000000000000000000..df417b536160b7d65cda83fafd9742be188a0067 --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/Experimentation/KeybindingResetDetector.cs @@ -0,0 +1,335 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.ComponentModel.Composition; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Experimentation; +using Microsoft.CodeAnalysis.Experiments; +using Microsoft.CodeAnalysis.Extensions; +using Microsoft.VisualStudio.LanguageServices.Experimentation; +using Microsoft.VisualStudio.OLE.Interop; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using Task = System.Threading.Tasks.Task; +using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; +using System.Threading; +using Roslyn.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.Experimentation +{ + /// + /// Detects if keybindings have been messed up by ReSharper disable, and offers the user the ability + /// to reset if so. + /// + /// + /// The only objects to hold permanent references to this object should be callbacks that are registered for in + /// . No other external objects should hold a reference to this. Unless the user clicks + /// 'Never show this again', this will persist for the life of the VS instance, and does not need to be manually disposed + /// in that case. + /// + [Export(typeof(IExperiment))] + internal sealed class KeybindingResetDetector : ForegroundThreadAffinitizedObject, IExperiment, IOleCommandTarget, IDisposable + { + // Flight info + private const string InternalFlightName = "keybindgoldbarint"; + private const string ExternalFlightName = "keybindgoldbarext"; + private const string KeybindingsFwLink = "https://go.microsoft.com/fwlink/?linkid=864209"; + + // Resharper commands and package + private const uint ResumeId = 707; + private const uint SuspendId = 708; + private const uint ToggleSuspendId = 709; + private static readonly Guid ReSharperPackageGuid = new Guid("0C6E6407-13FC-4878-869A-C8B4016C57FE"); + private static readonly Guid ReSharperCommandGroup = new Guid("{47F03277-5055-4922-899C-0F7F30D26BF1}"); + + private readonly VisualStudioWorkspace _workspace; + private readonly SVsServiceProvider _serviceProvider; + + private IExperimentationService _experimentationService; + private IVsUIShell _uiShell; + private IOleCommandTarget _oleCommandTarget; + + private bool _disposedValue = false; + private uint _priorityCommandTargetCookie = VSConstants.VSCOOKIE_NIL; + + /// + /// Must compare/write with Interlocked.CompareExchange, as can be called on any thread. + /// + const int InfoBarOpen = 1; + const int InfoBarClosed = 0; + private int _infoBarOpen = InfoBarClosed; + + [ImportingConstructor] + public KeybindingResetDetector(VisualStudioWorkspace workspace, SVsServiceProvider serviceProvider) + { + _workspace = workspace; + _serviceProvider = serviceProvider; + } + + public Task Initialize() + { + // Immediately bail if the user has asked to never see this bar again. + if (_workspace.Options.GetOption(KeybindingResetOptions.NeverShowAgain)) + { + return Task.CompletedTask; + } + + return InvokeBelowInputPriority(() => InitializeCore()); + } + + private void InitializeCore() + { + AssertIsForeground(); + + // Ensure one of the flights is enabled, otherwise bail + _experimentationService = _workspace.Services.GetRequiredService(); + if (!_experimentationService.IsExperimentEnabled(ExternalFlightName) && !_experimentationService.IsExperimentEnabled(InternalFlightName)) + { + return; + } + + var vsShell = _serviceProvider.GetService(typeof(SVsShell)) as IVsShell; + var hr = vsShell.IsPackageInstalled(ReSharperPackageGuid, out int extensionEnabled); + if (ErrorHandler.Failed(hr)) + { + FatalError.ReportWithoutCrash(Marshal.GetExceptionForHR(hr)); + return; + } + + var currentStatus = _workspace.Options.GetOption(KeybindingResetOptions.ReSharperStatus); + + if (extensionEnabled == 0) + { + // If 0, the extension is either disabled in the extension manager, or not installed at all. + // If this is a change, update the status. + bool needsReset = _workspace.Options.GetOption(KeybindingResetOptions.NeedsReset); + if (currentStatus != ReSharperStatus.NotInstalledOrDisabled) + { + // If we're going directly from Enabled->NotInstalled, we need to reset keybindings. Otherwise, the previous value of NeedsReset + // is correct. + var changedOptions = _workspace.Options; + if (currentStatus == ReSharperStatus.Enabled) + { + needsReset = true; + changedOptions = changedOptions.WithChangedOption(KeybindingResetOptions.NeedsReset, true); + } + + changedOptions = _workspace.Options.WithChangedOption(KeybindingResetOptions.ReSharperStatus, ReSharperStatus.NotInstalledOrDisabled); + + _workspace.Options = changedOptions; + } + + if (needsReset) + { + ShowGoldBar(); + } + } + else + { + UpdateReSharperEnableStatus(); + + // We need to monitor for suspend/resume commands, so create and install the command target. + var priorityCommandTargetRegistrar = _serviceProvider.GetService(typeof(SVsRegisterPriorityCommandTarget)) as IVsRegisterPriorityCommandTarget; + hr = priorityCommandTargetRegistrar.RegisterPriorityCommandTarget( + dwReserved: 0 /* from docs must be 0 */, + pCmdTrgt: this, + pdwCookie: out _priorityCommandTargetCookie); + + if (ErrorHandler.Failed(hr)) + { + FatalError.ReportWithoutCrash(Marshal.GetExceptionForHR(hr)); + } + } + } + + private void UpdateReSharperEnableStatus() + { + AssertIsForeground(); + + var isEnabled = IsReSharperEnabled(); + var options = _workspace.Options; + + if (isEnabled) + { + // Update the option if we weren't already enabled. There's no other actions to take, since + // ReSharper is enabled and we don't want to pop a gold bar + if (options.GetOption(KeybindingResetOptions.ReSharperStatus) != ReSharperStatus.Enabled) + { + options = options.WithChangedOption(KeybindingResetOptions.ReSharperStatus, ReSharperStatus.Enabled) + .WithChangedOption(KeybindingResetOptions.NeedsReset, false); + _workspace.Options = options; + } + } + else + { + bool needsReset; + if (options.GetOption(KeybindingResetOptions.ReSharperStatus) != ReSharperStatus.Suspended) + { + options = options.WithChangedOption(KeybindingResetOptions.ReSharperStatus, ReSharperStatus.Suspended) + .WithChangedOption(KeybindingResetOptions.NeedsReset, true); + needsReset = true; + _workspace.Options = options; + } + else + { + needsReset = options.GetOption(KeybindingResetOptions.NeedsReset); + } + + if (needsReset) + { + ShowGoldBar(); + } + } + } + + private bool IsReSharperEnabled() + { + AssertIsForeground(); + + if (_oleCommandTarget == null) + { + var oleServiceProvider = (IOleServiceProvider)_serviceProvider.GetService(typeof(IOleServiceProvider)); + _oleCommandTarget = (IOleCommandTarget)oleServiceProvider.QueryService(VSConstants.SID_SUIHostCommandDispatcher); + } + + var cmds = new OLECMD[1]; + cmds[0].cmdID = SuspendId; + cmds[0].cmdf = 0; + + ErrorHandler.ThrowOnFailure(_oleCommandTarget.QueryStatus(ReSharperCommandGroup, (uint)cmds.Length, cmds, IntPtr.Zero)); + + // When ReSharper is enabled, the ReSharper_Suspend command has the Enabled | Supported flags. When disabled, it has Invisible | Supported. + return ((OLECMDF)cmds[0].cmdf).HasFlag(OLECMDF.OLECMDF_ENABLED); + } + + private void ShowGoldBar() + { + ThisCanBeCalledOnAnyThread(); + + // If the gold bar is already open, do not show + if (Interlocked.CompareExchange(ref _infoBarOpen, InfoBarOpen, InfoBarClosed) == InfoBarOpen) + { + return; + } + + string message; + if (_experimentationService.IsExperimentEnabled(InternalFlightName)) + { + message = ServicesVSResources.We_noticed_you_suspended_ReSharper_Ultimate_Restore_Visual_Studio_keybindings_to_continue_to_navigate_and_refactor; + } + else if (_experimentationService.IsExperimentEnabled(ExternalFlightName)) + { + message = ServicesVSResources.We_noticed_your_keybindings_are_broken; + } + else + { + // Should never have gotten to checking this if one of the flights isn't enabled. + throw ExceptionUtilities.Unreachable; + } + + var infoBarService = _workspace.Services.GetRequiredService(); + infoBarService.ShowInfoBarInGlobalView( + message, + new InfoBarUI(title: ServicesVSResources.Restore_Visual_Studio_keybindings, + kind: InfoBarUI.UIKind.HyperLink, + action: RestoreVsKeybindings), + new InfoBarUI(title: ServicesVSResources.Use_Visual_Studio_Keybindings_for_extensions, + kind: InfoBarUI.UIKind.HyperLink, + action: OpenExtensionsHyperlink), + new InfoBarUI(title: ServicesVSResources.Never_show_this_again, + kind: InfoBarUI.UIKind.Button, + action: NeverShowAgain), + new InfoBarUI(title: "", kind: InfoBarUI.UIKind.Close, + action: InfoBarClose)); + } + + private void RestoreVsKeybindings() + { + AssertIsForeground(); + + if (_uiShell == null) + { + _uiShell = _serviceProvider.GetService(typeof(SVsUIShell)) as IVsUIShell; + } + + ErrorHandler.ThrowOnFailure(_uiShell.PostExecCommand( + VSConstants.GUID_VSStandardCommandSet97, + (uint)VSConstants.VSStd97CmdID.CustomizeKeyboard, + (uint)OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT, + null)); + + _workspace.Options = _workspace.Options.WithChangedOption(KeybindingResetOptions.NeedsReset, false); + } + + private void OpenExtensionsHyperlink() + { + ThisCanBeCalledOnAnyThread(); + Process.Start(KeybindingsFwLink); + + _workspace.Options = _workspace.Options.WithChangedOption(KeybindingResetOptions.NeedsReset, false); + } + + private void NeverShowAgain() + { + AssertIsForeground(); + + _workspace.Options = _workspace.Options.WithChangedOption(KeybindingResetOptions.NeverShowAgain, true); + + // The only external references to this object are as callbacks, which are removed by the dispose method. + Dispose(); + } + + private void InfoBarClose() + { + _infoBarOpen = InfoBarClosed; + } + + public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText) + { + // We don't care about query status, only when the command is actually executed + return (int)OLE.Interop.Constants.OLECMDERR_E_NOTSUPPORTED; + } + + public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) + { + if (pguidCmdGroup == ReSharperCommandGroup && nCmdID >= ResumeId && nCmdID <= ToggleSuspendId) + { + // Don't delay command processing to update resharper status + Task.Run(() => InvokeBelowInputPriority(UpdateReSharperEnableStatus)); + } + + // No matter the command, we never actually want to respond to it, so always return not supported. We're just monitoring. + return (int)OLE.Interop.Constants.OLECMDERR_E_NOTSUPPORTED; + } + + + void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + if (_priorityCommandTargetCookie != VSConstants.VSCOOKIE_NIL) + { + AssertIsForeground(); + var priorityCommandTargetRegistrar = _serviceProvider.GetService(typeof(SVsRegisterPriorityCommandTarget)) as IVsRegisterPriorityCommandTarget; + var hr = priorityCommandTargetRegistrar.UnregisterPriorityCommandTarget(_priorityCommandTargetCookie); + if (ErrorHandler.Failed(hr)) + { + FatalError.ReportWithoutCrash(Marshal.GetExceptionForHR(hr)); + } + } + } + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/VisualStudio/Core/Def/Implementation/Options/LocalUserRegistryOptionPersister.cs b/src/VisualStudio/Core/Def/Implementation/Options/LocalUserRegistryOptionPersister.cs index 4f4bbc59f0242a82679d5bc8b2577a50b6700f96..8542a7961f009a95392dad8f9f70657d8b989472 100644 --- a/src/VisualStudio/Core/Def/Implementation/Options/LocalUserRegistryOptionPersister.cs +++ b/src/VisualStudio/Core/Def/Implementation/Options/LocalUserRegistryOptionPersister.cs @@ -4,6 +4,7 @@ using System.ComponentModel.Composition; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Options.Providers; @@ -154,6 +155,19 @@ bool IOptionPersister.TryPersist(OptionKey optionKey, object value) subKey.SetValue(key, value, RegistryValueKind.QWord); return true; } + else if (optionKey.Option.Type.IsEnum) + { + // If the enum is larger than an int, store as a QWord + if (Marshal.SizeOf(Enum.GetUnderlyingType(optionKey.Option.Type)) > Marshal.SizeOf(typeof(int))) + { + subKey.SetValue(key, (long)value, RegistryValueKind.QWord); + } + else + { + subKey.SetValue(key, (int)value, RegistryValueKind.DWord); + } + return true; + } else { subKey.SetValue(key, value); diff --git a/src/VisualStudio/Core/Def/RoslynPackage.cs b/src/VisualStudio/Core/Def/RoslynPackage.cs index 2af413874070b9c6f0b73a704041c3072d17e48f..448f3ae3d8965f4bb7ebad66060d940437c76d9b 100644 --- a/src/VisualStudio/Core/Def/RoslynPackage.cs +++ b/src/VisualStudio/Core/Def/RoslynPackage.cs @@ -32,6 +32,7 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.VisualStudio.LanguageServices.Telemetry; using Microsoft.CodeAnalysis.Experiments; +using Microsoft.VisualStudio.LanguageServices.Experimentation; namespace Microsoft.VisualStudio.LanguageServices.Setup { @@ -118,7 +119,7 @@ protected override void LoadComponentsInUIContext() Task.Run(() => LoadComponentsBackground()); } - private void LoadComponentsBackground() + private async Task LoadComponentsBackground() { // Perf: Initialize the command handlers. var commandHandlerServiceFactory = this.ComponentModel.GetService(); @@ -127,6 +128,13 @@ private void LoadComponentsBackground() this.ComponentModel.GetService(); this.ComponentModel.GetService(); + + // Initialize any experiments async + var experiments = this.ComponentModel.DefaultExportProvider.GetExports(); + foreach (var experiment in experiments) + { + await experiment.Value.Initialize().ConfigureAwait(false); + } } private void LoadInteractiveMenus() diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs b/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs index 9b5f5ccd89003b73ccefe64ceade3bfbe2dd45eb..ce7f784b0c09f117fcdbd8712a8aa3adf8a528ea 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs +++ b/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs @@ -2005,6 +2005,15 @@ internal class ServicesVSResources { } } + /// + /// Looks up a localized string similar to Restore Visual Studio keybindings. + /// + internal static string Restore_Visual_Studio_keybindings { + get { + return ResourceManager.GetString("Restore_Visual_Studio_keybindings", resourceCulture); + } + } + /// /// Looks up a localized string similar to Returns:. /// @@ -2506,6 +2515,15 @@ internal class ServicesVSResources { } } + /// + /// Looks up a localized string similar to Use Visual Studio keybindings for ReSharper/IntelliJ/Vim/etc.. + /// + internal static string Use_Visual_Studio_Keybindings_for_extensions { + get { + return ResourceManager.GetString("Use_Visual_Studio_Keybindings_for_extensions", resourceCulture); + } + } + /// /// Looks up a localized string similar to Validating breakpoint location.... /// @@ -2542,6 +2560,25 @@ internal class ServicesVSResources { } } + /// + /// Looks up a localized string similar to We noticed you suspended ‘ReSharper Ultimate’. Restore Visual Studio keybindings to continue to navigate and refactor.. + /// + internal static string We_noticed_you_suspended_ReSharper_Ultimate_Restore_Visual_Studio_keybindings_to_continue_to_navigate_and_refactor { + get { + return ResourceManager.GetString("We_noticed_you_suspended_ReSharper_Ultimate_Restore_Visual_Studio_keybindings_to_" + + "continue_to_navigate_and_refactor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to We noticed your keybindings are broken.. + /// + internal static string We_noticed_your_keybindings_are_broken { + get { + return ResourceManager.GetString("We_noticed_your_keybindings_are_broken", resourceCulture); + } + } + /// /// Looks up a localized string similar to When generating properties:. /// diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.resx b/src/VisualStudio/Core/Def/ServicesVSResources.resx index 4e1cd28c6c48a86ae3abbe83d5e2953db84cf28a..e1254a176948d1feed2d276ee30123abeb6928de 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.resx +++ b/src/VisualStudio/Core/Def/ServicesVSResources.resx @@ -977,4 +977,16 @@ Additional information: {1} Search found no results + + Restore Visual Studio keybindings + + + Use Visual Studio keybindings for ReSharper/IntelliJ/Vim/etc. + + + We noticed your keybindings are broken. + + + We noticed you suspended ‘ReSharper Ultimate’. Restore Visual Studio keybindings to continue to navigate and refactor. + \ No newline at end of file