Skip to content
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
3 changes: 3 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"env": {
"CURSORLESS_TEST": "true"
},
"args": [
"--disable-extension",
"asvetliakov.vscode-neovim",
Expand Down
4 changes: 4 additions & 0 deletions src/editDisplayUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const getPendingEditDecorationTime = () =>
.get<number>("pendingEditDecorationTime")!;

export async function decorationSleep() {
if (process.env.CURSORLESS_TEST != null) {
return;
}

await sleep(getPendingEditDecorationTime());
}

Expand Down
9 changes: 2 additions & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,14 @@ import { TestCase } from "./TestCase";
import { ThatMark } from "./ThatMark";
import { Clipboard } from "./Clipboard";
import { TestCaseRecorder } from "./TestCaseRecorder";
import { getParseTreeApi } from "./getExtensionApi";

export async function activate(context: vscode.ExtensionContext) {
const fontMeasurements = new FontMeasurements(context);
await fontMeasurements.calculate();
const decorations = new Decorations(fontMeasurements);

const parseTreeExtension = vscode.extensions.getExtension("pokey.parse-tree");

if (parseTreeExtension == null) {
throw new Error("Depends on pokey.parse-tree extension");
}

const { getNodeAtLocation } = await parseTreeExtension.activate();
const { getNodeAtLocation } = await getParseTreeApi();

var isActive = vscode.workspace
.getConfiguration("cursorless")
Expand Down
32 changes: 32 additions & 0 deletions src/getExtensionApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as vscode from "vscode";
import { ThatMark } from "./ThatMark";
import NavigationMap from "./NavigationMap";
import { SyntaxNode } from "web-tree-sitter";

export interface CursorlessApi {
thatMark: ThatMark;
sourceMark: ThatMark;
navigationMap: NavigationMap;
addDecorations: () => void;
}

export interface ParseTreeApi {
getNodeAtLocation(location: vscode.Location): SyntaxNode;
loadLanguage: (languageId: string) => Promise<boolean>;
}

export async function getExtensionApi<T>(extensionId: string) {
const extension = vscode.extensions.getExtension(extensionId);

if (extension == null) {
throw new Error(`Could not get ${extensionId} extension`);
}

return (await extension.activate()) as T;
}

export const getCursorlessApi = () =>
getExtensionApi<CursorlessApi>("pokey.cursorless");

export const getParseTreeApi = () =>
getExtensionApi<ParseTreeApi>("pokey.parse-tree");
218 changes: 85 additions & 133 deletions src/test/suite/recorded.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import * as path from "path";
import * as yaml from "js-yaml";
import * as vscode from "vscode";
import { TestCaseFixture } from "../../TestCase";
import { ThatMark } from "../../ThatMark";
import NavigationMap from "../../NavigationMap";
import * as sinon from "sinon";
import { Clipboard } from "../../Clipboard";
Expand All @@ -16,6 +15,7 @@ import {
} from "../../toPlainObject";
import { walkFilesSync } from "./walkSync";
import { enableDebugLog } from "../../debug";
import { getCursorlessApi, getParseTreeApi } from "../../getExtensionApi";

function createPosition(position: PositionPlainObject) {
return new vscode.Position(position.line, position.character);
Expand All @@ -28,6 +28,8 @@ function createSelection(selection: SelectionPlainObject): vscode.Selection {
}

suite("recorded test cases", async function () {
this.timeout("100s");
this.retries(3);
const directory = path.join(
__dirname,
"../../../src/test/suite/fixtures/recorded"
Expand All @@ -39,139 +41,89 @@ suite("recorded test cases", async function () {
sinon.restore();
});

let lastLanguageId: string;

files.forEach(async (file) => {
test(file.split(".")[0], async function () {
this.timeout(100000);
const cursorless = vscode.extensions.getExtension("pokey.cursorless");

if (cursorless == null) {
throw new Error("Could not get cursorless extension");
}

const cursorlessApi: {
thatMark: ThatMark;
sourceMark: ThatMark;
navigationMap: NavigationMap;
addDecorations: () => void;
} = await cursorless.activate();
const buffer = await fsp.readFile(file);
const fixture = yaml.load(buffer.toString()) as TestCaseFixture;
const excludeFields: string[] = [];

await vscode.commands.executeCommand("workbench.action.closeAllEditors");
const document = await vscode.workspace.openTextDocument({
language: fixture.languageId,
content: fixture.initialState.documentContents,
});
const editor = await vscode.window.showTextDocument(document);

// Sleep on changing language is necessary otherwise the tree sitter
// will throw an exception on getNodeAtLocation()
if (lastLanguageId !== document.languageId) {
if (lastLanguageId != null) {
await new Promise((resolve) => setTimeout(resolve, 200));
}
lastLanguageId = document.languageId;
}

editor.selections = fixture.initialState.selections.map(createSelection);

if (fixture.initialState.thatMark) {
const initialThatMark = fixture.initialState.thatMark.map((mark) => ({
selection: createSelection(mark),
editor,
}));
cursorlessApi.thatMark.set(initialThatMark);
}
if (fixture.initialState.sourceMark) {
const initialSourceMark = fixture.initialState.sourceMark.map(
(mark) => ({
selection: createSelection(mark),
editor,
})
);
cursorlessApi.sourceMark.set(initialSourceMark);
}

if (fixture.initialState.clipboard) {
let mockClipboard = fixture.initialState.clipboard;
sinon.replace(Clipboard, "readText", async () => mockClipboard);
sinon.replace(Clipboard, "writeText", async (value: string) => {
mockClipboard = value;
});
} else {
excludeFields.push("clipboard");
}

// Wait for cursorless to set up decorations
cursorlessApi.addDecorations();

// Assert that recorded decorations are present
const assertDecorations = () => {
Object.entries(fixture.marks).forEach(([key, token]) => {
const { color, character } = NavigationMap.splitKey(key);
const currentToken = cursorlessApi.navigationMap.getToken(
color,
character
);
assert(
currentToken != null,
`Mark "${color} ${character}" not found`
);
assert.deepStrictEqual(rangeToPlainObject(currentToken.range), token);
});
};

// Tried three times, sleep 100ms between each
await tryAndRetry(assertDecorations, 3, 100);

const returnValue = await vscode.commands.executeCommand(
"cursorless.command",
fixture.spokenForm,
fixture.command.actionName,
fixture.command.partialTargets,
...fixture.command.extraArgs
);

// TODO Visible ranges are not asserted, see:
// https://github.com/pokey/cursorless-vscode/issues/160
const { visibleRanges, ...resultState } = await takeSnapshot(
cursorlessApi.thatMark,
cursorlessApi.sourceMark,
excludeFields
);

assert.deepStrictEqual(
resultState,
fixture.finalState,
"Unexpected final state"
);

assert.deepStrictEqual(
returnValue,
fixture.returnValue,
"Unexpected return value"
);
});
});
files.forEach((file) => test(file.split(".")[0], () => runTest(file)));
});

async function tryAndRetry(
callback: () => void,
numberOfThries: number,
sleepTime: number
) {
while (true) {
try {
return callback();
} catch (error) {
if (numberOfThries === 0) {
throw error;
}
numberOfThries--;
await new Promise((resolve) => setTimeout(resolve, sleepTime));
}
async function runTest(file: string) {
const buffer = await fsp.readFile(file);
const fixture = yaml.load(buffer.toString()) as TestCaseFixture;
const excludeFields: string[] = [];

const cursorlessApi = await getCursorlessApi();
const parseTreeApi = await getParseTreeApi();

await vscode.commands.executeCommand("workbench.action.closeAllEditors");
const document = await vscode.workspace.openTextDocument({
language: fixture.languageId,
content: fixture.initialState.documentContents,
});
const editor = await vscode.window.showTextDocument(document);

await parseTreeApi.loadLanguage(document.languageId);

editor.selections = fixture.initialState.selections.map(createSelection);

if (fixture.initialState.thatMark) {
const initialThatMark = fixture.initialState.thatMark.map((mark) => ({
selection: createSelection(mark),
editor,
}));
cursorlessApi.thatMark.set(initialThatMark);
}
if (fixture.initialState.sourceMark) {
const initialSourceMark = fixture.initialState.sourceMark.map((mark) => ({
selection: createSelection(mark),
editor,
}));
cursorlessApi.sourceMark.set(initialSourceMark);
}

if (fixture.initialState.clipboard) {
let mockClipboard = fixture.initialState.clipboard;
sinon.replace(Clipboard, "readText", async () => mockClipboard);
sinon.replace(Clipboard, "writeText", async (value: string) => {
mockClipboard = value;
});
} else {
excludeFields.push("clipboard");
}

// Wait for cursorless to set up decorations
cursorlessApi.addDecorations();

// Assert that recorded decorations are present
Object.entries(fixture.marks).forEach(([key, token]) => {
const { color, character } = NavigationMap.splitKey(key);
const currentToken = cursorlessApi.navigationMap.getToken(color, character);
assert(currentToken != null, `Mark "${color} ${character}" not found`);
assert.deepStrictEqual(rangeToPlainObject(currentToken.range), token);
});

const returnValue = await vscode.commands.executeCommand(
"cursorless.command",
fixture.spokenForm,
fixture.command.actionName,
fixture.command.partialTargets,
...fixture.command.extraArgs
);

// TODO Visible ranges are not asserted, see:
// https://github.com/pokey/cursorless-vscode/issues/160
const { visibleRanges, ...resultState } = await takeSnapshot(
cursorlessApi.thatMark,
cursorlessApi.sourceMark,
excludeFields
);

assert.deepStrictEqual(
resultState,
fixture.finalState,
"Unexpected final state"
);

assert.deepStrictEqual(
returnValue,
fixture.returnValue,
"Unexpected return value"
);
}