/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; import { Testing } from '../customTextEditor'; import { closeAllEditors, delay, disposeAll, randomFilePath } from './utils'; assert.ok(vscode.workspace.rootPath); const testWorkspaceRoot = vscode.Uri.file(path.join(vscode.workspace.rootPath!, 'customEditors')); const commands = Object.freeze({ open: 'vscode.open', openWith: 'vscode.openWith', save: 'workbench.action.files.save', undo: 'undo', }); async function writeRandomFile(options: { ext: string; contents: string; }): Promise { const fakeFile = randomFilePath({ root: testWorkspaceRoot, ext: options.ext }); await fs.promises.writeFile(fakeFile.fsPath, Buffer.from(options.contents)); return fakeFile; } const disposables: vscode.Disposable[] = []; function _register(disposable: T) { disposables.push(disposable); return disposable; } class CustomEditorUpdateListener { public static create() { return _register(new CustomEditorUpdateListener()); } private readonly commandSubscription: vscode.Disposable; private readonly unconsumedResponses: Array = []; private readonly callbackQueue: Array<(data: Testing.CustomEditorContentChangeEvent) => void> = []; private constructor() { this.commandSubscription = vscode.commands.registerCommand(Testing.abcEditorContentChangeCommand, (data: Testing.CustomEditorContentChangeEvent) => { if (this.callbackQueue.length) { const callback = this.callbackQueue.shift(); assert.ok(callback); callback!(data); } else { this.unconsumedResponses.push(data); } }); } dispose() { this.commandSubscription.dispose(); } async nextResponse(): Promise { if (this.unconsumedResponses.length) { return this.unconsumedResponses.shift()!; } return new Promise(resolve => { this.callbackQueue.push(resolve); }); } } suite('CustomEditor tests', () => { setup(async () => { await closeAllEditors(); await resetTestWorkspace(); }); teardown(async () => { await closeAllEditors(); disposeAll(disposables); await resetTestWorkspace(); }); test('Should load basic content from disk', async () => { const startingContent = `load, init`; const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); const listener = CustomEditorUpdateListener.create(); await vscode.commands.executeCommand(commands.open, testDocument); const { content } = await listener.nextResponse(); assert.equal(content, startingContent); }); test('Should support basic edits', async () => { const startingContent = `basic edit, init`; const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); const listener = CustomEditorUpdateListener.create(); await vscode.commands.executeCommand(commands.open, testDocument); await listener.nextResponse(); const newContent = `basic edit test`; await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); const { content } = await listener.nextResponse(); assert.equal(content, newContent); }); test('Should support single undo', async () => { const startingContent = `single undo, init`; const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); const listener = CustomEditorUpdateListener.create(); await vscode.commands.executeCommand(commands.open, testDocument); await listener.nextResponse(); const newContent = `undo test`; { await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); const { content } = await listener.nextResponse(); assert.equal(content, newContent); } await delay(100); { await vscode.commands.executeCommand(commands.undo); const { content } = await listener.nextResponse(); assert.equal(content, startingContent); } }); test('Should support multiple undo', async () => { const startingContent = `multiple undo, init`; const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); const listener = CustomEditorUpdateListener.create(); await vscode.commands.executeCommand(commands.open, testDocument); await listener.nextResponse(); const count = 10; // Make edits for (let i = 0; i < count; ++i) { await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, `${i}`); const { content } = await listener.nextResponse(); assert.equal(`${i}`, content); } // Then undo them in order for (let i = count - 1; i; --i) { await delay(100); await vscode.commands.executeCommand(commands.undo); const { content } = await listener.nextResponse(); assert.equal(`${i - 1}`, content); } { await delay(100); await vscode.commands.executeCommand(commands.undo); const { content } = await listener.nextResponse(); assert.equal(content, startingContent); } }); test('Should update custom editor on file move', async () => { const startingContent = `file move, init`; const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); const listener = CustomEditorUpdateListener.create(); await vscode.commands.executeCommand(commands.open, testDocument); await listener.nextResponse(); const newFileName = vscode.Uri.file(path.join(testWorkspaceRoot.fsPath, 'y.abc')); const edit = new vscode.WorkspaceEdit(); edit.renameFile(testDocument, newFileName); await vscode.workspace.applyEdit(edit); const response = (await listener.nextResponse()); assert.equal(response.content, startingContent); assert.equal(response.source.toString(), newFileName.toString()); }); test('Should support saving custom editors', async () => { const startingContent = `save, init`; const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); const listener = CustomEditorUpdateListener.create(); await vscode.commands.executeCommand(commands.open, testDocument); await listener.nextResponse(); const newContent = `save, new`; { await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); const { content } = await listener.nextResponse(); assert.equal(content, newContent); } { await vscode.commands.executeCommand(commands.save); const fileContent = (await fs.promises.readFile(testDocument.fsPath)).toString(); assert.equal(fileContent, newContent); } }); test('Should undo after saving custom editor', async () => { const startingContent = `undo after save, init`; const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); const listener = CustomEditorUpdateListener.create(); await vscode.commands.executeCommand(commands.open, testDocument); await listener.nextResponse(); const newContent = `undo after save, new`; { await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent); const { content } = await listener.nextResponse(); assert.equal(content, newContent); } { await vscode.commands.executeCommand(commands.save); const fileContent = (await fs.promises.readFile(testDocument.fsPath)).toString(); assert.equal(fileContent, newContent); } await delay(100); { await vscode.commands.executeCommand(commands.undo); const { content } = await listener.nextResponse(); assert.equal(content, startingContent); } }); test.skip('Should support untitled custom editors', async () => { const listener = CustomEditorUpdateListener.create(); const untitledFile = randomFilePath({ root: testWorkspaceRoot, ext: '.abc' }).with({ scheme: 'untitled' }); await vscode.commands.executeCommand(commands.open, untitledFile); assert.equal((await listener.nextResponse()).content, ''); await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, `123`); assert.equal((await listener.nextResponse()).content, '123'); await vscode.commands.executeCommand(commands.save); const content = await fs.promises.readFile(untitledFile.fsPath); assert.equal(content.toString(), '123'); }); test.skip('When switching away from a non-default custom editors and then back, we should continue using the non-default editor', async () => { const startingContent = `switch, init`; const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); const listener = CustomEditorUpdateListener.create(); { await vscode.commands.executeCommand(commands.open, testDocument, { preview: false }); const { content } = await listener.nextResponse(); assert.strictEqual(content, startingContent.toString()); assert.ok(!vscode.window.activeTextEditor); } // Switch to non-default editor await vscode.commands.executeCommand(commands.openWith, testDocument, 'default', { preview: false }); assert.strictEqual(vscode.window.activeTextEditor!?.document.uri.toString(), testDocument.toString()); // Then open a new document (hiding existing one) const otherFile = vscode.Uri.file(path.join(testWorkspaceRoot.fsPath, 'other.json')); await vscode.commands.executeCommand(commands.open, otherFile); assert.strictEqual(vscode.window.activeTextEditor!?.document.uri.toString(), otherFile.toString()); // And then back await vscode.commands.executeCommand('workbench.action.navigateBack'); await vscode.commands.executeCommand('workbench.action.navigateBack'); // Make sure we have the file on as text assert.ok(vscode.window.activeTextEditor); assert.strictEqual(vscode.window.activeTextEditor!?.document.uri.toString(), testDocument.toString()); }); test('Should release the text document when the editor is closed', async () => { const startingContent = `release document init,`; const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent }); const listener = CustomEditorUpdateListener.create(); await vscode.commands.executeCommand(commands.open, testDocument); await listener.nextResponse(); const doc = vscode.workspace.textDocuments.find(x => x.uri.toString() === testDocument.toString()); assert.ok(doc); assert.ok(!doc!.isClosed); await closeAllEditors(); await delay(100); assert.ok(doc!.isClosed); }); }); async function resetTestWorkspace() { try { await vscode.workspace.fs.delete(testWorkspaceRoot, { recursive: true }); } catch { // ok if file doesn't exist } await vscode.workspace.fs.createDirectory(testWorkspaceRoot); }