Initial addition of ReSharper status monitor for keybindings reset.

上级 9746c895
// 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> ReSharperStatus = new Option<ReSharperStatus>(nameof(KeybindingResetOptions),
nameof(ReSharperStatus), defaultValue: Experimentation.ReSharperStatus.NotInstalledOrDisabled,
storageLocations: new LocalUserProfileStorageLocation(LocalRegistryPath + nameof(ReSharperStatus)));
public static readonly Option<bool> NeedsReset = new Option<bool>(nameof(KeybindingResetOptions),
nameof(NeedsReset), defaultValue: false,
storageLocations: new LocalUserProfileStorageLocation(LocalRegistryPath + nameof(NeedsReset)));
public static readonly Option<bool> NeverShowAgain = new Option<bool>(nameof(KeybindingResetOptions),
nameof(NeverShowAgain), defaultValue: false,
storageLocations: new LocalUserProfileStorageLocation(LocalRegistryPath + nameof(NeverShowAgain)));
}
}
// 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
{
/// <summary>
/// Disabled in the extension manager or not installed.
/// </summary>
NotInstalledOrDisabled,
/// <summary>
/// ReSharper is suspended. Package is loaded, but is not actually performing actions.
/// </summary>
Suspended,
/// <summary>
/// ReSharper is running.
/// </summary>
Enabled
}
}
// 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();
}
}
// 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
{
/// <summary>
/// Detects if keybindings have been messed up by ReSharper disable, and offers the user the ability
/// to reset if so.
/// </summary>
/// <remarks>
/// The only objects to hold permanent references to this object should be callbacks that are registered for in
/// <see cref="InitializeCore"/>. 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.
/// </remarks>
[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;
/// <summary>
/// Must compare/write with Interlocked.CompareExchange, as <see cref="ShowGoldBar"/> can be called on any thread.
/// </summary>
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<IExperimentationService>();
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<IInfoBarService>();
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);
}
}
}
......@@ -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);
......
......@@ -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<ICommandHandlerServiceFactory>();
......@@ -127,6 +128,13 @@ private void LoadComponentsBackground()
this.ComponentModel.GetService<MiscellaneousTodoListTable>();
this.ComponentModel.GetService<MiscellaneousDiagnosticListTable>();
// Initialize any experiments async
var experiments = this.ComponentModel.DefaultExportProvider.GetExports<IExperiment>();
foreach (var experiment in experiments)
{
await experiment.Value.Initialize().ConfigureAwait(false);
}
}
private void LoadInteractiveMenus()
......
......@@ -2005,6 +2005,15 @@ internal class ServicesVSResources {
}
}
/// <summary>
/// Looks up a localized string similar to Restore Visual Studio keybindings.
/// </summary>
internal static string Restore_Visual_Studio_keybindings {
get {
return ResourceManager.GetString("Restore_Visual_Studio_keybindings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Returns:.
/// </summary>
......@@ -2506,6 +2515,15 @@ internal class ServicesVSResources {
}
}
/// <summary>
/// Looks up a localized string similar to Use Visual Studio keybindings for ReSharper/IntelliJ/Vim/etc..
/// </summary>
internal static string Use_Visual_Studio_Keybindings_for_extensions {
get {
return ResourceManager.GetString("Use_Visual_Studio_Keybindings_for_extensions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Validating breakpoint location....
/// </summary>
......@@ -2542,6 +2560,25 @@ internal class ServicesVSResources {
}
}
/// <summary>
/// Looks up a localized string similar to We noticed you suspended ‘ReSharper Ultimate’. Restore Visual Studio keybindings to continue to navigate and refactor..
/// </summary>
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);
}
}
/// <summary>
/// Looks up a localized string similar to We noticed your keybindings are broken..
/// </summary>
internal static string We_noticed_your_keybindings_are_broken {
get {
return ResourceManager.GetString("We_noticed_your_keybindings_are_broken", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to When generating properties:.
/// </summary>
......
......@@ -977,4 +977,16 @@ Additional information: {1}</value>
<data name="Search_found_no_results" xml:space="preserve">
<value>Search found no results</value>
</data>
<data name="Restore_Visual_Studio_keybindings" xml:space="preserve">
<value>Restore Visual Studio keybindings</value>
</data>
<data name="Use_Visual_Studio_Keybindings_for_extensions" xml:space="preserve">
<value>Use Visual Studio keybindings for ReSharper/IntelliJ/Vim/etc.</value>
</data>
<data name="We_noticed_your_keybindings_are_broken" xml:space="preserve">
<value>We noticed your keybindings are broken.</value>
</data>
<data name="We_noticed_you_suspended_ReSharper_Ultimate_Restore_Visual_Studio_keybindings_to_continue_to_navigate_and_refactor" xml:space="preserve">
<value>We noticed you suspended ‘ReSharper Ultimate’. Restore Visual Studio keybindings to continue to navigate and refactor.</value>
</data>
</root>
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册