Skip to content

Commit

Permalink
Merge pull request #3016 from VsVim/dev/nosami/fix-3015
Browse files Browse the repository at this point in the history
Handle alternative keyboard layouts a little better.
  • Loading branch information
nosami committed Sep 29, 2022
2 parents 604b6a9 + 2af829e commit 2250e9b
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 13 deletions.
14 changes: 11 additions & 3 deletions Src/VimMac/AlternateKeyUtil.cs
Expand Up @@ -236,10 +236,18 @@ bool IKeyUtil.TryConvertSpecialToKeyInput(NSEvent theEvent, out KeyInput keyInpu

private bool GetKeyInputFromKey(NSEvent theEvent, NSEventModifierMask modifierKeys, out KeyInput keyInput)
{
if(!string.IsNullOrEmpty(theEvent.CharactersIgnoringModifiers))
if(!string.IsNullOrEmpty(theEvent.Characters))
{
var keyModifiers = ConvertToKeyModifiers(modifierKeys);
keyInput = KeyInputUtil.ApplyKeyModifiersToChar(theEvent.CharactersIgnoringModifiers[0], keyModifiers);
VimKeyModifiers keyModifiers = VimKeyModifiers.None;
// With some keyboard layouts, Option + 4 produces the '$' symbol
// We don't want to convert this to Alt+$ as the modifier has already been applied here.
// In this case, CharactersIgnoringModifiers = "4" and Characters = "$"
if (theEvent.CharactersIgnoringModifiers == theEvent.Characters)
{
keyModifiers = ConvertToKeyModifiers(modifierKeys);
}

keyInput = KeyInputUtil.ApplyKeyModifiersToChar(theEvent.Characters[0], keyModifiers);
return true;
}
keyInput = null;
Expand Down
57 changes: 57 additions & 0 deletions Src/VimMac/DeadCharHandler.cs
@@ -0,0 +1,57 @@
using AppKit;
using Microsoft.VisualStudio.Text.Editor;

namespace Vim.UI.Cocoa
{
internal sealed class DeadCharHandler
{
private bool _lastEventWasDeadChar;
private bool _processingDeadChar;
private string _convertedDeadCharacters;
private readonly ITextView _textView;
private InvisibleTextView _invisibleTextView;

public DeadCharHandler(ITextView textView)
{
_textView = textView;
}

public string ConvertedDeadCharacters => _convertedDeadCharacters;

internal void SetConvertedDeadCharacters(string value)
{
_convertedDeadCharacters = value;
}

public bool LastEventWasDeadChar => _lastEventWasDeadChar;
public bool ProcessingDeadChar => _processingDeadChar;

public void InterpretEvent(NSEvent keyPress)
{
if (_convertedDeadCharacters != null)
{
// reset state
_convertedDeadCharacters = null;
_processingDeadChar = false;
}

_lastEventWasDeadChar = _processingDeadChar;

_processingDeadChar = KeyEventIsDeadChar(keyPress);

if (!_processingDeadChar && !_lastEventWasDeadChar)
{
return;
}

_invisibleTextView ??= new InvisibleTextView(this, _textView);
// Send the cloned key press to the invisible NSTextView
_invisibleTextView.InterpretEvent(keyPress);
}

private bool KeyEventIsDeadChar(NSEvent e)
{
return string.IsNullOrEmpty(e.Characters);
}
}
}
72 changes: 72 additions & 0 deletions Src/VimMac/InvisibleTextView.cs
@@ -0,0 +1,72 @@
using AppKit;
using Foundation;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;

namespace Vim.UI.Cocoa
{
/// <summary>
/// An invisible NSTextView used so we can leverage macOS to handle
/// the conversion of keycodes and dead key presses into characters for
/// any keyboard layout.
/// </summary>
internal sealed class InvisibleTextView : NSTextView
{
private readonly DeadCharHandler _deadCharHandler;
private readonly ITextBuffer _textBuffer;

public InvisibleTextView(DeadCharHandler deadCharHandler, ITextView textView)
{
_deadCharHandler = deadCharHandler;
_textBuffer = textView.TextBuffer;
textView.TextBuffer.Changing += TextBuffer_Changing;
textView.Closed += TextView_Closed;
}

public void InterpretEvent(NSEvent keypress)
{
InterpretKeyEvents(new[] { CloneEvent(keypress) });
}

public override void InsertText(NSObject text, NSRange replacementRange)
{
if (_deadCharHandler.LastEventWasDeadChar && !_deadCharHandler.ProcessingDeadChar)
{
// This is where we find out how the combination of keypresses
// has been interpreted.
_deadCharHandler.SetConvertedDeadCharacters((text as NSString)?.ToString());
}
}

private void TextBuffer_Changing(object sender, TextContentChangingEventArgs e)
{
if (_deadCharHandler.LastEventWasDeadChar || _deadCharHandler.ProcessingDeadChar)
{
// We need the dead key press event to register in the editor so
// that we get the correct subsequent keypress events, but we
// don't want to modify the textbuffer contents.
e.Cancel();
}
}

private void TextView_Closed(object sender, System.EventArgs e)
{
_textBuffer.Changing -= TextBuffer_Changing;
}

private NSEvent CloneEvent(NSEvent keyPress)
{
return NSEvent.KeyEvent(
keyPress.Type,
keyPress.LocationInWindow,
keyPress.ModifierFlags,
keyPress.Timestamp,
keyPress.WindowNumber,
keyPress.Context,
keyPress.Characters,
keyPress.CharactersIgnoringModifiers,
keyPress.IsARepeat,
keyPress.KeyCode);
}
}
}
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.18"
Version = "2.8.0.19"
)]

