Skip to content

Commit

Permalink
Merge pull request #2889 from VsVim/dev/nosami/fix-key-processor
Browse files Browse the repository at this point in the history
[Mac] Fixes some issues with VimKeyProcessor
  • Loading branch information
nosami committed May 29, 2021
2 parents 5124ebc + 593014d commit e50a3fb
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 68 deletions.
139 changes: 139 additions & 0 deletions Src/VimMac/KeyMappingTimeoutHandler.cs
@@ -0,0 +1,139 @@
using System;
using System.ComponentModel.Composition;
using System.Windows.Threading;

namespace Vim.UI.Wpf.Implementation.Misc
{
/// <summary>
/// This class is responsible for handling the timeout of key mappings for a given
/// IVimBuffer. If the timeout occurs before the key mapping is completed then the
/// keys should just be played as normal
/// </summary>
[Export(typeof(IVimBufferCreationListener))]
internal sealed class KeyMappingTimeoutHandler : IVimBufferCreationListener
{
#region TimerData

private sealed class TimerData
{
private readonly IVimBuffer _vimBuffer;
private readonly DispatcherTimer _timer;
private readonly IProtectedOperations _protectedOperations;
private readonly KeyMappingTimeoutHandler _keyMappingTimeoutHandler;

internal TimerData(IVimBuffer vimBuffer, IProtectedOperations protectedOperations, KeyMappingTimeoutHandler keyMappingTimeoutHandler)
{
_protectedOperations = protectedOperations;
_vimBuffer = vimBuffer;
_keyMappingTimeoutHandler = keyMappingTimeoutHandler;
_timer = new DispatcherTimer(DispatcherPriority.Input);
_timer.Tick += OnTimerTick;
_vimBuffer.KeyInputProcessed += OnKeyInputProcessed;
_vimBuffer.KeyInputBuffered += OnKeyInputBuffered;
}

internal void Close()
{
_timer.Tick -= OnTimerTick;
_vimBuffer.KeyInputProcessed -= OnKeyInputProcessed;
_vimBuffer.KeyInputBuffered -= OnKeyInputBuffered;
_timer.Stop();
}

private void OnTimerTick(object sender, EventArgs e)
{
try
{
// If the Timer is still enabled then go ahead and process the buffered
// KeyInput values
if (_timer.IsEnabled)
{
_vimBuffer.ProcessBufferedKeyInputs();
}

_keyMappingTimeoutHandler.RaiseTick();
}
catch (Exception ex)
{
_protectedOperations.Report(ex);
}
}

/// <summary>
/// When a KeyInput value is processed then it should stop the timer if it's
/// currently running. Actually processing a KeyInput means it wasn't buffered
/// </summary>
private void OnKeyInputProcessed(object sender, KeyInputProcessedEventArgs args)
{
_timer.Stop();
}

private void OnKeyInputBuffered(object sender, KeyInputSetEventArgs args)
{
try
{
var globalSettings = _vimBuffer.GlobalSettings;

// If 'timeout' is not enabled then ensure the timer is disabled and return. Ensuring
// it's disabled is necessary because the 'timeout' could be disabled in the middle
// of processing a key mapping
if (!globalSettings.Timeout)
{
_timer.Stop();
return;
}

if (_timer.IsEnabled)
{
_timer.Stop();
}

_timer.Interval = TimeSpan.FromMilliseconds(globalSettings.TimeoutLength);
_timer.Start();
}
catch (Exception ex)
{
// Several DispatcherTimer operations including setting the Interval can throw
// so catch them all here
_protectedOperations.Report(ex);
}
}
}

#endregion

private readonly IProtectedOperations _protectedOperations;

/// <summary>
/// This event is raised whenever any of the timers for the underlying IVimBuffer values
/// expires
/// </summary>
internal event EventHandler Tick;

[ImportingConstructor]
internal KeyMappingTimeoutHandler(IProtectedOperations protectedOperations)
{
_protectedOperations = protectedOperations;
}

internal void OnVimBufferCreated(IVimBuffer vimBuffer)
{
var timerData = new TimerData(vimBuffer, _protectedOperations, this);
vimBuffer.Closed += (sender, e) => timerData.Close();
}

private void RaiseTick()
{
Tick?.Invoke(this, EventArgs.Empty);
}

#region IVimBufferCreationListener

void IVimBufferCreationListener.VimBufferCreated(IVimBuffer vimBuffer)
{
OnVimBufferCreated(vimBuffer);
}

#endregion
}
}
2 changes: 1 addition & 1 deletion Src/VimMac/Properties/AddinInfo.cs
Expand Up @@ -5,7 +5,7 @@
[assembly: Addin(
"VsVim",
Namespace = "Vim.Mac",
Version = "2.8.0.11"
Version = "2.8.0.12"
)]

