mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[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:
parent
881abf6e5f
commit
2440f89be5
21 changed files with 650 additions and 26 deletions
|
@ -13,6 +13,9 @@ exports.Label = require('./lib/label')
|
|||
exports.AddFileOperation = require('./lib/operation/add_file_operation')
|
||||
exports.MoveFileOperation = require('./lib/operation/move_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.NoOperation = require('./lib/operation/no_operation')
|
||||
exports.Operation = require('./lib/operation')
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
// @ts-check
|
||||
const { RetainOp, InsertOp, RemoveOp } = require('./operation/scan_op')
|
||||
const Range = require('./range')
|
||||
|
||||
/**
|
||||
* @typedef {import("../types").CommentRawData} CommentRawData
|
||||
* @typedef {import("./types").CommentRawData} CommentRawData
|
||||
* @typedef {import("./operation/text_operation")} TextOperation
|
||||
*/
|
||||
|
||||
class Comment {
|
||||
|
@ -118,10 +120,32 @@ class Comment {
|
|||
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() {
|
||||
return this.ranges.length === 0
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {CommentRawData}
|
||||
*/
|
||||
toRaw() {
|
||||
return {
|
||||
resolved: this.resolved,
|
||||
|
@ -149,6 +173,13 @@ class Comment {
|
|||
this.ranges = mergedRanges
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Comment}
|
||||
*/
|
||||
clone() {
|
||||
return Comment.fromRaw(this.toRaw())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CommentRawData} rawComment
|
||||
* @returns {Comment}
|
|
@ -1,9 +1,9 @@
|
|||
// @ts-check
|
||||
const Comment = require('./comment')
|
||||
const Comment = require('../comment')
|
||||
|
||||
/**
|
||||
* @typedef {import("../types").CommentRawData} CommentRawData
|
||||
* @typedef {import("./range")} Range
|
||||
* @typedef {import("../types").CommentsListRawData} CommentsListRawData
|
||||
* @typedef {import("../range")} Range
|
||||
*/
|
||||
|
||||
class CommentList {
|
||||
|
@ -15,7 +15,7 @@ class CommentList {
|
|||
}
|
||||
|
||||
/**
|
||||
* @returns {CommentRawData[]}
|
||||
* @returns {CommentsListRawData}
|
||||
*/
|
||||
getComments() {
|
||||
return Array.from(this.comments).map(([commentId, comment]) => {
|
||||
|
@ -41,6 +41,7 @@ class CommentList {
|
|||
add(id, newComment) {
|
||||
const existingComment = this.getComment(id)
|
||||
if (existingComment) {
|
||||
existingComment.resolved = existingComment.resolved && newComment.resolved
|
||||
for (const range of newComment.ranges) {
|
||||
existingComment.addRange(range)
|
||||
}
|
||||
|
@ -57,7 +58,7 @@ class CommentList {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {CommentRawData[]} rawComments
|
||||
* @param {CommentsListRawData} rawComments
|
||||
*/
|
||||
static fromRaw(rawComments) {
|
||||
const comments = new Map()
|
||||
|
|
|
@ -11,15 +11,15 @@ const TrackedChangeList = require('./tracked_change_list')
|
|||
* @typedef {import("../types").StringFileRawData} StringFileRawData
|
||||
* @typedef {import("../operation/edit_operation")} EditOperation
|
||||
* @typedef {import("../types").BlobStore} BlobStore
|
||||
* @typedef {import("../types").CommentRawData} CommentRawData
|
||||
* @typedef {import("../types").CommentsListRawData} CommentsListRawData
|
||||
* @typedef {import("../types").TrackedChangeRawData} TrackedChangeRawData
|
||||
*/
|
||||
|
||||
class StringFileData extends FileData {
|
||||
/**
|
||||
* @param {string} content
|
||||
* @param {CommentRawData[] | undefined} [rawComments]
|
||||
* @param {TrackedChangeRawData[] | undefined} [rawTrackedChanges]
|
||||
* @param {CommentsListRawData} [rawComments]
|
||||
* @param {TrackedChangeRawData[]} [rawTrackedChanges]
|
||||
*/
|
||||
constructor(content, rawComments = [], rawTrackedChanges = []) {
|
||||
super()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @ts-check
|
||||
const Range = require('./range')
|
||||
const Range = require('../range')
|
||||
const TrackingProps = require('./tracking_props')
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @ts-check
|
||||
const Range = require('./range')
|
||||
const Range = require('../range')
|
||||
const TrackedChange = require('./tracked_change')
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
const EditOperation = require('./edit_operation')
|
||||
|
||||
class EditNoOperation extends EditOperation {}
|
||||
|
||||
module.exports = EditNoOperation
|
|
@ -1,8 +1,13 @@
|
|||
// @ts-check
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
const DeleteCommentOperation = require('./delete_comment_operation')
|
||||
const AddCommentOperation = require('./add_comment_operation')
|
||||
const TextOperation = require('./text_operation')
|
||||
|
||||
class EditOperationBuilder {
|
||||
|
@ -12,11 +17,41 @@ class EditOperationBuilder {
|
|||
* @returns {EditOperation}
|
||||
*/
|
||||
static fromJSON(raw) {
|
||||
if (raw.textOperation) {
|
||||
if (isTextOperation(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')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// @ts-check
|
||||
const core = require('../..')
|
||||
const EditNoOperation = require('./edit_no_operation')
|
||||
const TextOperation = require('./text_operation')
|
||||
/** @typedef {import('./edit_operation')} EditOperation */
|
||||
|
||||
|
@ -7,11 +9,70 @@ class EditOperationTransformer {
|
|||
* Transform two edit operations against each other.
|
||||
* @param {EditOperation} a
|
||||
* @param {EditOperation} b
|
||||
* @returns {[EditOperation, EditOperation]}
|
||||
*/
|
||||
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) {
|
||||
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(
|
||||
`Transform not implemented for ${a.constructor.name}○${b.constructor.name}`
|
||||
)
|
||||
|
|
|
@ -25,7 +25,7 @@ const {
|
|||
InvalidInsertionError,
|
||||
TooLongError,
|
||||
} = require('../errors')
|
||||
const Range = require('../file_data/range')
|
||||
const Range = require('../range')
|
||||
const TrackingProps = require('../file_data/tracking_props')
|
||||
/**
|
||||
* @typedef {import('../file_data/string_file_data')} StringFileData
|
||||
|
|
|
@ -11,7 +11,6 @@ type Range = {
|
|||
}
|
||||
|
||||
export type CommentRawData = {
|
||||
id: string
|
||||
ranges: Range[]
|
||||
resolved?: boolean
|
||||
}
|
||||
|
@ -27,9 +26,11 @@ export type TrackingPropsRawData = {
|
|||
ts: string
|
||||
}
|
||||
|
||||
export type CommentsListRawData = Array<{ id: string } & CommentRawData>
|
||||
|
||||
export type StringFileRawData = {
|
||||
content: string
|
||||
comments?: CommentRawData[]
|
||||
comments?: CommentsListRawData
|
||||
trackedChanges?: TrackedChangeRawData[]
|
||||
}
|
||||
|
||||
|
@ -58,4 +59,11 @@ export type RawTextOperation = {
|
|||
textOperation: RawScanOp[]
|
||||
}
|
||||
|
||||
export type RawEditOperation = RawTextOperation
|
||||
export type RawAddCommentOperation = CommentRawData & { commentId: string }
|
||||
|
||||
export type RawDeleteCommentOperation = { deleteComment: string }
|
||||
|
||||
export type RawEditOperation =
|
||||
| RawTextOperation
|
||||
| RawAddCommentOperation
|
||||
| RawDeleteCommentOperation
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -2,8 +2,8 @@
|
|||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const Comment = require('../lib/file_data/comment')
|
||||
const Range = require('../lib/file_data/range')
|
||||
const Comment = require('../lib/comment')
|
||||
const Range = require('../lib/range')
|
||||
|
||||
describe('Comment', function () {
|
||||
it('should move ranges to the right of insert', function () {
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
const { expect } = require('chai')
|
||||
const CommentList = require('../lib/file_data/comment_list')
|
||||
const Comment = require('../lib/file_data/comment')
|
||||
const Range = require('../lib/file_data/range')
|
||||
const Comment = require('../lib/comment')
|
||||
const Range = require('../lib/range')
|
||||
|
||||
describe('commentList', 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 () {
|
||||
const commentList = new CommentList(
|
||||
new Map([
|
||||
['comm1', new Comment([new Range(5, 10)])],
|
||||
['comm2', new Comment([new Range(20, 5)])],
|
||||
['comm1', new Comment([new Range(5, 10)], false)],
|
||||
['comm2', new Comment([new Range(20, 5)], true)],
|
||||
['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([
|
||||
{ id: 'comm1', ranges: [{ pos: 5, length: 10 }], resolved: false },
|
||||
{
|
||||
|
@ -92,7 +94,7 @@ describe('commentList', function () {
|
|||
{ pos: 20, length: 5 },
|
||||
{ pos: 40, length: 10 },
|
||||
],
|
||||
resolved: false,
|
||||
resolved: true,
|
||||
},
|
||||
{
|
||||
id: 'comm3',
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -5,6 +5,11 @@ const EditOperationTransformer = require('../lib/operation/edit_operation_transf
|
|||
const EditOperation = require('../lib/operation/edit_operation')
|
||||
const randomTextOperation = require('./support/random_text_operation')
|
||||
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 () {
|
||||
it('Cannot be instantiated', function () {
|
||||
|
@ -22,6 +27,171 @@ describe('EditOperationTransformer', function () {
|
|||
expect(aPrime).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 () {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const Range = require('../lib/file_data/range')
|
||||
const Range = require('../lib/range')
|
||||
|
||||
describe('Range', function () {
|
||||
it('should create a range', function () {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
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 { expect } = require('chai')
|
||||
|
||||
|
|
Loading…
Reference in a new issue