Maintain decorations sorted by line

This commit is contained in:
Daniel Imms
2022-05-11 09:34:51 -07:00
parent c0b4edaf31
commit 931ee9e89e
3 changed files with 207 additions and 25 deletions
+105
View File
@@ -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<number>;
function assertList(expected: number[]): void {
deepStrictEqual(Array.from(list.values()), expected);
}
beforeEach(() => {
list = new SortedList<number>(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 }
]);
});
});
+80
View File
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
export class SortedList<T> {
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<T> {
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<T> {
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;
}
}
+22 -25
View File
@@ -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<IInternalDecoration> = new SortedList(e => e.marker.line);
private _onDecorationRegistered = this.register(new EventEmitter<IInternalDecoration>());
public get onDecorationRegistered(): IEvent<IInternalDecoration> { 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<IInternalDecoration> {
// 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<IInternalDecoration> {
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();
}
}