[assembly: AddinName("VsVim")]
Expand Down
96 changes: 29 additions & 67 deletions Src/VimMac/VimKeyProcessor.cs
@@ -1,10 +1,7 @@
using AppKit;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Formatting;
using MonoDevelop.Ide;
using Vim.Mac;
using Vim.UI.Cocoa.Implementation.InlineRename;

namespace Vim.UI.Cocoa
Expand All @@ -21,9 +18,9 @@ namespace Vim.UI.Cocoa
/// </summary>
internal sealed class VimKeyProcessor : KeyProcessor
{
private readonly IVimBuffer _vimBuffer;
private readonly ITextView _textView;
private readonly IKeyUtil _keyUtil;

private IVimBuffer VimBuffer { get; }
private readonly ICompletionBroker _completionBroker;
private readonly ISignatureHelpBroker _signatureHelpBroker;
private readonly InlineRenameListenerFactory _inlineRenameListenerFactory;
Expand All @@ -36,18 +33,15 @@ internal sealed class VimKeyProcessor : KeyProcessor
InlineRenameListenerFactory inlineRenameListenerFactory)

{
VimBuffer = vimBuffer;
_vimBuffer = vimBuffer;
_textView = vimBuffer.TextView;
_keyUtil = keyUtil;
_completionBroker = completionBroker;
_signatureHelpBroker = signatureHelpBroker;
_inlineRenameListenerFactory = inlineRenameListenerFactory;
}

public ITextBuffer TextBuffer => VimBuffer.TextBuffer;

public ITextView TextView => VimBuffer.TextView;

public bool ModeChanged { get; private set; }
public override bool IsInterestedInHandledEvents => true;

/// <summary>
/// This handler is necessary to intercept keyboard input which maps to Vim
Expand All @@ -67,48 +61,21 @@ public override void KeyDown(KeyEventArgs e)
VimTrace.TraceInfo("VimKeyProcessor::KeyDown {0} {1}", e.Characters, e.CharactersIgnoringModifiers);

bool handled = false;
if (ShouldBeProcessedByVim(e))

// Attempt to map the key information into a KeyInput value which can be processed
// by Vim. If this works and the key is processed then the input is considered
// to be handled
bool canConvert = _keyUtil.TryConvertSpecialToKeyInput(e.Event, out KeyInput keyInput);
if (canConvert)
{
var oldMode = VimBuffer.Mode.ModeKind;

VimTrace.TraceDebug(oldMode.ToString());
// Attempt to map the key information into a KeyInput value which can be processed
// by Vim. If this works and the key is processed then the input is considered
// to be handled

if (_keyUtil.TryConvertSpecialToKeyInput(e.Event, out KeyInput keyInput))
{
var bufferedKeyInputsWasEmpty = VimBuffer.BufferedKeyInputs.IsEmpty;

handled = TryProcess(keyInput);

if (handled
&& BufferedKeysWasEmptyAndIsEmpty()
&& oldMode == ModeKind.Insert
&& CharTriggersCompletion(keyInput.Char)
&& !_completionBroker.IsCompletionActive(VimBuffer.TextView))
{
// Because VsVim handled the key press for us in insert mode,
// we need to trigger the completion window to open.
_completionBroker.TriggerCompletion(VimBuffer.TextView);
}

bool BufferedKeysWasEmptyAndIsEmpty()
{
// We don't want the completion window to appear if we
// have something like `inoremap fd <esc>`
// and we just typed the first 'f' or the 'd'
return bufferedKeyInputsWasEmpty
&& VimBuffer.BufferedKeyInputs.IsEmpty;
}
}
handled = TryProcess(e, keyInput);
}

