mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 06:33:55 +01:00
Merge pull request #35982 from overleaf/mg-history-ot-comments
HistoryOT - Fix comments being dropped and crashing on detached ranges GitOrigin-RevId: 0906a55a51a6fda9ac828efcbe72989573161261
This commit is contained in:
@@ -145,17 +145,19 @@ const buildRangesDecorations = ({
|
||||
for (const comment of comments) {
|
||||
if (!comment.resolved) {
|
||||
for (const range of comment.ranges) {
|
||||
decorations.push(
|
||||
Decoration.mark({
|
||||
class: 'ol-cm-change ol-cm-change-c',
|
||||
id: comment.id,
|
||||
rangeType: 'comment',
|
||||
comment,
|
||||
}).range(
|
||||
trackedDeletes.toCodeMirror(range.pos),
|
||||
trackedDeletes.toCodeMirror(range.end)
|
||||
const from = trackedDeletes.toCodeMirror(range.pos)
|
||||
const to = trackedDeletes.toCodeMirror(range.end)
|
||||
// a range hidden inside a tracked delete has no visible text to mark
|
||||
if (from < to) {
|
||||
decorations.push(
|
||||
Decoration.mark({
|
||||
class: 'ol-cm-change ol-cm-change-c',
|
||||
id: comment.id,
|
||||
rangeType: 'comment',
|
||||
comment,
|
||||
}).range(from, to)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Prec,
|
||||
Transaction,
|
||||
Annotation,
|
||||
ChangeSet,
|
||||
ChangeSpec,
|
||||
Text,
|
||||
StateEffect,
|
||||
@@ -22,9 +23,17 @@ import {
|
||||
InsertOp,
|
||||
RemoveOp,
|
||||
RetainOp,
|
||||
AddCommentOperation,
|
||||
DeleteCommentOperation,
|
||||
SetCommentStateOperation,
|
||||
StringFileData,
|
||||
TrackedChangeList,
|
||||
} from 'overleaf-editor-core'
|
||||
import { rangesUpdatedEffect, setTrackChangesUserId } from './history-ot'
|
||||
import { trackedDeletesFromState } from '@/features/source-editor/utils/tracked-deletes'
|
||||
import {
|
||||
TrackedDeletes,
|
||||
trackedDeletesFromState,
|
||||
} from '@/features/source-editor/utils/tracked-deletes'
|
||||
|
||||
/*
|
||||
* Integrate CodeMirror 6 with the real-time system, via ShareJS.
|
||||
@@ -355,65 +364,113 @@ class HistoryOTAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
onRemoteOp(operations: EditOperation[]) {
|
||||
const trackedDeletes = trackedDeletesFromState(this.editor.view.state)
|
||||
const changes: ChangeSpec[] = []
|
||||
let trackedChangesUpdated = false
|
||||
for (const operation of operations) {
|
||||
// One dispatch per batch: a per-op dispatch would rebuild rangesState from
|
||||
// shareDoc.snapshot (already final) against an intermediate doc.
|
||||
onRemoteOp(operations: EditOperation[], oldSnapshot?: StringFileData) {
|
||||
const view = this.editor.view
|
||||
let composed = ChangeSet.empty(view.state.doc.length)
|
||||
let trackedDeletes = trackedDeletesFromState(view.state)
|
||||
let trackedChanges: TrackedChangeList | null = null
|
||||
let snapshotLength = 0
|
||||
let trackedChangesOrCommentsUpdated = false
|
||||
|
||||
for (const [index, operation] of operations.entries()) {
|
||||
if (operation instanceof TextOperation) {
|
||||
const specs: ChangeSpec[] = []
|
||||
let cursor = 0
|
||||
for (const op of operation.ops) {
|
||||
if (op instanceof InsertOp) {
|
||||
if (op.tracking?.type !== 'delete') {
|
||||
changes.push({
|
||||
specs.push({
|
||||
from: trackedDeletes.toCodeMirror(cursor),
|
||||
insert: op.insertion,
|
||||
})
|
||||
}
|
||||
trackedChangesUpdated = true
|
||||
trackedChangesOrCommentsUpdated = true
|
||||
} else if (op instanceof RemoveOp) {
|
||||
changes.push({
|
||||
specs.push({
|
||||
from: trackedDeletes.toCodeMirror(cursor),
|
||||
to: trackedDeletes.toCodeMirror(cursor + op.length),
|
||||
})
|
||||
cursor += op.length
|
||||
trackedChangesUpdated = true
|
||||
trackedChangesOrCommentsUpdated = true
|
||||
} else if (op instanceof RetainOp) {
|
||||
if (op.tracking != null) {
|
||||
if (op.tracking.type === 'delete') {
|
||||
changes.push({
|
||||
specs.push({
|
||||
from: trackedDeletes.toCodeMirror(cursor),
|
||||
to: trackedDeletes.toCodeMirror(cursor + op.length),
|
||||
})
|
||||
}
|
||||
trackedChangesUpdated = true
|
||||
trackedChangesOrCommentsUpdated = true
|
||||
}
|
||||
cursor += op.length
|
||||
}
|
||||
}
|
||||
composed = composed.compose(ChangeSet.of(specs, composed.newLength))
|
||||
} else if (
|
||||
operation instanceof AddCommentOperation ||
|
||||
operation instanceof DeleteCommentOperation ||
|
||||
operation instanceof SetCommentStateOperation
|
||||
) {
|
||||
// no text change, but comment state changed — rebuild decorations
|
||||
trackedChangesOrCommentsUpdated = true
|
||||
}
|
||||
|
||||
const view = this.editor.view
|
||||
const effects: StateEffect<any>[] = []
|
||||
const scrollEffect = view
|
||||
.scrollSnapshot()
|
||||
.map(view.state.changes(changes))
|
||||
if (scrollEffect != null) {
|
||||
effects.push(scrollEffect)
|
||||
// shareDoc.snapshot is already the post-batch state, so advance our own
|
||||
// copy of oldSnapshot's tracked changes per op, keeping trackedDeletes
|
||||
// correct for the next op. Only the tracked-change list feeds
|
||||
// trackedDeletes, so there's no need to clone/edit the whole snapshot.
|
||||
if (index < operations.length - 1 && oldSnapshot) {
|
||||
try {
|
||||
if (trackedChanges === null) {
|
||||
// clone once so we never mutate oldSnapshot (owned by the sharejs event)
|
||||
trackedChanges = TrackedChangeList.fromRaw(
|
||||
oldSnapshot.getTrackedChanges().toRaw()
|
||||
)
|
||||
snapshotLength = oldSnapshot.getStringLength()
|
||||
}
|
||||
if (operation instanceof TextOperation) {
|
||||
if (operation.baseLength !== snapshotLength) {
|
||||
// op doesn't fit our snapshot — we're out of sync
|
||||
throw new Error('remote operation does not fit the snapshot')
|
||||
}
|
||||
// comment ops don't move tracked ranges
|
||||
trackedChanges.applyTextOperation(operation)
|
||||
snapshotLength = operation.targetLength
|
||||
}
|
||||
trackedDeletes = new TrackedDeletes(trackedChanges)
|
||||
} catch (error) {
|
||||
// out of sync — don't map remaining ops against the wrong state
|
||||
this.shareDoc.emit('error', error)
|
||||
debugConsole.error(
|
||||
'Could not advance snapshot for remote operations',
|
||||
error
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (trackedChangesUpdated) {
|
||||
effects.push(rangesUpdatedEffect.of(null))
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
changes,
|
||||
effects,
|
||||
annotations: [
|
||||
Transaction.remote.of(true),
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const effects: StateEffect<any>[] = []
|
||||
const scrollEffect = view.scrollSnapshot().map(composed)
|
||||
if (scrollEffect != null) {
|
||||
effects.push(scrollEffect)
|
||||
}
|
||||
if (trackedChangesOrCommentsUpdated) {
|
||||
effects.push(rangesUpdatedEffect.of(null))
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
changes: composed,
|
||||
effects,
|
||||
annotations: [
|
||||
Transaction.remote.of(true),
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
|
||||
this.checkContent()
|
||||
}
|
||||
|
||||
onCodeMirrorChange(
|
||||
|
||||
Reference in New Issue
Block a user