Skip to content

Commit ccde215

Browse files
committed
Move talon spoken forms json to its own file
1 parent 534c172 commit ccde215

File tree

5 files changed

+125
-57
lines changed

5 files changed

+125
-57
lines changed

packages/cursorless-engine/src/CustomSpokenForms.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import {
22
CustomRegexScopeType,
33
Disposer,
4-
FileSystem,
54
Notifier,
65
showError,
76
} from "@cursorless/common";
87
import { isEqual } from "lodash";
9-
import { dirname } from "node:path";
108
import {
119
DefaultSpokenFormMapEntry,
1210
defaultSpokenFormInfo,
@@ -18,10 +16,10 @@ import {
1816
SpokenFormType,
1917
} from "./SpokenFormMap";
2018
import {
19+
NeedsInitialTalonUpdateError,
2120
SpokenFormEntry,
22-
getSpokenFormEntries,
23-
spokenFormsPath,
24-
} from "./scopeProviders/getSpokenFormEntries";
21+
TalonSpokenForms,
22+
} from "./scopeProviders/SpokenFormEntry";
2523
import { ide } from "./singletons/ide.singleton";
2624

2725
const ENTRY_TYPES = [
@@ -68,11 +66,9 @@ export class CustomSpokenForms implements SpokenFormMap {
6866
return this.isInitialized_;
6967
}
7068

71-
constructor(fileSystem: FileSystem) {
69+
constructor(private talonSpokenForms: TalonSpokenForms) {
7270
this.disposer.push(
73-
fileSystem.watch(dirname(spokenFormsPath), () =>
74-
this.updateSpokenFormMaps(),
75-
),
71+
talonSpokenForms.onDidChange(() => this.updateSpokenFormMaps()),
7672
);
7773

7874
this.updateSpokenFormMaps();
@@ -88,13 +84,11 @@ export class CustomSpokenForms implements SpokenFormMap {
8884
private async updateSpokenFormMaps(): Promise<void> {
8985
let entries: SpokenFormEntry[];
9086
try {
91-
entries = await getSpokenFormEntries();
87+
entries = await this.talonSpokenForms.getSpokenFormEntries();
9288
} catch (err) {
93-
if ((err as any)?.code === "ENOENT") {
89+
if (err instanceof NeedsInitialTalonUpdateError) {
9490
// Handle case where spokenForms.json doesn't exist yet
95-
console.log(
96-
`Custom spoken forms file not found at ${spokenFormsPath}. Using default spoken forms.`,
97-
);
91+
console.log(err.message);
9892
this.needsInitialTalonUpdate_ = true;
9993
this.notifier.notifyListeners();
10094
} else {

packages/cursorless-engine/src/cursorlessEngine.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ScopeRangeWatcher } from "./scopeProviders/ScopeRangeWatcher";
2525
import { ScopeSupportChecker } from "./scopeProviders/ScopeSupportChecker";
2626
import { ScopeSupportWatcher } from "./scopeProviders/ScopeSupportWatcher";
2727
import { injectIde } from "./singletons/ide.singleton";
28+
import { TalonSpokenFormsJsonReader } from "./scopeProviders/getSpokenFormEntries";
2829

2930
export function createCursorlessEngine(
3031
treeSitter: TreeSitter,
@@ -56,8 +57,10 @@ export function createCursorlessEngine(
5657

5758
const languageDefinitions = new LanguageDefinitions(fileSystem, treeSitter);
5859

60+
const talonSpokenForms = new TalonSpokenFormsJsonReader(fileSystem);
61+
5962
const customSpokenFormGenerator = new CustomSpokenFormGeneratorImpl(
60-
fileSystem,
63+
talonSpokenForms,
6164
);
6265

6366
ide.disposeOnExit(rangeUpdater, languageDefinitions, hatTokenMap, debug);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import {
22
CommandComplete,
33
Disposer,
4-
FileSystem,
54
Listener,
65
ScopeType,
76
} from "@cursorless/common";
87
import { SpokenFormGenerator } from ".";
98
import { CustomSpokenFormGenerator } from "..";
109
import { CustomSpokenForms } from "../CustomSpokenForms";
10+
import { TalonSpokenForms } from "../scopeProviders/SpokenFormEntry";
1111

1212
export class CustomSpokenFormGeneratorImpl
1313
implements CustomSpokenFormGenerator
@@ -16,8 +16,8 @@ export class CustomSpokenFormGeneratorImpl
1616
private spokenFormGenerator: SpokenFormGenerator;
1717
private disposer = new Disposer();
1818

19-
constructor(fileSystem: FileSystem) {
20-
this.customSpokenForms = new CustomSpokenForms(fileSystem);
19+
constructor(talonSpokenForms: TalonSpokenForms) {
20+
this.customSpokenForms = new CustomSpokenForms(talonSpokenForms);
2121
this.spokenFormGenerator = new SpokenFormGenerator(this.customSpokenForms);
2222
this.disposer.push(
2323
this.customSpokenForms.onDidChangeCustomSpokenForms(() => {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Notifier, SimpleScopeTypeType } from "@cursorless/common";
2+
import { SpeakableSurroundingPairName } from "../SpokenFormMap";
3+
4+
export interface TalonSpokenForms {
5+
getSpokenFormEntries(): Promise<SpokenFormEntry[]>;
6+
onDidChange: Notifier["registerListener"];
7+
}
8+
9+
export interface CustomRegexSpokenFormEntry {
10+
type: "customRegex";
11+
id: string;
12+
spokenForms: string[];
13+
}
14+
15+
export interface PairedDelimiterSpokenFormEntry {
16+
type: "pairedDelimiter";
17+
id: SpeakableSurroundingPairName;
18+
spokenForms: string[];
19+
}
20+
21+
export interface SimpleScopeTypeTypeSpokenFormEntry {
22+
type: "simpleScopeTypeType";
23+
id: SimpleScopeTypeType;
24+
spokenForms: string[];
25+
}
26+
27+
export type SpokenFormEntry =
28+
| CustomRegexSpokenFormEntry
29+
| PairedDelimiterSpokenFormEntry
30+
| SimpleScopeTypeTypeSpokenFormEntry;
31+
32+
export class NeedsInitialTalonUpdateError extends Error {
33+
constructor(message: string) {
34+
super(message);
35+
this.name = "NeedsInitialTalonUpdateError";
36+
}
37+
}
Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,87 @@
1-
import { LATEST_VERSION, SimpleScopeTypeType } from "@cursorless/common";
2-
import { readFile } from "fs/promises";
3-
import { homedir } from "os";
4-
import { SpeakableSurroundingPairName } from "../SpokenFormMap";
5-
import * as path from "path";
1+
import {
2+
Disposer,
3+
FileSystem,
4+
LATEST_VERSION,
5+
Notifier,
6+
isTesting,
7+
} from "@cursorless/common";
8+
import * as crypto from "crypto";
9+
import { mkdir, readFile } from "fs/promises";
10+
import * as os from "os";
611

7-
export const spokenFormsPath = path.join(
8-
homedir(),
9-
".cursorless",
10-
"spokenForms.json",
11-
);
12+
import * as path from "path";
13+
import {
14+
NeedsInitialTalonUpdateError,
15+
SpokenFormEntry,
16+
TalonSpokenForms,
17+
} from "./SpokenFormEntry";
1218

13-
export interface CustomRegexSpokenFormEntry {
14-
type: "customRegex";
15-
id: string;
16-
spokenForms: string[];
19+
interface TalonSpokenFormsPayload {
20+
version: number;
21+
entries: SpokenFormEntry[];
1722
}
1823

19-
export interface PairedDelimiterSpokenFormEntry {
20-
type: "pairedDelimiter";
21-
id: SpeakableSurroundingPairName;
22-
spokenForms: string[];
23-
}
24+
export class TalonSpokenFormsJsonReader implements TalonSpokenForms {
25+
private disposer = new Disposer();
26+
private notifier = new Notifier();
27+
private spokenFormsPath;
2428

25-
export interface SimpleScopeTypeTypeSpokenFormEntry {
26-
type: "simpleScopeTypeType";
27-
id: SimpleScopeTypeType;
28-
spokenForms: string[];
29-
}
29+
constructor(private fileSystem: FileSystem) {
30+
const cursorlessDir = isTesting()
31+
? path.join(os.tmpdir(), crypto.randomBytes(16).toString("hex"))
32+
: path.join(os.homedir(), ".cursorless");
3033

31-
export type SpokenFormEntry =
32-
| CustomRegexSpokenFormEntry
33-
| PairedDelimiterSpokenFormEntry
34-
| SimpleScopeTypeTypeSpokenFormEntry;
34+
this.spokenFormsPath = path.join(cursorlessDir, "spokenForms.json");
3535

36-
export async function getSpokenFormEntries(): Promise<SpokenFormEntry[]> {
37-
const payload = JSON.parse(await readFile(spokenFormsPath, "utf-8"));
36+
this.init();
37+
}
38+
39+
private async init() {
40+
const parentDir = path.dirname(this.spokenFormsPath);
41+
await mkdir(parentDir, { recursive: true });
42+
this.disposer.push(
43+
this.fileSystem.watch(parentDir, () => this.notifier.notifyListeners()),
44+
);
45+
}
3846

3947
/**
40-
* This assignment is to ensure that the compiler will error if we forget to
41-
* handle spokenForms.json when we bump the command version.
48+
* Registers a callback to be run when the spoken forms change.
49+
* @param callback The callback to run when the scope ranges change
50+
* @returns A {@link Disposable} which will stop the callback from running
4251
*/
43-
const latestCommandVersion: 6 = LATEST_VERSION;
52+
onDidChange = this.notifier.registerListener;
4453

45-
if (payload.version !== latestCommandVersion) {
46-
// In the future, we'll need to handle migrations. Not sure exactly how yet.
47-
throw new Error(
48-
`Invalid spoken forms version. Expected ${LATEST_VERSION} but got ${payload.version}`,
49-
);
54+
async getSpokenFormEntries(): Promise<SpokenFormEntry[]> {
55+
let payload: TalonSpokenFormsPayload;
56+
try {
57+
payload = JSON.parse(await readFile(this.spokenFormsPath, "utf-8"));
58+
} catch (err) {
59+
if ((err as any)?.code === "ENOENT") {
60+
throw new NeedsInitialTalonUpdateError(
61+
`Custom spoken forms file not found at ${this.spokenFormsPath}. Using default spoken forms.`,
62+
);
63+
}
64+
65+
throw err;
66+
}
67+
68+
/**
69+
* This assignment is to ensure that the compiler will error if we forget to
70+
* handle spokenForms.json when we bump the command version.
71+
*/
72+
const latestCommandVersion: 6 = LATEST_VERSION;
73+
74+
if (payload.version !== latestCommandVersion) {
75+
// In the future, we'll need to handle migrations. Not sure exactly how yet.
76+
throw new Error(
77+
`Invalid spoken forms version. Expected ${LATEST_VERSION} but got ${payload.version}`,
78+
);
79+
}
80+
81+
return payload.entries;
5082
}
5183

52-
return payload.entries;
84+
dispose() {
85+
this.disposer.dispose();
86+
}
5387
}

0 commit comments

Comments
 (0)