Skip to content

Commit 6771a2e

Browse files
committed
Allow spoken forms generator to use custom
1 parent d1ef7dd commit 6771a2e

File tree

19 files changed

+924
-544
lines changed

19 files changed

+924
-544
lines changed

packages/common/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export { getKey, splitKey } from "./util/splitKey";
1111
export { hrtimeBigintToSeconds } from "./util/timeUtils";
1212
export * from "./util/walkSync";
1313
export * from "./util/walkAsync";
14+
export * from "./util/camelCaseToAllDown";
1415
export { Notifier } from "./util/Notifier";
1516
export type { Listener } from "./util/Notifier";
1617
export type { TokenHatSplittingMode } from "./ide/types/Configuration";
@@ -42,6 +43,7 @@ export * from "./types/TextEditorOptions";
4243
export * from "./types/TextLine";
4344
export * from "./types/Token";
4445
export * from "./types/HatTokenMap";
46+
export * from "./types/SpokenForm";
4547
export * from "./util/textFormatters";
4648
export * from "./types/snippet.types";
4749
export * from "./testUtil/fromPlainObject";
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export interface SpokenFormSuccess {
2+
type: "success";
3+
preferred: string;
4+
alternatives: string[];
5+
}
6+
7+
export interface SpokenFormError {
8+
type: "error";
9+
reason: string;
10+
requiresTalonUpdate: boolean;
11+
isSecret: boolean;
12+
}
13+
14+
export type SpokenForm = SpokenFormSuccess | SpokenFormError;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function camelCaseToAllDown(input: string): string {
2+
return input
3+
.replace(/([A-Z])/g, " $1")
4+
.split(" ")
5+
.map((word) => word.toLowerCase())
6+
.join(" ");
7+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { mapValues } from "lodash";
2+
import {
3+
SpokenFormMap,
4+
SpokenFormMapEntry,
5+
SpokenFormMapKeyTypes,
6+
} from "./SpokenFormMap";
7+
8+
type DefaultSpokenFormMapDefinition = {
9+
readonly [K in keyof SpokenFormMapKeyTypes]: Readonly<
10+
Record<SpokenFormMapKeyTypes[K], string | DefaultSpokenFormMapEntry>
11+
>;
12+
};
13+
14+
const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = {
15+
pairedDelimiter: {
16+
curlyBrackets: "curly",
17+
angleBrackets: "diamond",
18+
escapedDoubleQuotes: "escaped quad",
19+
escapedSingleQuotes: "escaped twin",
20+
escapedParentheses: "escaped round",
21+
escapedSquareBrackets: "escaped box",
22+
doubleQuotes: "quad",
23+
parentheses: "round",
24+
backtickQuotes: "skis",
25+
squareBrackets: "box",
26+
singleQuotes: "twin",
27+
any: "pair",
28+
string: "string",
29+
whitespace: "void",
30+
},
31+
32+
simpleScopeTypeType: {
33+
argumentOrParameter: "arg",
34+
attribute: "attribute",
35+
functionCall: "call",
36+
functionCallee: "callee",
37+
className: "class name",
38+
class: "class",
39+
comment: "comment",
40+
functionName: "funk name",
41+
namedFunction: "funk",
42+
ifStatement: "if state",
43+
instance: "instance",
44+
collectionItem: "item",
45+
collectionKey: "key",
46+
anonymousFunction: "lambda",
47+
list: "list",
48+
map: "map",
49+
name: "name",
50+
regularExpression: "regex",
51+
section: "section",
52+
sectionLevelOne: disabledByDefault("one section"),
53+
sectionLevelTwo: disabledByDefault("two section"),
54+
sectionLevelThree: disabledByDefault("three section"),
55+
sectionLevelFour: disabledByDefault("four section"),
56+
sectionLevelFive: disabledByDefault("five section"),
57+
sectionLevelSix: disabledByDefault("six section"),
58+
selector: "selector",
59+
statement: "state",
60+
branch: "branch",
61+
type: "type",
62+
value: "value",
63+
condition: "condition",
64+
unit: "unit",
65+
// XML, JSX
66+
xmlElement: "element",
67+
xmlBothTags: "tags",
68+
xmlStartTag: "start tag",
69+
xmlEndTag: "end tag",
70+
// LaTeX
71+
part: "part",
72+
chapter: "chapter",
73+
subSection: "subsection",
74+
subSubSection: "subsubsection",
75+
namedParagraph: "paragraph",
76+
subParagraph: "subparagraph",
77+
environment: "environment",
78+
// Talon
79+
command: "command",
80+
// Text-based scope types
81+
character: "char",
82+
word: "word",
83+
token: "token",
84+
identifier: "identifier",
85+
line: "line",
86+
sentence: "sentence",
87+
paragraph: "block",
88+
document: "file",
89+
nonWhitespaceSequence: "paint",
90+
boundedNonWhitespaceSequence: "short paint",
91+
url: "link",
92+
notebookCell: "cell",
93+
94+
string: secret("parse tree string"),
95+
switchStatementSubject: secret("subject"),
96+
},
97+
98+
surroundingPairForceDirection: {
99+
left: "left",
100+
right: "right",
101+
},
102+
103+
simpleModifier: {
104+
excludeInterior: "bounds",
105+
toRawSelection: "just",
106+
leading: "leading",
107+
trailing: "trailing",
108+
keepContentFilter: "content",
109+
keepEmptyFilter: "empty",
110+
inferPreviousMark: "its",
111+
startOf: "start of",
112+
endOf: "end of",
113+
interiorOnly: "inside",
114+
extendThroughStartOf: "head",
115+
extendThroughEndOf: "tail",
116+
everyScope: "every",
117+
},
118+
119+
modifierExtra: {
120+
first: "first",
121+
last: "last",
122+
previous: "previous",
123+
next: "next",
124+
forward: "forward",
125+
backward: "backward",
126+
},
127+
128+
customRegex: {},
129+
};
130+
131+
function disabledByDefault(
132+
...spokenForms: string[]
133+
): DefaultSpokenFormMapEntry {
134+
return {
135+
defaultSpokenForms: spokenForms,
136+
isDisabledByDefault: true,
137+
isSecret: false,
138+
};
139+
}
140+
141+
function secret(...spokenForms: string[]): DefaultSpokenFormMapEntry {
142+
return {
143+
defaultSpokenForms: spokenForms,
144+
isDisabledByDefault: true,
145+
isSecret: true,
146+
};
147+
}
148+
149+
export interface DefaultSpokenFormMapEntry {
150+
defaultSpokenForms: string[];
151+
isDisabledByDefault: boolean;
152+
isSecret: boolean;
153+
}
154+
155+
export type DefaultSpokenFormMap = {
156+
readonly [K in keyof SpokenFormMapKeyTypes]: Readonly<
157+
Record<SpokenFormMapKeyTypes[K], DefaultSpokenFormMapEntry>
158+
>;
159+
};
160+
161+
// FIXME: Don't cast here; need to make our own mapValues with stronger typing
162+
// using tricks from our object.d.ts
163+
export const defaultSpokenFormInfo = mapValues(
164+
defaultSpokenFormMapCore,
165+
(entry) =>
166+
mapValues(entry, (subEntry) =>
167+
typeof subEntry === "string"
168+
? {
169+
defaultSpokenForms: [subEntry],
170+
isDisabledByDefault: false,
171+
isSecret: false,
172+
}
173+
: subEntry,
174+
),
175+
) as DefaultSpokenFormMap;
176+
177+
// FIXME: Don't cast here; need to make our own mapValues with stronger typing
178+
// using tricks from our object.d.ts
179+
export const defaultSpokenFormMap = mapValues(defaultSpokenFormInfo, (entry) =>
180+
mapValues(
181+
entry,
182+
({
183+
defaultSpokenForms,
184+
isDisabledByDefault,
185+
isSecret,
186+
}): SpokenFormMapEntry => ({
187+
spokenForms: isDisabledByDefault ? [] : defaultSpokenForms,
188+
isCustom: false,
189+
defaultSpokenForms,
190+
requiresTalonUpdate: false,
191+
isSecret,
192+
}),
193+
),
194+
) as SpokenFormMap;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
ModifierType,
3+
SimpleScopeTypeType,
4+
SurroundingPairName,
5+
} from "@cursorless/common";
6+
7+
export type SpeakableSurroundingPairName =
8+
| Exclude<SurroundingPairName, "collectionBoundary">
9+
| "whitespace";
10+
11+
export type SimpleModifierType = Exclude<
12+
ModifierType,
13+
| "containingScope"
14+
| "ordinalScope"
15+
| "relativeScope"
16+
| "modifyIfUntyped"
17+
| "cascading"
18+
| "range"
19+
>;
20+
21+
export type ModifierExtra =
22+
| "first"
23+
| "last"
24+
| "previous"
25+
| "next"
26+
| "forward"
27+
| "backward";
28+
29+
export interface SpokenFormMapKeyTypes {
30+
pairedDelimiter: SpeakableSurroundingPairName;
31+
simpleScopeTypeType: SimpleScopeTypeType;
32+
surroundingPairForceDirection: "left" | "right";
33+
simpleModifier: SimpleModifierType;
34+
modifierExtra: ModifierExtra;
35+
customRegex: string;
36+
}
37+
38+
export type SpokenFormType = keyof SpokenFormMapKeyTypes;
39+
40+
export interface SpokenFormMapEntry {
41+
spokenForms: string[];
42+
isCustom: boolean;
43+
defaultSpokenForms: string[];
44+
requiresTalonUpdate: boolean;
45+
isSecret: boolean;
46+
}
47+
48+
export type SpokenFormMap = {
49+
readonly [K in keyof SpokenFormMapKeyTypes]: Readonly<
50+
Record<SpokenFormMapKeyTypes[K], SpokenFormMapEntry>
51+
>;
52+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {
2+
SpokenFormMap,
3+
SpokenFormMapEntry,
4+
SpokenFormMapKeyTypes,
5+
SpokenFormType,
6+
} from "../SpokenFormMap";
7+
8+
export type GeneratorSpokenFormMap = {
9+
readonly [K in keyof SpokenFormMapKeyTypes]: Record<
10+
SpokenFormMapKeyTypes[K],
11+
SingleTermSpokenForm
12+
>;
13+
};
14+
15+
export interface SingleTermSpokenForm {
16+
type: "singleTerm";
17+
spokenForms: SpokenFormMapEntry;
18+
spokenFormType: SpokenFormType;
19+
id: string;
20+
}
21+
22+
export type SpokenFormComponent =
23+
| SingleTermSpokenForm
24+
| string
25+
| SpokenFormComponent[];
26+
27+
export function getGeneratorSpokenForms(
28+
spokenFormMap: SpokenFormMap,
29+
): GeneratorSpokenFormMap {
30+
// FIXME: Don't cast here; need to make our own mapValues with stronger typing
31+
// using tricks from our object.d.ts
32+
return Object.fromEntries(
33+
Object.entries(spokenFormMap).map(([spokenFormType, map]) => [
34+
spokenFormType,
35+
Object.fromEntries(
36+
Object.entries(map).map(([id, spokenForms]) => [
37+
id,
38+
{
39+
type: "singleTerm",
40+
spokenForms,
41+
spokenFormType,
42+
id,
43+
},
44+
]),
45+
),
46+
]),
47+
) as GeneratorSpokenFormMap;
48+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
export class NoSpokenFormError extends Error {
2-
constructor(public reason: string) {
2+
constructor(
3+
public reason: string,
4+
public requiresTalonUpdate: boolean = false,
5+
public isSecret: boolean = false,
6+
) {
37
super(`No spoken form for: ${reason}`);
48
}
59
}

0 commit comments

Comments
 (0)