-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathrope.js
286 lines (237 loc) · 8.4 KB
/
rope.js
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
"use strict";
// Rope implemented with skip lists!
//
// Each element in the skip list contains a string, an array of next pointers
// and an array of subtree sizes.
//
// The next pointers work like normal skip lists. Here's some google results:
// http://en.wikipedia.org/wiki/Skip_list
// http://igoro.com/archive/skip-lists-are-fascinating/
//
// The subtree size is the number of characters between the start of the current
// element and the start of the next element at that level in the list.
//
// So, e.subtreesize[4] == e.str.length + no. chars between e and e.nexts[4].
//
//
// I use foo['bar'] syntax in a bunch of places to stop the closure compiler renaming
// exported methods.
// The split size is the maximum number of characters to have in each element
// in the list before splitting it out into multiple elements.
// Benchmarking reveals 512 to be a pretty good number for this.
const SPLIT_SIZE = 512
// Each skip list element has height >= H with P=bias^(H-1).
//
// I ran some benchmarks, expecting 0.5 to get the best speed. But, for some reason,
// the speed is a bit better around 0.62
const bias = 0.62
//inspect = require('util').inspect
const randomHeight = () => {
let length = 1
// This method uses successive bits of a random number to figure out whick skip lists
// to be part of. It is faster than the method below, but doesn't support weird biases.
// It turns out, it is slightly faster to have non-0.5 bias and that offsets the cost of
// calling random() more times (at least in v8)
// r = Math.random() * 2
// while r > 1
// r = (r - 1) * 2
// length++
while (Math.random() > bias) length++
return length
}
class Rope {
constructor(str) {
if (!(this instanceof Rope)) return new Rope(str)
this.head = {
nexts: [],
subtreesize: []
}
this.length = 0
if (str != null) this.insert(0, str)
}
forEach(fn) {
for (const s of this) fn(s)
}
toString() {
const strings = []
this.forEach(str => strings.push(str))
return strings.join('')
}
toJSON() { return this.toString() }
*[Symbol.iterator]() {
// Skip the head, since it has no string.
let e = this.head.nexts[0]
while (e) {
yield e.str
e = e.nexts[0]
}
}
// Navigate to a particular position in the string. Returns a cursor at that position.
seek(offset) {
if (typeof offset !== 'number') throw new Error('position must be a number')
if (offset < 0 || offset > this.length) {
throw new Error("pos " + offset + " must be within the rope (" + this.length + ")")
}
let e = this.head
const nodes = new Array(this.head.nexts.length)
const subtreesize = new Array(this.head.nexts.length)
if (e.nexts.length > 0) {
// Iterate backwards through the list.
let h = e.nexts.length
while (h--) {
while (offset > e.subtreesize[h]) {
offset -= e.subtreesize[h]
e = e.nexts[h]
}
subtreesize[h] = offset
nodes[h] = e
}
}
return [e, offset, nodes, subtreesize]
}
_spliceIn(nodes, subtreesize, insertPos, str) {
// This function splices the given string into the rope at the specified
// cursor. The cursor is moved to the end of the string.
const height = randomHeight()
const newE = {
str: str,
nexts: new Array(height),
subtreesize: new Array(height)
}
for (let i = 0; i < height; i++) {
if (i < this.head.nexts.length) {
newE.nexts[i] = nodes[i].nexts[i]
nodes[i].nexts[i] = newE
newE.subtreesize[i] = str.length + nodes[i].subtreesize[i] - subtreesize[i]
nodes[i].subtreesize[i] = subtreesize[i]
} else {
newE.nexts[i] = null
newE.subtreesize[i] = this.length - insertPos + str.length
this.head.nexts.push(newE)
this.head.subtreesize.push(insertPos)
}
nodes[i] = newE
subtreesize[i] = str.length
}
if (height < nodes.length) {
for (let i = height; i < nodes.length; i++) {
nodes[i].subtreesize[i] += str.length
subtreesize[i] += str.length
}
}
insertPos += str.length
this.length += str.length
return insertPos;
}
_updateLength(nodes, length) {
for (let i = 0; i < nodes.length; i++) {
nodes[i].subtreesize[i] += length
}
this.length += length
}
insert(insertPos, str) {
if (typeof str !== 'string') throw new Error('inserted text must be a string')
// The spread operator isn't in nodejs yet.
const cursor = this.seek(insertPos)
const [e, offset, nodes, subtreesize] = cursor
if (e.str != null && e.str.length + str.length < SPLIT_SIZE) {
// The new string will fit in the end of the current item
e.str = e.str.slice(0, offset) + str + e.str.slice(offset)
this._updateLength(nodes, str.length)
} else {
// Insert a new item
// If there's stuff at the end of the current item, we'll remove it for now:
let end = ''
if (e.str != null && e.str.length > offset) {
end = e.str.slice(offset)
e.str = e.str.slice(0, offset)
this._updateLength(nodes, -end.length)
}
// Split up the new string based on SPLIT_SIZE and insert each chunk.
for (let i = 0; i < str.length; i += SPLIT_SIZE) {
insertPos = this._spliceIn(nodes, subtreesize, insertPos, str.slice(i, i + SPLIT_SIZE))
}
if (end !== '') this._spliceIn(nodes, subtreesize, insertPos, end)
}
// For chaining.
return this
}
// Delete characters at the specified position. This function returns this
// for chaining, but if you want the deleted characters back you can pass a
// function to recieve them. It'll get called syncronously.
del(delPos, length, getDeleted) {
if (delPos < 0 || delPos + length > this.length) {
throw new Error(`positions #{delPos} and #{delPos + length} must be within the rope (#{this.length})`)
}
// Only collect strings if we need to.
let strings = getDeleted != null ? [] : null
const cursor = this.seek(delPos)
let e = cursor[0], offset = cursor[1], nodes = cursor[2]
this.length -= length
while (length > 0) {
// Delete up to length from e.
if (e.str == null || offset === e.str.length) {
// Move along to the next node.
e = nodes[0].nexts[0]
offset = 0
}
let removed = Math.min(length, e.str.length - offset)
if (removed < e.str.length) {
// We aren't removing the whole node.
if (strings != null) strings.push(e.str.slice(offset, offset + removed))
// Splice out the string
e.str = e.str.slice(0, offset) + e.str.slice(offset + removed)
for (let i = 0; i < nodes.length; i++) {
if (i < e.nexts.length) {
e.subtreesize[i] -= removed
} else {
nodes[i].subtreesize[i] -= removed
}
}
} else {
// Remove the whole node.
if (strings != null) strings.push(e.str)
// Unlink the element.
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
if (i < e.nexts.length) {
node.subtreesize[i] = nodes[i].subtreesize[i] + e.subtreesize[i] - removed
node.nexts[i] = e.nexts[i]
} else {
node.subtreesize[i] -= removed
}
}
// It would probably be better to make a little object pool here.
e = e.nexts[0]
}
length -= removed
}
if (getDeleted) getDeleted(strings.join(''))
return this;
}
// Extract a substring at the specified offset and of the specified length
substring(offsetIn, length) {
if (offsetIn < 0 || offsetIn + length > this.length) {
throw new Error(`Substring (#{offsetIn}-#{offsetIn+length} outside rope (length #{this.length})`);
}
let [e, offset] = this.seek(offsetIn)
const strings = []
if (e.str == null) e = e.nexts[0]
while (e && length > 0) {
let s = e.str.slice(offset, offset + length)
strings.push(s)
offset = 0
length -= s.length
e = e.nexts[0]
}
return strings.join('')
}
// For backwards compatibility.
each(fn) { this.forEach(fn) }
search(offset) { return this.seek(offset) }
}
module.exports = Rope;
// Uncomment these functions in order to run the split size test or the bias test.
// They have been removed to keep the compiled size down.
// Rope.setSplitSize = s => splitSize = s
// Rope.setBias = n => bias = n