| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- import { Range, RangeSet, RangeValue, Transaction } from '@codemirror/state'
- import {
- AnyOperation,
- Change,
- CommentOperation,
- } from '../../../../../../types/change'
- import { ThreadId } from '../../../../../../types/review-panel/review-panel'
- import { DocumentContainer } from '@/features/ide-react/editor/document-container'
- export type StoredComment = {
- text: string
- comments: {
- offset: number
- text: string
- comment: Change<CommentOperation>
- }[]
- }
- /**
- * Find tracked comments within the range of the current transaction's changes
- */
- export const findCommentsInCut = (
- currentDoc: DocumentContainer,
- transaction: Transaction
- ) => {
- const items: StoredComment[] = []
- transaction.changes.iterChanges((fromA, toA) => {
- const comments = currentDoc
- .ranges!.comments.filter(
- comment =>
- fromA <= comment.op.p && comment.op.p + comment.op.c.length <= toA
- )
- .map(comment => ({
- offset: comment.op.p - fromA,
- text: comment.op.c,
- comment,
- }))
- if (comments.length) {
- items.push({
- text: transaction.startState.sliceDoc(fromA, toA),
- comments,
- })
- }
- })
- return items
- }
- /**
- * Find stored comments matching the text of the current transaction's changes
- */
- export const findCommentsInPaste = (
- storedComments: StoredComment[],
- transaction: Transaction
- ) => {
- const ops: CommentOperation[] = []
- transaction.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
- const insertedText = inserted.toString()
- // note: only using the first match
- const matchedComment = storedComments.find(
- item => item.text === insertedText
- )
- if (matchedComment) {
- for (const { offset, text, comment } of matchedComment.comments) {
- // Resubmitting an existing comment op (by thread id) will move it
- ops.push({
- c: text,
- p: fromB + offset,
- t: comment.id as ThreadId,
- })
- }
- }
- })
- return ops
- }
- class CommentRangeValue extends RangeValue {
- constructor(
- public content: string,
- public comment: Change<CommentOperation>
- ) {
- super()
- }
- }
- /**
- * Find tracked comments with no content with the ranges of a transaction's changes
- */
- export const findDetachedCommentsInChanges = (
- currentDoc: DocumentContainer,
- transaction: Transaction
- ) => {
- const items: Range<CommentRangeValue>[] = []
- transaction.changes.iterChanges((fromA, toA) => {
- for (const comment of currentDoc.ranges!.comments) {
- const content = comment.op.c
- // TODO: handle comments that were never attached
- if (!content.length) {
- continue
- }
- const from = comment.op.p
- const to = from + content.length
- if (fromA <= from && to <= toA) {
- items.push(new CommentRangeValue(content, comment).range(from, to))
- }
- }
- })
- return RangeSet.of(items, true)
- }
- /**
- * Submit operations to the ShareJS doc
- * (used when restoring comments on paste)
- */
- const submitOps = (
- currentDoc: DocumentContainer,
- ops: AnyOperation[],
- transaction: Transaction
- ) => {
- for (const op of ops) {
- currentDoc.submitOp(op)
- }
- // Check that comments still match text. Will throw error if not.
- currentDoc.ranges!.validate(transaction.state.doc.toString())
- }
- /**
- * Wait for the ShareJS doc to fire an event, then submit the operations.
- */
- const submitOpsAfterEvent = (
- currentDoc: DocumentContainer,
- eventName: string,
- ops: AnyOperation[],
- transaction: Transaction
- ) => {
- // We have to wait until the change has been processed by the range
- // tracker, since if we move the ops into place beforehand, they will be
- // moved again when the changes are processed by the range tracker. This
- // ranges:dirty event is fired after the doc has applied the changes to
- // the range tracker.
- // TODO: could put this in an update listener instead, if the ShareJS doc has been updated by then?
- currentDoc.on(eventName, () => {
- currentDoc.off(eventName)
- window.setTimeout(() => submitOps(currentDoc, ops, transaction))
- })
- }
- /**
- * Look through the comments stored on cut, and restore those in text that matches the pasted text.
- */
- export const restoreCommentsOnPaste = (
- currentDoc: DocumentContainer,
- transaction: Transaction,
- storedComments: StoredComment[]
- ) => {
- if (storedComments.length) {
- const ops = findCommentsInPaste(storedComments, transaction)
- if (ops.length) {
- submitOpsAfterEvent(
- currentDoc,
- 'ranges:dirty.paste-cm6',
- ops,
- transaction
- )
- }
- }
- }
- /**
- * When undoing a change, find comments from the original content and restore them.
- */
- export const restoreDetachedComments = (
- currentDoc: DocumentContainer,
- transaction: Transaction,
- storedComments: RangeSet<any>
- ) => {
- const ops: CommentOperation[] = []
- const cursor = storedComments.iter()
- while (cursor.value) {
- const { id } = cursor.value.comment
- const comment = currentDoc.ranges!.comments.find(item => item.id === id)
- // check that the comment still exists and is detached
- if (comment && comment.op.c === '') {
- const content = transaction.state.doc.sliceString(
- cursor.from,
- cursor.from + cursor.value.content.length
- )
- if (cursor.value.content === content) {
- ops.push({
- c: cursor.value.content,
- p: cursor.from,
- t: id,
- })
- }
- }
- cursor.next()
- }
- // FIXME: timing issue with rapid undos
- if (ops.length) {
- window.setTimeout(() => {
- submitOps(currentDoc, ops, transaction)
- }, 0)
- }
- // submitOpsAfterEvent('ranges:dirty.undo-cm6', ops, transaction)
- }
|