Skip to content

Commit a1fa10d

Browse files
committed
Add support for spying ide; add ide.messages
1 parent 429b6b7 commit a1fa10d

20 files changed

+290
-60
lines changed

src/core/commandRunner/CommandRunner.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ export default class CommandRunner {
169169
console.error(err.message);
170170
console.error(err.stack);
171171
throw err;
172+
} finally {
173+
this.graph.testCaseRecorder.finallyHook();
172174
}
173175
}
174176

src/extension.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import * as vscode from "vscode";
2-
import graphFactories from "./util/graphFactories";
3-
import { Graph } from "./typings/Types";
4-
import makeGraph, { FactoryMap } from "./util/makeGraph";
2+
import CommandRunner from "./core/commandRunner/CommandRunner";
53
import { ThatMark } from "./core/ThatMark";
6-
import { getCommandServerApi, getParseTreeApi } from "./util/getExtensionApi";
74
import isTesting from "./testUtil/isTesting";
8-
import CommandRunner from "./core/commandRunner/CommandRunner";
5+
import { Graph } from "./typings/Types";
6+
import { getCommandServerApi, getParseTreeApi } from "./util/getExtensionApi";
7+
import graphFactories from "./util/graphFactories";
8+
import makeGraph, { FactoryMap } from "./util/makeGraph";
99

1010
/**
1111
* Extension entrypoint called by VSCode on Cursorless startup.
@@ -19,12 +19,15 @@ export async function activate(context: vscode.ExtensionContext) {
1919
const { getNodeAtLocation } = await getParseTreeApi();
2020
const commandServerApi = await getCommandServerApi();
2121

22-
const graph = makeGraph({
23-
...graphFactories,
24-
extensionContext: () => context,
25-
commandServerApi: () => commandServerApi,
26-
getNodeAtLocation: () => getNodeAtLocation,
27-
} as FactoryMap<Graph>);
22+
const graph = makeGraph(
23+
{
24+
...graphFactories,
25+
extensionContext: () => context,
26+
commandServerApi: () => commandServerApi,
27+
getNodeAtLocation: () => getNodeAtLocation,
28+
} as FactoryMap<Graph>,
29+
["ide"]
30+
);
2831
graph.debug.init();
2932
graph.snippets.init();
3033
await graph.decorations.init();

src/ide/ide.types.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Listener } from "../util/Notifier";
33

44
export interface IDE {
55
configuration: Configuration;
6+
messages: Messages;
67

78
/**
89
* Register disposables to be disposed of on IDE exit.
@@ -22,13 +23,30 @@ export interface Configuration {
2223
getOwnConfiguration<T extends CursorlessConfigKey>(
2324
key: T
2425
): CursorlessConfiguration[T] | undefined;
25-
onDidChangeConfiguration: (listener: Listener) => Disposable;
26+
onDidChangeConfiguration(listener: Listener): Disposable;
27+
}
28+
29+
export type MessageId = string;
2630

27-
mockConfiguration<T extends CursorlessConfigKey>(
28-
key: T,
29-
value: CursorlessConfiguration[T]
30-
): void;
31-
resetMocks(): void;
31+
export interface Messages {
32+
/**
33+
* Displays a warning message {@link message} to the user along with possible
34+
* {@link options} for them to select.
35+
*
36+
* @param id Each code site where we issue a warning should have a unique,
37+
* human readable id for testability. This allows us to have tests without
38+
* tying ourself to the specific wording of the warning message.
39+
* @param message The message to display to the user
40+
* @param options A list of options to display to the user. The selected
41+
* option will be returned by this function
42+
* @returns The option selected by the user, or undefined if no option was
43+
* selected
44+
*/
45+
showWarning(
46+
id: MessageId,
47+
message: string,
48+
...options: string[]
49+
): Promise<string | undefined>;
3250
}
3351

