Skip to content

Allow spoken forms generator to use custom #1947

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

Merged
merged 16 commits into from
Oct 23, 2023
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
47 changes: 47 additions & 0 deletions packages/common/src/types/SpokenForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* The spoken form of a command, scope type, etc, that can be spoken
* using a given set of custom or default spoken forms.
*/
export interface SpokenFormSuccess {
type: "success";

/**
* The spoken forms for this entity. These could either be a user's custom
* spoken forms, if we have access to them, or the default spoken forms, if we
* don't, or if we're testing. There will often only be a single entry in this
* array, but there can be multiple if the user has used the `|` syntax in their
* spoken form csv's to define aliases for a single spoken form.
*/
spokenForms: string[];
}

/**
* An error spoken form, which indicates that the given entity (command, scope
* type, etc) cannot be spoken, and the reason why.
*/
export interface SpokenFormError {
type: "error";

/**
* The reason why the entity cannot be spoken.
*/
reason: string;

/**
* If `true`, indicates that the entity wasn't found in the user's Talon spoken
* forms json, and so they need to update their cursorless-talon to get the
* given entity.
*/
requiresTalonUpdate: boolean;

/**
* If `true`, indicates that the entity is only for internal experimentation,
* and should not be exposed to users except within a targeted working group.
*/
isPrivate: boolean;
}

/**
* A spoken form, which can either be a success or an error.
*/
export type SpokenForm = SpokenFormSuccess | SpokenFormError;
17 changes: 17 additions & 0 deletions packages/common/src/util/camelCaseToAllDown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Converts a camelCase string to a string with spaces between each word, and
* all words in lowercase.
*
* Example: `camelCaseToAllDown("fooBarBaz")` returns `"foo bar baz"`.
*
* @param input A camelCase string
* @returns The same string, but with spaces between each word, and all words
* in lowercase
*/
export function camelCaseToAllDown(input: string): string {
return input
.replace(/([A-Z])/g, " $1")
.split(" ")
.map((word) => word.toLowerCase())
.join(" ");
}
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 isPrivate: boolean = false,
) {
super(`No spoken form for: ${reason}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { SpokenFormMapEntry } from "../spokenForms/SpokenFormMap";
import {
SpokenFormMapKeyTypes,
SpokenFormType,
} from "../spokenForms/SpokenFormType";

/**
* A component of a spoken form used internally during spoken form generation.
* This is a recursive type, so it can contain other spoken form components.
* During the final step of spoken form generation, it is flattened.
*
* FIXME: In the future, we want to replace `string` with something like
* `LiteralSpokenFormComponent` and `SpokenFormComponent[]` with something like
* `SequenceSpokenFormComponent`. We'd also like to avoid throwing
* `NoSpokenFormError` and instead return a `SpokenFormComponent` that
* represents an error. This would allow us to localize errors and still render
* the remainder of the spoken form component.
*/
export type SpokenFormComponent =
| CustomizableSpokenFormComponent
| string
| SpokenFormComponent[];

export interface CustomizableSpokenFormComponentForType<
T extends SpokenFormType,
> {
type: "customizable";
spokenForms: SpokenFormMapEntry;
spokenFormType: T;
id: SpokenFormMapKeyTypes[T];
}

/**
* A customizable spoken form component. This is a spoken form component that
* can be customized by the user. It is used internally during spoken form
* generation.
*/
export type CustomizableSpokenFormComponent = {
[K in SpokenFormType]: CustomizableSpokenFormComponentForType<K>;
}[SpokenFormType];
Original file line number Diff line number Diff line change
@@ -1,136 +1,10 @@
import {
ModifierType,
SimpleScopeTypeType,
SurroundingPairName,
CompositeKeyMap,
} from "@cursorless/common";

export const modifiers = {
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",

containingScope: null,
ordinalScope: null,
relativeScope: null,
modifyIfUntyped: null,
cascading: null,
range: null,
} as const satisfies Record<ModifierType, string | null>;

export const modifiersExtra = {
first: "first",
last: "last",
previous: "previous",
next: "next",
forward: "forward",
backward: "backward",
};

export const scopeSpokenForms = {
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: "one section",
sectionLevelTwo: "two section",
sectionLevelThree: "three section",
sectionLevelFour: "four section",
sectionLevelFive: "five section",
sectionLevelSix: "six section",
selector: "selector",
statement: "state",
string: "string",
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",

switchStatementSubject: null,
["private.fieldAccess"]: null,
} as const satisfies Record<SimpleScopeTypeType, string | null>;

type ExtendedSurroundingPairName = SurroundingPairName | "whitespace";

const surroundingPairsSpoken: Record<
ExtendedSurroundingPairName,
string | null
> = {
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",

// Used internally by the "item" scope type
collectionBoundary: null,
};
import { CompositeKeyMap } from "@cursorless/common";
import { SpeakableSurroundingPairName } from "../../spokenForms/SpokenFormType";
import { SpokenFormComponentMap } from "../getSpokenFormComponentMap";
import { CustomizableSpokenFormComponentForType } from "../SpokenFormComponent";

const surroundingPairsDelimiters: Record<
ExtendedSurroundingPairName,
SpeakableSurroundingPairName,
[string, string] | null
> = {
curlyBrackets: ["{", "}"],
Expand All @@ -150,36 +24,19 @@ const surroundingPairsDelimiters: Record<
string: null,
collectionBoundary: null,
};

const surroundingPairDelimiterToName = new CompositeKeyMap<
[string, string],
SurroundingPairName
SpeakableSurroundingPairName
>((pair) => pair);

for (const [name, pair] of Object.entries(surroundingPairsDelimiters)) {
if (pair != null) {
surroundingPairDelimiterToName.set(pair, name as SurroundingPairName);
}
}

export const surroundingPairForceDirections = {
left: "left",
right: "right",
};

/**
* Given a pair name (eg `parentheses`), returns the spoken form of the
* surrounding pair.
* @param surroundingPair The name of the surrounding pair
* @returns The spoken form of the surrounding pair
*/
export function surroundingPairNameToSpokenForm(
surroundingPair: SurroundingPairName,
): string {
const result = surroundingPairsSpoken[surroundingPair];
if (result == null) {
throw Error(`Unknown surrounding pair '${surroundingPair}'`);
surroundingPairDelimiterToName.set(
pair,
name as SpeakableSurroundingPairName,
);
}
return result;
}

/**
Expand All @@ -191,12 +48,13 @@ export function surroundingPairNameToSpokenForm(
* @returns The spoken form of the surrounding pair
*/
export function surroundingPairDelimitersToSpokenForm(
spokenFormMap: SpokenFormComponentMap,
left: string,
right: string,
): string {
): CustomizableSpokenFormComponentForType<"pairedDelimiter"> {
const pairName = surroundingPairDelimiterToName.get([left, right]);
if (pairName == null) {
throw Error(`Unknown surrounding pair delimiters '${left} ${right}'`);
}
return surroundingPairNameToSpokenForm(pairName);
return spokenFormMap.pairedDelimiter[pairName];
}
Loading