diff --git a/docs/docs/configuration/key-bindings.md b/docs/docs/configuration/key-bindings.md index 11d2200bb6..a6946aa767 100644 --- a/docs/docs/configuration/key-bindings.md +++ b/docs/docs/configuration/key-bindings.md @@ -223,3 +223,31 @@ Examples: {"key": "kk", "command": ":split", "when": "editorTextFocus"}, {"key": "", "command": ":d 2", "when": "insertMode"} ``` + +### Leader Key + +A leader key can be specified via the following configuration setting: + +``` +{ "vim.leader": "" } +``` +> NOTE: This setting is in `configuration.json`, not `keybindings.json` + +Alternatively, the leader key can be specified via an `Ex` command: +``` +:nmap +``` + +Once the leader key is defined, it may be used in both `keybindings.json` and via VimL map commands: + +``` +[ + { "key": "p", "command": "workbench.action.quickOpen", "when": "editorTextFocus && normalMode" } +] +``` + +or, alternatively, in VimL: + +``` +:nnoremap p +``` diff --git a/docs/docs/configuration/settings.md b/docs/docs/configuration/settings.md index e61853faa6..3528736bc2 100644 --- a/docs/docs/configuration/settings.md +++ b/docs/docs/configuration/settings.md @@ -98,6 +98,10 @@ The configuration file, `configuration.json` is in the Oni2 directory, whose loc - `vim.highlightedyank.duration` __(_int_ default: `300`)__ - The time, in milliseconds, the yank highlight is visible. +### Input + +- `vim.leader` __(_string_)__ - Specify a custom [leader key](./key-bindings#leader-key). + ### Layout - `workbench.editor.showTabs` __(_bool_ default: `true`)__ - When `false`, hides the editor tabs. diff --git a/integration_test/ExCommandKeybindingTest.re b/integration_test/ExCommandKeybindingTest.re index 794cf33f95..10e3e50b41 100644 --- a/integration_test/ExCommandKeybindingTest.re +++ b/integration_test/ExCommandKeybindingTest.re @@ -17,11 +17,11 @@ runTest( (dispatch, wait, _) => { let input = key => { let keyPress = - EditorInput.KeyPress.{ - scancode: Sdl2.Scancode.ofName(key), - keycode: Sdl2.Keycode.ofName(key), - modifiers: EditorInput.Modifiers.none, - }; + EditorInput.KeyPress.physicalKey( + ~scancode=Sdl2.Scancode.ofName(key), + ~keycode=Sdl2.Keycode.ofName(key), + ~modifiers=EditorInput.Modifiers.none, + ); let time = Revery.Time.now(); dispatch(KeyDown(keyPress, time)); diff --git a/integration_test/ExCommandKeybindingWithArgsTest.re b/integration_test/ExCommandKeybindingWithArgsTest.re index 7313a296e8..b398cd1226 100644 --- a/integration_test/ExCommandKeybindingWithArgsTest.re +++ b/integration_test/ExCommandKeybindingWithArgsTest.re @@ -18,11 +18,11 @@ runTest( (dispatch, wait, _) => { let input = key => { let keyPress = - EditorInput.KeyPress.{ - scancode: Sdl2.Scancode.ofName(key), - keycode: Sdl2.Keycode.ofName(key), - modifiers: EditorInput.Modifiers.none, - }; + EditorInput.KeyPress.physicalKey( + ~scancode=Sdl2.Scancode.ofName(key), + ~keycode=Sdl2.Keycode.ofName(key), + ~modifiers=EditorInput.Modifiers.none, + ); let time = Revery.Time.now(); dispatch(KeyDown(keyPress, time)); diff --git a/integration_test/KeySequenceJJTest.re b/integration_test/KeySequenceJJTest.re index 4b63228c76..9dac512d46 100644 --- a/integration_test/KeySequenceJJTest.re +++ b/integration_test/KeySequenceJJTest.re @@ -21,7 +21,8 @@ runTest( let keycode = Sdl2.Keycode.ofName(key); let modifiers = EditorInput.Modifiers.none; - let keyPress: EditorInput.KeyPress.t = {scancode, keycode, modifiers}; + let keyPress: EditorInput.KeyPress.t = + EditorInput.KeyPress.physicalKey(~keycode, ~scancode, ~modifiers); let time = Revery.Time.now(); dispatch(Model.Actions.KeyDown(keyPress, time)); diff --git a/integration_test/VimSimpleRemapTest.re b/integration_test/VimSimpleRemapTest.re index 5898099a66..81f7bdb15c 100644 --- a/integration_test/VimSimpleRemapTest.re +++ b/integration_test/VimSimpleRemapTest.re @@ -15,7 +15,8 @@ runTest(~name="VimSimpleRemapTest", (dispatch, wait, runEffects) => { let keycode = Sdl2.Keycode.ofName(key); let modifiers = EditorInput.Modifiers.none; - let keyPress: EditorInput.KeyPress.t = {scancode, keycode, modifiers}; + let keyPress: EditorInput.KeyPress.t = + EditorInput.KeyPress.physicalKey(~scancode, ~keycode, ~modifiers); let time = Revery.Time.now(); dispatch(Model.Actions.KeyDown(keyPress, time)); diff --git a/src/Core/Config.re b/src/Core/Config.re index ff5422b478..df9d63b25e 100644 --- a/src/Core/Config.re +++ b/src/Core/Config.re @@ -13,6 +13,8 @@ type key = Lookup.path; type resolver = (~vimSetting: option(string), key) => rawValue; type fileTypeResolver = (~fileType: string) => resolver; +let emptyResolver = (~vimSetting as _, _) => NotSet; + let key = Lookup.path; let keyAsString = Lookup.key; diff --git a/src/Core/Config.rei b/src/Core/Config.rei index 12d469a6aa..635af6f5cb 100644 --- a/src/Core/Config.rei +++ b/src/Core/Config.rei @@ -9,6 +9,8 @@ type rawValue = type resolver = (~vimSetting: option(string), key) => rawValue; type fileTypeResolver = (~fileType: string) => resolver; +let emptyResolver: resolver; + let key: string => key; let keyAsString: key => string; diff --git a/src/Feature/Input/Feature_Input.re b/src/Feature/Input/Feature_Input.re index 1fbeeaa489..adf4889877 100644 --- a/src/Feature/Input/Feature_Input.re +++ b/src/Feature/Input/Feature_Input.re @@ -4,6 +4,67 @@ open Oni_Core; open Utility; module Log = (val Log.withNamespace("Oni2.Feature.Input")); +// CONFIGURATION +module Configuration = { + open Oni_Core; + open Config.Schema; + + module CustomDecoders = { + let physicalKey = + custom( + ~decode= + Json.Decode.( + string + |> and_then(keyString => + if (keyString == "(none)") { + succeed(None); + } else { + switch ( + EditorInput.KeyPress.parse( + ~getKeycode, + ~getScancode, + keyString, + ) + ) { + | Ok([]) => + fail("Unable to parse key sequence: " ++ keyString) + | Ok([key]) => + switch (EditorInput.KeyPress.toPhysicalKey(key)) { + | None => fail("Not a physical key: " ++ keyString) + | Some(physicalKey) => succeed(Some(physicalKey)) + } + | Ok(_keys) => + fail( + "Unable to parse key sequence - too many keys: " + ++ keyString, + ) + | Error(msg) => + fail("Unable to parse key sequence: " ++ msg) + }; + } + ) + ), + ~encode= + Json.Encode.( + maybeKey => { + switch (maybeKey) { + | Some(key) => + EditorInput.KeyPress.toString( + ~keyCodeToString=Sdl2.Keycode.getName, + EditorInput.KeyPress.PhysicalKey(key), + ) + |> string + | None => "(none)" |> string + }; + } + ), + ); + }; + + let leaderKey = + setting("vim.leader", CustomDecoders.physicalKey, ~default=None); +}; + // MSG type outmsg = @@ -73,6 +134,8 @@ type model = { inputStateMachine: InputStateMachine.t, }; +type uniqueId = InputStateMachine.uniqueId; + let initial = keybindings => { open Schema; let inputStateMachine = @@ -107,9 +170,10 @@ type effect = | Unhandled(EditorInput.KeyPress.t) | RemapRecursionLimitHit; -let keyDown = (~key, ~context, {inputStateMachine, _} as model) => { +let keyDown = (~config, ~key, ~context, {inputStateMachine, _} as model) => { + let leaderKey = Configuration.leaderKey.get(config); let (inputStateMachine', effects) = - InputStateMachine.keyDown(~key, ~context, inputStateMachine); + InputStateMachine.keyDown(~leaderKey, ~key, ~context, inputStateMachine); ({...model, inputStateMachine: inputStateMachine'}, effects); }; @@ -119,12 +183,31 @@ let text = (~text, {inputStateMachine, _} as model) => { ({...model, inputStateMachine: inputStateMachine'}, effects); }; -let keyUp = (~key, ~context, {inputStateMachine, _} as model) => { +let keyUp = (~config, ~key, ~context, {inputStateMachine, _} as model) => { + let leaderKey = Configuration.leaderKey.get(config); let (inputStateMachine', effects) = - InputStateMachine.keyUp(~key, ~context, inputStateMachine); + InputStateMachine.keyUp(~leaderKey, ~key, ~context, inputStateMachine); ({...model, inputStateMachine: inputStateMachine'}, effects); }; +let addKeyBinding = (~binding, {inputStateMachine, _} as model) => { + open Schema; + let (inputStateMachine', uniqueId) = + InputStateMachine.addBinding( + binding.matcher, + binding.condition, + binding.command, + inputStateMachine, + ); + ({...model, inputStateMachine: inputStateMachine'}, uniqueId); +}; + +let remove = (uniqueId, {inputStateMachine, _} as model) => { + let inputStateMachine' = + InputStateMachine.remove(uniqueId, inputStateMachine); + {...model, inputStateMachine: inputStateMachine'}; +}; + // UPDATE module Internal = { let vimMapModeToWhenExpr = mode => { @@ -253,4 +336,5 @@ module Commands = { module Contributions = { let commands = Commands.[showInputState]; + let configuration = Configuration.[leaderKey.spec]; }; diff --git a/src/Feature/Input/Feature_Input.rei b/src/Feature/Input/Feature_Input.rei index 2d43069f39..bffc606d69 100644 --- a/src/Feature/Input/Feature_Input.rei +++ b/src/Feature/Input/Feature_Input.rei @@ -47,16 +47,36 @@ type effect = | RemapRecursionLimitHit; let keyDown: - (~key: KeyPress.t, ~context: WhenExpr.ContextKeys.t, model) => + ( + ~config: Config.resolver, + ~key: KeyPress.t, + ~context: WhenExpr.ContextKeys.t, + model + ) => (model, list(effect)); let text: (~text: string, model) => (model, list(effect)); let keyUp: - (~key: KeyPress.t, ~context: WhenExpr.ContextKeys.t, model) => + ( + ~config: Config.resolver, + ~key: KeyPress.t, + ~context: WhenExpr.ContextKeys.t, + model + ) => (model, list(effect)); +type uniqueId; + +let addKeyBinding: + (~binding: Schema.resolvedKeybinding, model) => (model, uniqueId); + +let remove: (uniqueId, model) => model; + // UPDATE let update: (msg, model) => (model, outmsg); -module Contributions: {let commands: list(Command.t(msg));}; +module Contributions: { + let commands: list(Command.t(msg)); + let configuration: list(Config.Schema.spec); +}; diff --git a/src/Input/Handler.re b/src/Input/Handler.re index d6653faa28..92d9701c7b 100644 --- a/src/Input/Handler.re +++ b/src/Input/Handler.re @@ -7,6 +7,8 @@ module Log = (val Oni_Core.Log.withNamespace("Oni2.Input.Handler")); module Zed_utf8 = Oni_Core.ZedBundled; +open Oni_Core.Utility; + module Internal = { let keyCodeToVimString = (keycode, keyString) => { let len = Zed_utf8.length(keyString); @@ -98,25 +100,28 @@ module Internal = { }; }; -let keyPressToCommand = - (~isTextInputActive, {modifiers, keycode, _}: EditorInput.KeyPress.t) => { - let altGr = modifiers.altGr; - let shiftKey = modifiers.shift; - let altKey = modifiers.alt; - let ctrlKey = modifiers.control; - let superKey = modifiers.meta; +let keyPressToCommand = (~isTextInputActive, key) => { + let maybeKey = EditorInput.KeyPress.toPhysicalKey(key); + maybeKey + |> OptionEx.flatMap(({modifiers, keycode, _}: EditorInput.PhysicalKey.t) => { + let altGr = modifiers.altGr; + let shiftKey = modifiers.shift; + let altKey = modifiers.alt; + let ctrlKey = modifiers.control; + let superKey = modifiers.meta; - if (altGr && isTextInputActive) { - None; - // If AltGr is pressed, and we're in text input mode, we'll assume the text input handled it - } else { - Internal.keyPressToString( - ~isTextInputActive, - ~shiftKey, - ~altKey, - ~ctrlKey, - ~superKey, - keycode, - ); - }; + if (altGr && isTextInputActive) { + None; + // If AltGr is pressed, and we're in text input mode, we'll assume the text input handled it + } else { + Internal.keyPressToString( + ~isTextInputActive, + ~shiftKey, + ~altKey, + ~ctrlKey, + ~superKey, + keycode, + ); + }; + }); }; diff --git a/src/Model/State.re b/src/Model/State.re index 33ba6e49e7..944dace799 100644 --- a/src/Model/State.re +++ b/src/Model/State.re @@ -491,6 +491,7 @@ let initial = Feature_AutoUpdate.Contributions.configuration, Feature_Buffers.Contributions.configuration, Feature_Editor.Contributions.configuration, + Feature_Input.Contributions.configuration, Feature_SideBar.Contributions.configuration, Feature_Syntax.Contributions.configuration, Feature_Terminal.Contributions.configuration, diff --git a/src/Store/InputStoreConnector.re b/src/Store/InputStoreConnector.re index 226396e071..a509c0b776 100644 --- a/src/Store/InputStoreConnector.re +++ b/src/Store/InputStoreConnector.re @@ -171,17 +171,11 @@ let start = (window: option(Revery.Window.t), runEffects) => { }; Some( - EditorInput.KeyPress.{ - scancode, - keycode, - modifiers: { - shift, - control, - alt, - meta, - altGr, - }, - }, + EditorInput.KeyPress.physicalKey( + ~scancode, + ~keycode, + ~modifiers={shift, control, alt, meta, altGr}, + ), ); }; }; @@ -199,8 +193,9 @@ let start = (window: option(Revery.Window.t), runEffects) => { let handleKeyPress = (state: State.t, key) => { let context = Model.ContextKeys.all(state); + let config = Model.Selectors.configResolver(state); let (input, effects) = - Feature_Input.keyDown(~context, ~key, state.input); + Feature_Input.keyDown(~config, ~context, ~key, state.input); let newState = {...state, input}; @@ -224,8 +219,10 @@ let start = (window: option(Revery.Window.t), runEffects) => { let handleKeyUp = (state: State.t, key) => { let context = Model.ContextKeys.all(state); + let config = Model.Selectors.configResolver(state); //let inputKey = reveryKeyToEditorKey(key); - let (input, effects) = Feature_Input.keyUp(~context, ~key, state.input); + let (input, effects) = + Feature_Input.keyUp(~config, ~context, ~key, state.input); let newState = {...state, input}; diff --git a/src/editor-input/EditorInput.re b/src/editor-input/EditorInput.re index fbc43c0443..8fb852eb4a 100644 --- a/src/editor-input/EditorInput.re +++ b/src/editor-input/EditorInput.re @@ -2,6 +2,8 @@ module Key = Key; module Modifiers = Modifiers; module Matcher = Matcher; module KeyPress = KeyPress; +module PhysicalKey = PhysicalKey; +module SpecialKey = SpecialKey; module IntSet = Set.Make({ @@ -27,9 +29,23 @@ module type Input = { | Unhandled(KeyPress.t) | RemapRecursionLimitHit; - let keyDown: (~context: context, ~key: KeyPress.t, t) => (t, list(effect)); + let keyDown: + ( + ~leaderKey: option(PhysicalKey.t)=?, + ~context: context, + ~key: KeyPress.t, + t + ) => + (t, list(effect)); let text: (~text: string, t) => (t, list(effect)); - let keyUp: (~context: context, ~key: KeyPress.t, t) => (t, list(effect)); + let keyUp: + ( + ~leaderKey: option(PhysicalKey.t)=?, + ~context: context, + ~key: KeyPress.t, + t + ) => + (t, list(effect)); let remove: (uniqueId, t) => t; @@ -132,10 +148,14 @@ module Make = (Config: { pressedScancodes: IntSet.empty, }; - let keyMatches = (keyMatcher, key: gesture) => { + let keyMatches = (~leaderKey, keyMatcher, key: gesture) => { switch (keyMatcher, key) { - | (KeyPress.{keycode, modifiers, _}, Down(_id, key)) => - key.keycode == keycode && Modifiers.equals(modifiers, key.modifiers) + | (KeyPress.SpecialKey(Leader), Down(_id, KeyPress.PhysicalKey(key))) => + switch (leaderKey) { + | None => false + | Some(leaderKey) => leaderKey == key + } + | (keyPress, Down(_id, key)) => KeyPress.equals(keyPress, key) | _ => false }; }; @@ -145,14 +165,15 @@ module Make = (Config: { bindings: model.bindings |> List.filter(binding => binding.id != uniqueId), }; - let applyKeyToBinding = (~context, key, binding) => + let applyKeyToBinding = (~leaderKey, ~context, key, binding) => if (!binding.enabled(context)) { None; } else { switch (binding.matcher) { | Unmatched(Matcher.AllKeysReleased) when key == AllKeysReleased => Some({...binding, matcher: Matched}) - | Unmatched(Matcher.Sequence([hd, ...tail])) when keyMatches(hd, key) => + | Unmatched(Matcher.Sequence([hd, ...tail])) + when keyMatches(~leaderKey, hd, key) => if (tail == []) { // If the sequence is fully exercise, we're matched! Some({ @@ -170,15 +191,17 @@ module Make = (Config: { }; }; - let applyKeyToBindings = (~context, key, bindings) => { - List.filter_map(applyKeyToBinding(~context, key), bindings); + let applyKeyToBindings = (~leaderKey, ~context, key, bindings) => { + List.filter_map(applyKeyToBinding(~leaderKey, ~context, key), bindings); }; - let applyKeysToBindings = (~context, keys, bindings) => { + let applyKeysToBindings = (~leaderKey, ~context, keys, bindings) => { let bindingsWithKeyUp = keys |> List.fold_left( - (acc, curr) => {applyKeyToBindings(~context, curr, acc)}, + (acc, curr) => { + applyKeyToBindings(~leaderKey, ~context, curr, acc) + }, bindings, ); @@ -212,7 +235,9 @@ module Make = (Config: { let bindingsWithoutUpKey = keysWithoutUps |> List.fold_left( - (acc, curr) => {applyKeyToBindings(~context, curr, acc)}, + (acc, curr) => { + applyKeyToBindings(~leaderKey, ~context, curr, acc) + }, unusedBindings, ); @@ -309,7 +334,8 @@ module Make = (Config: { }; // Pop keys for matcher, and ignore all effects - let useUpKeysForBinding = (~context, ~bindingId, initialBindings) => { + let useUpKeysForBinding = + (~leaderKey, ~context, ~bindingId, initialBindings) => { let rec loop = bindings => // Popped all the way if (bindings.revKeys == []) { @@ -317,6 +343,7 @@ module Make = (Config: { } else { let candidateBindings = applyKeysToBindings( + ~leaderKey, ~context, bindings.revKeys |> List.rev, bindings.bindings, @@ -355,7 +382,8 @@ module Make = (Config: { pressedScancodes: IntSet.empty, }; - let rec handleKeyCore = (~recursionDepth=0, ~context, gesture, bindings) => + let rec handleKeyCore = + (~leaderKey, ~recursionDepth=0, ~context, gesture, bindings) => // Hit the maximum remap recursion depth - just bail on this key. if (recursionDepth > Constants.maxRecursiveDepth) { let eff = @@ -369,7 +397,12 @@ module Make = (Config: { let revKeys = [gesture, ...bindings.revKeys]; let candidateBindings = - applyKeysToBindings(~context, revKeys |> List.rev, bindings.bindings); + applyKeysToBindings( + ~leaderKey, + ~context, + revKeys |> List.rev, + bindings.bindings, + ); let readyBindings = getReadyBindings(candidateBindings); let readyBindingCount = List.length(readyBindings); @@ -387,7 +420,12 @@ module Make = (Config: { // There is a matching, ready binding - let's run it. | Some(binding) => let bindings' = - useUpKeysForBinding(~context, ~bindingId=binding.id, bindings); + useUpKeysForBinding( + ~leaderKey, + ~context, + ~bindingId=binding.id, + bindings, + ); let text = getTextNotMatchingKeys(bindings'.text, revKeys); switch (binding.action) { | Dispatch(command) => ( @@ -398,18 +436,26 @@ module Make = (Config: { | Remap(_) => // Let flush handle the remap action flush( + ~leaderKey, ~recursionDepth, ~context, {...bindings, suppressText: true, revKeys}, ) }; - | None => flush(~recursionDepth, ~context, {...bindings, revKeys}) + | None => + flush( + ~leaderKey, + ~recursionDepth, + ~context, + {...bindings, revKeys}, + ) }; }; } - and runRemappedKeys = (~recursionDepth, ~context, ~keys, bindings) => { + and runRemappedKeys = + (~leaderKey, ~recursionDepth, ~context, ~keys, bindings) => { let (bindings', effects') = keys |> List.fold_left( @@ -417,7 +463,13 @@ module Make = (Config: { let gesture = Down(KeyDownId.get(), key); let (bindings, effs) = acc; let (bindings', effects') = - handleKeyCore(~recursionDepth, ~context, gesture, bindings); + handleKeyCore( + ~leaderKey, + ~recursionDepth, + ~context, + gesture, + bindings, + ); (bindings', effects' @ effs); }, @@ -426,13 +478,14 @@ module Make = (Config: { (bindings', List.rev(effects')); } - and flush = (~recursionDepth, ~context, initialBindings) => { + and flush = (~leaderKey, ~recursionDepth, ~context, initialBindings) => { let rec loop = (~revEffects: list(effect), bindings) => if (bindings.revKeys == []) { (bindings, revEffects); } else { let candidateBindings = applyKeysToBindings( + ~leaderKey, ~context, bindings.revKeys |> List.rev, bindings.bindings, @@ -451,10 +504,16 @@ module Make = (Config: { | Remap(keys) => // Use up keys for the bindings let rewindBindings = - useUpKeysForBinding(~context, ~bindingId=binding.id, bindings); + useUpKeysForBinding( + ~leaderKey, + ~context, + ~bindingId=binding.id, + bindings, + ); // Run all the new keys runRemappedKeys( + ~leaderKey, ~recursionDepth=recursionDepth + 1, ~context, ~keys, @@ -464,7 +523,12 @@ module Make = (Config: { let revEffects = [Execute(command), ...revEffects]; // Use up keys related to this binding let rewindBindings = - useUpKeysForBinding(~context, ~bindingId=binding.id, bindings); + useUpKeysForBinding( + ~leaderKey, + ~context, + ~bindingId=binding.id, + bindings, + ); // And then anything else past it - treat as unhandled popAll(~revEffects, {...rewindBindings, suppressText: true}); } @@ -484,15 +548,21 @@ module Make = (Config: { }; }; - let keyDown = (~context, ~key, bindings) => { + let keyDown = (~leaderKey=None, ~context, ~key, bindings) => { let id = KeyDownId.get(); + let pressedScancodes = + key + |> KeyPress.toPhysicalKey + |> Option.map((key: PhysicalKey.t) => { + IntSet.add(key.scancode, bindings.pressedScancodes) + }) + |> Option.value(~default=bindings.pressedScancodes); + handleKeyCore( + ~leaderKey, ~context, Down(id, key), - { - ...bindings, - pressedScancodes: IntSet.add(key.scancode, bindings.pressedScancodes), - }, + {...bindings, pressedScancodes}, ); }; @@ -514,10 +584,12 @@ module Make = (Config: { }; }; - let getEffectsForReleaseBindings = (~context, bindings) => { + let getEffectsForReleaseBindings = (~leaderKey, ~context, bindings) => { let releaseBindings = bindings.bindings - |> List.filter_map(applyKeyToBinding(~context, AllKeysReleased)); + |> List.filter_map( + applyKeyToBinding(~leaderKey, ~context, AllKeysReleased), + ); let rec loop = bindings => switch (bindings) { @@ -531,21 +603,28 @@ module Make = (Config: { loop(releaseBindings); }; - let keyUp = (~context, ~key, bindings) => { + let keyUp = (~leaderKey=None, ~context, ~key, bindings) => { let pressedScancodes = - IntSet.remove(KeyPress.(key.scancode), bindings.pressedScancodes); + key + |> KeyPress.toPhysicalKey + |> Option.map((key: PhysicalKey.t) => { + IntSet.remove(key.scancode, bindings.pressedScancodes) + }) + |> Option.value(~default=bindings.pressedScancodes); + let bindings = {...bindings, suppressText: false, pressedScancodes}; // If everything has been released, fire an [AllKeysReleased] event, // in case anything is listening for it. let initialEffects = if (IntSet.is_empty(pressedScancodes)) { - getEffectsForReleaseBindings(~context, bindings); + getEffectsForReleaseBindings(~leaderKey, ~context, bindings); } else { []; }; - let (bindings, effects) = handleKeyCore(~context, Up(key), bindings); + let (bindings, effects) = + handleKeyCore(~leaderKey, ~context, Up(key), bindings); (bindings, effects @ initialEffects); }; diff --git a/src/editor-input/EditorInput.rei b/src/editor-input/EditorInput.rei index 136212ac17..cdccfa1089 100644 --- a/src/editor-input/EditorInput.rei +++ b/src/editor-input/EditorInput.rei @@ -45,18 +45,44 @@ module Modifiers: { let equals: (t, t) => bool; }; -module KeyPress: { +module PhysicalKey: { [@deriving show] type t = { scancode: int, keycode: int, modifiers: Modifiers.t, }; +}; + +module SpecialKey: { + [@deriving show] + type t = + // Leader key defined by 'vim.leader' or `let mapleader = ""` in VimL + | Leader + // Special key used by VimL plugins + // No physical key associated with it, but useful for scoping remappings. + | Plug; + // TODO; + // | SNR; +}; + +module KeyPress: { + [@deriving show] + type t = + | PhysicalKey(PhysicalKey.t) + | SpecialKey(SpecialKey.t); let toString: // The name of the 'meta' key. Defaults to "Meta". (~meta: string=?, ~keyCodeToString: int => string, t) => string; + let physicalKey: + (~keycode: int, ~scancode: int, ~modifiers: Modifiers.t) => t; + + let specialKey: SpecialKey.t => t; + + let toPhysicalKey: t => option(PhysicalKey.t); + let parse: ( ~getKeycode: Key.t => option(int), @@ -106,9 +132,23 @@ module type Input = { // in remappings such that we hit the max limit. | RemapRecursionLimitHit; - let keyDown: (~context: context, ~key: KeyPress.t, t) => (t, list(effect)); + let keyDown: + ( + ~leaderKey: option(PhysicalKey.t)=?, + ~context: context, + ~key: KeyPress.t, + t + ) => + (t, list(effect)); let text: (~text: string, t) => (t, list(effect)); - let keyUp: (~context: context, ~key: KeyPress.t, t) => (t, list(effect)); + let keyUp: + ( + ~leaderKey: option(PhysicalKey.t)=?, + ~context: context, + ~key: KeyPress.t, + t + ) => + (t, list(effect)); let remove: (uniqueId, t) => t; diff --git a/src/editor-input/KeyPress.re b/src/editor-input/KeyPress.re index 4f10b05b53..bd15994162 100644 --- a/src/editor-input/KeyPress.re +++ b/src/editor-input/KeyPress.re @@ -1,8 +1,54 @@ [@deriving show] -type t = { - scancode: int, - keycode: int, - modifiers: Modifiers.t, +type t = + | PhysicalKey(PhysicalKey.t) + | SpecialKey(SpecialKey.t); + +let physicalKey = (~keycode, ~scancode, ~modifiers) => + PhysicalKey(PhysicalKey.{scancode, keycode, modifiers}); + +let specialKey = special => SpecialKey(special); + +let toPhysicalKey = + fun + | PhysicalKey(key) => Some(key) + | SpecialKey(_) => None; + +let equals = (keyA, keyB) => { + switch (keyA, keyB) { + | (SpecialKey(specialKeyA), SpecialKey(specialKeyB)) => + specialKeyA == specialKeyB + | (PhysicalKey(physicalKeyA), PhysicalKey(physicalKeyB)) => + physicalKeyA.keycode == physicalKeyB.keycode + && Modifiers.equals(physicalKeyA.modifiers, physicalKeyB.modifiers) + | (SpecialKey(_), PhysicalKey(_)) + | (PhysicalKey(_), SpecialKey(_)) => false + }; +}; + +let ofInternal = + ( + ~getKeycode, + ~getScancode, + ( + key: Matcher_internal.keyPress, + mods: list(Matcher_internal.modifier), + ), + ) => { + switch (key) { + | Matcher_internal.Special(special) => Ok(SpecialKey(special)) + | Matcher_internal.Physical(key) => + switch (getKeycode(key), getScancode(key)) { + | (Some(keycode), Some(scancode)) => + Ok( + PhysicalKey({ + modifiers: Matcher_internal.Helpers.internalModsToMods(mods), + scancode, + keycode, + }), + ) + | _ => Error("Unrecognized key: " ++ Key.toString(key)) + } + }; }; let parse = (~getKeycode, ~getScancode, str) => { @@ -19,21 +65,7 @@ let parse = (~getKeycode, ~getScancode, str) => { let flatMap = (f, r) => Result.bind(r, f); let finish = r => { - let f = ((key, mods)) => { - switch (getKeycode(key), getScancode(key)) { - | (Some(keycode), Some(scancode)) => - Ok({ - modifiers: Matcher_internal.Helpers.internalModsToMods(mods), - scancode, - keycode, - }) - | _ => Error("Unrecognized key: " ++ Key.toString(key)) - }; - }; - - let bindings = r |> List.map(f); - - bindings |> Base.Result.all; + r |> List.map(ofInternal(~getKeycode, ~getScancode)) |> Base.Result.all; }; str @@ -43,38 +75,47 @@ let parse = (~getKeycode, ~getScancode, str) => { |> flatMap(finish); }; -let toString = (~meta="Meta", ~keyCodeToString, {keycode, modifiers, _}) => { - let buffer = Buffer.create(16); - let separator = " + "; +let toString = (~meta="Meta", ~keyCodeToString, key) => { + switch (key) { + | SpecialKey(special) => + Printf.sprintf("Special(%s)", SpecialKey.show(special)) + | PhysicalKey({keycode, modifiers, _}) => + let buffer = Buffer.create(16); + let separator = " + "; - let keyString = keyCodeToString(keycode); + let keyString = keyCodeToString(keycode); - let onlyShiftPressed = - modifiers.shift && !modifiers.control && !modifiers.meta && !modifiers.alt; + let onlyShiftPressed = + modifiers.shift + && !modifiers.control + && !modifiers.meta + && !modifiers.alt; - let keyString = - String.length(keyString) == 1 && !onlyShiftPressed - ? String.lowercase_ascii(keyString) : keyString; + let keyString = + String.length(keyString) == 1 && !onlyShiftPressed + ? String.lowercase_ascii(keyString) : keyString; - if (modifiers.meta) { - Buffer.add_string(buffer, meta ++ separator); - }; + if (modifiers.meta) { + Buffer.add_string(buffer, meta ++ separator); + }; - if (modifiers.control) { - Buffer.add_string(buffer, "Ctrl" ++ separator); - }; + if (modifiers.control) { + Buffer.add_string(buffer, "Ctrl" ++ separator); + }; - if (modifiers.altGr) { - Buffer.add_string(buffer, "AltGr" ++ separator); - } else if (modifiers.alt) { - Buffer.add_string(buffer, "Alt" ++ separator); - }; + if (modifiers.altGr) { + Buffer.add_string(buffer, "AltGr" ++ separator); + } else if (modifiers.alt) { + Buffer.add_string(buffer, "Alt" ++ separator); + }; - if ((modifiers.meta || modifiers.control || modifiers.alt) && modifiers.shift) { - Buffer.add_string(buffer, "Shift" ++ separator); - }; + if ((modifiers.meta || modifiers.control || modifiers.alt) + && modifiers.shift) { + Buffer.add_string(buffer, "Shift" ++ separator); + }; - Buffer.add_string(buffer, keyString); + Buffer.add_string(buffer, keyString); - Buffer.contents(buffer); + Buffer.contents(buffer); + }; }; diff --git a/src/editor-input/Matcher.re b/src/editor-input/Matcher.re index d30dc0f5ae..a323444c55 100644 --- a/src/editor-input/Matcher.re +++ b/src/editor-input/Matcher.re @@ -18,25 +18,11 @@ let parse = (~getKeycode, ~getScancode, str) => { let flatMap = (f, r) => Result.bind(r, f); let finish = r => { - let f = ((key, mods)) => { - switch (getKeycode(key), getScancode(key)) { - | (Some(keycode), Some(scancode)) => - Ok( - KeyPress.{ - modifiers: Matcher_internal.Helpers.internalModsToMods(mods), - scancode, - keycode, - }, - ) - | _ => Error("Unrecognized key: " ++ Key.toString(key)) - }; - }; - switch (r) { | Matcher_internal.AllKeysReleased => Ok(AllKeysReleased) | Matcher_internal.Sequence(keys) => keys - |> List.map(f) + |> List.map(KeyPress.ofInternal(~getKeycode, ~getScancode)) |> Base.Result.all |> Result.map(keys => Sequence(keys)) }; diff --git a/src/editor-input/Matcher_internal.re b/src/editor-input/Matcher_internal.re index d2e502a17f..fafa59e05a 100644 --- a/src/editor-input/Matcher_internal.re +++ b/src/editor-input/Matcher_internal.re @@ -4,7 +4,11 @@ type modifier = | Alt | Meta; -type keyMatcher = (Key.t, list(modifier)); +type keyPress = + | Physical(Key.t) + | Special(SpecialKey.t); + +type keyMatcher = (keyPress, list(modifier)); type t = | Sequence(list(keyMatcher)) diff --git a/src/editor-input/Matcher_lexer.mll b/src/editor-input/Matcher_lexer.mll index af3cbd559c..6f93e5909c 100644 --- a/src/editor-input/Matcher_lexer.mll +++ b/src/editor-input/Matcher_lexer.mll @@ -40,47 +40,49 @@ rule token = parse raise (UnrecognizedModifier m) } | "" { ALLKEYSRELEASED } -| 'f' (['0'-'9'] as m) { BINDING ( Function(int_of_string (String.make 1 m)) ) } -| 'f' '1' (['0'-'9'] as m) { BINDING ( Function(int_of_string ("1" ^ (String.make 1 m))) ) } -| 'f' '1' (['0'-'9'] as m) { BINDING ( Function(int_of_string ("1" ^ (String.make 1 m))) ) } -| "esc" { BINDING (Escape) } -| "escape" { BINDING (Escape) } -| "up" { BINDING (Up) } -| "down" { BINDING (Down) } -| "left" { BINDING (Left) } -| "right" { BINDING (Right) } -| "tab" { BINDING (Tab) } -| "pageup" { BINDING (PageUp) } -| "pagedown" { BINDING (PageDown) } -| "cr" { BINDING (Return) } -| "enter" { BINDING (Return) } -| "space" { BINDING (Space) } -| "del" { BINDING (Delete) } -| "delete" { BINDING (Delete) } -| "pause" { BINDING (Pause) } -| "pausebreak" { BINDING (Pause) } -| "home" { BINDING (Home) } -| "end" { BINDING (End) } -| "del" { BINDING (Delete) } -| "delete" { BINDING (Delete) } -| "bs" { BINDING (Backspace) } -| "backspace" { BINDING (Backspace) } -| "capslock" { BINDING (CapsLock) } -| "insert" { BINDING (Insert) } -| "numpad_multiply" { BINDING(NumpadMultiply) } -| "numpad_add" { BINDING(NumpadAdd) } -| "numpad_separator" { BINDING(NumpadSeparator) } -| "numpad_subtract" { BINDING(NumpadSubtract) } -| "numpad_decimal" { BINDING(NumpadDecimal) } -| "numpad_divide" { BINDING(NumpadDivide) } +| 'f' (['0'-'9'] as m) { BINDING ( Physical(Function(int_of_string (String.make 1 m)) ) ) } +| 'f' '1' (['0'-'9'] as m) { BINDING ( Physical(Function(int_of_string ("1" ^ (String.make 1 m))) ) ) } +| 'f' '1' (['0'-'9'] as m) { BINDING ( Physical (Function(int_of_string ("1" ^ (String.make 1 m))) ) ) } +| "esc" { BINDING (Physical(Escape)) } +| "escape" { BINDING (Physical(Escape)) } +| "up" { BINDING (Physical(Up)) } +| "down" { BINDING (Physical(Down)) } +| "left" { BINDING (Physical(Left)) } +| "right" { BINDING (Physical(Right)) } +| "tab" { BINDING (Physical(Tab)) } +| "pageup" { BINDING (Physical(PageUp)) } +| "pagedown" { BINDING (Physical(PageDown)) } +| "cr" { BINDING (Physical(Return)) } +| "enter" { BINDING (Physical(Return)) } +| "space" { BINDING (Physical(Space)) } +| "del" { BINDING (Physical(Delete)) } +| "delete" { BINDING (Physical(Delete)) } +| "pause" { BINDING (Physical(Pause)) } +| "pausebreak" { BINDING (Physical(Pause)) } +| "home" { BINDING (Physical(Home)) } +| "end" { BINDING (Physical(End)) } +| "del" { BINDING (Physical(Delete)) } +| "delete" { BINDING (Physical(Delete)) } +| "bs" { BINDING (Physical(Backspace)) } +| "backspace" { BINDING (Physical(Backspace)) } +| "capslock" { BINDING (Physical(CapsLock)) } +| "insert" { BINDING (Physical(Insert)) } +| "numpad_multiply" { BINDING(Physical(NumpadMultiply)) } +| "numpad_add" { BINDING(Physical(NumpadAdd)) } +| "numpad_separator" { BINDING(Physical(NumpadSeparator)) } +| "numpad_subtract" { BINDING(Physical(NumpadSubtract)) } +| "numpad_decimal" { BINDING(Physical(NumpadDecimal)) } +| "numpad_divide" { BINDING(Physical(NumpadDivide)) } | "numpad" { numpad_digit lexbuf } +| "leader" { BINDING(Special(Leader)) } +| "plug" { BINDING(Special(Plug)) } | white { token lexbuf } | binding as i - { BINDING (Character (i)) } + { BINDING (Physical(Character (i))) } | '<' { LT } | '>' { GT } | eof { EOF } | _ { raise Error } and numpad_digit = parse -| ['0'-'9'] as digit { BINDING ( NumpadDigit(int_of_string (String.make 1 digit)) ) } +| ['0'-'9'] as digit { BINDING (Physical( NumpadDigit(int_of_string (String.make 1 digit))) ) } diff --git a/src/editor-input/Matcher_parser.mly b/src/editor-input/Matcher_parser.mly index a5d6bf5d8e..79ac9be5d1 100644 --- a/src/editor-input/Matcher_parser.mly +++ b/src/editor-input/Matcher_parser.mly @@ -1,5 +1,5 @@ %token MODIFIER -%token BINDING +%token BINDING %token ALLKEYSRELEASED %token LT GT %token EOF diff --git a/src/editor-input/PhysicalKey.re b/src/editor-input/PhysicalKey.re new file mode 100644 index 0000000000..5f6032e946 --- /dev/null +++ b/src/editor-input/PhysicalKey.re @@ -0,0 +1,6 @@ +[@deriving show] +type t = { + scancode: int, + keycode: int, + modifiers: Modifiers.t, +}; diff --git a/src/editor-input/SpecialKey.re b/src/editor-input/SpecialKey.re new file mode 100644 index 0000000000..a69629f6c5 --- /dev/null +++ b/src/editor-input/SpecialKey.re @@ -0,0 +1,6 @@ +[@deriving show] +type t = + | Leader + | Plug; +// TODO: +// | SNR; diff --git a/test/Input/KeybindingsTests.re b/test/Input/KeybindingsTests.re index 8698391f54..81215251c9 100644 --- a/test/Input/KeybindingsTests.re +++ b/test/Input/KeybindingsTests.re @@ -48,12 +48,11 @@ bindings: [ |} |> Yojson.Safe.from_string; -let getKeyFromSDL: string => EditorInput.KeyPress.t = - key => { - let scancode = Sdl2.Scancode.ofName(key); - let keycode = Sdl2.Keycode.ofName(key); - {keycode, scancode, modifiers: EditorInput.Modifiers.none}; - }; +let getKeyFromSDL = (~modifiers=EditorInput.Modifiers.none, key: string) => { + let scancode = Sdl2.Scancode.ofName(key); + let keycode = Sdl2.Keycode.ofName(key); + EditorInput.KeyPress.physicalKey(~keycode, ~scancode, ~modifiers); +}; let contextWithEditorTextFocus = WhenExpr.ContextKeys.( @@ -115,73 +114,98 @@ describe("Keybindings", ({describe, _}) => { expect.int(bindingCount(result)).toBe(1); expect.int(errorCount(result)).toBe(0); }); - // TODO: Bring back these tests - // test("regression test: #1152 (legacy expression)", ({expect, _}) => { - // let result = of_yojson_with_errors(regressionTest1152); - // - // expect.bool(isOk(result)).toBe(true); - // expect.int(errorCount(result)).toBe(0); - // - // result - // |> Utility.ResultEx.tapError(err => failwith(err)) - // |> Result.iter(((bindings, _)) => { - // let (_bindings, effects) = - // keyDown( - // ~context=contextWithEditorTextFocus, - // ~key=getKeyFromSDL("F2"), - // bindings, - // ); - // - // expect.equal(effects, [Execute("explorer.toggle")]); - // }); - // }); - // test("regression test: #1160 (legacy binding)", ({expect, _}) => { - // let result = of_yojson_with_errors(regressionTest1160); - // expect.bool(isOk(result)).toBe(true); - // expect.int(bindingCount(result)).toBe(4); - // expect.int(errorCount(result)).toBe(0); - // - // let validateKeyResultsInCommand = ((key, modifiers, cmd)) => { - // result - // |> Result.iter(((bindings, _)) => { - // let key = {...getKeyFromSDL(key), modifiers}; - // let (_bindings, effects) = - // keyDown(~context=contextWithEditorTextFocus, ~key, bindings); - // expect.equal(effects, [Execute(cmd)]); - // }); - // }; - // - // let modifier = (~control, ~shift, ~meta) => { - // ...EditorInput.Modifiers.none, - // control, - // shift, - // meta, - // }; - // - // let cases = [ - // ( - // "p", - // modifier(~control=true, ~shift=false, ~meta=false), - // "workbench.action.quickOpen", - // ), - // ( - // "p", - // modifier(~control=false, ~shift=false, ~meta=true), - // "workbench.action.quickOpen", - // ), - // ( - // "p", - // modifier(~control=true, ~shift=true, ~meta=false), - // "workbench.action.showCommands", - // ), - // ( - // "p", - // modifier(~control=false, ~shift=true, ~meta=true), - // "workbench.action.showCommands", - // ), - // ]; - // - // cases |> List.iter(validateKeyResultsInCommand); - // }); + test("regression test: #1152 (legacy expression)", ({expect, _}) => { + let result = of_yojson_with_errors(regressionTest1152); + + expect.bool(isOk(result)).toBe(true); + expect.int(errorCount(result)).toBe(0); + + result + |> Utility.ResultEx.tapError(err => failwith(err)) + |> Result.iter(((bindings, _)) => { + let input = + List.fold_left( + (acc, binding) => { + let (acc', _uniqueId) = + Feature_Input.addKeyBinding(~binding, acc); + acc'; + }, + Feature_Input.initial([]), + bindings, + ); + let (_bindings, effects) = + Feature_Input.keyDown( + ~config=Oni_Core.Config.emptyResolver, + ~context=contextWithEditorTextFocus, + ~key=getKeyFromSDL("F2"), + input, + ); + + expect.equal(effects, [Execute("explorer.toggle")]); + }); + }); + test("regression test: #1160 (legacy binding)", ({expect, _}) => { + let result = of_yojson_with_errors(regressionTest1160); + expect.bool(isOk(result)).toBe(true); + expect.int(bindingCount(result)).toBe(4); + expect.int(errorCount(result)).toBe(0); + + let validateKeyResultsInCommand = ((key, modifiers, cmd)) => { + result + |> Result.iter(((bindings, _)) => { + let input = + List.fold_left( + (acc, binding) => { + let (acc', _uniqueId) = + Feature_Input.addKeyBinding(~binding, acc); + acc'; + }, + Feature_Input.initial([]), + bindings, + ); + let key = getKeyFromSDL(~modifiers, key); + let (_bindings, effects) = + Feature_Input.keyDown( + ~config=Oni_Core.Config.emptyResolver, + ~context=contextWithEditorTextFocus, + ~key, + input, + ); + expect.equal(effects, [Execute(cmd)]); + }); + }; + + let modifier = (~control, ~shift, ~meta) => { + ...EditorInput.Modifiers.none, + control, + shift, + meta, + }; + + let cases = [ + ( + "p", + modifier(~control=true, ~shift=false, ~meta=false), + "workbench.action.quickOpen", + ), + ( + "p", + modifier(~control=false, ~shift=false, ~meta=true), + "workbench.action.quickOpen", + ), + ( + "p", + modifier(~control=true, ~shift=true, ~meta=false), + "workbench.action.showCommands", + ), + ( + "p", + modifier(~control=false, ~shift=true, ~meta=true), + "workbench.action.showCommands", + ), + ]; + + cases |> List.iter(validateKeyResultsInCommand); + }); }) }); diff --git a/test/Input/dune b/test/Input/dune index 3007c1834f..0003adfed6 100644 --- a/test/Input/dune +++ b/test/Input/dune @@ -3,4 +3,5 @@ (library_flags (-linkall -g)) (modules (:standard)) - (libraries Oni_Core_Test Oni2.input Oni2.core.whenExpr rely.lib)) + (libraries Oni_Core_Test Oni2.input Oni2.feature.input Oni2.core.whenExpr + rely.lib)) diff --git a/test/editor-input/InputTest.re b/test/editor-input/InputTest.re index 75c2efb946..0c557ccfd4 100644 --- a/test/editor-input/InputTest.re +++ b/test/editor-input/InputTest.re @@ -2,13 +2,16 @@ open TestFramework; open EditorInput; let aKeyNoModifiers = - KeyPress.{scancode: 101, keycode: 1, modifiers: Modifiers.none}; + KeyPress.physicalKey(~scancode=101, ~keycode=1, ~modifiers=Modifiers.none); let bKeyNoModifiers = - KeyPress.{scancode: 102, keycode: 2, modifiers: Modifiers.none}; + KeyPress.physicalKey(~scancode=102, ~keycode=2, ~modifiers=Modifiers.none); let cKeyNoModifiers = - KeyPress.{scancode: 103, keycode: 3, modifiers: Modifiers.none}; + KeyPress.physicalKey(~scancode=103, ~keycode=3, ~modifiers=Modifiers.none); + +let leaderKey = KeyPress.specialKey(SpecialKey.Leader); +let plugKey = KeyPress.specialKey(SpecialKey.Plug); module Input = EditorInput.Make({ @@ -17,6 +20,107 @@ module Input = }); describe("EditorInput", ({describe, _}) => { + describe("special keys", ({test, _}) => { + test("special keys can participate in remap", ({expect, _}) => { + // Add a a -> "commandA" mapping + let (bindings, _id) = + Input.empty + |> Input.addBinding( + Sequence([plugKey, aKeyNoModifiers]), + _ => true, + "commandA", + ); + + // Remap b -> a + let (bindings, _id) = + bindings + |> Input.addMapping( + Sequence([bKeyNoModifiers]), + _ => true, + [plugKey, aKeyNoModifiers], + ); + + // Pressing b should remap to a, which should execute "commandA" + let (_bindings, effects) = + Input.keyDown(~context=true, ~key=bKeyNoModifiers, bindings); + expect.equal(effects, [Execute("commandA")]); + }); + + test("leader key can participate in remap", ({expect, _}) => { + // Add a a -> "commandLeaderA" mapping + let (bindings, _id) = + Input.empty + |> Input.addBinding( + Sequence([leaderKey, aKeyNoModifiers]), + _ => true, + "commandLeaderA", + ); + + // Remap b -> + let (bindings, _id) = + bindings + |> Input.addMapping( + Sequence([bKeyNoModifiers]), + _ => true, + [leaderKey], + ); + + // Pressing b, as the leader key... + let (bindings, effects) = + Input.keyDown(~context=true, ~key=bKeyNoModifiers, bindings); + expect.equal(effects, []); + + // And then a to complete the binding + let (_bindings, effects) = + Input.keyDown(~context=true, ~key=aKeyNoModifiers, bindings); + expect.equal(effects, [Execute("commandLeaderA")]); + }); + + test("leader key defined as a", ({expect, _}) => { + let physicalKey = + PhysicalKey.{scancode: 101, keycode: 1, modifiers: Modifiers.none}; + let (bindings, _id) = + Input.empty + |> Input.addBinding(Sequence([leaderKey]), _ => true, "commandA"); + + let (_bindings, effects) = + Input.keyDown( + ~leaderKey=Some(physicalKey), + ~context=true, + ~key=aKeyNoModifiers, + bindings, + ); + expect.equal(effects, [Execute("commandA")]); + }); + }); + describe("leader key", ({test, _}) => { + test("no leader key defined", ({expect, _}) => { + let (bindings, _id) = + Input.empty + |> Input.addBinding(Sequence([leaderKey]), _ => true, "commandA"); + + let (_bindings, effects) = + Input.keyDown(~context=true, ~key=aKeyNoModifiers, bindings); + expect.equal(effects, [Unhandled(aKeyNoModifiers)]); + }); + + test("leader key defined as a", ({expect, _}) => { + let physicalKey = + PhysicalKey.{scancode: 101, keycode: 1, modifiers: Modifiers.none}; + let (bindings, _id) = + Input.empty + |> Input.addBinding(Sequence([leaderKey]), _ => true, "commandA"); + + let (_bindings, effects) = + Input.keyDown( + ~leaderKey=Some(physicalKey), + ~context=true, + ~key=aKeyNoModifiers, + bindings, + ); + expect.equal(effects, [Execute("commandA")]); + }); + }); describe("allKeysReleased", ({test, _}) => { test("basic release case", ({expect, _}) => { let (bindings, _id) = diff --git a/test/editor-input/MatcherTest.re b/test/editor-input/MatcherTest.re index 21c3127120..7f51543875 100644 --- a/test/editor-input/MatcherTest.re +++ b/test/editor-input/MatcherTest.re @@ -57,7 +57,9 @@ let modifiersControl = {...Modifiers.none, control: true}; let modifiersShift = {...Modifiers.none, shift: true}; let keyPress = (~modifiers=Modifiers.none, code) => - KeyPress.{keycode: code, scancode: code, modifiers}; + KeyPress.physicalKey(~keycode=code, ~scancode=code, ~modifiers); + +let specialKey = KeyPress.specialKey; describe("Matcher", ({describe, _}) => { describe("parser", ({test, _}) => { @@ -112,6 +114,10 @@ describe("Matcher", ({describe, _}) => { ("numpad_subtract", keyPress(148)), ("numpad_decimal", keyPress(149)), ("numpad_divide", keyPress(150)), + ("", specialKey(SpecialKey.Plug)), + ("", specialKey(SpecialKey.Plug)), + ("", specialKey(SpecialKey.Leader)), + ("", specialKey(SpecialKey.Leader)), ]; let runCase = case => {