mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-12 07:30:46 +02:00
Add in undo manager
This commit is contained in:
@@ -42,6 +42,7 @@ block content
|
||||
include ./editor/file-tree
|
||||
|
||||
.ui-layout-center
|
||||
#editor(ace-editor, theme="'cobalt'", show-print-margin="false", sharejs-doc="editor.sharejs_doc")
|
||||
|
||||
//- #loadingScreen
|
||||
//- h3 Loading...
|
||||
@@ -80,9 +81,10 @@ block content
|
||||
window.csrfToken = "!{csrfToken}";
|
||||
window.requirejs = {
|
||||
"paths" : {
|
||||
"underscore": "libs/underscore-1.3.3",
|
||||
"underscore": "../libs/underscore-1.3.3",
|
||||
"mathjax": "https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML",
|
||||
"moment": "libs/moment-2.4.0"
|
||||
"moment": "libs/moment-2.4.0",
|
||||
"ace": "#{jsPath}ace"
|
||||
},
|
||||
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}",
|
||||
"waitSeconds": 0,
|
||||
|
||||
@@ -2,6 +2,7 @@ define [
|
||||
"base"
|
||||
"ide/file-tree/FileTreeManager"
|
||||
"ide/connection/ConnectionManager"
|
||||
"ide/editor/EditorManager"
|
||||
"ide/directives/layout"
|
||||
"ide/services/ide"
|
||||
"directives/focus"
|
||||
@@ -11,6 +12,7 @@ define [
|
||||
App
|
||||
FileTreeManager
|
||||
ConnectionManager
|
||||
EditorManager
|
||||
) ->
|
||||
App.controller "IdeController", ["$scope", "$timeout", "ide", ($scope, $timeout, ide) ->
|
||||
$scope.state = {
|
||||
@@ -25,6 +27,7 @@ define [
|
||||
|
||||
ide.connectionManager = new ConnectionManager(ide, $scope)
|
||||
ide.fileTreeManager = new FileTreeManager(ide, $scope)
|
||||
ide.editorManager = new EditorManager(ide, $scope)
|
||||
]
|
||||
|
||||
angular.bootstrap(document.body, ["SharelatexApp"])
|
||||
@@ -24,4 +24,12 @@ define [], () ->
|
||||
|
||||
@$scope.$emit "project:joined"
|
||||
|
||||
setTimeout(joinProject, 100)
|
||||
setTimeout(joinProject, 100)
|
||||
|
||||
reconnectImmediately: () ->
|
||||
console.log "RECONNECT IMMEDIATELY STUB"
|
||||
@disconnect()
|
||||
#@tryReconnect()
|
||||
|
||||
disconnect: () ->
|
||||
@socket.disconnect()
|
||||
@@ -0,0 +1,250 @@
|
||||
define [
|
||||
"utils/EventEmitter"
|
||||
"ide/editor/ShareJsDoc"
|
||||
"underscore"
|
||||
], (EventEmitter, ShareJsDoc) ->
|
||||
class Document extends EventEmitter
|
||||
@getDocument: (ide, doc_id) ->
|
||||
@openDocs ||= {}
|
||||
if !@openDocs[doc_id]?
|
||||
@openDocs[doc_id] = new Document(ide, doc_id)
|
||||
return @openDocs[doc_id]
|
||||
|
||||
@hasUnsavedChanges: () ->
|
||||
for doc_id, doc of (@openDocs or {})
|
||||
return true if doc.hasBufferedOps()
|
||||
return false
|
||||
|
||||
constructor: (@ide, @doc_id) ->
|
||||
@connected = @ide.socket.socket.connected
|
||||
@joined = false
|
||||
@wantToBeJoined = false
|
||||
@_checkConsistency = _.bind(@_checkConsistency, @)
|
||||
@inconsistentCount = 0
|
||||
@_bindToEditorEvents()
|
||||
@_bindToSocketEvents()
|
||||
|
||||
attachToAce: (@ace) ->
|
||||
@doc?.attachToAce(@ace)
|
||||
editorDoc = @ace.getSession().getDocument()
|
||||
editorDoc.on "change", @_checkConsistency
|
||||
|
||||
detachFromAce: () ->
|
||||
@doc?.detachFromAce()
|
||||
editorDoc = @ace?.getSession().getDocument()
|
||||
editorDoc?.off "change", @_checkConsistency
|
||||
|
||||
_checkConsistency: () ->
|
||||
# We've been seeing a lot of errors when I think there shouldn't be
|
||||
# any, which may be related to this check happening before the change is
|
||||
# applied. If we use a timeout, hopefully we can reduce this.
|
||||
setTimeout () =>
|
||||
editorValue = @ace?.getValue()
|
||||
sharejsValue = @doc?.getSnapshot()
|
||||
if editorValue != sharejsValue
|
||||
@inconsistentCount++
|
||||
else
|
||||
@inconsistentCount = 0
|
||||
|
||||
if @inconsistentCount >= 3
|
||||
@_onError new Error("Editor text does not match server text")
|
||||
, 0
|
||||
|
||||
getSnapshot: () ->
|
||||
@doc?.getSnapshot()
|
||||
|
||||
getType: () ->
|
||||
@doc?.getType()
|
||||
|
||||
getInflightOp: () ->
|
||||
@doc?.getInflightOp()
|
||||
|
||||
getPendingOp: () ->
|
||||
@doc?.getPendingOp()
|
||||
|
||||
hasBufferedOps: () ->
|
||||
@doc?.hasBufferedOps()
|
||||
|
||||
_bindToSocketEvents: () ->
|
||||
@_onUpdateAppliedHandler = (update) => @_onUpdateApplied(update)
|
||||
@ide.socket.on "otUpdateApplied", @_onUpdateAppliedHandler
|
||||
@_onErrorHandler = (error, update) => @_onError(error, update)
|
||||
@ide.socket.on "otUpdateError", @_onErrorHandler
|
||||
@_onDisconnectHandler = (error) => @_onDisconnect(error)
|
||||
@ide.socket.on "disconnect", @_onDisconnectHandler
|
||||
|
||||
_bindToEditorEvents: () ->
|
||||
onReconnectHandler = (update) =>
|
||||
@_onReconnect(update)
|
||||
@_unsubscribeReconnectHandler = @ide.$scope.$on "project:joined", onReconnectHandler
|
||||
|
||||
_unBindFromEditorEvents: () ->
|
||||
@_unsubscribeReconnectHandler()
|
||||
|
||||
_unBindFromSocketEvents: () ->
|
||||
@ide.socket.removeListener "otUpdateApplied", @_onUpdateAppliedHandler
|
||||
@ide.socket.removeListener "otUpdateError", @_onUpdateErrorHandler
|
||||
@ide.socket.removeListener "disconnect", @_onDisconnectHandler
|
||||
|
||||
leaveAndCleanUp: () ->
|
||||
@leave (error) =>
|
||||
@_cleanUp()
|
||||
|
||||
join: (callback = (error) ->) ->
|
||||
@wantToBeJoined = true
|
||||
@_cancelLeave()
|
||||
if @connected
|
||||
return @_joinDoc callback
|
||||
else
|
||||
@_joinCallbacks ||= []
|
||||
@_joinCallbacks.push callback
|
||||
|
||||
leave: (callback = (error) ->) ->
|
||||
@wantToBeJoined = false
|
||||
@_cancelJoin()
|
||||
if (@doc? and @doc.hasBufferedOps())
|
||||
@_leaveCallbacks ||= []
|
||||
@_leaveCallbacks.push callback
|
||||
else if !@connected
|
||||
callback()
|
||||
else
|
||||
@_leaveDoc(callback)
|
||||
|
||||
pollSavedStatus: () ->
|
||||
# returns false if doc has ops waiting to be acknowledged or
|
||||
# sent that haven't changed since the last time we checked.
|
||||
# Otherwise returns true.
|
||||
inflightOp = @getInflightOp()
|
||||
pendingOp = @getPendingOp()
|
||||
if !inflightOp? and !pendingOp?
|
||||
# there's nothing going on
|
||||
saved = true
|
||||
else if inflightOp == @oldInflightOp
|
||||
saved = false
|
||||
else if pendingOp?
|
||||
saved = false
|
||||
else
|
||||
saved = true
|
||||
|
||||
@oldInflightOp = inflightOp
|
||||
return saved
|
||||
|
||||
_cancelLeave: () ->
|
||||
if @_leaveCallbacks?
|
||||
delete @_leaveCallbacks
|
||||
|
||||
_cancelJoin: () ->
|
||||
if @_joinCallbacks?
|
||||
delete @_joinCallbacks
|
||||
|
||||
_onUpdateApplied: (update) ->
|
||||
@ide.pushEvent "received-update",
|
||||
doc_id: @doc_id
|
||||
remote_doc_id: update?.doc
|
||||
wantToBeJoined: @wantToBeJoined
|
||||
update: update
|
||||
|
||||
if Math.random() < (@ide.disconnectRate or 0)
|
||||
console.log "Simulating disconnect"
|
||||
@ide.connectionManager.disconnect()
|
||||
return
|
||||
|
||||
if update?.doc == @doc_id and @doc?
|
||||
@doc.processUpdateFromServer update
|
||||
|
||||
if !@wantToBeJoined
|
||||
@leave()
|
||||
|
||||
_onDisconnect: () ->
|
||||
@connected = false
|
||||
@joined = false
|
||||
@doc?.updateConnectionState "disconnected"
|
||||
|
||||
_onReconnect: () ->
|
||||
@ide.pushEvent "reconnected:afterJoinProject"
|
||||
|
||||
@connected = true
|
||||
if @wantToBeJoined or @doc?.hasBufferedOps()
|
||||
@_joinDoc (error) =>
|
||||
return @_onError(error) if error?
|
||||
@doc.updateConnectionState "ok"
|
||||
@doc.flushPendingOps()
|
||||
@_callJoinCallbacks()
|
||||
|
||||
_callJoinCallbacks: () ->
|
||||
for callback in @_joinCallbacks or []
|
||||
callback()
|
||||
delete @_joinCallbacks
|
||||
|
||||
_joinDoc: (callback = (error) ->) ->
|
||||
if @doc?
|
||||
@ide.socket.emit 'joinDoc', @doc_id, @doc.getVersion(), (error, docLines, version, updates) =>
|
||||
return callback(error) if error?
|
||||
@joined = true
|
||||
@doc.catchUp( updates )
|
||||
callback()
|
||||
else
|
||||
@ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version) =>
|
||||
return callback(error) if error?
|
||||
@joined = true
|
||||
@doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket
|
||||
@_bindToShareJsDocEvents()
|
||||
callback()
|
||||
|
||||
_leaveDoc: (callback = (error) ->) ->
|
||||
@ide.socket.emit 'leaveDoc', @doc_id, (error) =>
|
||||
return callback(error) if error?
|
||||
@joined = false
|
||||
for callback in @_leaveCallbacks or []
|
||||
callback(error)
|
||||
delete @_leaveCallbacks
|
||||
callback(error)
|
||||
|
||||
_cleanUp: () ->
|
||||
delete Document.openDocs[@doc_id]
|
||||
@_unBindFromEditorEvents()
|
||||
@_unBindFromSocketEvents()
|
||||
|
||||
_bindToShareJsDocEvents: () ->
|
||||
@doc.on "error", (error, meta) => @_onError error, meta
|
||||
@doc.on "externalUpdate", () =>
|
||||
@ide.pushEvent "externalUpdate",
|
||||
doc_id: @doc_id
|
||||
@trigger "externalUpdate"
|
||||
@doc.on "remoteop", () =>
|
||||
@ide.pushEvent "remoteop",
|
||||
doc_id: @doc_id
|
||||
@trigger "remoteop"
|
||||
@doc.on "op:sent", (op) =>
|
||||
@ide.pushEvent "op:sent",
|
||||
doc_id: @doc_id
|
||||
op: op
|
||||
@trigger "op:sent"
|
||||
@doc.on "op:acknowledged", (op) =>
|
||||
@ide.pushEvent "op:acknowledged",
|
||||
doc_id: @doc_id
|
||||
op: op
|
||||
@trigger "op:acknowledged"
|
||||
@doc.on "op:timeout", (op) =>
|
||||
@ide.pushEvent "op:timeout",
|
||||
doc_id: @doc_id
|
||||
op: op
|
||||
@trigger "op:timeout"
|
||||
ga?('send', 'event', 'error', "op timeout", "Op was now acknowledged - #{@ide.socket.socket.transport.name}" )
|
||||
@ide.connectionManager.reconnectImmediately()
|
||||
@doc.on "flush", (inflightOp, pendingOp, version) =>
|
||||
@ide.pushEvent "flush",
|
||||
doc_id: @doc_id,
|
||||
inflightOp: inflightOp,
|
||||
pendingOp: pendingOp
|
||||
v: version
|
||||
|
||||
_onError: (error, meta = {}) ->
|
||||
console.error "ShareJS error", error, meta
|
||||
ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{@ide.socket.socket.transport.name}" )
|
||||
@ide.socket.disconnect()
|
||||
meta.doc_id = @doc_id
|
||||
@ide.reportError(error, meta)
|
||||
@doc?.clearInflightAndPendingOps()
|
||||
@_cleanUp()
|
||||
@trigger "error", error
|
||||
@@ -0,0 +1,55 @@
|
||||
define [
|
||||
"ide/editor/Document"
|
||||
"ide/editor/directives/aceEditor"
|
||||
], (Document) ->
|
||||
class EditorManager
|
||||
constructor: (@ide, @$scope) ->
|
||||
@$scope.editor = {
|
||||
sharejs_doc: null
|
||||
}
|
||||
|
||||
openDoc: (doc, options = {}) ->
|
||||
# TODO: Don't open if already open
|
||||
@_openNewDocument doc, (error, sharejs_doc) =>
|
||||
if error?
|
||||
@ide.showGenericServerErrorMessage()
|
||||
return
|
||||
|
||||
@$scope.$apply () =>
|
||||
@$scope.editor.sharejs_doc = sharejs_doc
|
||||
|
||||
_openNewDocument: (doc, callback = (error, sharejs_doc) ->) ->
|
||||
current_sharejs_doc = @$scope.editor.sharejs_doc
|
||||
if current_sharejs_doc?
|
||||
current_sharejs_doc.leaveAndCleanUp()
|
||||
@_unbindFromDocumentEvents(current_sharejs_doc)
|
||||
|
||||
new_sharejs_doc = Document.getDocument @ide, doc.id
|
||||
|
||||
new_sharejs_doc.join (error) =>
|
||||
return callback(error) if error?
|
||||
@_bindToDocumentEvents(new_sharejs_doc)
|
||||
callback null, new_sharejs_doc
|
||||
|
||||
_bindToDocumentEvents: (document) ->
|
||||
|
||||
document.on "error", (error) =>
|
||||
@openDoc(document.doc_id, forceReopen: true)
|
||||
|
||||
Modal.createModal
|
||||
title: "Out of sync"
|
||||
message: "Sorry, this file has gone out of sync and we need to do a full refresh. Please let us know if this happens frequently."
|
||||
buttons:[
|
||||
text: "Ok"
|
||||
]
|
||||
|
||||
document.on "externalUpdate", () =>
|
||||
Modal.createModal
|
||||
title: "Document Updated Externally"
|
||||
message: "This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history."
|
||||
buttons:[
|
||||
text: "Ok"
|
||||
]
|
||||
|
||||
_unbindFromDocumentEvents: (document) ->
|
||||
document.off()
|
||||
@@ -0,0 +1,123 @@
|
||||
define [
|
||||
"utils/EventEmitter"
|
||||
"../../../libs/sharejs"
|
||||
], (EventEmitter, ShareJs) ->
|
||||
class ShareJsDoc extends EventEmitter
|
||||
constructor: (@doc_id, docLines, version, @socket) ->
|
||||
# Dencode any binary bits of data
|
||||
# See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
|
||||
@type = "text"
|
||||
docLines = for line in docLines
|
||||
if line.text?
|
||||
@type = "json"
|
||||
line.text = decodeURIComponent(escape(line.text))
|
||||
else
|
||||
@type = "text"
|
||||
line = decodeURIComponent(escape(line))
|
||||
line
|
||||
|
||||
if @type == "text"
|
||||
snapshot = docLines.join("\n")
|
||||
else if @type == "json"
|
||||
snapshot = { lines: docLines }
|
||||
else
|
||||
throw new Error("Unknown type: #{@type}")
|
||||
|
||||
@connection = {
|
||||
send: (update) =>
|
||||
@_startInflightOpTimeout(update)
|
||||
@socket.emit "applyOtUpdate", @doc_id, update
|
||||
state: "ok"
|
||||
id: @socket.socket.sessionid
|
||||
}
|
||||
|
||||
@_doc = new ShareJs.Doc @connection, @doc_id,
|
||||
type: @type
|
||||
@_doc.on "change", () =>
|
||||
@trigger "change"
|
||||
@_doc.on "acknowledge", () =>
|
||||
@trigger "acknowledge"
|
||||
@_doc.on "remoteop", () =>
|
||||
@trigger "remoteop"
|
||||
|
||||
@_bindToDocChanges(@_doc)
|
||||
|
||||
@processUpdateFromServer
|
||||
open: true
|
||||
v: version
|
||||
snapshot: snapshot
|
||||
|
||||
submitOp: (args...) -> @_doc.submitOp(args...)
|
||||
|
||||
processUpdateFromServer: (message) ->
|
||||
try
|
||||
@_doc._onMessage message
|
||||
catch error
|
||||
# Version mismatches are thrown as errors
|
||||
@_handleError(error)
|
||||
|
||||
if message?.meta?.type == "external"
|
||||
@trigger "externalUpdate"
|
||||
|
||||
catchUp: (updates) ->
|
||||
for update, i in updates
|
||||
update.v = @_doc.version
|
||||
update.doc = @doc_id
|
||||
@processUpdateFromServer(update)
|
||||
|
||||
getSnapshot: () -> @_doc.snapshot
|
||||
getVersion: () -> @_doc.version
|
||||
getType: () -> @type
|
||||
|
||||
clearInflightAndPendingOps: () ->
|
||||
@_doc.inflightOp = null
|
||||
@_doc.inflightCallbacks = []
|
||||
@_doc.pendingOp = null
|
||||
@_doc.pendingCallbacks = []
|
||||
|
||||
flushPendingOps: () ->
|
||||
# This will flush any ops that are pending.
|
||||
# If there is an inflight op it will do nothing.
|
||||
@_doc.flush()
|
||||
|
||||
updateConnectionState: (state) ->
|
||||
@connection.state = state
|
||||
@connection.id = @socket.socket.sessionid
|
||||
@_doc.autoOpen = false
|
||||
@_doc._connectionStateChanged(state)
|
||||
|
||||
hasBufferedOps: () ->
|
||||
@_doc.inflightOp? or @_doc.pendingOp?
|
||||
|
||||
getInflightOp: () -> @_doc.inflightOp
|
||||
getPendingOp: () -> @_doc.pendingOp
|
||||
|
||||
attachToAce: (ace) -> @_doc.attach_ace(ace)
|
||||
detachFromAce: () -> @_doc.detach_ace?()
|
||||
|
||||
INFLIGHT_OP_TIMEOUT: 10000
|
||||
_startInflightOpTimeout: (update) ->
|
||||
meta =
|
||||
v: update.v
|
||||
op_sent_at: new Date()
|
||||
timer = setTimeout () =>
|
||||
@trigger "op:timeout", update
|
||||
, @INFLIGHT_OP_TIMEOUT
|
||||
@_doc.inflightCallbacks.push () =>
|
||||
clearTimeout timer
|
||||
|
||||
_handleError: (error, meta = {}) ->
|
||||
@trigger "error", error, meta
|
||||
|
||||
_bindToDocChanges: (doc) ->
|
||||
submitOp = doc.submitOp
|
||||
doc.submitOp = (args...) =>
|
||||
@trigger "op:sent", args...
|
||||
doc.pendingCallbacks.push () =>
|
||||
@trigger "op:acknowledged", args...
|
||||
submitOp.apply(doc, args)
|
||||
|
||||
flush = doc.flush
|
||||
doc.flush = (args...) =>
|
||||
@trigger "flush", doc.inflightOp, doc.pendingOp, doc.version
|
||||
flush.apply(doc, args)
|
||||
@@ -0,0 +1,97 @@
|
||||
define [
|
||||
"base"
|
||||
"ide/editor/undo/UndoManager"
|
||||
"ace/ace"
|
||||
"ace/keyboard/vim"
|
||||
"ace/keyboard/emacs"
|
||||
"ace/mode/latex"
|
||||
"ace/edit_session"
|
||||
], (App, UndoManager, Ace) ->
|
||||
LatexMode = require("ace/mode/latex").Mode
|
||||
EditSession = require('ace/edit_session').EditSession
|
||||
|
||||
App.directive "aceEditor", ["$timeout", ($timeout) ->
|
||||
return {
|
||||
scope: {
|
||||
theme: "="
|
||||
showPrintMargin: "="
|
||||
keybindings: "="
|
||||
sharejsDoc: "="
|
||||
}
|
||||
link: (scope, element, attrs) ->
|
||||
editor = Ace.edit(element.find(".ace-editor-body")[0])
|
||||
scope.undo =
|
||||
show_remote_warning: true
|
||||
|
||||
# Prevert Ctrl|Cmd-S from triggering save dialog
|
||||
editor.commands.addCommand
|
||||
name: "save",
|
||||
bindKey: win: "Ctrl-S", mac: "Command-S"
|
||||
exec: () ->
|
||||
readOnly: true
|
||||
editor.commands.removeCommand "transposeletters"
|
||||
editor.commands.removeCommand "showSettingsMenu"
|
||||
editor.commands.removeCommand "foldall"
|
||||
|
||||
scope.$watch "theme", (value) ->
|
||||
editor.setTheme("ace/theme/#{value}")
|
||||
|
||||
scope.$watch "showPrintMargin", (value) ->
|
||||
editor.setShowPrintMargin(value)
|
||||
|
||||
scope.$watch "keybindings", (value) ->
|
||||
Vim = require("ace/keyboard/vim").handler
|
||||
Emacs = require("ace/keyboard/emacs").handler
|
||||
keybindings = ace: null, vim: Vim, emacs: Emacs
|
||||
editor.setKeyboardHandler(keybindings[value])
|
||||
|
||||
scope.$watch "sharejsDoc", (sharejs_doc, old_sharejs_doc) ->
|
||||
console.log "attaching doc to ace", sharejs_doc, old_sharejs_doc
|
||||
if old_sharejs_doc?
|
||||
old_sharejs_doc.detachFromAce()
|
||||
old_sharejs_doc.off "remoteop.recordForUndo"
|
||||
|
||||
if sharejs_doc?
|
||||
lines = sharejs_doc.getSnapshot().split("\n")
|
||||
editor.setSession(new EditSession(lines))
|
||||
session = editor.getSession()
|
||||
session.setUseWrapMode(true)
|
||||
session.setMode(new LatexMode())
|
||||
|
||||
undoManager = new UndoManager({
|
||||
showUndoConflictWarning: () ->
|
||||
scope.$apply () ->
|
||||
scope.undo.show_remote_warning = true
|
||||
|
||||
$timeout () ->
|
||||
scope.undo.show_remote_warning = false
|
||||
, 4000
|
||||
|
||||
})
|
||||
session.setUndoManager(undoManager)
|
||||
|
||||
sharejs_doc.on "remoteop.recordForUndo", () =>
|
||||
console.log "Remote OP!"
|
||||
undoManager.nextUpdateIsRemote = true
|
||||
|
||||
sharejs_doc.attachToAce(editor)
|
||||
|
||||
template: """
|
||||
<div class="ace-editor-wrapper">
|
||||
<div
|
||||
class="undo-conflict-warning alert alert-danger small"
|
||||
ng-show="undo.show_remote_warning"
|
||||
>
|
||||
<strong>Watch out!</strong>
|
||||
We had to undo some of your collaborators changes before we could undo yours.
|
||||
<a
|
||||
href="#"
|
||||
class="pull-right"
|
||||
ng-click="undo.show_remote_warning = false"
|
||||
>Dismiss</a>
|
||||
</div>
|
||||
<div class="ace-editor-body"></div>
|
||||
</div>
|
||||
"""
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,358 @@
|
||||
define [
|
||||
"ace/range"
|
||||
"ace/edit_session"
|
||||
"ace/document"
|
||||
], () ->
|
||||
Range = require("ace/range").Range
|
||||
EditSession = require("ace/edit_session").EditSession
|
||||
Doc = require("ace/document").Document
|
||||
|
||||
class UndoManager
|
||||
constructor: (@manager) ->
|
||||
@reset()
|
||||
@nextUpdateIsRemote = false
|
||||
|
||||
reset: () ->
|
||||
@undoStack = []
|
||||
@redoStack = []
|
||||
|
||||
execute: (options) ->
|
||||
aceDeltaSets = options.args[0]
|
||||
@session = options.args[1]
|
||||
return if !aceDeltaSets?
|
||||
|
||||
lines = @session.getDocument().getAllLines()
|
||||
linesBeforeChange = @_revertAceDeltaSetsOnDocLines(aceDeltaSets, lines)
|
||||
simpleDeltaSets = @_aceDeltaSetsToSimpleDeltaSets(aceDeltaSets, linesBeforeChange)
|
||||
@undoStack.push(
|
||||
deltaSets: simpleDeltaSets
|
||||
remote: @nextUpdateIsRemote
|
||||
)
|
||||
@redoStack = []
|
||||
@nextUpdateIsRemote = false
|
||||
|
||||
undo: (dontSelect) ->
|
||||
localUpdatesMade = @_shiftLocalChangeToTopOfUndoStack()
|
||||
return if !localUpdatesMade
|
||||
|
||||
update = @undoStack.pop()
|
||||
return if !update?
|
||||
|
||||
if update.remote
|
||||
@manager.showUndoConflictWarning()
|
||||
|
||||
lines = @session.getDocument().getAllLines()
|
||||
linesBeforeDelta = @_revertSimpleDeltaSetsOnDocLines(update.deltaSets, lines)
|
||||
deltaSets = @_simpleDeltaSetsToAceDeltaSets(update.deltaSets, linesBeforeDelta)
|
||||
selectionRange = @session.undoChanges(deltaSets, dontSelect)
|
||||
@redoStack.push(update)
|
||||
return selectionRange
|
||||
|
||||
redo: (dontSelect) ->
|
||||
update = @redoStack.pop()
|
||||
return if !update?
|
||||
lines = @session.getDocument().getAllLines()
|
||||
deltaSets = @_simpleDeltaSetsToAceDeltaSets(update.deltaSets, lines)
|
||||
selectionRange = @session.redoChanges(deltaSets, dontSelect)
|
||||
@undoStack.push(update)
|
||||
return selectionRange
|
||||
|
||||
_shiftLocalChangeToTopOfUndoStack: () ->
|
||||
head = []
|
||||
localChangeExists = false
|
||||
while @undoStack.length > 0
|
||||
update = @undoStack.pop()
|
||||
head.unshift update
|
||||
if !update.remote
|
||||
localChangeExists = true
|
||||
break
|
||||
|
||||
if !localChangeExists
|
||||
@undoStack = @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
|
||||
localChange = head.shift()
|
||||
reorderedHead = [localChange]
|
||||
while head.length > 0
|
||||
remoteChange = head.shift()
|
||||
localChange = reorderedHead.pop()
|
||||
result = @_swapSimpleDeltaSetsOrder(localChange.deltaSets, remoteChange.deltaSets)
|
||||
if result?
|
||||
remoteChange.deltaSets = result[0]
|
||||
localChange.deltaSets = result[1]
|
||||
reorderedHead.push remoteChange
|
||||
reorderedHead.push localChange
|
||||
else
|
||||
reorderedHead.push localChange
|
||||
reorderedHead.push remoteChange
|
||||
break
|
||||
@undoStack = @undoStack.concat(reorderedHead).concat(head)
|
||||
return true
|
||||
|
||||
|
||||
_swapSimpleDeltaSetsOrder: (firstDeltaSets, secondDeltaSets) ->
|
||||
newFirstDeltaSets = @_copyDeltaSets(firstDeltaSets)
|
||||
newSecondDeltaSets = @_copyDeltaSets(secondDeltaSets)
|
||||
for firstDeltaSet in newFirstDeltaSets.slice(0).reverse()
|
||||
for firstDelta in firstDeltaSet.deltas.slice(0).reverse()
|
||||
for secondDeltaSet in newSecondDeltaSets
|
||||
for secondDelta in secondDeltaSet.deltas
|
||||
success = @_swapSimpleDeltaOrderInPlace(firstDelta, secondDelta)
|
||||
return null if !success
|
||||
return [newSecondDeltaSets, newFirstDeltaSets]
|
||||
|
||||
_copyDeltaSets: (deltaSets) ->
|
||||
newDeltaSets = []
|
||||
for deltaSet in deltaSets
|
||||
newDeltaSet =
|
||||
deltas: []
|
||||
group: deltaSet.group
|
||||
newDeltaSets.push newDeltaSet
|
||||
for delta in deltaSet.deltas
|
||||
newDelta =
|
||||
position: delta.position
|
||||
newDelta.insert = delta.insert if delta.insert?
|
||||
newDelta.remove = delta.remove if delta.remove?
|
||||
newDeltaSet.deltas.push newDelta
|
||||
return newDeltaSets
|
||||
|
||||
_swapSimpleDeltaOrderInPlace: (firstDelta, secondDelta) ->
|
||||
result = @_swapSimpleDeltaOrder(firstDelta, secondDelta)
|
||||
return false if !result?
|
||||
firstDelta.position = result[1].position
|
||||
secondDelta.position = result[0].position
|
||||
return true
|
||||
|
||||
_swapSimpleDeltaOrder: (firstDelta, secondDelta) ->
|
||||
if firstDelta.insert? and secondDelta.insert?
|
||||
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? and secondDelta.remove?
|
||||
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? and secondDelta.remove?
|
||||
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? and secondDelta.insert?
|
||||
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) ->
|
||||
doc = new Doc(docLines.join("\n"))
|
||||
doc.applyDeltas(deltas)
|
||||
return doc.getAllLines()
|
||||
|
||||
_revertAceDeltaSetsOnDocLines: (deltaSets, docLines) ->
|
||||
session = new EditSession(docLines.join("\n"))
|
||||
session.undoChanges(deltaSets)
|
||||
return session.getDocument().getAllLines()
|
||||
|
||||
_revertSimpleDeltaSetsOnDocLines: (deltaSets, docLines) ->
|
||||
doc = docLines.join("\n")
|
||||
for deltaSet in deltaSets.slice(0).reverse()
|
||||
for delta in deltaSet.deltas.slice(0).reverse()
|
||||
if delta.remove?
|
||||
doc = doc.slice(0, delta.position) + delta.remove + doc.slice(delta.position)
|
||||
else if delta.insert?
|
||||
doc = doc.slice(0, delta.position) + doc.slice(delta.position + delta.insert.length)
|
||||
else
|
||||
throw "Unknown delta type"
|
||||
return doc.split("\n")
|
||||
|
||||
_aceDeltaSetsToSimpleDeltaSets: (aceDeltaSets, docLines) ->
|
||||
for deltaSet in aceDeltaSets
|
||||
simpleDeltas = []
|
||||
for delta in deltaSet.deltas
|
||||
simpleDeltas.push @_aceDeltaToSimpleDelta(delta, docLines)
|
||||
docLines = @_applyAceDeltasToDocLines([delta], docLines)
|
||||
{
|
||||
deltas: simpleDeltas
|
||||
group: deltaSet.group
|
||||
}
|
||||
|
||||
_simpleDeltaSetsToAceDeltaSets: (simpleDeltaSets, docLines) ->
|
||||
for deltaSet in simpleDeltaSets
|
||||
aceDeltas = []
|
||||
for delta in deltaSet.deltas
|
||||
newAceDeltas = @_simpleDeltaToAceDeltas(delta, docLines)
|
||||
docLines = @_applyAceDeltasToDocLines(newAceDeltas, docLines)
|
||||
aceDeltas = aceDeltas.concat newAceDeltas
|
||||
{
|
||||
deltas: aceDeltas
|
||||
group: deltaSet.group
|
||||
}
|
||||
|
||||
_aceDeltaToSimpleDelta: (aceDelta, docLines) ->
|
||||
start = aceDelta.range.start
|
||||
linesBefore = docLines.slice(0, start.row)
|
||||
position =
|
||||
linesBefore.join("").length + # full lines
|
||||
linesBefore.length + # new line characters
|
||||
start.column # partial line
|
||||
switch aceDelta.action
|
||||
when "insertText"
|
||||
return {
|
||||
position: position
|
||||
insert: aceDelta.text
|
||||
}
|
||||
when "insertLines"
|
||||
return {
|
||||
position: position
|
||||
insert: aceDelta.lines.join("\n") + "\n"
|
||||
}
|
||||
when "removeText"
|
||||
return {
|
||||
position: position
|
||||
remove: aceDelta.text
|
||||
}
|
||||
when "removeLines"
|
||||
return {
|
||||
position: position
|
||||
remove: aceDelta.lines.join("\n") + "\n"
|
||||
}
|
||||
else
|
||||
throw "Unknown Ace action: #{aceDelta.action}"
|
||||
|
||||
_simplePositionToAcePosition: (position, docLines) ->
|
||||
column = 0
|
||||
row = 0
|
||||
for line in docLines
|
||||
if position > line.length
|
||||
row++
|
||||
position -= (line + "\n").length
|
||||
else
|
||||
column = position
|
||||
break
|
||||
return {row: row, column: column}
|
||||
|
||||
_textToAceActions: (simpleText, row, column, type) ->
|
||||
aceDeltas = []
|
||||
lines = simpleText.split("\n")
|
||||
|
||||
range = (options) -> new Range(options.start.row, options.start.column, options.end.row, options.end.column)
|
||||
|
||||
do stripFirstLine = () ->
|
||||
firstLine = lines.shift()
|
||||
if firstLine.length > 0
|
||||
aceDeltas.push {
|
||||
text: firstLine
|
||||
range: range(
|
||||
start: column: column, row: row
|
||||
end: column: column + firstLine.length, row: row
|
||||
)
|
||||
action: "#{type}Text"
|
||||
}
|
||||
column += firstLine.length
|
||||
|
||||
do stripFirstNewLine = () ->
|
||||
if lines.length > 0
|
||||
aceDeltas.push {
|
||||
text: "\n"
|
||||
range: range(
|
||||
start: column: column, row: row
|
||||
end: column: 0, row: row + 1
|
||||
)
|
||||
action: "#{type}Text"
|
||||
}
|
||||
row += 1
|
||||
|
||||
do stripMiddleFullLines = () ->
|
||||
middleLines = lines.slice(0, -1)
|
||||
if middleLines.length > 0
|
||||
aceDeltas.push {
|
||||
lines: middleLines
|
||||
range: range(
|
||||
start: column: 0, row: row
|
||||
end: column: 0, row: row + middleLines.length
|
||||
)
|
||||
action: "#{type}Lines"
|
||||
}
|
||||
row += middleLines.length
|
||||
|
||||
do stripLastLine = () ->
|
||||
if lines.length > 0
|
||||
lastLine = lines.pop()
|
||||
aceDeltas.push {
|
||||
text: lastLine
|
||||
range: range(
|
||||
start: column: 0, row: row
|
||||
end: column: lastLine.length , row: row
|
||||
)
|
||||
action: "#{type}Text"
|
||||
}
|
||||
|
||||
return aceDeltas
|
||||
|
||||
|
||||
_simpleDeltaToAceDeltas: (simpleDelta, docLines) ->
|
||||
{row, column} = @_simplePositionToAcePosition(simpleDelta.position, docLines)
|
||||
|
||||
if simpleDelta.insert?
|
||||
return @_textToAceActions(simpleDelta.insert, row, column, "insert")
|
||||
if simpleDelta.remove?
|
||||
return @_textToAceActions(simpleDelta.remove, row, column, "remove").reverse()
|
||||
else
|
||||
throw "Unknown simple delta: #{simpleDelta}"
|
||||
|
||||
_concatSimpleDeltas: (deltas) ->
|
||||
return [] if deltas.length == 0
|
||||
|
||||
concattedDeltas = []
|
||||
previousDelta = deltas.shift()
|
||||
for delta in deltas
|
||||
if delta.insert? and previousDelta.insert?
|
||||
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? and previousDelta.remove?
|
||||
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: () -> @undoStack.length > 0
|
||||
hasRedo: () -> @redoStack.length > 0
|
||||
|
||||
+3
@@ -7,6 +7,9 @@ define [
|
||||
entity.selected = false
|
||||
$scope.entity.selected = true
|
||||
|
||||
if ($scope.entity.type == "doc")
|
||||
ide.editorManager.openDoc($scope.entity)
|
||||
|
||||
$scope.inputs =
|
||||
name: $scope.entity.name
|
||||
|
||||
|
||||
@@ -7,5 +7,14 @@ define [
|
||||
ide = {}
|
||||
ide.$http = $http
|
||||
|
||||
ide.pushEvent = () ->
|
||||
console.log "PUSHING EVENT STUB", arguments
|
||||
|
||||
ide.reportError = () ->
|
||||
console.log "REPORTING ERROR STUB", arguments
|
||||
|
||||
ide.showGenericServerErrorMessage = () ->
|
||||
console.error "GENERIC SERVER ERROR MESSAGE STUB"
|
||||
|
||||
return ide
|
||||
]
|
||||
|
||||
@@ -4,8 +4,7 @@ define [
|
||||
"main/account-settings"
|
||||
"directives/asyncForm"
|
||||
"directives/stopPropagation"
|
||||
"directives/focusInput"
|
||||
"directives/focusOn"
|
||||
"directives/focus"
|
||||
"directives/equals"
|
||||
"directives/fineUpload"
|
||||
"directives/onEnter"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
define [], () ->
|
||||
class EventEmitter
|
||||
on: (event, callback) ->
|
||||
@events ||= {}
|
||||
[event, namespace] = event.split(".")
|
||||
@events[event] ||= []
|
||||
@events[event].push {
|
||||
callback: callback
|
||||
namespace: namespace
|
||||
}
|
||||
|
||||
off: (event) ->
|
||||
@events ||= {}
|
||||
if event?
|
||||
[event, namespace] = event.split(".")
|
||||
if !namespace?
|
||||
# Clear all listeners for event
|
||||
delete @events[event]
|
||||
else
|
||||
# Clear only namespaced listeners
|
||||
remaining_events = []
|
||||
for callback in @events[event] or []
|
||||
if callback.namespace != namespace
|
||||
remaining_events.push callback
|
||||
@events[event] = remaining_events
|
||||
else
|
||||
# Remove all listeners
|
||||
@events = {}
|
||||
|
||||
trigger: (event, args...) ->
|
||||
@events ||= {}
|
||||
for callback in @events[event] or []
|
||||
callback.callback(args...)
|
||||
@@ -62,11 +62,13 @@ window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents) ->
|
||||
# Should probably also replace the editor text with the doc snapshot.
|
||||
, 0
|
||||
|
||||
if keepEditorContents
|
||||
doc.del 0, doc.getText().length
|
||||
doc.insert 0, editorDoc.getValue()
|
||||
else
|
||||
editorDoc.setValue doc.getText()
|
||||
# MODIFIED by James: We will set the doc contents ourselves to
|
||||
# avoid an extra entry in the undo stack.
|
||||
# if keepEditorContents
|
||||
# doc.del 0, doc.getText().length
|
||||
# doc.insert 0, editorDoc.getValue()
|
||||
# else
|
||||
# editorDoc.setValue doc.getText()
|
||||
|
||||
check()
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
|
||||
#file-tree {
|
||||
background-color: #fafafa;
|
||||
border-right: 1px solid @toolbar-border-color;
|
||||
|
||||
ul.file-tree-list {
|
||||
font-size: 0.8rem;
|
||||
@@ -153,9 +152,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
#editor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
// The internal components of the aceEditor directive
|
||||
.ace-editor-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
.undo-conflict-warning {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.ace-editor-body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-layout-resizer {
|
||||
width: 6px;
|
||||
background-color: #f4f4f4;
|
||||
border-left: 1px solid @toolbar-border-color;
|
||||
border-right: 1px solid @toolbar-border-color;
|
||||
.ui-layout-toggler {
|
||||
color: #999;
|
||||
font-family: FontAwesome;
|
||||
|
||||
Reference in New Issue
Block a user