Skip to content

Commit

Permalink
Basic keyboard features (#2169)
Browse files Browse the repository at this point in the history
- Depends on #2168

## Checklist

- [x] 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
  • Loading branch information
pokey authored Jan 16, 2024
1 parent 6f01c31 commit 9ff8ea7
Show file tree
Hide file tree
Showing 16 changed files with 407 additions and 160 deletions.
7 changes: 7 additions & 0 deletions docs/user/experimental/keyboard/modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ Paste the following into your [VSCode `keybindings.json`](https://code.visualstu
"command": "cursorless.keyboard.modal.modeOn",
"when": "editorTextFocus"
},
{
"key": "ctrl+c",
"command": "cursorless.keyboard.targeted.targetSelection",
"when": "cursorless.keyboard.modal.mode && editorTextFocus"
},
{
"key": "escape",
"command": "cursorless.keyboard.escape",
Expand All @@ -37,6 +42,8 @@ Any keybindings that use modifier keys should go in `keybindings.json` as well,

The above allows you to press `ctrl-c` to switch to Cursorless mode, `escape` to exit Cursorless mode, and `backspace` to issue the delete action while in Cursorless mode.

If you're already in Cursorless mode, pressing `ctrl-c` again will target the current selection, which is useful if you have moved the cursor using your mouse while in Cursorless mode, and want to target your new cursor position.

### `settings.json`

To bind keys that do not have modifiers (eg just pressing `a`), add entries like the following to your [VSCode `settings.json`](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) (or edit these settings in the VSCode settings gui by saying `"cursorless settings"`):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,7 @@ import { CompositeKeyMap } from "@cursorless/common";
import { SpeakableSurroundingPairName } from "../../spokenForms/SpokenFormType";
import { SpokenFormComponentMap } from "../getSpokenFormComponentMap";
import { CustomizableSpokenFormComponentForType } from "../SpokenFormComponent";

const surroundingPairsDelimiters: Record<
SpeakableSurroundingPairName,
[string, string] | null
> = {
curlyBrackets: ["{", "}"],
angleBrackets: ["<", ">"],
escapedDoubleQuotes: ['\\"', '\\"'],
escapedSingleQuotes: ["\\'", "\\'"],
escapedParentheses: ["\\(", "\\)"],
escapedSquareBrackets: ["\\[", "\\]"],
doubleQuotes: ['"', '"'],
parentheses: ["(", ")"],
backtickQuotes: ["`", "`"],
squareBrackets: ["[", "]"],
singleQuotes: ["'", "'"],
whitespace: [" ", " "],

any: null,
string: null,
collectionBoundary: null,
};
import { surroundingPairsDelimiters } from "./surroundingPairsDelimiters";

const surroundingPairDelimiterToName = new CompositeKeyMap<
[string, string],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { SpeakableSurroundingPairName } from "../../spokenForms/SpokenFormType";

export const surroundingPairsDelimiters: Record<
SpeakableSurroundingPairName,
[string, string] | null
> = {
curlyBrackets: ["{", "}"],
angleBrackets: ["<", ">"],
escapedDoubleQuotes: ['\\"', '\\"'],
escapedSingleQuotes: ["\\'", "\\'"],
escapedParentheses: ["\\(", "\\)"],
escapedSquareBrackets: ["\\[", "\\]"],
doubleQuotes: ['"', '"'],
parentheses: ["(", ")"],
backtickQuotes: ["`", "`"],
squareBrackets: ["[", "]"],
singleQuotes: ["'", "'"],
whitespace: [" ", " "],

any: null,
string: null,
collectionBoundary: null,
};
1 change: 1 addition & 0 deletions packages/cursorless-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from "./testCaseRecorder/TestCaseRecorder";
export * from "./core/StoredTargets";
export * from "./typings/TreeSitter";
export * from "./cursorlessEngine";
export * from "./generateSpokenForm/defaultSpokenForms/surroundingPairsDelimiters";
export * from "./api/CursorlessEngineApi";
export * from "./CommandRunner";
export * from "./CommandHistory";
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,70 @@ import path from "path";
import { getCursorlessRepoRoot } from "@cursorless/common";
import { readFile } from "node:fs/promises";

interface TestCase {
name: string;
initialContent: string;
/**
* The sequence of keypresses that will be sent. The list of strings will simply
* be concatenated before sending. We could just represent this as a single string
* but it is more readable if each "token" is a separate string.
*/
keySequence: string[];
finalContent: string;
}

const testCases: TestCase[] = [
{
name: "and",
initialContent: "x T y\n",
// change plex and yank
keySequence: ["dx", "fa", "dy", "c"],
finalContent: " T \n",
},
{
name: "every",
initialContent: "a a\nb b\n",
// change every token air
keySequence: ["da", "x", "st", "c"],
finalContent: " \nb b\n",
},
{
name: "three",
initialContent: "a b c d e\n",
// change three tokens bat
keySequence: ["db", "3", "st", "c"],
finalContent: "a e\n",
},
{
name: "three backwards",
initialContent: "a b c d e\n",
// change three tokens backwards drum
keySequence: ["dd", "-3", "st", "c"],
finalContent: "a e\n",
},
{
name: "pair parens",
initialContent: "a + (b + c) + d",
// change parens bat
keySequence: ["db", "wp", "c"],
finalContent: "a + + d",
},
{
name: "pair string",
initialContent: 'a + "w" + b',
// change parens bat
keySequence: ["dw", "wj", "c"],
finalContent: "a + + b",
},
{
name: "wrap",
initialContent: "a",
// round wrap air
keySequence: ["da", "aw", "wp"],
finalContent: "(a)",
},
];

suite("Basic keyboard test", async function () {
endToEndTestSetup(this);

Expand All @@ -22,6 +86,9 @@ suite("Basic keyboard test", async function () {
test("Basic keyboard test", () => basic());
test("No automatic token expansion", () => noAutomaticTokenExpansion());
test("Run vscode command", () => vscodeCommand());
for (const t of testCases) {
test("Sequence " + t.name, () => sequence(t));
}
test("Check that entering and leaving mode is no-op", () =>
enterAndLeaveIsNoOp());
});
Expand Down Expand Up @@ -82,6 +149,22 @@ async function noAutomaticTokenExpansion() {
assert.isTrue(editor.selection.isEqual(new vscode.Selection(1, 0, 1, 0)));
}

/**
* sequence runs a test keyboard sequences.
*/
async function sequence(t: TestCase) {
const { hatTokenMap } = (await getCursorlessApi()).testHelpers!;

const editor = await openNewEditor(t.initialContent, {
languageId: "typescript",
});
await hatTokenMap.allocateHats();
editor.selection = new vscode.Selection(1, 0, 1, 0);
await vscode.commands.executeCommand("cursorless.keyboard.modal.modeOn");
await typeText(t.keySequence.join(""));
assert.equal(editor.document.getText(), t.finalContent);
}

async function vscodeCommand() {
const { hatTokenMap } = (await getCursorlessApi()).testHelpers!;

Expand Down
49 changes: 15 additions & 34 deletions packages/cursorless-vscode/src/keyboard/KeyboardCommandHandler.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ScopeType } from "@cursorless/common";
import { Modifier, SurroundingPairName } 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";
import { surroundingPairsDelimiters } from "@cursorless/cursorless-engine";

/**
* This class defines the keyboard commands available to our modal keyboard
Expand All @@ -27,15 +28,8 @@ import { ModalVscodeCommandDescriptor } from "./TokenTypes";
export class KeyboardCommandHandler {
constructor(private targeted: KeyboardCommandsTargeted) {}

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

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

async vscodeCommand({
Expand Down Expand Up @@ -78,22 +72,18 @@ export class KeyboardCommandHandler {
this.targeted.performSimpleActionOnTarget(actionName);
}

modifyTargetContainingScope(arg: { scopeType: ScopeType }) {
this.targeted.modifyTargetContainingScope(arg);
performWrapActionOnTarget({ delimiter }: { delimiter: SurroundingPairName }) {
const [left, right] = surroundingPairsDelimiters[delimiter]!;
this.targeted.performActionOnTarget((target) => ({
name: "wrapWithPairedDelimiter",
target,
left,
right,
}));
}

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

Expand All @@ -102,16 +92,7 @@ interface DecoratedMarkArg {
color?: HatColor;
shape?: HatShape;
};
}
interface TargetRelativeExclusiveScopeArg {
offset: Offset;
length: number | null;
scopeType: ScopeType;
}

interface Offset {
direction: "forward" | "backward" | null;
number: number | null;
mode: "replace" | "extend" | "append";
}

function isString(input: any): input is string {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { pick, toPairs } from "lodash";
import { pick, sortedUniq, toPairs } from "lodash";
import { Grammar, Parser } from "nearley";
import * as vscode from "vscode";
import { KeyboardCommandsModalLayer } from "./KeyboardCommandsModalLayer";
Expand Down Expand Up @@ -90,9 +90,9 @@ export default class KeyboardCommandsModal {
private computeLayer() {
const acceptableTokenTypeInfos = getAcceptableTokenTypes(this.parser);
// FIXME: Here's where we'd update sidebar
const acceptableTokenTypes = acceptableTokenTypeInfos
.map(({ type }) => type)
.sort();
const acceptableTokenTypes = sortedUniq(
acceptableTokenTypeInfos.map(({ type }) => type).sort(),
);
let layer = this.layerCache.get(acceptableTokenTypes);
if (layer == null) {
layer = new KeyboardCommandsModalLayer(
Expand Down
9 changes: 7 additions & 2 deletions packages/cursorless-vscode/src/keyboard/TokenTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SimpleScopeTypeType } from "@cursorless/common";
import { SimpleScopeTypeType, SurroundingPairName } from "@cursorless/common";
import { HatColor, HatShape } from "../ide/vscode/hatStyles.types";
import {
KeyboardActionType,
Expand All @@ -14,11 +14,12 @@ export interface SectionTypes {
color: HatColor;
misc: MiscValue;
scope: SimpleScopeTypeType;
pairedDelimiter: SurroundingPairName;
shape: HatShape;
vscodeCommand: ModalVscodeCommandDescriptor;
modifier: ModifierType;
}
type ModifierType = "nextPrev";
type ModifierType = "nextPrev" | "every";
type MiscValue =
| "combineColorAndShape"
| "makeRange"
Expand Down Expand Up @@ -48,17 +49,21 @@ export interface TokenTypeValueMap {
color: HatColor;
shape: HatShape;
vscodeCommand: ModalVscodeCommandDescriptor;
pairedDelimiter: SurroundingPairName;

// action config section
simpleAction: SimpleKeyboardActionType;
wrap: "wrap";

// misc config section
makeRange: "makeRange";
makeList: "makeList";
combineColorAndShape: "combineColorAndShape";
direction: "forward" | "backward";

// modifier config section
nextPrev: "nextPrev";
every: "every";

digit: number;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,19 @@ export function getTokenTypeKeyMaps(
color: config.getTokenKeyMap("color"),
shape: config.getTokenKeyMap("shape"),
vscodeCommand: config.getTokenKeyMap("vscodeCommand"),
pairedDelimiter: config.getTokenKeyMap("pairedDelimiter"),

// action config section
simpleAction: config.getTokenKeyMap(
"simpleAction",
"action",
simpleKeyboardActionNames,
),
wrap: config.getTokenKeyMap("wrap", "action", ["wrap"]),

// misc config section
makeRange: config.getTokenKeyMap("makeRange", "misc", ["makeRange"]),
makeList: config.getTokenKeyMap("makeList", "misc", ["makeList"]),
combineColorAndShape: config.getTokenKeyMap(
"combineColorAndShape",
"misc",
Expand All @@ -57,6 +60,7 @@ export function getTokenTypeKeyMaps(
]),

// modifier config section
every: config.getTokenKeyMap("every", "modifier", ["every"]),
nextPrev: config.getTokenKeyMap("nextPrev", "modifier", ["nextPrev"]),

digit: Object.fromEntries(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
KeyboardCommand,
KeyboardCommandArgTypes,
} from "../KeyboardCommandTypeHelpers";
import { Unused } from "./grammarHelpers";

/**
* Represents a post-processing function for a top-level rule of our grammar.
Expand All @@ -19,7 +18,5 @@ export interface CommandRulePostProcessor<
metadata: {
/** The command type */
type: T;
/** The names of the arguments to the command's argument payload */
argNames: (keyof KeyboardCommandArgTypes[T] | Unused)[];
};
}
Loading

0 comments on commit 9ff8ea7

Please sign in to comment.