Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
"label": "Generate grammar",
"type": "npm",
"script": "generate-grammar",
"path": "packages/cursorless-vscode",
"presentation": {
"reveal": "silent"
},
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"preinstall": "npx only-allow pnpm",
"test-compile": "tsc --build",
"test": "pnpm compile && pnpm lint && pnpm -F '!test-harness' test && pnpm -F test-harness test",
"generate-grammar": "pnpm -r generate-grammar",
"transform-recorded-tests": "./packages/common/scripts/my-ts-node.js packages/cursorless-engine/src/scripts/transformRecordedTests/index.ts",
"watch": "pnpm run -w --parallel '/^watch:.*/'",
"watch:esbuild": "pnpm run -r --parallel --if-present watch:esbuild",
Expand Down
9 changes: 9 additions & 0 deletions packages/cursorless-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
"compile:tsc": "tsc --build",
"compile:esbuild": "esbuild ./src/index.ts --sourcemap --format=esm --bundle --packages=external --outfile=./out/index.js",
"compile": "pnpm compile:tsc && pnpm compile:esbuild",
"generate-grammar:base": "nearleyc src/customCommandGrammar/grammar.ne",
"ensure-grammar-up-to-date": "pnpm -s generate-grammar:base | diff -u src/customCommandGrammar/generated/grammar.ts -",
"generate-grammar": "pnpm generate-grammar:base -o src/customCommandGrammar/generated/grammar.ts",
"generate-railroad": "nearley-railroad src/customCommandGrammar/grammar.ne -o out/railroad.html",
"test": "pnpm ensure-grammar-up-to-date",
"watch:tsc": "pnpm compile:tsc --watch",
"watch:esbuild": "pnpm compile:esbuild --watch",
"watch": "pnpm run --filter @cursorless/cursorless-engine --parallel '/^watch:.*/'"
Expand All @@ -22,6 +27,8 @@
"immutability-helper": "^3.1.1",
"itertools": "^2.2.5",
"lodash": "^4.17.21",
"moo": "0.5.2",
"nearley": "2.20.1",
"node-html-parser": "^6.1.12",
"sbd": "^1.0.19",
"uuid": "^9.0.1",
Expand All @@ -32,6 +39,8 @@
"@types/js-yaml": "^4.0.9",
"@types/lodash": "4.17.0",
"@types/mocha": "^10.0.6",
"@types/moo": "0.5.9",
"@types/nearley": "2.11.5",
"@types/sbd": "^1.0.5",
"@types/sinon": "^17.0.3",
"@types/uuid": "^9.0.8",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Generated automatically by nearley, version 2.20.1
// https://github.com/Hardmath123/nearley
// Bypasses TS6133. Allow declared but unused functions.
// @ts-ignore
function id(d: any[]): any { return d[0]; }
declare var simpleScopeTypeType: any;
declare var pairedDelimiter: any;

import { capture } from "../../util/grammarHelpers";
import { lexer } from "../lexer";

interface NearleyToken {
value: any;
[key: string]: any;
};

interface NearleyLexer {
reset: (chunk: any, info: any) => void;
next: () => NearleyToken | undefined;
save: () => any;
formatError: (token: any, message: string) => string;
has: (tokenType: any) => boolean;
};

interface NearleyRule {
name: string;
symbols: NearleySymbol[];
postprocess?: (d: any[], loc?: number, reject?: {}) => any;
};

type NearleySymbol = string | { literal: any } | { test: (token: any) => boolean };

interface Grammar {
Lexer: NearleyLexer | undefined;
ParserRules: NearleyRule[];
ParserStart: string;
};

const grammar: Grammar = {
Lexer: lexer,
ParserRules: [
{"name": "main", "symbols": ["scopeType"]},
{"name": "scopeType", "symbols": [(lexer.has("simpleScopeTypeType") ? {type: "simpleScopeTypeType"} : simpleScopeTypeType)], "postprocess": capture("type")},
{"name": "scopeType", "symbols": [(lexer.has("pairedDelimiter") ? {type: "pairedDelimiter"} : pairedDelimiter)], "postprocess":
([delimiter]) => ({ type: "surroundingPair", delimiter })
}
],
ParserStart: "main",
};

export default grammar;
14 changes: 14 additions & 0 deletions packages/cursorless-engine/src/customCommandGrammar/grammar.ne
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@preprocessor typescript
@{%
import { capture } from "../../util/grammarHelpers";
import { lexer } from "../lexer";
%}
@lexer lexer

main -> scopeType

# --------------------------- Scope types ---------------------------
scopeType -> %simpleScopeTypeType {% capture("type") %}
scopeType -> %pairedDelimiter {%
([delimiter]) => ({ type: "surroundingPair", delimiter })
%}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import assert from "assert";
import { ScopeType } from "@cursorless/common";
import { parseScopeType } from "./parseScopeType";

