/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import * as assert from 'assert'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from 'vs/workbench/api/node/extHostTypes'; import { MainThreadEditorsShape, WorkspaceEditDto } from 'vs/workbench/api/node/extHost.protocol'; import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/node/extHostDocumentSaveParticipant'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import * as vscode from 'vscode'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; import { isResourceTextEdit, ResourceTextEdit } from 'vs/editor/common/modes'; suite('ExtHostDocumentSaveParticipant', () => { let resource = URI.parse('foo:bar'); let mainThreadEditors = new class extends mock() { }; let documents: ExtHostDocuments; let nullLogService = new NullLogService(); let nullExtensionDescription: IExtensionDescription = { id: 'nullExtensionDescription', name: 'Null Extension Description', publisher: 'vscode', enableProposedApi: false, engines: undefined, extensionFolderPath: undefined, isBuiltin: false, version: undefined }; setup(() => { const documentsAndEditors = new ExtHostDocumentsAndEditors(SingleProxyRPCProtocol(null)); documentsAndEditors.$acceptDocumentsAndEditorsDelta({ addedDocuments: [{ isDirty: false, modeId: 'foo', uri: resource, versionId: 1, lines: ['foo'], EOL: '\n', }] }); documents = new ExtHostDocuments(SingleProxyRPCProtocol(null), documentsAndEditors); }); test('no listeners, no problem', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => assert.ok(true)); }); test('event delivery', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); let event: vscode.TextDocumentWillSaveEvent; let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { event = e; }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); assert.ok(event); assert.equal(event.reason, TextDocumentSaveReason.Manual); assert.equal(typeof event.waitUntil, 'function'); }); }); test('event delivery, immutable', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); let event: vscode.TextDocumentWillSaveEvent; let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { event = e; }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); assert.ok(event); assert.throws(() => event.document = null); }); }); test('event delivery, bad listener', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { throw new Error('💀'); }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => { sub.dispose(); const [first] = values; assert.equal(first, false); }); }); test('event delivery, bad listener doesn\'t prevent more events', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { throw new Error('💀'); }); let event: vscode.TextDocumentWillSaveEvent; let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { event = e; }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub1.dispose(); sub2.dispose(); assert.ok(event); }); }); test('event delivery, in subscriber order', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); let counter = 0; let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { assert.equal(counter++, 0); }); let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { assert.equal(counter++, 1); }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub1.dispose(); sub2.dispose(); }); }); test('event delivery, ignore bad listeners', async () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors, { timeout: 5, errors: 1 }); let callCount = 0; let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { callCount += 1; throw new Error('boom'); }); await participant.$participateInSave(resource, SaveReason.EXPLICIT); await participant.$participateInSave(resource, SaveReason.EXPLICIT); await participant.$participateInSave(resource, SaveReason.EXPLICIT); await participant.$participateInSave(resource, SaveReason.EXPLICIT); sub.dispose(); assert.equal(callCount, 2); }); test('event delivery, overall timeout', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors, { timeout: 20, errors: 5 }); let callCount = 0; let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { callCount += 1; event.waitUntil(TPromise.timeout(17)); }); let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { callCount += 1; event.waitUntil(TPromise.timeout(17)); }); let sub3 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { callCount += 1; }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => { sub1.dispose(); sub2.dispose(); sub3.dispose(); assert.equal(callCount, 2); assert.equal(values.length, 2); }); }); test('event delivery, waitUntil', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { event.waitUntil(TPromise.timeout(10)); event.waitUntil(TPromise.timeout(10)); event.waitUntil(TPromise.timeout(10)); }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); }); }); test('event delivery, waitUntil must be called sync', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { event.waitUntil(new TPromise((resolve, reject) => { setTimeout(() => { try { assert.throws(() => event.waitUntil(TPromise.timeout(10))); resolve(void 0); } catch (e) { reject(e); } }, 10); })); }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); }); }); test('event delivery, waitUntil will timeout', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors, { timeout: 5, errors: 3 }); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { event.waitUntil(TPromise.timeout(15)); }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => { sub.dispose(); const [first] = values; assert.equal(first, false); }); }); test('event delivery, waitUntil failure handling', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { e.waitUntil(TPromise.wrapError(new Error('dddd'))); }); let event: vscode.TextDocumentWillSaveEvent; let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { event = e; }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { assert.ok(event); sub1.dispose(); sub2.dispose(); }); }); test('event delivery, pushEdits sync', () => { let dto: WorkspaceEditDto; const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock() { $tryApplyWorkspaceEdit(_edits: WorkspaceEditDto) { dto = _edits; return TPromise.as(true); } }); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { e.waitUntil(TPromise.as([TextEdit.insert(new Position(0, 0), 'bar')])); e.waitUntil(TPromise.as([TextEdit.setEndOfLine(EndOfLine.CRLF)])); }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); assert.equal(dto.edits.length, 1); assert.ok(isResourceTextEdit(dto.edits[0])); assert.equal((dto.edits[0]).edits.length, 2); }); }); test('event delivery, concurrent change', () => { let edits: WorkspaceEditDto; const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock() { $tryApplyWorkspaceEdit(_edits: WorkspaceEditDto) { edits = _edits; return TPromise.as(true); } }); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { // concurrent change from somewhere documents.$acceptModelChanged(resource.toString(), { changes: [{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, rangeLength: undefined, text: 'bar' }], eol: undefined, versionId: 2 }, true); e.waitUntil(TPromise.as([TextEdit.insert(new Position(0, 0), 'bar')])); }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => { sub.dispose(); assert.equal(edits, undefined); assert.equal(values[0], false); }); }); test('event delivery, two listeners -> two document states', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock() { $tryApplyWorkspaceEdit(dto: WorkspaceEditDto) { for (const edit of dto.edits) { if (!isResourceTextEdit(edit)) { continue; } const { resource, edits } = edit; const uri = URI.revive(resource); for (const { text, range } of edits) { documents.$acceptModelChanged(uri.toString(), { changes: [{ range, text, rangeLength: undefined, }], eol: undefined, versionId: documents.getDocumentData(uri).version + 1 }, true); } } return TPromise.as(true); } }); const document = documents.getDocumentData(resource).document; let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { // the document state we started with assert.equal(document.version, 1); assert.equal(document.getText(), 'foo'); e.waitUntil(TPromise.as([TextEdit.insert(new Position(0, 0), 'bar')])); }); let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { // the document state AFTER the first listener kicked in assert.equal(document.version, 2); assert.equal(document.getText(), 'barfoo'); e.waitUntil(TPromise.as([TextEdit.insert(new Position(0, 0), 'bar')])); }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => { sub1.dispose(); sub2.dispose(); // the document state AFTER eventing is done assert.equal(document.version, 3); assert.equal(document.getText(), 'barbarfoo'); }); }); test('Log failing listener', function () { let didLogSomething = false; let participant = new ExtHostDocumentSaveParticipant(new class extends NullLogService { error(message: string | Error, ...args: any[]): void { didLogSomething = true; } }, documents, mainThreadEditors); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { throw new Error('boom'); }); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); assert.equal(didLogSomething, true); }); }); });