From 931ee9e89ef776a10b9659ec2d20695ae1220dfc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 May 2022 09:34:25 -0700 Subject: [PATCH] Maintain decorations sorted by line --- src/common/SortedList.test.ts | 105 +++++++++++++++++++++++ src/common/SortedList.ts | 80 +++++++++++++++++ src/common/services/DecorationService.ts | 47 +++++----- 3 files changed, 207 insertions(+), 25 deletions(-) create mode 100644 src/common/SortedList.test.ts create mode 100644 src/common/SortedList.ts diff --git a/src/common/SortedList.test.ts b/src/common/SortedList.test.ts new file mode 100644 index 00000000..5ecdbb77 --- /dev/null +++ b/src/common/SortedList.test.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { SortedList } from 'common/SortedList'; + +const deepStrictEqual = assert.deepStrictEqual; + +describe('SortedList', () => { + let list: SortedList; + function assertList(expected: number[]): void { + deepStrictEqual(Array.from(list.values()), expected); + } + + beforeEach(() => { + list = new SortedList(e => e); + }); + + describe('insert', () => { + it('should maintain sorted values', () => { + list.insert(10); + assertList([10]); + list.insert(8); + assertList([8, 10]); + list.insert(15); + assertList([8, 10, 15]); + list.insert(2); + assertList([2, 8, 10, 15]); + list.insert(1); + assertList([1, 2, 8, 10, 15]); + list.insert(6); + assertList([1, 2, 6, 8, 10, 15]); + }); + it('should allow duplicates of the same key', () => { + list.insert(5); + assertList([5]); + list.insert(5); + assertList([5, 5]); + list.insert(8); + assertList([5, 5, 8]); + list.insert(5); + assertList([5, 5, 5, 8]); + list.insert(8); + assertList([5, 5, 5, 8, 8]); + list.insert(6); + assertList([5, 5, 5, 6, 8, 8]); + }); + }); + it('delete', () => { + list.insert(1); + list.insert(2); + list.insert(4); + list.insert(3); + list.insert(5); + assertList([1, 2, 3, 4, 5]); + list.delete(1); + assertList([2, 3, 4, 5]); + list.delete(3); + assertList([2, 4, 5]); + list.delete(4); + assertList([2, 5]); + list.delete(5); + assertList([2]); + list.delete(2); + assertList([]); + }); + it('getKeyIterator', () => { + list.insert(5); + list.insert(5); + list.insert(8); + list.insert(5); + list.insert(8); + list.insert(6); + assertList([5, 5, 5, 6, 8, 8]); + deepStrictEqual(Array.from(list.getKeyIterator(5)), [5, 5, 5]); + deepStrictEqual(Array.from(list.getKeyIterator(6)), [6]); + deepStrictEqual(Array.from(list.getKeyIterator(8)), [8, 8]); + }); + it('clear', () => { + list.insert(1); + list.insert(2); + list.insert(4); + list.insert(3); + list.insert(5); + list.clear(); + assertList([]); + }); + it('custom key', () => { + const customList = new SortedList<{ key: number }>(e => e.key); + customList.insert({ key: 5 }); + customList.insert({ key: 2 }); + customList.insert({ key: 10 }); + customList.insert({ key: 5 }); + customList.insert({ key: 6 }); + deepStrictEqual(Array.from(customList.values()), [ + { key: 2 }, + { key: 5 }, + { key: 5 }, + { key: 6 }, + { key: 10 } + ]); + }); +}); diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts new file mode 100644 index 00000000..2112a73f --- /dev/null +++ b/src/common/SortedList.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export class SortedList { + private readonly _array: T[] = []; + + constructor( + private readonly _getKey: (value: T) => number + ) { + } + + public clear(): void { + this._array.length = 0; + } + + public insert(value: T): void { + if (this._array.length === 0) { + this._array.push(value); + return; + } + const i = this._search(this._getKey(value), 0, this._array.length - 1); + this._array.splice(i, 0, value); + } + + public delete(value: T): boolean { + if (this._array.length === 0) { + return false; + } + const key = this._getKey(value); + let i = this._search(key, 0, this._array.length - 1); + if (this._getKey(this._array[i]) !== key) { + return false; + } + do { + if (this._array[i] === value) { + this._array.splice(i, 1); + return true; + } + } while (++i < this._array.length && this._getKey(this._array[i]) === key); + return false; + } + + public *getKeyIterator(key: number): IterableIterator { + if (this._array.length === 0) { + return; + } + let i = this._search(key, 0, this._array.length - 1); + if (this._getKey(this._array[i]) !== key) { + return; + } + do { + yield this._array[i]; + } while (++i < this._array.length && this._getKey(this._array[i]) === key); + } + + public values(): IterableIterator { + return this._array.values(); + } + + private _search(key: number, min: number, max: number): number { + if (max < min) { + return min; + } + let mid = Math.floor((min + max) / 2); + if (this._getKey(this._array[mid]) > key) { + return this._search(key, min, mid - 1); + } + if (this._getKey(this._array[mid]) < key) { + return this._search(key, mid + 1, max); + } + // Value found! Since keys can be duplicates, move the result index back to the lowest index + // that matches the key. + while (mid > 0 && this._getKey(this._array[mid - 1]) === key) { + mid--; + } + return mid; + } +} diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index ed58c813..58718333 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -7,13 +7,19 @@ import { css } from 'common/Color'; import { EventEmitter } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { IDecorationService, IInternalDecoration } from 'common/services/Services'; +import { SortedList } from 'common/SortedList'; import { IColor } from 'common/Types'; import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm'; export class DecorationService extends Disposable implements IDecorationService { public serviceBrand: any; - private readonly _decorations: IInternalDecoration[] = []; + /** + * A list of all decorations, sorted by the marker's line value. This relies on the fact that + * while marker line values do change, they should all change by the same amount so this should + * never become out of order. + */ + private readonly _decorations: SortedList = new SortedList(e => e.marker.line); private _onDecorationRegistered = this.register(new EventEmitter()); public get onDecorationRegistered(): IEvent { return this._onDecorationRegistered.event; } @@ -35,56 +41,47 @@ export class DecorationService extends Disposable implements IDecorationService const markerDispose = decoration.marker.onDispose(() => decoration.dispose()); decoration.onDispose(() => { if (decoration) { - const index = this._decorations.indexOf(decoration); - if (index >= 0) { - this._decorations.splice(this._decorations.indexOf(decoration), 1); + if (this._decorations.delete(decoration)) { this._onDecorationRemoved.fire(decoration); } markerDispose.dispose(); } }); - this._decorations.push(decoration); + this._decorations.insert(decoration); this._onDecorationRegistered.fire(decoration); } return decoration; } public reset(): void { - for (let i = 0; i < this._decorations.length; i++) { - this._decorations[0].dispose(); + for (const d of this._decorations.values()) { + d.dispose(); } - this._decorations.length = 0; + this._decorations.clear(); } public *getDecorationsAtLine(line: number): IterableIterator { - // TODO: This could be made much faster if _decorations was sorted by line (and col?) - for (const d of this.decorations) { - if (d.marker.line === line) { - yield d; - } - } + return this._decorations.getKeyIterator(line); } public *getDecorationsAtCell(x: number, line: number): IterableIterator { let xmin = 0; let xmax = 0; - for (const d of this.decorations) { - if (d.marker.line === line) { - xmin = d.options.x ?? 0; - xmax = xmin + (d.options.width ?? 1); - if (x >= xmin && x < xmax) { - yield d; - } + for (const d of this._decorations.getKeyIterator(line)) { + console.log('d', d); + xmin = d.options.x ?? 0; + xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax) { + yield d; } } } public dispose(): void { - for (const decoration of this._decorations) { - this._onDecorationRemoved.fire(decoration); - decoration.dispose(); + for (const d of this._decorations.values()) { + this._onDecorationRemoved.fire(d); } - this._decorations.length = 0; + this.reset(); } }