VimTrace.TraceInfo("VimKeyProcessor::KeyDown Handled = {0}", handled);

var status = Mac.StatusBar.GetStatus(VimBuffer);
var status = Mac.StatusBar.GetStatus(_vimBuffer);
var text = status.Text;
if (VimBuffer.ModeKind == ModeKind.Command)
if (_vimBuffer.ModeKind == ModeKind.Command)
{
// Add a fake 'caret'
text = text.Insert(status.CaretPosition, "|");
Expand All @@ -117,21 +84,6 @@ bool BufferedKeysWasEmptyAndIsEmpty()
e.Handled = handled;
}

private bool CharTriggersCompletion(char c)
{
return c == '_' || c == '.' || char.IsLetterOrDigit(c);
}

/// <summary>
/// Try and process the given KeyInput with the IVimBuffer. This is overridable by
/// derived classes in order for them to prevent any KeyInput from reaching the
/// IVimBuffer
/// </summary>
private bool TryProcess(KeyInput keyInput)
{
return VimBuffer.CanProcess(keyInput) && VimBuffer.Process(keyInput).IsAnyHandled;
}

private bool KeyEventIsDeadChar(KeyEventArgs e)
{
return string.IsNullOrEmpty(e.Characters);
Expand All @@ -142,7 +94,7 @@ private bool IsEscapeKey(KeyEventArgs e)
return (NSKey)e.Event.KeyCode == NSKey.Escape;
}

private bool ShouldBeProcessedByVim(KeyEventArgs e)
private bool TryProcess(KeyEventArgs e, KeyInput keyInput)
{
if (KeyEventIsDeadChar(e))
// When a dead key combination is pressed we will get the key down events in
Expand All @@ -154,20 +106,30 @@ private bool ShouldBeProcessedByVim(KeyEventArgs e)
// we can process in the TextInput event
return false;

if (_completionBroker.IsCompletionActive(TextView) && !IsEscapeKey(e))
if ((_vimBuffer.ModeKind.IsAnyInsert() || _vimBuffer.ModeKind.IsAnySelect()) &&
!_vimBuffer.CanProcessAsCommand(keyInput) &&
keyInput.Char > 0x1f &&
_vimBuffer.BufferedKeyInputs.IsEmpty &&
!_vimBuffer.Vim.MacroRecorder.IsRecording)
return false;

if (_signatureHelpBroker.IsSignatureHelpActive(TextView) && !IsEscapeKey(e))
if (_completionBroker.IsCompletionActive(_textView) && !IsEscapeKey(e))
return false;

if (_signatureHelpBroker.IsSignatureHelpActive(_textView) && !IsEscapeKey(e))
return false;

if (_inlineRenameListenerFactory.InRename)
return false;

if (VimBuffer.Mode.ModeKind == ModeKind.Insert && e.Characters == "\t")
if (_vimBuffer.ModeKind.IsAnyInsert() && e.Characters == "\t")
// Allow tab key to work for snippet completion
//
// TODO: We should only really do this when the characters
// to the left of the caret form a valid snippet
return false;

return true;
return _vimBuffer.CanProcess(keyInput) && _vimBuffer.Process(keyInput).IsAnyHandled;
}
}
}

0 comments on commit e50a3fb

Please sign in to comment.