Conversation
|
⏭️ No files to mutate for |
|
⏭️ No files to mutate for |
Coverage report for
|
St.❔ |
Category | Percentage | Covered / Total |
|---|---|---|---|
| 🟢 | Statements | 100% | 0/0 |
| 🟢 | Branches | 100% | 0/0 |
| 🟢 | Functions | 100% | 0/0 |
| 🟢 | Lines | 100% | 0/0 |
Test suite run success
1 tests passing in 1 suite.
Report generated by 🧪jest coverage report action from 61d4dab
Coverage report for
|
St.❔ |
Category | Percentage | Covered / Total |
|---|---|---|---|
| 🟢 | Statements | 99.65% | 852/855 |
| 🟢 | Branches | 98.7% | 228/231 |
| 🟢 | Functions | 98.1% | 207/211 |
| 🟢 | Lines | 99.64% | 819/822 |
Test suite run success
441 tests passing in 25 suites.
Report generated by 🧪jest coverage report action from a74d393
Coverage report for
|
St.❔ |
Category | Percentage | Covered / Total |
|---|---|---|---|
| 🟡 | Statements | 70.97% | 22/31 |
| 🔴 | Branches | 20% | 1/5 |
| 🟡 | Functions | 75% | 6/8 |
| 🟡 | Lines | 68.97% | 20/29 |
Test suite run success
4 tests passing in 1 suite.
Report generated by 🧪jest coverage report action from a74d393
Coverage report for
|
St.❔ |
Category | Percentage | Covered / Total |
|---|---|---|---|
| 🟡 | Statements | 71.25% (+3.74% 🔼) |
228/320 |
| 🟡 | Branches | 77.36% (-2.64% 🔻) |
82/106 |
| 🟡 | Functions | 68.18% (+11.04% 🔼) |
30/44 |
| 🟡 | Lines | 71.2% (+3.68% 🔼) |
225/316 |
Show new covered files 🐣
St.❔ |
File | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|
| 🟢 | ... / getRangesIntersectionType.ts |
94.12% | 86.67% | 100% | 94.12% |
| 🟢 | OperationsTransformer.ts | 91.3% | 86.21% | 100% | 91.3% |
| 🟢 | BatchedOperation.ts | 88.89% | 76.47% | 100% | 88.57% |
Show files with reduced coverage 🔻
St.❔ |
File | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|
| 🟡 | CollaborationManager.ts | 80% (-3.82% 🔻) |
73.08% (-0.84% 🔻) |
55.56% (-6.94% 🔻) |
79.76% (-4.06% 🔻) |
Test suite run success
79 tests passing in 5 suites.
Report generated by 🧪jest coverage report action from a74d393
There was a problem hiding this comment.
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
packages/collaboration-manager/src/BatchedOperation.ts:13
- [nitpick] The file is named 'BatchedOperation.ts' while the exported class is named 'OperationsBatch', which may lead to confusion. Consider renaming either the file or the class so that they are consistent.
export class OperationsBatch<T extends OperationType> extends Operation<T> {
| public transform<K extends OperationType>(againstOp: Operation<K> | Operation<OperationType.Neutral>): Operation<T | OperationType.Neutral> { | ||
| return this.#transformer.transform(this, againstOp); | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
The transformation logic does not check if the adjusted text range becomes negative. This can cause tests (e.g., expecting null when over-deleting) to fail. Consider adding a validation step in the transformer methods (such as in #transformAgainstTextDelete) to return null if the resulting text range is invalid.
| public transform<K extends OperationType>(againstOp: Operation<K> | Operation<OperationType.Neutral>): Operation<T | OperationType.Neutral> { | |
| return this.#transformer.transform(this, againstOp); | |
| } | |
| /** | |
| public transform<K extends OperationType>(againstOp: Operation<K> | Operation<OperationType.Neutral>): Operation<T | OperationType.Neutral> | null { | |
| const transformedOp = this.#transformer.transform(this, againstOp); | |
| // Validate the resulting operation's index or range | |
| if (!this.isValidOperation(transformedOp)) { | |
| return null; | |
| } | |
| return transformedOp; | |
| } | |
| /** | |
| * Validates the operation to ensure its index or range is valid | |
| * | |
| * @param operation - operation to validate | |
| */ | |
| private isValidOperation(operation: Operation<T | OperationType.Neutral> | null): boolean { | |
| if (!operation) { | |
| return false; | |
| } | |
| // Example validation: Ensure index is non-negative | |
| if (operation.index.start < 0 || operation.index.end < 0) { | |
| return false; | |
| } | |
| // Add additional validation logic as needed | |
| return true; | |
| } | |
| /** |
| export class OperationsTransformer { | ||
| constructor() {} | ||
|
|
||
| public transform<T extends OperationType>(operation: Operation<T>, againstOp: Operation<OperationType>): Operation<T> | Operation<OperationType.Neutral> { |
| * | ||
| * Operations are batched on timeout basis or if batch is terminated from the outside | ||
| */ | ||
| export class OperationsBatch<T extends OperationType> extends Operation<T> { |
| * | ||
| * @param opBatch - operation batch to clone | ||
| */ | ||
| public static from<T extends OperationType>(opBatch: OperationsBatch<T>): OperationsBatch<T>; |
There was a problem hiding this comment.
Static methods should go before instance methods
| /** | ||
| * Every batch should have at least one operation | ||
| */ | ||
| const batch = new OperationsBatch(opBatchOrJSON.operations.shift()!); |
There was a problem hiding this comment.
This way the first operation would be copied by reference not by value
| /** | ||
| * Array of operations to batch | ||
| */ | ||
| operations: (Operation<T> | Operation<OperationType.Neutral>)[] = []; |
| #transformAgainstTextInsert<T extends OperationType>(operation: Operation<T>, againstOp: Operation<OperationType>): Operation<T> | Operation<OperationType.Neutral> { | ||
| const newIndexBuilder = new IndexBuilder().from(operation.index); | ||
|
|
||
| const amountOfInsertedCharacters = againstOp.data.payload!.length; |
There was a problem hiding this comment.
| const amountOfInsertedCharacters = againstOp.data.payload!.length; | |
| const insertedLength = againstOp.data.payload!.length; |
| newIndexBuilder.addTextRange([operation.index.textRange![0], operation.index.textRange![1] + amountOfInsertedCharacters]); | ||
| } | ||
|
|
||
| return new Operation(operation.type, newIndexBuilder.build(), operation.data, operation.userId, operation.rev); |
There was a problem hiding this comment.
const newOp = Operation.from(operation);
newOp.index = newIndexBuilder.build();
return newOp;
|
|
||
| const deletedRightSide = (againstOp.index.textRange![0] > operation.index.textRange![0]) | ||
| && (againstOp.index.textRange![0] < operation.index.textRange![1]) | ||
| && (againstOp.index.textRange![1] <= operation.index.textRange![1]); |
There was a problem hiding this comment.
| && (againstOp.index.textRange![1] <= operation.index.textRange![1]); | |
| && (againstOp.index.textRange![1] >= operation.index.textRange![1]); |
| * Cover case 2.1 | ||
| */ | ||
| if (deletedLeftSide) { | ||
| const deletedFromCurrentOpRange = operation.index.textRange![0] - againstOp.index.textRange![1]; |
There was a problem hiding this comment.
| const deletedFromCurrentOpRange = operation.index.textRange![0] - againstOp.index.textRange![1]; | |
| const deletedFromCurrentOpRange = againstOp.index.textRange![1] - operation.index.textRange![0]; |
| if (deletedLeftSide) { | ||
| const deletedFromCurrentOpRange = operation.index.textRange![0] - againstOp.index.textRange![1]; | ||
|
|
||
| newIndexBuilder.addTextRange([againstOp.index.textRange![0], operation.index.textRange![1] - deletedFromCurrentOpRange]); |
There was a problem hiding this comment.
| newIndexBuilder.addTextRange([againstOp.index.textRange![0], operation.index.textRange![1] - deletedFromCurrentOpRange]); | |
| newIndexBuilder.addTextRange([againstOp.index.textRange![0], operation.index.textRange![1] - deletedAmount]); |
| if (againstIndex.isBlockIndex && currentIndex.blockIndex !== undefined) { | ||
| /** | ||
| * Check that againstOp affects current operation | ||
| */ | ||
| if (againstIndex.blockIndex! <= currentIndex.blockIndex) { | ||
| return this.#transformAgainstBlockOperation(operation, againstOp); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Cover 4 case | ||
| * | ||
| * Check that againstOp is a text operation and current operation is also a text operation | ||
| */ | ||
| if (againstIndex.isTextIndex && currentIndex.isTextIndex) { | ||
| /** | ||
| * Check that againstOp affects current operation (text operation on the same block and same input) | ||
| * and against op happened on the left side or has overlapping range | ||
| */ | ||
| if (currentIndex.dataKey === againstIndex.dataKey && currentIndex.blockIndex === againstIndex.blockIndex && againstIndex.textRange![0] <= currentIndex.textRange![0]) { |
There was a problem hiding this comment.
Replace with
switch (true) {
case againstOp.index.isBlockIndex:
return this.#transformAgainsBlockOperation(...);
case againstOp.index.isTextIndex:
return this.#transformAgainsTextOperation(...);
default:
return Operation.from(operation);
};
And move other conditions into the respective methods
| if (operation instanceof BatchedOperation) { | ||
| operation.operations.forEach((op) => { | ||
| this.applyOperation(op); | ||
| }); | ||
| } else { | ||
| this.applyOperation(operation); | ||
| } |
There was a problem hiding this comment.
Better to move this condition into the applyOperation method since BatchedOperation is also an operation
There was a problem hiding this comment.
Ah, it's already there. So you can remove it here
| * @todo - add debounce timeout 500ms | ||
| */ | ||
| if (!this.#currentBatch.canAdd(operation)) { | ||
| this.#undoRedoManager.put(this.#currentBatch); |
There was a problem hiding this comment.
You can call emptyBatch here to be consistent
| /** | ||
| * If current operation could not be added to the batch, then terminate current batch and create a new one with current operation | ||
| * | ||
| * @todo - add debounce timeout 500ms |
There was a problem hiding this comment.
Would be great if you can implement it in the current PR as this functionality is already in the main branch
|
|
||
|
|
||
| describe('Operation', () => { | ||
| describe('.transform()', () => { |
There was a problem hiding this comment.
Don't delete it, those tests still should pass even tho the functionality in another file now
| /** | ||
| * Puts current batch to the undo stack and clears the batch | ||
| */ | ||
| #emptyBatch(): void { |
There was a problem hiding this comment.
I don't really like the name as it implies the current batch is being emptied. Also it doesn't show that the current batch is being added to the undo redo manager
| * Cover case 2 | ||
| */ | ||
| case OperationType.Delete: | ||
| if (operation.index.blockIndex !== undefined) { |
There was a problem hiding this comment.
You've already checked that on line 90
| if (sameInput && sameBlock && againstIndex.textRange![0] > index.textRange![1]) { | ||
| return Operation.from(operation); | ||
| } |
There was a problem hiding this comment.
You can extend condition here to check if it's not the same input or not the same block
| case (RangeIntersectionType.Right): | ||
| const overlapLength = index.textRange![1] - againstIndex.textRange![0]; | ||
|
|
||
| newIndexBuilder.addTextRange([index.textRange![0], index.textRange![1] - overlapLength]); |
There was a problem hiding this comment.
You can put againstOp.textRange[0] as end index here
| * @param operation - operation to transform against | ||
| * @param stack - stack to transform | ||
| */ | ||
| public transformStack(operation: Operation, stack: Operation[]): void { |
There was a problem hiding this comment.
Make it pure function please.
Also could be private
| * @param stack - stack to transform | ||
| */ | ||
| public transformStack(operation: Operation, stack: Operation[]): void { | ||
| const transformed = stack.flatMap((op) => { |
There was a problem hiding this comment.
Why flatMap? Transform doesn't return null anymore
gohabereg
left a comment
There was a problem hiding this comment.
Couple of minor comments, looks good to me otherwise
| */ | ||
| const batch = new BatchedOperation(Operation.from(opBatchOrJSON.operations[0])); | ||
|
|
||
| opBatchOrJSON.operations.slice(1).forEach((op) => { |
There was a problem hiding this comment.
Perhaps the linter is not set up, but would be great to have chained calls on the new lines. Here and in the other places
| opBatchOrJSON.operations.slice(1).forEach((op) => { | |
| opBatchOrJSON.operations.slice(1) | |
| .forEach((op) => { |
| return true; | ||
| } | ||
|
|
||
| if (!op.index.isTextIndex || !lastOp.index.isTextIndex) { |
There was a problem hiding this comment.
Maybe leave a todo that we might add other index types in the future
| * Redo last undone operation in the local stack | ||
| */ | ||
| public redo(): void { | ||
| this.#currentBatch?.terminate(); |
There was a problem hiding this comment.
It's not clear why in both cases (undo and redo) there's a call moveBatchToUndo
There was a problem hiding this comment.
Maybe putBatchToUndo would be better
| #debounce(): void { | ||
| clearTimeout(this.#debounceTimer); | ||
|
|
||
| this.#debounceTimer = setTimeout(() => { |
There was a problem hiding this comment.
Please cover untested lines and statements
gohabereg
left a comment
There was a problem hiding this comment.
Missed the most important file
| * Cover case 2 | ||
| */ | ||
| case OperationType.Delete: | ||
| if (againstOp.index.blockIndex! >= operation.index.blockIndex!) { |
There was a problem hiding this comment.
Could be just ===, you've checked > on line 104
| /** | ||
| * Cover case 2.1 | ||
| */ | ||
| case (RangeIntersectionType.Left): |
There was a problem hiding this comment.
intersectionType contains the type of current op compared to againstOp. So with RageIntersectionType.Left the current op is on the left — the against op is on the right. It should be RangeIntersectionType.Right I guess
| /** | ||
| * Cover case 2.2 | ||
| */ | ||
| case (RangeIntersectionType.Right): |
There was a problem hiding this comment.
And here should be RangeIntersectionType.Left
There was a problem hiding this comment.
You should add more test cases to cover all of that
Changes
OperationsBatchnow extendsOperationOperationType.Neutral- operation, that does not affect model on apply, so could be skipped, this type of operations could appear after transformation