@adecore/editor-core 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/block-folds.js +5 -2
  2. package/dist/bracket-tracker.js +4 -3
  3. package/dist/brackets.d.ts +2 -12
  4. package/dist/brackets.js +3 -15
  5. package/dist/closer-tracker.js +0 -1
  6. package/dist/doc-comments.js +0 -1
  7. package/dist/document.d.ts +2 -0
  8. package/dist/document.js +33 -35
  9. package/dist/edit-regions.js +3 -4
  10. package/dist/enter.d.ts +2 -0
  11. package/dist/enter.js +11 -9
  12. package/dist/fold-roles.js +2 -2
  13. package/dist/lexical-folds.d.ts +0 -1
  14. package/dist/lexical-folds.js +1 -4
  15. package/dist/lexical.d.ts +11 -5
  16. package/dist/lexical.js +13 -17
  17. package/dist/line-commands.js +7 -15
  18. package/dist/line-text.d.ts +2 -0
  19. package/dist/line-text.js +4 -0
  20. package/dist/markup-regions.js +2 -3
  21. package/dist/paste.js +2 -7
  22. package/dist/structure.js +2 -1
  23. package/dist/typing-context.js +5 -15
  24. package/package.json +2 -2
  25. package/src/block-folds.ts +39 -2
  26. package/src/bracket-tracker.ts +55 -12
  27. package/src/brackets.ts +35 -42
  28. package/src/closer-tracker.ts +6 -1
  29. package/src/comments.ts +103 -0
  30. package/src/doc-comments.ts +19 -1
  31. package/src/document.ts +505 -36
  32. package/src/edit-regions.ts +26 -4
  33. package/src/enter.ts +104 -9
  34. package/src/fold-roles.ts +34 -2
  35. package/src/index.ts +2 -0
  36. package/src/languages.ts +11 -0
  37. package/src/lexical-folds.ts +21 -5
  38. package/src/lexical.ts +31 -23
  39. package/src/line-commands.ts +81 -17
  40. package/src/line-text.ts +7 -0
  41. package/src/markup-regions.ts +101 -3
  42. package/src/offsets.ts +10 -0
  43. package/src/paste.ts +20 -7
  44. package/src/php-markup.ts +23 -0
  45. package/src/preserve-case.ts +12 -0
  46. package/src/rope.ts +70 -0
  47. package/src/script-source.ts +10 -0
  48. package/src/search.ts +38 -0
  49. package/src/structure.ts +29 -1
  50. package/src/text-span.ts +53 -0
  51. package/src/typing-context.ts +65 -41
  52. package/src/typing-handlers.ts +28 -0
  53. package/src/words.ts +10 -0
@@ -11,28 +11,36 @@ export function replaceWithCaseRespect(replacement: string, found: string): stri
11
11
  if (found === '' || replacement === '') {
12
12
  return replacement;
13
13
  }
14
+
14
15
  const [firstFound] = found;
15
16
  const [firstReplacement] = replacement;
16
17
  const head = upper.test(firstFound!) ? firstReplacement!.toUpperCase() : firstReplacement!.toLowerCase();
17
18
  const replacementTail = replacement.slice(firstReplacement!.length);
19
+
18
20
  if (replacementTail === '') {
19
21
  return head;
20
22
  }
23
+
21
24
  const foundTail = found.slice(firstFound!.length);
25
+
22
26
  if (foundTail === '') {
23
27
  return head + replacementTail;
24
28
  }
29
+
25
30
  const replacementCase = letterCase(replacementTail);
26
31
  const foundCase = letterCase(foundTail);
27
32
  // A tail without letters, like `x2`, follows the first letter of the match.
28
33
  const tailUpper = foundCase.hasLetters ? foundCase.upper : upper.test(firstFound!);
29
34
  const tailLower = foundCase.hasLetters ? foundCase.lower : lower.test(firstFound!);
35
+
30
36
  if (tailUpper && (replacementCase.lower || !replacementCase.upper)) {
31
37
  return head + replacementTail.toUpperCase();
32
38
  }
39
+
33
40
  if (tailLower && (replacementCase.lower || replacementCase.upper)) {
34
41
  return head + replacementTail.toLowerCase();
35
42
  }
43
+
36
44
  return head + replacementTail;
