-
-
Notifications
You must be signed in to change notification settings - Fork 91
Get custom spoken forms from Talon #1940
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
d92c0d2
eb9e422
fa94cc1
9c93fab
c1bd390
0adcc2e
285e614
4d20004
36d027e
3086cc4
a08bf21
37e3481
e2af29e
bb80326
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { Disposable } from "../ide/types/ide.types"; | ||
|
|
||
| /** | ||
| * A class that can be used to dispose of multiple disposables at once. This is | ||
| * useful for managing the lifetime of multiple disposables that are created | ||
| * together. It ensures that if one of the disposables throws an error during | ||
| * disposal, the rest of the disposables will still be disposed. | ||
| */ | ||
| export class Disposer implements Disposable { | ||
| private disposables: Disposable[] = []; | ||
|
|
||
| constructor(...disposables: Disposable[]) { | ||
| this.push(...disposables); | ||
| } | ||
|
|
||
| public push(...disposables: Disposable[]) { | ||
| this.disposables.push(...disposables); | ||
| } | ||
|
|
||
| dispose(): void { | ||
| this.disposables.forEach(({ dispose }) => { | ||
| try { | ||
pokey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| dispose(); | ||
| } catch (e) { | ||
| // do nothing; some of the VSCode disposables misbehave, and we don't | ||
| // want that to prevent us from disposing the rest of the disposables | ||
pokey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { | ||
| CommandComplete, | ||
| Disposer, | ||
| Listener, | ||
| ScopeType, | ||
| } from "@cursorless/common"; | ||
| import { SpokenFormGenerator } from "."; | ||
| import { CustomSpokenFormGenerator } from ".."; | ||
| import { CustomSpokenForms } from "../spokenForms/CustomSpokenForms"; | ||
| import { TalonSpokenForms } from "../scopeProviders/SpokenFormEntry"; | ||
|
|
||
| /** | ||
| * Simple facade that combines the {@link CustomSpokenForms} and | ||
| * {@link SpokenFormGenerator} classes. Its main purpose is to reconstruct the | ||
| * {@link SpokenFormGenerator} when the {@link CustomSpokenForms} change. | ||
| */ | ||
| export class CustomSpokenFormGeneratorImpl | ||
| implements CustomSpokenFormGenerator | ||
| { | ||
| private customSpokenForms: CustomSpokenForms; | ||
| private spokenFormGenerator: SpokenFormGenerator; | ||
| private disposer = new Disposer(); | ||
|
|
||
| constructor(talonSpokenForms: TalonSpokenForms) { | ||
| this.customSpokenForms = new CustomSpokenForms(talonSpokenForms); | ||
| this.spokenFormGenerator = new SpokenFormGenerator( | ||
| this.customSpokenForms.spokenFormMap, | ||
| ); | ||
| this.disposer.push( | ||
| this.customSpokenForms.onDidChangeCustomSpokenForms(() => { | ||
| this.spokenFormGenerator = new SpokenFormGenerator( | ||
| this.customSpokenForms.spokenFormMap, | ||
| ); | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| onDidChangeCustomSpokenForms(listener: Listener<[]>) { | ||
| return this.customSpokenForms.onDidChangeCustomSpokenForms(listener); | ||
| } | ||
|
|
||
| commandToSpokenForm(command: CommandComplete) { | ||
| return this.spokenFormGenerator.processCommand(command); | ||
| } | ||
|
|
||
| scopeTypeToSpokenForm(scopeType: ScopeType) { | ||
| return this.spokenFormGenerator.processScopeType(scopeType); | ||
| } | ||
|
|
||
| getCustomRegexScopeTypes() { | ||
| return this.customSpokenForms.getCustomRegexScopeTypes(); | ||
| } | ||
|
|
||
| get needsInitialTalonUpdate() { | ||
| return this.customSpokenForms.needsInitialTalonUpdate; | ||
| } | ||
|
|
||
| dispose = this.disposer.dispose; | ||
pokey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will move this dir into its own package as part of #1023; for now just making a dir to start collecting these things |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Node common | ||
|
|
||
| This directory contains utilities that are available in a node.js context. |
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to keep this file totally focused on the transport layer (watching a json file). That way we can swap it out for Talon rpc, Rango rpc, etc depending on the context |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,79 @@ | ||||||||||
| import { Disposer, FileSystem, Notifier, isTesting } from "@cursorless/common"; | ||||||||||
| import * as crypto from "crypto"; | ||||||||||
| import { mkdir, readFile } from "fs/promises"; | ||||||||||
| import * as os from "os"; | ||||||||||
|
|
||||||||||
| import * as path from "path"; | ||||||||||
| import { | ||||||||||
| NeedsInitialTalonUpdateError, | ||||||||||
| SpokenFormEntry, | ||||||||||
| TalonSpokenForms, | ||||||||||
| } from "../scopeProviders/SpokenFormEntry"; | ||||||||||
|
|
||||||||||
| interface TalonSpokenFormsPayload { | ||||||||||
| version: number; | ||||||||||
| spokenForms: SpokenFormEntry[]; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const LATEST_SPOKEN_FORMS_JSON_VERSION = 0; | ||||||||||
|
|
||||||||||
| export class TalonSpokenFormsJsonReader implements TalonSpokenForms { | ||||||||||
| private disposer = new Disposer(); | ||||||||||
| private notifier = new Notifier(); | ||||||||||
| public readonly spokenFormsPath; | ||||||||||
|
|
||||||||||
| constructor(private fileSystem: FileSystem) { | ||||||||||
| const cursorlessDir = isTesting() | ||||||||||
| ? path.join(os.tmpdir(), crypto.randomBytes(16).toString("hex")) | ||||||||||
| : path.join(os.homedir(), ".cursorless"); | ||||||||||
pokey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||
|
|
||||||||||
| this.spokenFormsPath = path.join(cursorlessDir, "state.json"); | ||||||||||
|
|
||||||||||
| this.init(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private async init() { | ||||||||||
| const parentDir = path.dirname(this.spokenFormsPath); | ||||||||||
| await mkdir(parentDir, { recursive: true }); | ||||||||||
pokey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||
| this.disposer.push( | ||||||||||
| this.fileSystem.watchDir(parentDir, () => | ||||||||||
| this.notifier.notifyListeners(), | ||||||||||
| ), | ||||||||||
| ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * 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 | ||||||||||
| */ | ||||||||||
| onDidChange = this.notifier.registerListener; | ||||||||||
|
|
||||||||||
| 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") { | ||||||||||
|
||||||||||
| } catch (err) { | |
| if ((err as any)?.code === "ENOENT") { | |
| } catch (err: any) { | |
| if (err?.code === "ENOENT") { |
but the idiomatic thing would be to do an instanceof check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
c1bd390 contains an alternative approach, courtesy of chatgpt. wdyt?
pokey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { Notifier, SimpleScopeTypeType } from "@cursorless/common"; | ||
| import { SpeakableSurroundingPairName } from "../spokenForms/SpokenFormType"; | ||
|
|
||
| /** | ||
| * Interface representing a communication mechanism whereby Talon can provide | ||
| * the user's custom spoken forms to the Cursorless engine. | ||
| */ | ||
| 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"; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.