Skip to content

Commit 9cedee8

Browse files
authored
keyboard: Support targeting modes for targeting selection (#2429)
- Fixes #2425 ## 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
1 parent ae824fd commit 9cedee8

File tree

10 files changed

+119
-37
lines changed

10 files changed

+119
-37
lines changed

packages/cursorless-vscode-e2e/src/suite/keyboard/basic.vscode.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,30 @@ const testCases: TestCase[] = [
8383
keySequence: ["da", "aw", "wp", "aw", "wp"],
8484
finalContent: "((a))\n",
8585
},
86+
{
87+
name: "simple mark",
88+
initialContent: "aaa bbb",
89+
// keyboard air
90+
// keyboard this
91+
keySequence: ["da", "mc", "c"],
92+
finalContent: "aaa ",
93+
},
94+
{
95+
name: "simple mark range",
96+
initialContent: "aaa bbb ccc",
97+
// keyboard air
98+
// keyboard past this
99+
keySequence: ["db", "fk", "mc", "c"],
100+
finalContent: "aaa ",
101+
},
102+
{
103+
name: "simple mark list",
104+
initialContent: "aaa bbb ccc",
105+
// keyboard air
106+
// keyboard and this
107+
keySequence: ["db", "fa", "mc", "c"],
108+
finalContent: "aaa ",
109+
},
86110
{
87111
name: "modifier range",
88112
initialContent: "aaa bbb ccc ddd",

packages/cursorless-vscode/src/keyboard/KeyboardCommandHandler.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { Modifier, SurroundingPairName } from "@cursorless/common";
1+
import { Modifier, PartialMark, SurroundingPairName } from "@cursorless/common";
2+
import { surroundingPairsDelimiters } from "@cursorless/cursorless-engine";
3+
import { isString } from "lodash";
24
import * as vscode from "vscode";
35
import { HatColor, HatShape } from "../ide/vscode/hatStyles.types";
46
import {
@@ -9,8 +11,6 @@ import KeyboardCommandsTargeted, {
911
TargetingMode,
1012
} from "./KeyboardCommandsTargeted";
1113
import { ModalVscodeCommandDescriptor } from "./TokenTypes";
12-
import { surroundingPairsDelimiters } from "@cursorless/cursorless-engine";
13-
import { isString } from "lodash";
1414

1515
/**
1616
* This class defines the keyboard commands available to our modal keyboard
@@ -100,6 +100,10 @@ export class KeyboardCommandHandler {
100100
}) {
101101
this.targeted.targetModifier(modifier, mode);
102102
}
103+
104+
targetMark({ mark, mode }: { mark: PartialMark; mode?: TargetingMode }) {
105+
this.targeted.targetMark(mark, mode);
106+
}
103107
}
104108

105109
interface DecoratedMarkArg {

packages/cursorless-vscode/src/keyboard/KeyboardCommandsTargeted.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
ActionDescriptor,
33
LATEST_VERSION,
44
Modifier,
5+
PartialMark,
56
PartialPrimitiveTargetDescriptor,
67
PartialTargetDescriptor,
78
ScopeType,
@@ -14,7 +15,7 @@ import KeyboardCommandsModal from "./KeyboardCommandsModal";
1415
import KeyboardHandler from "./KeyboardHandler";
1516
import { SimpleKeyboardActionDescriptor } from "./KeyboardActionType";
1617

17-
export type TargetingMode = "replace" | "extend" | "append";
18+
export type TargetingMode = "replace" | "makeRange" | "makeList";
1819

1920
interface TargetDecoratedMarkArgument {
2021
color?: HatColor;
@@ -104,15 +105,15 @@ export default class KeyboardCommandsTargeted {
104105
mode: TargetingMode,
105106
): PartialTargetDescriptor {
106107
switch (mode) {
107-
case "extend":
108+
case "makeRange":
108109
return {
109110
type: "range",
110111
anchor: getKeyboardTarget(),
111112
active: target,
112113
excludeActive: false,
113114
excludeAnchor: false,
114115
};
115-
case "append":
116+
case "makeList":
116117
return {
117118
type: "list",
118119
elements: [getKeyboardTarget(), target],
@@ -122,6 +123,18 @@ export default class KeyboardCommandsTargeted {
122123
}
123124
}
124125

126+
/**
127+
* Sets the highlighted target to the given mark
128+
*
129+
* @param mark The desired mark
130+
* @param mode The targeting mode
131+
* @returns A promise that resolves to the result of the cursorless command
132+
*/
133+
targetMark = async (mark: PartialMark, mode: TargetingMode = "replace") =>
134+
await setKeyboardTarget(
135+
this.applyTargetingMode({ type: "primitive", mark }, mode),
136+
);
137+
125138
/**
126139
* Applies {@link modifier} to the current target
127140
* @param param0 Describes the desired modifier

packages/cursorless-vscode/src/keyboard/TokenTypes.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface SectionTypes {
1414
action: PolymorphicKeyboardActionDescriptor;
1515
color: HatColor;
1616
misc: MiscValue;
17+
specialMark: SpecialMark;
1718
scope: SimpleScopeTypeType;
1819
pairedDelimiter: SurroundingPairName;
1920
shape: HatShape;
@@ -33,6 +34,7 @@ export type MiscValue =
3334
| "makeList"
3435
| "forward"
3536
| "backward";
37+
export type SpecialMark = "cursor";
3638

3739
/**
3840
* Maps from token type used in parser to the type of values that the token type
@@ -63,8 +65,7 @@ export interface TokenTypeValueMap {
6365
wrap: SpecificKeyboardActionDescriptor<"wrap">;
6466

6567
// misc config section
66-
makeRange: "makeRange";
67-
makeList: "makeList";
68+
targetingMode: "makeRange" | "makeList";
6869
combineColorAndShape: "combineColorAndShape";
6970
direction: "forward" | "backward";
7071

@@ -74,6 +75,9 @@ export interface TokenTypeValueMap {
7475
headTail: "extendThroughStartOf" | "extendThroughEndOf";
7576
simpleModifier: "interiorOnly" | "excludeInterior";
7677

78+
// mark config section
79+
simpleSpecialMark: SpecialMark;
80+
7781
digit: number;
7882
}
7983

packages/cursorless-vscode/src/keyboard/getTokenTypeKeyMaps.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ export function getTokenTypeKeyMaps(
5252
),
5353

5454
// misc config section
55-
makeRange: config.getTokenKeyMap("makeRange", "misc", only("makeRange")),
56-
makeList: config.getTokenKeyMap("makeList", "misc", only("makeList")),
55+
targetingMode: config.getTokenKeyMap(
56+
"targetingMode",
57+
"misc",
58+
only("makeRange", "makeList"),
59+
),
5760
combineColorAndShape: config.getTokenKeyMap(
5861
"combineColorAndShape",
5962
"misc",
@@ -79,6 +82,12 @@ export function getTokenTypeKeyMaps(
7982
only("interiorOnly", "excludeInterior"),
8083
),
8184

85+
// mark config section
86+
simpleSpecialMark: config.getTokenKeyMap(
87+
"simpleSpecialMark",
88+
"specialMark",
89+
),
90+
8291
digit: Object.fromEntries(
8392
range(10).map((value) => [
8493
value.toString(),

packages/cursorless-vscode/src/keyboard/grammar/generated/grammar.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
// Bypasses TS6133. Allow declared but unused functions.
44
// @ts-ignore
55
function id(d: any[]): any { return d[0]; }
6-
declare var makeRange: any;
7-
declare var makeList: any;
6+
declare var targetingMode: any;
87
declare var simpleAction: any;
98
declare var wrap: any;
109
declare var pairedDelimiter: any;
@@ -14,6 +13,7 @@ declare var headTail: any;
1413
declare var every: any;
1514
declare var nextPrev: any;
1615
declare var simpleScopeTypeType: any;
16+
declare var simpleSpecialMark: any;
1717
declare var color: any;
1818
declare var shape: any;
1919
declare var combineColorAndShape: any;
@@ -56,18 +56,27 @@ interface Grammar {
5656
const grammar: Grammar = {
5757
Lexer: keyboardLexer,
5858
ParserRules: [
59-
{"name": "main", "symbols": ["decoratedMark"], "postprocess":
60-
command("targetDecoratedMark", { decoratedMark: $0, mode: "replace" })
59+
{"name": "main$ebnf$1", "symbols": [(keyboardLexer.has("targetingMode") ? {type: "targetingMode"} : targetingMode)], "postprocess": id},
60+
{"name": "main$ebnf$1", "symbols": [], "postprocess": () => null},
61+
{"name": "main", "symbols": ["main$ebnf$1", "decoratedMark"], "postprocess":
62+
command(
63+
"targetDecoratedMark",
64+
([targetingMode, decoratedMark]) => ({ decoratedMark, mode: targetingMode ?? "replace" })
65+
)
6166
},
62-
{"name": "main", "symbols": [(keyboardLexer.has("makeRange") ? {type: "makeRange"} : makeRange), "decoratedMark"], "postprocess":
63-
command("targetDecoratedMark", { decoratedMark: $1, mode: "extend" })
67+
{"name": "main$ebnf$2", "symbols": [(keyboardLexer.has("targetingMode") ? {type: "targetingMode"} : targetingMode)], "postprocess": id},
68+
{"name": "main$ebnf$2", "symbols": [], "postprocess": () => null},
69+
{"name": "main", "symbols": ["main$ebnf$2", "mark"], "postprocess":
70+
command("targetMark", ([targetingMode, mark]) => ({ mark, mode: targetingMode ?? "replace" }))
6471
},
65-
{"name": "main", "symbols": [(keyboardLexer.has("makeList") ? {type: "makeList"} : makeList), "decoratedMark"], "postprocess":
66-
command("targetDecoratedMark", { decoratedMark: $1, mode: "append" })
72+
{"name": "main$ebnf$3", "symbols": [(keyboardLexer.has("targetingMode") ? {type: "targetingMode"} : targetingMode)], "postprocess": id},
73+
{"name": "main$ebnf$3", "symbols": [], "postprocess": () => null},
74+
{"name": "main", "symbols": ["main$ebnf$3", "modifier"], "postprocess":
75+
command(
76+
"modifyTarget",
77+
([targetingMode, modifier]) => ({ modifier, mode: targetingMode ?? "replace" })
78+
)
6779
},
68-
{"name": "main", "symbols": ["modifier"], "postprocess": command("modifyTarget", { modifier: $0 })},
69-
{"name": "main", "symbols": [(keyboardLexer.has("makeRange") ? {type: "makeRange"} : makeRange), "modifier"], "postprocess": command("modifyTarget", { modifier: $1, mode: "extend" })},
70-
{"name": "main", "symbols": [(keyboardLexer.has("makeList") ? {type: "makeList"} : makeList), "modifier"], "postprocess": command("modifyTarget", { modifier: $1, mode: "append" })},
7180
{"name": "main", "symbols": [(keyboardLexer.has("simpleAction") ? {type: "simpleAction"} : simpleAction)], "postprocess": command("performSimpleActionOnTarget", ["actionDescriptor"])},
7281
{"name": "main", "symbols": [(keyboardLexer.has("wrap") ? {type: "wrap"} : wrap), (keyboardLexer.has("pairedDelimiter") ? {type: "pairedDelimiter"} : pairedDelimiter)], "postprocess":
7382
command("performWrapActionOnTarget", ["actionDescriptor", "delimiter"])
@@ -105,6 +114,7 @@ const grammar: Grammar = {
105114
{"name": "scopeType", "symbols": [(keyboardLexer.has("pairedDelimiter") ? {type: "pairedDelimiter"} : pairedDelimiter)], "postprocess":
106115
([delimiter]) => ({ type: "surroundingPair", delimiter })
107116
},
117+
{"name": "mark", "symbols": [(keyboardLexer.has("simpleSpecialMark") ? {type: "simpleSpecialMark"} : simpleSpecialMark)], "postprocess": capture({ type: $0 })},
108118
{"name": "decoratedMark", "symbols": [(keyboardLexer.has("color") ? {type: "color"} : color)], "postprocess": capture("color")},
109119
{"name": "decoratedMark", "symbols": [(keyboardLexer.has("shape") ? {type: "shape"} : shape)], "postprocess": capture("shape")},
110120
{"name": "decoratedMark", "symbols": [(keyboardLexer.has("combineColorAndShape") ? {type: "combineColorAndShape"} : combineColorAndShape), (keyboardLexer.has("color") ? {type: "color"} : color), (keyboardLexer.has("shape") ? {type: "shape"} : shape)], "postprocess": capture(_, "color", "shape")},

packages/cursorless-vscode/src/keyboard/grammar/getAcceptableTokenTypes.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const testCases: TestCase[] = [
6161
direction: "forward",
6262
scopeType: MISSING,
6363
},
64+
mode: "replace",
6465
},
6566
},
6667
{
@@ -74,6 +75,7 @@ const testCases: TestCase[] = [
7475
direction: "forward",
7576
scopeType: MISSING,
7677
},
78+
mode: "replace",
7779
},
7880
},
7981
{
@@ -87,6 +89,7 @@ const testCases: TestCase[] = [
8789
direction: "forward",
8890
scopeType: MISSING,
8991
},
92+
mode: "replace",
9093
},
9194
},
9295
],
@@ -107,6 +110,7 @@ const testCases: TestCase[] = [
107110
type: NEXT,
108111
},
109112
},
113+
mode: "replace",
110114
},
111115
},
112116
{
@@ -120,6 +124,7 @@ const testCases: TestCase[] = [
120124
direction: "forward",
121125
scopeType: MISSING,
122126
},
127+
mode: "replace",
123128
},
124129
},
125130
],

packages/cursorless-vscode/src/keyboard/grammar/grammar.ne

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,28 @@ const { $0, $1, $2 } = argPositions;
1010

1111
# ===================== Top-level commands ===================
1212

13-
# --------------------------- Marks --------------------------
13+
# --------------------------- Mark --------------------------
1414
# "air"
15-
main -> decoratedMark {%
16-
command("targetDecoratedMark", { decoratedMark: $0, mode: "replace" })
15+
main -> %targetingMode:? decoratedMark {%
16+
command(
17+
"targetDecoratedMark",
18+
([targetingMode, decoratedMark]) => ({ decoratedMark, mode: targetingMode ?? "replace" })
19+
)
1720
%}
1821

19-
# "past air"
20-
main -> %makeRange decoratedMark {%
21-
command("targetDecoratedMark", { decoratedMark: $1, mode: "extend" })
22-
%}
23-
24-
# "and air"
25-
main -> %makeList decoratedMark {%
26-
command("targetDecoratedMark", { decoratedMark: $1, mode: "append" })
22+
# Other marks
23+
main -> %targetingMode:? mark {%
24+
command("targetMark", ([targetingMode, mark]) => ({ mark, mode: targetingMode ?? "replace" }))
2725
%}
2826

2927
# --------------------------- Modifier --------------------------
3028

31-
main -> modifier {% command("modifyTarget", { modifier: $0 }) %}
32-
main -> %makeRange modifier {% command("modifyTarget", { modifier: $1, mode: "extend" }) %}
33-
main -> %makeList modifier {% command("modifyTarget", { modifier: $1, mode: "append" }) %}
29+
main -> %targetingMode:? modifier {%
30+
command(
31+
"modifyTarget",
32+
([targetingMode, modifier]) => ({ modifier, mode: targetingMode ?? "replace" })
33+
)
34+
%}
3435

3536
# --------------------------- Actions --------------------------
3637

@@ -93,6 +94,11 @@ scopeType -> %pairedDelimiter {%
9394
([delimiter]) => ({ type: "surroundingPair", delimiter })
9495
%}
9596

97+
# --------------------------- Marks ---------------------------
98+
99+
# "this"
100+
mark -> %simpleSpecialMark {% capture({ type: $0 }) %}
101+
96102
# --------------------------- Other ---------------------------
97103
decoratedMark ->
98104
%color {% capture("color") %}

packages/cursorless-vscode/src/keyboard/grammar/grammar.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,15 @@ const testCases: TestCase[] = [
7272
},
7373
{
7474
tokens: [
75-
{ type: "makeRange", value: "makeRange" },
75+
{ type: "targetingMode", value: "makeRange" },
7676
{ type: "color", value: "green" },
7777
],
7878
expected: {
7979
arg: {
8080
decoratedMark: {
8181
color: "green",
8282
},
83-
mode: "extend",
83+
mode: "makeRange",
8484
},
8585
type: "targetDecoratedMark",
8686
},
@@ -103,6 +103,7 @@ const testCases: TestCase[] = [
103103
type: "namedFunction",
104104
},
105105
},
106+
mode: "replace",
106107
},
107108
type: "modifyTarget",
108109
},
@@ -124,6 +125,7 @@ const testCases: TestCase[] = [
124125
type: "namedFunction",
125126
},
126127
},
128+
mode: "replace",
127129
},
128130
type: "modifyTarget",
129131
},
@@ -135,6 +137,7 @@ const testCases: TestCase[] = [
135137
modifier: {
136138
type: "excludeInterior",
137139
},
140+
mode: "replace",
138141
},
139142
type: "modifyTarget",
140143
},
@@ -154,6 +157,7 @@ const testCases: TestCase[] = [
154157
},
155158
],
156159
},
160+
mode: "replace",
157161
},
158162
type: "modifyTarget",
159163
},

packages/cursorless-vscode/src/keyboard/keyboard-config.fixture.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@
6565
"fa": "makeList",
6666
"-": "backward"
6767
},
68+
"cursorless.experimental.keyboard.modal.keybindings.specialMark": {
69+
"mc": "cursor"
70+
},
6871
"cursorless.experimental.keyboard.modal.keybindings.modifier": {
6972
"n": "nextPrev",
7073
"*": "every",

0 commit comments

Comments
 (0)