mirror of
https://github.com/vale981/tridactyl
synced 2025-03-05 09:31:41 -05:00
Update keyseq
- Add and build grammars - Add to content.ts for interactive use - Add tests - Change bracketExpr parser - Improve comments - Apply prettier
This commit is contained in:
parent
e107fa5eb1
commit
35bcde6627
7 changed files with 701 additions and 86 deletions
|
@ -12,6 +12,7 @@ scripts/excmds_macros.py
|
|||
scripts/newtab.md.sh
|
||||
scripts/make_tutorial.sh
|
||||
scripts/make_docs.sh &
|
||||
nearleyc src/grammars/bracketexpr.ne > src/grammars/bracketexpr.ts
|
||||
|
||||
(webpack --display errors-only && scripts/git_version.sh)&
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import * as msgsafe from "./msgsafe"
|
|||
import state from "./state"
|
||||
import * as webext from "./lib/webext"
|
||||
import Mark from "mark.js"
|
||||
import * as keyseq from "./keyseq"
|
||||
;(window as any).tri = Object.assign(Object.create(null), {
|
||||
browserBg: webext.browserBg,
|
||||
commandline_content,
|
||||
|
@ -38,6 +39,7 @@ import Mark from "mark.js"
|
|||
itertools,
|
||||
keydown_content,
|
||||
Mark,
|
||||
keyseq,
|
||||
messaging,
|
||||
msgsafe,
|
||||
state,
|
||||
|
|
34
src/grammars/bracketexpr.ne
Normal file
34
src/grammars/bracketexpr.ne
Normal file
|
@ -0,0 +1,34 @@
|
|||
@preprocessor typescript
|
||||
|
||||
BracketExpr -> "<" Modifier ModKey ">" {% bexpr=>bexpr.slice(1,-1) %}
|
||||
| "<" Key ">" {% bexpr=>[{}].concat(bexpr.slice(1,-1)) %}
|
||||
Modifier -> [acmsACMS]:? [acmsACMS]:? [acmsACMS]:? [acmsACMS]:? "-" {%
|
||||
/** For each modifier present,
|
||||
add its long name as an attribute set to true to an object */
|
||||
(mods, _, reject) => {
|
||||
const longNames = new Map([
|
||||
["A", "altKey"],
|
||||
["C", "ctrlKey"],
|
||||
["M", "metaKey"],
|
||||
["S", "shiftKey"],
|
||||
])
|
||||
|
||||
let modifiersObj = {}
|
||||
for (let mod of mods) {
|
||||
if (mod === null || mod === "-") continue
|
||||
let longName = longNames.get(mod.toUpperCase())
|
||||
if (longName) {
|
||||
// Reject if the same name is used twice.
|
||||
if (longName in modifiersObj) return reject
|
||||
else modifiersObj[longName] = true
|
||||
}
|
||||
}
|
||||
return modifiersObj
|
||||
}
|
||||
%}
|
||||
# <>- permitted with modifiers, but not otherwise.
|
||||
ModKey -> "<" {% id %}
|
||||
| ">" {% id %}
|
||||
| "-" {% id %}
|
||||
| Key {% id %}
|
||||
Key -> [^\s<>-]:+ {% (key)=>key[0].join("") %}
|
300
src/keyseq.test.ts
Normal file
300
src/keyseq.test.ts
Normal file
|
@ -0,0 +1,300 @@
|
|||
import { testAll, testAllNoError, testAllObject } from "./test_utils"
|
||||
import * as ks from "./keyseq"
|
||||
import { mapstrToKeyseq as mks } from "./keyseq"
|
||||
|
||||
function mk(k, mod?: ks.KeyModifiers) {
|
||||
return new ks.MinimalKey(k, mod)
|
||||
}
|
||||
|
||||
{
|
||||
// {{{ parse and completions
|
||||
|
||||
const keymap = new Map([
|
||||
[[mk("u", { ctrlKey: true }), mk("j")], "scrollline 10"],
|
||||
[[mk("g"), mk("g")], "scrolltop"],
|
||||
[mks("<SA-Escape>"), "rarelyusedcommand"],
|
||||
])
|
||||
|
||||
// This one actually found a bug once!
|
||||
testAllObject(ks.parse, [
|
||||
[[[mk("g")], keymap], { keys: [mk("g")] }],
|
||||
[[[mk("g"), mk("g")], keymap], { exstr: "scrolltop" }],
|
||||
[
|
||||
[[mk("Escape", { shiftKey: true, altKey: true })], keymap],
|
||||
{ exstr: "rarelyusedcommand" },
|
||||
],
|
||||
])
|
||||
|
||||
testAllObject(ks.completions, [
|
||||
[[[mk("g")], keymap], new Map([[[mk("g"), mk("g")], "scrolltop"]])],
|
||||
[[mks("<C-u>j"), keymap], new Map([[mks("<C-u>j"), "scrollline 10"]])],
|
||||
])
|
||||
} // }}}
|
||||
|
||||
// {{{ mapstr -> keysequence
|
||||
|
||||
testAll(ks.bracketexprToKey, [
|
||||
["<C-a><CR>", [mk("a", { ctrlKey: true }), "<CR>"]],
|
||||
["<M-<>", [mk("<", { metaKey: true }), ""]],
|
||||
["<M-<>Foo", [mk("<", { metaKey: true }), "Foo"]],
|
||||
["<M-a>b", [mk("a", { metaKey: true }), "b"]],
|
||||
["<S-Escape>b", [mk("Escape", { shiftKey: true }), "b"]],
|
||||
["<Tab>b", [mk("Tab"), "b"]],
|
||||
["<>b", [mk("<"), ">b"]],
|
||||
["<tag >", [mk("<"), "tag >"]],
|
||||
])
|
||||
|
||||
testAllObject(ks.mapstrMapToKeyMap, [
|
||||
[
|
||||
new Map([["j", "scrollline 10"], ["gg", "scrolltop"]]),
|
||||
new Map([
|
||||
[[mk("j")], "scrollline 10"],
|
||||
[[mk("g"), mk("g")], "scrolltop"],
|
||||
]),
|
||||
],
|
||||
[
|
||||
new Map([["<C-u>j", "scrollline 10"], ["gg", "scrolltop"]]),
|
||||
new Map([
|
||||
[[mk("u", { ctrlKey: true }), mk("j")], "scrollline 10"],
|
||||
[[mk("g"), mk("g")], "scrolltop"],
|
||||
]),
|
||||
],
|
||||
])
|
||||
|
||||
testAllObject(ks.mapstrToKeyseq, [
|
||||
[
|
||||
"Some string",
|
||||
[
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "S",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "o",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "m",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "e",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: " ",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "s",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "t",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "r",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "i",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "n",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "g",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"hi<c-u>t<A-Enter>here",
|
||||
[
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "h",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "i",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: true,
|
||||
key: "u",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "t",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: true,
|
||||
ctrlKey: false,
|
||||
key: "Enter",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "h",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "e",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "r",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "e",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"wat's up <s-Escape>",
|
||||
[
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "w",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "a",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "t",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "'",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "s",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: " ",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "u",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "p",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: " ",
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
},
|
||||
{
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: "Escape",
|
||||
metaKey: false,
|
||||
shiftKey: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
["wat's up <s-Escape>", mks("wat's up <s-Escape>")],
|
||||
])
|
||||
|
||||
// Check order of modifiers doesn't matter
|
||||
// Check aliases
|
||||
testAllObject(mks, [
|
||||
["<SAC-cr>", mks("<ASC-return>")],
|
||||
["<ACM-lt>", mks("<CAM-<>")],
|
||||
])
|
||||
|
||||
// }}}
|
348
src/keyseq.ts
348
src/keyseq.ts
|
@ -1,18 +1,29 @@
|
|||
/** Key-sequence parser
|
||||
*
|
||||
* Given an iterable of keys and a mapping of keys to objects, return:
|
||||
*
|
||||
* - parser(keyseq, map):
|
||||
* the mapped object and a count
|
||||
* OR a suffix of keys[] that, if more keys are pressed, could map to an object.
|
||||
* - completions(keyseq, map):
|
||||
* an array of keysequences in map that keyseq is a valid prefix of.
|
||||
*
|
||||
* No key sequence in map may be a prefix of another key sequence in map. This
|
||||
* is a point of difference from Vim that removes any time-dependence in the
|
||||
* parser.
|
||||
*
|
||||
*/
|
||||
|
||||
If `map` is a Map of `MinimalKey[]` to objects (exstrs or callbacks)
|
||||
and `keyseq` is an array of [[MinimalKey]] compatible objects...
|
||||
|
||||
- `parse(keyseq, map)` returns the mapped object and a count OR a prefix
|
||||
of `MinimalKey[]` (possibly empty) that, if more keys are pressed, could
|
||||
map to an object.
|
||||
- `completions(keyseq, map)` returns the fragment of `map` that keyseq is
|
||||
a valid prefix of.
|
||||
- `mapstrToKeySeq` generates KeySequences for the rest of the API.
|
||||
|
||||
No key sequence in a `map` may be a prefix of another key sequence in that
|
||||
map. This is a point of difference from Vim that removes any time-dependence
|
||||
in the parser. Vimperator, Pentadactyl, saka-key, etc, all share this
|
||||
limitation.
|
||||
|
||||
*/
|
||||
|
||||
/** */
|
||||
import { izip } from "./itertools"
|
||||
import { Parser } from "./nearley_utils"
|
||||
import * as bracketexpr_grammar from "./grammars/bracketexpr"
|
||||
const bracketexpr_parser = new Parser(bracketexpr_grammar)
|
||||
|
||||
// {{{ General types
|
||||
|
||||
export type KeyModifiers = {
|
||||
altKey?: boolean
|
||||
|
@ -34,64 +45,276 @@ export class MinimalKey {
|
|||
}
|
||||
|
||||
/** Does this key match a given MinimalKey extending object? */
|
||||
match(keyevent) {
|
||||
public match(keyevent) {
|
||||
// 'in' doesn't include prototypes, so it's safe for this object.
|
||||
for (let attr in this) {
|
||||
if (this[attr] !== keyevent[attr]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public toMapstr() {
|
||||
let str = ""
|
||||
let needsBrackets = this.key.length > 1
|
||||
|
||||
// Format modifiers
|
||||
const modifiers = new Map([
|
||||
["A", "altKey"],
|
||||
["C", "ctrlKey"],
|
||||
["M", "metaKey"],
|
||||
["S", "shiftKey"],
|
||||
])
|
||||
for (const [letter, attr] of modifiers.entries()) {
|
||||
if (this[attr]) {
|
||||
str += letter
|
||||
needsBrackets = true
|
||||
}
|
||||
}
|
||||
if (str) {
|
||||
str += "-"
|
||||
}
|
||||
|
||||
// Format the rest
|
||||
str += this.key
|
||||
if (needsBrackets) {
|
||||
str = "<" + str + ">"
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
/** String starting with a bracket expr or a literal < to MinimalKey and remainder.
|
||||
import { MsgSafeKeyboardEvent } from "./msgsafe"
|
||||
|
||||
Bracket expressions generally start with a < contain no angle brackets or
|
||||
whitespace and end with a >. These special-cased expressions are also
|
||||
permitted: <{optional modifier}<> or <{optional modifier}>>
|
||||
type KeyEventLike = MinimalKey | MsgSafeKeyboardEvent | KeyboardEvent
|
||||
|
||||
// }}}
|
||||
|
||||
// {{{ parser and completions
|
||||
|
||||
type MapTarget = string | Function
|
||||
type KeyMap = Map<MinimalKey[], MapTarget>
|
||||
|
||||
export type ParserResponse = {
|
||||
keys?: KeyEventLike[]
|
||||
exstr?: string
|
||||
action?: Function
|
||||
}
|
||||
|
||||
export function parse(keyseq: KeyEventLike[], map: KeyMap): ParserResponse {
|
||||
// If keyseq is a prefix of a key in map, proceed, else try dropping keys
|
||||
// from keyseq until it is empty or is a prefix.
|
||||
let possibleMappings = completions(keyseq, map)
|
||||
while (possibleMappings.size === 0 && keyseq.length > 0) {
|
||||
keyseq.shift()
|
||||
possibleMappings = completions(keyseq, map)
|
||||
}
|
||||
|
||||
if (possibleMappings.size === 1) {
|
||||
const map = possibleMappings.keys().next().value
|
||||
if (map.length === keyseq.length) {
|
||||
const target = possibleMappings.values().next().value
|
||||
if (typeof target === "string") {
|
||||
return { exstr: target }
|
||||
} else {
|
||||
return { action: target }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// else
|
||||
return { keys: keyseq }
|
||||
}
|
||||
|
||||
/** True if seq1 is a prefix or equal to seq2 */
|
||||
function prefixes(seq1: KeyEventLike[], seq2: MinimalKey[]) {
|
||||
for (const [key1, key2] of izip(seq1, seq2)) {
|
||||
if (!key2.match(key1)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** returns the fragment of `map` that keyseq is a valid prefix of. */
|
||||
export function completions(keyseq: KeyEventLike[], map: KeyMap): KeyMap {
|
||||
const possibleMappings = new Map() as KeyMap
|
||||
for (const [ks, maptarget] of map.entries()) {
|
||||
if (prefixes(keyseq, ks)) {
|
||||
possibleMappings.set(ks, maptarget)
|
||||
}
|
||||
}
|
||||
return possibleMappings
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
||||
// {{{ mapStrToKeySeq stuff
|
||||
|
||||
/** Expand special key aliases that Vim provides to canonical values
|
||||
|
||||
Vim aliases are case insensitive.
|
||||
*/
|
||||
function expandAliases(key: string) {
|
||||
// Vim compatibility aliases
|
||||
const aliases = {
|
||||
cr: "Enter",
|
||||
return: "Enter",
|
||||
enter: "Enter",
|
||||
space: " ",
|
||||
bar: "|",
|
||||
del: "Delete",
|
||||
bs: "Backspace",
|
||||
lt: "<",
|
||||
}
|
||||
if (key.toLowerCase() in aliases) return aliases[key.toLowerCase()]
|
||||
else return key
|
||||
}
|
||||
|
||||
/** String starting with a `<` to MinimalKey and remainder.
|
||||
|
||||
Bracket expressions generally start with a `<` contain no angle brackets or
|
||||
whitespace and end with a `>.` These special-cased expressions are also
|
||||
permitted: `<{modifier}<>`, `<{modifier}>>`, and `<{modifier}->`.
|
||||
|
||||
If the string passed does not match this definition, it is treated as a
|
||||
literal <.
|
||||
literal `<.`
|
||||
|
||||
In sort of Backus Naur:
|
||||
Backus Naur approximation:
|
||||
|
||||
```
|
||||
- bracketexpr ::= '<' modifier? key '>'
|
||||
- modifier ::= 'm'|'s'|'a'|'c' '-'
|
||||
- key ::= '<'|'>'|/[^\s<>]+/
|
||||
- key ::= '<'|'>'|/[^\s<>-]+/
|
||||
```
|
||||
|
||||
See `src/grammars/bracketExpr.ne` for the canonical definition.
|
||||
|
||||
Modifiers are case insensitive.
|
||||
|
||||
The following case insensitive vim compatibility aliases are also defined:
|
||||
|
||||
cr: 'Enter',
|
||||
return: 'Enter',
|
||||
space: 'Enter',
|
||||
bar: '|',
|
||||
del: 'Delete',
|
||||
bs: 'Backspace',
|
||||
lt: '<',
|
||||
Some case insensitive vim compatibility aliases are also defined, see
|
||||
[[expandAliases]].
|
||||
|
||||
Compatibility breaks:
|
||||
|
||||
Shift + key must use the correct capitalisation of key: <S-j> != J, <S-J> == J.
|
||||
Shift + key must use the correct capitalisation of key:
|
||||
`<S-j> != J, <S-J> == J`.
|
||||
|
||||
In Vim <A-x> == <M-x> on most systems. Not so here: we can't detect
|
||||
In Vim `<A-x> == <M-x>` on most systems. Not so here: we can't detect
|
||||
platform, so just have to use what the browser gives us.
|
||||
|
||||
Vim has a predefined list of special key sequences, we don't: there are too
|
||||
many (and they're non-standard).[1].
|
||||
many (and they're non-standard) [1].
|
||||
|
||||
In Vim, you're still allowed to use <lt> within angled brackets:
|
||||
<M-<> == <M-lt> == <M-<lt>>
|
||||
In the future, we may just use the names as defined in keyNameList.h [2].
|
||||
|
||||
In Vim, you're still allowed to use `<lt>` within angled brackets:
|
||||
`<M-<> == <M-lt> == <M-<lt>>`
|
||||
Here only the first two will work.
|
||||
|
||||
Restrictions:
|
||||
|
||||
It is not possible to map to a keyevent that actually sends the key value
|
||||
of any of the aliases or to any multi-character sequence containing a space
|
||||
or >. It is unlikely that browsers will ever do either of those things.
|
||||
or `>.` It is unlikely that browsers will ever do either of those things.
|
||||
|
||||
[1]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
|
||||
[2]: https://searchfox.org/mozilla-central/source/dom/events/KeyNameList.h
|
||||
|
||||
*/
|
||||
export function bracketexprToKey(inputStr) {
|
||||
if (inputStr.indexOf(">") > 0) {
|
||||
try {
|
||||
const [
|
||||
[modifiers, key],
|
||||
remainder,
|
||||
] = bracketexpr_parser.feedUntilError(inputStr)
|
||||
return [new MinimalKey(expandAliases(key), modifiers), remainder]
|
||||
} catch (e) {
|
||||
// No valid bracketExpr
|
||||
return [new MinimalKey("<"), inputStr.slice(1)]
|
||||
}
|
||||
} else {
|
||||
// No end bracket to match == no valid bracketExpr
|
||||
return [new MinimalKey("<"), inputStr.slice(1)]
|
||||
}
|
||||
}
|
||||
|
||||
/** Generate KeySequences for the rest of the API.
|
||||
|
||||
A map expression is something like:
|
||||
|
||||
```
|
||||
j scrollline 10
|
||||
<C-f> scrollpage 0.5
|
||||
<C-d> scrollpage 0.5
|
||||
<C-/><C-n> mode normal
|
||||
```
|
||||
|
||||
A mapstr is the bit before the space.
|
||||
|
||||
mapstrToKeyseq turns a mapstr into a keySequence that looks like this:
|
||||
|
||||
```
|
||||
[MinimalKey {key: 'j'}]
|
||||
[MinimalKey {key: 'f', ctrlKey: true}]
|
||||
[MinimalKey {key: 'd', ctrlKey: true}]
|
||||
[MinimalKey {key: '/', ctrlKey: true}, MinimalKey {key: 'n', ctrlKey: true}]
|
||||
```
|
||||
|
||||
(All four {modifier}Key flags are actually provided on all MinimalKeys)
|
||||
*/
|
||||
export function mapstrToKeyseq(mapstr: string): MinimalKey[] {
|
||||
const keyseq: MinimalKey[] = []
|
||||
let key: MinimalKey
|
||||
while (mapstr.length) {
|
||||
if (mapstr[0] === "<") {
|
||||
;[key, mapstr] = bracketexprToKey(mapstr)
|
||||
keyseq.push(key)
|
||||
} else {
|
||||
keyseq.push(new MinimalKey(mapstr[0]))
|
||||
mapstr = mapstr.slice(1)
|
||||
}
|
||||
}
|
||||
return keyseq
|
||||
}
|
||||
|
||||
/** Convert a map of mapstrs (e.g. from config) to a KeyMap */
|
||||
export function mapstrMapToKeyMap(mapstrMap: Map<string, MapTarget>): KeyMap {
|
||||
const newKeyMap = new Map()
|
||||
for (const [mapstr, target] of mapstrMap.entries()) {
|
||||
newKeyMap.set(mapstrToKeyseq(mapstr), target)
|
||||
}
|
||||
return newKeyMap
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
||||
// {{{ Utility functions for dealing with KeyboardEvents
|
||||
|
||||
export function hasModifiers(keyEvent: KeyEventLike) {
|
||||
return (
|
||||
keyEvent.ctrlKey ||
|
||||
keyEvent.altKey ||
|
||||
keyEvent.metaKey ||
|
||||
keyEvent.shiftKey
|
||||
)
|
||||
}
|
||||
|
||||
/** shiftKey is true for any capital letter, most numbers, etc. Generally care about other modifiers. */
|
||||
export function hasNonShiftModifiers(keyEvent: KeyEventLike) {
|
||||
return keyEvent.ctrlKey || keyEvent.altKey || keyEvent.metaKey
|
||||
}
|
||||
|
||||
export function isSimpleKey(keyEvent: KeyEventLike) {
|
||||
return !(keyEvent.key.length > 1 || hasNonShiftModifiers(keyEvent))
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
||||
/* {{{ Deprecated
|
||||
|
||||
// OLD IMPLEMENTATION! See below for a simpler-looking one powered by nearley.
|
||||
// It's probably slower, but it supports multiple modifiers and will be easier
|
||||
// to understand and extend.
|
||||
export function bracketexprToKey(be: string): [MinimalKey, string] {
|
||||
function extractModifiers(be: string): [string, any] {
|
||||
const modifiers = new Map([
|
||||
|
@ -125,16 +348,6 @@ export function bracketexprToKey(be: string): [MinimalKey, string] {
|
|||
// General case:
|
||||
const beRegex = /<[^\s]+?>/u
|
||||
|
||||
// Vim compatibility aliases
|
||||
const aliases = {
|
||||
cr: "Enter",
|
||||
return: "Enter",
|
||||
space: "Enter",
|
||||
bar: "|",
|
||||
del: "Delete",
|
||||
bs: "Backspace",
|
||||
lt: "<",
|
||||
}
|
||||
if (beRegex.exec(be) !== null) {
|
||||
// Extract complete bracket expression and remove
|
||||
let bracketedBit = beRegex.exec(be)[0]
|
||||
|
@ -142,7 +355,7 @@ export function bracketexprToKey(be: string): [MinimalKey, string] {
|
|||
|
||||
// Extract key and alias if required
|
||||
let key = beRegex.exec(beWithoutModifiers)[0].slice(1, -1)
|
||||
if (key.toLowerCase() in aliases) key = aliases[key.toLowerCase()]
|
||||
key = expandAliases(key)
|
||||
|
||||
// Return constructed key and remainder of the string
|
||||
return [new MinimalKey(key, modifiers), be]
|
||||
|
@ -152,41 +365,4 @@ export function bracketexprToKey(be: string): [MinimalKey, string] {
|
|||
}
|
||||
}
|
||||
|
||||
export function mapstrToKeyseq(mapstr: string): MinimalKey[] {
|
||||
const keyseq: MinimalKey[] = []
|
||||
let key: MinimalKey
|
||||
while (mapstr.length) {
|
||||
if (mapstr[0] === "<") {
|
||||
;[key, mapstr] = bracketexprToKey(mapstr)
|
||||
keyseq.push(key)
|
||||
} else {
|
||||
keyseq.push(new MinimalKey(mapstr[0]))
|
||||
mapstr = mapstr.slice(1)
|
||||
}
|
||||
}
|
||||
return keyseq
|
||||
}
|
||||
|
||||
// {{{ Utility functions for dealing with KeyboardEvents
|
||||
|
||||
import { MsgSafeKeyboardEvent } from "./msgsafe"
|
||||
|
||||
type KeyEventLike = MinimalKey | MsgSafeKeyboardEvent | KeyboardEvent
|
||||
|
||||
export function hasModifiers(keyEvent: KeyEventLike) {
|
||||
return (
|
||||
keyEvent.ctrlKey ||
|
||||
keyEvent.altKey ||
|
||||
keyEvent.metaKey ||
|
||||
keyEvent.shiftKey
|
||||
)
|
||||
}
|
||||
|
||||
/** shiftKey is true for any capital letter, most numbers, etc. Generally care about other modifiers. */
|
||||
export function hasNonShiftModifiers(keyEvent: KeyEventLike) {
|
||||
return keyEvent.ctrlKey || keyEvent.altKey || keyEvent.metaKey
|
||||
}
|
||||
|
||||
export function isSimpleKey(keyEvent: KeyEventLike) {
|
||||
return !(keyEvent.key.length > 1 || hasNonShiftModifiers(keyEvent))
|
||||
}
|
||||
}}} */
|
||||
|
|
42
src/nearley_utils.ts
Normal file
42
src/nearley_utils.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import * as nearley from "nearley"
|
||||
|
||||
/** Friendlier interface around nearley parsers */
|
||||
export class Parser {
|
||||
private parser
|
||||
private initial_state
|
||||
/* public results */
|
||||
|
||||
constructor(grammar) {
|
||||
this.parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar))
|
||||
this.initial_state = this.parser.save()
|
||||
/* this.results = this.parser.results */
|
||||
}
|
||||
|
||||
feedUntilError(input) {
|
||||
let lastResult = undefined
|
||||
let consumedIndex = 0
|
||||
try {
|
||||
for (let val of input) {
|
||||
this.parser.feed(val)
|
||||
lastResult = this.parser.results[0]
|
||||
consumedIndex++
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
this.reset()
|
||||
if (lastResult === undefined) {
|
||||
throw "Error: no result!"
|
||||
} else {
|
||||
return [lastResult, input.slice(consumedIndex)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.parser.restore(this.initial_state)
|
||||
}
|
||||
|
||||
/* feed(input) { */
|
||||
/* return this.parser.feed(input) */
|
||||
/* } */
|
||||
}
|
60
src/test_utils.ts
Normal file
60
src/test_utils.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
// TODO: There's no reason to wrap the ans.
|
||||
function wrapPrimitives(testcases) {
|
||||
// Wrap all args and answers in arrays if they're not already
|
||||
return testcases.map(argOrAns => {
|
||||
if (argOrAns instanceof Array) return argOrAns
|
||||
else return [argOrAns]
|
||||
})
|
||||
}
|
||||
|
||||
/** Test each case in testcases.
|
||||
|
||||
Warning: if your function really accepts an array, that array must be
|
||||
double wrapped
|
||||
*/
|
||||
export function testAll(toTest, testcases) {
|
||||
testcases = testcases.map(wrapPrimitives)
|
||||
for (let [args, ans] of testcases) {
|
||||
test(`${toTest.name}(${args}) == ${JSON.stringify(ans)}`, () =>
|
||||
expect(
|
||||
(() => {
|
||||
let result = toTest(...args)
|
||||
if (result instanceof Array) return result
|
||||
else return [result]
|
||||
})(),
|
||||
).toEqual(expect.arrayContaining(ans)))
|
||||
}
|
||||
}
|
||||
|
||||
/** Test each case in testcases against an arbitrary expectAttr and eval'd Arg
|
||||
|
||||
For eval:
|
||||
- `ans` is an array containing the result of the function
|
||||
- `args` is an array of input arguments to the function
|
||||
- `toTest` is the function you gave as input
|
||||
*/
|
||||
export function testAllCustom(toTest, testcases, expectAttr, expectArg) {
|
||||
testcases = testcases.map(wrapPrimitives)
|
||||
for (let [args, ans] of testcases) {
|
||||
test(`${toTest.name}(${args}) == ${JSON.stringify(ans)}`, () =>
|
||||
expect(
|
||||
(() => {
|
||||
let result = toTest(...args)
|
||||
if (result instanceof Array) return result
|
||||
else return [result]
|
||||
})(),
|
||||
)[expectAttr](eval(expectArg)))
|
||||
}
|
||||
}
|
||||
|
||||
/** Call function with each testcase and check it doesn't throw */
|
||||
export function testAllNoError(toTest, testcases) {
|
||||
for (let args of wrapPrimitives(testcases)) {
|
||||
test(`try: ${toTest.name}(${args})`, () =>
|
||||
expect(() => toTest(...args)).not.toThrow())
|
||||
}
|
||||
}
|
||||
|
||||
export function testAllObject(toTest, testcases) {
|
||||
testAllCustom(toTest, testcases, "toMatchObject", "ans")
|
||||
}
|
Loading…
Add table
Reference in a new issue