From 593014dadba01d3a198696d1ae6eb42018b1cea0 Mon Sep 17 00:00:00 2001 From: nosami Date: Thu, 6 May 2021 13:49:24 +0100 Subject: [PATCH] Added KeyMappingTimeoutHandler to fix inoremap Fixes inoremap BufferedInput issues and makes the code closer to Windows version --- Src/VimMac/KeyMappingTimeoutHandler.cs | 139 +++++++++++++++++++++++++ Src/VimMac/VimKeyProcessor.cs | 36 +++---- 2 files changed, 152 insertions(+), 23 deletions(-) create mode 100644 Src/VimMac/KeyMappingTimeoutHandler.cs diff --git a/Src/VimMac/KeyMappingTimeoutHandler.cs b/Src/VimMac/KeyMappingTimeoutHandler.cs new file mode 100644 index 0000000000..e5e4eeea36 --- /dev/null +++ b/Src/VimMac/KeyMappingTimeoutHandler.cs @@ -0,0 +1,139 @@ +using System; +using System.ComponentModel.Composition; +using System.Windows.Threading; + +namespace Vim.UI.Wpf.Implementation.Misc +{ + /// + /// 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 + /// + [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); + } + } + + /// + /// 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 + /// + 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; + + /// + /// This event is raised whenever any of the timers for the underlying IVimBuffer values + /// expires + /// + 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 + } +} diff --git a/Src/VimMac/VimKeyProcessor.cs b/Src/VimMac/VimKeyProcessor.cs index 7340575e40..1c5e72d9f2 100644 --- a/Src/VimMac/VimKeyProcessor.cs +++ b/Src/VimMac/VimKeyProcessor.cs @@ -41,6 +41,8 @@ internal sealed class VimKeyProcessor : KeyProcessor _inlineRenameListenerFactory = inlineRenameListenerFactory; } + public override bool IsInterestedInHandledEvents => true; + /// /// This handler is necessary to intercept keyboard input which maps to Vim /// commands but doesn't map to text input. Any combination which can be @@ -66,16 +68,7 @@ public override void KeyDown(KeyEventArgs e) bool canConvert = _keyUtil.TryConvertSpecialToKeyInput(e.Event, out KeyInput keyInput); if (canConvert) { - if (ShouldBeProcessedByVim(e, keyInput)) - { - - handled = TryProcess(keyInput); - } - else - { - // Needed to handle things like insert mode macro recording - _vimBuffer.SimulateProcessed(keyInput); - } + handled = TryProcess(e, keyInput); } VimTrace.TraceInfo("VimKeyProcessor::KeyDown Handled = {0}", handled); @@ -91,11 +84,6 @@ public override void KeyDown(KeyEventArgs e) e.Handled = handled; } - private bool TryProcess(KeyInput keyInput) - { - return _vimBuffer.CanProcessAsCommand(keyInput) && _vimBuffer.Process(keyInput).IsAnyHandled; - } - private bool KeyEventIsDeadChar(KeyEventArgs e) { return string.IsNullOrEmpty(e.Characters); @@ -106,7 +94,7 @@ private bool IsEscapeKey(KeyEventArgs e) return (NSKey)e.Event.KeyCode == NSKey.Escape; } - private bool ShouldBeProcessedByVim(KeyEventArgs e, KeyInput keyInput) + 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 @@ -120,16 +108,15 @@ private bool ShouldBeProcessedByVim(KeyEventArgs e, KeyInput keyInput) if ((_vimBuffer.ModeKind.IsAnyInsert() || _vimBuffer.ModeKind.IsAnySelect()) && !_vimBuffer.CanProcessAsCommand(keyInput) && - keyInput.Char > 0x1f) + keyInput.Char > 0x1f && + _vimBuffer.BufferedKeyInputs.IsEmpty && + !_vimBuffer.Vim.MacroRecorder.IsRecording) return false; - if (IsEscapeKey(e)) - return true; - - if (_completionBroker.IsCompletionActive(_textView)) + if (_completionBroker.IsCompletionActive(_textView) && !IsEscapeKey(e)) return false; - if (_signatureHelpBroker.IsSignatureHelpActive(_textView)) + if (_signatureHelpBroker.IsSignatureHelpActive(_textView) && !IsEscapeKey(e)) return false; if (_inlineRenameListenerFactory.InRename) @@ -137,9 +124,12 @@ private bool ShouldBeProcessedByVim(KeyEventArgs e, KeyInput keyInput) 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; } } }