Просмотр исходного кода

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
Alasdair Smith 7 лет назад
Родитель
Сommit
b1a615bad9

+ 0 - 6
services/web/public/js/libs/sharejs.js

@@ -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();
     };

+ 526 - 21
services/web/public/src/ide/editor/directives/aceEditor/undo/UndoManager.js

@@ -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)
       })
+    }
 
-      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()
+    showUndoConflictWarning() {
+      this.$scope.$apply(() => {
+        return (this.$scope.undo.show_remote_warning = true)
+      })
+
+      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
+      }
+    }
+
+    _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
     }
-  }
 
-  return UndoManager
+    _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
+    }
+  })
 })