Skip to content

Commit 1bc2b31

Browse files
committed
Use parser for key sequences
1 parent 08cd7a1 commit 1bc2b31

29 files changed

+1397
-219
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ trim_trailing_whitespace = false
2828
[Makefile]
2929
indent_style = tab
3030

31-
[**/vendor/**]
31+
[**/{vendor,generated}/**]
3232
charset = unset
3333
end_of_line = unset
3434
indent_size = unset

.eslintrc.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,10 @@
7878
}
7979
}
8080
},
81-
"ignorePatterns": ["**/vendor/**/*.ts", "**/vendor/**/*.js", "**/out/**"]
81+
"ignorePatterns": [
82+
"**/vendor/**/*.ts",
83+
"**/vendor/**/*.js",
84+
"**/out/**",
85+
"**/generated/**"
86+
]
8287
}

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ repos:
4040
# tests use strings with trailing white space to represent the final
4141
# document contents. For example
4242
# packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/ruby/changeCondition.yml
43-
exclude: ^packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/.*/[^/]*\.yml$
43+
exclude: ^packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/.*/[^/]*\.yml$|/generated/|^patches/
4444
- repo: local
4545
hooks:
4646
- id: eslint

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
**/vendor
2+
**/generated
23

34
# We use our own format for our recorded yaml tests to keep them compact
45
/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/**/*.yml

