Skip to content

Add support for spying ide; add ide.messages #1033

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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/commandRunner/CommandRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ export default class CommandRunner {
console.error(err.message);
console.error(err.stack);
throw err;
} finally {
this.graph.testCaseRecorder.finallyHook();
}
}

Expand Down
25 changes: 14 additions & 11 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as vscode from "vscode";
import graphFactories from "./util/graphFactories";
import { Graph } from "./typings/Types";
import makeGraph, { FactoryMap } from "./util/makeGraph";
import CommandRunner from "./core/commandRunner/CommandRunner";
import { ThatMark } from "./core/ThatMark";
import { getCommandServerApi, getParseTreeApi } from "./util/getExtensionApi";
import isTesting from "./testUtil/isTesting";
import CommandRunner from "./core/commandRunner/CommandRunner";
import { Graph } from "./typings/Types";
import { getCommandServerApi, getParseTreeApi } from "./util/getExtensionApi";
import graphFactories from "./util/graphFactories";
import makeGraph, { FactoryMap } from "./util/makeGraph";

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

const graph = makeGraph({
...graphFactories,
extensionContext: () => context,
commandServerApi: () => commandServerApi,
getNodeAtLocation: () => getNodeAtLocation,
} as FactoryMap<Graph>);
const graph = makeGraph(
{
...graphFactories,
extensionContext: () => context,
commandServerApi: () => commandServerApi,
getNodeAtLocation: () => getNodeAtLocation,
} as FactoryMap<Graph>,
["ide"]
);
graph.debug.init();
graph.snippets.init();
await graph.decorations.init();
Expand Down
31 changes: 25 additions & 6 deletions src/ide/ide.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Listener } from "../util/Notifier";

export interface IDE {
configuration: Configuration;
messages: Messages;

/**
* Register disposables to be disposed of on IDE exit.
Expand All @@ -22,13 +23,31 @@ export interface Configuration {
getOwnConfiguration<T extends CursorlessConfigKey>(
key: T
): CursorlessConfiguration[T] | undefined;
onDidChangeConfiguration: (listener: Listener) => Disposable;
onDidChangeConfiguration(listener: Listener): Disposable;
}

export type MessageId = string;

mockConfiguration<T extends CursorlessConfigKey>(
key: T,
value: CursorlessConfiguration[T]
): void;
resetMocks(): void;
export interface Messages {
/**
* Displays a warning message {@link message} to the user along with possible
* {@link options} for them to select.
*
* @param id Each code site where we issue a warning should have a unique,
* human readable id for testability, eg "deprecatedPositionInference". This
* allows us to write tests without tying ourself to the specific wording of
* the warning message provided in {@link message}.
* @param message The message to display to the user
* @param options A list of options to display to the user. The selected
* option will be returned by this function
* @returns The option selected by the user, or `undefined` if no option was
* selected
*/
showWarning(
id: MessageId,
message: string,
...options: string[]
): Promise<string | undefined>;
}

export interface Disposable {
Expand Down
25 changes: 25 additions & 0 deletions src/ide/spies/SpyConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Listener } from "../../util/Notifier";
import {
Configuration,
CursorlessConfigKey,
CursorlessConfiguration,
Disposable,
} from "../ide.types";

export default class SpyConfiguration implements Configuration {
constructor(private original: Configuration) {}

onDidChangeConfiguration(listener: Listener<[]>): Disposable {
return this.original.onDidChangeConfiguration(listener);
}

getOwnConfiguration<T extends CursorlessConfigKey>(
key: T
): CursorlessConfiguration[T] | undefined {
return this.original.getOwnConfiguration(key);
}

getSpyValues() {
return undefined;
}
}
52 changes: 52 additions & 0 deletions src/ide/spies/SpyIDE.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { values } from "lodash";
import { Graph } from "../../typings/Types";
import { Disposable, IDE } from "../ide.types";
import SpyConfiguration from "./SpyConfiguration";
import SpyMessages, { Message } from "./SpyMessages";

export interface SpyIDERecordedValues {
configuration: undefined;
messages: Message[] | undefined;
}

export default class SpyIDE implements IDE {
configuration: SpyConfiguration;
messages: SpyMessages;

constructor(private original: IDE) {
this.configuration = new SpyConfiguration(original.configuration);
this.messages = new SpyMessages(original.messages);
}

disposeOnExit(...disposables: Disposable[]): () => void {
return this.original.disposeOnExit(...disposables);
}

getSpyValues(): SpyIDERecordedValues | undefined {
const ret = {
configuration: this.configuration.getSpyValues(),
messages: this.messages.getSpyValues(),
};

return values(ret).every((value) => value == null) ? undefined : ret;
}
}

export interface SpyInfo extends Disposable {
spy: SpyIDE;
}

export function injectSpyIde(graph: Graph): SpyInfo {
const original = graph.ide;
const spy = new SpyIDE(original);

graph.ide = spy;

return {
spy,

dispose() {
graph.ide = original;
},
};
}
33 changes: 33 additions & 0 deletions src/ide/spies/SpyMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { MessageId, Messages } from "../ide.types";

