Skip to content

Commit 8f88564

Browse files
Added glyph scope (#2050)
`chuck glyph dollar red air` Fixes #54 ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [x] 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) - [x] I have not broken the cheatsheet --------- Co-authored-by: Pokey Rule <[email protected]>
1 parent 1a4a922 commit 8f88564

File tree

24 files changed

+381
-58
lines changed

24 files changed

+381
-58
lines changed
Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1-
from ..get_list import get_lists
1+
from ..get_list import get_lists, get_raw_list
22

33

44
def get_scopes():
5-
return get_lists(
6-
["scope_type"],
7-
"scopeType",
5+
complex_scopes = get_raw_list("glyph_scope_type")
6+
return [
7+
*get_lists(
8+
["scope_type"],
9+
"scopeType",
10+
{
11+
"argumentOrParameter": "Argument",
12+
"boundedNonWhitespaceSequence": "Non whitespace sequence stopped by surrounding pair delimeters",
13+
},
14+
),
815
{
9-
"argumentOrParameter": "Argument",
10-
"boundedNonWhitespaceSequence": "Non whitespace sequence stopped by surrounding pair delimeters",
16+
"id": "glyph",
17+
"type": "scopeType",
18+
"variations": [
19+
{
20+
"spokenForm": f"{complex_scopes['glyph']} <character>",
21+
"description": "Instance of single character <character>",
22+
},
23+
],
1124
},
12-
)
25+
]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from talon import Module
2+
3+
mod = Module()
4+
5+
mod.list(
6+
"cursorless_glyph_scope_type",
7+
desc="Cursorless glyph scope type",
8+
)
9+
mod.list(
10+
"cursorless_glyph_scope_type_plural",
11+
desc="Plural version of Cursorless glyph scope type",
12+
)
13+
14+
15+
@mod.capture(rule="{user.cursorless_glyph_scope_type} <user.any_alphanumeric_key>")
16+
def cursorless_glyph_scope_type(m) -> dict[str, str]:
17+
return {
18+
"type": "glyph",
19+
"character": m.any_alphanumeric_key,
20+
}
21+
22+
23+
@mod.capture(
24+
rule="{user.cursorless_glyph_scope_type_plural} <user.any_alphanumeric_key>"
25+
)
26+
def cursorless_glyph_scope_type_plural(m) -> dict[str, str]:
27+
return {
28+
"type": "glyph",
29+
"character": m.any_alphanumeric_key,
30+
}

cursorless-talon/src/modifiers/scopes.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,39 @@
1515

1616

1717
@mod.capture(
18-
rule="{user.cursorless_scope_type} | {user.cursorless_custom_regex_scope_type}"
18+
rule="{user.cursorless_scope_type} | <user.cursorless_glyph_scope_type> | {user.cursorless_custom_regex_scope_type}"
1919
)
2020
def cursorless_scope_type(m) -> dict[str, str]:
2121
"""Cursorless scope type singular"""
2222
try:
2323
return {"type": m.cursorless_scope_type}
2424
except AttributeError:
25-
return {"type": "customRegex", "regex": m.cursorless_custom_regex_scope_type}
25+
pass
26+
27+
try:
28+
return m.cursorless_glyph_scope_type
29+
except AttributeError:
30+
pass
31+
32+
return {"type": "customRegex", "regex": m.cursorless_custom_regex_scope_type}
2633

2734

2835
@mod.capture(
29-
rule="{user.cursorless_scope_type_plural} | {user.cursorless_custom_regex_scope_type_plural}"
36+
rule="{user.cursorless_scope_type_plural} | <user.cursorless_glyph_scope_type_plural> | {user.cursorless_custom_regex_scope_type_plural}"
3037
)
3138
def cursorless_scope_type_plural(m) -> dict[str, str]:
3239
"""Cursorless scope type plural"""
3340
try:
3441
return {"type": m.cursorless_scope_type_plural}
3542
except AttributeError:
36-
return {
37-
"type": "customRegex",
38-
"regex": m.cursorless_custom_regex_scope_type_plural,
39-
}
43+
pass
44+
45+
try:
46+
return m.cursorless_glyph_scope_type_plural
47+
except AttributeError:
48+
pass
49+
50+
return {
51+
"type": "customRegex",
52+
"regex": m.cursorless_custom_regex_scope_type_plural,
53+
}

cursorless-talon/src/spoken_forms.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@
171171
},
172172
"surrounding_pair_scope_type": {
173173
"string": "string"
174+
},
175+
"glyph_scope_type": {
176+
"glyph": "glyph"
174177
}
175178
},
176179
"paired_delimiters.csv": {

cursorless-talon/src/spoken_forms.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def ret(filename: str, *args: P.args, **kwargs: P.kwargs) -> R:
6868
"wrapper_only_paired_delimiter": "pairedDelimiter",
6969
"surrounding_pair_scope_type": "pairedDelimiter",
7070
"scope_type": "simpleScopeTypeType",
71+
"glyph_scope_type": "complexScopeTypeType",
7172
"custom_regex_scope_type": "customRegex",
7273
}
7374

