Skip to content

Commit 324f8de

Browse files
committed
Allow spoken forms generator to use custom
1 parent 6c6f3a6 commit 324f8de

File tree

19 files changed

+982
-547
lines changed

19 files changed

+982
-547
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+
isPrivate: 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: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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: isDisabledByDefault("one section"),
53+
sectionLevelTwo: isDisabledByDefault("two section"),
54+
sectionLevelThree: isDisabledByDefault("three section"),
55+
sectionLevelFour: isDisabledByDefault("four section"),
56+
sectionLevelFive: isDisabledByDefault("five section"),
57+
sectionLevelSix: isDisabledByDefault("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+
["private.fieldAccess"]: isPrivate("access"),
95+
string: isPrivate("parse tree string"),
96+
switchStatementSubject: isPrivate("subject"),
97+
},
98+
99+
surroundingPairForceDirection: {
100+
left: "left",
101+
right: "right",
102+
},
103+
104+
simpleModifier: {
105+
excludeInterior: "bounds",
106+
toRawSelection: "just",
107+
leading: "leading",
108+
trailing: "trailing",
109+
keepContentFilter: "content",
110+
keepEmptyFilter: "empty",
111+
inferPreviousMark: "its",
112+
startOf: "start of",
113+
endOf: "end of",
114+
interiorOnly: "inside",
115+
extendThroughStartOf: "head",
116+
extendThroughEndOf: "tail",
117+
everyScope: "every",
118+
},
119+
120+
modifierExtra: {
121+
first: "first",
122+
last: "last",
123+
previous: "previous",
124+
next: "next",
125+
forward: "forward",
126+
backward: "backward",
127+
},
128+
129+
customRegex: {},
130+
};
131+
132+
function disabledByDefault(
133+
...spokenForms: string[]
134+
): DefaultSpokenFormMapEntry {
135+
return {
136+
defaultSpokenForms: spokenForms,
137+
isDisabledByDefault: true,
138+
isPrivate: false,
139+
};
140+
}
141+
142+
function secret(...spokenForms: string[]): DefaultSpokenFormMapEntry {
143+
return {
144+
defaultSpokenForms: spokenForms,
145+
isDisabledByDefault: true,
146+
isPrivate: true,
147+
};
148+
}
149+
150+
export interface DefaultSpokenFormMapEntry {
151+
defaultSpokenForms: string[];
152+
isDisabledByDefault: boolean;
153+
isSecret: boolean;
154+
}
155+
156+
export type DefaultSpokenFormMap = {
157+
readonly [K in keyof SpokenFormMapKeyTypes]: Readonly<
158+
Record<SpokenFormMapKeyTypes[K], DefaultSpokenFormMapEntry>
159+
>;
160+
};
161+
162+
// FIXME: Don't cast here; need to make our own mapValues with stronger typing
163+
// using tricks from our object.d.ts
164+
export const defaultSpokenFormInfo = mapValues(
165+
defaultSpokenFormMapCore,
166+
(entry) =>
167+
mapValues(entry, (subEntry) =>
168+
typeof subEntry === "string"
169+
? {
170+
defaultSpokenForms: [subEntry],
171+
isDisabledByDefault: false,
172+
isPrivate: false,
173+
}
174+
: subEntry,
175+
),
176+
) as DefaultSpokenFormMap;
177+
178+
// FIXME: Don't cast here; need to make our own mapValues with stronger typing
179+
// using tricks from our object.d.ts
180+
export const defaultSpokenFormMap = mapValues(defaultSpokenFormInfo, (entry) =>
181+
mapValues(
182+
entry,
183+
({
184+
defaultSpokenForms,
185+
isDisabledByDefault,
186+
isSecret,
187+
}): SpokenFormMapEntry => ({
188+
spokenForms: isDisabledByDefault ? [] : defaultSpokenForms,
189+
isCustom: false,
190+
defaultSpokenForms,
191+
requiresTalonUpdate: false,
192+
isSecret,
193+
}),
194+
),
195+
) as SpokenFormMap;
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
/**
30+
* This interface is the source of truth for the types used in our spoken form
31+
* map. The keys of this interface are the types of spoken forms that we
32+
* support, eg `simpleScopeTypeType`, `simpleModifier`, etc. The type of each
33+
* key is a disjunction of all identifiers that are allowed for the given type of
34+
* spoken form.
35+
*/
36+
export interface SpokenFormMapKeyTypes {
37+
pairedDelimiter: SpeakableSurroundingPairName;
38+
simpleScopeTypeType: SimpleScopeTypeType;
39+
surroundingPairForceDirection: "left" | "right";
40+
41+
/**
42+
* These modifier types are spoken by directly saying the spoken form for the
43+
* modifier type, unlike the more complex spoken forms such as
44+
* `relativeScope`, which can use various different custom spoken forms such
45+
* as `next`, `previous`, etc.
46+
*/
47+
simpleModifier: SimpleModifierType;
48+
49+
/**
50+
* These are customizable spoken forms used in speaking modifiers, but that
51+
* don't directly correspond to a modifier type. For example, `next` is a
52+
* customizable spoken form that can be used when speaking `relativeScope`
53+
* modifiers, but `next` itself isn't a modifier type.
54+
*/
55+
modifierExtra: ModifierExtra;
56+
customRegex: string;
57+
}
58+
59+
export type SpokenFormType = keyof SpokenFormMapKeyTypes;
60+
61+
export interface SpokenFormMapEntry {
62+
/**
63+
* The spoken forms for this entry. These could either be a user's custom
64+
* spoken forms, if we have access to them, or the default spoken forms, if we
65+
* don't, or if we're testing.
66+
*/
67+
spokenForms: string[];
68+
69+
/**
70+
* If `true`, indicates that the user is not using the default spoken forms
71+
* for this entry.
72+
*/
73+
isCustom: boolean;
74+
75+
/**
76+
* The default spoken forms for this entry.
77+
*/
78+
defaultSpokenForms: string[];
79+
80+
/**
81+
* If `true`, indicates that the entry wasn't found in the user's Talon spoken
82+
* forms json, and so they need to update their cursorless-talon to get the
83+
* given entity.
84+
*/
85+
requiresTalonUpdate: boolean;
86+
87+
/**
88+
* If `true`, indicates that the entry is only for internal experimentation,
89+
* and should not be exposed to users except within a targeted working group.
90+
*/
91+
isPrivate: boolean;
92+
}
93+
94+
/**
95+
* A spoken form map contains information about the spoken forms for all our
96+
* speakable entities, including scope types, paired delimiters, etc. It can
97+
* either contain the user's custom spoken forms, or the default spoken forms,
98+
* if we don't have access to the user's custom spoken forms, or if we're
99+
* testing.
100+
*
101+
* Each key of this map is a type of spoken form, eg `simpleScopeTypeType`, and
102+
* the value is a map of identifiers to {@link SpokenFormMapEntry}s.
103+
*/
104+
export type SpokenFormMap = {
105+
readonly [K in keyof SpokenFormMapKeyTypes]: Readonly<
106+
Record<SpokenFormMapKeyTypes[K], SpokenFormMapEntry>
107+
>;
108+
};
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+
}

0 commit comments

Comments
 (0)