mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #1684 from sharelatex/revert-1637-as-clear-undo-stack-remote-edit
Revert "Clear undo stack when remote change is received" GitOrigin-RevId: ac313f008e54696f548e6396851fe22568248055
This commit is contained in:
parent
61cd448832
commit
b1a615bad9
2 changed files with 526 additions and 27 deletions
|
@ -1689,9 +1689,6 @@ define(['ace/ace'], function () {
|
|||
suppress = true;
|
||||
// All the primitives we need are already in CM's API.
|
||||
editor.replaceRange(text, editor.posFromIndex(pos));
|
||||
// Clear CM's undo/redo history on remote edit. This prevents issues where
|
||||
// a user can accidentally remove another user's edits
|
||||
editor.clearHistory();
|
||||
suppress = false;
|
||||
return check();
|
||||
};
|
||||
|
@ -1701,9 +1698,6 @@ define(['ace/ace'], function () {
|
|||
var from = editor.posFromIndex(pos);
|
||||
var to = editor.posFromIndex(pos + text.length);
|
||||
editor.replaceRange('', from, to);
|
||||
// Clear CM's undo/redo history on remote edit. This prevents issues where
|
||||
// a user can accidentally remove another user's edits
|
||||
editor.clearHistory()
|
||||
suppress = false;
|
||||
return check();
|
||||
};
|
||||
|
|
|
@ -1,32 +1,537 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-throw-literal,
|
||||
no-undef,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS205: Consider reworking code to avoid use of IIFEs
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
define(['ace/ace'], function() {
|
||||
const BuiltInUndoManager = ace.require('ace/undomanager').UndoManager
|
||||
let UndoManager
|
||||
const { Range } = ace.require('ace/range')
|
||||
const { EditSession } = ace.require('ace/edit_session')
|
||||
const Doc = ace.require('ace/document').Document
|
||||
|
||||
class UndoManager {
|
||||
return (UndoManager = class UndoManager {
|
||||
constructor($scope, editor) {
|
||||
editor.on('changeSession', e => {
|
||||
e.session.setUndoManager(new BuiltInUndoManager())
|
||||
this.$scope = $scope
|
||||
this.editor = editor
|
||||
this.$scope.undo = { show_remote_warning: false }
|
||||
|
||||
e.oldSession.off('change', onChange)
|
||||
e.session.on('change', onChange)
|
||||
this.reset()
|
||||
|
||||
this.editor.on('changeSession', e => {
|
||||
this.reset()
|
||||
this.session = e.session
|
||||
return e.session.setUndoManager(this)
|
||||
})
|
||||
}
|
||||
|
||||
showUndoConflictWarning() {
|
||||
this.$scope.$apply(() => {
|
||||
return (this.$scope.undo.show_remote_warning = true)
|
||||
})
|
||||
|
||||
function onChange(change) {
|
||||
if (!change.remote) return
|
||||
|
||||
// HACK: remote changes in Ace are added by the ShareJS/Ace adapter
|
||||
// asynchronously via a timeout (see attach_ace function). This makes it
|
||||
// impossible to clear to undo stack when remote changes are received.
|
||||
// To hack around this we queue the undo stack clear so that it applies
|
||||
// after the change is applied
|
||||
setTimeout(() => {
|
||||
editor
|
||||
.getSession()
|
||||
.getUndoManager()
|
||||
.reset()
|
||||
return setTimeout(() => {
|
||||
return this.$scope.$apply(() => {
|
||||
return (this.$scope.undo.show_remote_warning = false)
|
||||
})
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.firstUpdate = true
|
||||
this.undoStack = []
|
||||
return (this.redoStack = [])
|
||||
}
|
||||
|
||||
execute(options) {
|
||||
let newDeltaSet
|
||||
if (this.firstUpdate) {
|
||||
// The first update we receive is Ace setting the document, which we should
|
||||
// ignore
|
||||
this.firstUpdate = false
|
||||
return
|
||||
}
|
||||
const aceDeltaSets = options.args[0]
|
||||
if (aceDeltaSets == null) {
|
||||
return
|
||||
}
|
||||
this.session = options.args[1]
|
||||
|
||||
// We need to split the delta sets into local or remote groups before pushing onto
|
||||
// the undo stack, since these are treated differently.
|
||||
const splitDeltaSets = []
|
||||
let currentDeltaSet = null // Make global to this function
|
||||
;(newDeltaSet = function() {
|
||||
currentDeltaSet = { group: 'doc', deltas: [] }
|
||||
return splitDeltaSets.push(currentDeltaSet)
|
||||
})()
|
||||
const currentRemoteState = null
|
||||
|
||||
for (var deltaSet of Array.from(aceDeltaSets || [])) {
|
||||
if (deltaSet.group === 'doc') {
|
||||
// ignore code folding etc.
|
||||
for (let delta of Array.from(deltaSet.deltas)) {
|
||||
if (
|
||||
currentDeltaSet.remote != null &&
|
||||
currentDeltaSet.remote !== !!delta.remote
|
||||
) {
|
||||
newDeltaSet()
|
||||
}
|
||||
currentDeltaSet.deltas.push(delta)
|
||||
currentDeltaSet.remote = !!delta.remote
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The lines are currently as they are after applying all these deltas, but to turn into simple deltas,
|
||||
// we need the lines before each delta group.
|
||||
let docLines = this.session.getDocument().getAllLines()
|
||||
docLines = this._revertAceDeltaSetsOnDocLines(aceDeltaSets, docLines)
|
||||
for (deltaSet of Array.from(splitDeltaSets)) {
|
||||
var simpleDeltaSet
|
||||
;({ simpleDeltaSet, docLines } = this._aceDeltaSetToSimpleDeltaSet(
|
||||
deltaSet,
|
||||
docLines
|
||||
))
|
||||
const frame = {
|
||||
deltaSets: [simpleDeltaSet],
|
||||
remote: deltaSet.remote
|
||||
}
|
||||
this.undoStack.push(frame)
|
||||
}
|
||||
return (this.redoStack = [])
|
||||
}
|
||||
|
||||
undo(dontSelect) {
|
||||
// We rely on the doclines being in sync with the undo stack, so make sure
|
||||
// any pending undo deltas are processed.
|
||||
this.session.$syncInformUndoManager()
|
||||
|
||||
const localUpdatesMade = this._shiftLocalChangeToTopOfUndoStack()
|
||||
if (!localUpdatesMade) {
|
||||
return
|
||||
}
|
||||
|
||||
const update = this.undoStack.pop()
|
||||
if (update == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (update.remote) {
|
||||
this.showUndoConflictWarning()
|
||||
}
|
||||
|
||||
const lines = this.session.getDocument().getAllLines()
|
||||
const linesBeforeDelta = this._revertSimpleDeltaSetsOnDocLines(
|
||||
update.deltaSets,
|
||||
lines
|
||||
)
|
||||
const deltaSets = this._simpleDeltaSetsToAceDeltaSets(
|
||||
update.deltaSets,
|
||||
linesBeforeDelta
|
||||
)
|
||||
const selectionRange = this.session.undoChanges(deltaSets, dontSelect)
|
||||
this.redoStack.push(update)
|
||||
return selectionRange
|
||||
}
|
||||
|
||||
redo(dontSelect) {
|
||||
const update = this.redoStack.pop()
|
||||
if (update == null) {
|
||||
return
|
||||
}
|
||||
const lines = this.session.getDocument().getAllLines()
|
||||
const deltaSets = this._simpleDeltaSetsToAceDeltaSets(
|
||||
update.deltaSets,
|
||||
lines
|
||||
)
|
||||
const selectionRange = this.session.redoChanges(deltaSets, dontSelect)
|
||||
this.undoStack.push(update)
|
||||
return selectionRange
|
||||
}
|
||||
|
||||
_shiftLocalChangeToTopOfUndoStack() {
|
||||
const head = []
|
||||
let localChangeExists = false
|
||||
while (this.undoStack.length > 0) {
|
||||
const update = this.undoStack.pop()
|
||||
head.unshift(update)
|
||||
if (!update.remote) {
|
||||
localChangeExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!localChangeExists) {
|
||||
this.undoStack = this.undoStack.concat(head)
|
||||
return false
|
||||
} else {
|
||||
// Undo stack looks like undoStack ++ reorderedhead ++ head
|
||||
// Reordered head starts of empty and consumes entries from head
|
||||
// while keeping the localChange at the top for as long as it can
|
||||
let localChange = head.shift()
|
||||
const reorderedHead = [localChange]
|
||||
while (head.length > 0) {
|
||||
const remoteChange = head.shift()
|
||||
localChange = reorderedHead.pop()
|
||||
const result = this._swapSimpleDeltaSetsOrder(
|
||||
localChange.deltaSets,
|
||||
remoteChange.deltaSets
|
||||
)
|
||||
if (result != null) {
|
||||
remoteChange.deltaSets = result[0]
|
||||
localChange.deltaSets = result[1]
|
||||
reorderedHead.push(remoteChange)
|
||||
reorderedHead.push(localChange)
|
||||
} else {
|
||||
reorderedHead.push(localChange)
|
||||
reorderedHead.push(remoteChange)
|
||||
break
|
||||
}
|
||||
}
|
||||
this.undoStack = this.undoStack.concat(reorderedHead).concat(head)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UndoManager
|
||||
_swapSimpleDeltaSetsOrder(firstDeltaSets, secondDeltaSets) {
|
||||
const newFirstDeltaSets = this._copyDeltaSets(firstDeltaSets)
|
||||
const newSecondDeltaSets = this._copyDeltaSets(secondDeltaSets)
|
||||
for (let firstDeltaSet of Array.from(
|
||||
newFirstDeltaSets.slice(0).reverse()
|
||||
)) {
|
||||
for (let firstDelta of Array.from(
|
||||
firstDeltaSet.deltas.slice(0).reverse()
|
||||
)) {
|
||||
for (let secondDeltaSet of Array.from(newSecondDeltaSets)) {
|
||||
for (let secondDelta of Array.from(secondDeltaSet.deltas)) {
|
||||
const success = this._swapSimpleDeltaOrderInPlace(
|
||||
firstDelta,
|
||||
secondDelta
|
||||
)
|
||||
if (!success) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [newSecondDeltaSets, newFirstDeltaSets]
|
||||
}
|
||||
|
||||
_copyDeltaSets(deltaSets) {
|
||||
const newDeltaSets = []
|
||||
for (let deltaSet of Array.from(deltaSets)) {
|
||||
const newDeltaSet = {
|
||||
deltas: [],
|
||||
group: deltaSet.group
|
||||
}
|
||||
newDeltaSets.push(newDeltaSet)
|
||||
for (let delta of Array.from(deltaSet.deltas)) {
|
||||
const newDelta = { position: delta.position }
|
||||
if (delta.insert != null) {
|
||||
newDelta.insert = delta.insert
|
||||
}
|
||||
if (delta.remove != null) {
|
||||
newDelta.remove = delta.remove
|
||||
}
|
||||
newDeltaSet.deltas.push(newDelta)
|
||||
}
|
||||
}
|
||||
return newDeltaSets
|
||||
}
|
||||
|
||||
_swapSimpleDeltaOrderInPlace(firstDelta, secondDelta) {
|
||||
const result = this._swapSimpleDeltaOrder(firstDelta, secondDelta)
|
||||
if (result == null) {
|
||||
return false
|
||||
}
|
||||
firstDelta.position = result[1].position
|
||||
secondDelta.position = result[0].position
|
||||
return true
|
||||
}
|
||||
|
||||
_swapSimpleDeltaOrder(firstDelta, secondDelta) {
|
||||
if (firstDelta.insert != null && secondDelta.insert != null) {
|
||||
if (
|
||||
secondDelta.position >=
|
||||
firstDelta.position + firstDelta.insert.length
|
||||
) {
|
||||
secondDelta.position -= firstDelta.insert.length
|
||||
return [secondDelta, firstDelta]
|
||||
} else if (secondDelta.position > firstDelta.position) {
|
||||
return null
|
||||
} else {
|
||||
firstDelta.position += secondDelta.insert.length
|
||||
return [secondDelta, firstDelta]
|
||||
}
|
||||
} else if (firstDelta.remove != null && secondDelta.remove != null) {
|
||||
if (secondDelta.position >= firstDelta.position) {
|
||||
secondDelta.position += firstDelta.remove.length
|
||||
return [secondDelta, firstDelta]
|
||||
} else if (
|
||||
secondDelta.position + secondDelta.remove.length >
|
||||
firstDelta.position
|
||||
) {
|
||||
return null
|
||||
} else {
|
||||
firstDelta.position -= secondDelta.remove.length
|
||||
return [secondDelta, firstDelta]
|
||||
}
|
||||
} else if (firstDelta.insert != null && secondDelta.remove != null) {
|
||||
if (
|
||||
secondDelta.position >=
|
||||
firstDelta.position + firstDelta.insert.length
|
||||
) {
|
||||
secondDelta.position -= firstDelta.insert.length
|
||||
return [secondDelta, firstDelta]
|
||||
} else if (
|
||||
secondDelta.position + secondDelta.remove.length >
|
||||
firstDelta.position
|
||||
) {
|
||||
return null
|
||||
} else {
|
||||
firstDelta.position -= secondDelta.remove.length
|
||||
return [secondDelta, firstDelta]
|
||||
}
|
||||
} else if (firstDelta.remove != null && secondDelta.insert != null) {
|
||||
if (secondDelta.position >= firstDelta.position) {
|
||||
secondDelta.position += firstDelta.remove.length
|
||||
return [secondDelta, firstDelta]
|
||||
} else {
|
||||
firstDelta.position += secondDelta.insert.length
|
||||
return [secondDelta, firstDelta]
|
||||
}
|
||||
} else {
|
||||
throw 'Unknown delta types'
|
||||
}
|
||||
}
|
||||
|
||||
_applyAceDeltasToDocLines(deltas, docLines) {
|
||||
const doc = new Doc(docLines.join('\n'))
|
||||
doc.applyDeltas(deltas)
|
||||
return doc.getAllLines()
|
||||
}
|
||||
|
||||
_revertAceDeltaSetsOnDocLines(deltaSets, docLines) {
|
||||
const session = new EditSession(docLines.join('\n'))
|
||||
session.undoChanges(deltaSets)
|
||||
return session.getDocument().getAllLines()
|
||||
}
|
||||
|
||||
_revertSimpleDeltaSetsOnDocLines(deltaSets, docLines) {
|
||||
let doc = docLines.join('\n')
|
||||
for (let deltaSet of Array.from(deltaSets.slice(0).reverse())) {
|
||||
for (let delta of Array.from(deltaSet.deltas.slice(0).reverse())) {
|
||||
if (delta.remove != null) {
|
||||
doc =
|
||||
doc.slice(0, delta.position) +
|
||||
delta.remove +
|
||||
doc.slice(delta.position)
|
||||
} else if (delta.insert != null) {
|
||||
doc =
|
||||
doc.slice(0, delta.position) +
|
||||
doc.slice(delta.position + delta.insert.length)
|
||||
} else {
|
||||
throw 'Unknown delta type'
|
||||
}
|
||||
}
|
||||
}
|
||||
return doc.split('\n')
|
||||
}
|
||||
|
||||
_aceDeltaSetToSimpleDeltaSet(deltaSet, docLines) {
|
||||
const simpleDeltas = []
|
||||
for (let delta of Array.from(deltaSet.deltas)) {
|
||||
simpleDeltas.push(this._aceDeltaToSimpleDelta(delta, docLines))
|
||||
docLines = this._applyAceDeltasToDocLines([delta], docLines)
|
||||
}
|
||||
const simpleDeltaSet = {
|
||||
deltas: simpleDeltas,
|
||||
group: deltaSet.group
|
||||
}
|
||||
return { simpleDeltaSet, docLines }
|
||||
}
|
||||
|
||||
_simpleDeltaSetsToAceDeltaSets(simpleDeltaSets, docLines) {
|
||||
return (() => {
|
||||
const result = []
|
||||
for (let deltaSet of Array.from(simpleDeltaSets)) {
|
||||
let aceDeltas = []
|
||||
for (let delta of Array.from(deltaSet.deltas)) {
|
||||
const newAceDeltas = this._simpleDeltaToAceDeltas(delta, docLines)
|
||||
docLines = this._applyAceDeltasToDocLines(newAceDeltas, docLines)
|
||||
aceDeltas = aceDeltas.concat(newAceDeltas)
|
||||
}
|
||||
result.push({
|
||||
deltas: aceDeltas,
|
||||
group: deltaSet.group
|
||||
})
|
||||
}
|
||||
return result
|
||||
})()
|
||||
}
|
||||
|
||||
_aceDeltaToSimpleDelta(aceDelta, docLines) {
|
||||
let simpleDelta
|
||||
const { start } = aceDelta
|
||||
if (start == null) {
|
||||
const JSONstringifyWithCycles = function(o) {
|
||||
const seen = []
|
||||
return JSON.stringify(o, function(k, v) {
|
||||
if (typeof v === 'object') {
|
||||
if (seen.indexOf(v) >= 0) {
|
||||
return '__cycle__'
|
||||
}
|
||||
seen.push(v)
|
||||
}
|
||||
return v
|
||||
})
|
||||
}
|
||||
const error = new Error(
|
||||
`aceDelta had no start event: ${JSONstringifyWithCycles(aceDelta)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
const linesBefore = docLines.slice(0, start.row)
|
||||
const position =
|
||||
linesBefore.join('').length + // full lines
|
||||
linesBefore.length + // new line characters
|
||||
start.column // partial line
|
||||
switch (aceDelta.action) {
|
||||
case 'insert':
|
||||
simpleDelta = {
|
||||
position,
|
||||
insert: aceDelta.lines.join('\n')
|
||||
}
|
||||
break
|
||||
case 'remove':
|
||||
simpleDelta = {
|
||||
position,
|
||||
remove: aceDelta.lines.join('\n')
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unknown Ace action: ${aceDelta.action}`)
|
||||
}
|
||||
return simpleDelta
|
||||
}
|
||||
|
||||
_simplePositionToAcePosition(position, docLines) {
|
||||
let column = 0
|
||||
let row = 0
|
||||
for (let line of Array.from(docLines)) {
|
||||
if (position > line.length) {
|
||||
row++
|
||||
position -= (line + '\n').length
|
||||
} else {
|
||||
column = position
|
||||
break
|
||||
}
|
||||
}
|
||||
return { row, column }
|
||||
}
|
||||
|
||||
_simpleDeltaToAceDeltas(simpleDelta, docLines) {
|
||||
let aceDelta, end
|
||||
const { row, column } = this._simplePositionToAcePosition(
|
||||
simpleDelta.position,
|
||||
docLines
|
||||
)
|
||||
|
||||
const lines = (simpleDelta.insert || simpleDelta.remove || '').split('\n')
|
||||
|
||||
const start = { column, row }
|
||||
if (lines.length > 1) {
|
||||
end = {
|
||||
row: row + lines.length - 1,
|
||||
column: lines[lines.length - 1].length
|
||||
}
|
||||
} else {
|
||||
end = {
|
||||
row,
|
||||
column: column + lines[0].length
|
||||
}
|
||||
}
|
||||
|
||||
if (simpleDelta.insert != null) {
|
||||
aceDelta = {
|
||||
action: 'insert',
|
||||
lines,
|
||||
start,
|
||||
end
|
||||
}
|
||||
} else if (simpleDelta.remove != null) {
|
||||
aceDelta = {
|
||||
action: 'remove',
|
||||
lines,
|
||||
start,
|
||||
end
|
||||
}
|
||||
} else {
|
||||
throw `Unknown simple delta: ${simpleDelta}`
|
||||
}
|
||||
|
||||
return [aceDelta]
|
||||
}
|
||||
|
||||
_concatSimpleDeltas(deltas) {
|
||||
if (deltas.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const concattedDeltas = []
|
||||
let previousDelta = deltas.shift()
|
||||
for (let delta of Array.from(deltas)) {
|
||||
if (delta.insert != null && previousDelta.insert != null) {
|
||||
if (
|
||||
previousDelta.position + previousDelta.insert.length ===
|
||||
delta.position
|
||||
) {
|
||||
previousDelta = {
|
||||
insert: previousDelta.insert + delta.insert,
|
||||
position: previousDelta.position
|
||||
}
|
||||
} else {
|
||||
concattedDeltas.push(previousDelta)
|
||||
previousDelta = delta
|
||||
}
|
||||
} else if (delta.remove != null && previousDelta.remove != null) {
|
||||
if (previousDelta.position === delta.position) {
|
||||
previousDelta = {
|
||||
remove: previousDelta.remove + delta.remove,
|
||||
position: delta.position
|
||||
}
|
||||
} else {
|
||||
concattedDeltas.push(previousDelta)
|
||||
previousDelta = delta
|
||||
}
|
||||
} else {
|
||||
concattedDeltas.push(previousDelta)
|
||||
previousDelta = delta
|
||||
}
|
||||
}
|
||||
concattedDeltas.push(previousDelta)
|
||||
|
||||
return concattedDeltas
|
||||
}
|
||||
|
||||
hasUndo() {
|
||||
return this.undoStack.length > 0
|
||||
}
|
||||
hasRedo() {
|
||||
return this.redoStack.length > 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue