Skip to content

Commit f2937c9

Browse files
pokeyfidgetingbits
authored and
fidgetingbits
committed
Allow spoken forms generator to use custom (cursorless-dev#1947)
This PR generalizes our spoken form generator to be able to handle custom spoken forms. Note that this PR doesn't contain the machinery to read custom spoken forms from talon; that happens in cursorless-dev#1940. This PR just prepares the api surface of the spoken form generator to handle those custom spoken forms ## Checklist - [ ] 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 fd194ef commit f2937c9

File tree

104 files changed

+1457
-642
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+1457
-642
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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* The spoken form of a command, scope type, etc, that can be spoken
3+
* using a given set of custom or default spoken forms.
4+
*/
5+
export interface SpokenFormSuccess {
6+
type: "success";
7+
8+
/**
9+
* The spoken forms for this entity. These could either be a user's custom
10+
* spoken forms, if we have access to them, or the default spoken forms, if we
11+
* don't, or if we're testing. There will often only be a single entry in this
12+
* array, but there can be multiple if the user has used the `|` syntax in their
13+
* spoken form csv's to define aliases for a single spoken form.
14+
*/
15+
spokenForms: string[];
16+
}
17+
18+
/**
19+
* An error spoken form, which indicates that the given entity (command, scope
20+
* type, etc) cannot be spoken, and the reason why.
21+
*/
22+
export interface SpokenFormError {
23+
type: "error";
24+
25+
/**
26+
* The reason why the entity cannot be spoken.
27+
*/
28+
reason: string;
29+
30+
/**
31+
* If `true`, indicates that the entity wasn't found in the user's Talon spoken
32+
* forms json, and so they need to update their cursorless-talon to get the
33+
* given entity.
34+
*/
35+
requiresTalonUpdate: boolean;
36+
37+
/**
38+
* If `true`, indicates that the entity is only for internal experimentation,
39+
* and should not be exposed to users except within a targeted working group.
40+
*/
41+
isPrivate: boolean;
42+
}
43+
44+
/**
45+
* A spoken form, which can either be a success or an error.
46+
*/
47+
export type SpokenForm = SpokenFormSuccess | SpokenFormError;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Converts a camelCase string to a string with spaces between each word, and
3+
* all words in lowercase.
4+
*
5+
* Example: `camelCaseToAllDown("fooBarBaz")` returns `"foo bar baz"`.
6+
*
7+
* @param input A camelCase string
8+
* @returns The same string, but with spaces between each word, and all words
9+
* in lowercase
10+
*/
11+
export function camelCaseToAllDown(input: string): string {
12+
return input
13+
.replace(/([A-Z])/g, " $1")
14+
.split(" ")
15+
.map((word) => word.toLowerCase())
16+
.join(" ");
17+
}
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 isPrivate: boolean = false,
6+
) {
37
super(`No spoken form for: ${reason}`);
48
}
59
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { SpokenFormMapEntry } from "../spokenForms/SpokenFormMap";
2+
import {
3+
SpokenFormMapKeyTypes,
4+
SpokenFormType,
5+
} from "../spokenForms/SpokenFormType";
6+
7+
/**
8+
* A component of a spoken form used internally during spoken form generation.
9+
* This is a recursive type, so it can contain other spoken form components.
10+
* During the final step of spoken form generation, it is flattened.
11+
*
12+
* FIXME: In the future, we want to replace `string` with something like
13+
* `LiteralSpokenFormComponent` and `SpokenFormComponent[]` with something like
14+
* `SequenceSpokenFormComponent`. We'd also like to avoid throwing
15+
* `NoSpokenFormError` and instead return a `SpokenFormComponent` that
16+
* represents an error. This would allow us to localize errors and still render
17+
* the remainder of the spoken form component.
18+
*/
19+
export type SpokenFormComponent =
20+
| CustomizableSpokenFormComponent
21+
| string
22+
| SpokenFormComponent[];
23+
24+
export interface CustomizableSpokenFormComponentForType<
25+
T extends SpokenFormType,
26+
> {
27+
type: "customizable";
28+
spokenForms: SpokenFormMapEntry;
29+
spokenFormType: T;
30+
id: SpokenFormMapKeyTypes[T];
31+
}
32+
33+
/**
34+
* A customizable spoken form component. This is a spoken form component that
35+
* can be customized by the user. It is used internally during spoken form
36+
* generation.
37+
*/
38+
export type CustomizableSpokenFormComponent = {
39+
[K in SpokenFormType]: CustomizableSpokenFormComponentForType<K>;
40+
}[SpokenFormType];
Lines changed: 14 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,136 +1,10 @@
1-
import {
2-
ModifierType,
3-
SimpleScopeTypeType,
4-
SurroundingPairName,
5-
CompositeKeyMap,
6-
} from "@cursorless/common";
7-
8-
export const modifiers = {
9-
excludeInterior: "bounds",
10-
toRawSelection: "just",
11-
leading: "leading",
12-
trailing: "trailing",
13-
keepContentFilter: "content",
14-
keepEmptyFilter: "empty",
15-
inferPreviousMark: "its",
16-
startOf: "start of",
17-
endOf: "end of",
18-
interiorOnly: "inside",
19-
extendThroughStartOf: "head",
20-
extendThroughEndOf: "tail",
21-
everyScope: "every",
22-
23-
containingScope: null,
24-
ordinalScope: null,
25-
relativeScope: null,
26-
modifyIfUntyped: null,
27-
cascading: null,
28-
range: null,
29-
} as const satisfies Record<ModifierType, string | null>;
30-
31-
export const modifiersExtra = {
32-
first: "first",
33-
last: "last",
34-
previous: "previous",
35-
next: "next",
36-
forward: "forward",
37-
backward: "backward",
38-
};
39-
40-
export const scopeSpokenForms = {
41-
argumentOrParameter: "arg",
42-
attribute: "attribute",
43-
functionCall: "call",
44-
functionCallee: "callee",
45-
className: "class name",
46-
class: "class",
47-
comment: "comment",
48-
functionName: "funk name",
49-
namedFunction: "funk",
50-
ifStatement: "if state",
51-
instance: "instance",
52-
collectionItem: "item",
53-
collectionKey: "key",
54-
anonymousFunction: "lambda",
55-
list: "list",
56-
map: "map",
57-
name: "name",
58-
regularExpression: "regex",
59-
section: "section",
60-
sectionLevelOne: "one section",
61-
sectionLevelTwo: "two section",
62-
sectionLevelThree: "three section",
63-
sectionLevelFour: "four section",
64-
sectionLevelFive: "five section",
65-
sectionLevelSix: "six section",
66-
selector: "selector",
67-
statement: "state",
68-
string: "string",
69-
branch: "branch",
70-
type: "type",
71-
value: "value",
72-
condition: "condition",
73-
unit: "unit",
74-
// XML, JSX
75-
xmlElement: "element",
76-
xmlBothTags: "tags",
77-
xmlStartTag: "start tag",
78-
xmlEndTag: "end tag",
79-
// LaTeX
80-
part: "part",
81-
chapter: "chapter",
82-
subSection: "subsection",
83-
subSubSection: "subsubsection",
84-
namedParagraph: "paragraph",
85-
subParagraph: "subparagraph",
86-
environment: "environment",
87-
// Talon
88-
command: "command",
89-
// Text-based scope types
90-
character: "char",
91-
word: "word",
92-
token: "token",
93-
identifier: "identifier",
94-
line: "line",
95-
sentence: "sentence",
96-
paragraph: "block",
97-
document: "file",
98-
nonWhitespaceSequence: "paint",
99-
boundedNonWhitespaceSequence: "short paint",
100-
url: "link",
101-
notebookCell: "cell",
102-
103-
switchStatementSubject: null,
104-
["private.fieldAccess"]: null,
105-
} as const satisfies Record<SimpleScopeTypeType, string | null>;
106-
107-
type ExtendedSurroundingPairName = SurroundingPairName | "whitespace";
108-
109-
const surroundingPairsSpoken: Record<
110-
ExtendedSurroundingPairName,
111-
string | null
112-
> = {
113-
curlyBrackets: "curly",
114-
angleBrackets: "diamond",
115-
escapedDoubleQuotes: "escaped quad",
116-
escapedSingleQuotes: "escaped twin",
117-
escapedParentheses: "escaped round",
118-
escapedSquareBrackets: "escaped box",
119-
doubleQuotes: "quad",
120-
parentheses: "round",
121-
backtickQuotes: "skis",
122-
squareBrackets: "box",
123-
singleQuotes: "twin",
124-
any: "pair",
125-
string: "string",
126-
whitespace: "void",
127-
128-
// Used internally by the "item" scope type
129-
collectionBoundary: null,
130-
};
1+
import { CompositeKeyMap } from "@cursorless/common";
2+
import { SpeakableSurroundingPairName } from "../../spokenForms/SpokenFormType";
3+
import { SpokenFormComponentMap } from "../getSpokenFormComponentMap";
4+
import { CustomizableSpokenFormComponentForType } from "../SpokenFormComponent";
1315