.vscode/tasks.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"type": "npm",
2727
"script": "esbuild",
2828
"path": "packages/cursorless-vscode",
29+
"dependsOn": ["Generate grammar"],
2930
"presentation": {
3031
"reveal": "silent"
3132
},
@@ -61,6 +62,16 @@
6162
},
6263
"group": "build"
6364
},
65+
{
66+
"label": "Generate grammar",
67+
"type": "npm",
68+
"script": "generate-grammar",
69+
"path": "packages/cursorless-vscode",
70+
"presentation": {
71+
"reveal": "silent"
72+
},
73+
"group": "build"
74+
},
6475
{
6576
"label": "Ensure test subset file exists",
6677
"type": "npm",

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@
4444
},
4545
"pnpm": {
4646
"patchedDependencies": {
47-
"@docusaurus/[email protected]": "patches/@[email protected]"
47+
"@docusaurus/[email protected]": "patches/@[email protected]",
48+
"@types/[email protected]": "patches/@[email protected]",
49+
4850
},
4951
"peerDependencyRules": {
5052
"ignoreMissing": [

packages/cursorless-vscode/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,7 @@
10911091
"funding": "https://github.com/sponsors/pokey",
10921092
"scripts": {
10931093
"build": "pnpm run esbuild:prod && pnpm -F cheatsheet-local build:prod && pnpm run populate-dist",
1094-
"build:dev": "pnpm run esbuild && pnpm -F cheatsheet-local build && pnpm run populate-dist",
1094+
"build:dev": "pnpm generate-grammar && pnpm run esbuild && pnpm -F cheatsheet-local build && pnpm run populate-dist",
10951095
"esbuild:base": "esbuild ./src/extension.ts --conditions=cursorless:bundler --bundle --outfile=dist/extension.cjs --external:vscode --format=cjs --platform=node",
10961096
"install-local": "bash ./scripts/install-local.sh",
10971097
"install-from-pr": "bash ./scripts/install-from-pr.sh",
@@ -1104,6 +1104,10 @@
11041104
"preprocess-svg-hats": "my-ts-node src/scripts/preprocessSvgHats.ts",
11051105
"hat-adjustment-add": "my-ts-node src/scripts/hatAdjustments/add.ts",
11061106
"hat-adjustment-average": "my-ts-node src/scripts/hatAdjustments/average.ts",
1107+
"generate-grammar:base": "nearleyc src/keyboard/grammar/grammar.ne",
1108+
"ensure-grammar-up-to-date": "pnpm -s generate-grammar:base | diff -u src/keyboard/grammar/generated/grammar.ts -",
1109+
"generate-grammar": "pnpm generate-grammar:base -o src/keyboard/grammar/generated/grammar.ts",
1110+
"test": "pnpm ensure-grammar-up-to-date",
11071111
"compile": "tsc --build",
11081112
"watch": "tsc --build --watch",
11091113
"clean": "rm -rf ./out tsconfig.tsbuildinfo ./dist ./build"
@@ -1115,6 +1119,7 @@
11151119
"@types/js-yaml": "^4.0.2",
11161120
"@types/lodash": "4.14.181",
11171121
"@types/mocha": "^10.0.3",
1122+
"@types/nearley": "2.11.5",
11181123
"@types/node": "^18.18.2",
11191124
"@types/semver": "^7.3.9",
11201125
"@types/sinon": "^10.0.2",
@@ -1127,6 +1132,7 @@
11271132
"fs-extra": "11.1.0",
11281133
"glob": "^7.1.7",
11291134
"mocha": "^10.2.0",
1135+
"nearley": "2.20.1",
11301136
"sinon": "^11.1.1"
11311137
},
11321138
"dependencies": {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { ScopeType, SimpleActionName } from "@cursorless/common";
2+
import * as vscode from "vscode";
3+
import KeyboardCommandsTargeted from "./KeyboardCommandsTargeted";
4+
import { ModalVscodeCommandDescriptor } from "./TokenTypes";
5+
import { HatColor, HatShape } from "../ide/vscode/hatStyles.types";
6+
7+
/**
8+
* This class defines the keyboard commands available to our modal keyboard
9+
* mode.
10+
*
11+
* Each method in this class corresponds to a top-level rule in the grammar. The
12+
* method name is the name of the rule, and the method's argument is the rule's
13+
* `arg` output.
14+
*
15+
* We try to keep all logic out of the grammar and use this class instead
16+
* because:
17+
*
18+
* 1. The grammar has no type information, autocomplete, or autoformatting
19+
* 2. If the grammar is defined by just a list of keys, as it is today, we can
20+
* actually detect partial arguments as they're being constructed and display
21+
* them to the user
22+
*
23+
* Thus, we use this class as a simple layer where we have strong types and can
24+
* do some simple logic.
25+
*/
26+
export class KeyboardCommandHandler {
27+
constructor(private targeted: KeyboardCommandsTargeted) {}
28+
29+
targetDecoratedMarkReplace({ decoratedMark }: DecoratedMarkArg) {
30+
this.targeted.targetDecoratedMark(decoratedMark);
31+
}
32+
33+
targetDecoratedMarkExtend({ decoratedMark }: DecoratedMarkArg) {
34+
this.targeted.targetDecoratedMark({
35+
...decoratedMark,
36+
mode: "extend",
37+
});
38+
}
39+
40+
async vscodeCommand({
41+
command: commandInfo,
42+
}: {
43+
command: ModalVscodeCommandDescriptor;
44+
}) {
45+
const {
46+
commandId,
47+
args,
48+
executeAtTarget,
49+
keepChangedSelection,
50+
exitCursorlessMode,
51+
} =
52+
typeof commandInfo === "string" || commandInfo instanceof String
53+
? ({ commandId: commandInfo } as Exclude<
54+
ModalVscodeCommandDescriptor,
55+
string
56+
>)
57+
: commandInfo;
58+
if (executeAtTarget) {
59+
await this.targeted.performVscodeCommandOnTarget(commandId, {
60+
args,
61+
keepChangedSelection,
62+
exitCursorlessMode,
63+
});
64+
return;
65+
}
66+
await vscode.commands.executeCommand(commandId, ...(args ?? []));
67+
}
68+
69+
performSimpleActionOnTarget({
70+
actionName,
71+
}: {
72+
actionName: SimpleActionName;
73+
}) {
74+
this.targeted.performActionOnTarget(actionName);
75+
}
76+
77+
targetScopeType(arg: { scopeType: ScopeType }) {
78+
this.targeted.targetScopeType(arg);
79+
}
80+
81+
targetRelativeScope({ offset, length, scopeType }: TargetRelativeScopeArg) {
82+
this.targeted.targetModifier({
83+
type: "relativeScope",
84+
offset: offset?.number ?? 1,
85+
direction: offset?.direction ?? "forward",
86+
length: length ?? 1,
87+
scopeType,
88+
});
89+
}
90+
}
91+
92+
interface DecoratedMarkArg {
93+
decoratedMark: {
94+
color?: HatColor;
95+
shape?: HatShape;
96+
};
97+
}
98+
interface TargetRelativeScopeArg {
99+
offset: Offset;
100+
length: number | null;
101+
scopeType: ScopeType;
102+
}
103+
104+
interface Offset {
105+
direction?: "forward" | "backward" | null;
106+
number?: number | null;
107+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { KeyboardCommandHandler } from "./KeyboardCommandHandler";
2+
3+
/**
4+
* Maps from the name of a method in KeyboardCommandHandler to the type of its
5+
* argument.
6+
*/
7+
export type KeyboardCommandArgTypes = {
8+
[K in keyof KeyboardCommandHandler]: KeyboardCommandHandler[K] extends (
9+
arg: infer T,
10+
) => void
11+
? T
12+
: never;
13+
};
14+
15+
export type KeyboardCommandTypeMap = {
16+
[K in keyof KeyboardCommandHandler]: {
17+
type: K;
18+
arg: KeyboardCommandArgTypes[K];
19+
};
20+
};
21+
22+
export type KeyboardCommand<T extends keyof KeyboardCommandHandler> = {
23+
type: T;
24+
arg: KeyboardCommandArgTypes[T];
25+
};
26+
27+
// Ensure that all methods in KeyboardCommandHandler take an object as their
28+
// first argument, and return void or Promise<void>. Note that the first check
29+
// may look backwards, because the arg type is contravariant, so the 'extends'
30+
// needs to be flipped.
31+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
32+
function assertExtends<A extends B, B>() {}
33+
assertExtends<
34+
Record<keyof KeyboardCommandArgTypes, (arg?: object) => never>,
35+
Pick<KeyboardCommandHandler, keyof KeyboardCommandArgTypes>
36+
>;
37+
assertExtends<
38+
Pick<KeyboardCommandHandler, keyof KeyboardCommandArgTypes>,
39+
Record<keyof KeyboardCommandArgTypes, (arg: never) => void | Promise<void>>
40+
>;

0 commit comments

Comments
 (0)