@@ -125,7 +126,7 @@ def handle_new_values(csv_name: str, values: list[SpokenFormEntry]):
125126
handle_csv("experimental/miscellaneous.csv"),
126127
handle_csv(
127128
"modifier_scope_types.csv",
128-
pluralize_lists=["scope_type"],
129+
pluralize_lists=["scope_type", "glyph_scope_type"],
129130
extra_allowed_values=[
130131
"private.fieldAccess",
131132
"private.switchStatementSubject",

packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@
1414
}
1515
]
1616
},
17+
{
18+
"id": "breakLine",
19+
"type": "action",
20+
"variations": [
21+
{
22+
"spokenForm": "break <target>",
23+
"description": "Break line"
24+
}
25+
]
26+
},
1727
{
1828
"id": "callAsFunction",
1929
"type": "action",
@@ -228,6 +238,16 @@
228238
}
229239
]
230240
},
241+
{
242+
"id": "joinLines",
243+
"type": "action",
244+
"variations": [
245+
{
246+
"spokenForm": "join <target>",
247+
"description": "Join lines"
248+
}
249+
]
250+
},
231251
{
232252
"id": "moveToTarget",
233253
"type": "action",
@@ -905,6 +925,16 @@
905925
"description": "Trailing delimiter range"
906926
}
907927
]
928+
},
929+
{
930+
"id": "visible",
931+
"type": "modifier",
932+
"variations": [
933+
{
934+
"spokenForm": "visible",
935+
"description": "Visible"
936+
}
937+
]
908938
}
909939
]
910940
},
@@ -1058,6 +1088,16 @@
10581088
}
10591089
]
10601090
},
1091+
{
1092+
"id": "show_scope_sidebar",
1093+
"type": "command",
1094+
"variations": [
1095+
{
1096+
"spokenForm": "bar cursorless",
1097+
"description": "Show cursorless sidebar"
1098+
}
1099+
]
1100+
},
10611101
{
10621102
"id": "show_scope_visualizer",
10631103
"type": "command",
@@ -1272,6 +1312,16 @@
12721312
}
12731313
]
12741314
},
1315+
{
1316+
"id": "glyph",
1317+
"type": "scopeType",
1318+
"variations": [
1319+
{
1320+
"spokenForm": "glyph <character>",
1321+
"description": "Instance of single character <character>"
1322+
}
1323+
]
1324+
},
12751325
{
12761326
"id": "identifier",
12771327
"type": "scopeType",
@@ -1537,7 +1587,7 @@
15371587
"type": "scopeType",
15381588
"variations": [
15391589
{
1540-
"spokenForm": "word",
1590+
"spokenForm": "sub",
15411591
"description": "Word"
15421592
}
15431593
]

packages/common/src/types/command/PartialTargetDescriptor.types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,17 @@ export interface OneOfScopeType {
205205
scopeTypes: ScopeType[];
206206
}
207207

208+
export interface GlyphScopeType {
209+
type: "glyph";
210+
character: string;
211+
}
212+
208213
export type ScopeType =
209214
| SimpleScopeType
210215
| SurroundingPairScopeType
211216
| CustomRegexScopeType
212-
| OneOfScopeType;
217+
| OneOfScopeType
218+
| GlyphScopeType;
213219

214220
export interface ContainingSurroundingPairModifier
215221
extends ContainingScopeModifier {

packages/cursorless-engine/src/core/Cheatsheet.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import produce from "immer";
44
import { sortBy } from "lodash";
55
import { ide } from "../singletons/ide.singleton";
66
import path from "path";
7+
import { getCursorlessRepoRoot } from "@cursorless/common";
78

89
/**
910
* The argument expected by the cheatsheet command.
@@ -56,27 +57,9 @@ export async function showCheatsheet({
5657
* @param spokenFormInfo The new value to use for default spoken forms.
5758
*/
5859
export async function updateDefaults(spokenFormInfo: CheatsheetInfo) {
59-
const { runMode, assetsRoot, workspaceFolders } = ide();
60-
61-
const workspacePath =
62-
runMode === "development"
63-
? assetsRoot
64-
: workspaceFolders?.[0].uri.path ?? null;
65-
66-
if (workspacePath == null) {
67-
throw new Error(
68-
"Please update defaults from Cursorless workspace or running in debug",
69-
);
70-
}
71-
7260
const defaultsPath = path.join(
73-
workspacePath,
74-
"packages",
75-
"cheatsheet",
76-
"src",
77-
"lib",
78-
"sampleSpokenFormInfos",
79-
"defaults.json",
61+
getCursorlessRepoRoot(),
62+
"packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json",
8063
);
8164

8265
const outputObject = produce(spokenFormInfo, (draft) => {
@@ -86,7 +69,7 @@ export async function updateDefaults(spokenFormInfo: CheatsheetInfo) {
8669
});
8770
});
8871

89-
await writeFile(defaultsPath, JSON.stringify(outputObject, null, "\t"));
72+
await writeFile(defaultsPath, JSON.stringify(outputObject, null, 2) + "\n");
9073
}
9174

9275
// FIXME: Stop duplicating these types once we have #945
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import assert from "node:assert";
2+
import { CustomSpokenFormGeneratorImpl } from "./CustomSpokenFormGeneratorImpl";
3+
import { asyncSafety } from "@cursorless/common";
4+
5+
suite("CustomSpokenFormGeneratorImpl", async function () {
6+
test(
7+
"glyph",
8+
asyncSafety(async () => {
9+
const generator = new CustomSpokenFormGeneratorImpl({
10+
async getSpokenFormEntries() {
11+
return [
12+
{
13+
type: "complexScopeTypeType",
14+
id: "glyph",
15+
spokenForms: ["foo"],
16+
},
17+
];
18+
},
19+
onDidChange: () => ({ dispose() {} }),
20+
});
21+
22+
await generator.customSpokenFormsInitialized;
23+
24+
const spokenForm = generator.scopeTypeToSpokenForm({
25+
type: "glyph",
26+
character: "a",
27+
});
28+
29+
assert.deepStrictEqual(spokenForm, {
30+
type: "success",
31+
spokenForms: ["foo air"],
32+
});
33+
}),
34+
);
35+
});

packages/cursorless-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,15 @@ export class CustomSpokenFormGeneratorImpl
2121
private spokenFormGenerator: SpokenFormGenerator;
2222
private disposable: Disposable;
2323

24+
/**
25+
* A promise that resolves when the custom spoken forms have been loaded.
26+
*/
27+
public readonly customSpokenFormsInitialized: Promise<void>;
28+
2429
constructor(talonSpokenForms: TalonSpokenForms) {
2530
this.customSpokenForms = new CustomSpokenForms(talonSpokenForms);
31+
this.customSpokenFormsInitialized =
32+
this.customSpokenForms.customSpokenFormsInitialized;
2633
this.spokenFormGenerator = new SpokenFormGenerator(
2734
this.customSpokenForms.spokenFormMap,
2835
);

0 commit comments

Comments
 (0)