|
|
@@ -0,0 +1,183 @@
|
|
|
+// @ts-check
|
|
|
+
|
|
|
+const _ = require('lodash')
|
|
|
+const { isDelete } = require('./Utils')
|
|
|
+
|
|
|
+/**
|
|
|
+ * @typedef {import('./types').Comment} Comment
|
|
|
+ * @typedef {import('./types').HistoryComment} HistoryComment
|
|
|
+ * @typedef {import('./types').HistoryRanges} HistoryRanges
|
|
|
+ * @typedef {import('./types').HistoryTrackedChange} HistoryTrackedChange
|
|
|
+ * @typedef {import('./types').Ranges} Ranges
|
|
|
+ * @typedef {import('./types').TrackedChange} TrackedChange
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * Convert editor ranges to history ranges
|
|
|
+ *
|
|
|
+ * @param {Ranges} ranges
|
|
|
+ * @return {HistoryRanges}
|
|
|
+ */
|
|
|
+function toHistoryRanges(ranges) {
|
|
|
+ const changes = ranges.changes ?? []
|
|
|
+ const comments = (ranges.comments ?? []).slice()
|
|
|
+
|
|
|
+ // Changes are assumed to be sorted, but not comments
|
|
|
+ comments.sort((a, b) => a.op.p - b.op.p)
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This will allow us to go through comments at a different pace as we loop
|
|
|
+ * through tracked changes
|
|
|
+ */
|
|
|
+ const commentsIterator = new CommentsIterator(comments)
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Current offset between editor pos and history pos
|
|
|
+ */
|
|
|
+ let offset = 0
|
|
|
+
|
|
|
+ /**
|
|
|
+ * History comments that might overlap with the tracked change considered
|
|
|
+ *
|
|
|
+ * @type {HistoryComment[]}
|
|
|
+ */
|
|
|
+ let pendingComments = []
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The final history comments generated
|
|
|
+ *
|
|
|
+ * @type {HistoryComment[]}
|
|
|
+ */
|
|
|
+ const historyComments = []
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The final history tracked changes generated
|
|
|
+ *
|
|
|
+ * @type {HistoryTrackedChange[]}
|
|
|
+ */
|
|
|
+ const historyChanges = []
|
|
|
+
|
|
|
+ for (const change of changes) {
|
|
|
+ historyChanges.push(toHistoryChange(change, offset))
|
|
|
+
|
|
|
+ // After this point, we're only interested in tracked deletes
|
|
|
+ if (!isDelete(change.op)) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fill pendingComments with new comments that start before this tracked
|
|
|
+ // delete and might overlap
|
|
|
+ for (const comment of commentsIterator.nextComments(change.op.p)) {
|
|
|
+ pendingComments.push(toHistoryComment(comment, offset))
|
|
|
+ }
|
|
|
+
|
|
|
+ // Save comments that are fully before this tracked delete
|
|
|
+ const newPendingComments = []
|
|
|
+ for (const historyComment of pendingComments) {
|
|
|
+ const commentEnd = historyComment.op.p + historyComment.op.c.length
|
|
|
+ if (commentEnd <= change.op.p) {
|
|
|
+ historyComments.push(historyComment)
|
|
|
+ } else {
|
|
|
+ newPendingComments.push(historyComment)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ pendingComments = newPendingComments
|
|
|
+
|
|
|
+ // The rest of pending comments overlap with this tracked change. Adjust
|
|
|
+ // their history length.
|
|
|
+ for (const historyComment of pendingComments) {
|
|
|
+ historyComment.op.hlen =
|
|
|
+ (historyComment.op.hlen ?? historyComment.op.c.length) +
|
|
|
+ change.op.d.length
|
|
|
+ }
|
|
|
+
|
|
|
+ // Adjust the offset
|
|
|
+ offset += change.op.d.length
|
|
|
+ }
|
|
|
+ // Save the last pending comments
|
|
|
+ for (const historyComment of pendingComments) {
|
|
|
+ historyComments.push(historyComment)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Save any comments that came after the last tracked change
|
|
|
+ for (const comment of commentsIterator.nextComments()) {
|
|
|
+ historyComments.push(toHistoryComment(comment, offset))
|
|
|
+ }
|
|
|
+
|
|
|
+ const historyRanges = {}
|
|
|
+ if (historyComments.length > 0) {
|
|
|
+ historyRanges.comments = historyComments
|
|
|
+ }
|
|
|
+ if (historyChanges.length > 0) {
|
|
|
+ historyRanges.changes = historyChanges
|
|
|
+ }
|
|
|
+ return historyRanges
|
|
|
+}
|
|
|
+
|
|
|
+class CommentsIterator {
|
|
|
+ /**
|
|
|
+ * Build a CommentsIterator
|
|
|
+ *
|
|
|
+ * @param {Comment[]} comments
|
|
|
+ */
|
|
|
+ constructor(comments) {
|
|
|
+ this.comments = comments
|
|
|
+ this.currentIndex = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Generator that returns the next comments to consider
|
|
|
+ *
|
|
|
+ * @param {number} beforePos - only return comments that start before this position
|
|
|
+ * @return {Iterable<Comment>}
|
|
|
+ */
|
|
|
+ *nextComments(beforePos = Infinity) {
|
|
|
+ while (this.currentIndex < this.comments.length) {
|
|
|
+ const comment = this.comments[this.currentIndex]
|
|
|
+ if (comment.op.p < beforePos) {
|
|
|
+ yield comment
|
|
|
+ this.currentIndex += 1
|
|
|
+ } else {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Convert an editor tracked change into a history tracked change
|
|
|
+ *
|
|
|
+ * @param {TrackedChange} change
|
|
|
+ * @param {number} offset - how much the history change is ahead of the
|
|
|
+ * editor change
|
|
|
+ * @return {HistoryTrackedChange}
|
|
|
+ */
|
|
|
+function toHistoryChange(change, offset) {
|
|
|
+ /** @type {HistoryTrackedChange} */
|
|
|
+ const historyChange = _.cloneDeep(change)
|
|
|
+ if (offset > 0) {
|
|
|
+ historyChange.op.hpos = change.op.p + offset
|
|
|
+ }
|
|
|
+ return historyChange
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Convert an editor comment into a history comment
|
|
|
+ *
|
|
|
+ * @param {Comment} comment
|
|
|
+ * @param {number} offset - how much the history comment is ahead of the
|
|
|
+ * editor comment
|
|
|
+ * @return {HistoryComment}
|
|
|
+ */
|
|
|
+function toHistoryComment(comment, offset) {
|
|
|
+ /** @type {HistoryComment} */
|
|
|
+ const historyComment = _.cloneDeep(comment)
|
|
|
+ if (offset > 0) {
|
|
|
+ historyComment.op.hpos = comment.op.p + offset
|
|
|
+ }
|
|
|
+ return historyComment
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = {
|
|
|
+ toHistoryRanges,
|
|
|
+}
|