mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #18659 from overleaf/em-crop-comments-tracked-deletes
Crop comments when processing tracked deletes GitOrigin-RevId: 662c9ed86a8ed4959d1671ce466548487f334f45
This commit is contained in:
parent
4101f4efeb
commit
55c342134c
5 changed files with 214 additions and 7 deletions
|
@ -23,6 +23,12 @@ class AddCommentOperation extends EditOperation {
|
||||||
constructor(commentId, ranges, resolved = false) {
|
constructor(commentId, ranges, resolved = false) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
for (const range of ranges) {
|
||||||
|
if (range.isEmpty()) {
|
||||||
|
throw new Error("AddCommentOperation can't be built with empty ranges")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @readonly */
|
/** @readonly */
|
||||||
this.commentId = commentId
|
this.commentId = commentId
|
||||||
|
|
||||||
|
|
|
@ -56,20 +56,36 @@ const RangesManager = {
|
||||||
RangesManager._emptyRangesCount(rangesTracker)
|
RangesManager._emptyRangesCount(rangesTracker)
|
||||||
const historyUpdates = []
|
const historyUpdates = []
|
||||||
for (const update of updates) {
|
for (const update of updates) {
|
||||||
rangesTracker.track_changes = Boolean(update.meta?.tc)
|
const trackingChanges = Boolean(update.meta?.tc)
|
||||||
|
rangesTracker.track_changes = trackingChanges
|
||||||
if (update.meta?.tc) {
|
if (update.meta?.tc) {
|
||||||
rangesTracker.setIdSeed(update.meta.tc)
|
rangesTracker.setIdSeed(update.meta.tc)
|
||||||
}
|
}
|
||||||
const historyOps = []
|
const historyOps = []
|
||||||
for (const op of update.op) {
|
for (const op of update.op) {
|
||||||
|
let croppedCommentOps = []
|
||||||
if (opts.historyRangesSupport) {
|
if (opts.historyRangesSupport) {
|
||||||
historyOps.push(
|
historyOps.push(
|
||||||
getHistoryOp(op, rangesTracker.comments, rangesTracker.changes)
|
getHistoryOp(op, rangesTracker.comments, rangesTracker.changes)
|
||||||
)
|
)
|
||||||
|
if (isDelete(op) && trackingChanges) {
|
||||||
|
// If a tracked delete overlaps a comment, the comment must be
|
||||||
|
// cropped. The extent of the cropping is calculated before the
|
||||||
|
// delete is applied, but the cropping operations are applied
|
||||||
|
// later, after the delete is applied.
|
||||||
|
croppedCommentOps = getCroppedCommentOps(op, rangesTracker.comments)
|
||||||
|
}
|
||||||
} else if (isInsert(op) || isDelete(op)) {
|
} else if (isInsert(op) || isDelete(op)) {
|
||||||
historyOps.push(op)
|
historyOps.push(op)
|
||||||
}
|
}
|
||||||
rangesTracker.applyOp(op, { user_id: update.meta?.user_id })
|
rangesTracker.applyOp(op, { user_id: update.meta?.user_id })
|
||||||
|
if (croppedCommentOps.length > 0) {
|
||||||
|
historyOps.push(
|
||||||
|
...croppedCommentOps.map(op =>
|
||||||
|
getHistoryOpForComment(op, rangesTracker.changes)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (historyOps.length > 0) {
|
if (historyOps.length > 0) {
|
||||||
historyUpdates.push({ ...update, op: historyOps })
|
historyUpdates.push({ ...update, op: historyOps })
|
||||||
|
@ -486,4 +502,70 @@ function getHistoryOpForComment(op, changes) {
|
||||||
return historyOp
|
return historyOp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the ops necessary to properly crop comments when a tracked delete is
|
||||||
|
* received
|
||||||
|
*
|
||||||
|
* The editor treats a tracked delete as a proper delete and updates the
|
||||||
|
* comment range accordingly. The history doesn't do that and remembers the
|
||||||
|
* extent of the comment in the tracked delete. In order to keep the history
|
||||||
|
* consistent with the editor, we'll send ops that will crop the comment in
|
||||||
|
* the history.
|
||||||
|
*
|
||||||
|
* @param {DeleteOp} op
|
||||||
|
* @param {Comment[]} comments
|
||||||
|
* @returns {CommentOp[]}
|
||||||
|
*/
|
||||||
|
function getCroppedCommentOps(op, comments) {
|
||||||
|
const deleteStart = op.p
|
||||||
|
const deleteLength = op.d.length
|
||||||
|
const deleteEnd = deleteStart + deleteLength
|
||||||
|
|
||||||
|
/** @type {HistoryCommentOp[]} */
|
||||||
|
const historyCommentOps = []
|
||||||
|
for (const comment of comments) {
|
||||||
|
const commentStart = comment.op.p
|
||||||
|
const commentLength = comment.op.c.length
|
||||||
|
const commentEnd = commentStart + commentLength
|
||||||
|
|
||||||
|
if (deleteStart <= commentStart && deleteEnd > commentStart) {
|
||||||
|
// The comment overlaps the start of the comment or all of it.
|
||||||
|
const overlapLength = Math.min(deleteEnd, commentEnd) - commentStart
|
||||||
|
|
||||||
|
/** @type {CommentOp} */
|
||||||
|
const commentOp = {
|
||||||
|
p: deleteStart,
|
||||||
|
c: comment.op.c.slice(overlapLength),
|
||||||
|
t: comment.op.t,
|
||||||
|
}
|
||||||
|
if (comment.op.resolved) {
|
||||||
|
commentOp.resolved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
historyCommentOps.push(commentOp)
|
||||||
|
} else if (
|
||||||
|
deleteStart > commentStart &&
|
||||||
|
deleteStart < commentEnd &&
|
||||||
|
deleteEnd >= commentEnd
|
||||||
|
) {
|
||||||
|
// The comment overlaps the end of the comment.
|
||||||
|
const overlapLength = commentEnd - deleteStart
|
||||||
|
|
||||||
|
/** @type {CommentOp} */
|
||||||
|
const commentOp = {
|
||||||
|
p: commentStart,
|
||||||
|
c: comment.op.c.slice(0, -overlapLength),
|
||||||
|
t: comment.op.t,
|
||||||
|
}
|
||||||
|
if (comment.op.resolved) {
|
||||||
|
commentOp.resolved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
historyCommentOps.push(commentOp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyCommentOps
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = RangesManager
|
module.exports = RangesManager
|
||||||
|
|
|
@ -518,6 +518,123 @@ describe('RangesManager', function () {
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('tracked delete that overlaps the start of a comment', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
// original text is "one three four five"
|
||||||
|
this.ranges = {
|
||||||
|
comments: makeRanges([{ c: 'three', p: 4, t: 'comment-id-1' }]),
|
||||||
|
}
|
||||||
|
this.updates = makeUpdates([{ d: 'ne thr', p: 1 }], {
|
||||||
|
tc: 'tracking-id',
|
||||||
|
})
|
||||||
|
this.newDocLines = ['oee four five']
|
||||||
|
this.result = this.RangesManager.applyUpdate(
|
||||||
|
this.project_id,
|
||||||
|
this.doc_id,
|
||||||
|
this.ranges,
|
||||||
|
this.updates,
|
||||||
|
this.newDocLines,
|
||||||
|
{ historyRangesSupport: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should crop the beginning of the comment', function () {
|
||||||
|
expect(this.result.historyUpdates.map(x => x.op)).to.deep.equal([
|
||||||
|
[
|
||||||
|
{ d: 'ne thr', p: 1 },
|
||||||
|
{ c: 'ee', p: 1, hpos: 7, t: 'comment-id-1' },
|
||||||
|
],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('tracked delete that overlaps a whole comment', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
// original text is "one three four five"
|
||||||
|
this.ranges = {
|
||||||
|
comments: makeRanges([{ c: 'three', p: 4, t: 'comment-id-1' }]),
|
||||||
|
}
|
||||||
|
this.updates = makeUpdates([{ d: 'ne three f', p: 1 }], {
|
||||||
|
tc: 'tracking-id',
|
||||||
|
})
|
||||||
|
this.newDocLines = ['oour five']
|
||||||
|
this.result = this.RangesManager.applyUpdate(
|
||||||
|
this.project_id,
|
||||||
|
this.doc_id,
|
||||||
|
this.ranges,
|
||||||
|
this.updates,
|
||||||
|
this.newDocLines,
|
||||||
|
{ historyRangesSupport: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should crop the beginning of the comment', function () {
|
||||||
|
expect(this.result.historyUpdates.map(x => x.op)).to.deep.equal([
|
||||||
|
[
|
||||||
|
{ d: 'ne three f', p: 1 },
|
||||||
|
{ c: '', p: 1, hpos: 11, t: 'comment-id-1' },
|
||||||
|
],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('tracked delete that overlaps the end of a comment', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
// original text is "one three four five"
|
||||||
|
this.ranges = {
|
||||||
|
comments: makeRanges([{ c: 'three', p: 4, t: 'comment-id-1' }]),
|
||||||
|
}
|
||||||
|
this.updates = makeUpdates([{ d: 'ee f', p: 7 }], {
|
||||||
|
tc: 'tracking-id',
|
||||||
|
})
|
||||||
|
this.newDocLines = ['one throur five']
|
||||||
|
this.result = this.RangesManager.applyUpdate(
|
||||||
|
this.project_id,
|
||||||
|
this.doc_id,
|
||||||
|
this.ranges,
|
||||||
|
this.updates,
|
||||||
|
this.newDocLines,
|
||||||
|
{ historyRangesSupport: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should crop the end of the comment', function () {
|
||||||
|
expect(this.result.historyUpdates.map(x => x.op)).to.deep.equal([
|
||||||
|
[
|
||||||
|
{ d: 'ee f', p: 7 },
|
||||||
|
{ c: 'thr', p: 4, t: 'comment-id-1' },
|
||||||
|
],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('tracked delete that overlaps the inside of a comment', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
// original text is "one three four five"
|
||||||
|
this.ranges = {
|
||||||
|
comments: makeRanges([{ c: 'three', p: 4, t: 'comment-id-1' }]),
|
||||||
|
}
|
||||||
|
this.updates = makeUpdates([{ d: 'hre', p: 5 }], {
|
||||||
|
tc: 'tracking-id',
|
||||||
|
})
|
||||||
|
this.newDocLines = ['one te four five']
|
||||||
|
this.result = this.RangesManager.applyUpdate(
|
||||||
|
this.project_id,
|
||||||
|
this.doc_id,
|
||||||
|
this.ranges,
|
||||||
|
this.updates,
|
||||||
|
this.newDocLines,
|
||||||
|
{ historyRangesSupport: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not crop the comment', function () {
|
||||||
|
expect(this.result.historyUpdates.map(x => x.op)).to.deep.equal([
|
||||||
|
[{ d: 'hre', p: 5 }],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -280,15 +280,11 @@ class OperationsBuilder {
|
||||||
this.pushTextOperation()
|
this.pushTextOperation()
|
||||||
|
|
||||||
// Add a comment operation
|
// Add a comment operation
|
||||||
|
const commentLength = op.hlen ?? op.c.length
|
||||||
const commentOp = {
|
const commentOp = {
|
||||||
pathname: this.pathname,
|
pathname: this.pathname,
|
||||||
commentId: op.t,
|
commentId: op.t,
|
||||||
ranges: [
|
ranges: commentLength > 0 ? [{ pos, length: commentLength }] : [],
|
||||||
{
|
|
||||||
pos,
|
|
||||||
length: op.hlen ?? op.c.length,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
if ('resolved' in op) {
|
if ('resolved' in op) {
|
||||||
commentOp.resolved = op.resolved
|
commentOp.resolved = op.resolved
|
||||||
|
|
|
@ -776,6 +776,7 @@ describe('UpdateTranslator', function () {
|
||||||
{ p: 3, d: 'bar' },
|
{ p: 3, d: 'bar' },
|
||||||
{ p: 5, c: 'comment this', t: 'comment-id-1' },
|
{ p: 5, c: 'comment this', t: 'comment-id-1' },
|
||||||
{ p: 7, c: 'another comment', t: 'comment-id-2' },
|
{ p: 7, c: 'another comment', t: 'comment-id-2' },
|
||||||
|
{ p: 9, c: '', t: 'comment-id-3' },
|
||||||
{ p: 10, i: 'baz' },
|
{ p: 10, i: 'baz' },
|
||||||
],
|
],
|
||||||
v: this.version,
|
v: this.version,
|
||||||
|
@ -812,6 +813,11 @@ describe('UpdateTranslator', function () {
|
||||||
commentId: 'comment-id-2',
|
commentId: 'comment-id-2',
|
||||||
ranges: [{ pos: 7, length: 15 }],
|
ranges: [{ pos: 7, length: 15 }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pathname: 'main.tex',
|
||||||
|
commentId: 'comment-id-3',
|
||||||
|
ranges: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pathname: 'main.tex',
|
pathname: 'main.tex',
|
||||||
textOperation: [10, 'baz', 10],
|
textOperation: [10, 'baz', 10],
|
||||||
|
|
Loading…
Reference in a new issue