37
45
  }
38
46
 
@@ -41,16 +49,20 @@ function letterCase(text: string): { upper: boolean; lower: boolean; hasLetters:
41
49
  let allUpper = true;
42
50
  let allLower = true;
43
51
  let hasLetters = false;
52
+
44
53
  for (const character of text) {
45
54
  if (!letter.test(character)) {
46
55
  continue;
47
56
  }
57
+
48
58
  hasLetters = true;
49
59
  allUpper &&= upper.test(character);
50
60
  allLower &&= lower.test(character);
61
+
51
62
  if (!allUpper && !allLower) {
52
63
  break;
53
64
  }
54
65
  }
66
+
55
67
  return { upper: allUpper, lower: allLower, hasLetters };
56
68
  }
package/src/rope.ts CHANGED
@@ -16,6 +16,7 @@ type Edges = { readonly startsWithLf: boolean; readonly endsWithCr: boolean };
16
16
  type Leaf = Edges & { readonly text: string; readonly breaks: readonly number[]; readonly length: number; readonly lines: number; readonly height: number };
17
17
  type Branch = Edges & { readonly left: Node; readonly right: Node; readonly length: number; readonly lines: number; readonly height: number };
18
18
  type Node = Leaf | Branch;
19
+
19
20
  export const ROPE_CHUNK_SIZE = 2048;
20
21
 
21
22
  const CR = 13;