3452
export interface Disposable {

src/ide/spies/SpyConfiguration.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Listener } from "../../util/Notifier";
2+
import {
3+
Configuration,
4+
CursorlessConfigKey,
5+
CursorlessConfiguration,
6+
Disposable,
7+
} from "../ide.types";
8+
9+
export default class SpyConfiguration implements Configuration {
10+
constructor(private original: Configuration) {}
11+
12+
onDidChangeConfiguration(listener: Listener<[]>): Disposable {
13+
return this.original.onDidChangeConfiguration(listener);
14+
}
15+
16+
getOwnConfiguration<T extends CursorlessConfigKey>(
17+
key: T
18+
): CursorlessConfiguration[T] | undefined {
19+
return this.original.getOwnConfiguration(key);
20+
}
21+
22+
getSpyValues() {
23+
return undefined;
24+
}
25+
}

src/ide/spies/SpyIDE.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { values } from "lodash";
2+
import { Graph } from "../../typings/Types";
3+
import { Disposable, IDE } from "../ide.types";
4+
import SpyConfiguration from "./SpyConfiguration";
5+
import SpyMessages, { Message } from "./SpyMessages";
6+
7+
export interface SpyIDERecordedValues {
8+
configuration: undefined;
9+
messages: Message[] | undefined;
10+
}
11+
12+
export default class SpyIDE implements IDE {
13+
configuration: SpyConfiguration;
14+
messages: SpyMessages;
15+
16+
constructor(private original: IDE) {
17+
this.configuration = new SpyConfiguration(original.configuration);
18+
this.messages = new SpyMessages(original.messages);
19+
}
20+
21+
disposeOnExit(...disposables: Disposable[]): () => void {
22+
return this.original.disposeOnExit(...disposables);
23+
}
24+
25+
getSpyValues(): SpyIDERecordedValues | undefined {
26+
const ret = {
27+
configuration: this.configuration.getSpyValues(),
28+
messages: this.messages.getSpyValues(),
29+
};
30+
31+
return values(ret).every((value) => value == null) ? undefined : ret;
32+
}
33+
}
34+
35+
export interface SpyInfo extends Disposable {
36+
spy: SpyIDE;
37+
}
38+
39+
export function injectSpyIde(graph: Graph): SpyInfo {
40+
const original = graph.ide;
41+
const spy = new SpyIDE(original);
42+
43+
graph.ide = spy;
44+
45+
return {
46+
spy,
47+
48+
dispose() {
49+
graph.ide = original;
50+
},
51+
};
52+
}

src/ide/spies/SpyMessages.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { MessageId, Messages } from "../ide.types";
2+
3+
type MessageType = "info" | "warning" | "error";
4+
5+
export interface Message {
6+
type: MessageType;
7+
8+
/**
9+
* Each place that we show a message, we should use a message class for
10+
* testability, so that our tests aren't tied to specific message wording.
11+
*/
12+
id: MessageId;
13+
}
14+
15+
export default class SpyMessages implements Messages {
16+
private shownMessages: Message[] = [];
17+
18+
constructor(private original: Messages) {}
19+
20+
showWarning(
21+
id: MessageId,
22+
message: string,
23+
...options: string[]
24+
): Promise<string | undefined> {
25+
this.shownMessages.push({ type: "warning", id });
26+
27+
return this.original.showWarning(id, message, ...options);
28+
}
29+
30+
getSpyValues() {
31+
return this.shownMessages.length > 0 ? this.shownMessages : undefined;
32+
}
33+
}

src/ide/vscode/VscodeConfiguration.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@ import {
55
CursorlessConfigKey,
66
CursorlessConfiguration,
77
} from "../ide.types";
8-
import { VscodeIDE } from "./VscodeIDE";
8+
import type VscodeIDE from "./VscodeIDE";
99

