-
-
Notifications
You must be signed in to change notification settings - Fork 96
Expand file tree
/
Copy pathCustomSpokenForms.ts
More file actions
180 lines (159 loc) · 5.74 KB
/
Copy pathCustomSpokenForms.ts
File metadata and controls
180 lines (159 loc) · 5.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import type {
CustomRegexScopeType,
Disposable,
SpokenFormEntry,
SpokenFormMapKeyTypes,
SpokenFormType,
TalonSpokenForms,
} from "@cursorless/common";
import {
DisabledCustomSpokenFormsError,
NeedsInitialTalonUpdateError,
Notifier,
SUPPORTED_ENTRY_TYPES,
showError,
} from "@cursorless/common";
import { isEqual } from "lodash-es";
import { ide } from "../singletons/ide.singleton";
import type { SpokenFormMap, SpokenFormMapEntry } from "./SpokenFormMap";
import {
defaultSpokenFormInfoMap,
defaultSpokenFormMap,
} from "./defaultSpokenFormMap";
import type { DefaultSpokenFormMapEntry } from "./defaultSpokenFormMap.types";
type Writable<T> = {
-readonly [K in keyof T]: T[K];
};
/**
* Maintains a {@link SpokenFormMap} containing the users's custom spoken forms. If
* for some reason, the custom spoken forms cannot be loaded, the default spoken
* forms will be used instead. We currently only support getting custom spoken
* forms for a subset of all customizable spoken forms.
*/
export class CustomSpokenForms {
private disposable: Disposable;
private notifier = new Notifier();
/**
* A promise that resolves when the custom spoken forms have been loaded.
*/
public readonly customSpokenFormsInitialized: Promise<void>;
private spokenFormMap_: Writable<SpokenFormMap> = { ...defaultSpokenFormMap };
get spokenFormMap(): SpokenFormMap {
return this.spokenFormMap_;
}
private needsInitialTalonUpdate_: boolean | undefined;
/**
* If `true`, indicates they need to update their Talon files to get the
* machinery used to share spoken forms from Talon to the VSCode extension.
*/
get needsInitialTalonUpdate() {
return this.needsInitialTalonUpdate_;
}
constructor(private talonSpokenForms: TalonSpokenForms) {
this.disposable = talonSpokenForms.onDidChange(() =>
this.updateSpokenFormMaps(),
);
this.customSpokenFormsInitialized = this.updateSpokenFormMaps();
}
/**
* Registers a callback to be run when the custom spoken forms change.
* @param callback The callback to run when the scope ranges change
* @returns A {@link Disposable} which will stop the callback from running
*/
onDidChangeCustomSpokenForms = this.notifier.registerListener;
private async updateSpokenFormMaps(): Promise<void> {
let allCustomEntries: SpokenFormEntry[];
try {
allCustomEntries = await this.talonSpokenForms.getSpokenFormEntries();
if (allCustomEntries.length === 0) {
throw new Error("Custom spoken forms list empty");
}
} catch (err) {
if (err instanceof NeedsInitialTalonUpdateError) {
// Handle case where spokenForms.json doesn't exist yet
this.needsInitialTalonUpdate_ = true;
} else if (err instanceof DisabledCustomSpokenFormsError) {
// Do nothing: this ide doesn't currently support custom spoken forms
} else {
console.error("Error loading custom spoken forms", err);
const msg = (err as Error).message.replace(/\.$/, "");
void showError(
ide().messages,
"CustomSpokenForms.updateSpokenFormMaps",
`Error loading custom spoken forms: ${msg}. Falling back to default spoken forms.`,
);
}
this.spokenFormMap_ = { ...defaultSpokenFormMap };
this.notifier.notifyListeners();
return;
}
for (const entryType of SUPPORTED_ENTRY_TYPES) {
updateEntriesForType(
this.spokenFormMap_,
entryType,
defaultSpokenFormInfoMap[entryType],
Object.fromEntries(
allCustomEntries
.filter((entry) => entry.type === entryType)
.map(({ id, spokenForms }) => [id, spokenForms]),
),
);
}
this.notifier.notifyListeners();
}
getCustomRegexScopeTypes(): CustomRegexScopeType[] {
return Object.keys(this.spokenFormMap_.customRegex).map((regex) => ({
type: "customRegex",
regex,
}));
}
dispose() {
this.disposable.dispose();
}
}
function updateEntriesForType<T extends SpokenFormType>(
spokenFormMapToUpdate: Writable<SpokenFormMap>,
key: T,
defaultEntries: Partial<
Record<SpokenFormMapKeyTypes[T], DefaultSpokenFormMapEntry>
>,
customEntries: Partial<Record<SpokenFormMapKeyTypes[T], string[]>>,
) {
/**
* The ids of the entries to include in the spoken form map. We need a
* union of the ids from the default entry and the custom entry. The custom
* entry could be missing private entries, or it could be missing entries
* because the Talon side is old. The default entry could be missing entries
* like custom regexes, where the user can create arbitrary ids.
*/
const ids = Array.from(
new Set([...Object.keys(defaultEntries), ...Object.keys(customEntries)]),
) as SpokenFormMapKeyTypes[T][];
const obj: Partial<Record<SpokenFormMapKeyTypes[T], SpokenFormMapEntry>> = {};
for (const id of ids) {
const { defaultSpokenForms = [], isPrivate = false } =
defaultEntries[id] ?? {};
const customSpokenForms = customEntries[id];
obj[id] =
customSpokenForms == null
? // No entry for the given id. This either means that the user needs to
// update Talon, or it's a private spoken form.
{
defaultSpokenForms,
spokenForms: [],
// If it's not a private spoken form, then it's a new scope type
requiresTalonUpdate: !isPrivate,
isCustom: false,
isPrivate,
}
: // We have an entry for the given id
{
defaultSpokenForms,
spokenForms: customSpokenForms,
requiresTalonUpdate: false,
isCustom: !isEqual(defaultSpokenForms, customSpokenForms),
isPrivate,
};
}
spokenFormMapToUpdate[key] = obj as SpokenFormMap[T];
}