[assembly: AddinName("VsVim")]
Expand Down
41 changes: 32 additions & 9 deletions Src/VimMac/VimKeyProcessor.cs
Expand Up @@ -24,6 +24,7 @@ internal sealed class VimKeyProcessor : KeyProcessor
private readonly ICompletionBroker _completionBroker;
private readonly ISignatureHelpBroker _signatureHelpBroker;
private readonly InlineRenameListenerFactory _inlineRenameListenerFactory;
private readonly DeadCharHandler _deadCharHandler;

public VimKeyProcessor(
IVimBuffer vimBuffer,
Expand All @@ -39,6 +40,7 @@ internal sealed class VimKeyProcessor : KeyProcessor
_completionBroker = completionBroker;
_signatureHelpBroker = signatureHelpBroker;
_inlineRenameListenerFactory = inlineRenameListenerFactory;
_deadCharHandler = new DeadCharHandler(_textView);
}

public override bool IsInterestedInHandledEvents => true;
Expand All @@ -62,15 +64,36 @@ public override void KeyDown(KeyEventArgs e)

bool handled = false;

// 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)
_deadCharHandler.InterpretEvent(e.Event);
if (KeyEventIsDeadChar(e))
{
handled = TryProcess(e, keyInput);
// Although there is nothing technically left to do, we still
// need to make sure that the event is processed by the
// underlying NSView so that InterpretKeyEvents is called
handled = false;
}
else
{
if (_deadCharHandler.ConvertedDeadCharacters != null)
{
foreach (var c in _deadCharHandler.ConvertedDeadCharacters)
{
var key = KeyInputUtil.CharToKeyInput(c);
handled &= TryProcess(null, key);
}
}
else
{
// 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)
{
handled = TryProcess(e, keyInput);
}
}
}

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

var status = Mac.StatusBar.GetStatus(_vimBuffer);
Expand All @@ -96,7 +119,7 @@ private bool IsEscapeKey(KeyEventArgs e)

private bool TryProcess(KeyEventArgs e, KeyInput keyInput)
{
if (KeyEventIsDeadChar(e))
if (e != null && KeyEventIsDeadChar(e))
// When a dead key combination is pressed we will get the key down events in
// sequence after the combination is complete. The dead keys will come first
// and be followed the final key which produces the char. That final key
Expand All @@ -122,7 +145,7 @@ private bool TryProcess(KeyEventArgs e, KeyInput keyInput)
if (_inlineRenameListenerFactory.InRename)
return false;

if (_vimBuffer.ModeKind.IsAnyInsert() && 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
Expand Down

0 comments on commit 2250e9b

Please sign in to comment.