Skip to content

Commit f67f131

Browse files
authored
fix(input/#382): Leader-key customization (#2672)
This adds support for specifying the `<leader>` key in `keybindings.json` as well as mappings coming from VimL (`map` family). The leader key can be specified via either: - `vim.leader` configuration setting in `configuration.json` - ie: `"vim.leader": "<space>"` - `:map <space> <leader>`"` via an Ex command. In addition, this also lays some groundwork for some other binding constructs we need... Like support for handling `<Plug>` and `<SNR>` as part of the VimL integration work. __TODO:__ - [x] Depends on #2668 - [x] Add `vim.leader` configuration setting - [x] Update documentation __Next steps:__ - Pull leader key from `let mapleader`, if available - this requires a bit of extra work in libvim: onivim/libvim#244
1 parent eea580c commit f67f131

26 files changed

+707
-263
lines changed

docs/docs/configuration/key-bindings.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,31 @@ Examples:
223223
{"key": "kk", "command": ":split", "when": "editorTextFocus"},
224224
{"key": "<C-D>", "command": ":d 2", "when": "insertMode"}
225225
```
226+
227+
### Leader Key
228+
229+
A leader key can be specified via the following configuration setting:
230+
231+
```
232+
{ "vim.leader": "<space>" }
233+
```
234+
> NOTE: This setting is in `configuration.json`, not `keybindings.json`
235+
236+
Alternatively, the leader key can be specified via an `Ex` command:
237+
```
238+
:nmap <space> <Leader>
239+
```
240+
241+
Once the leader key is defined, it may be used in both `keybindings.json` and via VimL map commands:
242+
243+
```
244+
[
245+
{ "key": "<Leader>p", "command": "workbench.action.quickOpen", "when": "editorTextFocus && normalMode" }
246+
]
247+
```
248+
249+
or, alternatively, in VimL:
250+
251+
```
252+
:nnoremap <Leader>p <C-S-P>
253+
```

docs/docs/configuration/settings.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ The configuration file, `configuration.json` is in the Oni2 directory, whose loc
9898

9999
- `vim.highlightedyank.duration` __(_int_ default: `300`)__ - The time, in milliseconds, the yank highlight is visible.
100100

101+
### Input
102+
103+
- `vim.leader` __(_string_)__ - Specify a custom [leader key](./key-bindings#leader-key).
104+
101105
### Layout
102106

103107
- `workbench.editor.showTabs` __(_bool_ default: `true`)__ - When `false`, hides the editor tabs.

integration_test/ExCommandKeybindingTest.re

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ runTest(
1717
(dispatch, wait, _) => {
1818
let input = key => {
1919
let keyPress =
20-
EditorInput.KeyPress.{
21-
scancode: Sdl2.Scancode.ofName(key),
22-
keycode: Sdl2.Keycode.ofName(key),
23-
modifiers: EditorInput.Modifiers.none,
24-
};
20+
EditorInput.KeyPress.physicalKey(
21+
~scancode=Sdl2.Scancode.ofName(key),
22+
~keycode=Sdl2.Keycode.ofName(key),
23+
~modifiers=EditorInput.Modifiers.none,
24+
);
2525
let time = Revery.Time.now();
2626

2727
dispatch(KeyDown(keyPress, time));

integration_test/ExCommandKeybindingWithArgsTest.re

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ runTest(
1818
(dispatch, wait, _) => {
1919
let input = key => {
2020
let keyPress =
21-
EditorInput.KeyPress.{
22-
scancode: Sdl2.Scancode.ofName(key),
23-
keycode: Sdl2.Keycode.ofName(key),
24-
modifiers: EditorInput.Modifiers.none,
25-
};
21+
EditorInput.KeyPress.physicalKey(
22+
~scancode=Sdl2.Scancode.ofName(key),
23+
~keycode=Sdl2.Keycode.ofName(key),
24+
~modifiers=EditorInput.Modifiers.none,
25+
);
2626
let time = Revery.Time.now();
2727

2828
dispatch(KeyDown(keyPress, time));

integration_test/KeySequenceJJTest.re

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ runTest(
2121
let keycode = Sdl2.Keycode.ofName(key);
2222
let modifiers = EditorInput.Modifiers.none;
2323

24-
let keyPress: EditorInput.KeyPress.t = {scancode, keycode, modifiers};
24+
let keyPress: EditorInput.KeyPress.t =
25+
EditorInput.KeyPress.physicalKey(~keycode, ~scancode, ~modifiers);
2526
let time = Revery.Time.now();
2627

2728
dispatch(Model.Actions.KeyDown(keyPress, time));

integration_test/VimSimpleRemapTest.re

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ runTest(~name="VimSimpleRemapTest", (dispatch, wait, runEffects) => {
1515
let keycode = Sdl2.Keycode.ofName(key);
1616
let modifiers = EditorInput.Modifiers.none;
1717

18-
let keyPress: EditorInput.KeyPress.t = {scancode, keycode, modifiers};
18+
let keyPress: EditorInput.KeyPress.t =
19+
EditorInput.KeyPress.physicalKey(~scancode, ~keycode, ~modifiers);
1920
let time = Revery.Time.now();
2021

2122
dispatch(Model.Actions.KeyDown(keyPress, time));

src/Core/Config.re

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ type key = Lookup.path;
1313
type resolver = (~vimSetting: option(string), key) => rawValue;
1414
type fileTypeResolver = (~fileType: string) => resolver;
1515

16+
let emptyResolver = (~vimSetting as _, _) => NotSet;
17+
1618
let key = Lookup.path;
1719
let keyAsString = Lookup.key;
1820

src/Core/Config.rei

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ type rawValue =
99
type resolver = (~vimSetting: option(string), key) => rawValue;
1010
type fileTypeResolver = (~fileType: string) => resolver;
1111

12+
let emptyResolver: resolver;
13+
1214
let key: string => key;
1315
let keyAsString: key => string;
1416

src/Feature/Input/Feature_Input.re

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,67 @@ open Oni_Core;
44
open Utility;
55
module Log = (val Log.withNamespace("Oni2.Feature.Input"));
66

7+
// CONFIGURATION
8+
module Configuration = {
9+
open Oni_Core;
10+
open Config.Schema;
11+
12+
module CustomDecoders = {
13+
let physicalKey =
14+
custom(
15+
~decode=
16+
Json.Decode.(
17+
string
18+
|> and_then(keyString =>
19+
if (keyString == "(none)") {
20+
succeed(None);
21+
} else {
22+
switch (
23+
EditorInput.KeyPress.parse(
24+
~getKeycode,
25+
~getScancode,
26+
keyString,
27+
)
28+
) {
29+
| Ok([]) =>
30+
fail("Unable to parse key sequence: " ++ keyString)
31+
| Ok([key]) =>
32+
switch (EditorInput.KeyPress.toPhysicalKey(key)) {
33+
| None => fail("Not a physical key: " ++ keyString)
34+
| Some(physicalKey) => succeed(Some(physicalKey))
35+
}
36+
| Ok(_keys) =>
37+
fail(
38+
"Unable to parse key sequence - too many keys: "
39+
++ keyString,
40+
)
41+
| Error(msg) =>
42+
fail("Unable to parse key sequence: " ++ msg)
43+
};
44+
}
45+
)
46+
),
47+
~encode=
48+
Json.Encode.(
49+
maybeKey => {
50+
switch (maybeKey) {
51+
| Some(key) =>
52+
EditorInput.KeyPress.toString(
53+
~keyCodeToString=Sdl2.Keycode.getName,
54+
EditorInput.KeyPress.PhysicalKey(key),
55+
)
56+
|> string
57+
| None => "(none)" |> string
58+
};
59+
}
60+
),
61+
);
62+
};
63+
64+
let leaderKey =
65+
setting("vim.leader", CustomDecoders.physicalKey, ~default=None);
66+
};
67+
768
// MSG
869

970
type outmsg =
@@ -73,6 +134,8 @@ type model = {
73134
inputStateMachine: InputStateMachine.t,
74135
};
75136

137+
type uniqueId = InputStateMachine.uniqueId;
138+
76139
let initial = keybindings => {
77140
open Schema;
78141
let inputStateMachine =
@@ -107,9 +170,10 @@ type effect =
107170
| Unhandled(EditorInput.KeyPress.t)
108171
| RemapRecursionLimitHit;
109172

110-
let keyDown = (~key, ~context, {inputStateMachine, _} as model) => {
173+
let keyDown = (~config, ~key, ~context, {inputStateMachine, _} as model) => {
174+
let leaderKey = Configuration.leaderKey.get(config);
111175
let (inputStateMachine', effects) =
112-
InputStateMachine.keyDown(~key, ~context, inputStateMachine);
176+
InputStateMachine.keyDown(~leaderKey, ~key, ~context, inputStateMachine);
113177
({...model, inputStateMachine: inputStateMachine'}, effects);
114178
};
115179

@@ -119,12 +183,31 @@ let text = (~text, {inputStateMachine, _} as model) => {
119183
({...model, inputStateMachine: inputStateMachine'}, effects);
120184
};
121185

122-
let keyUp = (~key, ~context, {inputStateMachine, _} as model) => {
186+
let keyUp = (~config, ~key, ~context, {inputStateMachine, _} as model) => {
187+
let leaderKey = Configuration.leaderKey.get(config);
123188
let (inputStateMachine', effects) =
124-
InputStateMachine.keyUp(~key, ~context, inputStateMachine);
189+
InputStateMachine.keyUp(~leaderKey, ~key, ~context, inputStateMachine);
125190
({...model, inputStateMachine: inputStateMachine'}, effects);
126191
};
127192

193+
let addKeyBinding = (~binding, {inputStateMachine, _} as model) => {
194+
open Schema;
195+
let (inputStateMachine', uniqueId) =
196+
InputStateMachine.addBinding(
197+
binding.matcher,
198+
binding.condition,
199+
binding.command,
200+
inputStateMachine,
201+
);
202+
({...model, inputStateMachine: inputStateMachine'}, uniqueId);
203+
};
204+
205+
let remove = (uniqueId, {inputStateMachine, _} as model) => {
206+
let inputStateMachine' =
207+
InputStateMachine.remove(uniqueId, inputStateMachine);
208+
{...model, inputStateMachine: inputStateMachine'};
209+
};
210+
128211
// UPDATE
129212
module Internal = {
130213
let vimMapModeToWhenExpr = mode => {
@@ -253,4 +336,5 @@ module Commands = {
253336

254337
module Contributions = {
255338
let commands = Commands.[showInputState];
339+
let configuration = Configuration.[leaderKey.spec];
256340
};

src/Feature/Input/Feature_Input.rei

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,36 @@ type effect =
4747
| RemapRecursionLimitHit;
4848

4949
let keyDown:
50-
(~key: KeyPress.t, ~context: WhenExpr.ContextKeys.t, model) =>
50+
(
51+
~config: Config.resolver,
52+
~key: KeyPress.t,
53+
~context: WhenExpr.ContextKeys.t,
54+
model
55+
) =>
5156
(model, list(effect));
5257

5358
let text: (~text: string, model) => (model, list(effect));
5459
let keyUp:
55-
(~key: KeyPress.t, ~context: WhenExpr.ContextKeys.t, model) =>
60+
(
61+
~config: Config.resolver,
62+
~key: KeyPress.t,
63+
~context: WhenExpr.ContextKeys.t,
64+
model
65+
) =>
5666
(model, list(effect));
5767

68+
type uniqueId;
69+
70+
let addKeyBinding:
71+
(~binding: Schema.resolvedKeybinding, model) => (model, uniqueId);
72+
73+
let remove: (uniqueId, model) => model;
74+
5875
// UPDATE
5976

6077
let update: (msg, model) => (model, outmsg);
6178

62-
module Contributions: {let commands: list(Command.t(msg));};
79+
module Contributions: {
80+
let commands: list(Command.t(msg));
81+
let configuration: list(Config.Schema.spec);
82+
};

0 commit comments

Comments
 (0)