type MessageType = "info" | "warning" | "error";

export interface Message {
type: MessageType;

/**
* Each place that we show a message, we should use a message class for
* testability, so that our tests aren't tied to specific message wording.
*/
id: MessageId;
}

export default class SpyMessages implements Messages {
private shownMessages: Message[] = [];

constructor(private original: Messages) {}

showWarning(
id: MessageId,
message: string,
...options: string[]
): Promise<string | undefined> {
this.shownMessages.push({ type: "warning", id });

return this.original.showWarning(id, message, ...options);
}

getSpyValues() {
return this.shownMessages.length > 0 ? this.shownMessages : undefined;
}
}
22 changes: 3 additions & 19 deletions src/ide/vscode/VscodeConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import {
CursorlessConfigKey,
CursorlessConfiguration,
} from "../ide.types";
import { VscodeIDE } from "./VscodeIDE";
import type VscodeIDE from "./VscodeIDE";

export class VscodeConfiguration implements Configuration {
export default class VscodeConfiguration implements Configuration {
private notifier = new Notifier();
private mocks: Partial<CursorlessConfiguration> = {};

constructor(private ide: VscodeIDE) {
constructor(ide: VscodeIDE) {
this.onDidChangeConfiguration = this.onDidChangeConfiguration.bind(this);

ide.disposeOnExit(
Expand All @@ -22,25 +21,10 @@ export class VscodeConfiguration implements Configuration {
getOwnConfiguration<T extends CursorlessConfigKey>(
key: T
): CursorlessConfiguration[T] | undefined {
if (key in this.mocks) {
return this.mocks[key];
}

return vscode.workspace
.getConfiguration("cursorless")
.get<CursorlessConfiguration[T]>(key);
}

onDidChangeConfiguration = this.notifier.registerListener;

mockConfiguration<T extends CursorlessConfigKey>(
key: T,
value: CursorlessConfiguration[T]
): void {
this.mocks[key] = value;
}

resetMocks(): void {
this.mocks = {};
}
}
7 changes: 5 additions & 2 deletions src/ide/vscode/VscodeIDE.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { pull } from "lodash";
import { Graph } from "../../typings/Types";
import { Disposable, IDE } from "../ide.types";
import { VscodeConfiguration } from "./VscodeConfiguration";
import VscodeConfiguration from "./VscodeConfiguration";
import VscodeMessages from "./VscodeMessages";

export class VscodeIDE implements IDE {
export default class VscodeIDE implements IDE {
configuration: VscodeConfiguration;
messages: VscodeMessages;

constructor(private graph: Graph) {
this.configuration = new VscodeConfiguration(this);
this.messages = new VscodeMessages();
}

disposeOnExit(...disposables: Disposable[]): () => void {
Expand Down
12 changes: 12 additions & 0 deletions src/ide/vscode/VscodeMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { window } from "vscode";
import { MessageId, Messages } from "../ide.types";

export default class VscodeMessages implements Messages {
async showWarning(
_id: MessageId,
message: string,
...options: string[]
): Promise<string | undefined> {
return await window.showWarningMessage(message, ...options);
}
}
2 changes: 1 addition & 1 deletion src/test/suite/fakes/ide/FakeConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
import { Graph } from "../../../../typings/Types";
import { Notifier } from "../../../../util/Notifier";

export class FakeConfiguration implements Configuration {
export default class FakeConfiguration implements Configuration {
private notifier = new Notifier();
private mocks: Partial<CursorlessConfiguration> = {};

Expand Down
26 changes: 24 additions & 2 deletions src/test/suite/fakes/ide/FakeIDE.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { pull } from "lodash";
import { Disposable, IDE } from "../../../../ide/ide.types";
import { Graph } from "../../../../typings/Types";
import { FakeConfiguration } from "./FakeConfiguration";
import FakeConfiguration from "./FakeConfiguration";
import FakeMessages from "./FakeMessages";

export class FakeIDE implements IDE {
export default class FakeIDE implements IDE {
configuration: FakeConfiguration;
messages: FakeMessages;
private disposables: Disposable[] = [];

constructor(graph: Graph) {
this.configuration = new FakeConfiguration(graph);
this.messages = new FakeMessages();
}

disposeOnExit(...disposables: Disposable[]): () => void {
Expand All @@ -21,3 +24,22 @@ export class FakeIDE implements IDE {
this.disposables.forEach((disposable) => disposable.dispose());
}
}

export interface FakeInfo extends Disposable {
fake: FakeIDE;
}

export function injectFakeIde(graph: Graph): FakeInfo {
const original = graph.ide;
const fake = new FakeIDE(graph);

graph.ide = fake;

return {
fake,

dispose() {
graph.ide = original;
},
};
}
10 changes: 10 additions & 0 deletions src/test/suite/fakes/ide/FakeMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Messages } from "../../../../ide/ide.types";

export default class FakeMessages implements Messages {
async showWarning(
_message: string,
..._options: string[]
): Promise<string | undefined> {
return undefined;
}
}
Loading