1326
const surroundingPairsDelimiters: Record<
133-
ExtendedSurroundingPairName,
7+
SpeakableSurroundingPairName,
1348
[string, string] | null
1359
> = {
13610
curlyBrackets: ["{", "}"],
@@ -150,36 +24,19 @@ const surroundingPairsDelimiters: Record<
15024
string: null,
15125
collectionBoundary: null,
15226
};
27+
15328
const surroundingPairDelimiterToName = new CompositeKeyMap<
15429
[string, string],
155-
SurroundingPairName
30+
SpeakableSurroundingPairName
15631
>((pair) => pair);
15732

15833
for (const [name, pair] of Object.entries(surroundingPairsDelimiters)) {
15934
if (pair != null) {
160-
surroundingPairDelimiterToName.set(pair, name as SurroundingPairName);
161-
}
162-
}
163-
164-
export const surroundingPairForceDirections = {
165-
left: "left",
166-
right: "right",
167-
};
168-
169-
/**
170-
* Given a pair name (eg `parentheses`), returns the spoken form of the
171-
* surrounding pair.
172-
* @param surroundingPair The name of the surrounding pair
173-
* @returns The spoken form of the surrounding pair
174-
*/
175-
export function surroundingPairNameToSpokenForm(
176-
surroundingPair: SurroundingPairName,
177-
): string {
178-
const result = surroundingPairsSpoken[surroundingPair];
179-
if (result == null) {
180-
throw Error(`Unknown surrounding pair '${surroundingPair}'`);
35+
surroundingPairDelimiterToName.set(
36+
pair,
37+
name as SpeakableSurroundingPairName,
38+
);
18139
}
182-
return result;
18340
}
18441

18542
/**
@@ -191,12 +48,13 @@ export function surroundingPairNameToSpokenForm(
19148
* @returns The spoken form of the surrounding pair
19249
*/
19350
export function surroundingPairDelimitersToSpokenForm(
51+
spokenFormMap: SpokenFormComponentMap,
19452
left: string,
19553
right: string,
196-
): string {
54+
): CustomizableSpokenFormComponentForType<"pairedDelimiter"> {
19755
const pairName = surroundingPairDelimiterToName.get([left, right]);
19856
if (pairName == null) {
19957
throw Error(`Unknown surrounding pair delimiters '${left} ${right}'`);
20058
}
201-
return surroundingPairNameToSpokenForm(pairName);
59+
return spokenFormMap.pairedDelimiter[pairName];
20260
}

0 commit comments

Comments
 (0)