10-
export class VscodeConfiguration implements Configuration {
10+
export default class VscodeConfiguration implements Configuration {
1111
private notifier = new Notifier();
12-
private mocks: Partial<CursorlessConfiguration> = {};
1312

14-
constructor(private ide: VscodeIDE) {
13+
constructor(ide: VscodeIDE) {
1514
this.onDidChangeConfiguration = this.onDidChangeConfiguration.bind(this);
1615

1716
ide.disposeOnExit(
@@ -22,25 +21,10 @@ export class VscodeConfiguration implements Configuration {
2221
getOwnConfiguration<T extends CursorlessConfigKey>(
2322
key: T
2423
): CursorlessConfiguration[T] | undefined {
25-
if (key in this.mocks) {
26-
return this.mocks[key];
27-
}
28-
2924
return vscode.workspace
3025
.getConfiguration("cursorless")
3126
.get<CursorlessConfiguration[T]>(key);
3227
}
3328

3429
onDidChangeConfiguration = this.notifier.registerListener;
35-
36-
mockConfiguration<T extends CursorlessConfigKey>(
37-
key: T,
38-
value: CursorlessConfiguration[T]
39-
): void {
40-
this.mocks[key] = value;
41-
}
42-
43-
resetMocks(): void {
44-
this.mocks = {};
45-
}
4630
}

src/ide/vscode/VscodeIDE.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { pull } from "lodash";
22
import { Graph } from "../../typings/Types";
33
import { Disposable, IDE } from "../ide.types";
4-
import { VscodeConfiguration } from "./VscodeConfiguration";
4+
import VscodeConfiguration from "./VscodeConfiguration";
5+
import VscodeMessages from "./VscodeMessages";
56

6-
export class VscodeIDE implements IDE {
7+
export default class VscodeIDE implements IDE {
78
configuration: VscodeConfiguration;
9+
messages: VscodeMessages;
810

911
constructor(private graph: Graph) {
1012
this.configuration = new VscodeConfiguration(this);
13+
this.messages = new VscodeMessages();
1114
}
1215

1316
disposeOnExit(...disposables: Disposable[]): () => void {

src/ide/vscode/VscodeMessages.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { window } from "vscode";
2+
import { MessageId, Messages } from "../ide.types";
3+
4+
export default class VscodeMessages implements Messages {
5+
async showWarning(
6+
_id: MessageId,
7+
message: string,
8+
...options: string[]
9+
): Promise<string | undefined> {
10+
return await window.showWarningMessage(message, ...options);
11+
}
12+
}

src/test/suite/fakes/ide/FakeConfiguration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
import { Graph } from "../../../../typings/Types";
77
import { Notifier } from "../../../../util/Notifier";
88

9-
export class FakeConfiguration implements Configuration {
9+
export default class FakeConfiguration implements Configuration {
1010
private notifier = new Notifier();
1111
private mocks: Partial<CursorlessConfiguration> = {};
1212

src/test/suite/fakes/ide/FakeIDE.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { pull } from "lodash";
22
import { Disposable, IDE } from "../../../../ide/ide.types";
33
import { Graph } from "../../../../typings/Types";
4-
import { FakeConfiguration } from "./FakeConfiguration";
4+
import FakeConfiguration from "./FakeConfiguration";
5+
import FakeMessages from "./FakeMessages";
56

6-
export class FakeIDE implements IDE {
7+
export default class FakeIDE implements IDE {
78
configuration: FakeConfiguration;
9+
messages: FakeMessages;
810
private disposables: Disposable[] = [];
911

1012
constructor(graph: Graph) {
1113
this.configuration = new FakeConfiguration(graph);
14+
this.messages = new FakeMessages();
1215
}
1316

1417
disposeOnExit(...disposables: Disposable[]): () => void {
@@ -21,3 +24,22 @@ export class FakeIDE implements IDE {
2124
this.disposables.forEach((disposable) => disposable.dispose());
2225
}
2326
}
27+
28+
export interface FakeInfo extends Disposable {
29+
fake: FakeIDE;
30+
}
31+
32+
export function injectFakeIde(graph: Graph): FakeInfo {
33+
const original = graph.ide;
34+
const fake = new FakeIDE(graph);
35+
36+
graph.ide = fake;
37+
38+
return {
39+
fake,
40+
41+
dispose() {
42+
graph.ide = original;
43+
},
44+
};
45+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Messages } from "../../../../ide/ide.types";
2+
3+
export default class FakeMessages implements Messages {
4+
async showWarning(
5+
_message: string,
6+
..._options: string[]
7+
): Promise<string | undefined> {
8+
return undefined;
9+
}
10+
}

0 commit comments

Comments
 (0)