Refactor ops model so it all happens in Document

This commit is contained in:
James Allen 2016-12-13 17:34:29 +00:00
parent 0a6a6c3c28
commit 898277b4af
8 changed files with 115 additions and 74 deletions

View file

@ -53,7 +53,6 @@ div.full-size(
events-bridge="reviewPanelEventsBridge" events-bridge="reviewPanelEventsBridge"
track-changes-enabled="trackChangesFeatureFlag", track-changes-enabled="trackChangesFeatureFlag",
track-changes= "editor.trackChanges", track-changes= "editor.trackChanges",
ranges-tracker="reviewPanel.rangesTracker",
doc-id="editor.open_doc_id" doc-id="editor.open_doc_id"
) )

View file

@ -1,7 +1,8 @@
define [ define [
"utils/EventEmitter" "utils/EventEmitter"
"ide/editor/ShareJsDoc" "ide/editor/ShareJsDoc"
], (EventEmitter, ShareJsDoc) -> "ide/review-panel/RangesTracker"
], (EventEmitter, ShareJsDoc, RangesTracker) ->
class Document extends EventEmitter class Document extends EventEmitter
@getDocument: (ide, doc_id) -> @getDocument: (ide, doc_id) ->
@openDocs ||= {} @openDocs ||= {}
@ -247,14 +248,15 @@ define [
@joined = true @joined = true
@doc.catchUp( updates ) @doc.catchUp( updates )
# TODO: Worry about whether these ranges are consistent with the doc still # TODO: Worry about whether these ranges are consistent with the doc still
@opening_ranges = ranges @ranges?.changes = ranges?.changes
@ranges?.comments = ranges?.comments
callback() callback()
else else
@ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version, updates, ranges) => @ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version, updates, ranges) =>
return callback(error) if error? return callback(error) if error?
@joined = true @joined = true
@doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket @doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket
@opening_ranges = ranges @ranges = new RangesTracker(ranges?.changes, ranges?.comments)
@_bindToShareJsDocEvents() @_bindToShareJsDocEvents()
callback() callback()
@ -313,6 +315,8 @@ define [
inflightOp: inflightOp, inflightOp: inflightOp,
pendingOp: pendingOp pendingOp: pendingOp
v: version v: version
@doc.on "change", (ops, oldSnapshot, msg) =>
@_applyOpsToRanges(ops, oldSnapshot, msg)
_onError: (error, meta = {}) -> _onError: (error, meta = {}) ->
meta.doc_id = @doc_id meta.doc_id = @doc_id
@ -325,3 +329,16 @@ define [
# the disconnect event, which means we try to leaveDoc when the connection comes back. # the disconnect event, which means we try to leaveDoc when the connection comes back.
# This could intefere with the new connection of a new instance of this document. # This could intefere with the new connection of a new instance of this document.
@_cleanUp() @_cleanUp()
_applyOpsToRanges: (ops = [], oldSnapshot, msg) ->
track_changes_as = null
remote_op = msg?
if remote_op and msg.meta?.tc
track_changes_as = msg.meta.user_id
else if !remote_op and @track_changes_as?
track_changes_as = @track_changes_as
console.log "CHANGED", oldSnapshot, ops, track_changes_as
@ranges.track_changes = track_changes_as?
for op in ops
console.log "APPLYING OP", op, @ranges.track_changes
@ranges.applyOp op, { user_id: track_changes_as }

View file

@ -34,8 +34,8 @@ define [
@_doc = new ShareJs.Doc @connection, @doc_id, @_doc = new ShareJs.Doc @connection, @doc_id,
type: @type type: @type
@_doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY) @_doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY)
@_doc.on "change", () => @_doc.on "change", (args...) =>
@trigger "change" @trigger "change", args...
@_doc.on "acknowledge", () => @_doc.on "acknowledge", () =>
@lastAcked = new Date() # note time of last ack from server for an op we sent @lastAcked = new Date() # note time of last ack from server for an op we sent
@trigger "acknowledge" @trigger "acknowledge"

View file

@ -56,7 +56,6 @@ define [
eventsBridge: "=" eventsBridge: "="
trackChanges: "=" trackChanges: "="
trackChangesEnabled: "=" trackChangesEnabled: "="
rangesTracker: "="
docId: "=" docId: "="
} }
link: (scope, element, attrs) -> link: (scope, element, attrs) ->

View file

