Skip to content

Commit

Permalink
Working custom regex test
Browse files Browse the repository at this point in the history
  • Loading branch information
pokey committed Oct 11, 2023
1 parent 1615d79 commit 42fc629
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 38 deletions.
37 changes: 19 additions & 18 deletions packages/cursorless-engine/src/CustomSpokenForms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,24 @@ const ENTRY_TYPES = [
"pairedDelimiter",
] as const;

type Writable<T> = {
-readonly [K in keyof T]: T[K];
};

/**
* Maintains a list of all scope types and notifies listeners when it changes.
*/
export class CustomSpokenForms implements SpokenFormMap {
export class CustomSpokenForms {
private disposer = new Disposer();
private notifier = new Notifier();

// Initialize to defaults
simpleScopeTypeType = defaultSpokenFormMap.simpleScopeTypeType;
pairedDelimiter = defaultSpokenFormMap.pairedDelimiter;
customRegex = defaultSpokenFormMap.customRegex;
private spokenFormMap_: Writable<SpokenFormMap> = { ...defaultSpokenFormMap };

// FIXME: Get these from Talon
surroundingPairForceDirection =
defaultSpokenFormMap.surroundingPairForceDirection;
simpleModifier = defaultSpokenFormMap.simpleModifier;
modifierExtra = defaultSpokenFormMap.modifierExtra;
get spokenFormMap(): SpokenFormMap {
return this.spokenFormMap_;
}

private isInitialized_ = false;
private customSpokenFormsInitialized_ = false;
private needsInitialTalonUpdate_: boolean | undefined;

/**
Expand All @@ -62,8 +61,8 @@ export class CustomSpokenForms implements SpokenFormMap {
* default spoken forms are currently being used while the custom spoken forms
* are being loaded.
*/
get isInitialized() {
return this.isInitialized_;
get customSpokenFormsInitialized() {
return this.customSpokenFormsInitialized_;
}

constructor(private talonSpokenForms: TalonSpokenForms) {
Expand All @@ -88,9 +87,7 @@ export class CustomSpokenForms implements SpokenFormMap {
} catch (err) {
if (err instanceof NeedsInitialTalonUpdateError) {
// Handle case where spokenForms.json doesn't exist yet
console.log(err.message);
this.needsInitialTalonUpdate_ = true;
this.notifier.notifyListeners();
} else {
console.error("Error loading custom spoken forms", err);
showError(
Expand All @@ -102,6 +99,10 @@ export class CustomSpokenForms implements SpokenFormMap {
);
}

this.spokenFormMap_ = { ...defaultSpokenFormMap };
this.customSpokenFormsInitialized_ = false;
this.notifier.notifyListeners();

return;
}

Expand All @@ -118,7 +119,7 @@ export class CustomSpokenForms implements SpokenFormMap {
const ids = Array.from(
new Set([...Object.keys(defaultEntry), ...Object.keys(entry)]),
);
this[entryType] = Object.fromEntries(
this.spokenFormMap_[entryType] = Object.fromEntries(
ids.map((id): [SpokenFormType, SpokenFormMapEntry] => {
const { defaultSpokenForms = [], isSecret = false } =
defaultEntry[id] ?? {};
Expand Down Expand Up @@ -151,12 +152,12 @@ export class CustomSpokenForms implements SpokenFormMap {
) as any;
}

this.isInitialized_ = true;
this.customSpokenFormsInitialized_ = true;
this.notifier.notifyListeners();
}

getCustomRegexScopeTypes(): CustomRegexScopeType[] {
return Object.keys(this.customRegex).map((regex) => ({
return Object.keys(this.spokenFormMap_.customRegex).map((regex) => ({
type: "customRegex",
regex,
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ export class CustomSpokenFormGeneratorImpl

constructor(talonSpokenForms: TalonSpokenForms) {
this.customSpokenForms = new CustomSpokenForms(talonSpokenForms);
this.spokenFormGenerator = new SpokenFormGenerator(this.customSpokenForms);
this.spokenFormGenerator = new SpokenFormGenerator(
this.customSpokenForms.spokenFormMap,
);
this.disposer.push(
this.customSpokenForms.onDidChangeCustomSpokenForms(() => {
this.spokenFormGenerator = new SpokenFormGenerator(
this.customSpokenForms,
this.customSpokenForms.spokenFormMap,
);
}),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import { ScopeSupportInfo, ScopeSupportLevels } from "@cursorless/common";
import { ScopeType, ScopeTypeInfo } from "@cursorless/common";
import Sinon = require("sinon");
import { assert } from "chai";
import { sleepWithBackoff } from "../../endToEndTestSetup";
import { isEqual } from "lodash";

export function assertCalledWithScopeInfo(
fake: Sinon.SinonSpy<[scopeInfos: ScopeSupportLevels], void>,
expectedScopeInfo: ScopeSupportInfo,
export async function assertCalledWithScopeInfo<T extends ScopeTypeInfo>(
fake: Sinon.SinonSpy<[scopeInfos: T[]], void>,
expectedScopeInfo: T,
) {
await sleepWithBackoff(25);
Sinon.assert.called(fake);
const actualScopeInfo = fake.lastCall.args[0].find(
(scopeInfo) =>
scopeInfo.scopeType.type === expectedScopeInfo.scopeType.type,
const actualScopeInfo = fake.lastCall.args[0].find((scopeInfo) =>
isEqual(scopeInfo.scopeType, expectedScopeInfo.scopeType),
);
assert.isDefined(actualScopeInfo);
assert.deepEqual(actualScopeInfo, expectedScopeInfo);
fake.resetHistory();
}

export async function assertCalledWithoutScopeInfo<T extends ScopeTypeInfo>(
fake: Sinon.SinonSpy<[scopeInfos: T[]], void>,
scopeType: ScopeType,
) {
await sleepWithBackoff(25);
Sinon.assert.called(fake);
const actualScopeInfo = fake.lastCall.args[0].find((scopeInfo) =>
isEqual(scopeInfo.scopeType, scopeType),
);
assert.isUndefined(actualScopeInfo);
fake.resetHistory();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import {
ScopeSupportLevels,
} from "@cursorless/common";
import Sinon = require("sinon");
import { sleepWithBackoff } from "../../endToEndTestSetup";
import { commands } from "vscode";
import { Position, Range, TextDocument, commands } from "vscode";
import { assertCalledWithScopeInfo } from "./assertCalledWithScopeInfo";

/**
Expand All @@ -22,24 +21,35 @@ export async function runBasicScopeInfoTest() {
const disposable = scopeProvider.onDidChangeScopeSupport(fake);

try {
assertCalledWithScopeInfo(fake, unsupported);
await assertCalledWithScopeInfo(fake, unsupported);

await openNewEditor(contents, {
const editor = await openNewEditor("", {
languageId: "typescript",
});
await sleepWithBackoff(25);
await assertCalledWithScopeInfo(fake, supported);

assertCalledWithScopeInfo(fake, present);
await editor.edit((editBuilder) => {
editBuilder.insert(new Position(0, 0), contents);
});
await assertCalledWithScopeInfo(fake, present);

await commands.executeCommand("workbench.action.closeAllEditors");
await sleepWithBackoff(25);
await editor.edit((editBuilder) => {
editBuilder.delete(getDocumentRange(editor.document));
});
await assertCalledWithScopeInfo(fake, supported);

assertCalledWithScopeInfo(fake, unsupported);
await commands.executeCommand("workbench.action.closeAllEditors");
await assertCalledWithScopeInfo(fake, unsupported);
} finally {
disposable.dispose();
}
}

function getDocumentRange(textDocument: TextDocument) {
const { end } = textDocument.lineAt(textDocument.lineCount - 1).range;
return new Range(0, 0, end.line, end.character);
}

const contents = `
function helloWorld() {
Expand All @@ -50,7 +60,10 @@ function getExpectedScope(scopeSupport: ScopeSupport): ScopeSupportInfo {
return {
humanReadableName: "named function",
isLanguageSpecific: true,
iterationScopeSupport: scopeSupport,
iterationScopeSupport:
scopeSupport === ScopeSupport.unsupported
? ScopeSupport.unsupported
: ScopeSupport.supportedAndPresentInEditor,
scopeType: {
type: "namedFunction",
},
Expand All @@ -64,4 +77,5 @@ function getExpectedScope(scopeSupport: ScopeSupport): ScopeSupportInfo {
}

const unsupported = getExpectedScope(ScopeSupport.unsupported);
const supported = getExpectedScope(ScopeSupport.supportedButNotPresentInEditor);
const present = getExpectedScope(ScopeSupport.supportedAndPresentInEditor);
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { getCursorlessApi, openNewEditor } from "@cursorless/vscode-common";
import {
LATEST_VERSION,
ScopeSupport,
ScopeSupportInfo,
ScopeSupportLevels,
ScopeType,
} from "@cursorless/common";
import Sinon = require("sinon");
import {
assertCalledWithScopeInfo,
assertCalledWithoutScopeInfo as assertCalledWithoutScope,
} from "./assertCalledWithScopeInfo";
import { stat, unlink, writeFile } from "fs/promises";
import { sleepWithBackoff } from "../../endToEndTestSetup";

/**
* Tests that the scope provider correctly reports the scope support for a
* simple named function.
*/
export async function runCustomRegexScopeInfoTest() {
const { scopeProvider, spokenFormsJsonPath } = (await getCursorlessApi())
.testHelpers!;
const fake = Sinon.fake<[scopeInfos: ScopeSupportLevels], void>();

const disposable = scopeProvider.onDidChangeScopeSupport(fake);

try {
await assertCalledWithoutScope(fake, scopeType);

await writeFile(
spokenFormsJsonPath,
JSON.stringify(spokenFormJsonContents),
);
await sleepWithBackoff(50);
await assertCalledWithScopeInfo(fake, unsupported);

await openNewEditor(contents);
await assertCalledWithScopeInfo(fake, present);

await unlink(spokenFormsJsonPath);
await sleepWithBackoff(50);
await assertCalledWithoutScope(fake, scopeType);
} finally {
disposable.dispose();

// Delete spokenFormsJsonPath if it exists
try {
await stat(spokenFormsJsonPath);
await unlink(spokenFormsJsonPath);
} catch (e) {
// Do nothing
}
}
}

const contents = `
hello world
`;

const regex = "[a-zA-Z]+";

const spokenFormJsonContents = {
version: LATEST_VERSION,
entries: [
{
type: "customRegex",
id: regex,
spokenForms: ["spaghetti"],
},
],
};

const scopeType: ScopeType = {
type: "customRegex",
regex,
};

function getExpectedScope(scopeSupport: ScopeSupport): ScopeSupportInfo {
return {
humanReadableName: "Regex `[a-zA-Z]+`",
isLanguageSpecific: false,
iterationScopeSupport:
scopeSupport === ScopeSupport.unsupported
? ScopeSupport.unsupported
: ScopeSupport.supportedAndPresentInEditor,
scopeType,
spokenForm: {
alternatives: [],
preferred: "spaghetti",
type: "success",
},
support: scopeSupport,
};
}

const unsupported = getExpectedScope(ScopeSupport.unsupported);
const present = getExpectedScope(ScopeSupport.supportedAndPresentInEditor);
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { endToEndTestSetup } from "../../endToEndTestSetup";
import { asyncSafety } from "@cursorless/common";
import { endToEndTestSetup } from "../../endToEndTestSetup";
import { runBasicScopeInfoTest } from "./runBasicScopeInfoTest";
import { runCustomRegexScopeInfoTest } from "./runCustomRegexScopeInfoTest";

suite("scope provider", async function () {
endToEndTestSetup(this);
Expand All @@ -9,4 +10,8 @@ suite("scope provider", async function () {
"basic",
asyncSafety(() => runBasicScopeInfoTest()),
);
test(
"custom regex",
asyncSafety(() => runCustomRegexScopeInfoTest()),
);
});

0 comments on commit 42fc629

Please sign in to comment.