diff --git a/src/vs/base/common/callbackList.ts b/src/vs/base/common/callbackList.ts index f533607af46..4471fb6d632 100644 --- a/src/vs/base/common/callbackList.ts +++ b/src/vs/base/common/callbackList.ts @@ -72,6 +72,13 @@ export default class CallbackList { return !this._callbacks || this._callbacks.length === 0; } + public entries(): [Function, any][] { + if (!this._callbacks) { + return []; + } + return this._callbacks.map((fn, index) => <[Function, any]>[fn, this._contexts[index]]); + } + public dispose(): void { this._callbacks = undefined; this._contexts = undefined; diff --git a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts new file mode 100644 index 00000000000..8a391e3d96d --- /dev/null +++ b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Event from 'vs/base/common/event'; +import CallbackList from 'vs/base/common/callbackList'; +import URI from 'vs/base/common/uri'; +import {sequence} from 'vs/base/common/async'; +import {illegalState} from 'vs/base/common/errors'; +import {TPromise} from 'vs/base/common/winjs.base'; +import {ExtHostDocuments} from 'vs/workbench/api/node/extHostDocuments'; + +export interface TextDocumentWillSaveEvent { + waitUntil(t: Thenable): void; +} + +export class ExtHostDocumentSaveParticipant { + + private _callbacks = new CallbackList(); + private _documents: ExtHostDocuments; + + constructor(documents: ExtHostDocuments) { + this._documents = documents; + } + + dispose(): void { + this._callbacks.dispose(); + } + + get onWillSaveTextDocumentEvent(): Event { + return (listener, thisArg, disposables) => { + this._callbacks.add(listener, thisArg); + const result = { dispose: () => this._callbacks.remove(listener, thisArg) }; + if (Array.isArray(disposables)) { + disposables.push(result); + } + return result; + }; + } + + $participateInSave(resource: URI): TPromise { + const entries = this._callbacks.entries(); + + return sequence(entries.map(([fn, thisArg]) => { + return () => { + const document = this._documents.getDocumentData(resource).document; + return this._deliverEventAsync(fn, thisArg, document); + }; + })); + } + + private _deliverEventAsync(listener: Function, thisArg: any, document: vscode.TextDocument): TPromise { + const promises: TPromise[] = []; + const event = { + waitUntil(p: Thenable) { + promises.push(TPromise.wrap(p)); + } + }; + try { + listener.apply(thisArg, [event]); + } finally { + event.waitUntil = () => { throw illegalState(); }; + return TPromise.join(promises).then(() => void 0, err => void 0 /* ignore */); + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts new file mode 100644 index 00000000000..4934f66e034 --- /dev/null +++ b/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {ExtHostDocumentSaveParticipant, TextDocumentWillSaveEvent} from 'vs/workbench/api/node/extHostDocumentSaveParticipant'; +import {OneGetThreadService} from './testThreadService'; +import * as EditorCommon from 'vs/editor/common/editorCommon'; + +suite('ExtHostDocumentSaveParticipant', () => { + + let resource = URI.parse('foo:bar'); + let documents: ExtHostDocuments; + + setup(() => { + + documents = new ExtHostDocuments(OneGetThreadService(null)); + documents.$acceptModelAdd({ + isDirty: false, + modeId: 'foo', + url: resource, + versionId: 1, + value: { + EOL: '\n', + lines: ['foo'], + BOM: '', + length: -1, + options: { + tabSize: 4, + insertSpaces: true, + trimAutoWhitespace: true, + defaultEOL: EditorCommon.DefaultEndOfLine.LF + } + } + }); + }); + + test('no listeners, no problem', () => { + const participant = new ExtHostDocumentSaveParticipant(documents); + return participant.$participateInSave(resource).then(() => assert.ok(true)); + }); + + test('event delivery', () => { + const participant = new ExtHostDocumentSaveParticipant(documents); + + let event: TextDocumentWillSaveEvent; + let sub = participant.onWillSaveTextDocumentEvent(function (e) { + event = e; + }); + + return participant.$participateInSave(resource).then(() => { + sub.dispose(); + + assert.ok(event); + assert.equal(typeof event.waitUntil, 'function'); + }); + }); + + test('event delivery in subscribe order', () => { + const participant = new ExtHostDocumentSaveParticipant(documents); + + let counter = 0; + let sub1 = participant.onWillSaveTextDocumentEvent(function (event) { + assert.equal(counter++, 0); + }); + + let sub2 = participant.onWillSaveTextDocumentEvent(function (event) { + assert.equal(counter++, 1); + }); + + return participant.$participateInSave(resource).then(() => { + sub1.dispose(); + sub2.dispose(); + }); + }); + + test('event delivery, waitUntil', () => { + const participant = new ExtHostDocumentSaveParticipant(documents); + + let sub = participant.onWillSaveTextDocumentEvent(function (event) { + + event.waitUntil(TPromise.timeout(10)); + event.waitUntil(TPromise.timeout(10)); + event.waitUntil(TPromise.timeout(10)); + }); + + return participant.$participateInSave(resource).then(() => { + sub.dispose(); + }); + + }); + + test('event delivery, waitUntil must be called sync', () => { + const participant = new ExtHostDocumentSaveParticipant(documents); + + let sub = participant.onWillSaveTextDocumentEvent(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).then(() => { + sub.dispose(); + }); + }); + + test('event delivery, waitUntil failure handling', () => { + const participant = new ExtHostDocumentSaveParticipant(documents); + + let sub1 = participant.onWillSaveTextDocumentEvent(function (e) { + e.waitUntil(TPromise.wrapError('dddd')); + }); + + let event: TextDocumentWillSaveEvent; + let sub2 = participant.onWillSaveTextDocumentEvent(function (e) { + event = e; + }); + + return participant.$participateInSave(resource).then(() => { + assert.ok(event); + sub1.dispose(); + sub2.dispose(); + }); + }); + +});