Skip to content

Commit 42fc629

Browse files
committed
Working custom regex test
1 parent 1615d79 commit 42fc629

File tree

6 files changed

+173
-38
lines changed

6 files changed

+173
-38
lines changed

packages/cursorless-engine/src/CustomSpokenForms.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,24 @@ const ENTRY_TYPES = [
2828
"pairedDelimiter",
2929
] as const;
3030

31+
type Writable<T> = {
32+
-readonly [K in keyof T]: T[K];
33+
};
34+
3135
/**
3236
* Maintains a list of all scope types and notifies listeners when it changes.
3337
*/
34-
export class CustomSpokenForms implements SpokenFormMap {
38+
export class CustomSpokenForms {
3539
private disposer = new Disposer();
3640
private notifier = new Notifier();
3741

38-
// Initialize to defaults
39-
simpleScopeTypeType = defaultSpokenFormMap.simpleScopeTypeType;
40-
pairedDelimiter = defaultSpokenFormMap.pairedDelimiter;
41-
customRegex = defaultSpokenFormMap.customRegex;
42+
private spokenFormMap_: Writable<SpokenFormMap> = { ...defaultSpokenFormMap };
4243

43-
// FIXME: Get these from Talon
44-
surroundingPairForceDirection =
45-
defaultSpokenFormMap.surroundingPairForceDirection;
46-
simpleModifier = defaultSpokenFormMap.simpleModifier;
47-
modifierExtra = defaultSpokenFormMap.modifierExtra;
44+
get spokenFormMap(): SpokenFormMap {
45+
return this.spokenFormMap_;
46+
}
4847

49-
private isInitialized_ = false;
48+
private customSpokenFormsInitialized_ = false;
5049
private needsInitialTalonUpdate_: boolean | undefined;
5150

5251
/**
@@ -62,8 +61,8 @@ export class CustomSpokenForms implements SpokenFormMap {
6261
* default spoken forms are currently being used while the custom spoken forms
6362
* are being loaded.
6463
*/
65-
get isInitialized() {
66-
return this.isInitialized_;
64+
get customSpokenFormsInitialized() {
65+
return this.customSpokenFormsInitialized_;
6766
}
6867

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

102+
this.spokenFormMap_ = { ...defaultSpokenFormMap };
103+
this.customSpokenFormsInitialized_ = false;
104+
this.notifier.notifyListeners();
105+
105106
return;
106107
}
107108

@@ -118,7 +119,7 @@ export class CustomSpokenForms implements SpokenFormMap {
118119
const ids = Array.from(
119120
new Set([...Object.keys(defaultEntry), ...Object.keys(entry)]),
120121
);
121-
this[entryType] = Object.fromEntries(
122+
this.spokenFormMap_[entryType] = Object.fromEntries(
122123
ids.map((id): [SpokenFormType, SpokenFormMapEntry] => {
123124
const { defaultSpokenForms = [], isSecret = false } =
124125
defaultEntry[id] ?? {};
@@ -151,12 +152,12 @@ export class CustomSpokenForms implements SpokenFormMap {
151152
) as any;
152153
}
153154

154-
this.isInitialized_ = true;
155+
this.customSpokenFormsInitialized_ = true;
155156
this.notifier.notifyListeners();
156157
}
157158

158159
getCustomRegexScopeTypes(): CustomRegexScopeType[] {
159-
return Object.keys(this.customRegex).map((regex) => ({
160+
return Object.keys(this.spokenFormMap_.customRegex).map((regex) => ({
160161
type: "customRegex",
161162
regex,
162163
}));

packages/cursorless-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ export class CustomSpokenFormGeneratorImpl
1818

1919
constructor(talonSpokenForms: TalonSpokenForms) {
2020
this.customSpokenForms = new CustomSpokenForms(talonSpokenForms);
21-
this.spokenFormGenerator = new SpokenFormGenerator(this.customSpokenForms);
21+
this.spokenFormGenerator = new SpokenFormGenerator(
22+
this.customSpokenForms.spokenFormMap,
23+
);
2224
this.disposer.push(
2325
this.customSpokenForms.onDidChangeCustomSpokenForms(() => {
2426
this.spokenFormGenerator = new SpokenFormGenerator(
25-
this.customSpokenForms,
27+
this.customSpokenForms.spokenFormMap,
2628
);
2729
}),
2830
);
Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
1-
import { ScopeSupportInfo, ScopeSupportLevels } from "@cursorless/common";
1+
import { ScopeType, ScopeTypeInfo } from "@cursorless/common";
22
import Sinon = require("sinon");
33
import { assert } from "chai";
4+
import { sleepWithBackoff } from "../../endToEndTestSetup";
5+
import { isEqual } from "lodash";
46

5-
export function assertCalledWithScopeInfo(
6-
fake: Sinon.SinonSpy<[scopeInfos: ScopeSupportLevels], void>,
7-
expectedScopeInfo: ScopeSupportInfo,
7+
export async function assertCalledWithScopeInfo<T extends ScopeTypeInfo>(
8+
fake: Sinon.SinonSpy<[scopeInfos: T[]], void>,
9+
expectedScopeInfo: T,
810
) {
11+
await sleepWithBackoff(25);
912
Sinon.assert.called(fake);
10-
const actualScopeInfo = fake.lastCall.args[0].find(
11-
(scopeInfo) =>
12-
scopeInfo.scopeType.type === expectedScopeInfo.scopeType.type,
13+
const actualScopeInfo = fake.lastCall.args[0].find((scopeInfo) =>
14+
isEqual(scopeInfo.scopeType, expectedScopeInfo.scopeType),
1315
);
1416
assert.isDefined(actualScopeInfo);
1517
assert.deepEqual(actualScopeInfo, expectedScopeInfo);
1618
fake.resetHistory();
1719
}
20+
21+
export async function assertCalledWithoutScopeInfo<T extends ScopeTypeInfo>(
22+
fake: Sinon.SinonSpy<[scopeInfos: T[]], void>,
23+
scopeType: ScopeType,
24+
) {
25+
await sleepWithBackoff(25);
26+
Sinon.assert.called(fake);
27+
const actualScopeInfo = fake.lastCall.args[0].find((scopeInfo) =>
28+
isEqual(scopeInfo.scopeType, scopeType),
29+
);
30+
assert.isUndefined(actualScopeInfo);
31+
fake.resetHistory();
32+
}

packages/cursorless-vscode-e2e/src/suite/scopeProvider/runBasicScopeInfoTest.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import {
55
ScopeSupportLevels,
66
} from "@cursorless/common";
77
import Sinon = require("sinon");
8-
import { sleepWithBackoff } from "../../endToEndTestSetup";
9-
import { commands } from "vscode";
8+
import { Position, Range, TextDocument, commands } from "vscode";
109
import { assertCalledWithScopeInfo } from "./assertCalledWithScopeInfo";
1110

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

2423
try {
25-
assertCalledWithScopeInfo(fake, unsupported);
24+
await assertCalledWithScopeInfo(fake, unsupported);
2625

27-
await openNewEditor(contents, {
26+
const editor = await openNewEditor("", {
2827
languageId: "typescript",
2928
});
30-
await sleepWithBackoff(25);
29+
await assertCalledWithScopeInfo(fake, supported);
3130

32-
assertCalledWithScopeInfo(fake, present);
31+
await editor.edit((editBuilder) => {
32+
editBuilder.insert(new Position(0, 0), contents);
33+
});
34+
await assertCalledWithScopeInfo(fake, present);
3335

34-
await commands.executeCommand("workbench.action.closeAllEditors");
35-
await sleepWithBackoff(25);
36+
await editor.edit((editBuilder) => {
37+
editBuilder.delete(getDocumentRange(editor.document));
38+
});
39+
await assertCalledWithScopeInfo(fake, supported);
3640

37-
assertCalledWithScopeInfo(fake, unsupported);
41+
await commands.executeCommand("workbench.action.closeAllEditors");
42+
await assertCalledWithScopeInfo(fake, unsupported);
3843
} finally {
3944
disposable.dispose();
4045
}
4146
}
4247

48+
function getDocumentRange(textDocument: TextDocument) {
49+
const { end } = textDocument.lineAt(textDocument.lineCount - 1).range;
50+
return new Range(0, 0, end.line, end.character);
51+
}
52+
4353
const contents = `
4454
function helloWorld() {
4555
@@ -50,7 +60,10 @@ function getExpectedScope(scopeSupport: ScopeSupport): ScopeSupportInfo {
5060
return {
5161
humanReadableName: "named function",
5262
isLanguageSpecific: true,
53-
iterationScopeSupport: scopeSupport,
63+
iterationScopeSupport:
64+
scopeSupport === ScopeSupport.unsupported
65+
? ScopeSupport.unsupported
66+
: ScopeSupport.supportedAndPresentInEditor,
5467
scopeType: {
5568
type: "namedFunction",
5669
},
@@ -64,4 +77,5 @@ function getExpectedScope(scopeSupport: ScopeSupport): ScopeSupportInfo {
6477
}
6578

6679
const unsupported = getExpectedScope(ScopeSupport.unsupported);
80+
const supported = getExpectedScope(ScopeSupport.supportedButNotPresentInEditor);
6781
const present = getExpectedScope(ScopeSupport.supportedAndPresentInEditor);
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { getCursorlessApi, openNewEditor } from "@cursorless/vscode-common";
2+
import {
3+
LATEST_VERSION,
4+
ScopeSupport,
5+
ScopeSupportInfo,
6+
ScopeSupportLevels,
7+
ScopeType,
8+
} from "@cursorless/common";
9+
import Sinon = require("sinon");
10+
import {
11+
assertCalledWithScopeInfo,
12+
assertCalledWithoutScopeInfo as assertCalledWithoutScope,
13+
} from "./assertCalledWithScopeInfo";
14+
import { stat, unlink, writeFile } from "fs/promises";
15+
import { sleepWithBackoff } from "../../endToEndTestSetup";
16+
17+
/**
18+
* Tests that the scope provider correctly reports the scope support for a
19+
* simple named function.
20+
*/
21+
export async function runCustomRegexScopeInfoTest() {
22+
const { scopeProvider, spokenFormsJsonPath } = (await getCursorlessApi())
23+
.testHelpers!;
24+
const fake = Sinon.fake<[scopeInfos: ScopeSupportLevels], void>();
25+
26+
const disposable = scopeProvider.onDidChangeScopeSupport(fake);
27+
28+
try {
29+
await assertCalledWithoutScope(fake, scopeType);
30+
31+
await writeFile(
32+
spokenFormsJsonPath,
33+
JSON.stringify(spokenFormJsonContents),
34+
);
35+
await sleepWithBackoff(50);
36+
await assertCalledWithScopeInfo(fake, unsupported);
37+
38+
await openNewEditor(contents);
39+
await assertCalledWithScopeInfo(fake, present);
40+
41+
await unlink(spokenFormsJsonPath);
42+
await sleepWithBackoff(50);
43+
await assertCalledWithoutScope(fake, scopeType);
44+
} finally {
45+
disposable.dispose();
46+
47+
// Delete spokenFormsJsonPath if it exists
48+
try {
49+
await stat(spokenFormsJsonPath);
50+
await unlink(spokenFormsJsonPath);
51+
} catch (e) {
52+
// Do nothing
53+
}
54+
}
55+
}
56+
57+
const contents = `
58+
hello world
59+
`;
60+
61+
const regex = "[a-zA-Z]+";
62+
63+
const spokenFormJsonContents = {
64+
version: LATEST_VERSION,
65+
entries: [
66+
{
67+
type: "customRegex",
68+
id: regex,
69+
spokenForms: ["spaghetti"],
70+
},
71+
],
72+
};
73+
74+
const scopeType: ScopeType = {
75+
type: "customRegex",
76+
regex,
77+
};
78+
79+
function getExpectedScope(scopeSupport: ScopeSupport): ScopeSupportInfo {
80+
return {
81+
humanReadableName: "Regex `[a-zA-Z]+`",
82+
isLanguageSpecific: false,
83+
iterationScopeSupport:
84+
scopeSupport === ScopeSupport.unsupported
85+
? ScopeSupport.unsupported
86+
: ScopeSupport.supportedAndPresentInEditor,
87+
scopeType,
88+
spokenForm: {
89+
alternatives: [],
90+
preferred: "spaghetti",
91+
type: "success",
92+
},
93+
support: scopeSupport,
94+
};
95+
}
96+
97+
const unsupported = getExpectedScope(ScopeSupport.unsupported);
98+
const present = getExpectedScope(ScopeSupport.supportedAndPresentInEditor);
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { endToEndTestSetup } from "../../endToEndTestSetup";
21
import { asyncSafety } from "@cursorless/common";
2+
import { endToEndTestSetup } from "../../endToEndTestSetup";
33
import { runBasicScopeInfoTest } from "./runBasicScopeInfoTest";
4+
import { runCustomRegexScopeInfoTest } from "./runCustomRegexScopeInfoTest";
45

56
suite("scope provider", async function () {
67
endToEndTestSetup(this);
@@ -9,4 +10,8 @@ suite("scope provider", async function () {
910
"basic",
1011
asyncSafety(() => runBasicScopeInfoTest()),
1112
);
13+
test(
14+
"custom regex",
15+
asyncSafety(() => runCustomRegexScopeInfoTest()),
16+
);
1217
});

0 commit comments

Comments
 (0)