Skip to content

Commit d5be7e7

Browse files
authored
Add scope type parser piece of custom command grammar (#2295)
Initial work towards #492; will be used to parse scope types in #2131 Exposes a function `parseScopeType` that can parse strings like `funk`, `curly` etc into their corresponding scope type payloads Here's a railroad: https://deploy-preview-2295--cursorless.netlify.app/custom-command-railroad ## Checklist - [ ] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [ ] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [ ] I have not broken the cheatsheet
1 parent 83ed3fe commit d5be7e7

File tree

18 files changed

+465
-174
lines changed

18 files changed

+465
-174
lines changed

.vscode/tasks.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
"label": "Generate grammar",
6767
"type": "npm",
6868
"script": "generate-grammar",
69-
"path": "packages/cursorless-vscode",
7069
"presentation": {
7170
"reveal": "silent"
7271
},

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"preinstall": "npx only-allow pnpm",
1919
"test-compile": "tsc --build",
2020
"test": "pnpm compile && pnpm lint && pnpm -F '!test-harness' test && pnpm -F test-harness test",
21+
"generate-grammar": "pnpm -r generate-grammar",
2122
"transform-recorded-tests": "./packages/common/scripts/my-ts-node.js packages/cursorless-engine/src/scripts/transformRecordedTests/index.ts",
2223
"watch": "pnpm run -w --parallel '/^watch:.*/'",
2324
"watch:esbuild": "pnpm run -r --parallel --if-present watch:esbuild",

packages/cursorless-engine/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
"compile:tsc": "tsc --build",
99
"compile:esbuild": "esbuild ./src/index.ts --sourcemap --format=esm --bundle --packages=external --outfile=./out/index.js",
1010
"compile": "pnpm compile:tsc && pnpm compile:esbuild",
11+
"generate-grammar:base": "nearleyc src/customCommandGrammar/grammar.ne",
12+
"ensure-grammar-up-to-date": "pnpm -s generate-grammar:base | diff -u src/customCommandGrammar/generated/grammar.ts -",
13+
"generate-grammar": "pnpm generate-grammar:base -o src/customCommandGrammar/generated/grammar.ts",
14+
"generate-railroad": "nearley-railroad src/customCommandGrammar/grammar.ne -o out/railroad.html",
15+
"test": "pnpm ensure-grammar-up-to-date",
1116
"watch:tsc": "pnpm compile:tsc --watch",
1217
"watch:esbuild": "pnpm compile:esbuild --watch",
1318
"watch": "pnpm run --filter @cursorless/cursorless-engine --parallel '/^watch:.*/'"
@@ -22,6 +27,8 @@
2227
"immutability-helper": "^3.1.1",
2328
"itertools": "^2.2.5",
2429
"lodash": "^4.17.21",
30+
"moo": "0.5.2",
31+
"nearley": "2.20.1",
2532
"node-html-parser": "^6.1.12",
2633
"sbd": "^1.0.19",
2734
"uuid": "^9.0.1",
@@ -32,6 +39,8 @@
3239
"@types/js-yaml": "^4.0.9",
3340
"@types/lodash": "4.17.0",
3441
"@types/mocha": "^10.0.6",
42+
"@types/moo": "0.5.9",
43+
"@types/nearley": "2.11.5",
3544
"@types/sbd": "^1.0.5",
3645
"@types/sinon": "^17.0.3",
3746
"@types/uuid": "^9.0.8",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Generated automatically by nearley, version 2.20.1
2+
// http://github.com/Hardmath123/nearley
3+
// Bypasses TS6133. Allow declared but unused functions.
4+
// @ts-ignore
5+
function id(d: any[]): any { return d[0]; }
6+
declare var simpleScopeTypeType: any;
7+
declare var pairedDelimiter: any;
8+
9+
import { capture } from "../../util/grammarHelpers";
10+
import { lexer } from "../lexer";
11+
12+
interface NearleyToken {
13+
value: any;
14+
[key: string]: any;
15+
};
16+
17+
interface NearleyLexer {
18+
reset: (chunk: any, info: any) => void;
19+
next: () => NearleyToken | undefined;
20+
save: () => any;
21+
formatError: (token: any, message: string) => string;
22+
has: (tokenType: any) => boolean;
23+
};
24+
25+
interface NearleyRule {
26+
name: string;
27+
symbols: NearleySymbol[];
28+
postprocess?: (d: any[], loc?: number, reject?: {}) => any;
29+
};
30+
31+
type NearleySymbol = string | { literal: any } | { test: (token: any) => boolean };
32+
33+
interface Grammar {
34+
Lexer: NearleyLexer | undefined;
35+
ParserRules: NearleyRule[];
36+
ParserStart: string;
37+
};
38+
39+
const grammar: Grammar = {
40+
Lexer: lexer,
41+
ParserRules: [
42+
{"name": "main", "symbols": ["scopeType"]},
43+
{"name": "scopeType", "symbols": [(lexer.has("simpleScopeTypeType") ? {type: "simpleScopeTypeType"} : simpleScopeTypeType)], "postprocess": capture("type")},
44+
{"name": "scopeType", "symbols": [(lexer.has("pairedDelimiter") ? {type: "pairedDelimiter"} : pairedDelimiter)], "postprocess":
45+
([delimiter]) => ({ type: "surroundingPair", delimiter })
46+
}
47+
],
48+
ParserStart: "main",
49+
};
50+
51+
export default grammar;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@preprocessor typescript
2+
@{%
3+
import { capture } from "../../util/grammarHelpers";
4+
import { lexer } from "../lexer";
5+
%}
6+
@lexer lexer
7+
8+
main -> scopeType
9+
10+
# --------------------------- Scope types ---------------------------
11+
scopeType -> %simpleScopeTypeType {% capture("type") %}
12+
scopeType -> %pairedDelimiter {%
13+
([delimiter]) => ({ type: "surroundingPair", delimiter })
14+
%}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import assert from "assert";
2+
import { ScopeType } from "@cursorless/common";
3+
import { parseScopeType } from "./parseScopeType";
4+
5+
interface TestCase {
6+
input: string;
7+
expectedOutput: ScopeType;
8+
}
9+
10+
const testCases: TestCase[] = [
11+
{
12+
input: "funk",
13+
expectedOutput: {
14+
type: "namedFunction",
15+
},
16+
},
17+
{
18+
input: "curly",
19+
expectedOutput: {
20+
type: "surroundingPair",
21+
delimiter: "curlyBrackets",
22+
},
23+
},
24+
{
25+
input: "string",
26+
expectedOutput: {
27+
type: "surroundingPair",
28+
delimiter: "string",
29+
},
30+
},
31+
];
32+
33+
suite("custom grammar: scope types", () => {
34+
testCases.forEach(({ input, expectedOutput }) => {
35+
test(input, () => {
36+
assert.deepStrictEqual(parseScopeType(input), expectedOutput);
37+
});
38+
});
39+
});
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as assert from "assert";
2+
import { unitTestSetup } from "../test/unitTestSetup";
3+
import { lexer } from "./lexer";
4+
5+
interface Token {
6+
type: string;
7+
value: string;
8+
}
9+
10+
interface Fixture {
11+
input: string;
12+
expectedOutput: Token[];
13+
}
14+
15+
const fixtures: Fixture[] = [
16+
{
17+
input: "funk",
18+
expectedOutput: [
19+
{
20+
type: "simpleScopeTypeType",
21+
value: "namedFunction",
22+
},
23+
],
24+
},
25+
{
26+
input: "curly",
27+
expectedOutput: [
28+
{
29+
type: "pairedDelimiter",
30+
value: "curlyBrackets",
31+
},
32+
],
33+
},
34+
{
35+
input: "state name",
36+
expectedOutput: [
37+
{
38+
type: "simpleScopeTypeType",
39+
value: "statement",
40+
},
41+
{
42+
type: "ws",
43+
value: " ",
44+
},
45+
{
46+
type: "simpleScopeTypeType",
47+
value: "name",
48+
},
49+
],
50+
},
51+
{
52+
input: "funk name",
53+
expectedOutput: [
54+
{
55+
type: "simpleScopeTypeType",
56+
value: "functionName",
57+
},
58+
],
59+
},
60+
];
61+
62+
suite("custom grammar lexer", () => {
63+
unitTestSetup();
64+
65+
fixtures.forEach(({ input, expectedOutput }) => {
66+
test(input, () => {
67+
assert.deepStrictEqual(
68+
Array.from(lexer.reset(input)).map(({ type, value }) => ({
69+
type,
70+
value,
71+
})),
72+
expectedOutput,
73+
);
74+
});
75+
});
76+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { simpleScopeTypeTypes, surroundingPairNames } from "@cursorless/common";
2+
import moo from "moo";
3+
import { defaultSpokenFormMap } from "../spokenForms/defaultSpokenFormMap";
4+
5+
interface Token {
6+
type: string;
7+
value: string;
8+
}
9+
10+
const tokens: Record<string, Token> = {};
11+
12+
// FIXME: Remove the duplication below?
13+
14+
for (const simpleScopeTypeType of simpleScopeTypeTypes) {
15+
const { spokenForms } =
16+
defaultSpokenFormMap.simpleScopeTypeType[simpleScopeTypeType];
17+
for (const spokenForm of spokenForms) {
18+
tokens[spokenForm] = {
19+
type: "simpleScopeTypeType",
20+
value: simpleScopeTypeType,
21+
};
22+
}
23+
}
24+
25+
for (const pairedDelimiter of surroundingPairNames) {
26+
const { spokenForms } = defaultSpokenFormMap.pairedDelimiter[pairedDelimiter];
27+
for (const spokenForm of spokenForms) {
28+
tokens[spokenForm] = {
29+
type: "pairedDelimiter",
30+
value: pairedDelimiter,
31+
};
32+
}
33+
}
34+
35+
export const lexer = moo.compile({
36+
ws: /[ \t]+/,
37+
token: {
38+
match: Object.keys(tokens),
39+
type: (text) => tokens[text].type,
40+
value: (text) => tokens[text].value,
41+
},
42+
});
43+
44+
(lexer as any).transform = (token: { value: string }) => token.value;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Parser, Grammar } from "nearley";
2+
import grammar from "./generated/grammar";
3+
import { ScopeType } from "@cursorless/common";
4+
5+
function getScopeTypeParser(): Parser {
6+
return new Parser(
7+
// eslint-disable-next-line @typescript-eslint/naming-convention
8+
Grammar.fromCompiled({ ...grammar, ParserStart: "scopeType" }),
9+
);
10+
}
11+
12+
/**
13+
* Given a textual representation of a scope type, parse it into a scope type.
14+
*
15+
* @param input A textual representation of a scope type
16+
* @returns A parsed scope type
17+
*/
18+
export function parseScopeType(input: string): ScopeType {
19+
const parser = getScopeTypeParser();
20+
parser.feed(input);
21+
22+
if (parser.results.length !== 1) {
23+
throw new Error(
24+
`Expected exactly one result, got ${parser.results.length}`,
25+
);
26+
}
27+
28+
return parser.results[0] as ScopeType;
29+
}

packages/cursorless-engine/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export * from "./api/CursorlessEngineApi";
1010
export * from "./CommandRunner";
1111
export * from "./CommandHistory";
1212
export * from "./CommandHistoryAnalyzer";
13+
export * from "./util/grammarHelpers";

packages/cursorless-engine/src/spokenForms/defaultSpokenFormMap.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mapSpokenForms } from "./SpokenFormMap";
1+
import { SpokenFormMap, mapSpokenForms } from "./SpokenFormMap";
22
import { defaultSpokenFormMapCore } from "./defaultSpokenFormMapCore";
33
import { DefaultSpokenFormInfoMap } from "./defaultSpokenFormMap.types";
44

@@ -23,7 +23,7 @@ export const defaultSpokenFormInfoMap: DefaultSpokenFormInfoMap =
2323
* A spoken form map constructed from the default spoken forms. It is designed to
2424
* be used as a fallback when the Talon spoken form map is not available.
2525
*/
26-
export const defaultSpokenFormMap = mapSpokenForms(
26+
export const defaultSpokenFormMap: SpokenFormMap = mapSpokenForms(
2727
defaultSpokenFormInfoMap,
2828
({ defaultSpokenForms, isDisabledByDefault, isPrivate }) => ({
2929
spokenForms: isDisabledByDefault ? [] : defaultSpokenForms,

0 commit comments

Comments
 (0)