2024-02-01 06:31:11 -05:00
|
|
|
// @ts-check
|
2024-02-16 08:38:07 -05:00
|
|
|
const { RetainOp, InsertOp, RemoveOp } = require('./operation/scan_op')
|
2024-01-23 08:52:10 -05:00
|
|
|
const Range = require('./range')
|
|
|
|
|
|
|
|
/**
|
2024-02-16 08:38:07 -05:00
|
|
|
* @typedef {import("./types").CommentRawData} CommentRawData
|
|
|
|
* @typedef {import("./operation/text_operation")} TextOperation
|
2024-01-23 08:52:10 -05:00
|
|
|
*/
|
|
|
|
|
|
|
|
class Comment {
|
2024-02-01 06:31:11 -05:00
|
|
|
/**
|
2024-02-20 06:28:51 -05:00
|
|
|
* @readonly
|
|
|
|
* @type {ReadonlyArray<Range>}
|
2024-02-01 06:31:11 -05:00
|
|
|
*/
|
|
|
|
ranges = []
|
|
|
|
|
|
|
|
/**
|
2024-02-20 06:28:51 -05:00
|
|
|
* @readonly
|
2024-02-01 06:31:11 -05:00
|
|
|
* @type {boolean}
|
|
|
|
*/
|
|
|
|
resolved = false
|
|
|
|
|
2024-01-23 08:52:10 -05:00
|
|
|
/**
|
2024-02-20 06:28:51 -05:00
|
|
|
* @param {ReadonlyArray<Range>} ranges
|
2024-01-23 08:52:10 -05:00
|
|
|
* @param {boolean} [resolved]
|
|
|
|
*/
|
|
|
|
constructor(ranges, resolved = false) {
|
|
|
|
this.resolved = resolved
|
2024-02-20 06:28:51 -05:00
|
|
|
this.ranges = this.mergeRanges(ranges)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {number} cursor
|
|
|
|
* @param {number} length
|
|
|
|
* @param {boolean} [extendComment]
|
2024-02-20 06:28:51 -05:00
|
|
|
* @returns {Comment}
|
2024-02-01 06:31:11 -05:00
|
|
|
*/
|
|
|
|
applyInsert(cursor, length, extendComment = false) {
|
|
|
|
let existingRangeExtended = false
|
2024-02-13 10:08:54 -05:00
|
|
|
const newRanges = []
|
2024-02-01 06:31:11 -05:00
|
|
|
|
|
|
|
for (const commentRange of this.ranges) {
|
2024-02-05 08:19:45 -05:00
|
|
|
if (cursor === commentRange.end) {
|
2024-02-01 06:31:11 -05:00
|
|
|
// insert right after the comment
|
|
|
|
if (extendComment) {
|
2024-02-13 10:08:54 -05:00
|
|
|
newRanges.push(commentRange.extendBy(length))
|
2024-02-01 06:31:11 -05:00
|
|
|
existingRangeExtended = true
|
2024-02-13 10:08:54 -05:00
|
|
|
} else {
|
|
|
|
newRanges.push(commentRange)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
2024-02-05 08:19:45 -05:00
|
|
|
} else if (cursor === commentRange.start) {
|
2024-02-01 06:31:11 -05:00
|
|
|
// insert at the start of the comment
|
|
|
|
if (extendComment) {
|
2024-02-13 10:08:54 -05:00
|
|
|
newRanges.push(commentRange.extendBy(length))
|
2024-02-01 06:31:11 -05:00
|
|
|
existingRangeExtended = true
|
|
|
|
} else {
|
2024-02-13 10:08:54 -05:00
|
|
|
newRanges.push(commentRange.moveBy(length))
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
2024-02-05 08:19:45 -05:00
|
|
|
} else if (commentRange.startIsAfter(cursor)) {
|
2024-02-01 06:31:11 -05:00
|
|
|
// insert before the comment
|
2024-02-13 10:08:54 -05:00
|
|
|
newRanges.push(commentRange.moveBy(length))
|
2024-02-05 08:19:45 -05:00
|
|
|
} else if (commentRange.containsCursor(cursor)) {
|
2024-02-01 06:31:11 -05:00
|
|
|
// insert is inside the comment
|
|
|
|
if (extendComment) {
|
2024-02-13 10:08:54 -05:00
|
|
|
newRanges.push(commentRange.extendBy(length))
|
2024-02-01 06:31:11 -05:00
|
|
|
existingRangeExtended = true
|
|
|
|
} else {
|
|
|
|
const [rangeUpToCursor, , rangeAfterCursor] = commentRange.insertAt(
|
|
|
|
cursor,
|
|
|
|
length
|
|
|
|
)
|
|
|
|
|
|
|
|
// use current commentRange for the part before the cursor
|
2024-02-13 10:08:54 -05:00
|
|
|
newRanges.push(new Range(commentRange.pos, rangeUpToCursor.length))
|
2024-02-01 06:31:11 -05:00
|
|
|
// add the part after the cursor as a new range
|
2024-02-13 10:08:54 -05:00
|
|
|
newRanges.push(rangeAfterCursor)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
2024-02-13 10:08:54 -05:00
|
|
|
} else {
|
|
|
|
// insert is after the comment
|
|
|
|
newRanges.push(commentRange)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the insert is not inside any range, add a new range
|
|
|
|
if (extendComment && !existingRangeExtended) {
|
2024-02-20 06:28:51 -05:00
|
|
|
newRanges.push(new Range(cursor, length))
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
2024-02-20 06:28:51 -05:00
|
|
|
|
|
|
|
return new Comment(newRanges, this.resolved)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {Range} deletedRange
|
2024-02-20 06:28:51 -05:00
|
|
|
* @returns {Comment}
|
2024-02-01 06:31:11 -05:00
|
|
|
*/
|
|
|
|
applyDelete(deletedRange) {
|
2024-02-13 10:08:54 -05:00
|
|
|
const newRanges = []
|
|
|
|
|
2024-02-01 06:31:11 -05:00
|
|
|
for (const commentRange of this.ranges) {
|
|
|
|
if (commentRange.overlaps(deletedRange)) {
|
2024-02-13 10:08:54 -05:00
|
|
|
newRanges.push(commentRange.subtract(deletedRange))
|
2024-02-01 06:31:11 -05:00
|
|
|
} else if (commentRange.startsAfter(deletedRange)) {
|
2024-02-13 10:08:54 -05:00
|
|
|
newRanges.push(commentRange.moveBy(-deletedRange.length))
|
|
|
|
} else {
|
|
|
|
newRanges.push(commentRange)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-20 06:28:51 -05:00
|
|
|
return new Comment(newRanges, this.resolved)
|
2024-02-01 06:31:11 -05:00
|
|
|
}
|
|
|
|
|
2024-02-16 08:38:07 -05:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {TextOperation} operation
|
2024-02-26 08:51:03 -05:00
|
|
|
* @param {string} commentId
|
2024-02-20 06:28:51 -05:00
|
|
|
* @returns {Comment}
|
2024-02-16 08:38:07 -05:00
|
|
|
*/
|
2024-02-26 08:51:03 -05:00
|
|
|
applyTextOperation(operation, commentId) {
|
2024-02-20 06:28:51 -05:00
|
|
|
/** @type {Comment} */
|
|
|
|
let comment = this
|
2024-02-16 08:38:07 -05:00
|
|
|
let cursor = 0
|
|
|
|
for (const op of operation.ops) {
|
|
|
|
if (op instanceof RetainOp) {
|
|
|
|
cursor += op.length
|
|
|
|
} else if (op instanceof InsertOp) {
|
2024-02-26 08:51:03 -05:00
|
|
|
comment = comment.applyInsert(
|
|
|
|
cursor,
|
|
|
|
op.insertion.length,
|
|
|
|
op.commentIds?.includes(commentId)
|
|
|
|
)
|
2024-02-16 08:38:07 -05:00
|
|
|
cursor += op.insertion.length
|
|
|
|
} else if (op instanceof RemoveOp) {
|
2024-02-20 06:28:51 -05:00
|
|
|
comment = comment.applyDelete(new Range(cursor, op.length))
|
2024-02-16 08:38:07 -05:00
|
|
|
}
|
|
|
|
}
|
2024-02-20 06:28:51 -05:00
|
|
|
return comment
|
2024-02-16 08:38:07 -05:00
|
|
|
}
|
|
|
|
|
2024-02-01 06:31:11 -05:00
|
|
|
isEmpty() {
|
|
|
|
return this.ranges.length === 0
|
2024-01-23 08:52:10 -05:00
|
|
|
}
|
|
|
|
|
2024-02-16 08:38:07 -05:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @returns {CommentRawData}
|
|
|
|
*/
|
2024-01-23 08:52:10 -05:00
|
|
|
toRaw() {
|
|
|
|
return {
|
|
|
|
resolved: this.resolved,
|
|
|
|
ranges: this.ranges.map(range => range.toRaw()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-20 06:28:51 -05:00
|
|
|
/**
|
|
|
|
* @param {ReadonlyArray<Range>} ranges
|
|
|
|
* @returns {ReadonlyArray<Range>}
|
|
|
|
*/
|
|
|
|
mergeRanges(ranges) {
|
2024-02-01 06:31:11 -05:00
|
|
|
/** @type {Range[]} */
|
|
|
|
const mergedRanges = []
|
|
|
|
|
2024-02-20 06:28:51 -05:00
|
|
|
const sortedRanges = [...ranges].sort((a, b) => a.start - b.start)
|
|
|
|
for (const range of sortedRanges) {
|
2024-02-01 06:31:11 -05:00
|
|
|
if (range.isEmpty()) {
|
|
|
|
continue
|
|
|
|
}
|
2024-02-13 10:08:54 -05:00
|
|
|
const lastMerged = mergedRanges[mergedRanges.length - 1]
|
2024-02-01 06:31:11 -05:00
|
|
|
|
2024-02-13 10:08:54 -05:00
|
|
|
if (lastMerged?.canMerge(range)) {
|
|
|
|
mergedRanges[mergedRanges.length - 1] = lastMerged.merge(range)
|
2024-02-01 06:31:11 -05:00
|
|
|
} else {
|
|
|
|
mergedRanges.push(range)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-20 06:28:51 -05:00
|
|
|
return mergedRanges
|
2024-02-16 08:38:07 -05:00
|
|
|
}
|
|
|
|
|
2024-01-23 08:52:10 -05:00
|
|
|
/**
|
|
|
|
* @param {CommentRawData} rawComment
|
|
|
|
* @returns {Comment}
|
|
|
|
*/
|
|
|
|
static fromRaw(rawComment) {
|
|
|
|
return new Comment(
|
|
|
|
rawComment.ranges.map(range => Range.fromRaw(range)),
|
|
|
|
rawComment.resolved
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Comment
|