Skip to content

Commit

Permalink
Allow spoken forms generator to use custom
Browse files Browse the repository at this point in the history
  • Loading branch information
pokey committed Oct 16, 2023
1 parent c2a01d8 commit 3a5c453
Show file tree
Hide file tree
Showing 19 changed files with 924 additions and 544 deletions.
2 changes: 2 additions & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { getKey, splitKey } from "./util/splitKey";
export { hrtimeBigintToSeconds } from "./util/timeUtils";
export * from "./util/walkSync";
export * from "./util/walkAsync";
export * from "./util/camelCaseToAllDown";
export { Notifier } from "./util/Notifier";
export type { Listener } from "./util/Notifier";
export type { TokenHatSplittingMode } from "./ide/types/Configuration";
Expand Down Expand Up @@ -42,6 +43,7 @@ export * from "./types/TextEditorOptions";
export * from "./types/TextLine";
export * from "./types/Token";
export * from "./types/HatTokenMap";
export * from "./types/SpokenForm";
export * from "./util/textFormatters";
export * from "./types/snippet.types";
export * from "./testUtil/fromPlainObject";
Expand Down
14 changes: 14 additions & 0 deletions packages/common/src/types/SpokenForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface SpokenFormSuccess {
type: "success";
preferred: string;
alternatives: string[];
}

export interface SpokenFormError {
type: "error";
reason: string;
requiresTalonUpdate: boolean;
isSecret: boolean;
}

export type SpokenForm = SpokenFormSuccess | SpokenFormError;
7 changes: 7 additions & 0 deletions packages/common/src/util/camelCaseToAllDown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function camelCaseToAllDown(input: string): string {
return input
.replace(/([A-Z])/g, " $1")
.split(" ")
.map((word) => word.toLowerCase())
.join(" ");
}
194 changes: 194 additions & 0 deletions packages/cursorless-engine/src/DefaultSpokenFormMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { mapValues } from "lodash";
import {
SpokenFormMap,
SpokenFormMapEntry,
SpokenFormMapKeyTypes,
} from "./SpokenFormMap";

type DefaultSpokenFormMapDefinition = {
readonly [K in keyof SpokenFormMapKeyTypes]: Readonly<
Record<SpokenFormMapKeyTypes[K], string | DefaultSpokenFormMapEntry>
>;
};

const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = {
pairedDelimiter: {
curlyBrackets: "curly",
angleBrackets: "diamond",
escapedDoubleQuotes: "escaped quad",
escapedSingleQuotes: "escaped twin",
escapedParentheses: "escaped round",
escapedSquareBrackets: "escaped box",
doubleQuotes: "quad",
parentheses: "round",
backtickQuotes: "skis",
squareBrackets: "box",
singleQuotes: "twin",
any: "pair",
string: "string",
whitespace: "void",
},

simpleScopeTypeType: {
argumentOrParameter: "arg",
attribute: "attribute",
functionCall: "call",
functionCallee: "callee",
className: "class name",
class: "class",
comment: "comment",
functionName: "funk name",
namedFunction: "funk",
ifStatement: "if state",
instance: "instance",
collectionItem: "item",
collectionKey: "key",
anonymousFunction: "lambda",
list: "list",
map: "map",
name: "name",
regularExpression: "regex",
section: "section",
sectionLevelOne: disabledByDefault("one section"),
sectionLevelTwo: disabledByDefault("two section"),
sectionLevelThree: disabledByDefault("three section"),
sectionLevelFour: disabledByDefault("four section"),
sectionLevelFive: disabledByDefault("five section"),
sectionLevelSix: disabledByDefault("six section"),
selector: "selector",
statement: "state",
branch: "branch",
type: "type",
value: "value",
condition: "condition",
unit: "unit",
// XML, JSX
xmlElement: "element",
xmlBothTags: "tags",
xmlStartTag: "start tag",
xmlEndTag: "end tag",
// LaTeX
part: "part",
chapter: "chapter",
subSection: "subsection",
subSubSection: "subsubsection",
namedParagraph: "paragraph",
subParagraph: "subparagraph",
environment: "environment",
// Talon
command: "command",
// Text-based scope types
character: "char",
word: "word",
token: "token",
identifier: "identifier",
line: "line",
sentence: "sentence",
paragraph: "block",
document: "file",
nonWhitespaceSequence: "paint",
boundedNonWhitespaceSequence: "short paint",
url: "link",
notebookCell: "cell",

string: secret("parse tree string"),
switchStatementSubject: secret("subject"),
},

surroundingPairForceDirection: {
left: "left",
right: "right",
},

simpleModifier: {
excludeInterior: "bounds",
toRawSelection: "just",
leading: "leading",
trailing: "trailing",
keepContentFilter: "content",
keepEmptyFilter: "empty",
inferPreviousMark: "its",
startOf: "start of",
endOf: "end of",
interiorOnly: "inside",
extendThroughStartOf: "head",
extendThroughEndOf: "tail",
everyScope: "every",
},

modifierExtra: {
first: "first",
last: "last",
previous: "previous",
next: "next",
forward: "forward",
backward: "backward",
},

customRegex: {},
};

function disabledByDefault(
...spokenForms: string[]
): DefaultSpokenFormMapEntry {
return {
defaultSpokenForms: spokenForms,
isDisabledByDefault: true,
isSecret: false,
};
}

function secret(...spokenForms: string[]): DefaultSpokenFormMapEntry {
return {
defaultSpokenForms: spokenForms,
isDisabledByDefault: true,
isSecret: true,
};
}

export interface DefaultSpokenFormMapEntry {
defaultSpokenForms: string[];
isDisabledByDefault: boolean;
isSecret: boolean;
}

export type DefaultSpokenFormMap = {
readonly [K in keyof SpokenFormMapKeyTypes]: Readonly<
Record<SpokenFormMapKeyTypes[K], DefaultSpokenFormMapEntry>
>;
};

// FIXME: Don't cast here; need to make our own mapValues with stronger typing
// using tricks from our object.d.ts
export const defaultSpokenFormInfo = mapValues(
defaultSpokenFormMapCore,
(entry) =>
mapValues(entry, (subEntry) =>
typeof subEntry === "string"
? {
defaultSpokenForms: [subEntry],
isDisabledByDefault: false,
isSecret: false,
}
: subEntry,
),
) as DefaultSpokenFormMap;

// FIXME: Don't cast here; need to make our own mapValues with stronger typing
// using tricks from our object.d.ts
export const defaultSpokenFormMap = mapValues(defaultSpokenFormInfo, (entry) =>
mapValues(
entry,
({
defaultSpokenForms,
isDisabledByDefault,
isSecret,
}): SpokenFormMapEntry => ({
spokenForms: isDisabledByDefault ? [] : defaultSpokenForms,
isCustom: false,
defaultSpokenForms,
requiresTalonUpdate: false,
isSecret,
}),
),
) as SpokenFormMap;
52 changes: 52 additions & 0 deletions packages/cursorless-engine/src/SpokenFormMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
ModifierType,
SimpleScopeTypeType,
SurroundingPairName,
} from "@cursorless/common";

export type SpeakableSurroundingPairName =
| Exclude<SurroundingPairName, "collectionBoundary">
| "whitespace";

export type SimpleModifierType = Exclude<
ModifierType,
| "containingScope"
| "ordinalScope"
| "relativeScope"
| "modifyIfUntyped"
| "cascading"
| "range"
>;

export type ModifierExtra =
| "first"
| "last"
| "previous"
| "next"
| "forward"
| "backward";

export interface SpokenFormMapKeyTypes {
pairedDelimiter: SpeakableSurroundingPairName;
simpleScopeTypeType: SimpleScopeTypeType;
surroundingPairForceDirection: "left" | "right";
simpleModifier: SimpleModifierType;
modifierExtra: ModifierExtra;
customRegex: string;
}

export type SpokenFormType = keyof SpokenFormMapKeyTypes;

export interface SpokenFormMapEntry {
spokenForms: string[];
isCustom: boolean;
defaultSpokenForms: string[];
requiresTalonUpdate: boolean;
isSecret: boolean;
}

export type SpokenFormMap = {
readonly [K in keyof SpokenFormMapKeyTypes]: Readonly<
Record<SpokenFormMapKeyTypes[K], SpokenFormMapEntry>
>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
SpokenFormMap,
SpokenFormMapEntry,
SpokenFormMapKeyTypes,
SpokenFormType,
} from "../SpokenFormMap";

export type GeneratorSpokenFormMap = {
readonly [K in keyof SpokenFormMapKeyTypes]: Record<
SpokenFormMapKeyTypes[K],
SingleTermSpokenForm
>;
};

export interface SingleTermSpokenForm {
type: "singleTerm";
spokenForms: SpokenFormMapEntry;
spokenFormType: SpokenFormType;
id: string;
}

export type SpokenFormComponent =
| SingleTermSpokenForm
| string
| SpokenFormComponent[];

export function getGeneratorSpokenForms(
spokenFormMap: SpokenFormMap,
): GeneratorSpokenFormMap {
// FIXME: Don't cast here; need to make our own mapValues with stronger typing
// using tricks from our object.d.ts
return Object.fromEntries(
Object.entries(spokenFormMap).map(([spokenFormType, map]) => [
spokenFormType,
Object.fromEntries(
Object.entries(map).map(([id, spokenForms]) => [
id,
{
type: "singleTerm",
spokenForms,
spokenFormType,
id,
},
]),
),
]),
) as GeneratorSpokenFormMap;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export class NoSpokenFormError extends Error {
constructor(public reason: string) {
constructor(
public reason: string,
public requiresTalonUpdate: boolean = false,
public isSecret: boolean = false,
) {
super(`No spoken form for: ${reason}`);
}
}
Loading

0 comments on commit 3a5c453

Please sign in to comment.