diff --git a/src/vs/base/common/callbackList.ts b/src/vs/base/common/callbackList.ts index 9a27d3b844b..96e0781ba87 100644 --- a/src/vs/base/common/callbackList.ts +++ b/src/vs/base/common/callbackList.ts @@ -6,47 +6,21 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { LinkedList } from 'vs/base/common/linkedList'; export default class CallbackList { - private _callbacks: Function[]; - private _contexts: any[]; + private _callbacks: LinkedList<[Function, any]>; - public add(callback: Function, context: any = null, bucket?: IDisposable[]): void { + public add(callback: Function, context: any = null, bucket?: IDisposable[]): () => void { if (!this._callbacks) { - this._callbacks = []; - this._contexts = []; + this._callbacks = new LinkedList<[Function, any]>(); } - this._callbacks.push(callback); - this._contexts.push(context); - + const remove = this._callbacks.insert([callback, context]); if (Array.isArray(bucket)) { - bucket.push({ dispose: () => this.remove(callback, context) }); - } - } - - public remove(callback: Function, context: any = null): void { - if (!this._callbacks) { - return; - } - - let foundCallbackWithDifferentContext = false; - for (let i = 0, len = this._callbacks.length; i < len; i++) { - if (this._callbacks[i] === callback) { - if (this._contexts[i] === context) { - // callback & context match => remove it - this._callbacks.splice(i, 1); - this._contexts.splice(i, 1); - return; - } else { - foundCallbackWithDifferentContext = true; - } - } - } - - if (foundCallbackWithDifferentContext) { - throw new Error('When adding a listener with a context, you should remove it with the same context'); + bucket.push({ dispose: remove }); } + return remove; } public invoke(...args: any[]): any[] { @@ -54,13 +28,12 @@ export default class CallbackList { return undefined; } - const ret: any[] = [], - callbacks = this._callbacks.slice(0), - contexts = this._contexts.slice(0); + const ret: any[] = []; + const elements = this._callbacks.toArray(); - for (let i = 0, len = callbacks.length; i < len; i++) { + for (const [callback, context] of elements) { try { - ret.push(callbacks[i].apply(contexts[i], args)); + ret.push(callback.apply(context, args)); } catch (e) { onUnexpectedError(e); } @@ -68,19 +41,20 @@ export default class CallbackList { return ret; } - public isEmpty(): boolean { - 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]]); + return this._callbacks + ? this._callbacks.toArray() + : []; + } + + public isEmpty(): boolean { + return !this._callbacks || this._callbacks.isEmpty(); } public dispose(): void { this._callbacks = undefined; - this._contexts = undefined; } } diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 9ca573aac02..19ad6dd8657 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -82,7 +82,7 @@ export class Emitter { this._options.onFirstListenerAdd(this); } - this._callbacks.add(listener, thisArgs); + const remove = this._callbacks.add(listener, thisArgs); if (firstListener && this._options && this._options.onFirstListenerDidAdd) { this._options.onFirstListenerDidAdd(this); @@ -97,7 +97,7 @@ export class Emitter { dispose: () => { result.dispose = Emitter._noop; if (!this._disposed) { - this._callbacks.remove(listener, thisArgs); + remove(); if (this._options && this._options.onLastListenerRemove && this._callbacks.isEmpty()) { this._options.onLastListenerRemove(this); } @@ -545,4 +545,4 @@ export class Relay implements IDisposable { this.disposable.dispose(); this.emitter.dispose(); } -} \ No newline at end of file +} diff --git a/src/vs/base/common/linkedList.ts b/src/vs/base/common/linkedList.ts new file mode 100644 index 00000000000..e908f3c0e57 --- /dev/null +++ b/src/vs/base/common/linkedList.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +class Node { + element: E; + next: Node; + prev: Node; + + constructor(element: E) { + this.element = element; + } +} + +export class LinkedList { + + private _first: Node; + private _last: Node; + + isEmpty(): boolean { + return !this._first; + } + + insert(element: E) { + const newNode = new Node(element); + if (!this._first) { + this._first = newNode; + this._last = newNode; + } else { + const oldLast = this._last; + this._last = newNode; + newNode.prev = oldLast; + oldLast.next = newNode; + } + + return () => { + + for (let candidate = this._first; candidate instanceof Node; candidate = candidate.next) { + if (candidate !== newNode) { + continue; + } + if (candidate.prev && candidate.next) { + // middle + let anchor = candidate.prev; + anchor.next = candidate.next; + candidate.next.prev = anchor; + + } else if (!candidate.prev && !candidate.next) { + // only node + this._first = undefined; + this._last = undefined; + + } else if (!candidate.next) { + // last + this._last = this._last.prev; + this._last.next = undefined; + + } else if (!candidate.prev) { + // first + this._first = this._first.next; + this._first.prev = undefined; + } + + // done + break; + } + }; + } + + iterator() { + let _done: boolean; + let _value: E; + let element = { + get done() { return _done; }, + get value() { return _value; } + }; + let node = this._first; + return { + next(): { done: boolean; value: E } { + if (!node) { + _done = true; + _value = undefined; + } else { + _done = false; + _value = node.element; + node = node.next; + } + return element; + } + }; + } + + toArray(): E[] { + let result: E[] = []; + for (let node = this._first; node instanceof Node; node = node.next) { + result.push(node.element); + } + return result; + } +} diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 2e26cb50b0c..c11572642b1 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -179,6 +179,29 @@ suite('Event', function () { } }); + test('reusing event function and context', function () { + let counter = 0; + function listener() { + counter += 1; + } + const context = {}; + + let emitter = new Emitter(); + let reg1 = emitter.event(listener, context); + let reg2 = emitter.event(listener, context); + + emitter.fire(); + assert.equal(counter, 2); + + reg1.dispose(); + emitter.fire(); + assert.equal(counter, 3); + + reg2.dispose(); + emitter.fire(); + assert.equal(counter, 3); + }); + test('Debounce Event', function (done: () => void) { let doc = new Samples.Document3(); @@ -660,4 +683,4 @@ suite('Event utils', () => { assert.deepEqual(result, [1, 2, 3, 4, 5]); }); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/common/linkedList.test.ts b/src/vs/base/test/common/linkedList.test.ts new file mode 100644 index 00000000000..267eccaa806 --- /dev/null +++ b/src/vs/base/test/common/linkedList.test.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { LinkedList } from 'vs/base/common/linkedList'; + +suite('LinkedList', function () { + + function assertElements(list: LinkedList, ...elements: E[]) { + // first: assert toArray + assert.deepEqual(list.toArray(), elements); + + // second: assert iterator + for (let iter = list.iterator(), element = iter.next(); !element.done; element = iter.next()) { + assert.equal(elements.shift(), element.value); + } + assert.equal(elements.length, 0); + } + + test('Insert/Iter', function () { + const list = new LinkedList(); + list.insert(0); + list.insert(1); + list.insert(2); + assertElements(list, 0, 1, 2); + }); + + test('Insert/Remove', function () { + let list = new LinkedList(); + let disp = list.insert(0); + list.insert(1); + list.insert(2); + disp(); + assertElements(list, 1, 2); + + list = new LinkedList(); + list.insert(0); + disp = list.insert(1); + list.insert(2); + disp(); + assertElements(list, 0, 2); + + list = new LinkedList(); + list.insert(0); + list.insert(1); + disp = list.insert(2); + disp(); + assertElements(list, 0, 1); + }); + + test('Insert/toArray', function () { + let list = new LinkedList(); + list.insert('foo'); + list.insert('bar'); + list.insert('far'); + list.insert('boo'); + + assert.deepEqual( + list.toArray(), + [ + 'foo', + 'bar', + 'far', + 'boo', + ] + ); + }); +}); diff --git a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts index f1adf5fdf56..3be0250cf70 100644 --- a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts @@ -37,12 +37,8 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic get onWillSaveTextDocumentEvent(): Event { return (listener, thisArg, disposables) => { - this._callbacks.add(listener, thisArg); - const result = { - dispose: () => { - this._callbacks.remove(listener, thisArg); - } - }; + const remove = this._callbacks.add(listener, thisArg); + const result = { dispose: remove }; if (Array.isArray(disposables)) { disposables.push(result); }