Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

keyboard: Use parser for key sequences #2051

Merged
merged 44 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1bc2b31
Use parser for key sequences
pokey Nov 24, 2023
5d4ae88
cleanup grammar
pokey Nov 24, 2023
f5953e3
More tweaks
pokey Nov 24, 2023
f5e3bf9
More tweaks#
pokey Nov 24, 2023
c086d77
More tweaks
pokey Nov 24, 2023
c7efc82
Again
pokey Nov 24, 2023
4c89400
More cleanup
pokey Nov 24, 2023
774cab8
whoops
pokey Nov 24, 2023
48ab4e9
Improve naming
pokey Nov 24, 2023
a44b2f9
Merge branch 'main' into pokey/keyboard-parser
pokey Dec 1, 2023
11b87f6
PR feedback
pokey Dec 1, 2023
9292dc4
tweak
pokey Dec 1, 2023
9e9ce0a
Add example config and use it for tests
pokey Dec 1, 2023
877b806
docs
pokey Dec 1, 2023
54c71d1
more PR feedback
pokey Dec 2, 2023
dd25e8d
cleanup
pokey Dec 2, 2023
75cd229
more
pokey Dec 2, 2023
8860978
Show keys pressed
pokey Dec 2, 2023
7539276
Use trie
pokey Dec 3, 2023
8b51ad2
Add railroad to website
pokey Dec 4, 2023
5b53394
tweak
pokey Dec 4, 2023
4cc6725
rename
pokey Dec 4, 2023
47cca49
improve WorkQueue
pokey Dec 4, 2023
84b1140
Use uniqWithHash
pokey Dec 4, 2023
9601b92
use `|`
pokey Dec 4, 2023
6e6698e
relative => nextPrev
pokey Dec 4, 2023
b6042c2
Use proper sentinel values
pokey Dec 4, 2023
0a829d5
docs
pokey Dec 4, 2023
dec3998
more cleanup
pokey Dec 4, 2023
a16dcc1
lexer => keyboardLexer
pokey Dec 4, 2023
0f0aa79
Remove defaultKeyMap as we have no current use case
pokey Dec 4, 2023
467469b
improve clarity
pokey Dec 4, 2023
4137211
Remove `getSingularSectionEntry`
pokey Dec 4, 2023
eec992e
Make config sections singular
pokey Dec 4, 2023
aa1aa0c
Fix dependency spec
pokey Dec 4, 2023
8be7fa8
Tweaks
pokey Dec 4, 2023
f8fff04
Cache layers
pokey Dec 4, 2023
845f3d8
Finish plural => singular
pokey Dec 4, 2023
7b5fadf
Update doc
pokey Dec 4, 2023
6b39ea1
One more test
pokey Dec 4, 2023
c96370f
Merge branch 'main' into pokey/keyboard-parser
pokey Dec 5, 2023
843d774
one more test for good measure
pokey Dec 5, 2023
eda5c6f
Rename
pokey Dec 5, 2023
bae5514
rename
pokey Dec 5, 2023
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
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ trim_trailing_whitespace = false
[Makefile]
indent_style = tab

[**/vendor/**]
[**/{vendor,generated}/**]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We keep the parser generator output in source control because it's quite short, and includes types so we can type check it, and then imports work without needing a preprocessing step. We have a step in CI that ensures it's up to date, and it auto-updates when you run extension as it's quite quick to generate

charset = unset
end_of_line = unset
indent_size = unset
Expand Down
7 changes: 6 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,10 @@
}
}
},
"ignorePatterns": ["**/vendor/**/*.ts", "**/vendor/**/*.js", "**/out/**"]
"ignorePatterns": [
"**/vendor/**/*.ts",
"**/vendor/**/*.js",
"**/out/**",
"**/generated/**"
]
}
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ repos:
# tests use strings with trailing white space to represent the final
# document contents. For example
# packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/languages/ruby/changeCondition.yml
exclude: ^packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/.*/[^/]*\.yml$
exclude: ^packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/.*/[^/]*\.yml$|/generated/|^patches/
pokey marked this conversation as resolved.
Show resolved Hide resolved
- repo: local
hooks:
- id: eslint
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
**/vendor
**/generated

