Skip to content

Commit 3cedfbe

Browse files
authored
Merge branch 'master' into osc52
2 parents 24b04e9 + 0d799ba commit 3cedfbe

File tree

5 files changed

+124
-16
lines changed

5 files changed

+124
-16
lines changed

demo/client.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ if ('WebAssembly' in window) {
4646

4747
// Pulling in the module's types relies on the <reference> above, it's looks a
4848
// little weird here as we're importing "this" module
49-
import { Terminal as TerminalType, ITerminalOptions } from '@xterm/xterm';
49+
import { Terminal as TerminalType, ITerminalOptions, type IDisposable } from '@xterm/xterm';
5050

5151
export interface IWindowWithTerminal extends Window {
5252
term: TerminalType;
@@ -263,6 +263,7 @@ if (document.location.pathname === '/test') {
263263
document.getElementById('add-grapheme-clusters').addEventListener('click', addGraphemeClusters);
264264
document.getElementById('add-decoration').addEventListener('click', addDecoration);
265265
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
266+
document.getElementById('decoration-stress-test').addEventListener('click', decorationStressTest);
266267
document.getElementById('weblinks-test').addEventListener('click', testWeblinks);
267268
document.getElementById('bce').addEventListener('click', coloredErase);
268269
addVtButtons();
@@ -1180,6 +1181,33 @@ function addOverviewRuler(): void {
11801181
term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' } });
11811182
}
11821183

1184+
let decorationStressTestDecorations: IDisposable[] | undefined;
1185+
function decorationStressTest(): void {
1186+
if (decorationStressTestDecorations) {
1187+
for (const d of decorationStressTestDecorations) {
1188+
d.dispose();
1189+
}
1190+
decorationStressTestDecorations = undefined;
1191+
} else {
1192+
const t = term as Terminal;
1193+
const buffer = t.buffer.active;
1194+
const cursorY = buffer.baseY + buffer.cursorY;
1195+
decorationStressTestDecorations = [];
1196+
for (const x of [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]) {
1197+
for (let y = 0; y < t.buffer.active.length; y++) {
1198+
const cursorOffsetY = y - cursorY;
1199+
decorationStressTestDecorations.push(t.registerDecoration({
1200+
marker: t.registerMarker(cursorOffsetY),
1201+
x,
1202+
width: 4,
1203+
backgroundColor: '#FF0000',
1204+
overviewRulerOptions: { color: '#FF0000' }
1205+
}));
1206+
}
1207+
}
1208+
}
1209+
}
1210+
11831211
(console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => {
11841212
function getBox(width: number, height: number): any {
11851213
return {

demo/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ <h3>Test</h3>
102102
<dt>Decorations</dt>
103103
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
104104
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
105+
<dd><button id="decoration-stress-test" title="Toggle between adding and removing a decoration to each line">Stress Test</button></dd>
105106

106107
<dt>Weblinks Addon</dt>
107108
<dd><button id="weblinks-test" title="Various url conditions from demo data, hover&click to test">Test URLs</button></dd>

src/browser/public/Terminal.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOption
2020
*/
2121
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
2222

23+
let $value = 0;
24+
2325
export class Terminal extends Disposable implements ITerminalApi {
2426
private _core: ITerminal;
2527
private _addonManager: AddonManager;
@@ -249,16 +251,16 @@ export class Terminal extends Disposable implements ITerminalApi {
249251
}
250252

251253
private _verifyIntegers(...values: number[]): void {
252-
for (const value of values) {
253-
if (value === Infinity || isNaN(value) || value % 1 !== 0) {
254+
for ($value of values) {
255+
if ($value === Infinity || isNaN($value) || $value % 1 !== 0) {
254256
throw new Error('This API only accepts integers');
255257
}
256258
}
257259
}
258260

259261
private _verifyPositiveIntegers(...values: number[]): void {
260-
for (const value of values) {
261-
if (value && (value === Infinity || isNaN(value) || value % 1 !== 0 || value < 0)) {
262+
for ($value of values) {
263+
if ($value && ($value === Infinity || isNaN($value) || $value % 1 !== 0 || $value < 0)) {
262264
throw new Error('This API only accepts positive integers');
263265
}
264266
}

src/common/SortedList.ts

+86-10
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,27 @@
33
* @license MIT
44
*/
55

6+
import { IdleTaskQueue } from 'common/TaskQueue';
7+
68
// Work variables to avoid garbage collection.
79
let i = 0;
810

911
/**
10-
* A generic list that is maintained in sorted order and allows values with duplicate keys. This
11-
* list is based on binary search and as such locating a key will take O(log n) amortized, this
12-
* includes the by key iterator.
12+
* A generic list that is maintained in sorted order and allows values with duplicate keys. Deferred
13+
* batch insertion and deletion is used to significantly reduce the time it takes to insert and
14+
* delete a large amount of items in succession. This list is based on binary search and as such
15+
* locating a key will take O(log n) amortized, this includes the by key iterator.
1316
*/
1417
export class SortedList<T> {
15-
private readonly _array: T[] = [];
18+
private _array: T[] = [];
19+
20+
private readonly _insertedValues: T[] = [];
21+
private readonly _flushInsertedTask = new IdleTaskQueue();
22+
private _isFlushingInserted = false;
23+
24+
private readonly _deletedIndices: number[] = [];
25+
private readonly _flushDeletedTask = new IdleTaskQueue();
26+
private _isFlushingDeleted = false;
1627

1728
constructor(
1829
private readonly _getKey: (value: T) => number
@@ -21,18 +32,50 @@ export class SortedList<T> {
2132

2233
public clear(): void {
2334
this._array.length = 0;
35+
this._insertedValues.length = 0;
36+
this._flushInsertedTask.clear();
37+
this._isFlushingInserted = false;
38+
this._deletedIndices.length = 0;
39+
this._flushDeletedTask.clear();
40+
this._isFlushingDeleted = false;
2441
}
2542

2643
public insert(value: T): void {
27-
if (this._array.length === 0) {
28-
this._array.push(value);
29-
return;
44+
this._flushCleanupDeleted();
45+
if (this._insertedValues.length === 0) {
46+
this._flushInsertedTask.enqueue(() => this._flushInserted());
47+
}
48+
this._insertedValues.push(value);
49+
}
50+
51+
private _flushInserted(): void {
52+
const sortedAddedValues = this._insertedValues.sort((a, b) => this._getKey(a) - this._getKey(b));
53+
let sortedAddedValuesIndex = 0;
54+
let arrayIndex = 0;
55+
56+
const newArray = new Array(this._array.length + this._insertedValues.length);
57+
58+
for (let newArrayIndex = 0; newArrayIndex < newArray.length; newArrayIndex++) {
59+
if (arrayIndex >= this._array.length || this._getKey(sortedAddedValues[sortedAddedValuesIndex]) <= this._getKey(this._array[arrayIndex])) {
60+
newArray[newArrayIndex] = sortedAddedValues[sortedAddedValuesIndex];
61+
sortedAddedValuesIndex++;
62+
} else {
63+
newArray[newArrayIndex] = this._array[arrayIndex++];
64+
}
65+
}
66+
67+
this._array = newArray;
68+
this._insertedValues.length = 0;
69+
}
70+
71+
private _flushCleanupInserted(): void {
72+
if (!this._isFlushingInserted && this._insertedValues.length > 0) {
73+
this._flushInsertedTask.flush();
3074
}
31-
i = this._search(this._getKey(value));
32-
this._array.splice(i, 0, value);
3375
}
3476

3577
public delete(value: T): boolean {
78+
this._flushCleanupInserted();
3679
if (this._array.length === 0) {
3780
return false;
3881
}
@@ -49,14 +92,43 @@ export class SortedList<T> {
4992
}
5093
do {
5194
if (this._array[i] === value) {
52-
this._array.splice(i, 1);
95+
if (this._deletedIndices.length === 0) {
96+
this._flushDeletedTask.enqueue(() => this._flushDeleted());
97+
}
98+
this._deletedIndices.push(i);
5399
return true;
54100
}
55101
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
56102
return false;
57103
}
58104

105+
private _flushDeleted(): void {
106+
this._isFlushingDeleted = true;
107+
const sortedDeletedIndices = this._deletedIndices.sort((a, b) => a - b);
108+
let sortedDeletedIndicesIndex = 0;
109+
const newArray = new Array(this._array.length - sortedDeletedIndices.length);
110+
let newArrayIndex = 0;
111+
for (let i = 0; i < this._array.length; i++) {
112+
if (sortedDeletedIndices[sortedDeletedIndicesIndex] === i) {
113+
sortedDeletedIndicesIndex++;
114+
} else {
115+
newArray[newArrayIndex++] = this._array[i];
116+
}
117+
}
118+
this._array = newArray;
119+
this._deletedIndices.length = 0;
120+
this._isFlushingDeleted = false;
121+
}
122+
123+
private _flushCleanupDeleted(): void {
124+
if (!this._isFlushingDeleted && this._deletedIndices.length > 0) {
125+
this._flushDeletedTask.flush();
126+
}
127+
}
128+
59129
public *getKeyIterator(key: number): IterableIterator<T> {
130+
this._flushCleanupInserted();
131+
this._flushCleanupDeleted();
60132
if (this._array.length === 0) {
61133
return;
62134
}
@@ -73,6 +145,8 @@ export class SortedList<T> {
73145
}
74146

75147
public forEachByKey(key: number, callback: (value: T) => void): void {
148+
this._flushCleanupInserted();
149+
this._flushCleanupDeleted();
76150
if (this._array.length === 0) {
77151
return;
78152
}
@@ -89,6 +163,8 @@ export class SortedList<T> {
89163
}
90164

91165
public values(): IterableIterator<T> {
166+
this._flushCleanupInserted();
167+
this._flushCleanupDeleted();
92168
// Duplicate the array to avoid issues when _array changes while iterating
93169
return [...this._array].values();
94170
}

src/common/services/DecorationService.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ export class DecorationService extends Disposable implements IDecorationService
4545
const decoration = new Decoration(options);
4646
if (decoration) {
4747
const markerDispose = decoration.marker.onDispose(() => decoration.dispose());
48-
decoration.onDispose(() => {
48+
const listener = decoration.onDispose(() => {
49+
listener.dispose();
4950
if (decoration) {
5051
if (this._decorations.delete(decoration)) {
5152
this._onDecorationRemoved.fire(decoration);

0 commit comments

Comments
 (0)