@@ -30,13 +31,17 @@ function leaf(text: string): Leaf | null {
30
31
  if (!text.length) {
31
32
  return null;
32
33
  }
34
+
33
35
  const breaks: number[] = [];
36
+
34
37
  for (let index = 0; index < text.length; index++) {
35
38
  const code = text.charCodeAt(index);
39
+
36
40
  if (code === LF || (code === CR && text.charCodeAt(index + 1) !== LF)) {
37
41
  breaks.push(index);
38
42
  }
39
43
  }
44
+
40
45
  return {
41
46
  text,
42
47
  breaks,
@@ -65,16 +70,22 @@ function balance(left: Node, right: Node): Node {
65
70
  if (left.left.height >= left.right.height) {
66
71
  return branch(left.left, branch(left.right, right));
67
72
  }
73
+
68
74
  const middle = left.right as Branch;
75
+
69
76
  return branch(branch(left.left, middle.left), branch(middle.right, right));
70
77
  }
78
+
71
79
  if (right.height > left.height + 1 && 'left' in right) {
72
80
  if (right.right.height >= right.left.height) {
73
81
  return branch(branch(left, right.left), right.right);
74
82
  }
83
+
75
84
  const middle = right.left as Branch;
85
+
76
86
  return branch(branch(left, middle.left), branch(middle.right, right.right));
77
87
  }
88
+
78
89
  return branch(left, right);
79
90
  }
80
91
 
@@ -82,18 +93,23 @@ function join(left: Node | null, right: Node | null): Node | null {
82
93
  if (!left) {
83
94
  return right;
84
95
  }
96
+
85
97
  if (!right) {
86
98
  return left;
87
99
  }
100
+
88
101
  if ('text' in left && 'text' in right && left.length + right.length <= ROPE_CHUNK_SIZE) {
89
102
  return leaf(left.text + right.text);
90
103
  }
104
+
91
105
  if (left.height > right.height + 1 && 'left' in left) {
92
106
  return balance(left.left, join(left.right, right)!);
93
107
  }
108
+
94
109
  if (right.height > left.height + 1 && 'left' in right) {
95
110
  return balance(join(left, right.left)!, right.right);
96
111
  }
112
+
97
113
  return branch(left, right);
98
114
  }
99
115
 
@@ -101,35 +117,47 @@ function split(node: Node | null, offset: number): [Node | null, Node | null] {
101
117
  if (!node || offset <= 0) {
102
118
  return [null, node];
103
119
  }
120
+
104
121
  if (offset >= node.length) {
105
122
  return [node, null];
106
123
  }
124
+
107
125
  if ('text' in node) {
108
126
  return [leaf(node.text.slice(0, offset)), leaf(node.text.slice(offset))];
109
127
  }
128
+
110
129
  if (offset < node.left.length) {
111
130
  const [before, after] = split(node.left, offset);
131
+
112
132
  return [before, join(after, node.right)];
113
133
  }
134
+
114
135
  const [before, after] = split(node.right, offset - node.left.length);
136
+
115
137
  return [join(node.left, before), after];
116
138
  }
117
139
 
118
140
  function build(text: string): Node | null {
119
141
  const leaves: Node[] = [];
142
+
120
143
  for (let offset = 0; offset < text.length; offset += ROPE_CHUNK_SIZE) {
121
144
  leaves.push(leaf(text.slice(offset, offset + ROPE_CHUNK_SIZE))!);
122
145
  }
146
+
123
147
  const tree = (from: number, to: number): Node | null => {
124
148
  if (from === to) {
125
149
  return null;
126
150
  }
151
+
127
152
  if (to - from === 1) {
128
153
  return leaves[from]!;
129
154
  }
155
+
130
156
  const middle = (from + to) >>> 1;
157
+
131
158
  return branch(tree(from, middle)!, tree(middle, to)!);
132
159
  };
160
+
133
161
  return tree(0, leaves.length);
134
162
  }
135
163
 
@@ -137,13 +165,17 @@ function collect(node: Node | null, from: number, to: number, chunks: string[]):
137
165
  if (!node || from >= to) {
138
166
  return;
139
167
  }
168
+
140
169
  if ('text' in node) {
141
170
  chunks.push(node.text.slice(from, to));
171
+
142
172
  return;
143
173
  }
174
+
144
175
  if (from < node.left.length) {
145
176
  collect(node.left, from, Math.min(to, node.left.length), chunks);
146
177
  }
178
+
147
179
  if (to > node.left.length) {
148
180
  collect(node.right, Math.max(0, from - node.left.length), to - node.left.length, chunks);
149
181
  }
@@ -153,13 +185,17 @@ function* chunksOf(node: Node | null, from: number, to: number): Generator<strin
153
185
  if (!node || from >= to) {
154
186
  return;
155
187
  }
188
+
156
189
  if ('text' in node) {
157
190
  yield node.text.slice(from, to);
191
+
158
192
  return;
159
193
  }
194
+
160
195
  if (from < node.left.length) {
161
196
  yield* chunksOf(node.left, from, Math.min(to, node.left.length));
162
197
  }
198
+
163
199
  if (to > node.left.length) {
164
200
  yield* chunksOf(node.right, Math.max(0, from - node.left.length), to - node.left.length);
165
201
  }
@@ -181,9 +217,11 @@ export class TextRope {
181
217
  static from(text: string): TextRope {
182
218
  return new TextRope(build(text));
183
219
  }
220
+
184
221
  get length(): number {
185
222
  return this.root?.length ?? 0;
186
223
  }
224
+
187
225
  get lineCount(): number {
188
226
  return (this.root?.lines ?? 0) + 1;
189
227
  }
@@ -192,7 +230,9 @@ export class TextRope {
192
230
  if (offset < 0 || offset >= this.length) {
193
231
  return '';
194
232
  }
233
+
195
234
  let node = this.root!;
235
+
196
236
  while ('left' in node) {
197
237
  if (offset < node.left.length) {
198
238
  node = node.left;
@@ -201,18 +241,21 @@ export class TextRope {
201
241
  node = node.right;
202
242
  }
203
243
  }
244
+
204
245
  return node.text.charAt(offset);
205
246
  }
206
247
 
207
248
  slice(from = 0, to = this.length): string {
208
249
  const chunks: string[] = [];
209
250
  collect(this.root, clampInteger(from, this.length), clampInteger(to, this.length), chunks);
251
+
210
252
  return chunks.join('');
211
253
  }
212
254
 
213
255
  replace(from: number, to: number, text: string): TextRope {
214
256
  const [before, rest] = split(this.root, from);
215
257
  const [, after] = split(rest, to - from);
258
+
216
259
  return new TextRope(join(join(before, build(text)), after));
217
260
  }
218
261
 
@@ -223,26 +266,33 @@ export class TextRope {
223
266
  let column = 0;
224
267
  let offset = 0;
225
268
  const pieces: string[] = [];
269
+
226
270
  const advance = (end: number, keep: boolean): void => {
227
271
  while (offset < end && !chunk.done) {
228
272
  const count = Math.min(end - offset, chunk.value.length - column);
273
+
229
274
  if (keep) {
230
275
  pieces.push(chunk.value.slice(column, column + count));
231
276
  }
277
+
232
278
  offset += count;
233
279
  column += count;
280
+
234
281
  if (column === chunk.value.length) {
235
282
  chunk = chunks.next();
236
283
  column = 0;
237
284
  }
238
285
  }
239
286
  };
287
+
240
288
  for (const edit of edits) {
241
289
  advance(edit.from, true);
242
290
  pieces.push(edit.text);
243
291
  advance(edit.to, false);
244
292
  }
293
+
245
294
  advance(this.length, true);
295
+
246
296
  return TextRope.from(pieces.join(''));
247
297
  }
248
298
 
@@ -251,28 +301,35 @@ export class TextRope {
251
301
  if (this.root === other.root) {
252
302
  return true;
253
303
  }
304
+
254
305
  const left = chunksOf(this.root, from, to);
255
306
  const right = chunksOf(other.root, from, to);
256
307
  let leftChunk = left.next();
257
308
  let rightChunk = right.next();
258
309
  let leftAt = 0;
259
310
  let rightAt = 0;
311
+
260
312
  while (!leftChunk.done && !rightChunk.done) {
261
313
  const length = Math.min(leftChunk.value.length - leftAt, rightChunk.value.length - rightAt);
314
+
262
315
  if (leftChunk.value.slice(leftAt, leftAt + length) !== rightChunk.value.slice(rightAt, rightAt + length)) {
263
316
  return false;
264
317
  }
318
+
265
319
  leftAt += length;
266
320
  rightAt += length;
321
+
267
322
  if (leftAt === leftChunk.value.length) {
268
323
  leftChunk = left.next();
269
324
  leftAt = 0;
270
325
  }
326
+
271
327
  if (rightAt === rightChunk.value.length) {
272
328
  rightChunk = right.next();
273
329
  rightAt = 0;
274
330
  }
275
331
  }
332
+
276
333
  return Boolean(leftChunk.done && rightChunk.done);
277
334
  }
278
335
 
@@ -280,9 +337,11 @@ export class TextRope {
280
337
  offset = clampInteger(offset, this.length);
281
338
  let node = this.root;
282
339
  let count = 0;
340
+
283
341
  if (!node) {
284
342
  return count;
285
343
  }
344
+
286
345
  while ('left' in node) {
287
346
  if (offset < node.left.length) {
288
347
  node = node.left;
@@ -292,40 +351,50 @@ export class TextRope {
292
351
  node = node.right;
293
352
  }
294
353
  }
354
+
295
355
  let low = 0;
296
356
  let high = node.breaks.length;
357
+
297
358
  while (low < high) {
298
359
  const middle = (low + high) >>> 1;
360
+
299
361
  if (node.breaks[middle]! < offset) {
300
362
  low = middle + 1;
301
363
  } else {
302
364
  high = middle;
303
365
  }
304
366
  }
367
+
305
368
  return count + low;
306
369
  }
307
370
 
308
371
  lineBounds(index: number): Omit<DocumentLine, 'text'> {
309
372
  index = clampInteger(index, this.lineCount - 1);
310
373
  const start = index === 0 ? 0 : this.breakAt(index - 1) + 1;
374
+
311
375
  if (index === this.lineCount - 1) {
312
376
  return { start, end: this.length, next: this.length };
313
377
  }
378
+
314
379
  const newline = this.breakAt(index);
315
380
  const crlf = this.charAt(newline) === '\n' && this.charAt(newline - 1) === '\r';
381
+
316
382
  return { start, end: newline - (crlf ? 1 : 0), next: newline + 1 };
317
383
  }
318
384
 
319
385
  getLine(index: number): DocumentLine {
320
386
  const bounds = this.lineBounds(index);
387
+
321
388
  return { ...bounds, text: this.slice(bounds.start, bounds.end) };
322
389
  }
323
390
 
324
391
  private breakAt(index: number): number {
325
392
  let node = this.root!;
326
393
  let offset = 0;
394
+
327
395
  while ('left' in node) {
328
396
  const leftLines = node.left.lines - sharedBreak(node.left, node.right);
397
+
329
398
  if (index < leftLines) {
330
399
  node = node.left;
331
400
  } else {
@@ -334,6 +403,7 @@ export class TextRope {
334
403
  node = node.right;
335
404
  }
336
405
  }
406
+
337
407
  return offset + node.breaks[index]!;
338
408
  }
339
409
  }
@@ -5,19 +5,26 @@ export function scriptSource(text: string, start: number, complete: boolean, con
5
5
  const chunks: string[] = [];
6
6
  let cursor = 0;
7
7
  let island: number | undefined;
8
+
8
9
  const finish = (end: number): string => {
9
10
  chunks.push(text.slice(cursor, island ?? end));
11
+
10
12
  if (island !== undefined) {
11
13
  chunks.push(' '.repeat(end - island));
12
14
  }
15
+
13
16
  return chunks.join('');
14
17
  };
18
+
15
19
  for (const match of text.matchAll(events)) {
16
20
  const state = context(start + match.index);
21
+
17
22
  if (state === null) {
18
23
  return undefined;
19
24
  }
25
+
20
26
  const inScript = state.embedded === 'javascript' && state.embedStart === start;
27
+
21
28
  if (/^<\?(?:php\b|=)/.test(match[0])) {
22
29
  if (inScript) {
23
30
  island = match.index;
@@ -25,11 +32,13 @@ export function scriptSource(text: string, start: number, complete: boolean, con
25
32
  } else if (match[0] === '?>') {
26
33
  if (island !== undefined && state.embed?.embedStart === start && (state.mode === 'code' || state.mode === 'line-comment')) {
27
34
  let end = match.index + 2;
35
+
28
36
  if (text.startsWith('\r\n', end)) {
29
37
  end += 2;
30
38
  } else if (text[end] === '\n') {
31
39
  end++;
32
40
  }
41
+
33
42
  // Spaces preserve document offsets without treating PHP source newlines as script text.
34
43
  chunks.push(text.slice(cursor, island), ' '.repeat(end - island));
35
44
  cursor = end;
@@ -39,5 +48,6 @@ export function scriptSource(text: string, start: number, complete: boolean, con
39
48
  return finish(match.index);
40
49
  }
41
50
  }
51
+
42
52
  return complete ? finish(text.length) : undefined;
43
53
  }
package/src/search.ts CHANGED
@@ -47,33 +47,44 @@ export function findMatches(text: string, query: string, revision: number, optio
47
47
  /* Validation and regex execution start when the iterator advances. */
48
48
  export function* iterateMatches(text: string, query: string, revision: number, options: FindOptions = {}): Generator<FindMatch, void, unknown> {
49
49
  options = { ...options };
50
+
50
51
  if (!query && !options.regex) {
51
52
  return;
52
53
  }
54
+
53
55
  const from = options.from ?? 0;
54
56
  const to = options.to ?? text.length;
55
57
  const maximum = options.maxResults ?? Infinity;
58
+
56
59
  if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to < from || to > text.length) {
57
60
  throw new RangeError('Search bounds must be ordered offsets inside the document.');
58
61
  }
62
+
59
63
  if (maximum !== Infinity && (!Number.isInteger(maximum) || maximum < 0)) {
60
64
  throw new RangeError('maxResults must be a nonnegative integer.');
61
65
  }
66
+
62
67
  const source = options.regex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
63
68
  const expression = new RegExp(source, `gmu${options.caseSensitive ? '' : 'i'}`);
64
69
  expression.lastIndex = from;
65
70
  let count = 0;
71
+
66
72
  while (count < maximum) {
67
73
  const match = expression.exec(text);
74
+
68
75
  if (!match || match.index > to) {
69
76
  break;
70
77
  }
78
+
71
79
  const end = match.index + match[0].length;
80
+
72
81
  const wholeWord =
73
82
  !options.wholeWord ||
74
83
  (!wordCharacter.test(characterBefore(text, match.index)) && !wordCharacter.test(String.fromCodePoint(text.codePointAt(end) ?? 0)));
84
+
75
85
  if (match.index >= from && end <= to && !splitsSurrogate(text, match.index) && !splitsSurrogate(text, end) && wholeWord) {
76
86
  count++;
87
+
77
88
  yield {
78
89
  from: match.index,
79
90
  to: end,
@@ -84,6 +95,7 @@ export function* iterateMatches(text: string, query: string, revision: number, o
84
95
  revision
85
96
  };
86
97
  }
98
+
87
99
  if (!match[0].length) {
88
100
  expression.lastIndex = match.index + ((text.codePointAt(match.index) ?? 0) > 0xffff ? 2 : 1);
89
101
  }
@@ -96,12 +108,15 @@ export function findMatchFrom(text: string, query: string, revision: number, off
96
108
  let first: FindMatch | null = null;
97
109
  let last: FindMatch | null = null;
98
110
  let previous: FindMatch | null = null;
111
+
99
112
  for (const match of iterateMatches(text, query, revision, options)) {
100
113
  first ??= match;
101
114
  last = match;
115
+
102
116
  if (!options.backwards && match.from >= position) {
103
117
  return match;
104
118
  }
119
+
105
120
  if (options.backwards) {
106
121
  if (match.to <= position) {
107
122
  previous = match;
@@ -110,6 +125,7 @@ export function findMatchFrom(text: string, query: string, revision: number, off
110
125
  }
111
126
  }
112
127
  }
128
+
113
129
  return options.backwards ? (previous ?? (options.wrap === false ? null : last)) : options.wrap === false ? null : first;
114
130
  }
115
131
 
@@ -118,59 +134,81 @@ export function replacementText(text: string, match: FindMatch, replacement: str
118
134
  if (maxCharacters !== Infinity && (!Number.isSafeInteger(maxCharacters) || maxCharacters < 0)) {
119
135
  throw new RangeError('The replacement character limit must be nonnegative.');
120
136
  }
137
+
121
138
  const expanded = literal || !match.regex ? replacement : expandPatterns(text, match, replacement, maxCharacters);
139
+
122
140
  if (expanded.length > maxCharacters) {
123
141
  throw new RangeError('Replacement output exceeds its character limit.');
124
142
  }
143
+
125
144
  const result = preserveCase ? replaceWithCaseRespect(expanded, match.text) : expanded;
145
+
126
146
  if (result.length > maxCharacters) {
127
147
  throw new RangeError('Replacement output exceeds its character limit.');
128
148
  }
149
+
129
150
  return result;
130
151
  }
131
152
 
132
153
  function expandPatterns(text: string, match: FindMatch, replacement: string, maxCharacters: number): string {
133
154
  const pieces: string[] = [];
155
+
134
156
  let characters = 0,
135
157
  cursor = 0;
158
+
136
159
  const append = (value: string): void => {
137
160
  characters += value.length;
161
+
138
162
  if (characters > maxCharacters) {
139
163
  throw new RangeError('Replacement output exceeds its character limit.');
140
164
  }
165
+
141
166
  pieces.push(value);
142
167
  };
168
+
143
169
  const expand = (token: string, group: string): string => {
144
170
  if (group === '$') {
145
171
  return '$';
146
172
  }
173
+
147
174
  if (group === '&') {
148
175
  return match.text;
149
176
  }
177
+
150
178
  if (group === '`') {
151
179
  return text.slice(0, match.from);
152
180
  }
181
+
153
182
  if (group === "'") {
154
183
  return text.slice(match.to);
155
184
  }
185
+
156
186
  if (group.startsWith('<')) {
157
187
  return match.groups ? (match.groups[group.slice(1, -1)] ?? '') : token;
158
188
  }
189
+
159
190
  const index = Number(group);
191
+
160
192
  if (index > 0 && index <= match.captures.length) {
161
193
  return match.captures[index - 1] ?? '';
162
194
  }
195
+
163
196
  const first = Number(group[0]);
197
+
164
198
  if (group.length === 2 && first > 0 && first <= match.captures.length) {
165
199
  return (match.captures[first - 1] ?? '') + group[1];
166
200
  }
201
+
167
202
  return token;
168
203
  };
204
+
169
205
  for (const found of replacement.matchAll(/\$(\$|&|`|'|\d{1,2}|<[^>]*>)/g)) {
170
206
  append(replacement.slice(cursor, found.index));
171
207
  append(expand(found[0], found[1]!));
172
208
  cursor = found.index + found[0].length;
173
209
  }
210
+
174
211
  append(replacement.slice(cursor));
212
+
175
213
  return pieces.join('');
176
214
  }