interface TestCase {
input: string;
expectedOutput: ScopeType;
}

const testCases: TestCase[] = [
{
input: "funk",
expectedOutput: {
type: "namedFunction",
},
},
{
input: "curly",
expectedOutput: {
type: "surroundingPair",
delimiter: "curlyBrackets",
},
},
{
input: "string",
expectedOutput: {
type: "surroundingPair",
delimiter: "string",
},
},
];

suite("custom grammar: scope types", () => {
testCases.forEach(({ input, expectedOutput }) => {
test(input, () => {
assert.deepStrictEqual(parseScopeType(input), expectedOutput);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as assert from "assert";
import { unitTestSetup } from "../test/unitTestSetup";
import { lexer } from "./lexer";

interface Token {
type: string;
value: string;
}

interface Fixture {
input: string;
expectedOutput: Token[];
}

const fixtures: Fixture[] = [
{
input: "funk",
expectedOutput: [
{
type: "simpleScopeTypeType",
value: "namedFunction",
},
],
},
{
input: "curly",
expectedOutput: [
{
type: "pairedDelimiter",
value: "curlyBrackets",
},
],
},
{
input: "state name",
expectedOutput: [
{
type: "simpleScopeTypeType",
value: "statement",
},
{
type: "ws",
value: " ",
},
{
type: "simpleScopeTypeType",
value: "name",
},
],
},
{
input: "funk name",
expectedOutput: [
{
type: "simpleScopeTypeType",
value: "functionName",
},
],
},
];

suite("custom grammar lexer", () => {
unitTestSetup();

fixtures.forEach(({ input, expectedOutput }) => {
test(input, () => {
assert.deepStrictEqual(
Array.from(lexer.reset(input)).map(({ type, value }) => ({
type,
value,
})),
expectedOutput,
);
});
});
});
44 changes: 44 additions & 0 deletions packages/cursorless-engine/src/customCommandGrammar/lexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { simpleScopeTypeTypes, surroundingPairNames } from "@cursorless/common";
import moo from "moo";
import { defaultSpokenFormMap } from "../spokenForms/defaultSpokenFormMap";

interface Token {
type: string;
value: string;
}

const tokens: Record<string, Token> = {};

// FIXME: Remove the duplication below?

for (const simpleScopeTypeType of simpleScopeTypeTypes) {
const { spokenForms } =
defaultSpokenFormMap.simpleScopeTypeType[simpleScopeTypeType];
for (const spokenForm of spokenForms) {
tokens[spokenForm] = {
type: "simpleScopeTypeType",
value: simpleScopeTypeType,
};
}
}

for (const pairedDelimiter of surroundingPairNames) {
const { spokenForms } = defaultSpokenFormMap.pairedDelimiter[pairedDelimiter];
for (const spokenForm of spokenForms) {
tokens[spokenForm] = {
type: "pairedDelimiter",
value: pairedDelimiter,
};
}
}

export const lexer = moo.compile({
ws: /[ \t]+/,
token: {
match: Object.keys(tokens),
type: (text) => tokens[text].type,
value: (text) => tokens[text].value,
},
});

(lexer as any).transform = (token: { value: string }) => token.value;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Parser, Grammar } from "nearley";
import grammar from "./generated/grammar";
import { ScopeType } from "@cursorless/common";

function getScopeTypeParser(): Parser {
return new Parser(
// eslint-disable-next-line @typescript-eslint/naming-convention
Grammar.fromCompiled({ ...grammar, ParserStart: "scopeType" }),
);
}

/**
* Given a textual representation of a scope type, parse it into a scope type.
*
* @param input A textual representation of a scope type
* @returns A parsed scope type
*/
export function parseScopeType(input: string): ScopeType {
const parser = getScopeTypeParser();
parser.feed(input);

if (parser.results.length !== 1) {
throw new Error(
`Expected exactly one result, got ${parser.results.length}`,
);
}

return parser.results[0] as ScopeType;
}
1 change: 1 addition & 0 deletions packages/cursorless-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from "./api/CursorlessEngineApi";
export * from "./CommandRunner";
export * from "./CommandHistory";
export * from "./CommandHistoryAnalyzer";
export * from "./util/grammarHelpers";
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mapSpokenForms } from "./SpokenFormMap";
import { SpokenFormMap, mapSpokenForms } from "./SpokenFormMap";
import { defaultSpokenFormMapCore } from "./defaultSpokenFormMapCore";
import { DefaultSpokenFormInfoMap } from "./defaultSpokenFormMap.types";

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