aboutsummaryrefslogtreecommitdiffstats
path: root/node_modules/xterm/src/common/CircularList.ts
blob: 4d2c04eca52c45a6f766a6f6237a825120baa9bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/**
 * Copyright (c) 2016 The xterm.js authors. All rights reserved.
 * @license MIT
 */

import { ICircularList } from 'common/Types';
import { EventEmitter, IEvent } from 'common/EventEmitter';

export interface IInsertEvent {
  index: number;
  amount: number;
}

export interface IDeleteEvent {
  index: number;
  amount: number;
}

/**
 * Represents a circular list; a list with a maximum size that wraps around when push is called,
 * overriding values at the start of the list.
 */
export class CircularList<T> implements ICircularList<T> {
  protected _array: (T | undefined)[];
  private _startIndex: number;
  private _length: number;

  public onDeleteEmitter = new EventEmitter<IDeleteEvent>();
  public get onDelete(): IEvent<IDeleteEvent> { return this.onDeleteEmitter.event; }
  public onInsertEmitter = new EventEmitter<IInsertEvent>();
  public get onInsert(): IEvent<IInsertEvent> { return this.onInsertEmitter.event; }
  public onTrimEmitter = new EventEmitter<number>();
  public get onTrim(): IEvent<number> { return this.onTrimEmitter.event; }

  constructor(
    private _maxLength: number
  ) {
    this._array = new Array<T>(this._maxLength);
    this._startIndex = 0;
    this._length = 0;
  }

  public get maxLength(): number {
    return this._maxLength;
  }

  public set maxLength(newMaxLength: number) {
    // There was no change in maxLength, return early.
    if (this._maxLength === newMaxLength) {
      return;
    }

    // Reconstruct array, starting at index 0. Only transfer values from the
    // indexes 0 to length.
    const newArray = new Array<T | undefined>(newMaxLength);
    for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {
      newArray[i] = this._array[this._getCyclicIndex(i)];
    }
    this._array = newArray;
    this._maxLength = newMaxLength;
    this._startIndex = 0;
  }

  public get length(): number {
    return this._length;
  }

  public set length(newLength: number) {
    if (newLength > this._length) {
      for (let i = this._length; i < newLength; i++) {
        this._array[i] = undefined;
      }
    }
    this._length = newLength;
  }

  /**
   * Gets the value at an index.
   *
   * Note that for performance reasons there is no bounds checking here, the index reference is
   * circular so this should always return a value and never throw.
   * @param index The index of the value to get.
   * @return The value corresponding to the index.
   */
  public get(index: number): T | undefined {
    return this._array[this._getCyclicIndex(index)];
  }

  /**
   * Sets the value at an index.
   *
   * Note that for performance reasons there is no bounds checking here, the index reference is
   * circular so this should always return a value and never throw.
   * @param index The index to set.
   * @param value The value to set.
   */
  public set(index: number, value: T | undefined): void {
    this._array[this._getCyclicIndex(index)] = value;
  }

  /**
   * Pushes a new value onto the list, wrapping around to the start of the array, overriding index 0
   * if the maximum length is reached.
   * @param value The value to push onto the list.
   */
  public push(value: T): void {
    this._array[this._getCyclicIndex(this._length)] = value;
    if (this._length === this._maxLength) {
      this._startIndex = ++this._startIndex % this._maxLength;
      this.onTrimEmitter.fire(1);
    } else {
      this._length++;
    }
  }

  /**
   * Advance ringbuffer index and return current element for recycling.
   * Note: The buffer must be full for this method to work.
   * @throws When the buffer is not full.
   */
  public recycle(): T {
    if (this._length !== this._maxLength) {
      throw new Error('Can only recycle when the buffer is full');
    }
    this._startIndex = ++this._startIndex % this._maxLength;
    this.onTrimEmitter.fire(1);
    return this._array[this._getCyclicIndex(this._length - 1)]!;
  }

  /**
   * Ringbuffer is at max length.
   */
  public get isFull(): boolean {
    return this._length === this._maxLength;
  }

  /**
   * Removes and returns the last value on the list.
   * @return The popped value.
   */
  public pop(): T | undefined {
    return this._array[this._getCyclicIndex(this._length-- - 1)];
  }

  /**
   * Deletes and/or inserts items at a particular index (in that order). Unlike
   * Array.prototype.splice, this operation does not return the deleted items as a new array in
   * order to save creating a new array. Note that this operation may shift all values in the list
   * in the worst case.
   * @param start The index to delete and/or insert.
   * @param deleteCount The number of elements to delete.
   * @param items The items to insert.
   */
  public splice(start: number, deleteCount: number, ...items: T[]): void {
    // Delete items
    if (deleteCount) {
      for (let i = start; i < this._length - deleteCount; i++) {
        this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];
      }
      this._length -= deleteCount;
      this.onDeleteEmitter.fire({ index: start, amount: deleteCount });
    }

    // Add items
    for (let i = this._length - 1; i >= start; i--) {
      this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];
    }
    for (let i = 0; i < items.length; i++) {
      this._array[this._getCyclicIndex(start + i)] = items[i];
    }
    if (items.length) {
      this.onInsertEmitter.fire({ index: start, amount: items.length });
    }

    // Adjust length as needed
    if (this._length + items.length > this._maxLength) {
      const countToTrim = (this._length + items.length) - this._maxLength;
      this._startIndex += countToTrim;
      this._length = this._maxLength;
      this.onTrimEmitter.fire(countToTrim);
    } else {
      this._length += items.length;
    }
  }

  /**
   * Trims a number of items from the start of the list.
   * @param count The number of items to remove.
   */
  public trimStart(count: number): void {
    if (count > this._length) {
      count = this._length;
    }
    this._startIndex += count;
    this._length -= count;
    this.onTrimEmitter.fire(count);
  }

  public shiftElements(start: number, count: number, offset: number): void {
    if (count <= 0) {
      return;
    }
    if (start < 0 || start >= this._length) {
      throw new Error('start argument out of range');
    }
    if (start + offset < 0) {
      throw new Error('Cannot shift elements in list beyond index 0');
    }

    if (offset > 0) {
      for (let i = count - 1; i >= 0; i--) {
        this.set(start + i + offset, this.get(start + i));
      }
      const expandListBy = (start + count + offset) - this._length;
      if (expandListBy > 0) {
        this._length += expandListBy;
        while (this._length > this._maxLength) {
          this._length--;
          this._startIndex++;
          this.onTrimEmitter.fire(1);
        }
      }
    } else {
      for (let i = 0; i < count; i++) {
        this.set(start + i + offset, this.get(start + i));
      }
    }
  }

  /**
   * Gets the cyclic index for the specified regular index. The cyclic index can then be used on the
   * backing array to get the element associated with the regular index.
   * @param index The regular index.
   * @returns The cyclic index.
   */
  private _getCyclicIndex(index: number): number {
    return (this._startIndex + index) % this._maxLength;
  }
}