]>
Commit | Line | Data |
---|---|---|
49b76baf DI |
1 | /** |
2 | * Represents a circular list; a list with a maximum size that wraps around when push is called, | |
3 | * overriding values at the start of the list. | |
50cfe2b2 DI |
4 | * @module xterm/utils/CircularList |
5 | * @license MIT | |
49b76baf | 6 | */ |
70fda994 DI |
7 | import { EventEmitter } from '../EventEmitter'; |
8 | ||
207c4cf9 | 9 | // TODO: Do we need the ID here? |
65256c87 DI |
10 | interface ListEntry<T> { |
11 | id: number; | |
12 | value: T; | |
13 | } | |
14 | ||
70fda994 | 15 | export class CircularList<T> extends EventEmitter { |
65256c87 | 16 | private _array: ListEntry<T>[]; |
49b76baf DI |
17 | private _startIndex: number; |
18 | private _length: number; | |
19 | ||
65256c87 DI |
20 | private _nextId = 0; |
21 | ||
49b76baf | 22 | constructor(maxLength: number) { |
70fda994 | 23 | super(); |
65256c87 | 24 | this._array = new Array<ListEntry<T>>(maxLength); |
49b76baf DI |
25 | this._startIndex = 0; |
26 | this._length = 0; | |
27 | } | |
28 | ||
29 | public get maxLength(): number { | |
30 | return this._array.length; | |
31 | } | |
32 | ||
33 | public set maxLength(newMaxLength: number) { | |
34 | // Reconstruct array, starting at index 0. Only transfer values from the | |
35 | // indexes 0 to length. | |
65256c87 DI |
36 | let newArray = new Array<ListEntry<T>>(newMaxLength); |
37 | // Reset ids when maxLength is changed | |
38 | this._nextId = 0; | |
49b76baf | 39 | for (let i = 0; i < Math.min(newMaxLength, this.length); i++) { |
65256c87 DI |
40 | newArray[i] = { |
41 | id: this._nextId++, | |
42 | value: this._array[this._getCyclicIndex(i)].value | |
43 | }; | |
49b76baf DI |
44 | } |
45 | this._array = newArray; | |
46 | this._startIndex = 0; | |
47 | } | |
48 | ||
49 | public get length(): number { | |
50 | return this._length; | |
51 | } | |
52 | ||
53 | public set length(newLength: number) { | |
49b76baf DI |
54 | if (newLength > this._length) { |
55 | for (let i = this._length; i < newLength; i++) { | |
56 | this._array[i] = undefined; | |
57 | } | |
58 | } | |
59 | this._length = newLength; | |
60 | } | |
61 | ||
65256c87 DI |
62 | public get forEach(): (callbackfn: (value: T, index: number) => void) => void { |
63 | return (callbackfn: (value: T, index: number) => void) => { | |
64 | let i = 0; | |
65 | let length = this.length; | |
66 | for (let i = 0; i < length; i++) { | |
67 | callbackfn(this.get(i), i); | |
68 | } | |
69 | }; | |
49b76baf DI |
70 | } |
71 | ||
72 | /** | |
73 | * Gets the value at an index. | |
74 | * | |
75 | * Note that for performance reasons there is no bounds checking here, the index reference is | |
76 | * circular so this should always return a value and never throw. | |
77 | * @param index The index of the value to get. | |
78 | * @return The value corresponding to the index. | |
79 | */ | |
80 | public get(index: number): T { | |
65256c87 DI |
81 | return this.getEntry(index).value; |
82 | } | |
83 | ||
84 | public getEntry(index: number): ListEntry<T> { | |
49b76baf DI |
85 | return this._array[this._getCyclicIndex(index)]; |
86 | } | |
87 | ||
88 | /** | |
89 | * Sets the value at an index. | |
90 | * | |
91 | * Note that for performance reasons there is no bounds checking here, the index reference is | |
92 | * circular so this should always return a value and never throw. | |
93 | * @param index The index to set. | |
94 | * @param value The value to set. | |
95 | */ | |
96 | public set(index: number, value: T): void { | |
65256c87 DI |
97 | this._array[this._getCyclicIndex(index)].value = value; |
98 | } | |
99 | ||
100 | private _setEntry(index: number, entry: ListEntry<T>): void { | |
101 | this._array[this._getCyclicIndex(index)] = entry; | |
49b76baf DI |
102 | } |
103 | ||
104 | /** | |
105 | * Pushes a new value onto the list, wrapping around to the start of the array, overriding index 0 | |
106 | * if the maximum length is reached. | |
107 | * @param value The value to push onto the list. | |
108 | */ | |
109 | public push(value: T): void { | |
65256c87 DI |
110 | this._array[this._getCyclicIndex(this._length)] = { |
111 | id: this._nextId, | |
112 | value | |
113 | }; | |
49b76baf DI |
114 | if (this._length === this.maxLength) { |
115 | this._startIndex++; | |
3b35d12e DI |
116 | if (this._startIndex === this.maxLength) { |
117 | this._startIndex = 0; | |
118 | } | |
70fda994 | 119 | this.emit('trim', 1); |
49b76baf DI |
120 | } else { |
121 | this._length++; | |
122 | } | |
123 | } | |
124 | ||
bfaf9cd6 DI |
125 | /** |
126 | * Removes and returns the last value on the list. | |
127 | * @return The popped value. | |
128 | */ | |
61fbbb00 | 129 | public pop(): T { |
65256c87 | 130 | return this._array[this._getCyclicIndex(this._length-- - 1)].value; |
61fbbb00 DI |
131 | } |
132 | ||
bfaf9cd6 DI |
133 | /** |
134 | * Deletes and/or inserts items at a particular index (in that order). Unlike | |
135 | * Array.prototype.splice, this operation does not return the deleted items as a new array in | |
136 | * order to save creating a new array. Note that this operation may shift all values in the list | |
137 | * in the worst case. | |
138 | * @param start The index to delete and/or insert. | |
139 | * @param deleteCount The number of elements to delete. | |
140 | * @param items The items to insert. | |
141 | */ | |
be2e4b5d | 142 | public splice(start: number, deleteCount: number, ...items: T[]): void { |
65256c87 | 143 | // Delete items |
61fbbb00 DI |
144 | if (deleteCount) { |
145 | for (let i = start; i < this._length - deleteCount; i++) { | |
146 | this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)]; | |
147 | } | |
148 | this._length -= deleteCount; | |
149 | } | |
65256c87 | 150 | |
61fbbb00 | 151 | if (items && items.length) { |
65256c87 | 152 | // Add items |
61fbbb00 DI |
153 | for (let i = this._length - 1; i >= start; i--) { |
154 | this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)]; | |
155 | } | |
156 | for (let i = 0; i < items.length; i++) { | |
65256c87 DI |
157 | this._array[this._getCyclicIndex(start + i)] = { |
158 | id: this._nextId, | |
159 | value: items[i] | |
160 | }; | |
61fbbb00 DI |
161 | } |
162 | ||
65256c87 | 163 | // Adjust length as needed |
61fbbb00 | 164 | if (this._length + items.length > this.maxLength) { |
70fda994 DI |
165 | const countToTrim = (this._length + items.length) - this.maxLength; |
166 | this._startIndex += countToTrim; | |
61fbbb00 | 167 | this._length = this.maxLength; |
70fda994 | 168 | this.emit('trim', countToTrim); |
61fbbb00 DI |
169 | } else { |
170 | this._length += items.length; | |
171 | } | |
172 | } | |
173 | } | |
174 | ||
bfaf9cd6 DI |
175 | /** |
176 | * Trims a number of items from the start of the list. | |
177 | * @param count The number of items to remove. | |
178 | */ | |
3b35d12e | 179 | public trimStart(count: number): void { |
bfaf9cd6 DI |
180 | if (count > this._length) { |
181 | count = this._length; | |
182 | } | |
3b35d12e | 183 | this._startIndex += count; |
bfaf9cd6 | 184 | this._length -= count; |
70fda994 | 185 | this.emit('trim', count); |
3b35d12e DI |
186 | } |
187 | ||
969a2e95 DI |
188 | public shiftElements(start: number, count: number, offset: number): void { |
189 | if (count <= 0) { | |
190 | return; | |
191 | } | |
192 | if (start < 0 || start >= this._length) { | |
193 | throw new Error('start argument out of range'); | |
194 | } | |
195 | if (start + offset < 0) { | |
196 | throw new Error('Cannot shift elements in list beyond index 0'); | |
197 | } | |
198 | ||
199 | if (offset > 0) { | |
200 | for (let i = count - 1; i >= 0; i--) { | |
fe11837a DI |
201 | this._setEntry(start + i + offset, { |
202 | id: this._nextId++, | |
203 | value: this.get(start + i) | |
204 | }); | |
969a2e95 DI |
205 | } |
206 | const expandListBy = (start + count + offset) - this._length; | |
207 | if (expandListBy > 0) { | |
208 | this._length += expandListBy; | |
209 | while (this._length > this.maxLength) { | |
210 | this._length--; | |
211 | this._startIndex++; | |
70fda994 | 212 | this.emit('trim', 1); |
969a2e95 DI |
213 | } |
214 | } | |
215 | } else { | |
216 | for (let i = 0; i < count; i++) { | |
217 | this.set(start + i + offset, this.get(start + i)); | |
218 | } | |
219 | } | |
220 | } | |
221 | ||
bfaf9cd6 DI |
222 | /** |
223 | * Gets the cyclic index for the specified regular index. The cyclic index can then be used on the | |
224 | * backing array to get the element associated with the regular index. | |
225 | * @param index The regular index. | |
226 | * @returns The cyclic index. | |
227 | */ | |
49b76baf DI |
228 | private _getCyclicIndex(index: number): number { |
229 | return (this._startIndex + index) % this.maxLength; | |
230 | } | |
231 | } |