@type-editor/changeset 0.0.2
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.
- package/.editorconfig +8 -0
- package/.prettierignore +8 -0
- package/.prettierrc +9 -0
- package/LICENSE +48 -0
- package/README.md +153 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +1 -0
- package/eslint.config.ts +69 -0
- package/package.json +54 -0
- package/src/Change.ts +456 -0
- package/src/ChangeSet.ts +578 -0
- package/src/Span.ts +170 -0
- package/src/compute-diff.ts +100 -0
- package/src/default-encoder.ts +41 -0
- package/src/index.ts +8 -0
- package/src/max-simplify-distance.ts +7 -0
- package/src/myers-diff/run-myers-diff.ts +261 -0
- package/src/simplify-changes/expand-to-word-boundaries.ts +43 -0
- package/src/simplify-changes/fill-change.ts +99 -0
- package/src/simplify-changes/get-text.ts +69 -0
- package/src/simplify-changes/has-word-boundary.ts +34 -0
- package/src/simplify-changes/is-letter.ts +62 -0
- package/src/simplify-changes/simplify-adjacent-changes.ts +111 -0
- package/src/simplify-changes.ts +42 -0
- package/src/tokenizer/tokenize-block-node.ts +47 -0
- package/src/tokenizer/tokenize-fragment.ts +46 -0
- package/src/tokenizer/tokenize-textNode.ts +31 -0
- package/src/types/ChangeJSON.ts +23 -0
- package/src/types/ChangeSetConfig.ts +19 -0
- package/src/types/TokenEncoder.ts +52 -0
- package/src/types/TrimmedRange.ts +10 -0
- package/tsconfig.json +27 -0
- package/typedoc.json +11 -0
- package/vite.config.ts +54 -0
package/src/Change.ts
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import {Span} from './Span';
|
|
2
|
+
import type { ChangeJSON } from './types/ChangeJSON';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Result of merging overlapping changes.
|
|
7
|
+
*
|
|
8
|
+
* @template Data - The type of metadata in the changes.
|
|
9
|
+
*/
|
|
10
|
+
interface MergeResult<Data> {
|
|
11
|
+
/** The merged change, or null if no actual change occurred. */
|
|
12
|
+
change: Change<Data> | null;
|
|
13
|
+
/** The next change from the X changeset to process. */
|
|
14
|
+
nextX: Change<Data> | null;
|
|
15
|
+
/** The next change from the Y changeset to process. */
|
|
16
|
+
nextY: Change<Data> | null;
|
|
17
|
+
/** The updated index in the X changeset. */
|
|
18
|
+
indexX: number;
|
|
19
|
+
/** The updated index in the Y changeset. */
|
|
20
|
+
indexY: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Represents a change between two document versions with metadata.
|
|
25
|
+
*
|
|
26
|
+
* A Change tracks a replaced range in the document, including both what was
|
|
27
|
+
* deleted from the old version and what was inserted in the new version.
|
|
28
|
+
* It uses two coordinate systems:
|
|
29
|
+
* - A coordinates: positions in the old document
|
|
30
|
+
* - B coordinates: positions in the new document
|
|
31
|
+
*
|
|
32
|
+
* @template Data - The type of metadata associated with the changed content.
|
|
33
|
+
*/
|
|
34
|
+
export class Change<Data = any> {
|
|
35
|
+
|
|
36
|
+
private readonly _fromA: number;
|
|
37
|
+
private readonly _toA: number;
|
|
38
|
+
private readonly _fromB: number;
|
|
39
|
+
private readonly _toB: number;
|
|
40
|
+
private readonly _deleted: ReadonlyArray<Span<Data>>;
|
|
41
|
+
private readonly _inserted: ReadonlyArray<Span<Data>>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a new Change representing a document modification.
|
|
45
|
+
*
|
|
46
|
+
* @param fromA - The start position of the range in the old document (A coordinates).
|
|
47
|
+
* @param toA - The end position of the range in the old document (A coordinates).
|
|
48
|
+
* @param fromB - The start position of the range in the new document (B coordinates).
|
|
49
|
+
* @param toB - The end position of the range in the new document (B coordinates).
|
|
50
|
+
* @param deleted - Metadata spans for the deleted content. The total length of these
|
|
51
|
+
* spans must equal `toA - fromA`.
|
|
52
|
+
* @param inserted - Metadata spans for the inserted content. The total length of these
|
|
53
|
+
* spans must equal `toB - fromB`.
|
|
54
|
+
*/
|
|
55
|
+
constructor(fromA: number,
|
|
56
|
+
toA: number,
|
|
57
|
+
fromB: number,
|
|
58
|
+
toB: number,
|
|
59
|
+
deleted: ReadonlyArray<Span<Data>>,
|
|
60
|
+
inserted: ReadonlyArray<Span<Data>>) {
|
|
61
|
+
this._fromA = fromA;
|
|
62
|
+
this._toA = toA;
|
|
63
|
+
this._fromB = fromB;
|
|
64
|
+
this._toB = toB;
|
|
65
|
+
this._deleted = deleted;
|
|
66
|
+
this._inserted = inserted;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The start position in the old document (A coordinates). */
|
|
70
|
+
get fromA(): number {
|
|
71
|
+
return this._fromA;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The end position in the old document (A coordinates). */
|
|
75
|
+
get toA(): number {
|
|
76
|
+
return this._toA;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The start position in the new document (B coordinates). */
|
|
80
|
+
get fromB(): number {
|
|
81
|
+
return this._fromB;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The end position in the new document (B coordinates). */
|
|
85
|
+
get toB(): number {
|
|
86
|
+
return this._toB;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The spans of deleted content with associated metadata. */
|
|
90
|
+
get deleted(): ReadonlyArray<Span<Data>> {
|
|
91
|
+
return this._deleted;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The spans of inserted content with associated metadata. */
|
|
95
|
+
get inserted(): ReadonlyArray<Span<Data>> {
|
|
96
|
+
return this._inserted;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The length of the deleted range in the old document. */
|
|
100
|
+
get lenA(): number {
|
|
101
|
+
return this._toA - this._fromA;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The length of the inserted range in the new document. */
|
|
105
|
+
get lenB(): number {
|
|
106
|
+
return this._toB - this._fromB;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Merges two changesets into a single changeset.
|
|
111
|
+
*
|
|
112
|
+
* This combines two sequential changesets where the end document of the first
|
|
113
|
+
* changeset is the start document of the second. The result is a single changeset
|
|
114
|
+
* spanning from the start of the first to the end of the second.
|
|
115
|
+
*
|
|
116
|
+
* The merge operates by synchronizing over a "middle" coordinate system:
|
|
117
|
+
* - For the first changeset (x): the B coordinates represent the middle document
|
|
118
|
+
* - For the second changeset (y): the A coordinates represent the middle document
|
|
119
|
+
*
|
|
120
|
+
* @template Data - The type of metadata in the changes.
|
|
121
|
+
* @param x - The first changeset.
|
|
122
|
+
* @param y - The second changeset applied after x.
|
|
123
|
+
* @param combine - Function to combine metadata when spans need to be merged.
|
|
124
|
+
* @returns A single changeset representing both transformations.
|
|
125
|
+
*/
|
|
126
|
+
public static merge<Data>(x: ReadonlyArray<Change<Data>>,
|
|
127
|
+
y: ReadonlyArray<Change<Data>>,
|
|
128
|
+
combine: (dataA: Data, dataB: Data) => Data): ReadonlyArray<Change<Data>> {
|
|
129
|
+
// Fast paths for empty changesets
|
|
130
|
+
if (x.length === 0) {
|
|
131
|
+
return y;
|
|
132
|
+
}
|
|
133
|
+
if (y.length === 0) {
|
|
134
|
+
return x;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const result: Array<Change<Data>> = [];
|
|
138
|
+
|
|
139
|
+
// Iterate over both sets in parallel, using the middle coordinate
|
|
140
|
+
// system (B in x, A in y) to synchronize.
|
|
141
|
+
let currentX: Change<Data> | null = x[0];
|
|
142
|
+
let currentY: Change<Data> | null = y[0];
|
|
143
|
+
let indexX = 0;
|
|
144
|
+
let indexY = 0;
|
|
145
|
+
|
|
146
|
+
// Track cumulative offsets incrementally to avoid O(n²) recalculation
|
|
147
|
+
let cumulativeXOffset = 0;
|
|
148
|
+
let cumulativeYOffset = 0;
|
|
149
|
+
|
|
150
|
+
while (currentX || currentY) {
|
|
151
|
+
if (!currentX && !currentY) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
// currentX entirely before currentY in the middle coordinate system
|
|
155
|
+
else if (currentX && (!currentY || currentX.toB < currentY._fromA)) {
|
|
156
|
+
result.push(this.adjustChangeForYOffset(currentX, cumulativeYOffset));
|
|
157
|
+
cumulativeXOffset += currentX.lenB - currentX.lenA;
|
|
158
|
+
currentX = ++indexX < x.length ? x[indexX] : null;
|
|
159
|
+
}
|
|
160
|
+
// currentY entirely before currentX in the middle coordinate system
|
|
161
|
+
else if (currentY && (!currentX || currentY.toA < currentX.fromB)) {
|
|
162
|
+
result.push(this.adjustChangeForXOffset(currentY, cumulativeXOffset));
|
|
163
|
+
cumulativeYOffset += currentY.lenB - currentY.lenA;
|
|
164
|
+
currentY = ++indexY < y.length ? y[indexY] : null;
|
|
165
|
+
}
|
|
166
|
+
// Changes overlap - need to merge them
|
|
167
|
+
else {
|
|
168
|
+
const mergeResult: MergeResult<Data> = this.mergeOverlappingChanges(
|
|
169
|
+
x, y, currentX, currentY, indexX, indexY,
|
|
170
|
+
cumulativeXOffset, cumulativeYOffset, combine
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (mergeResult.change) {
|
|
174
|
+
result.push(mergeResult.change);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Update cumulative offsets for all processed changes
|
|
178
|
+
for (let i = indexX; i < mergeResult.indexX; i++) {
|
|
179
|
+
cumulativeXOffset += x[i].lenB - x[i].lenA;
|
|
180
|
+
}
|
|
181
|
+
for (let i = indexY; i < mergeResult.indexY; i++) {
|
|
182
|
+
cumulativeYOffset += y[i].lenB - y[i].lenA;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
currentX = mergeResult.nextX;
|
|
186
|
+
currentY = mergeResult.nextY;
|
|
187
|
+
indexX = mergeResult.indexX;
|
|
188
|
+
indexY = mergeResult.indexY;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Deserializes a Change from its JSON representation.
|
|
197
|
+
*
|
|
198
|
+
* @template Data - The type of metadata in the change.
|
|
199
|
+
* @param json - The JSON object representing the change.
|
|
200
|
+
* @returns A new Change instance reconstructed from the JSON data.
|
|
201
|
+
*/
|
|
202
|
+
public static fromJSON<Data>(json: ChangeJSON<Data>): Change<Data> {
|
|
203
|
+
return new Change(
|
|
204
|
+
json.fromA,
|
|
205
|
+
json.toA,
|
|
206
|
+
json.fromB,
|
|
207
|
+
json.toB,
|
|
208
|
+
json.deleted.map((deleted: { length: number; data: Data }): Span<Data> => new Span(deleted.length, deleted.data)),
|
|
209
|
+
json.inserted.map((inserted: { length: number; data: Data }): Span<Data> => new Span(inserted.length, inserted.data))
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Adjusts a change from the X changeset by applying a Y offset.
|
|
215
|
+
*
|
|
216
|
+
* @param change - The change to adjust.
|
|
217
|
+
* @param offset - The offset to apply to B coordinates.
|
|
218
|
+
* @returns A new change with adjusted coordinates, or the original if offset is 0.
|
|
219
|
+
*/
|
|
220
|
+
private static adjustChangeForYOffset<Data>(change: Change<Data>,
|
|
221
|
+
offset: number): Change<Data> {
|
|
222
|
+
if (offset === 0) {
|
|
223
|
+
return change;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return new Change(
|
|
227
|
+
change.fromA,
|
|
228
|
+
change.toA,
|
|
229
|
+
change.fromB + offset,
|
|
230
|
+
change.toB + offset,
|
|
231
|
+
change.deleted,
|
|
232
|
+
change.inserted
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Adjusts a change from the Y changeset by applying an X offset.
|
|
238
|
+
*
|
|
239
|
+
* @param change - The change to adjust.
|
|
240
|
+
* @param offset - The offset to apply to A coordinates.
|
|
241
|
+
* @returns A new change with adjusted coordinates, or the original if offset is 0.
|
|
242
|
+
*/
|
|
243
|
+
private static adjustChangeForXOffset<Data>(change: Change<Data>,
|
|
244
|
+
offset: number): Change<Data> {
|
|
245
|
+
if (offset === 0) {
|
|
246
|
+
return change;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return new Change(
|
|
250
|
+
change.fromA - offset,
|
|
251
|
+
change.toA - offset,
|
|
252
|
+
change.fromB,
|
|
253
|
+
change.toB,
|
|
254
|
+
change.deleted,
|
|
255
|
+
change.inserted
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Merges overlapping changes from both changesets.
|
|
261
|
+
*
|
|
262
|
+
* When changes from X and Y overlap in the middle coordinate system, they need
|
|
263
|
+
* to be merged. The rules are:
|
|
264
|
+
* - Deletions from X and insertions from Y are kept
|
|
265
|
+
* - Areas covered by X but not Y are insertions from X
|
|
266
|
+
* - Areas covered by Y but not X are deletions from Y
|
|
267
|
+
*
|
|
268
|
+
* @param x - The first changeset array.
|
|
269
|
+
* @param y - The second changeset array.
|
|
270
|
+
* @param currentX - Current change from X.
|
|
271
|
+
* @param currentY - Current change from Y.
|
|
272
|
+
* @param indexX - Current index in X.
|
|
273
|
+
* @param indexY - Current index in Y.
|
|
274
|
+
* @param xOffset - Pre-calculated cumulative offset from previous X changes.
|
|
275
|
+
* @param yOffset - Pre-calculated cumulative offset from previous Y changes.
|
|
276
|
+
* @param combine - Function to combine metadata.
|
|
277
|
+
* @returns The merge result containing the merged change and updated state.
|
|
278
|
+
*/
|
|
279
|
+
private static mergeOverlappingChanges<Data>(x: ReadonlyArray<Change<Data>>,
|
|
280
|
+
y: ReadonlyArray<Change<Data>>,
|
|
281
|
+
currentX: Change<Data> | null,
|
|
282
|
+
currentY: Change<Data> | null,
|
|
283
|
+
indexX: number,
|
|
284
|
+
indexY: number,
|
|
285
|
+
xOffset: number,
|
|
286
|
+
yOffset: number,
|
|
287
|
+
combine: (dataA: Data, dataB: Data) => Data): MergeResult<Data> {
|
|
288
|
+
// At this point, we're guaranteed that at least one of currentX or currentY
|
|
289
|
+
// is not null and they overlap. We assert this for type safety.
|
|
290
|
+
if (!currentX || !currentY) {
|
|
291
|
+
throw new Error('mergeOverlappingChanges called with null change');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Start position in the middle coordinate system
|
|
295
|
+
let position: number = Math.min(currentX.fromB, currentY.fromA);
|
|
296
|
+
|
|
297
|
+
// Calculate the merged change's boundaries using pre-calculated offsets
|
|
298
|
+
const fromA: number = Math.min(currentX.fromA, currentY.fromA - xOffset);
|
|
299
|
+
let toA: number = fromA;
|
|
300
|
+
const fromB: number = Math.min(currentY.fromB, currentX.fromB + yOffset);
|
|
301
|
+
let toB: number = fromB;
|
|
302
|
+
|
|
303
|
+
let deleted: ReadonlyArray<Span> = Span.none;
|
|
304
|
+
let inserted: ReadonlyArray<Span> = Span.none;
|
|
305
|
+
|
|
306
|
+
// Track whether we've entered each change to avoid double-processing
|
|
307
|
+
let enteredX = false;
|
|
308
|
+
let enteredY = false;
|
|
309
|
+
|
|
310
|
+
// Mutable references to current changes
|
|
311
|
+
let workingX: Change<Data> | null = currentX;
|
|
312
|
+
let workingY: Change<Data> | null = currentY;
|
|
313
|
+
|
|
314
|
+
// Process all overlapping changes
|
|
315
|
+
while (true) {
|
|
316
|
+
const nextPositionX: number = this.getNextBoundary(workingX, position, 'fromB', 'toB');
|
|
317
|
+
const nextPositionY: number = this.getNextBoundary(workingY, position, 'fromA', 'toA');
|
|
318
|
+
const nextPosition: number = Math.min(nextPositionX, nextPositionY);
|
|
319
|
+
|
|
320
|
+
const inX: boolean = workingX && position >= workingX.fromB;
|
|
321
|
+
const inY: boolean = workingY && position >= workingY.fromA;
|
|
322
|
+
|
|
323
|
+
if (!inX && !inY) {
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
// Process X change at entry point
|
|
329
|
+
if (inX && position === workingX?.fromB && !enteredX) {
|
|
330
|
+
deleted = Span.join(deleted, workingX.deleted, combine);
|
|
331
|
+
toA += workingX.lenA;
|
|
332
|
+
enteredX = true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Process X insertions where Y doesn't cover
|
|
336
|
+
if (inX && !inY && workingX) {
|
|
337
|
+
const slicedInserted: ReadonlyArray<Span<Data>> = Span.slice(
|
|
338
|
+
workingX.inserted,
|
|
339
|
+
position - workingX.fromB,
|
|
340
|
+
nextPosition - workingX.fromB
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
inserted = Span.join(inserted, slicedInserted, combine);
|
|
344
|
+
toB += nextPosition - position;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Process Y change at entry point
|
|
348
|
+
if (inY && position === workingY?.fromA && !enteredY) {
|
|
349
|
+
inserted = Span.join(inserted, workingY.inserted, combine);
|
|
350
|
+
toB += workingY.lenB;
|
|
351
|
+
enteredY = true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Process Y deletions where X doesn't cover
|
|
355
|
+
if (inY && !inX && workingY) {
|
|
356
|
+
const slicedDeleted: ReadonlyArray<Span<Data>> = Span.slice(
|
|
357
|
+
workingY.deleted,
|
|
358
|
+
position - workingY.fromA,
|
|
359
|
+
nextPosition - workingY.fromA
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
deleted = Span.join(deleted, slicedDeleted, combine);
|
|
363
|
+
toA += nextPosition - position;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Advance X if we've finished this change
|
|
367
|
+
if (inX && nextPosition === workingX?.toB) {
|
|
368
|
+
indexX++;
|
|
369
|
+
workingX = indexX < x.length ? x[indexX] : null;
|
|
370
|
+
enteredX = false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Advance Y if we've finished this change
|
|
374
|
+
if (inY && nextPosition === workingY?.toA) {
|
|
375
|
+
indexY++;
|
|
376
|
+
workingY = indexY < y.length ? y[indexY] : null;
|
|
377
|
+
enteredY = false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
position = nextPosition;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Only create a change if there's actual content
|
|
384
|
+
const change: Change = (fromA < toA || fromB < toB)
|
|
385
|
+
? new Change(fromA, toA, fromB, toB, deleted, inserted)
|
|
386
|
+
: null;
|
|
387
|
+
|
|
388
|
+
return {change, nextX: workingX, nextY: workingY, indexX, indexY};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Determines the next boundary position for a change.
|
|
393
|
+
*
|
|
394
|
+
* @param change - The change to examine.
|
|
395
|
+
* @param currentPosition - The current position.
|
|
396
|
+
* @param fromKey - The property name for the start position.
|
|
397
|
+
* @param toKey - The property name for the end position.
|
|
398
|
+
* @returns The next boundary position (or a large number if no change).
|
|
399
|
+
*/
|
|
400
|
+
private static getNextBoundary<Data>(change: Change<Data> | null,
|
|
401
|
+
currentPosition: number,
|
|
402
|
+
fromKey: 'fromA' | 'fromB',
|
|
403
|
+
toKey: 'toA' | 'toB'): number {
|
|
404
|
+
if (!change) {
|
|
405
|
+
return Number.MAX_SAFE_INTEGER;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return currentPosition >= change[fromKey] ? change[toKey] : change[fromKey];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Creates a sub-change by slicing ranges from both coordinate systems.
|
|
413
|
+
*
|
|
414
|
+
* This extracts a portion of this change, specified by ranges in both
|
|
415
|
+
* the A and B coordinate systems. If the slice covers the entire change,
|
|
416
|
+
* returns this instance to avoid allocation.
|
|
417
|
+
*
|
|
418
|
+
* @param startA - The start position in A coordinates (relative to this change).
|
|
419
|
+
* @param endA - The end position in A coordinates (relative to this change).
|
|
420
|
+
* @param startB - The start position in B coordinates (relative to this change).
|
|
421
|
+
* @param endB - The end position in B coordinates (relative to this change).
|
|
422
|
+
* @returns A new Change representing the specified slice, or this if unchanged.
|
|
423
|
+
*/
|
|
424
|
+
public slice(startA: number,
|
|
425
|
+
endA: number,
|
|
426
|
+
startB: number,
|
|
427
|
+
endB: number): Change<Data> {
|
|
428
|
+
// If slicing the entire range, return this to avoid allocation
|
|
429
|
+
const coversFullRangeA: boolean = startA === 0 && endA === this.lenA;
|
|
430
|
+
const coversFullRangeB: boolean = startB === 0 && endB === this.lenB;
|
|
431
|
+
|
|
432
|
+
if (coversFullRangeA && coversFullRangeB) {
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return new Change(
|
|
437
|
+
this._fromA + startA,
|
|
438
|
+
this._fromA + endA,
|
|
439
|
+
this._fromB + startB,
|
|
440
|
+
this._fromB + endB,
|
|
441
|
+
Span.slice(this._deleted, startA, endA),
|
|
442
|
+
Span.slice(this._inserted, startB, endB));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Serializes this Change to a JSON-compatible representation.
|
|
447
|
+
*
|
|
448
|
+
* Since the Change class structure matches the ChangeJSON interface,
|
|
449
|
+
* this method returns the instance itself.
|
|
450
|
+
*
|
|
451
|
+
* @returns A JSON representation of this change.
|
|
452
|
+
*/
|
|
453
|
+
public toJSON(): ChangeJSON<Data> {
|
|
454
|
+
return this;
|
|
455
|
+
}
|
|
456
|
+
}
|