[overleaf-editor-core] AddCommentOperation and DeleteCommentOperation (#16871)

* [overleaf-editor-core] AddCommentOperation and DeleteCommentOperation

* added add comment op test

* delete comment op test

* import core to escape circle deps

* desctructure in tests

* require directly in builder

* invert of add comment is always delete comment

* no merging on compose

* NoOp if comment is not found

* use comment.clone()

* update test

* change CommentRawData type

* jsdoc assert type

* fix formating

* EditNoOperation

* return other in compose

* use ReturnType

* Revert "use ReturnType"

This reverts commit 2c7e04f1541310e9fc08963170a783a437ed1992.

* transorm add comment operation

* transform delete comment operation

* moved comment.js

* format fix

* fix transform addComment and textoperation

* fix merge

* test more complex test operations

* change to else if

* move range.js

* fix types

* fix AddComment and TextOperation transform

* fixed AddComment-TextOperation trasform, added test

* deletecommentoperation should win

* should not delete comment

* remove unused function, fix type

* fix format

* add resolved for existing comment

* transform EditNoOperation

* fix test description

* change the order of EditNoOperation

* fix DeleteCommentOperation-DeleteCommentOperation transform

* fix types after merging main

* refactor operation types

GitOrigin-RevId: 6f127763a6dc50d4fe3524d9b25dc7526b6b0028
This commit is contained in:
Domagoj Kriskovic 2024-02-16 14:38:07 +01:00 committed by Copybot
parent 881abf6e5f
commit 2440f89be5
21 changed files with 650 additions and 26 deletions

View file

@ -13,6 +13,9 @@ exports.Label = require('./lib/label')
exports.AddFileOperation = require('./lib/operation/add_file_operation') exports.AddFileOperation = require('./lib/operation/add_file_operation')
exports.MoveFileOperation = require('./lib/operation/move_file_operation') exports.MoveFileOperation = require('./lib/operation/move_file_operation')
exports.EditFileOperation = require('./lib/operation/edit_file_operation') exports.EditFileOperation = require('./lib/operation/edit_file_operation')
exports.EditNoOperation = require('./lib/operation/edit_no_operation')
exports.AddCommentOperation = require('./lib/operation/add_comment_operation')
exports.DeleteCommentOperation = require('./lib/operation/delete_comment_operation')
exports.SetFileMetadataOperation = require('./lib/operation/set_file_metadata_operation') exports.SetFileMetadataOperation = require('./lib/operation/set_file_metadata_operation')
exports.NoOperation = require('./lib/operation/no_operation') exports.NoOperation = require('./lib/operation/no_operation')
exports.Operation = require('./lib/operation') exports.Operation = require('./lib/operation')

View file

@ -1,8 +1,10 @@
// @ts-check // @ts-check
const { RetainOp, InsertOp, RemoveOp } = require('./operation/scan_op')
const Range = require('./range') const Range = require('./range')
/** /**
* @typedef {import("../types").CommentRawData} CommentRawData * @typedef {import("./types").CommentRawData} CommentRawData
* @typedef {import("./operation/text_operation")} TextOperation
*/ */
class Comment { class Comment {
@ -118,10 +120,32 @@ class Comment {
this.mergeRanges() this.mergeRanges()
} }
/**
*
* @param {TextOperation} operation
*/
applyTextOperation(operation) {
let cursor = 0
for (const op of operation.ops) {
if (op instanceof RetainOp) {
cursor += op.length
} else if (op instanceof InsertOp) {
this.applyInsert(cursor, op.insertion.length)
cursor += op.insertion.length
} else if (op instanceof RemoveOp) {
this.applyDelete(new Range(cursor, op.length))
}
}
}
isEmpty() { isEmpty() {
return this.ranges.length === 0 return this.ranges.length === 0
} }
/**
*
* @returns {CommentRawData}
*/
toRaw() { toRaw() {
return { return {
resolved: this.resolved, resolved: this.resolved,
@ -149,6 +173,13 @@ class Comment {
this.ranges = mergedRanges this.ranges = mergedRanges
} }
/**
* @returns {Comment}
*/
clone() {
return Comment.fromRaw(this.toRaw())
}
/** /**
* @param {CommentRawData} rawComment * @param {CommentRawData} rawComment
* @returns {Comment} * @returns {Comment}

View file

@ -1,9 +1,9 @@
// @ts-check // @ts-check
const Comment = require('./comment') const Comment = require('../comment')
/** /**
* @typedef {import("../types").CommentRawData} CommentRawData * @typedef {import("../types").CommentsListRawData} CommentsListRawData
* @typedef {import("./range")} Range * @typedef {import("../range")} Range
*/ */
class CommentList { class CommentList {
@ -15,7 +15,7 @@ class CommentList {
} }
/** /**
* @returns {CommentRawData[]} * @returns {CommentsListRawData}
*/ */
getComments() { getComments() {
return Array.from(this.comments).map(([commentId, comment]) => { return Array.from(this.comments).map(([commentId, comment]) => {
@ -41,6 +41,7 @@ class CommentList {
add(id, newComment) { add(id, newComment) {
const existingComment = this.getComment(id) const existingComment = this.getComment(id)
if (existingComment) { if (existingComment) {
existingComment.resolved = existingComment.resolved && newComment.resolved
for (const range of newComment.ranges) { for (const range of newComment.ranges) {
existingComment.addRange(range) existingComment.addRange(range)
} }
@ -57,7 +58,7 @@ class CommentList {
} }
/** /**
* @param {CommentRawData[]} rawComments * @param {CommentsListRawData} rawComments
*/ */
static fromRaw(rawComments) { static fromRaw(rawComments) {
const comments = new Map() const comments = new Map()

View file

@ -11,15 +11,15 @@ const TrackedChangeList = require('./tracked_change_list')
* @typedef {import("../types").StringFileRawData} StringFileRawData * @typedef {import("../types").StringFileRawData} StringFileRawData
* @typedef {import("../operation/edit_operation")} EditOperation * @typedef {import("../operation/edit_operation")} EditOperation
* @typedef {import("../types").BlobStore} BlobStore * @typedef {import("../types").BlobStore} BlobStore
* @typedef {import("../types").CommentRawData} CommentRawData * @typedef {import("../types").CommentsListRawData} CommentsListRawData
* @typedef {import("../types").TrackedChangeRawData} TrackedChangeRawData * @typedef {import("../types").TrackedChangeRawData} TrackedChangeRawData
*/ */
class StringFileData extends FileData { class StringFileData extends FileData {
/** /**
* @param {string} content * @param {string} content
* @param {CommentRawData[] | undefined} [rawComments] * @param {CommentsListRawData} [rawComments]
* @param {TrackedChangeRawData[] | undefined} [rawTrackedChanges] * @param {TrackedChangeRawData[]} [rawTrackedChanges]
*/ */
constructor(content, rawComments = [], rawTrackedChanges = []) { constructor(content, rawComments = [], rawTrackedChanges = []) {
super() super()

View file

@ -1,5 +1,5 @@
// @ts-check // @ts-check
const Range = require('./range') const Range = require('../range')
const TrackingProps = require('./tracking_props') const TrackingProps = require('./tracking_props')
/** /**

View file

@ -1,5 +1,5 @@
// @ts-check // @ts-check
const Range = require('./range') const Range = require('../range')
const TrackedChange = require('./tracked_change') const TrackedChange = require('./tracked_change')
/** /**

View file

@ -0,0 +1,102 @@
// @ts-check
const core = require('../../index')
const Comment = require('../comment')
const EditOperation = require('./edit_operation')
/**
* @typedef {import('./delete_comment_operation')} DeleteCommentOperation
* @typedef {import('../types').CommentRawData} CommentRawData
* @typedef {import('../types').RawAddCommentOperation} RawAddCommentOperation
* @typedef {import('../file_data/string_file_data')} StringFileData
*/
/**
* @extends EditOperation
*/
class AddCommentOperation extends EditOperation {
/**
* @param {string} commentId
* @param {Comment} comment
*/
constructor(commentId, comment) {
super()
this.commentId = commentId
this.comment = comment
}
/**
*
* @returns {RawAddCommentOperation}
*/
toJSON() {
return {
...this.comment.toRaw(),
commentId: this.commentId,
}
}
/**
* @param {StringFileData} fileData
*/
apply(fileData) {
fileData.comments.add(this.commentId, this.comment)
}
/**
*
* @returns {DeleteCommentOperation}
*/
invert() {
return new core.DeleteCommentOperation(this.commentId)
}
/**
* @inheritdoc
* @param {EditOperation} other
* @returns {boolean}
*/
canBeComposedWith(other) {
return (
(other instanceof AddCommentOperation &&
this.commentId === other.commentId) ||
(other instanceof core.DeleteCommentOperation &&
this.commentId === other.commentId)
)
}
/**
* @inheritdoc
* @param {EditOperation} other
* @returns {EditOperation}
*/
compose(other) {
if (
other instanceof core.DeleteCommentOperation &&
other.commentId === this.commentId
) {
return other
}
if (
other instanceof AddCommentOperation &&
other.commentId === this.commentId
) {
return other
}
throw new Error(
`Trying to compose AddCommentOperation with ${other?.constructor?.name}.`
)
}
/**
* @inheritdoc
* @param {RawAddCommentOperation} raw
* @returns {AddCommentOperation}
*/
static fromJSON(raw) {
return new AddCommentOperation(raw.commentId, Comment.fromRaw(raw))
}
}
module.exports = AddCommentOperation

View file

@ -0,0 +1,66 @@
// @ts-check
const core = require('../../index')
const EditNoOperation = require('./edit_no_operation')
const EditOperation = require('./edit_operation')
/**
* @typedef {import('./add_comment_operation')} AddCommentOperation
* @typedef {import('../types').RawDeleteCommentOperation} RawDeleteCommentOperation
* @typedef {import('../file_data/string_file_data')} StringFileData
*/
/**
* @extends EditOperation
*/
class DeleteCommentOperation extends EditOperation {
/**
* @param {string} commentId
*/
constructor(commentId) {
super()
this.commentId = commentId
}
/**
* @inheritdoc
* @returns {RawDeleteCommentOperation}
*/
toJSON() {
return {
deleteComment: this.commentId,
}
}
/**
* @inheritdoc
* @param {StringFileData} fileData
*/
apply(fileData) {
fileData.comments.delete(this.commentId)
}
/**
* @inheritdoc
* @param {StringFileData} previousState
* @returns {AddCommentOperation | EditNoOperation}
*/
invert(previousState) {
const comment = previousState.comments.getComment(this.commentId)
if (!comment) {
return new EditNoOperation()
}
return new core.AddCommentOperation(this.commentId, comment.clone())
}
/**
* @inheritdoc
* @param {RawDeleteCommentOperation} raw
* @returns {DeleteCommentOperation}
*/
static fromJSON(raw) {
return new DeleteCommentOperation(raw.deleteComment)
}
}
module.exports = DeleteCommentOperation

View file

@ -0,0 +1,5 @@
const EditOperation = require('./edit_operation')
class EditNoOperation extends EditOperation {}
module.exports = EditNoOperation

View file

@ -1,8 +1,13 @@
// @ts-check // @ts-check
/** /**
* @typedef {import('./edit_operation')} EditOperation * @typedef {import('./edit_operation')} EditOperation
* @typedef {import('../types').RawTextOperation} RawTextOperation
* @typedef {import('../types').RawAddCommentOperation} RawAddCommentOperation
* @typedef {import('../types').RawDeleteCommentOperation} RawDeleteCommentOperation
* @typedef {import('../types').RawEditOperation} RawEditOperation * @typedef {import('../types').RawEditOperation} RawEditOperation
*/ */
const DeleteCommentOperation = require('./delete_comment_operation')
const AddCommentOperation = require('./add_comment_operation')
const TextOperation = require('./text_operation') const TextOperation = require('./text_operation')
class EditOperationBuilder { class EditOperationBuilder {
@ -12,11 +17,41 @@ class EditOperationBuilder {
* @returns {EditOperation} * @returns {EditOperation}
*/ */
static fromJSON(raw) { static fromJSON(raw) {
if (raw.textOperation) { if (isTextOperation(raw)) {
return TextOperation.fromJSON(raw) return TextOperation.fromJSON(raw)
} }
if (isRawAddCommentOperation(raw)) {
return AddCommentOperation.fromJSON(raw)
}
if (isRawDeleteCommentOperation(raw)) {
return DeleteCommentOperation.fromJSON(raw)
}
throw new Error('Unsupported operation in EditOperationBuilder.fromJSON') throw new Error('Unsupported operation in EditOperationBuilder.fromJSON')
} }
} }
/**
* @param {*} raw
* @returns {raw is RawTextOperation}
*/
function isTextOperation(raw) {
return raw?.textOperation !== undefined
}
/**
* @param {*} raw
* @returns {raw is RawAddCommentOperation}
*/
function isRawAddCommentOperation(raw) {
return raw?.commentId && Array.isArray(raw.ranges)
}
/**
* @param {*} raw
* @returns {raw is RawDeleteCommentOperation}
*/
function isRawDeleteCommentOperation(raw) {
return raw?.deleteComment
}
module.exports = EditOperationBuilder module.exports = EditOperationBuilder

View file

@ -1,4 +1,6 @@
// @ts-check // @ts-check
const core = require('../..')
const EditNoOperation = require('./edit_no_operation')
const TextOperation = require('./text_operation') const TextOperation = require('./text_operation')
/** @typedef {import('./edit_operation')} EditOperation */ /** @typedef {import('./edit_operation')} EditOperation */
@ -7,11 +9,70 @@ class EditOperationTransformer {
* Transform two edit operations against each other. * Transform two edit operations against each other.
* @param {EditOperation} a * @param {EditOperation} a
* @param {EditOperation} b * @param {EditOperation} b
* @returns {[EditOperation, EditOperation]}
*/ */
static transform(a, b) { static transform(a, b) {
const { AddCommentOperation, DeleteCommentOperation } = core
if (a instanceof EditNoOperation || b instanceof EditNoOperation) {
return [a, b]
}
if (a instanceof TextOperation && b instanceof TextOperation) { if (a instanceof TextOperation && b instanceof TextOperation) {
return TextOperation.transform(a, b) return TextOperation.transform(a, b)
} }
if (a instanceof TextOperation && b instanceof DeleteCommentOperation) {
return [a, b]
}
if (a instanceof DeleteCommentOperation && b instanceof TextOperation) {
return [a, b]
}
if (a instanceof AddCommentOperation && b instanceof TextOperation) {
const comment = a.comment.clone()
comment.applyTextOperation(b)
return [new AddCommentOperation(a.commentId, comment), b]
}
if (a instanceof TextOperation && b instanceof AddCommentOperation) {
const comment = b.comment.clone()
comment.applyTextOperation(a)
return [a, new AddCommentOperation(b.commentId, comment)]
}
if (a instanceof AddCommentOperation && b instanceof AddCommentOperation) {
return [a, b]
}
if (
a instanceof DeleteCommentOperation &&
b instanceof AddCommentOperation
) {
if (a.commentId === b.commentId) {
return [a, new EditNoOperation()]
}
return [a, b]
}
if (
a instanceof AddCommentOperation &&
b instanceof DeleteCommentOperation
) {
if (a.commentId === b.commentId) {
return [new EditNoOperation(), b]
}
return [a, b]
}
if (
a instanceof DeleteCommentOperation &&
b instanceof DeleteCommentOperation
) {
if (a.commentId === b.commentId) {
return [new EditNoOperation(), new EditNoOperation()]
}
return [a, b]
}
throw new Error( throw new Error(
`Transform not implemented for ${a.constructor.name}${b.constructor.name}` `Transform not implemented for ${a.constructor.name}${b.constructor.name}`
) )

View file

@ -25,7 +25,7 @@ const {
InvalidInsertionError, InvalidInsertionError,
TooLongError, TooLongError,
} = require('../errors') } = require('../errors')
const Range = require('../file_data/range') const Range = require('../range')
const TrackingProps = require('../file_data/tracking_props') const TrackingProps = require('../file_data/tracking_props')
/** /**
* @typedef {import('../file_data/string_file_data')} StringFileData * @typedef {import('../file_data/string_file_data')} StringFileData

View file

@ -11,7 +11,6 @@ type Range = {
} }
export type CommentRawData = { export type CommentRawData = {
id: string
ranges: Range[] ranges: Range[]
resolved?: boolean resolved?: boolean
} }
@ -27,9 +26,11 @@ export type TrackingPropsRawData = {
ts: string ts: string
} }
export type CommentsListRawData = Array<{ id: string } & CommentRawData>
export type StringFileRawData = { export type StringFileRawData = {
content: string content: string
comments?: CommentRawData[] comments?: CommentsListRawData
trackedChanges?: TrackedChangeRawData[] trackedChanges?: TrackedChangeRawData[]
} }
@ -58,4 +59,11 @@ export type RawTextOperation = {
textOperation: RawScanOp[] textOperation: RawScanOp[]
} }
export type RawEditOperation = RawTextOperation export type RawAddCommentOperation = CommentRawData & { commentId: string }
export type RawDeleteCommentOperation = { deleteComment: string }
export type RawEditOperation =
| RawTextOperation
| RawAddCommentOperation
| RawDeleteCommentOperation

View file

@ -0,0 +1,91 @@
// @ts-check
const { expect } = require('chai')
const { AddCommentOperation, DeleteCommentOperation } = require('..')
const Comment = require('../lib/comment')
const StringFileData = require('../lib/file_data/string_file_data')
describe('AddCommentOperation', function () {
it('constructs an AddCommentOperation fromJSON', function () {
const op = AddCommentOperation.fromJSON({
commentId: '123',
resolved: true,
ranges: [{ pos: 0, length: 1 }],
})
expect(op).to.be.instanceOf(AddCommentOperation)
expect(op.commentId).to.equal('123')
expect(op.comment).to.be.instanceOf(Comment)
expect(op.comment.resolved).to.be.true
})
it('should convert to JSON', function () {
const op = new AddCommentOperation(
'123',
Comment.fromRaw({
ranges: [
{
pos: 0,
length: 1,
},
],
})
)
expect(op.toJSON()).to.eql({
commentId: '123',
resolved: false,
ranges: [
{
pos: 0,
length: 1,
},
],
})
})
it('should apply operation', function () {
const fileData = new StringFileData('abc')
const op = new AddCommentOperation(
'123',
Comment.fromRaw({ ranges: [{ pos: 0, length: 1 }] })
)
op.apply(fileData)
expect(fileData.getComments()).to.eql([
{
id: '123',
ranges: [{ pos: 0, length: 1 }],
resolved: false,
},
])
})
it('should invert operation', function () {
const fileData = new StringFileData('abc')
const op = new AddCommentOperation(
'123',
Comment.fromRaw({ ranges: [{ pos: 0, length: 1 }] })
)
op.apply(fileData)
expect(fileData.getComments()).to.eql([
{
id: '123',
ranges: [{ pos: 0, length: 1 }],
resolved: false,
},
])
const invertedOp = op.invert()
invertedOp.apply(fileData)
expect(fileData.getComments()).to.eql([])
})
it('should compose with DeleteCommentOperation', function () {
const addOp = new AddCommentOperation(
'123',
Comment.fromRaw({ ranges: [{ pos: 0, length: 1 }] })
)
const deleteOp = new DeleteCommentOperation('123')
expect(addOp.canBeComposedWith(deleteOp)).to.be.true
const composedOp = addOp.compose(deleteOp)
expect(composedOp).to.be.instanceOf(DeleteCommentOperation)
})
})

View file

@ -2,8 +2,8 @@
'use strict' 'use strict'
const { expect } = require('chai') const { expect } = require('chai')
const Comment = require('../lib/file_data/comment') const Comment = require('../lib/comment')
const Range = require('../lib/file_data/range') const Range = require('../lib/range')
describe('Comment', function () { describe('Comment', function () {
it('should move ranges to the right of insert', function () { it('should move ranges to the right of insert', function () {

View file

@ -3,8 +3,8 @@
const { expect } = require('chai') const { expect } = require('chai')
const CommentList = require('../lib/file_data/comment_list') const CommentList = require('../lib/file_data/comment_list')
const Comment = require('../lib/file_data/comment') const Comment = require('../lib/comment')
const Range = require('../lib/file_data/range') const Range = require('../lib/range')
describe('commentList', function () { describe('commentList', function () {
it('checks if toRaw() returns a correct comment list', function () { it('checks if toRaw() returns a correct comment list', function () {
@ -77,13 +77,15 @@ describe('commentList', function () {
it('should add range to existing if a new comment has the same id', function () { it('should add range to existing if a new comment has the same id', function () {
const commentList = new CommentList( const commentList = new CommentList(
new Map([ new Map([
['comm1', new Comment([new Range(5, 10)])], ['comm1', new Comment([new Range(5, 10)], false)],
['comm2', new Comment([new Range(20, 5)])], ['comm2', new Comment([new Range(20, 5)], true)],
['comm3', new Comment([new Range(30, 15)])], ['comm3', new Comment([new Range(30, 15)])],
]) ])
) )
commentList.add('comm2', new Comment([new Range(40, 10)])) commentList.add('comm1', new Comment([new Range(5, 10)], true))
commentList.add('comm2', new Comment([new Range(40, 10)], true))
expect(commentList.getComments()).to.eql([ expect(commentList.getComments()).to.eql([
{ id: 'comm1', ranges: [{ pos: 5, length: 10 }], resolved: false }, { id: 'comm1', ranges: [{ pos: 5, length: 10 }], resolved: false },
{ {
@ -92,7 +94,7 @@ describe('commentList', function () {
{ pos: 20, length: 5 }, { pos: 20, length: 5 },
{ pos: 40, length: 10 }, { pos: 40, length: 10 },
], ],
resolved: false, resolved: true,
}, },
{ {
id: 'comm3', id: 'comm3',

View file

@ -0,0 +1,49 @@
// @ts-check
const { expect } = require('chai')
const { AddCommentOperation, DeleteCommentOperation } = require('..')
const Comment = require('../lib/comment')
const StringFileData = require('../lib/file_data/string_file_data')
const Range = require('../lib/range')
describe('DeleteCommentOperation', function () {
it('constructs an DeleteCommentOperation fromJSON', function () {
const op = DeleteCommentOperation.fromJSON({
deleteComment: '123',
})
expect(op).to.be.instanceOf(DeleteCommentOperation)
})
it('should convert to JSON', function () {
const op = new DeleteCommentOperation('123')
expect(op.toJSON()).to.eql({
deleteComment: '123',
})
})
it('should apply operation', function () {
const fileData = new StringFileData('abc')
const op = new DeleteCommentOperation('123')
fileData.comments.add('123', new Comment([new Range(0, 1)]))
op.apply(fileData)
expect(fileData.getComments()).to.eql([])
})
it('should invert operation', function () {
const fileData = new StringFileData('abc')
const op = new DeleteCommentOperation('123')
fileData.comments.add('123', new Comment([new Range(0, 1)]))
const invertedOp = /** @type {InstanceType<AddCommentOperation>} */ (
op.invert(fileData)
)
expect(invertedOp).to.be.instanceOf(AddCommentOperation)
expect(invertedOp.commentId).to.equal('123')
expect(invertedOp.comment).to.be.instanceOf(Comment)
expect(invertedOp.comment.ranges).to.eql([new Range(0, 1)])
})
it('should not throw if comment not found', function () {
const fileData = new StringFileData('abc')
const op = new DeleteCommentOperation('123')
expect(() => op.invert(fileData)).to.not.throw()
})
})

View file

@ -5,6 +5,11 @@ const EditOperationTransformer = require('../lib/operation/edit_operation_transf
const EditOperation = require('../lib/operation/edit_operation') const EditOperation = require('../lib/operation/edit_operation')
const randomTextOperation = require('./support/random_text_operation') const randomTextOperation = require('./support/random_text_operation')
const random = require('./support/random') const random = require('./support/random')
const AddCommentOperation = require('../lib/operation/add_comment_operation')
const DeleteCommentOperation = require('../lib/operation/delete_comment_operation')
const Comment = require('../lib/comment')
const Range = require('../lib/range')
const EditNoOperation = require('../lib/operation/edit_no_operation')
describe('EditOperation', function () { describe('EditOperation', function () {
it('Cannot be instantiated', function () { it('Cannot be instantiated', function () {
@ -22,6 +27,171 @@ describe('EditOperationTransformer', function () {
expect(aPrime).to.be.an.instanceof(TextOperation) expect(aPrime).to.be.an.instanceof(TextOperation)
expect(bPrime).to.be.an.instanceof(TextOperation) expect(bPrime).to.be.an.instanceof(TextOperation)
}) })
it('Transforms TextOperation and EditNoOperation', function () {
const a = new TextOperation().insert('foo')
const b = new EditNoOperation()
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(TextOperation)
expect(bPrime).to.be.an.instanceof(EditNoOperation)
})
it('Transforms two AddCommentOperations with same commentId', function () {
const a = new AddCommentOperation('comm1', new Comment([new Range(0, 1)]))
const b = new AddCommentOperation('comm1', new Comment([new Range(2, 3)]))
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(AddCommentOperation)
expect(bPrime).to.be.an.instanceof(AddCommentOperation)
})
it('Transforms two AddCommentOperations with different commentId', function () {
const a = new AddCommentOperation('comm1', new Comment([new Range(0, 1)]))
const b = new AddCommentOperation('comm2', new Comment([new Range(2, 3)]))
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(AddCommentOperation)
expect(aPrime.toJSON()).to.eql(a.toJSON())
expect(bPrime).to.be.an.instanceof(AddCommentOperation)
expect(bPrime.toJSON()).to.eql(b.toJSON())
})
it('Transforms two DeleteCommentOperations with same commentId', function () {
const a = new DeleteCommentOperation('comm1')
const b = new DeleteCommentOperation('comm1')
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(EditNoOperation)
expect(bPrime).to.be.an.instanceof(EditNoOperation)
})
it('Transforms two DeleteCommentOperations with different commentId', function () {
const a = new DeleteCommentOperation('comm1')
const b = new DeleteCommentOperation('comm2')
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(DeleteCommentOperation)
expect(aPrime.toJSON()).to.eql(a.toJSON())
expect(bPrime).to.be.an.instanceof(DeleteCommentOperation)
expect(bPrime.toJSON()).to.eql(b.toJSON())
})
it('Transforms AddCommentOperation and DeleteCommentOperation with same commentId', function () {
const a = new AddCommentOperation('comm1', new Comment([new Range(0, 1)]))
const b = new DeleteCommentOperation('comm1')
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(EditNoOperation)
expect(bPrime).to.be.an.instanceof(DeleteCommentOperation)
expect(bPrime.toJSON()).to.eql(b.toJSON())
})
it('Transforms DeleteCommentOperation and AddCommentOperation with same commentId', function () {
const a = new DeleteCommentOperation('comm1')
const b = new AddCommentOperation('comm1', new Comment([new Range(0, 1)]))
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(DeleteCommentOperation)
expect(aPrime.toJSON()).to.eql(a.toJSON())
expect(bPrime).to.be.an.instanceof(EditNoOperation)
})
it('Transforms AddCommentOperation and TextOperation', function () {
// abc hello[ world] xyz - insert(9, " world")
// abc hello |xyz| - addComment(10, 3, "comment_id")
const a = new TextOperation().retain(9).insert(' world')
const b = new AddCommentOperation('comm1', new Comment([new Range(10, 3)]))
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(TextOperation)
expect(aPrime.toJSON()).to.eql(a.toJSON())
expect(bPrime).to.be.an.instanceof(AddCommentOperation)
expect(bPrime.toJSON()).to.eql({
commentId: 'comm1',
ranges: [{ pos: 16, length: 3 }],
resolved: false,
})
})
it('Transforms TextOperation and AddCommentOperation', function () {
// abc hello |xyz| - addComment(10, 3, "comment_id")
// abc hello[ world] xyz - insert(9, " world")
const a = new AddCommentOperation('comm1', new Comment([new Range(10, 3)]))
const b = new TextOperation().retain(9).insert(' world')
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(bPrime).to.be.an.instanceof(TextOperation)
expect(bPrime.toJSON()).to.eql(b.toJSON())
expect(aPrime).to.be.an.instanceof(AddCommentOperation)
expect(aPrime.toJSON()).to.eql({
commentId: 'comm1',
ranges: [{ pos: 16, length: 3 }],
resolved: false,
})
})
it('Transforms AddCommentOperation and TextOperation that makes a detached comment', function () {
// [abc hello xyz] - delete(0, 13)
// abc |hello| xyz - addComment(5, 5, "comment_id")
const a = new TextOperation().remove(13)
const b = new AddCommentOperation('comm1', new Comment([new Range(5, 5)]))
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(TextOperation)
expect(aPrime.toJSON()).to.eql(a.toJSON())
expect(bPrime).to.be.an.instanceof(AddCommentOperation)
expect(bPrime.toJSON()).to.eql({
commentId: 'comm1',
ranges: [],
resolved: false,
})
})
it('Transforms AddCommentOperation and deletion TextOperation', function () {
// abc hell{o xy}z - retain(8).delete(4)
// abc hello |xyz| - addComment(10, 3, "comment_id")
// abc hell|z|
const a = new TextOperation().retain(8).remove(4)
const b = new AddCommentOperation('comm1', new Comment([new Range(10, 3)]))
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(TextOperation)
expect(aPrime.toJSON()).to.eql(a.toJSON())
expect(bPrime).to.be.an.instanceof(AddCommentOperation)
expect(bPrime.toJSON()).to.eql({
commentId: 'comm1',
ranges: [{ pos: 8, length: 1 }],
resolved: false,
})
})
it('Transforms AddCommentOperation and complex TextOperation', function () {
// [foo ]abc hell{o xy}z - insert(0, "foo ").retain(8).delete(4)
// abc hello |xyz| - addComment(10, 3, "comment_id")
// foo abc hell|z|
const a = new TextOperation().insert('foo ').retain(8).remove(4)
const b = new AddCommentOperation('comm1', new Comment([new Range(10, 3)]))
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(TextOperation)
expect(aPrime.toJSON()).to.eql(a.toJSON())
expect(bPrime).to.be.an.instanceof(AddCommentOperation)
expect(bPrime.toJSON()).to.eql({
commentId: 'comm1',
ranges: [{ pos: 12, length: 1 }],
resolved: false,
})
})
it('Transforms DeleteCommentOperation and TextOperation', function () {
const a = new TextOperation().retain(9).insert(' world')
const b = new DeleteCommentOperation('comm1')
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
expect(aPrime).to.be.an.instanceof(TextOperation)
expect(aPrime.toJSON()).to.eql(a.toJSON())
expect(bPrime).to.be.an.instanceof(DeleteCommentOperation)
expect(bPrime.toJSON()).to.eql(b.toJSON())
})
}) })
describe('EditOperationBuilder', function () { describe('EditOperationBuilder', function () {

View file

@ -2,7 +2,7 @@
'use strict' 'use strict'
const { expect } = require('chai') const { expect } = require('chai')
const Range = require('../lib/file_data/range') const Range = require('../lib/range')
describe('Range', function () { describe('Range', function () {
it('should create a range', function () { it('should create a range', function () {

View file

@ -1,6 +1,6 @@
// @ts-check // @ts-check
const TrackedChange = require('../lib/file_data/tracked_change') const TrackedChange = require('../lib/file_data/tracked_change')
const Range = require('../lib/file_data/range') const Range = require('../lib/range')
const TrackingProps = require('../lib/file_data/tracking_props') const TrackingProps = require('../lib/file_data/tracking_props')
const { expect } = require('chai') const { expect } = require('chai')