diff --git a/spec/app/edit-session-replication-spec.coffee b/spec/app/edit-session-replication-spec.coffee index 38833ed27..25e6434ea 100644 --- a/spec/app/edit-session-replication-spec.coffee +++ b/spec/app/edit-session-replication-spec.coffee @@ -1,32 +1,56 @@ {Site} = require 'telepath' Editor = require 'editor' +Environment = require 'environment' describe "EditSession replication", -> - [editSession1, editSession2] = [] + [env1, env2, editSession1, editSession2] = [] beforeEach -> - editSession1 = project.open('sample.js') - editSession1.setScrollTop(5) - editSession1.setScrollLeft(5) - editSession1.setCursorScreenPosition([0, 5]) - editSession1.addSelectionForBufferRange([[1, 2], [3, 4]]) + env1 = new Environment(siteId: 1) + env2 = env1.clone(siteId: 2) + envConnection = env1.connect(env2) + doc2 = null - doc1 = editSession1.getState() - doc2 = doc1.clone(new Site(2)) - doc1.connect(doc2) - editSession2 = deserialize(doc2) + env1.run -> + editSession1 = project.open('sample.js') + editSession1.setScrollTop(5) + editSession1.setScrollLeft(5) + editSession1.setCursorScreenPosition([0, 5]) + editSession1.addSelectionForBufferRange([[1, 2], [3, 4]]) + doc1 = editSession1.getState() + doc2 = doc1.clone(env2.site) + envConnection.connect(doc1, doc2) - it "replicates the initial selections", -> - expect(editSession2.getSelectedBufferRanges()).toEqual editSession1.getSelectedBufferRanges() + env2.run -> + editSession2 = deserialize(doc2) + + afterEach -> + env1.destroy() + env2.destroy() + + it "replicates the selections of existing replicas", -> + expect(editSession2.getRemoteSelectedBufferRanges()).toEqual editSession1.getSelectedBufferRanges() - it "replicates changes to selections", -> editSession1.getLastSelection().setBufferRange([[2, 3], [4, 5]]) - expect(editSession2.getLastSelection().getBufferRange()).toEqual [[2, 3], [4, 5]] + expect(editSession2.getRemoteSelectedBufferRanges()).toEqual editSession1.getSelectedBufferRanges() editSession1.addCursorAtBufferPosition([5, 6]) - expect(editSession2.getSelectedBufferRanges()).toEqual editSession1.getSelectedBufferRanges() + expect(editSession2.getRemoteSelectedBufferRanges()).toEqual editSession1.getSelectedBufferRanges() editSession1.consolidateSelections() - expect(editSession2.getSelectedBufferRanges()).toEqual editSession1.getSelectedBufferRanges() + expect(editSession2.getRemoteSelectedBufferRanges()).toEqual editSession1.getSelectedBufferRanges() + + it "introduces a local cursor for a new replica at the position of the last remote cursor", -> + expect(editSession2.getCursors().length).toBe 1 + expect(editSession2.getSelections().length).toBe 1 + expect(editSession2.getCursorBufferPosition()).toEqual [3, 4] + expect(editSession2.getSelectedBufferRanges()).toEqual [[[3, 4], [3, 4]]] + + expect(editSession1.getRemoteCursors().length).toBe 1 + expect(editSession1.getRemoteSelections().length).toBe 1 + [cursor] = editSession1.getRemoteCursors() + [selection] = editSession1.getRemoteSelections() + expect(cursor.getBufferPosition()).toEqual [3, 4] + expect(selection.getBufferRange()).toEqual [[3, 4], [3, 4]] it "replicates the scroll position", -> expect(editSession2.getScrollTop()).toBe editSession1.getScrollTop() diff --git a/spec/environment.coffee b/spec/environment.coffee new file mode 100644 index 000000000..1e78d4074 --- /dev/null +++ b/spec/environment.coffee @@ -0,0 +1,58 @@ +{Site} = require 'telepath' +fsUtils = require 'fs-utils' +Project = require 'project' + +module.exports = +class Environment + constructor: ({@site, @state, siteId, projectPath}={}) -> + @site ?= new Site(siteId ? 1) + if @state? + @run => @project = deserialize(@state.get('project')) + else + @state = @site.createDocument({}) + @project = new Project(projectPath ? fsUtils.resolveOnLoadPath('fixtures')) + @state.set(project: @project.getState()) + + clone: (params) -> + site = new Site(params.siteId) + new Environment(site: site, state: @state.clone(site)) + + destroy: -> + @project.destroy() + + getState: -> @state + + run: (fn) -> + uninstall = @install() + fn() + uninstall() + + install: -> + oldSite = window.site + oldProject = window.project + window.site = @site + window.project = @project + -> + window.site = oldSite + window.project = oldProject + + connect: (otherEnv) -> + new EnvironmentConnection(this, otherEnv) + + + connectDocuments: (docA, docB, envB) -> + +class EnvironmentConnection + constructor: (@envA, @envB) -> + @envA.getState().connect(@envB.getState()) + + connect: (docA, docB) -> + unless docA.site is @envA.site + throw new Error("Document and environment sites do not match (doc: site #{docA.site.id}, env: site #{@envA.site.id})") + unless docB.site is @envB.site + throw new Error("Document and environment sites do not match (doc: site #{docB.site.id}, env: site #{@envB.site.id})") + + connection = docA.connect(docB) + connection.abFilter = (fn) => @envB.run(fn) + connection.baFilter = (fn) => @envA.run(fn) + connection diff --git a/src/app/display-buffer-marker.coffee b/src/app/display-buffer-marker.coffee index 831621c0b..9ed38d552 100644 --- a/src/app/display-buffer-marker.coffee +++ b/src/app/display-buffer-marker.coffee @@ -141,6 +141,15 @@ class DisplayBufferMarker isDestroyed: -> @bufferMarker.isDestroyed() + getOriginSiteId: -> + @bufferMarker.getOriginSiteId() + + isLocal: -> + @bufferMarker.isLocal() + + isRemote: -> + @bufferMarker.isRemote() + getAttributes: -> @bufferMarker.getAttributes() diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index d09b2a68c..75dd08168 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -31,12 +31,16 @@ class EditSession languageMode: null displayBuffer: null cursors: null + remoteCursors: null selections: null + remoteSelections: null suppressSelectionMerging: false constructor: (optionsOrState) -> @cursors = [] + @remoteCursors = [] @selections = [] + @remoteSelections = [] if optionsOrState instanceof telepath.Document @state = optionsOrState @id = @state.get('id') @@ -64,7 +68,10 @@ class EditSession scrollLeft: 0 @setBuffer(buffer) @setDisplayBuffer(displayBuffer) - @addCursorAtScreenPosition([0, 0]) unless suppressCursorCreation + + if @getCursors().length is 0 and not suppressCursorCreation + position = _.last(@getRemoteCursors())?.getBufferPosition() ? [0, 0] + @addCursorAtBufferPosition(position) @languageMode = new LanguageMode(this, @buffer.getExtension()) @state.on 'changed', ({key, newValue}) => @@ -831,6 +838,8 @@ class EditSession getCursor: -> _.last(@cursors) + getRemoteCursors: -> new Array(@remoteCursors...) + # Adds a cursor at the provided `screenPosition`. # # screenPosition - An {Array} of two numbers: the screen row, and the screen column. @@ -856,7 +865,10 @@ class EditSession # Returns the new {Cursor}. addCursor: (marker) -> cursor = new Cursor(editSession: this, marker: marker) - @cursors.push(cursor) + if marker.isLocal() + @cursors.push(cursor) + else + @remoteCursors.push(cursor) @trigger 'cursor-added', cursor cursor @@ -879,7 +891,12 @@ class EditSession @destroyFoldsIntersectingBufferRange(marker.getBufferRange()) cursor = @addCursor(marker) selection = new Selection(_.extend({editSession: this, marker, cursor}, options)) - @selections.push(selection) + + if marker.isLocal() + @selections.push(selection) + else + @remoteSelections.push(selection) + selectionBufferRange = selection.getBufferRange() @mergeIntersectingSelections() if selection.destroyed @@ -929,7 +946,10 @@ class EditSession # # selection - The {Selection} to remove. removeSelection: (selection) -> - _.remove(@selections, selection) + if selection.isLocal() + _.remove(@selections, selection) + else + _.remove(@remoteSelections, selection) # Clears every selection. TODO clearSelections: -> @@ -964,14 +984,16 @@ class EditSession getLastSelection: -> _.last(@selections) + getRemoteSelections: -> new Array(@remoteSelections...) + # Gets all selections, ordered by their position in the buffer. # # Returns an {Array} of {Selection}s. getSelectionsOrderedByBufferPosition: -> - @getSelections().sort (a, b) -> - aRange = a.getBufferRange() - bRange = b.getBufferRange() - aRange.end.compare(bRange.end) + @getSelections().sort (a, b) -> a.compare(b) + + getRemoteSelectionsOrderedByBufferPosition: -> + @getRemoteSelections().sort (a, b) -> a.compare(b) # Gets the very last selection, as it's ordered in the buffer. # @@ -1042,6 +1064,9 @@ class EditSession getSelectedBufferRanges: -> selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition() + getRemoteSelectedBufferRanges: -> + selection.getBufferRange() for selection in @getRemoteSelectionsOrderedByBufferPosition() + # Gets the selected text of the most recently added {Selection}. # # Returns a {String}. diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 9add86d18..cbf3a05d1 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -524,6 +524,15 @@ class Selection @setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), options) otherSelection.destroy() + compare: (other) -> + @getBufferRange().compare(other.getBufferRange()) + + isLocal: -> + @marker.isLocal() + + isRemote: -> + @marker.isRemote() + ### Internal ### screenRangeChanged: -> diff --git a/vendor/telepath b/vendor/telepath index f3b548bf6..703dad9db 160000 --- a/vendor/telepath +++ b/vendor/telepath @@ -1 +1 @@ -Subproject commit f3b548bf69ecc9d4c791a1d12e4f63ac73ab50fd +Subproject commit 703dad9db8de361287806de54b558a4c8bea0b08