@ -10,22 +10,15 @@ define [
constructor: (@$scope, @editor, @element) -> constructor: (@$scope, @editor, @element) ->
window.trackChangesManager ?= @ window.trackChangesManager ?= @
@$scope.$watch "rangesTracker", (rangesTracker) =>
return if !rangesTracker?
@disconnectFromRangesTracker()
@rangesTracker = rangesTracker
@connectToRangesTracker()
@$scope.$watch "trackChanges", (track_changes) => @$scope.$watch "trackChanges", (track_changes) =>
return if !track_changes? return if !track_changes?
@rangesTracker?.track_changes = track_changes @setTrackChanges(track_changes)
@$scope.$watch "sharejsDoc", (doc) => @$scope.$watch "sharejsDoc", (doc) =>
return if !doc? return if !doc?
if doc.opening_ranges?.changes? @disconnectFromRangesTracker()
@rangesTracker.changes = doc.opening_ranges.changes @rangesTracker = doc.ranges
if doc.opening_ranges?.comments? @connectToRangesTracker()
@rangesTracker.comments = doc.opening_ranges.comments
@$scope.$on "comment:add", (e, comment) => @$scope.$on "comment:add", (e, comment) =>
@addCommentToSelection(comment) @addCommentToSelection(comment)
@ -65,48 +58,15 @@ define [
onResize = () => onResize = () =>
@recalculateReviewEntriesScreenPositions() @recalculateReviewEntriesScreenPositions()
onChange = (e) =>
if !@editor.initing
# This change is trigger by a sharejs 'change' event, which is before the
# sharejs 'remoteop' event. So wait until the next event loop when the 'remoteop'
# will have fired, before we decide if it was a remote op.
setTimeout () =>
if @nextUpdateMetaData?
user_id = @nextUpdateMetaData.user_id
# The remote op may have contained multiple atomic ops, each of which is an Ace
# 'change' event (i.e. bulk commenting out of lines is a single remote op
# but gives us one event for each % inserted). These all come in a single event loop
# though, so wait until the next one before clearing the metadata.
setTimeout () =>
@nextUpdateMetaData = null
else
user_id = window.user.id
was_tracking = @rangesTracker.track_changes
if @dont_track_next_update
@rangesTracker.track_changes = false
@dont_track_next_update = false
@applyChange(e, { user_id })
@rangesTracker.track_changes = was_tracking
# TODO: Just for debugging, remove before going live.
setTimeout () =>
@checkMapping()
, 100
onChangeSession = (e) => onChangeSession = (e) =>
e.oldSession?.getDocument().off "change", onChange
e.session.getDocument().on "change", onChange
@redrawAnnotations() @redrawAnnotations()
bindToAce = () => bindToAce = () =>
@editor.getSession().getDocument().on "change", onChange
@editor.on "changeSelection", onChangeSelection @editor.on "changeSelection", onChangeSelection
@editor.on "changeSession", onChangeSession @editor.on "changeSession", onChangeSession
@editor.renderer.on "resize", onResize @editor.renderer.on "resize", onResize
unbindFromAce = () => unbindFromAce = () =>
@editor.getSession().getDocument().off "change", onChange
@editor.off "changeSelection", onChangeSelection @editor.off "changeSelection", onChangeSelection
@editor.off "changeSession", onChangeSession @editor.off "changeSession", onChangeSession
@editor.renderer.off "resize", onResize @editor.renderer.off "resize", onResize
@ -133,40 +93,48 @@ define [
@rangesTracker.off "comment:resolved" @rangesTracker.off "comment:resolved"
@rangesTracker.off "comment:unresolved" @rangesTracker.off "comment:unresolved"
connectToRangesTracker: () -> setTrackChanges: (value) ->
@rangesTracker.track_changes = @$scope.trackChanges if value
@$scope.sharejsDoc?.track_changes_as = window.user.id
else
@$scope.sharejsDoc?.track_changes_as = null
connectToRangesTracker: () ->
@setTrackChanges(@$scope.trackChanges)
# Add a timeout because on remote ops, we get these notifications before
# ace has updated
@rangesTracker.on "insert:added", (change) => @rangesTracker.on "insert:added", (change) =>
sl_console.log "[insert:added]", change sl_console.log "[insert:added]", change
@_onInsertAdded(change) setTimeout () => @_onInsertAdded(change)
@rangesTracker.on "insert:removed", (change) => @rangesTracker.on "insert:removed", (change) =>
sl_console.log "[insert:removed]", change sl_console.log "[insert:removed]", change
@_onInsertRemoved(change) setTimeout () => @_onInsertRemoved(change)
@rangesTracker.on "delete:added", (change) => @rangesTracker.on "delete:added", (change) =>
sl_console.log "[delete:added]", change sl_console.log "[delete:added]", change
@_onDeleteAdded(change) setTimeout () => @_onDeleteAdded(change)
@rangesTracker.on "delete:removed", (change) => @rangesTracker.on "delete:removed", (change) =>
sl_console.log "[delete:removed]", change sl_console.log "[delete:removed]", change
@_onDeleteRemoved(change) setTimeout () => @_onDeleteRemoved(change)
@rangesTracker.on "changes:moved", (changes) => @rangesTracker.on "changes:moved", (changes) =>
sl_console.log "[changes:moved]", changes sl_console.log "[changes:moved]", changes
@_onChangesMoved(changes) setTimeout () => @_onChangesMoved(changes)
@rangesTracker.on "comment:added", (comment) => @rangesTracker.on "comment:added", (comment) =>
sl_console.log "[comment:added]", comment sl_console.log "[comment:added]", comment
@_onCommentAdded(comment) setTimeout () => @_onCommentAdded(comment)
@rangesTracker.on "comment:moved", (comment) => @rangesTracker.on "comment:moved", (comment) =>
sl_console.log "[comment:moved]", comment sl_console.log "[comment:moved]", comment
@_onCommentMoved(comment) setTimeout () => @_onCommentMoved(comment)
@rangesTracker.on "comment:removed", (comment) => @rangesTracker.on "comment:removed", (comment) =>
sl_console.log "[comment:removed]", comment sl_console.log "[comment:removed]", comment
@_onCommentRemoved(comment) setTimeout () => @_onCommentRemoved(comment)
@rangesTracker.on "comment:resolved", (comment) => @rangesTracker.on "comment:resolved", (comment) =>
sl_console.log "[comment:resolved]", comment sl_console.log "[comment:resolved]", comment
@_onCommentRemoved(comment) setTimeout () => @_onCommentRemoved(comment)
@rangesTracker.on "comment:unresolved", (comment) => @rangesTracker.on "comment:unresolved", (comment) =>
sl_console.log "[comment:unresolved]", comment sl_console.log "[comment:unresolved]", comment
@_onCommentAdded(comment) setTimeout () => @_onCommentAdded(comment)
redrawAnnotations: () -> redrawAnnotations: () ->
for change in @rangesTracker.changes for change in @rangesTracker.changes

View file

@ -71,7 +71,7 @@ class Doc
# Its important that these event handlers are called with oldSnapshot. # Its important that these event handlers are called with oldSnapshot.
# The reason is that the OT type APIs might need to access the snapshots to # The reason is that the OT type APIs might need to access the snapshots to
# determine information about the received op. # determine information about the received op.
@emit 'change', docOp, oldSnapshot @emit 'change', docOp, oldSnapshot, msg
@emit 'remoteop', docOp, oldSnapshot, msg if isRemote @emit 'remoteop', docOp, oldSnapshot, msg if isRemote
_connectionStateChanged: (state, data) -> _connectionStateChanged: (state, data) ->
@ -274,6 +274,7 @@ class Doc
submitOp: (op, callback) -> submitOp: (op, callback) ->
op = @type.normalize(op) if @type.normalize? op = @type.normalize(op) if @type.normalize?
oldSnapshot = @snapshot
# If this throws an exception, no changes should have been made to the doc # If this throws an exception, no changes should have been made to the doc
@snapshot = @type.apply @snapshot, op @snapshot = @type.apply @snapshot, op
@ -284,7 +285,7 @@ class Doc
@pendingCallbacks.push callback if callback @pendingCallbacks.push callback if callback
@emit 'change', op @emit 'change', op, oldSnapshot
@delayedFlush() @delayedFlush()

View file

@ -31,7 +31,8 @@ checkValidComponent = (c) ->
i_type = typeof c.i i_type = typeof c.i
d_type = typeof c.d d_type = typeof c.d
throw new Error 'component needs an i or d field' unless (i_type == 'string') ^ (d_type == 'string') c_type = typeof c.c
throw new Error 'component needs an i, d or c field' unless (i_type == 'string') ^ (d_type == 'string') ^ (c_type == 'string')
throw new Error 'position cannot be negative' unless c.p >= 0 throw new Error 'position cannot be negative' unless c.p >= 0
@ -44,11 +45,15 @@ text.apply = (snapshot, op) ->
for component in op for component in op
if component.i? if component.i?
snapshot = strInject snapshot, component.p, component.i snapshot = strInject snapshot, component.p, component.i
else else if component.d?
deleted = snapshot[component.p...(component.p + component.d.length)] deleted = snapshot[component.p...(component.p + component.d.length)]
throw new Error "Delete component '#{component.d}' does not match deleted text '#{deleted}'" unless component.d == deleted throw new Error "Delete component '#{component.d}' does not match deleted text '#{deleted}'" unless component.d == deleted
snapshot = snapshot[...component.p] + snapshot[(component.p + component.d.length)..] snapshot = snapshot[...component.p] + snapshot[(component.p + component.d.length)..]
else if component.c?
comment = snapshot[component.p...(component.p + component.c.length)]
throw new Error "Comment component '#{component.c}' does not match commented text '#{comment}'" unless component.c == comment
else
throw new Error "Unknown op type"
snapshot snapshot
@ -112,7 +117,7 @@ transformPosition = (pos, c, insertAfter) ->
pos + c.i.length pos + c.i.length
else else
pos pos
else else if c.d?
# I think this could also be written as: Math.min(c.p, Math.min(c.p - otherC.p, otherC.d.length)) # I think this could also be written as: Math.min(c.p, Math.min(c.p - otherC.p, otherC.d.length))
# but I think its harder to read that way, and it compiles using ternary operators anyway # but I think its harder to read that way, and it compiles using ternary operators anyway
# so its no slower written like this. # so its no slower written like this.
@ -122,6 +127,10 @@ transformPosition = (pos, c, insertAfter) ->
c.p c.p
else else
pos - c.d.length pos - c.d.length
else if c.c?
pos
else
throw new Error("unknown op type")
# Helper method to transform a cursor position as a result of an op. # Helper method to transform a cursor position as a result of an op.
# #
@ -143,7 +152,7 @@ text._tc = transformComponent = (dest, c, otherC, side) ->
if c.i? if c.i?
append dest, {i:c.i, p:transformPosition(c.p, otherC, side == 'right')} append dest, {i:c.i, p:transformPosition(c.p, otherC, side == 'right')}
else # Delete else if c.d? # Delete
if otherC.i? # delete vs insert if otherC.i? # delete vs insert
s = c.d s = c.d
if c.p < otherC.p if c.p < otherC.p
@ -152,7 +161,7 @@ text._tc = transformComponent = (dest, c, otherC, side) ->
if s != '' if s != ''
append dest, {d:s, p:c.p + otherC.i.length} append dest, {d:s, p:c.p + otherC.i.length}
else # Delete vs delete else if otherC.d? # Delete vs delete
if c.p >= otherC.p + otherC.d.length if c.p >= otherC.p + otherC.d.length
append dest, {d:c.d, p:c.p - otherC.d.length} append dest, {d:c.d, p:c.p - otherC.d.length}
else if c.p + c.d.length <= otherC.p else if c.p + c.d.length <= otherC.p
@ -178,6 +187,51 @@ text._tc = transformComponent = (dest, c, otherC, side) ->
newC.p = transformPosition newC.p, otherC newC.p = transformPosition newC.p, otherC
append dest, newC append dest, newC
else if otherC.c?
append dest, c
else
throw new Error("unknown op type")
else if c.c? # Comment
if otherC.i?
if c.p < otherC.p < c.p + c.c.length
offset = otherC.p - c.p
new_c = (c.c[0..(offset-1)] + otherC.i + c.c[offset...])
append dest, {c:new_c, p:c.p, t: c.t}
else
append dest, {c:c.c, p:transformPosition(c.p, otherC, true), t: c.t}
else if otherC.d?
if c.p >= otherC.p + otherC.d.length
append dest, {c:c.c, p:c.p - otherC.d.length, t: c.t}
else if c.p + c.c.length <= otherC.p
append dest, c
else # Delete overlaps comment
# They overlap somewhere.
newC = {c:'', p:c.p, t: c.t}
if c.p < otherC.p
newC.c = c.c[...(otherC.p - c.p)]
if c.p + c.c.length > otherC.p + otherC.d.length
newC.c += c.c[(otherC.p + otherC.d.length - c.p)..]
# This is entirely optional - just for a check that the deleted
# text in the two ops matches
intersectStart = Math.max c.p, otherC.p
intersectEnd = Math.min c.p + c.c.length, otherC.p + otherC.d.length
cIntersect = c.c[intersectStart - c.p...intersectEnd - c.p]
otherIntersect = otherC.d[intersectStart - otherC.p...intersectEnd - otherC.p]
throw new Error 'Delete ops delete different text in the same region of the document' unless cIntersect == otherIntersect
newC.p = transformPosition newC.p, otherC
append dest, newC
else if otherC.c?
append dest, c
else
throw new Error("unknown op type")
dest dest
invertComponent = (c) -> invertComponent = (c) ->

View file

@ -154,10 +154,13 @@ define [
if view == $scope.SubViews.OVERVIEW if view == $scope.SubViews.OVERVIEW
refreshOverviewPanel() refreshOverviewPanel()
$scope.$watch "editor.open_doc_id", (open_doc_id) -> $scope.$watch "editor.sharejs_doc", (doc) ->
return if !open_doc_id? return if !doc?
rangesTrackers[open_doc_id] ?= new RangesTracker() console.log "DOC changed", doc
$scope.reviewPanel.rangesTracker = rangesTrackers[open_doc_id] # The open doc range tracker is kept up to date in real-time so
# replace any outdated info with this
rangesTrackers[doc.doc_id] = doc.ranges
$scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id]
$scope.$watch (() -> $scope.$watch (() ->
entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {} entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {}