Skip to content

Commit 2a339e1

Browse files
committed
imp(collaboration-manager): operationsTransformer improved
1 parent f2be325 commit 2a339e1

File tree

2 files changed

+103
-104
lines changed

2 files changed

+103
-104
lines changed

packages/collaboration-manager/src/OperationsTransformer.ts

Lines changed: 96 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { IndexBuilder } from "@editorjs/model";
22
import { Operation, OperationType } from "./Operation";
3+
import { getRangesIntersectionType, RangeIntersectionType } from "./utils/getRangesIntersectionType";
34

45
/**
56
* Class that transforms operation against another operation
@@ -19,63 +20,40 @@ export class OperationsTransformer {
1920
}
2021

2122
/**
22-
* Method that desides what kind of transformation should be applied to the operation
23+
* Method that returns new operation based on the type of againstOp index
2324
* Cases:
24-
* 1. Against operations is a block operation and current operation is also a block operation
25-
* - check that againstOp affects current operation and transform against block operation
25+
* 1. Against operation is a block operation and current operation is also a block operation
26+
* - check if againstOp affects current operation, update operation's block index
2627
*
2728
* 2. Against operation is a block operation and current operation is a text operation
28-
* - same as above, check that againstOp affects current operation and transform against block operation
29+
* - same as above, check if againstOp affects current operation and update operation's block index
2930
*
3031
* 3. Against operation is a text operation and current operation is a block operation
3132
* - text operation does not afftect block operation - so return copy of current operation
3233
*
3334
* 4. Against operation is a text operation and current operation is also a text operation
34-
* - check that againstOp affects current operation and transform against text operation
35+
* - check if againstOp affects current operation and update operation's text index
3536
*
3637
* @param operation - operation to be transformed
3738
* @param againstOp - operation against which the current operation should be transformed
3839
* @returns new operation
3940
*/
4041
#applyTransformation<T extends OperationType>(operation: Operation<T>, againstOp: Operation<OperationType>): Operation<T> | Operation<OperationType.Neutral> {
41-
const currentIndex = operation.index;
4242
const againstIndex = againstOp.index;
4343

44-
/**
45-
* Cover 1 and 2 cases
46-
*
47-
* Check that againstOp is a block operation
48-
*/
49-
if (againstIndex.isBlockIndex && currentIndex.blockIndex !== undefined) {
50-
/**
51-
* Check that againstOp affects current operation
52-
*/
53-
if (againstIndex.blockIndex! <= currentIndex.blockIndex) {
54-
return this.#transformAgainstBlockOperation(operation, againstOp);
55-
}
56-
}
44+
switch (true) {
45+
case (againstIndex.isBlockIndex):
46+
return this.#transformAgainstBlockOperation(operation, againstOp);
47+
48+
case (againstIndex.isTextIndex):
49+
return this.#transformAgainstTextOperation(operation, againstOp);
5750

58-
/**
59-
* Cover 4 case
60-
*
61-
* Check that againstOp is a text operation and current operation is also a text operation
62-
*/
63-
if (againstIndex.isTextIndex && currentIndex.isTextIndex) {
6451
/**
65-
* Check that againstOp affects current operation (text operation on the same block and same input)
66-
* and against op happened on the left side or has overlapping range
52+
* @todo Cover all index types
6753
*/
68-
if (currentIndex.dataKey === againstIndex.dataKey && currentIndex.blockIndex === againstIndex.blockIndex && againstIndex.textRange![0] <= currentIndex.textRange![0]) {
69-
return this.#transformAgainstTextOperation(operation, againstOp);
70-
}
54+
default:
55+
throw new Error('Unsupported index type');
7156
}
72-
73-
/**
74-
* Cover 3 case
75-
*
76-
* Return copy of current operation
77-
*/
78-
return Operation.from(operation);
7957
}
8058

8159
/**
@@ -100,7 +78,21 @@ export class OperationsTransformer {
10078
*/
10179
#transformAgainstBlockOperation<T extends OperationType>(operation: Operation<T>, againstOp: Operation<OperationType>): Operation<T> | Operation<OperationType.Neutral> {
10280
const newIndexBuilder = new IndexBuilder().from(operation.index);
103-
81+
82+
/**
83+
* If current operation has no block index, return copy of the current operation
84+
*/
85+
if (!operation.index.isBlockIndex) {
86+
return Operation.from(operation);
87+
}
88+
89+
/**
90+
* Check that againstOp affects current operation
91+
*/
92+
if (againstOp.index.blockIndex! <= operation.index.blockIndex!) {
93+
return Operation.from(operation);
94+
}
95+
10496
/**
10597
* Update the index of the current operation
10698
*/
@@ -136,7 +128,11 @@ export class OperationsTransformer {
136128
/**
137129
* Return new operation with the updated index
138130
*/
139-
return new Operation(operation.type, newIndexBuilder.build(), operation.data, operation.userId, operation.rev);
131+
const newOp = Operation.from(operation);
132+
133+
newOp.index = newIndexBuilder.build();
134+
135+
return newOp;
140136
}
141137

142138
/**
@@ -195,31 +191,36 @@ export class OperationsTransformer {
195191
#transformAgainstTextInsert<T extends OperationType>(operation: Operation<T>, againstOp: Operation<OperationType>): Operation<T> | Operation<OperationType.Neutral> {
196192
const newIndexBuilder = new IndexBuilder().from(operation.index);
197193

198-
const amountOfInsertedCharacters = againstOp.data.payload!.length;
199-
const againstOpIsOnTheLeft = againstOp.index.textRange![0] < operation.index.textRange![0];
200-
const currentOpAgregatesAgainstOp = (operation.index.textRange![0] <= againstOp.index.textRange![0]) && (operation.index.textRange![1] >= againstOp.index.textRange![1]);
194+
const insertedLength = againstOp.data.payload!.length;
195+
196+
const index = operation.index;
197+
const againstIndex = againstOp.index;
201198

202199
/**
203-
* Cover case 1
200+
* In this case, againstOp is insert operatioin, there would be only two possible intersections
201+
* - None - inserted text is on the left side of the current operation
202+
* - Includes - inserted text is inside of the current operation text range
204203
*/
205-
if (againstOpIsOnTheLeft) {
206-
/**
207-
* Move text index of the current operation to the right by amount of inserted characters
208-
*/
209-
newIndexBuilder.addTextRange([againstOp.index.textRange![0] + amountOfInsertedCharacters, againstOp.index.textRange![1] + amountOfInsertedCharacters]);
204+
const intersectionType = getRangesIntersectionType(index.textRange!, againstIndex.textRange!);
205+
206+
switch (intersectionType) {
207+
case (RangeIntersectionType.None):
208+
newIndexBuilder.addTextRange([index.textRange![0] + insertedLength, index.textRange![1] + insertedLength]);
209+
break;
210+
211+
case (RangeIntersectionType.Includes):
212+
newIndexBuilder.addTextRange([index.textRange![0], index.textRange![1] + insertedLength]);
213+
break;
210214
}
211215

212216
/**
213-
* Cover case 2
217+
* Return new operation with the updated index
214218
*/
215-
if (currentOpAgregatesAgainstOp) {
216-
/**
217-
* Move right bound of the current operation to the right by amount of inserted characters to include the inserted text
218-
*/
219-
newIndexBuilder.addTextRange([operation.index.textRange![0], operation.index.textRange![1] + amountOfInsertedCharacters]);
220-
}
219+
const newOp = Operation.from(operation);
221220

222-
return new Operation(operation.type, newIndexBuilder.build(), operation.data, operation.userId, operation.rev);
221+
newOp.index = newIndexBuilder.build();
222+
223+
return newOp;
223224
}
224225

225226
/**
@@ -248,67 +249,58 @@ export class OperationsTransformer {
248249
*/
249250
#transformAgainstTextDelete<T extends OperationType>(operation: Operation<T>, againstOp: Operation<OperationType>): Operation<T> | Operation<OperationType.Neutral> {
250251
const newIndexBuilder = new IndexBuilder().from(operation.index);
251-
252252
const deletedAmount = againstOp.data.payload!.length;
253253

254-
const deleteIsOnTheLeft = againstOp.index.textRange![1] < operation.index.textRange![0];
255-
256-
const deletedLeftSide = (againstOp.index.textRange![0] <= operation.index.textRange![0])
257-
&& (againstOp.index.textRange![1] < operation.index.textRange![1])
258-
&& (againstOp.index.textRange![1] > operation.index.textRange![0]);
259-
260-
const deletedRightSide = (againstOp.index.textRange![0] > operation.index.textRange![0])
261-
&& (againstOp.index.textRange![0] < operation.index.textRange![1])
262-
&& (againstOp.index.textRange![1] <= operation.index.textRange![1]);
263-
264-
const deletedInside = (againstOp.index.textRange![0] > operation.index.textRange![0])
265-
&& (againstOp.index.textRange![1] < operation.index.textRange![1]);
254+
const index = operation.index;
255+
const againstIndex = againstOp.index;
256+
257+
const intersectionType = getRangesIntersectionType(index.textRange!, againstIndex.textRange!);
266258

267-
const deletedFull = (againstOp.index.textRange![0] <= operation.index.textRange![0])
268-
&& (againstOp.index.textRange![1] >= operation.index.textRange![1]);
259+
switch (intersectionType) {
260+
/**
261+
* Cover case 1
262+
*/
263+
case (RangeIntersectionType.None):
264+
newIndexBuilder.addTextRange([index.textRange![0] - deletedAmount, index.textRange![1] - deletedAmount]);
265+
break;
269266

270-
/**
271-
* Cover case 1
272-
*/
273-
if (deleteIsOnTheLeft) {
274-
newIndexBuilder.addTextRange([operation.index.textRange![0] - deletedAmount, operation.index.textRange![1] - deletedAmount]);
275-
}
267+
/**
268+
* Cover case 2.1
269+
*/
270+
case (RangeIntersectionType.Left):
271+
newIndexBuilder.addTextRange([againstIndex.textRange![0], index.textRange![1] - deletedAmount]);
272+
break;
276273

277-
/**
278-
* Cover case 2.1
279-
*/
280-
if (deletedLeftSide) {
281-
const deletedFromCurrentOpRange = operation.index.textRange![0] - againstOp.index.textRange![1];
274+
/**
275+
* Cover case 2.2
276+
*/
277+
case (RangeIntersectionType.Right):
278+
const overlapLength = index.textRange![1] - againstIndex.textRange![0];
282279

283-
newIndexBuilder.addTextRange([againstOp.index.textRange![0], operation.index.textRange![1] - deletedFromCurrentOpRange]);
284-
}
280+
newIndexBuilder.addTextRange([index.textRange![0], index.textRange![1] - overlapLength]);
281+
break;
285282

286-
/**
287-
* Cover case 2.2
288-
*/
289-
if (deletedRightSide) {
290-
const deletedFromCurrentOpRange = operation.index.textRange![1] - againstOp.index.textRange![0];
283+
/**
284+
* Cover case 3
285+
*/
286+
case (RangeIntersectionType.Includes):
287+
newIndexBuilder.addTextRange([index.textRange![0], index.textRange![1] - deletedAmount]);
288+
break;
291289

292-
newIndexBuilder.addTextRange([operation.index.textRange![0], operation.index.textRange![1] - deletedFromCurrentOpRange]);
290+
/**
291+
* Cover case 4
292+
*/
293+
case (RangeIntersectionType.Included):
294+
return new Operation(OperationType.Neutral, newIndexBuilder.build(), { payload: [] }, operation.userId, operation.rev);
293295
}
294296

295297
/**
296-
* Cover case 3
298+
* Return new operation with updated index
297299
*/
298-
if (deletedInside) {
299-
newIndexBuilder.addTextRange([operation.index.textRange![0], operation.index.textRange![1] - deletedAmount]);
300-
}
300+
const newOp = Operation.from(operation);
301301

302-
/**
303-
* Cover case 4
304-
*/
305-
if (deletedFull) {
306-
return new Operation(OperationType.Neutral, newIndexBuilder.build(), { payload: [] }, operation.userId, operation.rev);
307-
}
302+
newOp.index = newIndexBuilder.build();
308303

309-
/**
310-
* Return new operation with updated index
311-
*/
312-
return new Operation(operation.type, newIndexBuilder.build(), operation.data, operation.userId, operation.rev);
304+
return newOp;
313305
}
314306
}

packages/collaboration-manager/src/utils/getRangesIntersectionType.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ export enum RangeIntersectionType {
1111
None = 'none',
1212
}
1313

14+
/**
15+
* Returns the type of intersection between two text ranges
16+
*
17+
* @param range - Range to check
18+
* @param rangeToCompare - Range to compare with
19+
* @returns Type of intersection
20+
*/
1421
export function getRangesIntersectionType(range: TextRange, rangeToCompare: TextRange): RangeIntersectionType {
1522
const [start, end] = range;
1623
const [startToCompare, endToCompare] = rangeToCompare;

0 commit comments

Comments
 (0)