Skip to content

Commit

Permalink
Move talon spoken forms json to its own file
Browse files Browse the repository at this point in the history
  • Loading branch information
pokey committed Oct 10, 2023
1 parent 6fef105 commit 08fe4e8
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 57 deletions.
22 changes: 8 additions & 14 deletions packages/cursorless-engine/src/CustomSpokenForms.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import {
CustomRegexScopeType,
Disposer,
FileSystem,
Notifier,
showError,
} from "@cursorless/common";
import { isEqual } from "lodash";
import { dirname } from "node:path";
import {
DefaultSpokenFormMapEntry,
defaultSpokenFormInfo,
Expand All @@ -18,10 +16,10 @@ import {
SpokenFormType,
} from "./SpokenFormMap";
import {
NeedsInitialTalonUpdateError,
SpokenFormEntry,
getSpokenFormEntries,
spokenFormsPath,
} from "./scopeProviders/getSpokenFormEntries";
TalonSpokenForms,
} from "./scopeProviders/SpokenFormEntry";
import { ide } from "./singletons/ide.singleton";

const ENTRY_TYPES = [
Expand Down Expand Up @@ -68,11 +66,9 @@ export class CustomSpokenForms implements SpokenFormMap {
return this.isInitialized_;
}

constructor(fileSystem: FileSystem) {
constructor(private talonSpokenForms: TalonSpokenForms) {
this.disposer.push(
fileSystem.watch(dirname(spokenFormsPath), () =>
this.updateSpokenFormMaps(),
),
talonSpokenForms.onDidChange(() => this.updateSpokenFormMaps()),
);

this.updateSpokenFormMaps();
Expand All @@ -88,13 +84,11 @@ export class CustomSpokenForms implements SpokenFormMap {
private async updateSpokenFormMaps(): Promise<void> {
let entries: SpokenFormEntry[];
try {
entries = await getSpokenFormEntries();
entries = await this.talonSpokenForms.getSpokenFormEntries();
} catch (err) {
if ((err as any)?.code === "ENOENT") {
if (err instanceof NeedsInitialTalonUpdateError) {
// Handle case where spokenForms.json doesn't exist yet
console.log(
`Custom spoken forms file not found at ${spokenFormsPath}. Using default spoken forms.`,
);
console.log(err.message);
this.needsInitialTalonUpdate_ = true;
this.notifier.notifyListeners();
} else {
Expand Down
5 changes: 4 additions & 1 deletion packages/cursorless-engine/src/cursorlessEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ScopeRangeWatcher } from "./scopeProviders/ScopeRangeWatcher";
import { ScopeSupportChecker } from "./scopeProviders/ScopeSupportChecker";
import { ScopeSupportWatcher } from "./scopeProviders/ScopeSupportWatcher";
import { injectIde } from "./singletons/ide.singleton";
import { TalonSpokenFormsJsonReader } from "./scopeProviders/getSpokenFormEntries";

export function createCursorlessEngine(
treeSitter: TreeSitter,
Expand Down Expand Up @@ -56,8 +57,10 @@ export function createCursorlessEngine(

const languageDefinitions = new LanguageDefinitions(fileSystem, treeSitter);

const talonSpokenForms = new TalonSpokenFormsJsonReader(fileSystem);

const customSpokenFormGenerator = new CustomSpokenFormGeneratorImpl(
fileSystem,
talonSpokenForms,
);

ide.disposeOnExit(rangeUpdater, languageDefinitions, hatTokenMap, debug);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
CommandComplete,
Disposer,
FileSystem,
Listener,
ScopeType,
} from "@cursorless/common";
import { SpokenFormGenerator } from ".";
import { CustomSpokenFormGenerator } from "..";
import { CustomSpokenForms } from "../CustomSpokenForms";
import { TalonSpokenForms } from "../scopeProviders/SpokenFormEntry";

export class CustomSpokenFormGeneratorImpl
implements CustomSpokenFormGenerator
Expand All @@ -16,8 +16,8 @@ export class CustomSpokenFormGeneratorImpl
private spokenFormGenerator: SpokenFormGenerator;
private disposer = new Disposer();

constructor(fileSystem: FileSystem) {
this.customSpokenForms = new CustomSpokenForms(fileSystem);
constructor(talonSpokenForms: TalonSpokenForms) {
this.customSpokenForms = new CustomSpokenForms(talonSpokenForms);
this.spokenFormGenerator = new SpokenFormGenerator(this.customSpokenForms);
this.disposer.push(
this.customSpokenForms.onDidChangeCustomSpokenForms(() => {
Expand Down
37 changes: 37 additions & 0 deletions packages/cursorless-engine/src/scopeProviders/SpokenFormEntry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Notifier, SimpleScopeTypeType } from "@cursorless/common";
import { SpeakableSurroundingPairName } from "../SpokenFormMap";

export interface TalonSpokenForms {
getSpokenFormEntries(): Promise<SpokenFormEntry[]>;
onDidChange: Notifier["registerListener"];
}

export interface CustomRegexSpokenFormEntry {
type: "customRegex";
id: string;
spokenForms: string[];
}

export interface PairedDelimiterSpokenFormEntry {
type: "pairedDelimiter";
id: SpeakableSurroundingPairName;
spokenForms: string[];
}

export interface SimpleScopeTypeTypeSpokenFormEntry {
type: "simpleScopeTypeType";
id: SimpleScopeTypeType;
spokenForms: string[];
}

export type SpokenFormEntry =
| CustomRegexSpokenFormEntry
| PairedDelimiterSpokenFormEntry
| SimpleScopeTypeTypeSpokenFormEntry;

export class NeedsInitialTalonUpdateError extends Error {
constructor(message: string) {
super(message);
this.name = "NeedsInitialTalonUpdateError";
}
}
112 changes: 73 additions & 39 deletions packages/cursorless-engine/src/scopeProviders/getSpokenFormEntries.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,87 @@
import { LATEST_VERSION, SimpleScopeTypeType } from "@cursorless/common";
import { readFile } from "fs/promises";
import { homedir } from "os";
import { SpeakableSurroundingPairName } from "../SpokenFormMap";
import * as path from "path";
import {
Disposer,
FileSystem,
LATEST_VERSION,
Notifier,
isTesting,
} from "@cursorless/common";
import * as crypto from "crypto";
import { mkdir, readFile } from "fs/promises";
import * as os from "os";

export const spokenFormsPath = path.join(
homedir(),
".cursorless",
"spokenForms.json",
);
import * as path from "path";
import {
NeedsInitialTalonUpdateError,
SpokenFormEntry,
TalonSpokenForms,
} from "./SpokenFormEntry";

export interface CustomRegexSpokenFormEntry {
type: "customRegex";
id: string;
spokenForms: string[];
interface TalonSpokenFormsPayload {
version: number;
entries: SpokenFormEntry[];
}

export interface PairedDelimiterSpokenFormEntry {
type: "pairedDelimiter";
id: SpeakableSurroundingPairName;
spokenForms: string[];
}
export class TalonSpokenFormsJsonReader implements TalonSpokenForms {
private disposer = new Disposer();
private notifier = new Notifier();
private spokenFormsPath;

export interface SimpleScopeTypeTypeSpokenFormEntry {
type: "simpleScopeTypeType";
id: SimpleScopeTypeType;
spokenForms: string[];
}
constructor(private fileSystem: FileSystem) {
const cursorlessDir = isTesting()
? path.join(os.tmpdir(), crypto.randomBytes(16).toString("hex"))
: path.join(os.homedir(), ".cursorless");

export type SpokenFormEntry =
| CustomRegexSpokenFormEntry
| PairedDelimiterSpokenFormEntry
| SimpleScopeTypeTypeSpokenFormEntry;
this.spokenFormsPath = path.join(cursorlessDir, "spokenForms.json");

export async function getSpokenFormEntries(): Promise<SpokenFormEntry[]> {
const payload = JSON.parse(await readFile(spokenFormsPath, "utf-8"));
this.init();
}

private async init() {
const parentDir = path.dirname(this.spokenFormsPath);
await mkdir(parentDir, { recursive: true });
this.disposer.push(
this.fileSystem.watch(parentDir, () => this.notifier.notifyListeners()),
);
}

/**
* This assignment is to ensure that the compiler will error if we forget to
* handle spokenForms.json when we bump the command version.
* Registers a callback to be run when the 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
*/
const latestCommandVersion: 6 = LATEST_VERSION;
onDidChange = this.notifier.registerListener;

if (payload.version !== latestCommandVersion) {
// In the future, we'll need to handle migrations. Not sure exactly how yet.
throw new Error(
`Invalid spoken forms version. Expected ${LATEST_VERSION} but got ${payload.version}`,
);
async getSpokenFormEntries(): Promise<SpokenFormEntry[]> {
let payload: TalonSpokenFormsPayload;
try {
payload = JSON.parse(await readFile(this.spokenFormsPath, "utf-8"));
} catch (err) {
if ((err as any)?.code === "ENOENT") {
throw new NeedsInitialTalonUpdateError(
`Custom spoken forms file not found at ${this.spokenFormsPath}. Using default spoken forms.`,
);
}

throw err;
}

/**
* This assignment is to ensure that the compiler will error if we forget to
* handle spokenForms.json when we bump the command version.
*/
const latestCommandVersion: 6 = LATEST_VERSION;

if (payload.version !== latestCommandVersion) {
// In the future, we'll need to handle migrations. Not sure exactly how yet.
throw new Error(
`Invalid spoken forms version. Expected ${LATEST_VERSION} but got ${payload.version}`,
);
}

return payload.entries;
}

return payload.entries;
dispose() {
this.disposer.dispose();
}
}

0 comments on commit 08fe4e8

Please sign in to comment.