# We use our own format for our recorded yaml tests to keep them compact
/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/**/*.yml
Expand Down
11 changes: 11 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"type": "npm",
"script": "esbuild",
"path": "packages/cursorless-vscode",
"dependsOn": ["Generate grammar"],
"presentation": {
"reveal": "silent"
},
Expand Down Expand Up @@ -61,6 +62,16 @@
},
"group": "build"
},
{
"label": "Generate grammar",
"type": "npm",
"script": "generate-grammar",
"path": "packages/cursorless-vscode",
"presentation": {
"reveal": "silent"
},
"group": "build"
},
{
"label": "Ensure test subset file exists",
"type": "npm",
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
},
"pnpm": {
"patchedDependencies": {
"@docusaurus/[email protected]": "patches/@[email protected]"
"@docusaurus/[email protected]": "patches/@[email protected]",
"@types/[email protected]": "patches/@[email protected]",
josharian marked this conversation as resolved.
Show resolved Hide resolved
"[email protected]": "patches/[email protected]"
},
"peerDependencyRules": {
"ignoreMissing": [
Expand Down
9 changes: 8 additions & 1 deletion packages/cursorless-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1091,7 +1091,7 @@
"funding": "https://github.com/sponsors/pokey",
"scripts": {
"build": "pnpm run esbuild:prod && pnpm -F cheatsheet-local build:prod && pnpm run populate-dist",
"build:dev": "pnpm run esbuild && pnpm -F cheatsheet-local build && pnpm run populate-dist",
"build:dev": "pnpm generate-grammar && pnpm run esbuild && pnpm -F cheatsheet-local build && pnpm run populate-dist",
"esbuild:base": "esbuild ./src/extension.ts --conditions=cursorless:bundler --bundle --outfile=dist/extension.cjs --external:vscode --format=cjs --platform=node",
"install-local": "bash ./scripts/install-local.sh",
"install-from-pr": "bash ./scripts/install-from-pr.sh",
Expand All @@ -1104,6 +1104,11 @@
"preprocess-svg-hats": "my-ts-node src/scripts/preprocessSvgHats.ts",
"hat-adjustment-add": "my-ts-node src/scripts/hatAdjustments/add.ts",
"hat-adjustment-average": "my-ts-node src/scripts/hatAdjustments/average.ts",
"generate-grammar:base": "nearleyc src/keyboard/grammar/grammar.ne",
"ensure-grammar-up-to-date": "pnpm -s generate-grammar:base | diff -u src/keyboard/grammar/generated/grammar.ts -",
"generate-grammar": "pnpm generate-grammar:base -o src/keyboard/grammar/generated/grammar.ts",
"generate-railroad": "nearley-railroad src/keyboard/grammar/grammar.ne -o out/railroad.html",
josharian marked this conversation as resolved.
Show resolved Hide resolved
"test": "pnpm ensure-grammar-up-to-date",
"compile": "tsc --build",
"watch": "tsc --build --watch",
"clean": "rm -rf ./out tsconfig.tsbuildinfo ./dist ./build"
Expand All @@ -1115,6 +1120,7 @@
"@types/js-yaml": "^4.0.2",
"@types/lodash": "4.14.181",
"@types/mocha": "^10.0.3",
"@types/nearley": "2.11.5",
"@types/node": "^18.18.2",
"@types/semver": "^7.3.9",
"@types/sinon": "^10.0.2",
Expand All @@ -1127,6 +1133,7 @@
"fs-extra": "11.1.0",
"glob": "^7.1.7",
"mocha": "^10.2.0",
"nearley": "2.20.1",
"sinon": "^11.1.1"
},
"dependencies": {
Expand Down
36 changes: 36 additions & 0 deletions packages/cursorless-vscode/src/keyboard/KeyboardActionType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ActionType, actionNames } from "@cursorless/common";

pokey marked this conversation as resolved.
Show resolved Hide resolved
const extraKeyboardActionNames = ["wrap"] as const;
const excludedKeyboardActionNames = [
"wrapWithPairedDelimiter",
"wrapWithSnippet",
] as const;
const complexKeyboardActionTypes = ["wrap"] as const;

type ExtraKeyboardActionType = (typeof extraKeyboardActionNames)[number];
type ExcludedKeyboardActionType = (typeof excludedKeyboardActionNames)[number];
type ComplexKeyboardActionType = (typeof complexKeyboardActionTypes)[number];
export type SimpleKeyboardActionType = Exclude<
KeyboardActionType,
ComplexKeyboardActionType
>;
export type KeyboardActionType =
| Exclude<ActionType, ExcludedKeyboardActionType>
| ExtraKeyboardActionType;

const keyboardActionNames: KeyboardActionType[] = [
...actionNames.filter(
(
actionName,
): actionName is Exclude<KeyboardActionType, ExtraKeyboardActionType> =>
!excludedKeyboardActionNames.includes(actionName as any),
),
...extraKeyboardActionNames,
];

export const simpleKeyboardActionNames = keyboardActionNames.filter(
(actionName): actionName is SimpleKeyboardActionType =>
!complexKeyboardActionTypes.includes(
actionName as ComplexKeyboardActionType,
),
);
112 changes: 112 additions & 0 deletions packages/cursorless-vscode/src/keyboard/KeyboardCommandHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { ScopeType } from "@cursorless/common";
import * as vscode from "vscode";
import { HatColor, HatShape } from "../ide/vscode/hatStyles.types";
import { SimpleKeyboardActionType } from "./KeyboardActionType";
import KeyboardCommandsTargeted from "./KeyboardCommandsTargeted";
import { ModalVscodeCommandDescriptor } from "./TokenTypes";

/**
* This class defines the keyboard commands available to our modal keyboard
* mode.
*
* Each method in this class corresponds to a top-level rule in the grammar. The
* method name is the name of the rule, and the method's argument is the rule's
* `arg` output.
*
* We try to keep all logic out of the grammar and use this class instead
* because:
*
* 1. The grammar has no type information, autocomplete, or autoformatting
* 2. If the grammar is defined by just a list of keys, as it is today, we can
* actually detect partial arguments as they're being constructed and display
* them to the user
*
* Thus, we use this class as a simple layer where we have strong types and can
* do some simple logic.
*/
export class KeyboardCommandHandler {
constructor(private targeted: KeyboardCommandsTargeted) {}

targetDecoratedMarkReplace({ decoratedMark }: DecoratedMarkArg) {
this.targeted.targetDecoratedMark(decoratedMark);
}

targetDecoratedMarkExtend({ decoratedMark }: DecoratedMarkArg) {
this.targeted.targetDecoratedMark({
...decoratedMark,
mode: "extend",
});
}

async vscodeCommand({
command: commandInfo,
}: {
command: ModalVscodeCommandDescriptor;
}) {
const {
pokey marked this conversation as resolved.
Show resolved Hide resolved
commandId,
args,
executeAtTarget,
keepChangedSelection,
exitCursorlessMode,
} =
typeof commandInfo === "string" || commandInfo instanceof String
? ({ commandId: commandInfo } as Exclude<
ModalVscodeCommandDescriptor,
string
>)
: commandInfo;
if (executeAtTarget) {
await this.targeted.performVscodeCommandOnTarget(commandId, {
args,
keepChangedSelection,
exitCursorlessMode,
});
return;
}
await vscode.commands.executeCommand(commandId, ...(args ?? []));
}

performSimpleActionOnTarget({
actionName,
}: {
actionName: SimpleKeyboardActionType;
}) {
this.targeted.performActionOnTarget(actionName);
}

targetScopeType(arg: { scopeType: ScopeType }) {
pokey marked this conversation as resolved.
Show resolved Hide resolved
this.targeted.targetScopeType(arg);
}

targetRelativeExclusiveScope({
offset,
length,
scopeType,
}: TargetRelativeExclusiveScopeArg) {
this.targeted.targetModifier({
type: "relativeScope",
offset: offset?.number ?? 1,
direction: offset?.direction ?? "forward",
length: length ?? 1,
scopeType,
});
}
}

interface DecoratedMarkArg {
decoratedMark: {
color?: HatColor;
shape?: HatShape;
};
}
interface TargetRelativeExclusiveScopeArg {
offset: Offset;
length: number | null;
scopeType: ScopeType;
}

interface Offset {
direction: "forward" | "backward" | null;
number: number | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { KeyboardCommandHandler } from "./KeyboardCommandHandler";

/**
* Maps from the name of a method in KeyboardCommandHandler to the type of its
* argument.
*/
export type KeyboardCommandArgTypes = {
pokey marked this conversation as resolved.
Show resolved Hide resolved
[K in keyof KeyboardCommandHandler]: KeyboardCommandHandler[K] extends (
arg: infer T,
) => void
? T
: never;
};

export type KeyboardCommandTypeMap = {
[K in keyof KeyboardCommandHandler]: {
type: K;
arg: KeyboardCommandArgTypes[K];
};
};

export type KeyboardCommand<T extends keyof KeyboardCommandHandler> = {
type: T;
arg: KeyboardCommandArgTypes[T];
};

// Ensure that all methods in KeyboardCommandHandler take an object as their
// first argument, and return void or Promise<void>. Note that the first check
// may look backwards, because the arg type is contravariant, so the 'extends'
// needs to be flipped.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function assertExtends<A extends B, B>() {}
assertExtends<
Record<keyof KeyboardCommandArgTypes, (arg?: object) => never>,
Pick<KeyboardCommandHandler, keyof KeyboardCommandArgTypes>
>;
assertExtends<
Pick<KeyboardCommandHandler, keyof KeyboardCommandArgTypes>,
Record<keyof KeyboardCommandArgTypes, (arg: never) => void | Promise<void>>
>;
Loading