Add in undo manager

This commit is contained in:
James Allen
2014-06-24 15:31:44 +01:00
parent 84f998ba4a
commit a1b715d1e9
14 changed files with 982 additions and 11 deletions
+4 -2
View File
@@ -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
@@ -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
]
+1 -2
View File
@@ -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;