From 00b91821e8c7f7b8d46d4cf98c8fd71f362d940a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 21 Nov 2017 15:44:34 +0100 Subject: [PATCH 01/65] Leave editors open after host switches away from them --- lib/guest-portal-binding.js | 72 +++++++---- lib/host-portal-binding.js | 21 ++-- lib/teletype-package.js | 11 ++ test/guest-portal-binding.test.js | 11 +- test/portal-binding-manager.test.js | 10 ++ test/portal-list-component.test.js | 6 + test/teletype-package.test.js | 189 +++++++++++++--------------- 7 files changed, 174 insertions(+), 146 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index fc045c27..0afb5ccc 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -15,9 +15,9 @@ class GuestPortalBinding { this.activePaneItem = null this.editorBindingsByEditorProxy = new Map() this.bufferBindingsByBufferProxy = new Map() - this.addedPaneItems = new WeakSet() this.emitter = new Emitter() - this.lastSetActiveEditorProxyPromise = Promise.resolve() + this.lastEditorProxyChangePromise = Promise.resolve() + this.openEditorProxies = new Set() } async initialize () { @@ -25,7 +25,11 @@ class GuestPortalBinding { this.portal = await this.client.joinPortal(this.portalId) if (!this.portal) return false - this.portal.setDelegate(this) + await this.portal.setDelegate(this) + if (this.openEditorProxies.size === 0) { + await this.openPaneItem(this.getEmptyPortalPaneItem()) + } + return true } catch (error) { this.didFailToJoin(error) @@ -34,8 +38,7 @@ class GuestPortalBinding { } dispose () { - if (this.activePaneItemDestroySubscription) this.activePaneItemDestroySubscription.dispose() - if (this.activePaneItem) this.activePaneItem.destroy() + this.openEditorProxies.clear() if (this.emptyPortalItem) this.emptyPortalItem.destroy() this.emitDidDispose() } @@ -54,17 +57,32 @@ class GuestPortalBinding { this.emitter.emit('did-change') } - async setActiveEditorProxy (editorProxy) { - this.lastSetActiveEditorProxyPromise = this.lastSetActiveEditorProxyPromise.then(async () => { - if (editorProxy == null) { - await this.replaceActivePaneItem(this.getEmptyPortalPaneItem()) - } else { - const editor = this.findOrCreateEditorForEditorProxy(editorProxy) - await this.replaceActivePaneItem(editor) + activateEditorProxy (editorProxy) { + this.lastEditorProxyChangePromise = this.lastEditorProxyChangePromise.then(async () => { + if (this.openEditorProxies.size === 0) { + this.getEmptyPortalPaneItem().destroy() + } + this.openEditorProxies.add(editorProxy) + + const editor = this.findOrCreateEditorForEditorProxy(editorProxy) + await this.openPaneItem(editor) + }) + + return this.lastEditorProxyChangePromise + } + + removeEditorProxy (editorProxy) { + this.lastEditorProxyChangePromise = this.lastEditorProxyChangePromise.then(async () => { + const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) + editorBinding.editor.destroy() + + this.openEditorProxies.delete(editorProxy) + if (this.openEditorProxies.size === 0) { + await this.openPaneItem(this.getEmptyPortalPaneItem()) } }) - return this.lastSetActiveEditorProxyPromise + return this.lastEditorProxyChangePromise } // Private @@ -155,28 +173,28 @@ class GuestPortalBinding { } leave () { + this.editorBindingsByEditorProxy.forEach((binding) => { + binding.editor.destroy() + }) + if (this.portal) this.portal.dispose() } - async replaceActivePaneItem (newActivePaneItem) { + async openPaneItem (newActivePaneItem) { this.newActivePaneItem = newActivePaneItem - - if (this.activePaneItem) { - const pane = this.workspace.paneForItem(this.activePaneItem) - const index = pane.getItems().indexOf(this.activePaneItem) - pane.addItem(newActivePaneItem, {index, moved: this.addedPaneItems.has(newActivePaneItem)}) - pane.removeItem(this.activePaneItem) - } else { - await this.workspace.open(newActivePaneItem) - } - this.addedPaneItems.add(newActivePaneItem) - + await this.workspace.open(newActivePaneItem) this.activePaneItem = this.newActivePaneItem - if (this.activePaneItemDestroySubscription) this.activePaneItemDestroySubscription.dispose() - this.activePaneItemDestroySubscription = this.activePaneItem.onDidDestroy(this.leave.bind(this)) this.newActivePaneItem = null } + // Private + shouldShowEmptyPortalPaneItem () { + return ( + this.getActivePaneItem() !== this.getEmptyPortalPaneItem() && + this.editorBindingsByEditorProxy.size === 0 + ) + } + getActivePaneItem () { return this.newActivePaneItem ? this.newActivePaneItem : this.activePaneItem } diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index b18ed761..a1dccdf8 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -22,9 +22,10 @@ class HostPortalBinding { if (!this.portal) return false this.portal.setDelegate(this) - this.disposables.add(this.workspace.observeActiveTextEditor( - this.didChangeActiveTextEditor.bind(this) - )) + this.disposables.add( + this.workspace.observeActiveTextEditor(this.didChangeActiveTextEditor.bind(this)), + this.workspace.onDidDestroyPaneItem(this.didDestroyPaneItem.bind(this)) + ) this.workspace.getElement().classList.add('teletype-Host') return true @@ -64,10 +65,7 @@ class HostPortalBinding { } didChangeActiveTextEditor (editor) { - if (editor == null || editor.isRemote) { - this.portal.setActiveEditorProxy(null) - return - } + if (editor == null || editor.isRemote) return let editorBinding = this.editorBindingsByEditor.get(editor) if (!editorBinding) { @@ -98,7 +96,14 @@ class HostPortalBinding { this.editorBindingsByEditor.set(editor, editorBinding) } - this.portal.setActiveEditorProxy(editorBinding.editorProxy) + this.portal.activateEditorProxy(editorBinding.editorProxy) + } + + didDestroyPaneItem ({item}) { + const editorBinding = this.editorBindingsByEditor.get(item) + if (editorBinding) { + this.portal.removeEditorProxy(editorBinding.editorProxy) + } } getBufferProxyURI (buffer) { diff --git a/lib/teletype-package.js b/lib/teletype-package.js index 45a55246..3eb0d95f 100644 --- a/lib/teletype-package.js +++ b/lib/teletype-package.js @@ -50,6 +50,9 @@ class TeletypePackage { this.subscriptions.add(this.commandRegistry.add('atom-workspace', { 'teletype:join-portal': () => this.joinPortal() })) + this.subscriptions.add(this.commandRegistry.add('teletype-RemotePaneItem', { + 'teletype:leave-portal': () => this.leavePortal() + })) this.subscriptions.add(this.commandRegistry.add('atom-workspace.teletype-Host', { 'teletype:close-portal': () => this.closeHostPortal() })) @@ -101,6 +104,14 @@ class TeletypePackage { hostPortalBinding.close() } + async leavePortal () { + this.showPopover() + + const manager = await this.getPortalBindingManager() + const guestPortalBinding = await manager.getActiveGuestPortalBinding() + guestPortalBinding.leave() + } + async consumeStatusBar (statusBar) { const teletypeClient = await this.getClient() const portalBindingManager = await this.getPortalBindingManager() diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index 03c3519d..5a112d9e 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -78,18 +78,17 @@ suite('GuestPortalBinding', () => { activePaneItemChangeEvents.push(item) }) - portalBinding.setActiveEditorProxy(buildEditorProxy('uri-1')) - portalBinding.setActiveEditorProxy(buildEditorProxy('uri-2')) - portalBinding.setActiveEditorProxy(null) - await portalBinding.setActiveEditorProxy(buildEditorProxy('uri-3')) + portalBinding.activateEditorProxy(buildEditorProxy('uri-1')) + portalBinding.activateEditorProxy(buildEditorProxy('uri-2')) + await portalBinding.activateEditorProxy(buildEditorProxy('uri-3')) assert.deepEqual( activePaneItemChangeEvents.map((i) => i.getTitle()), - ['@some-host: uri-1', '@some-host: uri-2', '@some-host: No Active File', '@some-host: uri-3'] + ['@some-host: uri-1', '@some-host: uri-2', '@some-host: uri-3'] ) assert.deepEqual( atomEnv.workspace.getPaneItems().map((i) => i.getTitle()), - ['@some-host: uri-3'] + ['@some-host: uri-1', '@some-host: uri-2', '@some-host: uri-3'] ) disposable.dispose() diff --git a/test/portal-binding-manager.test.js b/test/portal-binding-manager.test.js index 819c59f8..2d998eb3 100644 --- a/test/portal-binding-manager.test.js +++ b/test/portal-binding-manager.test.js @@ -106,6 +106,9 @@ function buildPortalBindingManager () { const workspace = { element: document.createElement('div'), + async open () { + + }, getElement () { return this.element }, @@ -114,14 +117,21 @@ function buildPortalBindingManager () { }, observeActivePaneItem () { return new Disposable(() => {}) + }, + onDidDestroyPaneItem () { + return new Disposable(() => {}) } } return new PortalBindingManager({client, workspace, notificationManager}) } +let nextIdentityId = 1 function buildPortal () { return { + getSiteIdentity () { + return {login: 'identity-' + nextIdentityId++} + }, dispose () { this.delegate.dispose() }, diff --git a/test/portal-list-component.test.js b/test/portal-list-component.test.js index a666a14c..71fc904f 100644 --- a/test/portal-list-component.test.js +++ b/test/portal-list-component.test.js @@ -254,6 +254,12 @@ class FakeWorkspace { observeActiveTextEditor () { return new Disposable(() => {}) } + + onDidDestroyPaneItem () { + return new Disposable(() => {}) + } + + paneForItem () {} } class FakeNotificationManager { diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index c2aac6c3..11d590c2 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -106,6 +106,42 @@ suite('TeletypePackage', function () { assert.equal(observedGuestItems.size, 2) }) + test('opening and closing multiple editors on the host', async function () { + const hostEnv = buildAtomEnvironment() + const hostPackage = await buildPackage(hostEnv) + const guestEnv = buildAtomEnvironment() + const guestPackage = await buildPackage(guestEnv) + const portalId = (await hostPackage.sharePortal()).id + + guestPackage.joinPortal(portalId) + const emptyPortalPaneItem = await getNextRemotePaneItemPromise(guestEnv) + assert(emptyPortalPaneItem instanceof EmptyPortalPaneItem) + + const hostEditor1 = await hostEnv.workspace.open() + const guestEditor1 = await getNextActiveTextEditorPromise(guestEnv) + assert.equal(getPaneItems(guestEnv).length, 1) + + const hostEditor2 = await hostEnv.workspace.open() + const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) + assert.equal(getPaneItems(guestEnv).length, 2) + + hostEnv.workspace.paneForItem(hostEditor1).activateItem(hostEditor1) + assert.equal(await getNextActiveTextEditorPromise(guestEnv), guestEditor1) + assert.equal(getPaneItems(guestEnv).length, 2) + + hostEditor1.destroy() + await condition(() => getPaneItems(guestEnv).length === 1) + + hostEditor2.destroy() + assert.equal(await getNextRemotePaneItemPromise(guestEnv), emptyPortalPaneItem) + assert.equal(getPaneItems(guestEnv).length, 1) + + await hostEnv.workspace.open() + const guestEditor3 = await getNextRemotePaneItemPromise(guestEnv) + assert(guestEditor3 instanceof TextEditor) + assert.equal(getPaneItems(guestEnv).length, 1) + }) + test('host joining another portal as a guest', async () => { const hostAndGuestEnv = buildAtomEnvironment() const hostAndGuestPackage = await buildPackage(hostAndGuestEnv) @@ -131,10 +167,7 @@ suite('TeletypePackage', function () { // No transitivity: When Portal 1 host is viewing contents of Portal 2, Portal 1 guests are placed on hold assert.equal(hostAndGuestEnv.workspace.getActivePaneItem(), hostAndGuestRemotePaneItem) - await condition(() => - getRemotePaneItems(guestOnlyEnv).length === 1 && - getRemotePaneItems(guestOnlyEnv)[0] instanceof EmptyPortalPaneItem - ) + await condition(() => deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1])) }) test('guest sharing another portal as a host', async () => { @@ -153,8 +186,8 @@ suite('TeletypePackage', function () { assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1]) // While already participating as a guest in Portal 1, share a new portal as a host (Portal 2) - const guestAndHostLocalEditor = await guestAndHostEnv.workspace.open(path.join(temp.path(), 'host+guest')) - assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostLocalEditor]) + const guestAndHostLocalEditor1 = await guestAndHostEnv.workspace.open(path.join(temp.path(), 'host+guest-buffer-1')) + assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostLocalEditor1]) const portal2Id = (await guestAndHostPackage.sharePortal()).id guestOnlyPackage.joinPortal(portal2Id) const guestOnlyRemotePaneItem1 = await getNextRemotePaneItemPromise(guestOnlyEnv) @@ -164,25 +197,24 @@ suite('TeletypePackage', function () { // Portal 2 host continues to exist as a guest in Portal 1 hostOnlyEnv.workspace.open(path.join(temp.path(), 'host-only-buffer-2')) const guestAndHostRemotePaneItem2 = await getNextRemotePaneItemPromise(guestAndHostEnv) - assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem2, guestAndHostLocalEditor]) + assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2]) assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) - // No transitivity: When Portal 2 host is viewing contents of Portal 1, Portal 2 guests are placed on hold + // No transitivity: When Portal 2 host is viewing contents of Portal 1, Portal 2 guests can only see contents of Portal 2 guestAndHostEnv.workspace.getActivePane().activateItemAtIndex(0) - assert.equal(guestAndHostEnv.workspace.getActivePaneItem(), guestAndHostRemotePaneItem2) - await condition(() => - getRemotePaneItems(guestOnlyEnv).length === 1 && - getRemotePaneItems(guestOnlyEnv)[0] instanceof EmptyPortalPaneItem - ) + assert.equal(guestAndHostEnv.workspace.getActivePaneItem(), guestAndHostRemotePaneItem1) + assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) - // Portal 2 guests remain on hold while Portal 2 host observes changes in Portal 1 + // As Portal 2 host observes changes in Portal 1, Portal 2 guests continue to only see contents of Portal 2 await hostOnlyEnv.workspace.open(path.join(temp.path(), 'host-only-buffer-3')) const guestAndHostRemotePaneItem3 = await getNextRemotePaneItemPromise(guestAndHostEnv) - assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem3, guestAndHostLocalEditor]) - await condition(() => - getRemotePaneItems(guestOnlyEnv).length === 1 && - getRemotePaneItems(guestOnlyEnv)[0] instanceof EmptyPortalPaneItem - ) + assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostRemotePaneItem3, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2]) + assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) + + // When Portal 2 host shares another local buffer, Portal 2 guests see that buffer + const guestAndHostLocalEditor2 = await guestAndHostEnv.workspace.open(path.join(temp.path(), 'host+guest-buffer-2')) + const guestOnlyRemotePaneItem2 = await getNextRemotePaneItemPromise(guestOnlyEnv) + assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1, guestOnlyRemotePaneItem2]) }) test('host attempting to share another portal', async () => { @@ -336,9 +368,9 @@ suite('TeletypePackage', function () { assert(isTransmitting(guestStatusBar)) assert.equal(guestEnv.workspace.getPaneItems().length, 2) - guestEnv.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + await guestPackage.leavePortal() assert(isTransmitting(guestStatusBar)) - guestEnv.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + await guestPackage.leavePortal() await condition(() => !isTransmitting(guestStatusBar)) await host1Package.closeHostPortal() @@ -355,83 +387,21 @@ suite('TeletypePackage', function () { assert(errorNotification, 'Expected notifications to include "Portal not found" error') }) - test('preserving guest portal position in workspace', async function () { + test('guest leaving portal', async () => { const hostEnv = buildAtomEnvironment() const hostPackage = await buildPackage(hostEnv) const guestEnv = buildAtomEnvironment() const guestPackage = await buildPackage(guestEnv) - - const guestLocalEditor1 = await guestEnv.workspace.open(path.join(temp.path(), 'guest-1')) - assert.deepEqual(getPaneItems(guestEnv), [guestLocalEditor1]) - const portal = await hostPackage.sharePortal() await guestPackage.joinPortal(portal.id) - await hostEnv.workspace.open(path.join(temp.path(), 'host-1')) - const guestRemoteEditor1 = await getNextRemotePaneItemPromise(guestEnv) - const guestLocalEditor2 = await guestEnv.workspace.open(path.join(temp.path(), 'guest-2')) - assert.deepEqual(getPaneItems(guestEnv), [guestLocalEditor1, guestRemoteEditor1, guestLocalEditor2]) - - await hostEnv.workspace.open(path.join(temp.path(), 'host-2')) - const guestRemoteEditor2 = await getNextRemotePaneItemPromise(guestEnv) - - assert.deepEqual(getPaneItems(guestEnv), [guestLocalEditor1, guestRemoteEditor2, guestLocalEditor2]) - }) - - test('host without an active text editor', async function () { - const hostEnv = buildAtomEnvironment() - const hostPackage = await buildPackage(hostEnv) - const guestEnv = buildAtomEnvironment() - const guestPackage = await buildPackage(guestEnv) - const portalId = (await hostPackage.sharePortal()).id - - guestPackage.joinPortal(portalId) - let guestEditor = await getNextRemotePaneItemPromise(guestEnv) - assert(guestEditor instanceof EmptyPortalPaneItem) - - const hostEditor1 = await hostEnv.workspace.open() - guestEditor = await getNextRemotePaneItemPromise(guestEnv) - assert(guestEditor instanceof TextEditor) - - hostEditor1.destroy() - guestEditor = await getNextRemotePaneItemPromise(guestEnv) - assert(guestEditor instanceof EmptyPortalPaneItem) await hostEnv.workspace.open() - guestEditor = await getNextRemotePaneItemPromise(guestEnv) - assert(guestEditor instanceof TextEditor) - }) - - suite('guest leaving portal', async () => { - test('via closing text editor portal pane item', async () => { - const hostEnv = buildAtomEnvironment() - const hostPackage = await buildPackage(hostEnv) - const hostPortal = await hostPackage.sharePortal() - await hostEnv.workspace.open(path.join(temp.path(), 'some-file')) - - const guestEnv = buildAtomEnvironment() - const guestPackage = await buildPackage(guestEnv) - const guestPortal = await guestPackage.joinPortal(hostPortal.id) - - const guestEditor = getRemotePaneItems(guestEnv)[0] - assert(guestEditor instanceof TextEditor) - guestEnv.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - assert(guestPortal.disposed) - }) - - test('via closing empty portal pane item', async () => { - const hostEnv = buildAtomEnvironment() - const hostPackage = await buildPackage(hostEnv) - const hostPortal = await hostPackage.sharePortal() - - const guestEnv = buildAtomEnvironment() - const guestPackage = await buildPackage(guestEnv) - const guestPortal = await guestPackage.joinPortal(hostPortal.id) + await hostEnv.workspace.open() + await hostEnv.workspace.open() + await condition(() => getPaneItems(guestEnv).length === 3) - const guestEditor = getRemotePaneItems(guestEnv)[0] - assert(guestEditor instanceof EmptyPortalPaneItem) - guestEnv.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - assert(guestPortal.disposed) - }) + await guestPackage.leavePortal() + await condition(() => getPaneItems(guestEnv).length === 0) }) test('host closing portal', async function () { @@ -471,34 +441,43 @@ suite('TeletypePackage', function () { const hostEditor1 = await hostEnv.workspace.open(path.join(temp.path(), 'file-1')) hostEditor1.setText('const hello = "world"') hostEditor1.setCursorBufferPosition([0, 4]) - await getNextActiveTextEditorPromise(guestEnv) + const guestEditor1 = await getNextActiveTextEditorPromise(guestEnv) const hostEditor2 = await hostEnv.workspace.open(path.join(temp.path(), 'file-2')) hostEditor2.setText('const goodnight = "moon"') hostEditor2.setCursorBufferPosition([0, 2]) - await condition(() => guestEnv.workspace.getActiveTextEditor().getText() === 'const goodnight = "moon"') + const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) - const guestEditor = guestEnv.workspace.getActiveTextEditor() - await condition(() => deepEqual(getCursorDecoratedRanges(hostEditor2), getCursorDecoratedRanges(guestEditor))) - guestEditor.setCursorBufferPosition([0, 5]) + await condition(() => deepEqual(getCursorDecoratedRanges(hostEditor2), getCursorDecoratedRanges(guestEditor2))) + guestEditor2.setCursorBufferPosition([0, 5]) - const guestEditorTitleChangeEvents = [] - guestEditor.onDidChangeTitle((title) => guestEditorTitleChangeEvents.push(title)) + const guestEditor1TitleChangeEvents = [] + const guestEditor2TitleChangeEvents = [] + guestEditor1.onDidChangeTitle((title) => guestEditor1TitleChangeEvents.push(title)) + guestEditor2.onDidChangeTitle((title) => guestEditor2TitleChangeEvents.push(title)) hostPackage.closeHostPortal() - await condition(() => guestEditor.getTitle() === 'untitled') - assert.deepEqual(guestEditorTitleChangeEvents, ['untitled']) - assert.equal(guestEditor.getText(), 'const goodnight = "moon"') - assert(guestEditor.isModified()) - assert.deepEqual(getCursorDecoratedRanges(guestEditor), [ + await condition(() => guestEditor1.getTitle() === 'untitled' && guestEditor2.getTitle() === 'untitled') + + assert.deepEqual(guestEditor1TitleChangeEvents, ['untitled']) + assert.equal(guestEditor1.getText(), 'const hello = "world"') + assert(guestEditor1.isModified()) + assert.deepEqual(getCursorDecoratedRanges(guestEditor1), [ + {start: {row: 0, column: 4}, end: {row: 0, column: 4}} + ]) + + assert.deepEqual(guestEditor2TitleChangeEvents, ['untitled']) + assert.equal(guestEditor2.getText(), 'const goodnight = "moon"') + assert(guestEditor2.isModified()) + assert.deepEqual(getCursorDecoratedRanges(guestEditor2), [ {start: {row: 0, column: 5}, end: {row: 0, column: 5}} ]) // Ensure that the guest can still edit the buffer or modify selections. - guestEditor.getBuffer().setTextInRange([[0, 0], [0, 5]], 'let') - guestEditor.setCursorBufferPosition([0, 7]) - assert.equal(guestEditor.getText(), 'let goodnight = "moon"') - assert.deepEqual(getCursorDecoratedRanges(guestEditor), [ + guestEditor2.getBuffer().setTextInRange([[0, 0], [0, 5]], 'let') + guestEditor2.setCursorBufferPosition([0, 7]) + assert.equal(guestEditor2.getText(), 'let goodnight = "moon"') + assert.deepEqual(getCursorDecoratedRanges(guestEditor2), [ {start: {row: 0, column: 7}, end: {row: 0, column: 7}} ]) }) From 6949ff174c16ef9c7c88a119517427e3a6670e93 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Nov 2017 13:16:17 +0100 Subject: [PATCH 02/65] Delete unnecessary method --- lib/guest-portal-binding.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 0afb5ccc..3ac58368 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -187,14 +187,6 @@ class GuestPortalBinding { this.newActivePaneItem = null } - // Private - shouldShowEmptyPortalPaneItem () { - return ( - this.getActivePaneItem() !== this.getEmptyPortalPaneItem() && - this.editorBindingsByEditorProxy.size === 0 - ) - } - getActivePaneItem () { return this.newActivePaneItem ? this.newActivePaneItem : this.activePaneItem } From f0248532ee9846a0cce0bbab74c4931eb3961ec2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2017 09:26:47 +0100 Subject: [PATCH 03/65] Batch marker updates when updating tether This is a temporary stopgap measure until Atom supports batched marker update events. --- lib/editor-binding.js | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/editor-binding.js b/lib/editor-binding.js index e2e668fe..3f788d88 100644 --- a/lib/editor-binding.js +++ b/lib/editor-binding.js @@ -106,14 +106,14 @@ class EditorBinding { editor.emitter.emit('did-change-title', editor.getTitle()) } - observeMarker (marker, relayLocalSelections = true) { + observeMarker (marker, relay = true) { const didChangeDisposable = marker.onDidChange(({textChanged}) => { if (textChanged) { if (marker.getRange().isEmpty()) marker.clearTail() } else { - this.editorProxy.updateSelections({ + this.updateSelections({ [marker.id]: getSelectionState(marker) - }, this.preserveFollowState) + }) } }) const didDestroyDisposable = marker.onDidDestroy(() => { @@ -122,13 +122,18 @@ class EditorBinding { this.subscriptions.remove(didChangeDisposable) this.subscriptions.remove(didDestroyDisposable) - this.editorProxy.updateSelections({ + this.updateSelections({ [marker.id]: null - }, this.preserveFollowState) + }) }) this.subscriptions.add(didChangeDisposable) this.subscriptions.add(didDestroyDisposable) - if (relayLocalSelections) this.relayLocalSelections() + + if (relay) { + this.updateSelections({ + [marker.id]: getSelectionState(marker) + }) + } } async editorDidChangeScrollTop () { @@ -207,10 +212,7 @@ class EditorBinding { if (state === FollowState.RETRACTED) { this.editor.destroyFoldsIntersectingBufferRange(Range(position, position)) - - this.preserveFollowState = true - this.editor.setCursorBufferPosition(position) - this.preserveFollowState = false + this.batchMarkerUpdates(() => this.editor.setCursorBufferPosition(position)) localCursorDecorationProperties.style = {opacity: 0} } else { @@ -286,6 +288,22 @@ class EditorBinding { selectionUpdates[marker.id] = getSelectionState(marker) } this.editorProxy.updateSelections(selectionUpdates, initialUpdate) + + batchMarkerUpdates (fn) { + this.batchedMarkerUpdates = {} + this.isBatchingMarkerUpdates = true + fn() + this.isBatchingMarkerUpdates = false + this.editorProxy.updateSelections(this.batchedMarkerUpdates) + this.batchedMarkerUpdates = null + } + + updateSelections (update) { + if (this.isBatchingMarkerUpdates) { + Object.assign(this.batchedMarkerUpdates, update) + } else { + this.editorProxy.updateSelections(update) + } } buildSitePositionsComponent (position) { From c72abcf072f96a697ffc76836c8620c2817b244d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2017 09:27:35 +0100 Subject: [PATCH 04/65] Don't send selections twice when creating editor proxy on host --- lib/host-portal-binding.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index a1dccdf8..8145ed55 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -86,10 +86,7 @@ class HostPortalBinding { } editorBinding = new EditorBinding({editor, portal: this.portal, isHost: true}) - const editorProxy = this.portal.createEditorProxy({ - bufferProxy, - selections: editor.selectionsMarkerLayer.bufferMarkerLayer.createSnapshot() - }) + const editorProxy = this.portal.createEditorProxy({bufferProxy}) editorBinding.setEditorProxy(editorProxy) editorProxy.setDelegate(editorBinding) From b59dad5bb3781016a1bd370bb183feccb3d0bc2a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Nov 2017 09:28:19 +0100 Subject: [PATCH 05/65] Use the new initialUpdate parameter when first relaying local selections --- lib/editor-binding.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/editor-binding.js b/lib/editor-binding.js index 3f788d88..631d3e0b 100644 --- a/lib/editor-binding.js +++ b/lib/editor-binding.js @@ -59,7 +59,7 @@ class EditorBinding { this.subscriptions.add(this.editor.element.onDidChangeScrollTop(this.editorDidChangeScrollTop.bind(this))) this.subscriptions.add(this.editor.element.onDidChangeScrollLeft(this.editorDidChangeScrollLeft.bind(this))) this.subscriptions.add(subscribeToResizeEvents(this.editor.element, this.editorDidResize.bind(this))) - this.relayLocalSelections(true) + this.relayLocalSelections() this.aboveViewportSitePositionsComponent = this.buildSitePositionsComponent('upper-right') this.insideViewportSitePositionsComponent = this.buildSitePositionsComponent('middle-right') @@ -280,14 +280,15 @@ class EditorBinding { this.markersByLayerAndId.delete(markerLayer) } - relayLocalSelections (initialUpdate = false) { + relayLocalSelections () { const selectionUpdates = {} const selectionMarkers = this.selectionsMarkerLayer.getMarkers() for (let i = 0; i < selectionMarkers.length; i++) { const marker = selectionMarkers[i] selectionUpdates[marker.id] = getSelectionState(marker) } - this.editorProxy.updateSelections(selectionUpdates, initialUpdate) + this.updateSelections(selectionUpdates, {initialUpdate: true}) + } batchMarkerUpdates (fn) { this.batchedMarkerUpdates = {} From 2e8c77ba4958f0b631d8d4326807920250177bf1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 28 Nov 2017 16:26:47 +0100 Subject: [PATCH 06/65] Pin teletype-client@e5dd46f --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4350028c..a3192660 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "temp": "^0.8.3" }, "dependencies": { - "@atom/teletype-client": "^0.28.2", + "@atom/teletype-client": "https://github.com/atom/teletype-client#e5dd46f7ba13134d43411a2fa92b1b87061efac3", "etch": "^0.12.6" }, "consumedServices": { From be83bff07c81ea18f6ace8e94ad163312d075480 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 30 Nov 2017 11:01:59 -0500 Subject: [PATCH 07/65] Replace EditorBinding.isPositionVisible /w isScrollNeededToViewPosition --- lib/editor-binding.js | 6 ++++-- test/editor-binding.test.js | 18 +++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/editor-binding.js b/lib/editor-binding.js index 631d3e0b..62ef8345 100644 --- a/lib/editor-binding.js +++ b/lib/editor-binding.js @@ -203,8 +203,10 @@ class EditorBinding { } } - isPositionVisible (bufferPosition) { - return this.getDirectionFromViewportToPosition(bufferPosition) === 'inside' + isScrollNeededToViewPosition (position) { + const isPositionVisible = this.getDirectionFromViewportToPosition(position) === 'inside' + const isEditorAttachedToDOM = document.body.contains(this.editor.element) + return isEditorAttachedToDOM && !isPositionVisible } updateTether (state, position) { diff --git a/test/editor-binding.test.js b/test/editor-binding.test.js index 40b36f56..836df5d3 100644 --- a/test/editor-binding.test.js +++ b/test/editor-binding.test.js @@ -416,21 +416,25 @@ suite('EditorBinding', function () { assert.equal(editorProxy.getFollowedSiteId(), null) }) - test('isPositionVisible(position)', async () => { + test('isScrollNeededToViewPosition(position)', async () => { const editor = new TextEditor({autoHeight: false}) const binding = new EditorBinding({editor, portal: new FakePortal()}) const editorProxy = new FakeEditorProxy(binding) binding.setEditorProxy(editorProxy) + // If the editor is not yet attached to the DOM, scrolling isn't gonna help. + assert(!binding.isScrollNeededToViewPosition({row: 1, column: 0})) + assert(!binding.isScrollNeededToViewPosition({row: 0, column: 9})) + attachToDOM(editor.element) await setEditorHeightInLines(editor, 4) await setEditorWidthInChars(editor, 7) editor.setText('a pretty long line\n'.repeat(100)) - assert(binding.isPositionVisible({row: 1, column: 0})) - assert(!binding.isPositionVisible({row: 0, column: 9})) - assert(!binding.isPositionVisible({row: 6, column: 0})) + assert(!binding.isScrollNeededToViewPosition({row: 1, column: 0})) + assert(binding.isScrollNeededToViewPosition({row: 0, column: 9})) + assert(binding.isScrollNeededToViewPosition({row: 6, column: 0})) // Ensure text is rendered, so that we can scroll down/right. await editor.component.getNextUpdatePromise() @@ -438,9 +442,9 @@ suite('EditorBinding', function () { setEditorScrollTopInLines(editor, 5) setEditorScrollLeftInChars(editor, 5) - assert(binding.isPositionVisible({row: 6, column: 7})) - assert(!binding.isPositionVisible({row: 6, column: 0})) - assert(!binding.isPositionVisible({row: 3, column: 7})) + assert(!binding.isScrollNeededToViewPosition({row: 6, column: 7})) + assert(binding.isScrollNeededToViewPosition({row: 6, column: 0})) + assert(binding.isScrollNeededToViewPosition({row: 3, column: 7})) }) test('destroys folds intersecting the position of the leader', async () => { From 20017e25af543b158c094af26a5a7e35c577b722 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 30 Nov 2017 14:52:23 -0500 Subject: [PATCH 08/65] =?UTF-8?q?=F0=9F=91=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/teletype-package.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index 11d590c2..597bc570 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -122,7 +122,7 @@ suite('TeletypePackage', function () { assert.equal(getPaneItems(guestEnv).length, 1) const hostEditor2 = await hostEnv.workspace.open() - const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) + const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) // eslint-disable-line no-unused-vars assert.equal(getPaneItems(guestEnv).length, 2) hostEnv.workspace.paneForItem(hostEditor1).activateItem(hostEditor1) @@ -212,7 +212,7 @@ suite('TeletypePackage', function () { assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) // When Portal 2 host shares another local buffer, Portal 2 guests see that buffer - const guestAndHostLocalEditor2 = await guestAndHostEnv.workspace.open(path.join(temp.path(), 'host+guest-buffer-2')) + await guestAndHostEnv.workspace.open(path.join(temp.path(), 'host+guest-buffer-2')) const guestOnlyRemotePaneItem2 = await getNextRemotePaneItemPromise(guestOnlyEnv) assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1, guestOnlyRemotePaneItem2]) }) From 0446d5cd0332138595df2ea3dc4f552161687686 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 30 Nov 2017 14:55:14 -0500 Subject: [PATCH 09/65] :fire: --- lib/guest-portal-binding.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 3ac58368..0e84d236 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -57,18 +57,8 @@ class GuestPortalBinding { this.emitter.emit('did-change') } - activateEditorProxy (editorProxy) { - this.lastEditorProxyChangePromise = this.lastEditorProxyChangePromise.then(async () => { - if (this.openEditorProxies.size === 0) { - this.getEmptyPortalPaneItem().destroy() - } - this.openEditorProxies.add(editorProxy) + addEditorProxy (editorProxy) { - const editor = this.findOrCreateEditorForEditorProxy(editorProxy) - await this.openPaneItem(editor) - }) - - return this.lastEditorProxyChangePromise } removeEditorProxy (editorProxy) { From c07584003c29f5abbecab9d5b1628f1b5ed0551b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 30 Nov 2017 16:21:39 -0500 Subject: [PATCH 10/65] :arrow_up: teletype-client --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a3192660..94ffb2f7 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "temp": "^0.8.3" }, "dependencies": { - "@atom/teletype-client": "https://github.com/atom/teletype-client#e5dd46f7ba13134d43411a2fa92b1b87061efac3", + "@atom/teletype-client": "https://github.com/atom/teletype-client#multi-buffer-sharing", "etch": "^0.12.6" }, "consumedServices": { From 27bab7d1ab9572f681eea284742332ecf76523df Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 30 Nov 2017 10:27:06 -0500 Subject: [PATCH 11/65] =?UTF-8?q?=F0=9F=92=9A=20Get=20the=20test=20suite?= =?UTF-8?q?=20back=20to=20green?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/editor-binding.js | 2 +- lib/guest-portal-binding.js | 21 +++++++++++++ lib/host-portal-binding.js | 4 +++ test/guest-portal-binding.test.js | 8 ++--- test/teletype-package.test.js | 52 +++++++++++++++++++------------ 5 files changed, 62 insertions(+), 25 deletions(-) diff --git a/lib/editor-binding.js b/lib/editor-binding.js index 62ef8345..1eacf579 100644 --- a/lib/editor-binding.js +++ b/lib/editor-binding.js @@ -289,7 +289,7 @@ class EditorBinding { const marker = selectionMarkers[i] selectionUpdates[marker.id] = getSelectionState(marker) } - this.updateSelections(selectionUpdates, {initialUpdate: true}) + this.editorProxy.updateSelections(selectionUpdates, {initialUpdate: true}) } batchMarkerUpdates (fn) { diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 0e84d236..81626123 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -75,6 +75,27 @@ class GuestPortalBinding { return this.lastEditorProxyChangePromise } + updateActivePositions () {} + + updateTether (followState, editorProxy, position) { + this.lastEditorProxyChangePromise = this.lastEditorProxyChangePromise.then(async () => { + if (editorProxy == null) return + + if (this.openEditorProxies.size === 0) { + this.getEmptyPortalPaneItem().destroy() + } + this.openEditorProxies.add(editorProxy) + + const editor = this.findOrCreateEditorForEditorProxy(editorProxy) + await this.openPaneItem(editor) + + const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) + if (position) editorBinding.updateTether(followState, position) + }) + + return this.lastEditorProxyChangePromise + } + // Private findOrCreateEditorForEditorProxy (editorProxy) { let editor diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index 8145ed55..8118005b 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -96,6 +96,10 @@ class HostPortalBinding { this.portal.activateEditorProxy(editorBinding.editorProxy) } + updateActivePositions () {} + + updateTether () {} + didDestroyPaneItem ({item}) { const editorBinding = this.editorBindingsByEditor.get(item) if (editorBinding) { diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index 5a112d9e..d54cfea5 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -1,6 +1,6 @@ const assert = require('assert') const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') -const {TeletypeClient} = require('@atom/teletype-client') +const {FollowState, TeletypeClient} = require('@atom/teletype-client') const GuestPortalBinding = require('../lib/guest-portal-binding') suite('GuestPortalBinding', () => { @@ -78,9 +78,9 @@ suite('GuestPortalBinding', () => { activePaneItemChangeEvents.push(item) }) - portalBinding.activateEditorProxy(buildEditorProxy('uri-1')) - portalBinding.activateEditorProxy(buildEditorProxy('uri-2')) - await portalBinding.activateEditorProxy(buildEditorProxy('uri-3')) + portalBinding.updateTether(FollowState.RETRACTED, buildEditorProxy('uri-1')) + portalBinding.updateTether(FollowState.RETRACTED, buildEditorProxy('uri-2')) + await portalBinding.updateTether(FollowState.RETRACTED, buildEditorProxy('uri-3')) assert.deepEqual( activePaneItemChangeEvents.map((i) => i.getTitle()), diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index 597bc570..707897df 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -88,6 +88,7 @@ suite('TeletypePackage', function () { [[0, 5], [0, 7]] ]) await condition(() => deepEqual(getCursorDecoratedRanges(hostEditor1), getCursorDecoratedRanges(guestEditor1))) + await timeout(guestPackage.tetherDisconnectWindow) const hostEditor2 = await hostEnv.workspace.open(temp.path({extension: '.md'})) hostEditor2.setText('# Hello, World') @@ -197,7 +198,11 @@ suite('TeletypePackage', function () { // Portal 2 host continues to exist as a guest in Portal 1 hostOnlyEnv.workspace.open(path.join(temp.path(), 'host-only-buffer-2')) const guestAndHostRemotePaneItem2 = await getNextRemotePaneItemPromise(guestAndHostEnv) - assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2]) + // TODO Remove sorting. Order should be guaranteed. + assert.deepEqual( + getPaneItems(guestAndHostEnv).sort((a, b) => a.id - b.id), + [guestAndHostRemotePaneItem1, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2].sort((a, b) => a.id - b.id) + ) assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) // No transitivity: When Portal 2 host is viewing contents of Portal 1, Portal 2 guests can only see contents of Portal 2 @@ -208,7 +213,11 @@ suite('TeletypePackage', function () { // As Portal 2 host observes changes in Portal 1, Portal 2 guests continue to only see contents of Portal 2 await hostOnlyEnv.workspace.open(path.join(temp.path(), 'host-only-buffer-3')) const guestAndHostRemotePaneItem3 = await getNextRemotePaneItemPromise(guestAndHostEnv) - assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostRemotePaneItem3, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2]) + // TODO Remove sorting. Order should be guaranteed. + assert.deepEqual( + getPaneItems(guestAndHostEnv).sort((a, b) => a.id - b.id), + [guestAndHostRemotePaneItem1, guestAndHostRemotePaneItem3, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2].sort((a, b) => a.id - b.id) + ) assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) // When Portal 2 host shares another local buffer, Portal 2 guests see that buffer @@ -698,6 +707,7 @@ suite('TeletypePackage', function () { guestEditor2.setCursorBufferPosition([0, Infinity]) guestEditor2.insertText('\nconst goodbye = "moon"') await editorsEqual(guestEditor2, hostEditor2) + await timeout(guestPackage.tetherDisconnectWindow) hostEditor2.undo() assert.equal(hostEditor2.getText(), 'hello = "world"\nconst goodbye = "moon"') @@ -799,29 +809,31 @@ suite('TeletypePackage', function () { await condition(() => guestEditor1.lineTextForBufferRow(1).includes('y')) assert(guestEditor1.getCursorBufferPosition().isEqual([20, 29])) + // TODO Update tests below to reflect new behavior + // Reconnect and retract the tether when the host switches editors - const hostEditor2 = await hostEnv.workspace.open() - hostEditor2.setText(('y'.repeat(30) + '\n').repeat(30)) - hostEditor2.setCursorBufferPosition([2, 2]) - const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) - await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) - hostEditor2.setCursorBufferPosition([4, 4]) - await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) + // const hostEditor2 = await hostEnv.workspace.open() + // hostEditor2.setText(('y'.repeat(30) + '\n').repeat(30)) + // hostEditor2.setCursorBufferPosition([2, 2]) + // const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) + // await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) + // hostEditor2.setCursorBufferPosition([4, 4]) + // await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) // Disconnect tether if guest scrolls the tether position out of view - guestEditor2.setCursorBufferPosition([20, 0]) - await timeout(guestPackage.tetherDisconnectWindow) - hostEditor2.setCursorBufferPosition([4, 5]) - hostEditor2.insertText('z') - await condition(() => guestEditor2.lineTextForBufferRow(4).includes('z')) - assert(guestEditor2.getCursorBufferPosition().isEqual([20, 0])) + // guestEditor2.setCursorBufferPosition([20, 0]) + // await timeout(guestPackage.tetherDisconnectWindow) + // hostEditor2.setCursorBufferPosition([4, 5]) + // hostEditor2.insertText('z') + // await condition(() => guestEditor2.lineTextForBufferRow(4).includes('z')) + // assert(guestEditor2.getCursorBufferPosition().isEqual([20, 0])) // When host switches back to an existing editor, reconnect the tether - hostEnv.workspace.getActivePane().activateItem(hostEditor1) - await getNextActiveTextEditorPromise(guestEnv) - await condition(() => deepEqual(guestEditor1.getCursorBufferPosition(), hostEditor1.getCursorBufferPosition())) - hostEditor1.setCursorBufferPosition([1, 20]) - await condition(() => deepEqual(guestEditor1.getCursorBufferPosition(), hostEditor1.getCursorBufferPosition())) + // hostEnv.workspace.getActivePane().activateItem(hostEditor1) + // await getNextActiveTextEditorPromise(guestEnv) + // await condition(() => deepEqual(guestEditor1.getCursorBufferPosition(), hostEditor1.getCursorBufferPosition())) + // hostEditor1.setCursorBufferPosition([1, 20]) + // await condition(() => deepEqual(guestEditor1.getCursorBufferPosition(), hostEditor1.getCursorBufferPosition())) }) test('adding and removing workspace element classes when sharing a portal', async () => { From b613abf7cff51feed01ed40cbc70a471bc72dcf8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Dec 2017 08:55:36 +0100 Subject: [PATCH 12/65] Open pane item only if it isn't already the active one --- lib/guest-portal-binding.js | 10 ++++++---- test/teletype-package.test.js | 12 ++---------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 81626123..25bec437 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -192,10 +192,12 @@ class GuestPortalBinding { } async openPaneItem (newActivePaneItem) { - this.newActivePaneItem = newActivePaneItem - await this.workspace.open(newActivePaneItem) - this.activePaneItem = this.newActivePaneItem - this.newActivePaneItem = null + if (newActivePaneItem !== this.getActivePaneItem()) { + this.newActivePaneItem = newActivePaneItem + await this.workspace.open(newActivePaneItem) + this.activePaneItem = this.newActivePaneItem + this.newActivePaneItem = null + } } getActivePaneItem () { diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index 707897df..f3498a08 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -198,11 +198,7 @@ suite('TeletypePackage', function () { // Portal 2 host continues to exist as a guest in Portal 1 hostOnlyEnv.workspace.open(path.join(temp.path(), 'host-only-buffer-2')) const guestAndHostRemotePaneItem2 = await getNextRemotePaneItemPromise(guestAndHostEnv) - // TODO Remove sorting. Order should be guaranteed. - assert.deepEqual( - getPaneItems(guestAndHostEnv).sort((a, b) => a.id - b.id), - [guestAndHostRemotePaneItem1, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2].sort((a, b) => a.id - b.id) - ) + assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2]) assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) // No transitivity: When Portal 2 host is viewing contents of Portal 1, Portal 2 guests can only see contents of Portal 2 @@ -213,11 +209,7 @@ suite('TeletypePackage', function () { // As Portal 2 host observes changes in Portal 1, Portal 2 guests continue to only see contents of Portal 2 await hostOnlyEnv.workspace.open(path.join(temp.path(), 'host-only-buffer-3')) const guestAndHostRemotePaneItem3 = await getNextRemotePaneItemPromise(guestAndHostEnv) - // TODO Remove sorting. Order should be guaranteed. - assert.deepEqual( - getPaneItems(guestAndHostEnv).sort((a, b) => a.id - b.id), - [guestAndHostRemotePaneItem1, guestAndHostRemotePaneItem3, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2].sort((a, b) => a.id - b.id) - ) + assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostRemotePaneItem3, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2]) assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) // When Portal 2 host shares another local buffer, Portal 2 guests see that buffer From e914eb0d25e1c20a54d1f4b42700fdf6f53b22c2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Dec 2017 10:30:00 +0100 Subject: [PATCH 13/65] Disconnect tether if moving away from leader's active editor --- lib/editor-binding.js | 9 +++-- lib/guest-portal-binding.js | 49 ++++++++++++++++------- test/guest-portal-binding.test.js | 1 + test/portal-binding-manager.test.js | 3 ++ test/portal-list-component.test.js | 4 ++ test/teletype-package.test.js | 62 ++++++++++++++++------------- 6 files changed, 83 insertions(+), 45 deletions(-) diff --git a/lib/editor-binding.js b/lib/editor-binding.js index 1eacf579..fdf60170 100644 --- a/lib/editor-binding.js +++ b/lib/editor-binding.js @@ -139,21 +139,24 @@ class EditorBinding { async editorDidChangeScrollTop () { const {element} = this.editor await element.component.getNextUpdatePromise() - this.updateActivePositions(this.positionsBySiteId) + // TODO: move into portal bindings. + // this.updateActivePositions(this.positionsBySiteId) this.editorProxy.didScroll() } async editorDidChangeScrollLeft () { const {element} = this.editor await element.component.getNextUpdatePromise() - this.updateActivePositions(this.positionsBySiteId) + // TODO: move into portal bindings. + // this.updateActivePositions(this.positionsBySiteId) this.editorProxy.didScroll() } async editorDidResize () { const {element} = this.editor await element.component.getNextUpdatePromise() - this.updateActivePositions(this.positionsBySiteId) + // TODO: move into portal bindings. + // this.updateActivePositions(this.positionsBySiteId) } updateSelectionsForSiteId (siteId, selections) { diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 25bec437..365a3040 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -1,5 +1,5 @@ -const {Emitter, TextEditor, TextBuffer} = require('atom') -const {Errors} = require('@atom/teletype-client') +const {CompositeDisposable, Emitter, TextEditor, TextBuffer} = require('atom') +const {Errors, FollowState} = require('@atom/teletype-client') const BufferBinding = require('./buffer-binding') const EditorBinding = require('./editor-binding') const EmptyPortalPaneItem = require('./empty-portal-pane-item') @@ -15,7 +15,9 @@ class GuestPortalBinding { this.activePaneItem = null this.editorBindingsByEditorProxy = new Map() this.bufferBindingsByBufferProxy = new Map() + this.editorProxiesByEditor = new WeakMap() this.emitter = new Emitter() + this.subscriptions = new CompositeDisposable() this.lastEditorProxyChangePromise = Promise.resolve() this.openEditorProxies = new Set() } @@ -30,6 +32,7 @@ class GuestPortalBinding { await this.openPaneItem(this.getEmptyPortalPaneItem()) } + this.subscriptions.add(this.workspace.onDidChangeActivePaneItem(this.didChangeActivePaneItem.bind(this))) return true } catch (error) { this.didFailToJoin(error) @@ -38,6 +41,7 @@ class GuestPortalBinding { } dispose () { + this.subscriptions.dispose() this.openEditorProxies.clear() if (this.emptyPortalItem) this.emptyPortalItem.destroy() this.emitDidDispose() @@ -63,13 +67,13 @@ class GuestPortalBinding { removeEditorProxy (editorProxy) { this.lastEditorProxyChangePromise = this.lastEditorProxyChangePromise.then(async () => { - const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) - editorBinding.editor.destroy() - this.openEditorProxies.delete(editorProxy) if (this.openEditorProxies.size === 0) { await this.openPaneItem(this.getEmptyPortalPaneItem()) } + + const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) + editorBinding.editor.destroy() }) return this.lastEditorProxyChangePromise @@ -78,19 +82,23 @@ class GuestPortalBinding { updateActivePositions () {} updateTether (followState, editorProxy, position) { - this.lastEditorProxyChangePromise = this.lastEditorProxyChangePromise.then(async () => { - if (editorProxy == null) return + if (!editorProxy) return - if (this.openEditorProxies.size === 0) { - this.getEmptyPortalPaneItem().destroy() + this.lastEditorProxyChangePromise = this.lastEditorProxyChangePromise.then(async () => { + if (followState === FollowState.RETRACTED) { + const editor = this.findOrCreateEditorForEditorProxy(editorProxy) + await this.openPaneItem(editor) + + this.openEditorProxies.add(editorProxy) + if (this.openEditorProxies.size > 0) { + this.getEmptyPortalPaneItem().destroy() + } } - this.openEditorProxies.add(editorProxy) - - const editor = this.findOrCreateEditorForEditorProxy(editorProxy) - await this.openPaneItem(editor) const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) - if (position) editorBinding.updateTether(followState, position) + if (editorBinding && position) { + editorBinding.updateTether(followState, position) + } }) return this.lastEditorProxyChangePromise @@ -110,11 +118,15 @@ class GuestPortalBinding { editor, portal: this.portal, isHost: false, - didDispose: () => this.editorBindingsByEditorProxy.delete(editorProxy) + didDispose: () => { + this.editorBindingsByEditorProxy.delete(editorProxy) + this.editorProxiesByEditor.delete(editor) + } }) editorBinding.setEditorProxy(editorProxy) editorProxy.setDelegate(editorBinding) this.editorBindingsByEditorProxy.set(editorProxy, editorBinding) + this.editorProxiesByEditor.set(editor, editorProxy) } return editor } @@ -200,6 +212,13 @@ class GuestPortalBinding { } } + didChangeActivePaneItem (paneItem) { + if (paneItem !== this.getEmptyPortalPaneItem()) { + const editorProxy = this.editorProxiesByEditor.get(paneItem) + this.portal.activateEditorProxy(editorProxy) + } + } + getActivePaneItem () { return this.newActivePaneItem ? this.newActivePaneItem : this.activePaneItem } diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index d54cfea5..ccc8c02d 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -33,6 +33,7 @@ suite('GuestPortalBinding', () => { setDelegate (delegate) { this.delegate = delegate }, + activateEditorProxy () {}, getSiteIdentity (siteId) { return {login: 'site-' + siteId} } diff --git a/test/portal-binding-manager.test.js b/test/portal-binding-manager.test.js index 2d998eb3..76afa97d 100644 --- a/test/portal-binding-manager.test.js +++ b/test/portal-binding-manager.test.js @@ -118,6 +118,9 @@ function buildPortalBindingManager () { observeActivePaneItem () { return new Disposable(() => {}) }, + onDidChangeActivePaneItem () { + return new Disposable(() => {}) + }, onDidDestroyPaneItem () { return new Disposable(() => {}) } diff --git a/test/portal-list-component.test.js b/test/portal-list-component.test.js index 71fc904f..e8b989e2 100644 --- a/test/portal-list-component.test.js +++ b/test/portal-list-component.test.js @@ -259,6 +259,10 @@ class FakeWorkspace { return new Disposable(() => {}) } + onDidChangeActivePaneItem () { + return new Disposable(() => {}) + } + paneForItem () {} } diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index f3498a08..be486f9e 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -181,7 +181,7 @@ suite('TeletypePackage', function () { // Start out as a guest in another user's portal (Portal 1) const portal1Id = (await hostOnlyPackage.sharePortal()).id - guestAndHostPackage.joinPortal(portal1Id) + const guestAndHostPortal1 = await guestAndHostPackage.joinPortal(portal1Id) hostOnlyEnv.workspace.open(path.join(temp.path(), 'host-only-buffer-1')) const guestAndHostRemotePaneItem1 = await getNextRemotePaneItemPromise(guestAndHostEnv) assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1]) @@ -194,6 +194,7 @@ suite('TeletypePackage', function () { const guestOnlyRemotePaneItem1 = await getNextRemotePaneItemPromise(guestOnlyEnv) assert(guestOnlyRemotePaneItem1 instanceof TextEditor) assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) + guestAndHostPortal1.follow(1) // reconnect tether after disconnecting it due to opening a local editor. // Portal 2 host continues to exist as a guest in Portal 1 hostOnlyEnv.workspace.open(path.join(temp.path(), 'host-only-buffer-2')) @@ -205,6 +206,7 @@ suite('TeletypePackage', function () { guestAndHostEnv.workspace.getActivePane().activateItemAtIndex(0) assert.equal(guestAndHostEnv.workspace.getActivePaneItem(), guestAndHostRemotePaneItem1) assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) + guestAndHostPortal1.follow(1) // reconnect tether after disconnecting it due to switching to a different editor. // As Portal 2 host observes changes in Portal 1, Portal 2 guests continue to only see contents of Portal 2 await hostOnlyEnv.workspace.open(path.join(temp.path(), 'host-only-buffer-3')) @@ -738,7 +740,7 @@ suite('TeletypePackage', function () { await condition(() => deepEqual(getCursorDecoratedRanges(hostEditor), getCursorDecoratedRanges(guestEditor))) }) - test('tethering to other collaborators', async () => { + test.only('tethering to other collaborators', async () => { const hostEnv = buildAtomEnvironment() const hostPackage = await buildPackage(hostEnv) const guestEnv = buildAtomEnvironment() @@ -752,10 +754,10 @@ suite('TeletypePackage', function () { hostEditor1.setText(('x'.repeat(30) + '\n').repeat(30)) hostEditor1.setCursorBufferPosition([2, 2]) - const portal = await hostPackage.sharePortal() - guestPackage.joinPortal(portal.id) + const hostPortal = await hostPackage.sharePortal() + const guestPortal = await guestPackage.joinPortal(hostPortal.id) - const guestEditor1 = await getNextActiveTextEditorPromise(guestEnv) + const guestEditor1 = guestEnv.workspace.getActiveTextEditor() // Jump to host cursor when joining await condition(() => deepEqual(guestEditor1.getCursorBufferPosition(), hostEditor1.getCursorBufferPosition())) @@ -801,31 +803,37 @@ suite('TeletypePackage', function () { await condition(() => guestEditor1.lineTextForBufferRow(1).includes('y')) assert(guestEditor1.getCursorBufferPosition().isEqual([20, 29])) - // TODO Update tests below to reflect new behavior + // Retract guest's tether and ensure following across tabs still works. + const hostEditor2 = await hostEnv.workspace.open() + hostEditor2.setText(('y'.repeat(30) + '\n').repeat(30)) + hostEditor2.setCursorBufferPosition([2, 2]) + + guestPortal.follow(1) - // Reconnect and retract the tether when the host switches editors - // const hostEditor2 = await hostEnv.workspace.open() - // hostEditor2.setText(('y'.repeat(30) + '\n').repeat(30)) - // hostEditor2.setCursorBufferPosition([2, 2]) - // const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) - // await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) - // hostEditor2.setCursorBufferPosition([4, 4]) - // await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) + const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) + await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) + hostEditor2.setCursorBufferPosition([4, 4]) + await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) // Disconnect tether if guest scrolls the tether position out of view - // guestEditor2.setCursorBufferPosition([20, 0]) - // await timeout(guestPackage.tetherDisconnectWindow) - // hostEditor2.setCursorBufferPosition([4, 5]) - // hostEditor2.insertText('z') - // await condition(() => guestEditor2.lineTextForBufferRow(4).includes('z')) - // assert(guestEditor2.getCursorBufferPosition().isEqual([20, 0])) - - // When host switches back to an existing editor, reconnect the tether - // hostEnv.workspace.getActivePane().activateItem(hostEditor1) - // await getNextActiveTextEditorPromise(guestEnv) - // await condition(() => deepEqual(guestEditor1.getCursorBufferPosition(), hostEditor1.getCursorBufferPosition())) - // hostEditor1.setCursorBufferPosition([1, 20]) - // await condition(() => deepEqual(guestEditor1.getCursorBufferPosition(), hostEditor1.getCursorBufferPosition())) + guestEditor2.setCursorBufferPosition([20, 0]) + await timeout(guestPackage.tetherDisconnectWindow) + hostEditor2.setCursorBufferPosition([4, 5]) + hostEditor2.insertText('z') + await condition(() => guestEditor2.lineTextForBufferRow(4).includes('z')) + assert(guestEditor2.getCursorBufferPosition().isEqual([20, 0])) + + // Retract guest's tether and ensure it gets disconnected after switching to a different tab. + guestPortal.follow(1) + await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) + guestEnv.workspace.getActivePane().activateItem(guestEditor1) + await timeout(guestPackage.tetherDisconnectWindow) + hostEditor2.setCursorBufferPosition([8, 2]) + hostEditor2.insertText('x') + await condition(() => guestEditor2.lineTextForBufferRow(4).includes('z')) + assert(!guestEditor2.getCursorBufferPosition().isEqual(hostEditor2.getCursorBufferPosition())) + + // TODO: ensure host can follow guest. }) test('adding and removing workspace element classes when sharing a portal', async () => { From 9b68c00f9ae20b54759adb0cd90bf73d384e7d13 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Dec 2017 12:02:52 +0100 Subject: [PATCH 14/65] Skip active position of other collaborators test --- test/editor-binding.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/editor-binding.test.js b/test/editor-binding.test.js index 836df5d3..9ec1c5d7 100644 --- a/test/editor-binding.test.js +++ b/test/editor-binding.test.js @@ -347,7 +347,8 @@ suite('EditorBinding', function () { assert.deepEqual(getCursorClasses(editor), []) }) - test('showing the active position of other collaborators', async () => { + test.skip('showing the active position of other collaborators', async () => { + // TODO: move this test into portal binding tests. const editor = new TextEditor({autoHeight: false}) editor.setText(SAMPLE_TEXT) From 1380f554e4f48914f86c60df635ca49be7deb5e2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Dec 2017 12:04:13 +0100 Subject: [PATCH 15/65] Test following behavior with guest->host, host->guest and guest->guest --- test/teletype-package.test.js | 242 +++++++++++++++++++++++----------- 1 file changed, 162 insertions(+), 80 deletions(-) diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index be486f9e..dcb7cb6f 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -740,100 +740,182 @@ suite('TeletypePackage', function () { await condition(() => deepEqual(getCursorDecoratedRanges(hostEditor), getCursorDecoratedRanges(guestEditor))) }) - test.only('tethering to other collaborators', async () => { - const hostEnv = buildAtomEnvironment() - const hostPackage = await buildPackage(hostEnv) - const guestEnv = buildAtomEnvironment() - const guestPackage = await buildPackage(guestEnv) - const guestWorkspaceElement = guestEnv.views.getView(guestEnv.workspace) - guestWorkspaceElement.style.height = '100px' - guestWorkspaceElement.style.width = '250px' - containerElement.appendChild(guestWorkspaceElement) + suite('tethering', () => { + test('guest following host', async () => { + const hostEnv = buildAtomEnvironment() + const hostPackage = await buildPackage(hostEnv) + const guestEnv = buildAtomEnvironment() + const guestPackage = await buildPackage(guestEnv) + + const hostPortal = await hostPackage.sharePortal() + const guestPortal = await guestPackage.joinPortal(hostPortal.id) + + const hostEditor1 = await hostEnv.workspace.open() + hostEditor1.setText(('x'.repeat(30) + '\n').repeat(30)) + hostEditor1.setCursorBufferPosition([2, 2]) + + const hostEditor2 = await hostEnv.workspace.open() + hostEditor2.setText(('y'.repeat(30) + '\n').repeat(30)) + hostEditor2.setCursorBufferPosition([2, 2]) + + await condition(() => guestEnv.workspace.getTextEditors().length === 2) + + await verifyTetheringRules({ + leaderEnv: hostEnv, + leaderPortal: hostPortal, + followerEnv: guestEnv, + followerPortal: guestPortal + }) + }) - const hostEditor1 = await hostEnv.workspace.open() - hostEditor1.setText(('x'.repeat(30) + '\n').repeat(30)) - hostEditor1.setCursorBufferPosition([2, 2]) + test.skip('host following guest', async () => { + const hostEnv = buildAtomEnvironment() + const hostPackage = await buildPackage(hostEnv) + const guestEnv = buildAtomEnvironment() + const guestPackage = await buildPackage(guestEnv) - const hostPortal = await hostPackage.sharePortal() - const guestPortal = await guestPackage.joinPortal(hostPortal.id) + const hostPortal = await hostPackage.sharePortal() + const guestPortal = await guestPackage.joinPortal(hostPortal.id) - const guestEditor1 = guestEnv.workspace.getActiveTextEditor() + const hostEditor1 = await hostEnv.workspace.open() + hostEditor1.setText(('x'.repeat(30) + '\n').repeat(30)) + hostEditor1.setCursorBufferPosition([2, 2]) - // Jump to host cursor when joining - await condition(() => deepEqual(guestEditor1.getCursorBufferPosition(), hostEditor1.getCursorBufferPosition())) + const hostEditor2 = await hostEnv.workspace.open() + hostEditor2.setText(('y'.repeat(30) + '\n').repeat(30)) + hostEditor2.setCursorBufferPosition([2, 2]) - // Initially, guests follow the host's cursor - hostEditor1.setCursorBufferPosition([3, 3]) - await condition(() => deepEqual(guestEditor1.getCursorBufferPosition(), hostEditor1.getCursorBufferPosition())) + await condition(() => guestEnv.workspace.getTextEditors().length === 2) - // When followers move their cursor, their cursor does not follow the - // leader's cursor so long as the leader's cursor stays within the - // follower's viewport - guestEditor1.setCursorBufferPosition([2, 10]) - hostEditor1.setCursorBufferPosition([3, 5]) - hostEditor1.insertText('y') - await condition(() => guestEditor1.lineTextForBufferRow(3).includes('y')) - assert(guestEditor1.getCursorBufferPosition().isEqual([2, 10])) + await verifyTetheringRules({ + leaderEnv: guestEnv, + leaderPortal: guestPortal, + followerEnv: hostEnv, + followerPortal: hostPortal + }) + }) - // When the leader moves their cursor out of the follower's viewport, the - // follower's cursor moves to the same position if the unfollow period - // has elapsed. - await timeout(guestPackage.tetherDisconnectWindow) - hostEditor1.setCursorBufferPosition([20, 10]) - await condition(() => deepEqual(guestEditor1.getCursorBufferPosition(), hostEditor1.getCursorBufferPosition())) + test('guest following guest', async () => { + const hostEnv = buildAtomEnvironment() + const hostPackage = await buildPackage(hostEnv) - // If the leader moves to non-visible columns (not just rows), we update - // the tether - await condition(() => guestEditor1.getFirstVisibleScreenRow() > 0) - guestEditor1.setCursorBufferPosition([20, 9]) - await timeout(guestPackage.tetherDisconnectWindow) - hostEditor1.setCursorBufferPosition([20, 30]) - await condition(() => deepEqual(guestEditor1.getCursorBufferPosition(), hostEditor1.getCursorBufferPosition())) + const guest1Env = buildAtomEnvironment() + const guest1Package = await buildPackage(guest1Env) - // Disconnect tether if leader's cursor position moves within the tether - // disconnect window - guestEditor1.setCursorBufferPosition([20, 29]) - hostEditor1.setCursorBufferPosition([0, 0]) - hostEditor1.insertText('y') - await condition(() => guestEditor1.lineTextForBufferRow(0).includes('y')) - assert(guestEditor1.getCursorBufferPosition().isEqual([20, 29])) - await timeout(guestPackage.tetherDisconnectWindow) - hostEditor1.setCursorBufferPosition([1, 0]) - hostEditor1.insertText('y') - await condition(() => guestEditor1.lineTextForBufferRow(1).includes('y')) - assert(guestEditor1.getCursorBufferPosition().isEqual([20, 29])) + const guest2Env = buildAtomEnvironment() + const guest2Package = await buildPackage(guest2Env) - // Retract guest's tether and ensure following across tabs still works. - const hostEditor2 = await hostEnv.workspace.open() - hostEditor2.setText(('y'.repeat(30) + '\n').repeat(30)) - hostEditor2.setCursorBufferPosition([2, 2]) + const hostPortal = await hostPackage.sharePortal() + const guest1Portal = await guest1Package.joinPortal(hostPortal.id) + const guest2Portal = await guest2Package.joinPortal(hostPortal.id) - guestPortal.follow(1) + const hostEditor1 = await hostEnv.workspace.open() + hostEditor1.setText(('x'.repeat(30) + '\n').repeat(30)) + hostEditor1.setCursorBufferPosition([2, 2]) - const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) - await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) - hostEditor2.setCursorBufferPosition([4, 4]) - await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) + const hostEditor2 = await hostEnv.workspace.open() + hostEditor2.setText(('y'.repeat(30) + '\n').repeat(30)) + hostEditor2.setCursorBufferPosition([2, 2]) - // Disconnect tether if guest scrolls the tether position out of view - guestEditor2.setCursorBufferPosition([20, 0]) - await timeout(guestPackage.tetherDisconnectWindow) - hostEditor2.setCursorBufferPosition([4, 5]) - hostEditor2.insertText('z') - await condition(() => guestEditor2.lineTextForBufferRow(4).includes('z')) - assert(guestEditor2.getCursorBufferPosition().isEqual([20, 0])) - - // Retract guest's tether and ensure it gets disconnected after switching to a different tab. - guestPortal.follow(1) - await condition(() => deepEqual(guestEditor2.getCursorBufferPosition(), hostEditor2.getCursorBufferPosition())) - guestEnv.workspace.getActivePane().activateItem(guestEditor1) - await timeout(guestPackage.tetherDisconnectWindow) - hostEditor2.setCursorBufferPosition([8, 2]) - hostEditor2.insertText('x') - await condition(() => guestEditor2.lineTextForBufferRow(4).includes('z')) - assert(!guestEditor2.getCursorBufferPosition().isEqual(hostEditor2.getCursorBufferPosition())) + await condition(() => guest1Env.workspace.getTextEditors().length === 2) + await condition(() => guest2Env.workspace.getTextEditors().length === 2) - // TODO: ensure host can follow guest. + await verifyTetheringRules({ + leaderEnv: guest1Env, + leaderPortal: guest1Portal, + followerEnv: guest2Env, + followerPortal: guest2Portal + }) + }) + + async function verifyTetheringRules ({leaderEnv, leaderPortal, followerEnv, followerPortal}) { + // Setup DOM for follower's workspace. + const followerWorkspaceElement = followerEnv.views.getView(followerEnv.workspace) + followerWorkspaceElement.style.height = '100px' + followerWorkspaceElement.style.width = '250px' + containerElement.appendChild(followerWorkspaceElement) + + const leaderEditors = leaderEnv.workspace.getTextEditors() + const followerEditors = followerEnv.workspace.getTextEditors() + + // Reset follow state. + leaderPortal.unfollow() + followerPortal.unfollow() + + // Jump to leader cursor and follow it as it moves. + leaderEnv.workspace.getActivePane().activateItem(leaderEditors[0]) + followerPortal.follow(leaderPortal.siteId) + await condition(() => ( + followerEnv.workspace.getActivePaneItem() === followerEditors[0] && + deepEqual(followerEditors[0].getCursorBufferPosition(), leaderEditors[0].getCursorBufferPosition()) + )) + + leaderEditors[0].setCursorBufferPosition([3, 3]) + await condition(() => deepEqual(followerEditors[0].getCursorBufferPosition(), leaderEditors[0].getCursorBufferPosition())) + + // When followers move their cursor, their cursor does not follow the + // leader's cursor so long as the leader's cursor stays within the + // follower's viewport. + followerEditors[0].setCursorBufferPosition([2, 10]) + leaderEditors[0].setCursorBufferPosition([3, 5]) + leaderEditors[0].insertText('Y') + await condition(() => followerEditors[0].lineTextForBufferRow(3).includes('Y')) + assert(followerEditors[0].getCursorBufferPosition().isEqual([2, 10])) + + // When the leader moves their cursor out of the follower's viewport, the + // follower's cursor moves to the same position if the unfollow period + // has elapsed. + await timeout(followerPortal.tetherDisconnectWindow) + leaderEditors[0].setCursorBufferPosition([20, 10]) + await condition(() => deepEqual(followerEditors[0].getCursorBufferPosition(), leaderEditors[0].getCursorBufferPosition())) + + // If the leader moves to non-visible columns (not just rows), we update + // the tether. + await condition(() => followerEditors[0].getFirstVisibleScreenRow() > 0) + followerEditors[0].setCursorBufferPosition([20, 9]) + await timeout(followerPortal.tetherDisconnectWindow) + leaderEditors[0].setCursorBufferPosition([20, 30]) + await condition(() => deepEqual(followerEditors[0].getCursorBufferPosition(), leaderEditors[0].getCursorBufferPosition())) + + // Disconnect tether if leader's cursor position moves within the tether + // disconnect window. + followerEditors[0].setCursorBufferPosition([20, 29]) + leaderEditors[0].setCursorBufferPosition([0, 0]) + leaderEditors[0].insertText('Y') + await condition(() => followerEditors[0].lineTextForBufferRow(0).includes('Y')) + assert(followerEditors[0].getCursorBufferPosition().isEqual([20, 29])) + await timeout(followerPortal.tetherDisconnectWindow) + leaderEditors[0].setCursorBufferPosition([1, 0]) + leaderEditors[0].insertText('Y') + await condition(() => followerEditors[0].lineTextForBufferRow(1).includes('Y')) + assert(followerEditors[0].getCursorBufferPosition().isEqual([20, 29])) + + // Retract follower's tether and ensure following across tabs still works. + leaderEnv.workspace.paneForItem(leaderEditors[1]).activateItem(leaderEditors[1]) + followerPortal.follow(leaderPortal.siteId) + + await condition(() => deepEqual(followerEditors[1].getCursorBufferPosition(), leaderEditors[1].getCursorBufferPosition())) + leaderEditors[1].setCursorBufferPosition([4, 4]) + await condition(() => deepEqual(followerEditors[1].getCursorBufferPosition(), leaderEditors[1].getCursorBufferPosition())) + + // Disconnect tether if follower scrolls the tether position out of view. + followerEditors[1].setCursorBufferPosition([20, 0]) + await timeout(followerPortal.tetherDisconnectWindow) + leaderEditors[1].setCursorBufferPosition([4, 5]) + leaderEditors[1].insertText('Z') + await condition(() => followerEditors[1].lineTextForBufferRow(4).includes('Z')) + assert(followerEditors[1].getCursorBufferPosition().isEqual([20, 0])) + + // Retract follower's tether and ensure it gets disconnected after switching to a different tab. + followerPortal.follow(leaderPortal.siteId) + await condition(() => deepEqual(followerEditors[1].getCursorBufferPosition(), leaderEditors[1].getCursorBufferPosition())) + followerEnv.workspace.getActivePane().activateItem(followerEditors[0]) + await timeout(followerPortal.tetherDisconnectWindow) + leaderEditors[1].setCursorBufferPosition([8, 2]) + leaderEditors[1].insertText('X') + await condition(() => followerEditors[1].lineTextForBufferRow(8).includes('X')) + assert(!followerEditors[1].getCursorBufferPosition().isEqual(leaderEditors[1].getCursorBufferPosition())) + } }) test('adding and removing workspace element classes when sharing a portal', async () => { From a8771879466cbc45b89dffa64b8eb72c43f6c5c7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Dec 2017 13:52:57 +0100 Subject: [PATCH 16/65] :art: --- test/teletype-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index dcb7cb6f..458cd4a9 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -890,7 +890,7 @@ suite('TeletypePackage', function () { await condition(() => followerEditors[0].lineTextForBufferRow(1).includes('Y')) assert(followerEditors[0].getCursorBufferPosition().isEqual([20, 29])) - // Retract follower's tether and ensure following across tabs still works. + // When re-following, ensure that you are taken to the leader's current tab. leaderEnv.workspace.paneForItem(leaderEditors[1]).activateItem(leaderEditors[1]) followerPortal.follow(leaderPortal.siteId) From aaf92a12e121db70790e9b3ca9e062dae74b529e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Dec 2017 14:46:04 +0100 Subject: [PATCH 17/65] Show local selections on all remote editors after un-retracting tether --- lib/guest-portal-binding.js | 4 ++++ test/teletype-package.test.js | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 365a3040..79949d2e 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -93,6 +93,10 @@ class GuestPortalBinding { if (this.openEditorProxies.size > 0) { this.getEmptyPortalPaneItem().destroy() } + } else { + this.editorBindingsByEditorProxy.forEach((editorBinding) => { + editorBinding.updateTether(followState) + }) } const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index 458cd4a9..8814f6e6 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -909,12 +909,21 @@ suite('TeletypePackage', function () { // Retract follower's tether and ensure it gets disconnected after switching to a different tab. followerPortal.follow(leaderPortal.siteId) await condition(() => deepEqual(followerEditors[1].getCursorBufferPosition(), leaderEditors[1].getCursorBufferPosition())) + followerEnv.workspace.getActivePane().activateItem(followerEditors[0]) await timeout(followerPortal.tetherDisconnectWindow) + + followerEditors[0].setCursorBufferPosition([3, 4]) leaderEditors[1].setCursorBufferPosition([8, 2]) leaderEditors[1].insertText('X') + await condition(() => followerEditors[1].lineTextForBufferRow(8).includes('X')) - assert(!followerEditors[1].getCursorBufferPosition().isEqual(leaderEditors[1].getCursorBufferPosition())) + + assert.equal(followerEnv.workspace.getActivePaneItem(), followerEditors[0]) + assert(getCursorDecoratedRanges(followerEditors[0]).find((r) => r.isEqual([[3, 4], [3, 4]]))) + + assert.equal(leaderEnv.workspace.getActivePaneItem(), leaderEditors[1]) + assert(getCursorDecoratedRanges(leaderEditors[1]).find((r) => r.isEqual([[8, 3], [8, 3]]))) } }) From 9c41034910be82376e3a2c3f66b19154420eb879 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 1 Dec 2017 09:20:21 -0500 Subject: [PATCH 18/65] Clarify that we'll implement GuestPortalBinding.addEditorProxy later --- lib/guest-portal-binding.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 79949d2e..6fd11653 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -62,7 +62,7 @@ class GuestPortalBinding { } addEditorProxy (editorProxy) { - + // TODO Implement in order to allow guests to open any editor that's in the host's workspace } removeEditorProxy (editorProxy) { From 35e09a957aab5980120bc98fa3d9565f2540c51e Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 1 Dec 2017 11:52:08 -0500 Subject: [PATCH 19/65] RFC-001: Describe display of avatars to indicate participant position --- .../001-allow-guests-to-open-multiple-remote-buffers.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/rfcs/001-allow-guests-to-open-multiple-remote-buffers.md b/doc/rfcs/001-allow-guests-to-open-multiple-remote-buffers.md index 52da9972..86d60955 100644 --- a/doc/rfcs/001-allow-guests-to-open-multiple-remote-buffers.md +++ b/doc/rfcs/001-allow-guests-to-open-multiple-remote-buffers.md @@ -32,7 +32,12 @@ When a host closes a buffer, it will be removed from all guest portals. If anoth You can follow any other guest participating in the host's workspace in the exact same way. If they move between buffers, you will follow them. The host does not enjoy any special privilege with respect to the ability to be followed between different files. -When a participant is viewing a different buffer than you are viewing, that participant's avatar appears in the bottom right with an icon (e.g., https://octicons.github.com/icon/link-external) indicating that they're working on a different buffer. +When viewing an editor associated with a portal, each participant sees the avatars for the other portal participants (just as they did prior to this RFC). As a host, when your active pane item is a local editor (i.e., an editor that you're sharing in your portal), the editor shows the avatars for the other portal participants. As a guest, when your active pane item is a remote editor (i.e., an editor that you're viewing from the host's portal), the editor shows the avatars for the other portal participants. The location of each avatar within the editor indicates the relative position of that participant: +- Top-right: Participants in the same editor but in a row above your viewport +- Middle-right: Participants in the same editor and inside your viewport +- Bottom-right: Participants in the same editor but in a row below your viewport or a column outside of your viewport +- Bottom-right with TBD icon 1: Participants in a different editor in the portal +- Bottom-right with TBD icon 2: Participants in a different pane item not associated with the portal Editors for remote buffers are *only* automatically opened when you are following another collaborator. If you are not following someone, no editors are automatically opened. When you start following another collaborator again, an editor will be automatically opened based on their location. You can also open any buffer in the host's workspace directly by navigating to it... From 4b3379cf117ae038f6ddf7481a09f22a63f93530 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Dec 2017 17:47:30 +0100 Subject: [PATCH 20/65] Port participant positions logic into `GuestPortalBinding` --- lib/editor-binding.js | 16 +-- lib/guest-portal-binding.js | 72 +++++++++- test/editor-binding.test.js | 99 +------------- test/guest-portal-binding.test.js | 216 +++++++++++++++++++++++++----- test/helpers/editor-helpers.js | 22 +++ 5 files changed, 286 insertions(+), 139 deletions(-) create mode 100644 test/helpers/editor-helpers.js diff --git a/lib/editor-binding.js b/lib/editor-binding.js index fdf60170..849c87b8 100644 --- a/lib/editor-binding.js +++ b/lib/editor-binding.js @@ -1,7 +1,7 @@ /* global ResizeObserver */ const path = require('path') -const {Range, Disposable, CompositeDisposable} = require('atom') +const {Range, Emitter, Disposable, CompositeDisposable} = require('atom') const normalizeURI = require('./normalize-uri') const {FollowState} = require('@atom/teletype-client') const SitePositionsComponent = require('./site-positions-component') @@ -10,10 +10,13 @@ function doNothing () {} module.exports = class EditorBinding { - constructor ({editor, portal, isHost, didDispose}) { + constructor ({editor, portal, isHost, didResize, didScroll, didDispose}) { this.editor = editor this.portal = portal this.isHost = isHost + this.emitter = new Emitter() + this.emitDidResize = didResize || doNothing + this.emitDidScroll = didScroll || doNothing this.emitDidDispose = didDispose || doNothing this.selectionsMarkerLayer = this.editor.selectionsMarkerLayer.bufferMarkerLayer this.markerLayersBySiteId = new Map() @@ -139,24 +142,21 @@ class EditorBinding { async editorDidChangeScrollTop () { const {element} = this.editor await element.component.getNextUpdatePromise() - // TODO: move into portal bindings. - // this.updateActivePositions(this.positionsBySiteId) this.editorProxy.didScroll() + this.emitDidScroll() } async editorDidChangeScrollLeft () { const {element} = this.editor await element.component.getNextUpdatePromise() - // TODO: move into portal bindings. - // this.updateActivePositions(this.positionsBySiteId) this.editorProxy.didScroll() + this.emitDidScroll() } async editorDidResize () { const {element} = this.editor await element.component.getNextUpdatePromise() - // TODO: move into portal bindings. - // this.updateActivePositions(this.positionsBySiteId) + this.emitDidResize() } updateSelectionsForSiteId (siteId, selections) { diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 6fd11653..f82d8d45 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -3,6 +3,7 @@ const {Errors, FollowState} = require('@atom/teletype-client') const BufferBinding = require('./buffer-binding') const EditorBinding = require('./editor-binding') const EmptyPortalPaneItem = require('./empty-portal-pane-item') +const SitePositionsComponent = require('./site-positions-component') module.exports = class GuestPortalBinding { @@ -20,6 +21,7 @@ class GuestPortalBinding { this.subscriptions = new CompositeDisposable() this.lastEditorProxyChangePromise = Promise.resolve() this.openEditorProxies = new Set() + this.positionsBySiteId = {} } async initialize () { @@ -27,6 +29,15 @@ class GuestPortalBinding { this.portal = await this.client.joinPortal(this.portalId) if (!this.portal) return false + this.aboveViewportSitePositionsComponent = this.buildSitePositionsComponent('upper-right') + this.insideViewportSitePositionsComponent = this.buildSitePositionsComponent('middle-right') + this.outsideViewportSitePositionsComponent = this.buildSitePositionsComponent('lower-right') + + const workspaceElement = this.workspace.getElement() + workspaceElement.appendChild(this.aboveViewportSitePositionsComponent.element) + workspaceElement.appendChild(this.insideViewportSitePositionsComponent.element) + workspaceElement.appendChild(this.outsideViewportSitePositionsComponent.element) + await this.portal.setDelegate(this) if (this.openEditorProxies.size === 0) { await this.openPaneItem(this.getEmptyPortalPaneItem()) @@ -79,7 +90,39 @@ class GuestPortalBinding { return this.lastEditorProxyChangePromise } - updateActivePositions () {} + updateActivePositions (positionsBySiteId) { + const aboveViewportSiteIds = [] + const insideViewportSiteIds = [] + const outsideViewportSiteIds = [] + + for (const siteId in positionsBySiteId) { + const {editorProxy, position} = positionsBySiteId[siteId] + const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) + if (!editorBinding || editorBinding.editor !== this.getActivePaneItem()) { + outsideViewportSiteIds.push(siteId) + } else { + switch (editorBinding.getDirectionFromViewportToPosition(position)) { + case 'upward': + aboveViewportSiteIds.push(siteId) + break + case 'inside': + insideViewportSiteIds.push(siteId) + break + case 'downward': + case 'leftward': + case 'rightward': + outsideViewportSiteIds.push(siteId) + break + } + } + } + + const followedSiteId = null // FIXME + this.aboveViewportSitePositionsComponent.update({siteIds: aboveViewportSiteIds, followedSiteId}) + this.insideViewportSitePositionsComponent.update({siteIds: insideViewportSiteIds, followedSiteId}) + this.outsideViewportSitePositionsComponent.update({siteIds: outsideViewportSiteIds, followedSiteId}) + this.positionsBySiteId = positionsBySiteId + } updateTether (followState, editorProxy, position) { if (!editorProxy) return @@ -122,6 +165,8 @@ class GuestPortalBinding { editor, portal: this.portal, isHost: false, + didScroll: () => this.updateActivePositions(this.positionsBySiteId), + didResize: () => this.updateActivePositions(this.positionsBySiteId), didDispose: () => { this.editorBindingsByEditorProxy.delete(editorProxy) this.editorProxiesByEditor.delete(editor) @@ -219,6 +264,16 @@ class GuestPortalBinding { didChangeActivePaneItem (paneItem) { if (paneItem !== this.getEmptyPortalPaneItem()) { const editorProxy = this.editorProxiesByEditor.get(paneItem) + if (editorProxy) { + this.workspace.element.appendChild(this.aboveViewportSitePositionsComponent.element) + this.workspace.element.appendChild(this.insideViewportSitePositionsComponent.element) + this.workspace.element.appendChild(this.outsideViewportSitePositionsComponent.element) + } else { + this.aboveViewportSitePositionsComponent.element.remove() + this.insideViewportSitePositionsComponent.element.remove() + this.outsideViewportSitePositionsComponent.element.remove() + } + this.portal.activateEditorProxy(editorProxy) } } @@ -239,4 +294,19 @@ class GuestPortalBinding { onDidChange (callback) { return this.emitter.on('did-change', callback) } + + buildSitePositionsComponent (position) { + return new SitePositionsComponent({ + position, + displayedParticipantsCount: 3, + portal: this.portal, + onSelectSiteId: (siteId) => { + if (siteId === this.portal.getFollowedSiteId()) { + this.portal.unfollow() + } else { + this.portal.follow(siteId) + } + } + }) + } } diff --git a/test/editor-binding.test.js b/test/editor-binding.test.js index 9ec1c5d7..f25b7746 100644 --- a/test/editor-binding.test.js +++ b/test/editor-binding.test.js @@ -5,6 +5,12 @@ const SAMPLE_TEXT = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js' const {TextEditor, TextBuffer, Range} = require('atom') const EditorBinding = require('../lib/editor-binding') const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') +const { + setEditorHeightInLines, + setEditorWidthInChars, + setEditorScrollTopInLines, + setEditorScrollLeftInChars +} = require('./helpers/editor-helpers') const {FollowState} = require('@atom/teletype-client') suite('EditorBinding', function () { @@ -347,76 +353,6 @@ suite('EditorBinding', function () { assert.deepEqual(getCursorClasses(editor), []) }) - test.skip('showing the active position of other collaborators', async () => { - // TODO: move this test into portal binding tests. - const editor = new TextEditor({autoHeight: false}) - editor.setText(SAMPLE_TEXT) - - const binding = new EditorBinding({editor, portal: new FakePortal()}) - const editorProxy = new FakeEditorProxy(binding) - binding.setEditorProxy(editorProxy) - - const { - aboveViewportSitePositionsComponent, - insideViewportSitePositionsComponent, - outsideViewportSitePositionsComponent - } = binding - assert(editor.element.contains(aboveViewportSitePositionsComponent.element)) - assert(editor.element.contains(insideViewportSitePositionsComponent.element)) - assert(editor.element.contains(outsideViewportSitePositionsComponent.element)) - - attachToDOM(editor.element) - - await setEditorHeightInLines(editor, 3) - await setEditorWidthInChars(editor, 5) - await setEditorScrollTopInLines(editor, 5) - await setEditorScrollLeftInChars(editor, 5) - - binding.updateActivePositions({ - 1: {row: 2, column: 5}, // collaborator above visible area - 2: {row: 9, column: 5}, // collaborator below visible area - 3: {row: 6, column: 1}, // collaborator to the left of visible area - 4: {row: 6, column: 15}, // collaborator to the right of visible area - 5: {row: 6, column: 6} // collaborator inside of visible area - }) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, [1]) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [5]) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 3, 4]) - - await setEditorScrollLeftInChars(editor, 0) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, [1]) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [3]) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4, 5]) - - await setEditorScrollTopInLines(editor, 2) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, []) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 3, 4, 5]) - - await setEditorHeightInLines(editor, 7) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [3]) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 4, 5]) - - await setEditorWidthInChars(editor, 10) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [1, 3, 5]) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4]) - - // Selecting a site will follow them. - outsideViewportSitePositionsComponent.props.onSelectSiteId(2) - assert.equal(editorProxy.getFollowedSiteId(), 2) - - // Selecting the same site again will unfollow them. - outsideViewportSitePositionsComponent.props.onSelectSiteId(2) - assert.equal(editorProxy.getFollowedSiteId(), null) - }) - test('isScrollNeededToViewPosition(position)', async () => { const editor = new TextEditor({autoHeight: false}) const binding = new EditorBinding({editor, portal: new FakePortal()}) @@ -509,29 +445,6 @@ suite('EditorBinding', function () { attachedElements.push(element) document.body.insertBefore(element, document.body.firstChild) } - - async function setEditorHeightInLines (editor, lines) { - editor.element.style.height = editor.getLineHeightInPixels() * lines + 'px' - return editor.component.getNextUpdatePromise() - } - - async function setEditorWidthInChars (editor, chars) { - editor.element.style.width = - editor.component.getGutterContainerWidth() + - chars * editor.getDefaultCharWidth() + - 'px' - return editor.component.getNextUpdatePromise() - } - - async function setEditorScrollTopInLines (editor, lines) { - editor.element.setScrollTop(editor.getLineHeightInPixels() * lines) - return editor.component.getNextUpdatePromise() - } - - async function setEditorScrollLeftInChars (editor, chars) { - editor.element.setScrollLeft(editor.getDefaultCharWidth() * chars) - return editor.component.getNextUpdatePromise() - } }) class FakeEditorProxy { diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index ccc8c02d..c23ef114 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -1,10 +1,25 @@ const assert = require('assert') +const fs = require('fs') +const path = require('path') const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') const {FollowState, TeletypeClient} = require('@atom/teletype-client') const GuestPortalBinding = require('../lib/guest-portal-binding') +const SAMPLE_TEXT = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js'), 'utf8') +const { + setEditorHeightInLines, + setEditorWidthInChars, + setEditorScrollTopInLines, + setEditorScrollLeftInChars +} = require('./helpers/editor-helpers') suite('GuestPortalBinding', () => { + let attachedElements = [] + teardown(async () => { + while (attachedElements.length > 0) { + attachedElements.pop().remove() + } + await destroyAtomEnvironments() }) @@ -27,20 +42,12 @@ suite('GuestPortalBinding', () => { }) test('showing notifications when sites join or leave', async () => { - const stubPubSubGateway = {} - const client = new TeletypeClient({pubSubGateway: stubPubSubGateway}) - const portal = { - setDelegate (delegate) { - this.delegate = delegate - }, - activateEditorProxy () {}, - getSiteIdentity (siteId) { - return {login: 'site-' + siteId} + const portal = new FakePortal() + const client = { + joinPortal () { + return portal } } - client.joinPortal = function () { - return portal - } const atomEnv = buildAtomEnvironment() const portalBinding = buildGuestPortalBinding(client, atomEnv, 'portal-id') await portalBinding.initialize() @@ -61,12 +68,7 @@ suite('GuestPortalBinding', () => { test('switching the active editor in rapid succession', async () => { const stubPubSubGateway = {} const client = new TeletypeClient({pubSubGateway: stubPubSubGateway}) - const portal = { - getSiteIdentity (siteId) { - return {login: 'some-host'} - }, - dispose () {} - } + const portal = new FakePortal() client.joinPortal = function () { return portal } @@ -79,22 +81,130 @@ suite('GuestPortalBinding', () => { activePaneItemChangeEvents.push(item) }) - portalBinding.updateTether(FollowState.RETRACTED, buildEditorProxy('uri-1')) - portalBinding.updateTether(FollowState.RETRACTED, buildEditorProxy('uri-2')) - await portalBinding.updateTether(FollowState.RETRACTED, buildEditorProxy('uri-3')) + portalBinding.updateTether(FollowState.RETRACTED, new FakeEditorProxy('uri-1')) + portalBinding.updateTether(FollowState.RETRACTED, new FakeEditorProxy('uri-2')) + await portalBinding.updateTether(FollowState.RETRACTED, new FakeEditorProxy('uri-3')) assert.deepEqual( activePaneItemChangeEvents.map((i) => i.getTitle()), - ['@some-host: uri-1', '@some-host: uri-2', '@some-host: uri-3'] + ['@site-1: uri-1', '@site-1: uri-2', '@site-1: uri-3'] ) assert.deepEqual( atomEnv.workspace.getPaneItems().map((i) => i.getTitle()), - ['@some-host: uri-1', '@some-host: uri-2', '@some-host: uri-3'] + ['@site-1: uri-1', '@site-1: uri-2', '@site-1: uri-3'] ) disposable.dispose() }) + test('showing the active position of other collaborators', async () => { + const environment = buildAtomEnvironment() + + // TODO Extract helper: loadPackageStylesheets + // Load also package style sheets, so that additional UI elements are styled + // correctly. + const packageStyleSheetPath = path.join(__dirname, '..', 'styles', 'teletype.less') + const compiledStyleSheet = environment.themes.loadStylesheet(packageStyleSheetPath) + environment.styles.addStyleSheet(compiledStyleSheet) + + const {workspace} = environment + attachToDOM(workspace.getElement()) + + const client = { + joinPortal () { + return new FakePortal() + } + } + const portalBinding = buildGuestPortalBinding(client, environment, 'some-portal') + await portalBinding.initialize() + + const editorProxy1 = new FakeEditorProxy('editor-1') + const editorProxy2 = new FakeEditorProxy('editor-2') + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy1) + + const editor = workspace.getActiveTextEditor() + editor.buffer.setTextInRange([[0, 0], [0, 0]], SAMPLE_TEXT, {undo: 'skip'}) + + const { + aboveViewportSitePositionsComponent, + insideViewportSitePositionsComponent, + outsideViewportSitePositionsComponent + } = portalBinding + assert(workspace.element.contains(aboveViewportSitePositionsComponent.element)) + assert(workspace.element.contains(insideViewportSitePositionsComponent.element)) + assert(workspace.element.contains(outsideViewportSitePositionsComponent.element)) + + await setEditorHeightInLines(editor, 3) + await setEditorWidthInChars(editor, 5) + await setEditorScrollTopInLines(editor, 5) + await setEditorScrollLeftInChars(editor, 5) + + const activePositionsBySiteId = { + 1: {editorProxy: editorProxy1, position: {row: 2, column: 5}}, // collaborator above visible area + 2: {editorProxy: editorProxy1, position: {row: 9, column: 5}}, // collaborator below visible area + 3: {editorProxy: editorProxy1, position: {row: 6, column: 1}}, // collaborator to the left of visible area + 4: {editorProxy: editorProxy1, position: {row: 6, column: 15}}, // collaborator to the right of visible area + 5: {editorProxy: editorProxy1, position: {row: 6, column: 6}}, // collaborator inside of visible area + 6: {editorProxy: editorProxy2, position: {row: 0, column: 0}} // collaborator in a different editor + } + portalBinding.updateActivePositions(activePositionsBySiteId) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, [1]) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [5]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 3, 4, 6]) + + await setEditorScrollLeftInChars(editor, 0) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, [1]) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [3]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4, 5, 6]) + + await setEditorScrollTopInLines(editor, 2) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 3, 4, 5, 6]) + + await setEditorHeightInLines(editor, 7) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [3]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 4, 5, 6]) + + await setEditorWidthInChars(editor, 10) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [1, 3, 5]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4, 6]) + + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy2) + portalBinding.updateActivePositions(activePositionsBySiteId) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [6]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 3, 4, 5]) + + // Selecting a site will follow them. + outsideViewportSitePositionsComponent.props.onSelectSiteId(2) + assert.equal(portalBinding.portal.getFollowedSiteId(), 2) + + // Selecting the same site again will unfollow them. + outsideViewportSitePositionsComponent.props.onSelectSiteId(2) + assert.equal(portalBinding.portal.getFollowedSiteId(), null) + + // Focusing a pane item that does not belong to the portal will hide site positions. + await workspace.open() + assert(!workspace.element.contains(aboveViewportSitePositionsComponent.element)) + assert(!workspace.element.contains(insideViewportSitePositionsComponent.element)) + assert(!workspace.element.contains(outsideViewportSitePositionsComponent.element)) + + // Re-focusing a pane item that belongs to the portal will show site positions again. + await workspace.open(editor) + assert(workspace.element.contains(aboveViewportSitePositionsComponent.element)) + assert(workspace.element.contains(insideViewportSitePositionsComponent.element)) + assert(workspace.element.contains(outsideViewportSitePositionsComponent.element)) + }) + function buildGuestPortalBinding (client, atomEnv, portalId) { return new GuestPortalBinding({ client, @@ -104,21 +214,53 @@ suite('GuestPortalBinding', () => { }) } - function buildEditorProxy (uri) { - const bufferProxy = { - uri, - dispose () {}, - setDelegate () {}, - createCheckpoint () {}, - groupChangesSinceCheckpoint () {}, - applyGroupingInterval () {} + function attachToDOM (element) { + attachedElements.push(element) + document.body.insertBefore(element, document.body.firstChild) + } + + class FakeEditorProxy { + constructor (uri) { + this.bufferProxy = { + uri, + dispose () {}, + setDelegate () {}, + createCheckpoint () {}, + groupChangesSinceCheckpoint () {}, + applyGroupingInterval () {} + } + } + + follow () {} + + didScroll () {} + + setDelegate () {} + + updateSelections () {} + } + + class FakePortal { + follow (siteId) { + this.followedSiteId = siteId + } + + unfollow () { + this.followedSiteId = null + } + + getFollowedSiteId () { + return this.followedSiteId } - const editorProxy = { - bufferProxy, - follow () {}, - setDelegate () {}, - updateSelections () {} + + activateEditorProxy () {} + + setDelegate (delegate) { + this.delegate = delegate + } + + getSiteIdentity (siteId) { + return {login: 'site-' + siteId} } - return editorProxy } }) diff --git a/test/helpers/editor-helpers.js b/test/helpers/editor-helpers.js new file mode 100644 index 00000000..a5ad01f3 --- /dev/null +++ b/test/helpers/editor-helpers.js @@ -0,0 +1,22 @@ +exports.setEditorHeightInLines = async function setEditorHeightInLines (editor, lines) { + editor.element.style.height = editor.getLineHeightInPixels() * lines + 'px' + return editor.component.getNextUpdatePromise() +} + +exports.setEditorWidthInChars = async function setEditorWidthInChars (editor, chars) { + editor.element.style.width = + editor.component.getGutterContainerWidth() + + chars * editor.getDefaultCharWidth() + + 'px' + return editor.component.getNextUpdatePromise() +} + +exports.setEditorScrollTopInLines = async function setEditorScrollTopInLines (editor, lines) { + editor.element.setScrollTop(editor.getLineHeightInPixels() * lines) + return editor.component.getNextUpdatePromise() +} + +exports.setEditorScrollLeftInChars = async function setEditorScrollLeftInChars (editor, chars) { + editor.element.setScrollLeft(editor.getDefaultCharWidth() * chars) + return editor.component.getNextUpdatePromise() +} From 9e9fd75227ed76ba0ac5619b94713d22ec89675e Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 1 Dec 2017 15:25:00 -0500 Subject: [PATCH 21/65] :art: --- lib/guest-portal-binding.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index f82d8d45..3ca6d2f6 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -98,9 +98,7 @@ class GuestPortalBinding { for (const siteId in positionsBySiteId) { const {editorProxy, position} = positionsBySiteId[siteId] const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) - if (!editorBinding || editorBinding.editor !== this.getActivePaneItem()) { - outsideViewportSiteIds.push(siteId) - } else { + if (editorBinding && editorBinding.editor === this.getActivePaneItem()) { switch (editorBinding.getDirectionFromViewportToPosition(position)) { case 'upward': aboveViewportSiteIds.push(siteId) @@ -114,6 +112,8 @@ class GuestPortalBinding { outsideViewportSiteIds.push(siteId) break } + } else { + outsideViewportSiteIds.push(siteId) } } From 8697e52a53a1960ed645c2764c4f2e13f7012bb0 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 1 Dec 2017 16:25:36 -0500 Subject: [PATCH 22/65] :fire: Remove obsolete code in EditorBinding As of 4b3379cf117ae038f6ddf7481a09f22a63f93530, this functionality lives in GuestPortalBinding. --- lib/editor-binding.js | 52 ------------------------------------------- 1 file changed, 52 deletions(-) diff --git a/lib/editor-binding.js b/lib/editor-binding.js index 849c87b8..9595e465 100644 --- a/lib/editor-binding.js +++ b/lib/editor-binding.js @@ -4,7 +4,6 @@ const path = require('path') const {Range, Emitter, Disposable, CompositeDisposable} = require('atom') const normalizeURI = require('./normalize-uri') const {FollowState} = require('@atom/teletype-client') -const SitePositionsComponent = require('./site-positions-component') function doNothing () {} @@ -34,10 +33,6 @@ class EditorBinding { if (!this.isHost) this.restoreOriginalEditorMethods(this.editor) if (this.localCursorLayerDecoration) this.localCursorLayerDecoration.destroy() - this.aboveViewportSitePositionsComponent.destroy() - this.insideViewportSitePositionsComponent.destroy() - this.outsideViewportSitePositionsComponent.destroy() - this.emitDidDispose() } @@ -63,14 +58,6 @@ class EditorBinding { this.subscriptions.add(this.editor.element.onDidChangeScrollLeft(this.editorDidChangeScrollLeft.bind(this))) this.subscriptions.add(subscribeToResizeEvents(this.editor.element, this.editorDidResize.bind(this))) this.relayLocalSelections() - - this.aboveViewportSitePositionsComponent = this.buildSitePositionsComponent('upper-right') - this.insideViewportSitePositionsComponent = this.buildSitePositionsComponent('middle-right') - this.outsideViewportSitePositionsComponent = this.buildSitePositionsComponent('lower-right') - - this.editor.element.appendChild(this.aboveViewportSitePositionsComponent.element) - this.editor.element.appendChild(this.insideViewportSitePositionsComponent.element) - this.editor.element.appendChild(this.outsideViewportSitePositionsComponent.element) } monkeyPatchEditorMethods (editor, editorProxy) { @@ -227,36 +214,6 @@ class EditorBinding { this.localCursorLayerDecoration.setProperties(localCursorDecorationProperties) } - updateActivePositions (positionsBySiteId) { - const aboveViewportSiteIds = [] - const insideViewportSiteIds = [] - const outsideViewportSiteIds = [] - const followedSiteId = this.editorProxy.getFollowedSiteId() - - for (let siteId in positionsBySiteId) { - siteId = parseInt(siteId) - const position = positionsBySiteId[siteId] - switch (this.getDirectionFromViewportToPosition(position)) { - case 'upward': - aboveViewportSiteIds.push(siteId) - break - case 'inside': - insideViewportSiteIds.push(siteId) - break - case 'downward': - case 'leftward': - case 'rightward': - outsideViewportSiteIds.push(siteId) - break - } - } - - this.aboveViewportSitePositionsComponent.update({siteIds: aboveViewportSiteIds, followedSiteId}) - this.insideViewportSitePositionsComponent.update({siteIds: insideViewportSiteIds, followedSiteId}) - this.outsideViewportSitePositionsComponent.update({siteIds: outsideViewportSiteIds, followedSiteId}) - this.positionsBySiteId = positionsBySiteId - } - getDirectionFromViewportToPosition (bufferPosition) { const {element} = this.editor if (!document.contains(element)) return @@ -312,15 +269,6 @@ class EditorBinding { } } - buildSitePositionsComponent (position) { - return new SitePositionsComponent({ - position, - displayedParticipantsCount: 3, - portal: this.portal, - onSelectSiteId: this.toggleFollowingForSiteId.bind(this) - }) - } - toggleFollowingForSiteId (siteId) { if (siteId === this.editorProxy.getFollowedSiteId()) { this.editorProxy.unfollow() From c5ab19ac1b0eab6eb60d2451d5a2dbbf05a2f6e2 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 1 Dec 2017 16:25:44 -0500 Subject: [PATCH 23/65] =?UTF-8?q?=F0=9F=92=9A=20Apply=20temporary=20hack?= =?UTF-8?q?=20to=20get=20the=20integration=20tests=20passing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/site-positions-component.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/site-positions-component.js b/lib/site-positions-component.js index f9fb9857..b18d58ca 100644 --- a/lib/site-positions-component.js +++ b/lib/site-positions-component.js @@ -28,6 +28,10 @@ class SitePositionsComponent { renderSite (siteId) { const {portal, followedSiteId} = this.props + + // FIXME Without this statement, tests fail in strange ways 🤔 + if (!portal.getSiteIdentity(siteId)) return null + const {login} = portal.getSiteIdentity(siteId) return $.div({className: 'SitePositionsComponent-site'}, From 306b707a2341b4b8af11cdf68e360af4a8ef6658 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 1 Dec 2017 16:42:46 -0500 Subject: [PATCH 24/65] Dispose of SitePositionComponents when disposing of GuestPortalBinding --- lib/guest-portal-binding.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 3ca6d2f6..15f83f9d 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -54,7 +54,11 @@ class GuestPortalBinding { dispose () { this.subscriptions.dispose() this.openEditorProxies.clear() + this.aboveViewportSitePositionsComponent.destroy() + this.insideViewportSitePositionsComponent.destroy() + this.outsideViewportSitePositionsComponent.destroy() if (this.emptyPortalItem) this.emptyPortalItem.destroy() + this.emitDidDispose() } From 5d0720d5b17a8810f8a06f96c5064a47fd32c134 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 1 Dec 2017 16:43:50 -0500 Subject: [PATCH 25/65] If we don't have a position for a site, assume it's outside the viewport --- lib/guest-portal-binding.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 15f83f9d..5a2ab91e 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -102,7 +102,7 @@ class GuestPortalBinding { for (const siteId in positionsBySiteId) { const {editorProxy, position} = positionsBySiteId[siteId] const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) - if (editorBinding && editorBinding.editor === this.getActivePaneItem()) { + if (position && editorBinding && editorBinding.editor === this.getActivePaneItem()) { switch (editorBinding.getDirectionFromViewportToPosition(position)) { case 'upward': aboveViewportSiteIds.push(siteId) From 48475914ffec0978df2ad085430cc59de63cb9f9 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 1 Dec 2017 16:47:26 -0500 Subject: [PATCH 26/65] Add TODOs --- lib/guest-portal-binding.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 5a2ab91e..7fbb6c1f 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -33,6 +33,7 @@ class GuestPortalBinding { this.insideViewportSitePositionsComponent = this.buildSitePositionsComponent('middle-right') this.outsideViewportSitePositionsComponent = this.buildSitePositionsComponent('lower-right') + // TODO Extract method const workspaceElement = this.workspace.getElement() workspaceElement.appendChild(this.aboveViewportSitePositionsComponent.element) workspaceElement.appendChild(this.insideViewportSitePositionsComponent.element) @@ -269,10 +270,12 @@ class GuestPortalBinding { if (paneItem !== this.getEmptyPortalPaneItem()) { const editorProxy = this.editorProxiesByEditor.get(paneItem) if (editorProxy) { + // TODO Extract method this.workspace.element.appendChild(this.aboveViewportSitePositionsComponent.element) this.workspace.element.appendChild(this.insideViewportSitePositionsComponent.element) this.workspace.element.appendChild(this.outsideViewportSitePositionsComponent.element) } else { + // TODO Extract method this.aboveViewportSitePositionsComponent.element.remove() this.insideViewportSitePositionsComponent.element.remove() this.outsideViewportSitePositionsComponent.element.remove() From c5ca1fa1810ca2f3e8be767f64cf65fcd91e8a17 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 1 Dec 2017 17:16:09 -0500 Subject: [PATCH 27/65] :art: Extract methods --- lib/guest-portal-binding.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 7fbb6c1f..5435d841 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -32,12 +32,7 @@ class GuestPortalBinding { this.aboveViewportSitePositionsComponent = this.buildSitePositionsComponent('upper-right') this.insideViewportSitePositionsComponent = this.buildSitePositionsComponent('middle-right') this.outsideViewportSitePositionsComponent = this.buildSitePositionsComponent('lower-right') - - // TODO Extract method - const workspaceElement = this.workspace.getElement() - workspaceElement.appendChild(this.aboveViewportSitePositionsComponent.element) - workspaceElement.appendChild(this.insideViewportSitePositionsComponent.element) - workspaceElement.appendChild(this.outsideViewportSitePositionsComponent.element) + this.showSitePositions() await this.portal.setDelegate(this) if (this.openEditorProxies.size === 0) { @@ -270,15 +265,9 @@ class GuestPortalBinding { if (paneItem !== this.getEmptyPortalPaneItem()) { const editorProxy = this.editorProxiesByEditor.get(paneItem) if (editorProxy) { - // TODO Extract method - this.workspace.element.appendChild(this.aboveViewportSitePositionsComponent.element) - this.workspace.element.appendChild(this.insideViewportSitePositionsComponent.element) - this.workspace.element.appendChild(this.outsideViewportSitePositionsComponent.element) + this.showSitePositions() } else { - // TODO Extract method - this.aboveViewportSitePositionsComponent.element.remove() - this.insideViewportSitePositionsComponent.element.remove() - this.outsideViewportSitePositionsComponent.element.remove() + this.hideSitePositions() } this.portal.activateEditorProxy(editorProxy) @@ -302,6 +291,19 @@ class GuestPortalBinding { return this.emitter.on('did-change', callback) } + showSitePositions () { + const workspaceElement = this.workspace.getElement() + workspaceElement.appendChild(this.aboveViewportSitePositionsComponent.element) + workspaceElement.appendChild(this.insideViewportSitePositionsComponent.element) + workspaceElement.appendChild(this.outsideViewportSitePositionsComponent.element) + } + + hideSitePositions () { + this.aboveViewportSitePositionsComponent.element.remove() + this.insideViewportSitePositionsComponent.element.remove() + this.outsideViewportSitePositionsComponent.element.remove() + } + buildSitePositionsComponent (position) { return new SitePositionsComponent({ position, From 9a484f7dfcbc3f28e73e0890a089a610902a2b69 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 1 Dec 2017 17:17:07 -0500 Subject: [PATCH 28/65] Identify site position helper methods as part of the class's private API --- lib/guest-portal-binding.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 5435d841..75418bb9 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -291,6 +291,7 @@ class GuestPortalBinding { return this.emitter.on('did-change', callback) } + // Private showSitePositions () { const workspaceElement = this.workspace.getElement() workspaceElement.appendChild(this.aboveViewportSitePositionsComponent.element) @@ -298,12 +299,14 @@ class GuestPortalBinding { workspaceElement.appendChild(this.outsideViewportSitePositionsComponent.element) } + // Private hideSitePositions () { this.aboveViewportSitePositionsComponent.element.remove() this.insideViewportSitePositionsComponent.element.remove() this.outsideViewportSitePositionsComponent.element.remove() } + // Private buildSitePositionsComponent (position) { return new SitePositionsComponent({ position, From 4e7b3c7d3891321a60e267906de759154024afd6 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 1 Dec 2017 17:28:52 -0500 Subject: [PATCH 29/65] :art: Extract helper function: loadPackageStyleSheets --- test/editor-binding.test.js | 7 ++----- test/guest-portal-binding.test.js | 9 ++------- test/helpers/ui-helpers.js | 9 +++++++++ 3 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 test/helpers/ui-helpers.js diff --git a/test/editor-binding.test.js b/test/editor-binding.test.js index f25b7746..f81042a8 100644 --- a/test/editor-binding.test.js +++ b/test/editor-binding.test.js @@ -5,6 +5,7 @@ const SAMPLE_TEXT = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js' const {TextEditor, TextBuffer, Range} = require('atom') const EditorBinding = require('../lib/editor-binding') const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') +const {loadPackageStyleSheets} = require('./helpers/ui-helpers') const { setEditorHeightInLines, setEditorWidthInChars, @@ -21,11 +22,7 @@ suite('EditorBinding', function () { setup(() => { // Load the editor default styles by instantiating a new AtomEnvironment. const environment = buildAtomEnvironment() - // Load also package style sheets, so that additional UI elements are styled - // correctly. - const packageStyleSheetPath = path.join(__dirname, '..', 'styles', 'teletype.less') - const compiledStyleSheet = environment.themes.loadStylesheet(packageStyleSheetPath) - environment.styles.addStyleSheet(compiledStyleSheet) + loadPackageStyleSheets(environment) // Position editor absolutely to prevent its size from being affected by the // window size of the test runner. We also give it an initial width and // height so that the editor component can perform initial measurements. diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index c23ef114..f906aa30 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -2,6 +2,7 @@ const assert = require('assert') const fs = require('fs') const path = require('path') const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') +const {loadPackageStyleSheets} = require('./helpers/ui-helpers') const {FollowState, TeletypeClient} = require('@atom/teletype-client') const GuestPortalBinding = require('../lib/guest-portal-binding') const SAMPLE_TEXT = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js'), 'utf8') @@ -100,13 +101,7 @@ suite('GuestPortalBinding', () => { test('showing the active position of other collaborators', async () => { const environment = buildAtomEnvironment() - // TODO Extract helper: loadPackageStylesheets - // Load also package style sheets, so that additional UI elements are styled - // correctly. - const packageStyleSheetPath = path.join(__dirname, '..', 'styles', 'teletype.less') - const compiledStyleSheet = environment.themes.loadStylesheet(packageStyleSheetPath) - environment.styles.addStyleSheet(compiledStyleSheet) - + loadPackageStyleSheets(environment) const {workspace} = environment attachToDOM(workspace.getElement()) diff --git a/test/helpers/ui-helpers.js b/test/helpers/ui-helpers.js new file mode 100644 index 00000000..8f1635c9 --- /dev/null +++ b/test/helpers/ui-helpers.js @@ -0,0 +1,9 @@ +const path = require('path') + +// Load package style sheets for the given environment so that the package's +// UI elements are styled correctly. +exports.loadPackageStyleSheets = function (environment) { + const packageStyleSheetPath = path.join(__dirname, '..', '..', 'styles', 'teletype.less') + const compiledStyleSheet = environment.themes.loadStylesheet(packageStyleSheetPath) + environment.styles.addStyleSheet(compiledStyleSheet) +} From d52bb83469095aa2b9981ff363943a46494d9e90 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Dec 2017 11:06:11 +0100 Subject: [PATCH 30/65] Show site active positions for guest portals --- lib/guest-portal-binding.js | 7 +++++-- lib/site-positions-component.js | 4 ---- test/teletype-package.test.js | 2 ++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 75418bb9..6507f7f6 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -95,7 +95,10 @@ class GuestPortalBinding { const insideViewportSiteIds = [] const outsideViewportSiteIds = [] - for (const siteId in positionsBySiteId) { + for (let siteId in positionsBySiteId) { + siteId = parseInt(siteId) + if (siteId === this.portal.siteId) continue + const {editorProxy, position} = positionsBySiteId[siteId] const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) if (position && editorBinding && editorBinding.editor === this.getActivePaneItem()) { @@ -117,7 +120,7 @@ class GuestPortalBinding { } } - const followedSiteId = null // FIXME + const followedSiteId = this.portal.getFollowedSiteId() this.aboveViewportSitePositionsComponent.update({siteIds: aboveViewportSiteIds, followedSiteId}) this.insideViewportSitePositionsComponent.update({siteIds: insideViewportSiteIds, followedSiteId}) this.outsideViewportSitePositionsComponent.update({siteIds: outsideViewportSiteIds, followedSiteId}) diff --git a/lib/site-positions-component.js b/lib/site-positions-component.js index b18d58ca..f9fb9857 100644 --- a/lib/site-positions-component.js +++ b/lib/site-positions-component.js @@ -28,10 +28,6 @@ class SitePositionsComponent { renderSite (siteId) { const {portal, followedSiteId} = this.props - - // FIXME Without this statement, tests fail in strange ways 🤔 - if (!portal.getSiteIdentity(siteId)) return null - const {login} = portal.getSiteIdentity(siteId) return $.div({className: 'SitePositionsComponent-site'}, diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index 8814f6e6..599cde50 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -3,6 +3,7 @@ const {Errors} = require('@atom/teletype-client') const {TextBuffer, TextEditor} = require('atom') const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') +const {loadPackageStyleSheets} = require('./helpers/ui-helpers') const assert = require('assert') const condition = require('./helpers/condition') const deepEqual = require('deep-equal') @@ -830,6 +831,7 @@ suite('TeletypePackage', function () { async function verifyTetheringRules ({leaderEnv, leaderPortal, followerEnv, followerPortal}) { // Setup DOM for follower's workspace. + loadPackageStyleSheets(followerEnv) const followerWorkspaceElement = followerEnv.views.getView(followerEnv.workspace) followerWorkspaceElement.style.height = '100px' followerWorkspaceElement.style.width = '250px' From 89e465d7bfac49176cac2dcc2dd0cb836490fde2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Dec 2017 11:21:11 +0100 Subject: [PATCH 31/65] Remove unnecessary conditional --- lib/guest-portal-binding.js | 10 ++++------ test/teletype-package.test.js | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 6507f7f6..d1d20ac9 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -256,12 +256,10 @@ class GuestPortalBinding { } async openPaneItem (newActivePaneItem) { - if (newActivePaneItem !== this.getActivePaneItem()) { - this.newActivePaneItem = newActivePaneItem - await this.workspace.open(newActivePaneItem) - this.activePaneItem = this.newActivePaneItem - this.newActivePaneItem = null - } + this.newActivePaneItem = newActivePaneItem + await this.workspace.open(newActivePaneItem) + this.activePaneItem = this.newActivePaneItem + this.newActivePaneItem = null } didChangeActivePaneItem (paneItem) { diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index 599cde50..337c9e88 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -200,7 +200,7 @@ suite('TeletypePackage', function () { // Portal 2 host continues to exist as a guest in Portal 1 hostOnlyEnv.workspace.open(path.join(temp.path(), 'host-only-buffer-2')) const guestAndHostRemotePaneItem2 = await getNextRemotePaneItemPromise(guestAndHostEnv) - assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2]) + assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostRemotePaneItem2, guestAndHostLocalEditor1]) assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) // No transitivity: When Portal 2 host is viewing contents of Portal 1, Portal 2 guests can only see contents of Portal 2 @@ -212,7 +212,7 @@ suite('TeletypePackage', function () { // As Portal 2 host observes changes in Portal 1, Portal 2 guests continue to only see contents of Portal 2 await hostOnlyEnv.workspace.open(path.join(temp.path(), 'host-only-buffer-3')) const guestAndHostRemotePaneItem3 = await getNextRemotePaneItemPromise(guestAndHostEnv) - assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostRemotePaneItem3, guestAndHostLocalEditor1, guestAndHostRemotePaneItem2]) + assert.deepEqual(getPaneItems(guestAndHostEnv), [guestAndHostRemotePaneItem1, guestAndHostRemotePaneItem2, guestAndHostRemotePaneItem3, guestAndHostLocalEditor1]) assert.deepEqual(getPaneItems(guestOnlyEnv), [guestOnlyRemotePaneItem1]) // When Portal 2 host shares another local buffer, Portal 2 guests see that buffer From 18a8da7278a6f9d78a41ba481ac350415a254a9c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Dec 2017 13:14:13 +0100 Subject: [PATCH 32/65] Update site positions correctly when manually switching tabs --- lib/guest-portal-binding.js | 23 ++++++++++++++--------- lib/portal-binding-manager.js | 2 +- test/guest-portal-binding.test.js | 31 +++++++++++++++++++------------ 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index d1d20ac9..460968a8 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -13,7 +13,7 @@ class GuestPortalBinding { this.workspace = workspace this.notificationManager = notificationManager this.emitDidDispose = didDispose - this.activePaneItem = null + this.lastActivePaneItem = null this.editorBindingsByEditorProxy = new Map() this.bufferBindingsByBufferProxy = new Map() this.editorProxiesByEditor = new WeakMap() @@ -203,10 +203,10 @@ class GuestPortalBinding { } activate () { - const activePaneItem = this.getActivePaneItem() - const pane = this.workspace.paneForItem(activePaneItem) - if (pane && activePaneItem) { - pane.activateItem(activePaneItem) + const paneItem = this.lastActivePaneItem + const pane = this.workspace.paneForItem(paneItem) + if (pane && paneItem) { + pane.activateItem(paneItem) pane.activate() } } @@ -233,7 +233,6 @@ class GuestPortalBinding { description: 'Your host stopped sharing their editor.', dismissable: true }) - this.activePaneItem = null } hostDidLoseConnection () { @@ -244,7 +243,6 @@ class GuestPortalBinding { ), dismissable: true }) - this.activePaneItem = null } leave () { @@ -258,7 +256,7 @@ class GuestPortalBinding { async openPaneItem (newActivePaneItem) { this.newActivePaneItem = newActivePaneItem await this.workspace.open(newActivePaneItem) - this.activePaneItem = this.newActivePaneItem + this.lastActivePaneItem = this.newActivePaneItem this.newActivePaneItem = null } @@ -275,8 +273,15 @@ class GuestPortalBinding { } } + hasPaneItem (paneItem) { + return ( + paneItem === this.getEmptyPortalPaneItem() || + this.editorProxiesByEditor.has(paneItem) + ) + } + getActivePaneItem () { - return this.newActivePaneItem ? this.newActivePaneItem : this.activePaneItem + return this.newActivePaneItem || this.workspace.getActivePaneItem() } getEmptyPortalPaneItem () { diff --git a/lib/portal-binding-manager.js b/lib/portal-binding-manager.js index 548ab99f..c0435d08 100644 --- a/lib/portal-binding-manager.js +++ b/lib/portal-binding-manager.js @@ -110,7 +110,7 @@ class PortalBindingManager { const activePaneItem = this.workspace.getActivePaneItem() for (const [_, portalBindingPromise] of this.promisesByGuestPortalId) { // eslint-disable-line no-unused-vars const portalBinding = await portalBindingPromise - if (portalBinding.getActivePaneItem() === activePaneItem) { + if (portalBinding.hasPaneItem(activePaneItem)) { return portalBinding } } diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index f906aa30..57745e32 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -117,8 +117,8 @@ suite('GuestPortalBinding', () => { const editorProxy2 = new FakeEditorProxy('editor-2') await portalBinding.updateTether(FollowState.RETRACTED, editorProxy1) - const editor = workspace.getActiveTextEditor() - editor.buffer.setTextInRange([[0, 0], [0, 0]], SAMPLE_TEXT, {undo: 'skip'}) + const editor1 = workspace.getActiveTextEditor() + editor1.buffer.setTextInRange([[0, 0], [0, 0]], SAMPLE_TEXT, {undo: 'skip'}) const { aboveViewportSitePositionsComponent, @@ -129,10 +129,10 @@ suite('GuestPortalBinding', () => { assert(workspace.element.contains(insideViewportSitePositionsComponent.element)) assert(workspace.element.contains(outsideViewportSitePositionsComponent.element)) - await setEditorHeightInLines(editor, 3) - await setEditorWidthInChars(editor, 5) - await setEditorScrollTopInLines(editor, 5) - await setEditorScrollLeftInChars(editor, 5) + await setEditorHeightInLines(editor1, 3) + await setEditorWidthInChars(editor1, 5) + await setEditorScrollTopInLines(editor1, 5) + await setEditorScrollLeftInChars(editor1, 5) const activePositionsBySiteId = { 1: {editorProxy: editorProxy1, position: {row: 2, column: 5}}, // collaborator above visible area @@ -148,25 +148,25 @@ suite('GuestPortalBinding', () => { assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [5]) assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 3, 4, 6]) - await setEditorScrollLeftInChars(editor, 0) + await setEditorScrollLeftInChars(editor1, 0) assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, [1]) assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [3]) assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4, 5, 6]) - await setEditorScrollTopInLines(editor, 2) + await setEditorScrollTopInLines(editor1, 2) assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, []) assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 3, 4, 5, 6]) - await setEditorHeightInLines(editor, 7) + await setEditorHeightInLines(editor1, 7) assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [3]) assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 4, 5, 6]) - await setEditorWidthInChars(editor, 10) + await setEditorWidthInChars(editor1, 10) assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [1, 3, 5]) @@ -189,15 +189,22 @@ suite('GuestPortalBinding', () => { // Focusing a pane item that does not belong to the portal will hide site positions. await workspace.open() + assert(!workspace.element.contains(aboveViewportSitePositionsComponent.element)) assert(!workspace.element.contains(insideViewportSitePositionsComponent.element)) assert(!workspace.element.contains(outsideViewportSitePositionsComponent.element)) - // Re-focusing a pane item that belongs to the portal will show site positions again. - await workspace.open(editor) + // Focusing a pane item that belongs to the portal will show site positions again. + await workspace.open(editor1) + portalBinding.updateActivePositions(activePositionsBySiteId) + assert(workspace.element.contains(aboveViewportSitePositionsComponent.element)) assert(workspace.element.contains(insideViewportSitePositionsComponent.element)) assert(workspace.element.contains(outsideViewportSitePositionsComponent.element)) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [1, 3, 5]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4, 6]) }) function buildGuestPortalBinding (client, atomEnv, portalId) { From 37d642a3f8d4cddc37095c57f4262972b54e1329 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Dec 2017 13:35:35 +0100 Subject: [PATCH 33/65] :art: --- lib/guest-portal-binding.js | 43 ++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 460968a8..474cac87 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -128,30 +128,33 @@ class GuestPortalBinding { } updateTether (followState, editorProxy, position) { - if (!editorProxy) return - - this.lastEditorProxyChangePromise = this.lastEditorProxyChangePromise.then(async () => { - if (followState === FollowState.RETRACTED) { - const editor = this.findOrCreateEditorForEditorProxy(editorProxy) - await this.openPaneItem(editor) + if (editorProxy) { + this.lastEditorProxyChangePromise = this.lastEditorProxyChangePromise.then(() => + this._updateTether(followState, editorProxy, position) + ) + } - this.openEditorProxies.add(editorProxy) - if (this.openEditorProxies.size > 0) { - this.getEmptyPortalPaneItem().destroy() - } - } else { - this.editorBindingsByEditorProxy.forEach((editorBinding) => { - editorBinding.updateTether(followState) - }) - } + return this.lastEditorProxyChangePromise + } - const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) - if (editorBinding && position) { - editorBinding.updateTether(followState, position) + // Private + async _updateTether (followState, editorProxy, position) { + if (followState === FollowState.RETRACTED) { + const editor = this.findOrCreateEditorForEditorProxy(editorProxy) + await this.openPaneItem(editor) + + this.openEditorProxies.add(editorProxy) + if (this.openEditorProxies.size > 0) { + this.getEmptyPortalPaneItem().destroy() } - }) + } else { + this.editorBindingsByEditorProxy.forEach((b) => b.updateTether(followState)) + } - return this.lastEditorProxyChangePromise + const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) + if (editorBinding && position) { + editorBinding.updateTether(followState, position) + } } // Private From 1209421f5e3306e8d27f810d1f283e160c0a41db Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Dec 2017 14:25:07 +0100 Subject: [PATCH 34/65] Simplify toggling of empty portal pane item --- lib/guest-portal-binding.js | 32 ++++++++++++----------- test/portal-binding-manager.test.js | 39 ++++++----------------------- 2 files changed, 24 insertions(+), 47 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 474cac87..211688b7 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -20,7 +20,6 @@ class GuestPortalBinding { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.lastEditorProxyChangePromise = Promise.resolve() - this.openEditorProxies = new Set() this.positionsBySiteId = {} } @@ -35,9 +34,7 @@ class GuestPortalBinding { this.showSitePositions() await this.portal.setDelegate(this) - if (this.openEditorProxies.size === 0) { - await this.openPaneItem(this.getEmptyPortalPaneItem()) - } + await this.toggleEmptyPortalPaneItem() this.subscriptions.add(this.workspace.onDidChangeActivePaneItem(this.didChangeActivePaneItem.bind(this))) return true @@ -49,7 +46,6 @@ class GuestPortalBinding { dispose () { this.subscriptions.dispose() - this.openEditorProxies.clear() this.aboveViewportSitePositionsComponent.destroy() this.insideViewportSitePositionsComponent.destroy() this.outsideViewportSitePositionsComponent.destroy() @@ -78,12 +74,11 @@ class GuestPortalBinding { removeEditorProxy (editorProxy) { this.lastEditorProxyChangePromise = this.lastEditorProxyChangePromise.then(async () => { - this.openEditorProxies.delete(editorProxy) - if (this.openEditorProxies.size === 0) { - await this.openPaneItem(this.getEmptyPortalPaneItem()) - } - const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) + editorBinding.dispose() + + await this.toggleEmptyPortalPaneItem() + editorBinding.editor.destroy() }) @@ -142,11 +137,7 @@ class GuestPortalBinding { if (followState === FollowState.RETRACTED) { const editor = this.findOrCreateEditorForEditorProxy(editorProxy) await this.openPaneItem(editor) - - this.openEditorProxies.add(editorProxy) - if (this.openEditorProxies.size > 0) { - this.getEmptyPortalPaneItem().destroy() - } + await this.toggleEmptyPortalPaneItem() } else { this.editorBindingsByEditorProxy.forEach((b) => b.updateTether(followState)) } @@ -205,6 +196,17 @@ class GuestPortalBinding { return buffer } + // Private + async toggleEmptyPortalPaneItem () { + const emptyPortalItem = this.getEmptyPortalPaneItem() + const pane = this.workspace.paneForItem(emptyPortalItem) + if (this.editorBindingsByEditorProxy.size === 0) { + if (!pane) await this.openPaneItem(emptyPortalItem) + } else { + if (pane) emptyPortalItem.destroy() + } + } + activate () { const paneItem = this.lastActivePaneItem const pane = this.workspace.paneForItem(paneItem) diff --git a/test/portal-binding-manager.test.js b/test/portal-binding-manager.test.js index 76afa97d..489c7bc8 100644 --- a/test/portal-binding-manager.test.js +++ b/test/portal-binding-manager.test.js @@ -1,8 +1,12 @@ const assert = require('assert') -const {Disposable} = require('atom') +const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') const PortalBindingManager = require('../lib/portal-binding-manager') suite('PortalBindingManager', () => { + teardown(async () => { + await destroyAtomEnvironments() + }) + suite('host portal binding', () => { test('idempotently creating the host portal binding', async () => { const manager = buildPortalBindingManager() @@ -85,6 +89,7 @@ suite('PortalBindingManager', () => { }) function buildPortalBindingManager () { + const {workspace, notifications: notificationManager} = buildAtomEnvironment() const client = { resolveLastCreatePortalPromise: null, resolveLastJoinPortalPromise: null, @@ -95,43 +100,13 @@ function buildPortalBindingManager () { return new Promise((resolve) => { this.resolveLastJoinPortalPromise = resolve }) } } - - const notificationManager = { - addInfo () {}, - addSuccess () {}, - addError (error, options) { - throw new Error(error + '\n' + options.description) - } - } - - const workspace = { - element: document.createElement('div'), - async open () { - - }, - getElement () { - return this.element - }, - observeActiveTextEditor () { - return new Disposable(() => {}) - }, - observeActivePaneItem () { - return new Disposable(() => {}) - }, - onDidChangeActivePaneItem () { - return new Disposable(() => {}) - }, - onDidDestroyPaneItem () { - return new Disposable(() => {}) - } - } - return new PortalBindingManager({client, workspace, notificationManager}) } let nextIdentityId = 1 function buildPortal () { return { + activateEditorProxy () {}, getSiteIdentity () { return {login: 'identity-' + nextIdentityId++} }, From 865eb7aae0be3e6abc6ab324940a9fe1768c99f6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Dec 2017 14:50:39 +0100 Subject: [PATCH 35/65] :art: Extract findOrCreate methods in HostPortalBinding --- lib/host-portal-binding.js | 65 +++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index 8118005b..397da62c 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -65,45 +65,58 @@ class HostPortalBinding { } didChangeActiveTextEditor (editor) { - if (editor == null || editor.isRemote) return + if (editor && !editor.isRemote) { + const editorProxy = this.findOrCreateEditorProxyForEditor(editor) + this.portal.activateEditorProxy(editorProxy) + } else { + this.portal.activateEditorProxy(null) + } + } - let editorBinding = this.editorBindingsByEditor.get(editor) - if (!editorBinding) { - const buffer = editor.getBuffer() - - let bufferBinding = this.bufferBindingsByBuffer.get(buffer) - let bufferProxy = bufferBinding ? bufferBinding.bufferProxy : null - if (!bufferBinding) { - bufferBinding = new BufferBinding({buffer}) - bufferProxy = this.portal.createBufferProxy({ - uri: this.getBufferProxyURI(buffer), - history: buffer.getHistory() - }) - bufferBinding.setBufferProxy(bufferProxy) - bufferProxy.setDelegate(bufferBinding) - - this.bufferBindingsByBuffer.set(buffer, bufferBinding) - } + updateActivePositions () {} + + updateTether () {} + + didDestroyPaneItem ({item}) { + const editorBinding = this.editorBindingsByEditor.get(item) + if (editorBinding) { + this.portal.removeEditorProxy(editorBinding.editorProxy) + } + } + findOrCreateEditorProxyForEditor (editor) { + let editorBinding = this.editorBindingsByEditor.get(editor) + if (editorBinding) { + return editorBinding.editorProxy + } else { editorBinding = new EditorBinding({editor, portal: this.portal, isHost: true}) + const bufferProxy = this.findOrCreateBufferProxyForBuffer(editor.getBuffer()) const editorProxy = this.portal.createEditorProxy({bufferProxy}) editorBinding.setEditorProxy(editorProxy) editorProxy.setDelegate(editorBinding) this.editorBindingsByEditor.set(editor, editorBinding) - } - this.portal.activateEditorProxy(editorBinding.editorProxy) + return editorProxy + } } - updateActivePositions () {} + findOrCreateBufferProxyForBuffer (buffer) { + let bufferBinding = this.bufferBindingsByBuffer.get(buffer) + if (bufferBinding) { + return bufferBinding.bufferProxy + } else { + bufferBinding = new BufferBinding({buffer}) + const bufferProxy = this.portal.createBufferProxy({ + uri: this.getBufferProxyURI(buffer), + history: buffer.getHistory() + }) + bufferBinding.setBufferProxy(bufferProxy) + bufferProxy.setDelegate(bufferBinding) - updateTether () {} + this.bufferBindingsByBuffer.set(buffer, bufferBinding) - didDestroyPaneItem ({item}) { - const editorBinding = this.editorBindingsByEditor.get(item) - if (editorBinding) { - this.portal.removeEditorProxy(editorBinding.editorProxy) + return bufferProxy } } From 5ba7652550e646713dad6a10ae827cba9a59f8ef Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Dec 2017 16:48:39 +0100 Subject: [PATCH 36/65] Implement `updateTether` also on `HostPortalBinding` Signed-off-by: Jason Rudolph --- lib/host-portal-binding.js | 31 ++++++++++++++++++++++++++++++- test/teletype-package.test.js | 2 +- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index 397da62c..52439ed7 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -1,5 +1,6 @@ const path = require('path') const {CompositeDisposable, Emitter} = require('atom') +const {FollowState} = require('@atom/teletype-client') const BufferBinding = require('./buffer-binding') const EditorBinding = require('./editor-binding') @@ -10,9 +11,11 @@ class HostPortalBinding { this.workspace = workspace this.notificationManager = notificationManager this.editorBindingsByEditor = new WeakMap() + this.editorBindingsByEditorProxy = new Map() this.bufferBindingsByBuffer = new WeakMap() this.disposables = new CompositeDisposable() this.emitter = new Emitter() + this.lastUpdateTetherPromise = Promise.resolve() this.didDispose = didDispose } @@ -75,7 +78,26 @@ class HostPortalBinding { updateActivePositions () {} - updateTether () {} + updateTether (followState, editorProxy, position) { + if (editorProxy) { + this.lastUpdateTetherPromise = this.lastUpdateTetherPromise.then(() => + this._updateTether(followState, editorProxy, position) + ) + } + } + + // Private + async _updateTether (followState, editorProxy, position) { + const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) + + if (followState === FollowState.RETRACTED) { + await this.workspace.open(editorBinding.editor) + } else { + this.editorBindingsByEditorProxy.forEach((b) => b.updateTether(followState)) + } + + if (position) editorBinding.updateTether(followState, position) + } didDestroyPaneItem ({item}) { const editorBinding = this.editorBindingsByEditor.get(item) @@ -92,10 +114,17 @@ class HostPortalBinding { editorBinding = new EditorBinding({editor, portal: this.portal, isHost: true}) const bufferProxy = this.findOrCreateBufferProxyForBuffer(editor.getBuffer()) const editorProxy = this.portal.createEditorProxy({bufferProxy}) + editorBinding = new EditorBinding({ + editor, + portal: this.portal, + isHost: true, + didDispose: () => this.editorBindingsByEditorProxy.delete(editorProxy) + }) editorBinding.setEditorProxy(editorProxy) editorProxy.setDelegate(editorBinding) this.editorBindingsByEditor.set(editor, editorBinding) + this.editorBindingsByEditorProxy.set(editorProxy, editorBinding) return editorProxy } diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index 337c9e88..c51d9710 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -769,7 +769,7 @@ suite('TeletypePackage', function () { }) }) - test.skip('host following guest', async () => { + test('host following guest', async () => { const hostEnv = buildAtomEnvironment() const hostPackage = await buildPackage(hostEnv) const guestEnv = buildAtomEnvironment() From fa03403f3bb0604f2f821c033ad526ed1c60ddc2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Dec 2017 17:58:12 +0100 Subject: [PATCH 37/65] Move site active positions logic into SitePositionsController This will be used later in `HostPortalBinding` to show positions for every participant also for host portals. Signed-off-by: Jason Rudolph --- lib/editor-binding.js | 26 ++-- lib/guest-portal-binding.js | 100 +++------------ lib/host-portal-binding.js | 11 +- lib/site-positions-controller.js | 104 ++++++++++++++++ test/editor-binding.test.js | 18 --- test/guest-portal-binding.test.js | 155 +---------------------- test/helpers/fake-portal.js | 24 ++++ test/site-positions-controller.test.js | 164 +++++++++++++++++++++++++ 8 files changed, 333 insertions(+), 269 deletions(-) create mode 100644 lib/site-positions-controller.js create mode 100644 test/helpers/fake-portal.js create mode 100644 test/site-positions-controller.test.js diff --git a/lib/editor-binding.js b/lib/editor-binding.js index 9595e465..c0c364f3 100644 --- a/lib/editor-binding.js +++ b/lib/editor-binding.js @@ -9,14 +9,11 @@ function doNothing () {} module.exports = class EditorBinding { - constructor ({editor, portal, isHost, didResize, didScroll, didDispose}) { + constructor ({editor, portal, isHost}) { this.editor = editor this.portal = portal this.isHost = isHost this.emitter = new Emitter() - this.emitDidResize = didResize || doNothing - this.emitDidScroll = didScroll || doNothing - this.emitDidDispose = didDispose || doNothing this.selectionsMarkerLayer = this.editor.selectionsMarkerLayer.bufferMarkerLayer this.markerLayersBySiteId = new Map() this.markersByLayerAndId = new WeakMap() @@ -33,7 +30,8 @@ class EditorBinding { if (!this.isHost) this.restoreOriginalEditorMethods(this.editor) if (this.localCursorLayerDecoration) this.localCursorLayerDecoration.destroy() - this.emitDidDispose() + this.emitter.emit('did-dispose') + this.emitter.dispose() } setEditorProxy (editorProxy) { @@ -130,20 +128,32 @@ class EditorBinding { const {element} = this.editor await element.component.getNextUpdatePromise() this.editorProxy.didScroll() - this.emitDidScroll() + this.emitter.emit('did-scroll') } async editorDidChangeScrollLeft () { const {element} = this.editor await element.component.getNextUpdatePromise() this.editorProxy.didScroll() - this.emitDidScroll() + this.emitter.emit('did-scroll') } async editorDidResize () { const {element} = this.editor await element.component.getNextUpdatePromise() - this.emitDidResize() + this.emitter.emit('did-resize') + } + + onDidDispose (callback) { + return this.emitter.on('did-dispose', callback) + } + + onDidScroll (callback) { + return this.emitter.on('did-scroll', callback) + } + + onDidResize (callback) { + return this.emitter.on('did-resize', callback) } updateSelectionsForSiteId (siteId, selections) { diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 211688b7..d30e9a5c 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -3,7 +3,7 @@ const {Errors, FollowState} = require('@atom/teletype-client') const BufferBinding = require('./buffer-binding') const EditorBinding = require('./editor-binding') const EmptyPortalPaneItem = require('./empty-portal-pane-item') -const SitePositionsComponent = require('./site-positions-component') +const SitePositionsController = require('./site-positions-controller') module.exports = class GuestPortalBinding { @@ -20,7 +20,6 @@ class GuestPortalBinding { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.lastEditorProxyChangePromise = Promise.resolve() - this.positionsBySiteId = {} } async initialize () { @@ -28,10 +27,12 @@ class GuestPortalBinding { this.portal = await this.client.joinPortal(this.portalId) if (!this.portal) return false - this.aboveViewportSitePositionsComponent = this.buildSitePositionsComponent('upper-right') - this.insideViewportSitePositionsComponent = this.buildSitePositionsComponent('middle-right') - this.outsideViewportSitePositionsComponent = this.buildSitePositionsComponent('lower-right') - this.showSitePositions() + this.sitePositionsController = new SitePositionsController({ + portal: this.portal, + workspace: this.workspace, + editorBindingForEditorProxy: (editorProxy) => this.editorBindingsByEditorProxy.get(editorProxy) + }) + this.sitePositionsController.show() await this.portal.setDelegate(this) await this.toggleEmptyPortalPaneItem() @@ -46,9 +47,7 @@ class GuestPortalBinding { dispose () { this.subscriptions.dispose() - this.aboveViewportSitePositionsComponent.destroy() - this.insideViewportSitePositionsComponent.destroy() - this.outsideViewportSitePositionsComponent.destroy() + this.sitePositionsController.destroy() if (this.emptyPortalItem) this.emptyPortalItem.destroy() this.emitDidDispose() @@ -86,40 +85,7 @@ class GuestPortalBinding { } updateActivePositions (positionsBySiteId) { - const aboveViewportSiteIds = [] - const insideViewportSiteIds = [] - const outsideViewportSiteIds = [] - - for (let siteId in positionsBySiteId) { - siteId = parseInt(siteId) - if (siteId === this.portal.siteId) continue - - const {editorProxy, position} = positionsBySiteId[siteId] - const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) - if (position && editorBinding && editorBinding.editor === this.getActivePaneItem()) { - switch (editorBinding.getDirectionFromViewportToPosition(position)) { - case 'upward': - aboveViewportSiteIds.push(siteId) - break - case 'inside': - insideViewportSiteIds.push(siteId) - break - case 'downward': - case 'leftward': - case 'rightward': - outsideViewportSiteIds.push(siteId) - break - } - } else { - outsideViewportSiteIds.push(siteId) - } - } - - const followedSiteId = this.portal.getFollowedSiteId() - this.aboveViewportSitePositionsComponent.update({siteIds: aboveViewportSiteIds, followedSiteId}) - this.insideViewportSitePositionsComponent.update({siteIds: insideViewportSiteIds, followedSiteId}) - this.outsideViewportSitePositionsComponent.update({siteIds: outsideViewportSiteIds, followedSiteId}) - this.positionsBySiteId = positionsBySiteId + this.sitePositionsController.updateActivePositions(positionsBySiteId) } updateTether (followState, editorProxy, position) { @@ -161,18 +127,17 @@ class GuestPortalBinding { editorBinding = new EditorBinding({ editor, portal: this.portal, - isHost: false, - didScroll: () => this.updateActivePositions(this.positionsBySiteId), - didResize: () => this.updateActivePositions(this.positionsBySiteId), - didDispose: () => { - this.editorBindingsByEditorProxy.delete(editorProxy) - this.editorProxiesByEditor.delete(editor) - } + isHost: false }) editorBinding.setEditorProxy(editorProxy) editorProxy.setDelegate(editorBinding) + this.editorBindingsByEditorProxy.set(editorProxy, editorBinding) this.editorProxiesByEditor.set(editor, editorProxy) + editorBinding.onDidDispose(() => { + this.editorBindingsByEditorProxy.delete(editorProxy) + this.editorProxiesByEditor.delete(editor) + }) } return editor } @@ -269,9 +234,9 @@ class GuestPortalBinding { if (paneItem !== this.getEmptyPortalPaneItem()) { const editorProxy = this.editorProxiesByEditor.get(paneItem) if (editorProxy) { - this.showSitePositions() + this.sitePositionsController.show() } else { - this.hideSitePositions() + this.sitePositionsController.hide() } this.portal.activateEditorProxy(editorProxy) @@ -301,35 +266,4 @@ class GuestPortalBinding { onDidChange (callback) { return this.emitter.on('did-change', callback) } - - // Private - showSitePositions () { - const workspaceElement = this.workspace.getElement() - workspaceElement.appendChild(this.aboveViewportSitePositionsComponent.element) - workspaceElement.appendChild(this.insideViewportSitePositionsComponent.element) - workspaceElement.appendChild(this.outsideViewportSitePositionsComponent.element) - } - - // Private - hideSitePositions () { - this.aboveViewportSitePositionsComponent.element.remove() - this.insideViewportSitePositionsComponent.element.remove() - this.outsideViewportSitePositionsComponent.element.remove() - } - - // Private - buildSitePositionsComponent (position) { - return new SitePositionsComponent({ - position, - displayedParticipantsCount: 3, - portal: this.portal, - onSelectSiteId: (siteId) => { - if (siteId === this.portal.getFollowedSiteId()) { - this.portal.unfollow() - } else { - this.portal.follow(siteId) - } - } - }) - } } diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index 52439ed7..e38b655b 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -111,20 +111,17 @@ class HostPortalBinding { if (editorBinding) { return editorBinding.editorProxy } else { - editorBinding = new EditorBinding({editor, portal: this.portal, isHost: true}) const bufferProxy = this.findOrCreateBufferProxyForBuffer(editor.getBuffer()) const editorProxy = this.portal.createEditorProxy({bufferProxy}) - editorBinding = new EditorBinding({ - editor, - portal: this.portal, - isHost: true, - didDispose: () => this.editorBindingsByEditorProxy.delete(editorProxy) - }) + editorBinding = new EditorBinding({editor, portal: this.portal, isHost: true}) editorBinding.setEditorProxy(editorProxy) editorProxy.setDelegate(editorBinding) this.editorBindingsByEditor.set(editor, editorBinding) this.editorBindingsByEditorProxy.set(editorProxy, editorBinding) + editorBinding.onDidDispose(() => { + this.editorBindingsByEditorProxy.delete(editorProxy) + }) return editorProxy } diff --git a/lib/site-positions-controller.js b/lib/site-positions-controller.js new file mode 100644 index 00000000..50481998 --- /dev/null +++ b/lib/site-positions-controller.js @@ -0,0 +1,104 @@ +const {CompositeDisposable} = require('atom') +const SitePositionsComponent = require('./site-positions-component') + +module.exports = +class SitePositionsController { + constructor ({portal, workspace}) { + this.portal = portal + this.workspace = workspace + this.subscriptions = new CompositeDisposable() + this.editorBindingsByEditorProxy = new WeakMap() + this.aboveViewportSitePositionsComponent = this.buildSitePositionsComponent('upper-right') + this.insideViewportSitePositionsComponent = this.buildSitePositionsComponent('middle-right') + this.outsideViewportSitePositionsComponent = this.buildSitePositionsComponent('lower-right') + this.positionsBySiteId = {} + } + + destroy () { + this.subscriptions.dispose() + this.aboveViewportSitePositionsComponent.destroy() + this.insideViewportSitePositionsComponent.destroy() + this.outsideViewportSitePositionsComponent.destroy() + } + + show () { + const workspaceElement = this.workspace.getElement() + workspaceElement.appendChild(this.aboveViewportSitePositionsComponent.element) + workspaceElement.appendChild(this.insideViewportSitePositionsComponent.element) + workspaceElement.appendChild(this.outsideViewportSitePositionsComponent.element) + } + + hide () { + this.aboveViewportSitePositionsComponent.element.remove() + this.insideViewportSitePositionsComponent.element.remove() + this.outsideViewportSitePositionsComponent.element.remove() + } + + addEditorBinding (editorBinding) { + this.editorBindingsByEditorProxy.set(editorBinding.editorProxy, editorBinding) + + const didResizeSubscription = editorBinding.onDidResize(() => this.updateActivePositions(this.positionsBySiteId)) + const didScrollSubscription = editorBinding.onDidScroll(() => this.updateActivePositions(this.positionsBySiteId)) + this.subscriptions.add(didResizeSubscription) + this.subscriptions.add(didScrollSubscription) + + editorBinding.onDidDispose(() => { + didResizeSubscription.dispose() + didScrollSubscription.dispose() + this.editorBindingsByEditorProxy.delete(editorBinding.editorProxy) + }) + } + + updateActivePositions (positionsBySiteId) { + const aboveViewportSiteIds = [] + const insideViewportSiteIds = [] + const outsideViewportSiteIds = [] + + for (let siteId in positionsBySiteId) { + siteId = parseInt(siteId) + if (siteId === this.portal.siteId) continue + + const {editorProxy, position} = positionsBySiteId[siteId] + const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) + if (position && editorBinding && editorBinding.editor === this.workspace.getActivePaneItem()) { + switch (editorBinding.getDirectionFromViewportToPosition(position)) { + case 'upward': + aboveViewportSiteIds.push(siteId) + break + case 'inside': + insideViewportSiteIds.push(siteId) + break + case 'downward': + case 'leftward': + case 'rightward': + outsideViewportSiteIds.push(siteId) + break + } + } else { + outsideViewportSiteIds.push(siteId) + } + } + + const followedSiteId = this.portal.getFollowedSiteId() + this.aboveViewportSitePositionsComponent.update({siteIds: aboveViewportSiteIds, followedSiteId}) + this.insideViewportSitePositionsComponent.update({siteIds: insideViewportSiteIds, followedSiteId}) + this.outsideViewportSitePositionsComponent.update({siteIds: outsideViewportSiteIds, followedSiteId}) + this.positionsBySiteId = positionsBySiteId + } + + // Private + buildSitePositionsComponent (position) { + return new SitePositionsComponent({ + position, + displayedParticipantsCount: 3, + portal: this.portal, + onSelectSiteId: (siteId) => { + if (siteId === this.portal.getFollowedSiteId()) { + this.portal.unfollow() + } else { + this.portal.follow(siteId) + } + } + }) + } +} diff --git a/test/editor-binding.test.js b/test/editor-binding.test.js index f81042a8..0473f87c 100644 --- a/test/editor-binding.test.js +++ b/test/editor-binding.test.js @@ -464,24 +464,6 @@ class FakeEditorProxy { } } } - - follow (siteId) { - this.followedSiteId = siteId - this.followState = FollowState.RETRACTED - } - - unfollow () { - this.followedSiteId = null - this.followState = FollowState.DISCONNECTED - } - - getFollowedSiteId () { - if (this.followState === FollowState.DISCONNECTED) { - return null - } else { - return this.followedSiteId - } - } } class FakePortal { diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index 57745e32..9224d172 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -1,26 +1,11 @@ const assert = require('assert') -const fs = require('fs') -const path = require('path') const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') -const {loadPackageStyleSheets} = require('./helpers/ui-helpers') const {FollowState, TeletypeClient} = require('@atom/teletype-client') +const FakePortal = require('./helpers/fake-portal') const GuestPortalBinding = require('../lib/guest-portal-binding') -const SAMPLE_TEXT = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js'), 'utf8') -const { - setEditorHeightInLines, - setEditorWidthInChars, - setEditorScrollTopInLines, - setEditorScrollLeftInChars -} = require('./helpers/editor-helpers') suite('GuestPortalBinding', () => { - let attachedElements = [] - teardown(async () => { - while (attachedElements.length > 0) { - attachedElements.pop().remove() - } - await destroyAtomEnvironments() }) @@ -98,114 +83,7 @@ suite('GuestPortalBinding', () => { disposable.dispose() }) - test('showing the active position of other collaborators', async () => { - const environment = buildAtomEnvironment() - - loadPackageStyleSheets(environment) - const {workspace} = environment - attachToDOM(workspace.getElement()) - - const client = { - joinPortal () { - return new FakePortal() - } - } - const portalBinding = buildGuestPortalBinding(client, environment, 'some-portal') - await portalBinding.initialize() - - const editorProxy1 = new FakeEditorProxy('editor-1') - const editorProxy2 = new FakeEditorProxy('editor-2') - await portalBinding.updateTether(FollowState.RETRACTED, editorProxy1) - - const editor1 = workspace.getActiveTextEditor() - editor1.buffer.setTextInRange([[0, 0], [0, 0]], SAMPLE_TEXT, {undo: 'skip'}) - - const { - aboveViewportSitePositionsComponent, - insideViewportSitePositionsComponent, - outsideViewportSitePositionsComponent - } = portalBinding - assert(workspace.element.contains(aboveViewportSitePositionsComponent.element)) - assert(workspace.element.contains(insideViewportSitePositionsComponent.element)) - assert(workspace.element.contains(outsideViewportSitePositionsComponent.element)) - - await setEditorHeightInLines(editor1, 3) - await setEditorWidthInChars(editor1, 5) - await setEditorScrollTopInLines(editor1, 5) - await setEditorScrollLeftInChars(editor1, 5) - - const activePositionsBySiteId = { - 1: {editorProxy: editorProxy1, position: {row: 2, column: 5}}, // collaborator above visible area - 2: {editorProxy: editorProxy1, position: {row: 9, column: 5}}, // collaborator below visible area - 3: {editorProxy: editorProxy1, position: {row: 6, column: 1}}, // collaborator to the left of visible area - 4: {editorProxy: editorProxy1, position: {row: 6, column: 15}}, // collaborator to the right of visible area - 5: {editorProxy: editorProxy1, position: {row: 6, column: 6}}, // collaborator inside of visible area - 6: {editorProxy: editorProxy2, position: {row: 0, column: 0}} // collaborator in a different editor - } - portalBinding.updateActivePositions(activePositionsBySiteId) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, [1]) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [5]) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 3, 4, 6]) - - await setEditorScrollLeftInChars(editor1, 0) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, [1]) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [3]) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4, 5, 6]) - - await setEditorScrollTopInLines(editor1, 2) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, []) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 3, 4, 5, 6]) - - await setEditorHeightInLines(editor1, 7) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [3]) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 4, 5, 6]) - - await setEditorWidthInChars(editor1, 10) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [1, 3, 5]) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4, 6]) - - await portalBinding.updateTether(FollowState.RETRACTED, editorProxy2) - portalBinding.updateActivePositions(activePositionsBySiteId) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [6]) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 3, 4, 5]) - - // Selecting a site will follow them. - outsideViewportSitePositionsComponent.props.onSelectSiteId(2) - assert.equal(portalBinding.portal.getFollowedSiteId(), 2) - - // Selecting the same site again will unfollow them. - outsideViewportSitePositionsComponent.props.onSelectSiteId(2) - assert.equal(portalBinding.portal.getFollowedSiteId(), null) - - // Focusing a pane item that does not belong to the portal will hide site positions. - await workspace.open() - - assert(!workspace.element.contains(aboveViewportSitePositionsComponent.element)) - assert(!workspace.element.contains(insideViewportSitePositionsComponent.element)) - assert(!workspace.element.contains(outsideViewportSitePositionsComponent.element)) - - // Focusing a pane item that belongs to the portal will show site positions again. - await workspace.open(editor1) - portalBinding.updateActivePositions(activePositionsBySiteId) - - assert(workspace.element.contains(aboveViewportSitePositionsComponent.element)) - assert(workspace.element.contains(insideViewportSitePositionsComponent.element)) - assert(workspace.element.contains(outsideViewportSitePositionsComponent.element)) - - assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) - assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [1, 3, 5]) - assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4, 6]) - }) + test('toggling site position components visibility when switching tabs') function buildGuestPortalBinding (client, atomEnv, portalId) { return new GuestPortalBinding({ @@ -216,11 +94,6 @@ suite('GuestPortalBinding', () => { }) } - function attachToDOM (element) { - attachedElements.push(element) - document.body.insertBefore(element, document.body.firstChild) - } - class FakeEditorProxy { constructor (uri) { this.bufferProxy = { @@ -241,28 +114,4 @@ suite('GuestPortalBinding', () => { updateSelections () {} } - - class FakePortal { - follow (siteId) { - this.followedSiteId = siteId - } - - unfollow () { - this.followedSiteId = null - } - - getFollowedSiteId () { - return this.followedSiteId - } - - activateEditorProxy () {} - - setDelegate (delegate) { - this.delegate = delegate - } - - getSiteIdentity (siteId) { - return {login: 'site-' + siteId} - } - } }) diff --git a/test/helpers/fake-portal.js b/test/helpers/fake-portal.js new file mode 100644 index 00000000..9c05e920 --- /dev/null +++ b/test/helpers/fake-portal.js @@ -0,0 +1,24 @@ +module.exports = +class FakePortal { + follow (siteId) { + this.followedSiteId = siteId + } + + unfollow () { + this.followedSiteId = null + } + + getFollowedSiteId () { + return this.followedSiteId + } + + activateEditorProxy () {} + + setDelegate (delegate) { + this.delegate = delegate + } + + getSiteIdentity (siteId) { + return {login: 'site-' + siteId} + } +} diff --git a/test/site-positions-controller.test.js b/test/site-positions-controller.test.js new file mode 100644 index 00000000..370f6c4e --- /dev/null +++ b/test/site-positions-controller.test.js @@ -0,0 +1,164 @@ +const assert = require('assert') +const fs = require('fs') +const path = require('path') +const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') +const {loadPackageStyleSheets} = require('./helpers/ui-helpers') +const { + setEditorHeightInLines, + setEditorWidthInChars, + setEditorScrollTopInLines, + setEditorScrollLeftInChars +} = require('./helpers/editor-helpers') +const {TextEditor, TextBuffer} = require('atom') +const SAMPLE_TEXT = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js'), 'utf8') +const FakePortal = require('./helpers/fake-portal') + +const EditorBinding = require('../lib/editor-binding') +const SitePositionsController = require('../lib/site-positions-controller') + +suite('SitePositionsController', () => { + let attachedElements = [] + + teardown(async () => { + while (attachedElements.length > 0) { + attachedElements.pop().remove() + } + + await destroyAtomEnvironments() + }) + + test('show() and hide()', async () => { + const {workspace} = buildAtomEnvironment() + const controller = new SitePositionsController({ + workspace, + portal: {}, + editorBindingForEditorProxy: () => {} + }) + + controller.show() + assert(workspace.element.contains(controller.aboveViewportSitePositionsComponent.element)) + assert(workspace.element.contains(controller.insideViewportSitePositionsComponent.element)) + assert(workspace.element.contains(controller.outsideViewportSitePositionsComponent.element)) + + controller.hide() + assert(!workspace.element.contains(controller.aboveViewportSitePositionsComponent.element)) + assert(!workspace.element.contains(controller.insideViewportSitePositionsComponent.element)) + assert(!workspace.element.contains(controller.outsideViewportSitePositionsComponent.element)) + }) + + test('updateActivePositions(positionsBySiteId)', async () => { + const environment = buildAtomEnvironment() + loadPackageStyleSheets(environment) + const {workspace} = environment + attachToDOM(workspace.getElement()) + + const portal = new FakePortal() + const controller = new SitePositionsController({workspace, portal}) + + const editorProxy1 = new FakeEditorProxy() + const editor1 = new TextEditor({autoHeight: false, buffer: new TextBuffer(SAMPLE_TEXT)}) + const editorBinding1 = new EditorBinding({portal, editor: editor1}) + editorBinding1.setEditorProxy(editorProxy1) + controller.addEditorBinding(editorBinding1) + + const editorProxy2 = new FakeEditorProxy() + const editor2 = new TextEditor({autoHeight: false, buffer: new TextBuffer(SAMPLE_TEXT)}) + const editorBinding2 = new EditorBinding({portal, editor: editor2}) + editorBinding2.setEditorProxy(editorProxy2) + controller.addEditorBinding(editorBinding2) + + const { + aboveViewportSitePositionsComponent, + insideViewportSitePositionsComponent, + outsideViewportSitePositionsComponent + } = controller + + await workspace.open(editor1) + await setEditorHeightInLines(editor1, 3) + await setEditorWidthInChars(editor1, 5) + await setEditorScrollTopInLines(editor1, 5) + await setEditorScrollLeftInChars(editor1, 5) + + const activePositionsBySiteId = { + 1: {editorProxy: editorProxy1, position: {row: 2, column: 5}}, // collaborator above visible area + 2: {editorProxy: editorProxy1, position: {row: 9, column: 5}}, // collaborator below visible area + 3: {editorProxy: editorProxy1, position: {row: 6, column: 1}}, // collaborator to the left of visible area + 4: {editorProxy: editorProxy1, position: {row: 6, column: 15}}, // collaborator to the right of visible area + 5: {editorProxy: editorProxy1, position: {row: 6, column: 6}}, // collaborator inside of visible area + 6: {editorProxy: editorProxy2, position: {row: 0, column: 0}} // collaborator in a different editor + } + controller.updateActivePositions(activePositionsBySiteId) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, [1]) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [5]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 3, 4, 6]) + + await setEditorScrollLeftInChars(editor1, 0) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, [1]) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [3]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4, 5, 6]) + + await setEditorScrollTopInLines(editor1, 2) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 3, 4, 5, 6]) + + await setEditorHeightInLines(editor1, 7) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [3]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 4, 5, 6]) + + await setEditorWidthInChars(editor1, 10) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [1, 3, 5]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4, 6]) + + await workspace.open(editor2) + controller.updateActivePositions(activePositionsBySiteId) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [6]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 3, 4, 5]) + + await workspace.open(new TextEditor()) + controller.updateActivePositions(activePositionsBySiteId) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [1, 2, 3, 4, 5, 6]) + + await workspace.open(editor1) + controller.updateActivePositions(activePositionsBySiteId) + + assert.deepEqual(aboveViewportSitePositionsComponent.props.siteIds, []) + assert.deepEqual(insideViewportSitePositionsComponent.props.siteIds, [1, 3, 5]) + assert.deepEqual(outsideViewportSitePositionsComponent.props.siteIds, [2, 4, 6]) + + // Selecting a site will follow them. + outsideViewportSitePositionsComponent.props.onSelectSiteId(42) + assert.equal(controller.portal.getFollowedSiteId(), 42) + + // Selecting the same site again will unfollow them. + outsideViewportSitePositionsComponent.props.onSelectSiteId(42) + assert.equal(controller.portal.getFollowedSiteId(), null) + }) + + function attachToDOM (element) { + attachedElements.push(element) + document.body.insertBefore(element, document.body.firstChild) + } +}) + +class FakeEditorProxy { + constructor () { + this.bufferProxy = {uri: 'fake-buffer-proxy-uri'} + } + + didScroll () {} + + updateSelections () {} +} From 6cd665980d8ec8f5acfef4836e825ddcf4d72526 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 4 Dec 2017 13:17:26 -0500 Subject: [PATCH 38/65] =?UTF-8?q?=F0=9F=91=95=F0=9F=94=A5=20Remove=20unuse?= =?UTF-8?q?d=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/editor-binding.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/editor-binding.js b/lib/editor-binding.js index c0c364f3..038d7456 100644 --- a/lib/editor-binding.js +++ b/lib/editor-binding.js @@ -5,8 +5,6 @@ const {Range, Emitter, Disposable, CompositeDisposable} = require('atom') const normalizeURI = require('./normalize-uri') const {FollowState} = require('@atom/teletype-client') -function doNothing () {} - module.exports = class EditorBinding { constructor ({editor, portal, isHost}) { From b0125aba9509594519de830cea09db1fc09765a9 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 4 Dec 2017 14:06:28 -0500 Subject: [PATCH 39/65] =?UTF-8?q?=E2=9C=85=20Test=20toggling=20site=20posi?= =?UTF-8?q?tion=20component=20visibility=20when=20switching=20tabs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/site-positions-controller.js | 3 +++ test/guest-portal-binding.test.js | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/site-positions-controller.js b/lib/site-positions-controller.js index 50481998..38781540 100644 --- a/lib/site-positions-controller.js +++ b/lib/site-positions-controller.js @@ -8,6 +8,7 @@ class SitePositionsController { this.workspace = workspace this.subscriptions = new CompositeDisposable() this.editorBindingsByEditorProxy = new WeakMap() + this.visible = false this.aboveViewportSitePositionsComponent = this.buildSitePositionsComponent('upper-right') this.insideViewportSitePositionsComponent = this.buildSitePositionsComponent('middle-right') this.outsideViewportSitePositionsComponent = this.buildSitePositionsComponent('lower-right') @@ -26,12 +27,14 @@ class SitePositionsController { workspaceElement.appendChild(this.aboveViewportSitePositionsComponent.element) workspaceElement.appendChild(this.insideViewportSitePositionsComponent.element) workspaceElement.appendChild(this.outsideViewportSitePositionsComponent.element) + this.visible = true } hide () { this.aboveViewportSitePositionsComponent.element.remove() this.insideViewportSitePositionsComponent.element.remove() this.outsideViewportSitePositionsComponent.element.remove() + this.visible = false } addEditorBinding (editorBinding) { diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index 9224d172..3a931882 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -83,7 +83,25 @@ suite('GuestPortalBinding', () => { disposable.dispose() }) - test('toggling site position components visibility when switching tabs') + test('toggling site position components visibility when switching tabs', async () => { + const stubPubSubGateway = {} + const client = new TeletypeClient({pubSubGateway: stubPubSubGateway}) + const portal = new FakePortal() + client.joinPortal = () => portal + const atomEnv = buildAtomEnvironment() + const portalBinding = buildGuestPortalBinding(client, atomEnv, 'some-portal') + await portalBinding.initialize() + + await portalBinding.updateTether(FollowState.RETRACTED, new FakeEditorProxy('some-uri')) + const portalPaneItem = atomEnv.workspace.getActivePaneItem() + assert.equal(portalBinding.sitePositionsController.visible, true) + + await atomEnv.workspace.open() + assert.equal(portalBinding.sitePositionsController.visible, false) + + await atomEnv.workspace.open(portalPaneItem) + assert.equal(portalBinding.sitePositionsController.visible, true) + }) function buildGuestPortalBinding (client, atomEnv, portalId) { return new GuestPortalBinding({ From 04d4028d7c5b13c8b124342a457d4be295912056 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 4 Dec 2017 15:17:58 -0500 Subject: [PATCH 40/65] Notify SitePositionsController about new EditorBindings --- lib/guest-portal-binding.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index d30e9a5c..c1a0f728 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -138,6 +138,8 @@ class GuestPortalBinding { this.editorBindingsByEditorProxy.delete(editorProxy) this.editorProxiesByEditor.delete(editor) }) + + this.sitePositionsController.addEditorBinding(editorBinding) } return editor } From 92069c4ea204c57caf5fd3ed886f221ad189c5b0 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 4 Dec 2017 14:51:26 -0500 Subject: [PATCH 41/65] :fire: --- lib/guest-portal-binding.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index c1a0f728..805a4757 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -27,11 +27,7 @@ class GuestPortalBinding { this.portal = await this.client.joinPortal(this.portalId) if (!this.portal) return false - this.sitePositionsController = new SitePositionsController({ - portal: this.portal, - workspace: this.workspace, - editorBindingForEditorProxy: (editorProxy) => this.editorBindingsByEditorProxy.get(editorProxy) - }) + this.sitePositionsController = new SitePositionsController({portal: this.portal, workspace: this.workspace}) this.sitePositionsController.show() await this.portal.setDelegate(this) From 3951c2387305821bd5e897abd1b0b9f81f889483 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 4 Dec 2017 14:58:10 -0500 Subject: [PATCH 42/65] Implement `HostPortalBinding.updateActivePositions` --- lib/host-portal-binding.js | 10 +++++++++- test/host-portal-binding.test.js | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index e38b655b..74c0676e 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -3,6 +3,7 @@ const {CompositeDisposable, Emitter} = require('atom') const {FollowState} = require('@atom/teletype-client') const BufferBinding = require('./buffer-binding') const EditorBinding = require('./editor-binding') +const SitePositionsController = require('./site-positions-controller') module.exports = class HostPortalBinding { @@ -24,6 +25,9 @@ class HostPortalBinding { this.portal = await this.client.createPortal() if (!this.portal) return false + this.sitePositionsController = new SitePositionsController({portal: this.portal, workspace: this.workspace}) + this.sitePositionsController.show() + this.portal.setDelegate(this) this.disposables.add( this.workspace.observeActiveTextEditor(this.didChangeActiveTextEditor.bind(this)), @@ -76,7 +80,9 @@ class HostPortalBinding { } } - updateActivePositions () {} + updateActivePositions (positionsBySiteId) { + this.sitePositionsController.updateActivePositions(positionsBySiteId) + } updateTether (followState, editorProxy, position) { if (editorProxy) { @@ -123,6 +129,8 @@ class HostPortalBinding { this.editorBindingsByEditorProxy.delete(editorProxy) }) + this.sitePositionsController.addEditorBinding(editorBinding) + return editorProxy } } diff --git a/test/host-portal-binding.test.js b/test/host-portal-binding.test.js index 84af2486..0bf297df 100644 --- a/test/host-portal-binding.test.js +++ b/test/host-portal-binding.test.js @@ -57,6 +57,9 @@ suite('HostPortalBinding', () => { assert(atomEnv.notifications.getNotifications()[0].message.includes('@site-3')) }) + // TODO + test('toggling site position components visibility when switching between shared and non-shared pane items') + function buildHostPortalBinding (client, atomEnv) { return new HostPortalBinding({ client, From 1380adb2725f3c70b27e096d84cd9af7bd2819be Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 4 Dec 2017 15:49:56 -0500 Subject: [PATCH 43/65] Place SitePositionComponents in workspace center --- lib/site-positions-controller.js | 8 ++++---- test/portal-list-component.test.js | 10 ++++++++++ test/site-positions-controller.test.js | 14 ++++++++------ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/site-positions-controller.js b/lib/site-positions-controller.js index 38781540..89cba7d1 100644 --- a/lib/site-positions-controller.js +++ b/lib/site-positions-controller.js @@ -23,10 +23,10 @@ class SitePositionsController { } show () { - const workspaceElement = this.workspace.getElement() - workspaceElement.appendChild(this.aboveViewportSitePositionsComponent.element) - workspaceElement.appendChild(this.insideViewportSitePositionsComponent.element) - workspaceElement.appendChild(this.outsideViewportSitePositionsComponent.element) + const containerElement = this.workspace.getCenter().paneContainer.getElement() + containerElement.appendChild(this.aboveViewportSitePositionsComponent.element) + containerElement.appendChild(this.insideViewportSitePositionsComponent.element) + containerElement.appendChild(this.outsideViewportSitePositionsComponent.element) this.visible = true } diff --git a/test/portal-list-component.test.js b/test/portal-list-component.test.js index e8b989e2..6317bbe2 100644 --- a/test/portal-list-component.test.js +++ b/test/portal-list-component.test.js @@ -247,6 +247,16 @@ suite('PortalListComponent', function () { class FakeWorkspace { async open () {} + getCenter () { + return { + paneContainer: { + getElement () { + return document.createElement('div') + } + } + } + } + getElement () { return document.createElement('div') } diff --git a/test/site-positions-controller.test.js b/test/site-positions-controller.test.js index 370f6c4e..8adf8a10 100644 --- a/test/site-positions-controller.test.js +++ b/test/site-positions-controller.test.js @@ -35,15 +35,17 @@ suite('SitePositionsController', () => { editorBindingForEditorProxy: () => {} }) + const workspaceCenterElement = workspace.getCenter().paneContainer.getElement() + controller.show() - assert(workspace.element.contains(controller.aboveViewportSitePositionsComponent.element)) - assert(workspace.element.contains(controller.insideViewportSitePositionsComponent.element)) - assert(workspace.element.contains(controller.outsideViewportSitePositionsComponent.element)) + assert(workspaceCenterElement.contains(controller.aboveViewportSitePositionsComponent.element)) + assert(workspaceCenterElement.contains(controller.insideViewportSitePositionsComponent.element)) + assert(workspaceCenterElement.contains(controller.outsideViewportSitePositionsComponent.element)) controller.hide() - assert(!workspace.element.contains(controller.aboveViewportSitePositionsComponent.element)) - assert(!workspace.element.contains(controller.insideViewportSitePositionsComponent.element)) - assert(!workspace.element.contains(controller.outsideViewportSitePositionsComponent.element)) + assert(!workspaceCenterElement.contains(controller.aboveViewportSitePositionsComponent.element)) + assert(!workspaceCenterElement.contains(controller.insideViewportSitePositionsComponent.element)) + assert(!workspaceCenterElement.contains(controller.outsideViewportSitePositionsComponent.element)) }) test('updateActivePositions(positionsBySiteId)', async () => { From b7a353b39553dcb4dfea26f74591cd10c200285b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 4 Dec 2017 17:24:18 -0500 Subject: [PATCH 44/65] =?UTF-8?q?=F0=9F=93=9D=20Update=20status=20of=20RFC?= =?UTF-8?q?-001?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/rfcs/001-allow-guests-to-open-multiple-remote-buffers.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/rfcs/001-allow-guests-to-open-multiple-remote-buffers.md b/doc/rfcs/001-allow-guests-to-open-multiple-remote-buffers.md index 86d60955..6d67548d 100644 --- a/doc/rfcs/001-allow-guests-to-open-multiple-remote-buffers.md +++ b/doc/rfcs/001-allow-guests-to-open-multiple-remote-buffers.md @@ -2,7 +2,9 @@ ## Status -This is a proposal and is not yet implemented. +This is a proposal and is not yet fully implemented. + +[#262](https://github.com/atom/teletype/pull/262) implements the majority of this proposal, and the remaining functionality is planned for future pull requests. See [#262](https://github.com/atom/teletype/pull/262) for details. ## Summary From 63452337416e70c42f71e9361fcfcdd51405120a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2017 14:20:32 +0100 Subject: [PATCH 45/65] :art: --- test/host-portal-binding.test.js | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/test/host-portal-binding.test.js b/test/host-portal-binding.test.js index 0bf297df..35be5beb 100644 --- a/test/host-portal-binding.test.js +++ b/test/host-portal-binding.test.js @@ -3,6 +3,7 @@ const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom- const {TeletypeClient} = require('@atom/teletype-client') const HostPortalBinding = require('../lib/host-portal-binding') const FakeClipboard = require('./helpers/fake-clipboard') +const FakePortal = require('./helpers/fake-portal') suite('HostPortalBinding', () => { teardown(async () => { @@ -10,8 +11,7 @@ suite('HostPortalBinding', () => { }) test('handling an unexpected error when joining a portal', async () => { - const stubPubSubGateway = {} - const client = new TeletypeClient({pubSubGateway: stubPubSubGateway}) + const client = new TeletypeClient({pubSubGateway: {}}) client.createPortal = function () { throw new Error('It broke!') } @@ -28,17 +28,8 @@ suite('HostPortalBinding', () => { }) test('showing notifications when sites join or leave', async () => { - const stubPubSubGateway = {} - const client = new TeletypeClient({pubSubGateway: stubPubSubGateway}) - const portal = { - setDelegate (delegate) { - this.delegate = delegate - }, - getSiteIdentity (siteId) { - return {login: 'site-' + siteId} - }, - setActiveEditorProxy () {} - } + const portal = new FakePortal() + const client = new TeletypeClient({pubSubGateway: {}}) client.createPortal = function () { return portal } From ae369fc5c841316f2c8572447e93d21ebd0e2350 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2017 14:21:04 +0100 Subject: [PATCH 46/65] Toggle SitePositionsController visibility when switching tabs --- lib/host-portal-binding.js | 3 +- test/helpers/fake-portal.js | 17 +++++++++++ test/host-portal-binding.test.js | 49 ++++++++++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index 74c0676e..859de279 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -26,7 +26,6 @@ class HostPortalBinding { if (!this.portal) return false this.sitePositionsController = new SitePositionsController({portal: this.portal, workspace: this.workspace}) - this.sitePositionsController.show() this.portal.setDelegate(this) this.disposables.add( @@ -75,8 +74,10 @@ class HostPortalBinding { if (editor && !editor.isRemote) { const editorProxy = this.findOrCreateEditorProxyForEditor(editor) this.portal.activateEditorProxy(editorProxy) + this.sitePositionsController.show() } else { this.portal.activateEditorProxy(null) + this.sitePositionsController.hide() } } diff --git a/test/helpers/fake-portal.js b/test/helpers/fake-portal.js index 9c05e920..2471e34f 100644 --- a/test/helpers/fake-portal.js +++ b/test/helpers/fake-portal.js @@ -1,5 +1,20 @@ module.exports = class FakePortal { + createBufferProxy () { + return { + dispose () {}, + setDelegate () {} + } + } + + createEditorProxy () { + return { + dispose () {}, + setDelegate () {}, + updateSelections () {} + } + } + follow (siteId) { this.followedSiteId = siteId } @@ -14,6 +29,8 @@ class FakePortal { activateEditorProxy () {} + removeEditorProxy () {} + setDelegate (delegate) { this.delegate = delegate } diff --git a/test/host-portal-binding.test.js b/test/host-portal-binding.test.js index 35be5beb..b6c684f4 100644 --- a/test/host-portal-binding.test.js +++ b/test/host-portal-binding.test.js @@ -1,4 +1,5 @@ const assert = require('assert') +const {Emitter, TextEditor} = require('atom') const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') const {TeletypeClient} = require('@atom/teletype-client') const HostPortalBinding = require('../lib/host-portal-binding') @@ -48,8 +49,37 @@ suite('HostPortalBinding', () => { assert(atomEnv.notifications.getNotifications()[0].message.includes('@site-3')) }) - // TODO - test('toggling site position components visibility when switching between shared and non-shared pane items') + test('toggling site position components visibility when switching between shared and non-shared pane items', async () => { + const client = new TeletypeClient({pubSubGateway: {}}) + const portal = new FakePortal() + client.createPortal = () => portal + const atomEnv = buildAtomEnvironment() + const portalBinding = buildHostPortalBinding(client, atomEnv) + + const localEditor1 = await atomEnv.workspace.open() + await portalBinding.initialize() + assert.equal(portalBinding.sitePositionsController.visible, true) + + const localNonEditor = await atomEnv.workspace.open(new FakePaneItem()) + assert.equal(portalBinding.sitePositionsController.visible, false) + + const localEditor2 = await atomEnv.workspace.open() + assert.equal(portalBinding.sitePositionsController.visible, true) + + const remoteEditor = new TextEditor() + remoteEditor.isRemote = true + await atomEnv.workspace.open(remoteEditor) + assert.equal(portalBinding.sitePositionsController.visible, false) + + await atomEnv.workspace.open(localEditor2) + assert.equal(portalBinding.sitePositionsController.visible, true) + + remoteEditor.destroy() + localEditor1.destroy() + localEditor2.destroy() + localNonEditor.destroy() + assert.equal(portalBinding.sitePositionsController.visible, false) + }) function buildHostPortalBinding (client, atomEnv) { return new HostPortalBinding({ @@ -60,3 +90,18 @@ suite('HostPortalBinding', () => { }) } }) + +class FakePaneItem { + constructor () { + this.element = document.createElement('div') + this.emitter = new Emitter() + } + + destroy () { + this.emitter.emit('did-destroy') + } + + onDidDestroy (callback) { + return this.emitter.on('did-destroy', callback) + } +} From f91a10f50e24e97903be1ff3ef7791d5b61a6769 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2017 14:32:41 +0100 Subject: [PATCH 47/65] Increase top margin for avatars displayed in upper-right corner --- styles/teletype.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/teletype.less b/styles/teletype.less index 851b7a79..192a7714 100644 --- a/styles/teletype.less +++ b/styles/teletype.less @@ -369,7 +369,7 @@ } &.upper-right { - top: @corner-margin; + top: @corner-margin * 3; display: flex; flex-direction: row; justify-content: flex-end; From 68188aa3dc98478cb2baa18a87ebc09a071e4ae8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2017 14:38:49 +0100 Subject: [PATCH 48/65] Destroy site positions controller when disposing HostPortalBinding --- lib/host-portal-binding.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index 859de279..668b3aa6 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -46,6 +46,7 @@ class HostPortalBinding { dispose () { this.workspace.getElement().classList.remove('teletype-Host') + this.sitePositionsController.destroy() this.disposables.dispose() this.didDispose() } From 3aae9476d3f6f30f05e3001f00f5feedcfd14cff Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2017 15:01:33 +0100 Subject: [PATCH 49/65] Search among all panes before opening a new remote editor This will prevent "Workspace can only contain one instance of object" errors if the leader switches to a remote editor that has been moved into a pane that is inactive when `updateTether` gets called. --- lib/guest-portal-binding.js | 2 +- test/guest-portal-binding.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 805a4757..834f13c1 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -223,7 +223,7 @@ class GuestPortalBinding { async openPaneItem (newActivePaneItem) { this.newActivePaneItem = newActivePaneItem - await this.workspace.open(newActivePaneItem) + await this.workspace.open(newActivePaneItem, {searchAllPanes: true}) this.lastActivePaneItem = this.newActivePaneItem this.newActivePaneItem = null } diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index 3a931882..8972574c 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -83,6 +83,31 @@ suite('GuestPortalBinding', () => { disposable.dispose() }) + test('switching the active editor to a remote editor that had been moved into a non-active pane', async () => { + const stubPubSubGateway = {} + const client = new TeletypeClient({pubSubGateway: stubPubSubGateway}) + client.joinPortal = () => new FakePortal() + const atomEnv = buildAtomEnvironment() + const portalBinding = buildGuestPortalBinding(client, atomEnv, 'some-portal') + await portalBinding.initialize() + + const editorProxy1 = new FakeEditorProxy('editor-1') + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy1) + + const editorProxy2 = new FakeEditorProxy('editor-2') + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy2) + + const leftPane = atomEnv.workspace.getActivePane() + const rightPane = leftPane.splitRight({moveActiveItem: true}) + assert.equal(leftPane.getItems().length, 1) + assert.equal(rightPane.getItems().length, 1) + + leftPane.activate() + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy2) + assert.equal(leftPane.getItems().length, 1) + assert.equal(rightPane.getItems().length, 1) + }) + test('toggling site position components visibility when switching tabs', async () => { const stubPubSubGateway = {} const client = new TeletypeClient({pubSubGateway: stubPubSubGateway}) From 512b417f3f1b059995663940d8a135d37862a002 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2017 15:19:00 +0100 Subject: [PATCH 50/65] Search among all panes before switching to a remote editor on the host This will prevent "Workspace can only contain one instance of object" errors if the leader switches to a remote editor that has been moved into a pane that is inactive when `updateTether` gets called. --- lib/host-portal-binding.js | 2 +- test/guest-portal-binding.test.js | 24 +++--------------------- test/helpers/fake-editor-proxy.js | 21 +++++++++++++++++++++ test/helpers/fake-portal.js | 8 +++++++- test/host-portal-binding.test.js | 30 +++++++++++++++++++++++++++++- 5 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 test/helpers/fake-editor-proxy.js diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index 668b3aa6..fe6a3796 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -99,7 +99,7 @@ class HostPortalBinding { const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) if (followState === FollowState.RETRACTED) { - await this.workspace.open(editorBinding.editor) + await this.workspace.open(editorBinding.editor, {searchAllPanes: true}) } else { this.editorBindingsByEditorProxy.forEach((b) => b.updateTether(followState)) } diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index 8972574c..ba902f56 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -2,6 +2,7 @@ const assert = require('assert') const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') const {FollowState, TeletypeClient} = require('@atom/teletype-client') const FakePortal = require('./helpers/fake-portal') +const FakeEditorProxy = require('./helpers/fake-editor-proxy') const GuestPortalBinding = require('../lib/guest-portal-binding') suite('GuestPortalBinding', () => { @@ -101,11 +102,13 @@ suite('GuestPortalBinding', () => { const rightPane = leftPane.splitRight({moveActiveItem: true}) assert.equal(leftPane.getItems().length, 1) assert.equal(rightPane.getItems().length, 1) + assert.equal(atomEnv.workspace.getActivePane(), rightPane) leftPane.activate() await portalBinding.updateTether(FollowState.RETRACTED, editorProxy2) assert.equal(leftPane.getItems().length, 1) assert.equal(rightPane.getItems().length, 1) + assert.equal(atomEnv.workspace.getActivePane(), rightPane) }) test('toggling site position components visibility when switching tabs', async () => { @@ -136,25 +139,4 @@ suite('GuestPortalBinding', () => { workspace: atomEnv.workspace }) } - - class FakeEditorProxy { - constructor (uri) { - this.bufferProxy = { - uri, - dispose () {}, - setDelegate () {}, - createCheckpoint () {}, - groupChangesSinceCheckpoint () {}, - applyGroupingInterval () {} - } - } - - follow () {} - - didScroll () {} - - setDelegate () {} - - updateSelections () {} - } }) diff --git a/test/helpers/fake-editor-proxy.js b/test/helpers/fake-editor-proxy.js new file mode 100644 index 00000000..8a5160e2 --- /dev/null +++ b/test/helpers/fake-editor-proxy.js @@ -0,0 +1,21 @@ +module.exports = +class FakeEditorProxy { + constructor (uri) { + this.bufferProxy = { + uri, + dispose () {}, + setDelegate () {}, + createCheckpoint () {}, + groupChangesSinceCheckpoint () {}, + applyGroupingInterval () {} + } + } + + follow () {} + + didScroll () {} + + setDelegate () {} + + updateSelections () {} +} diff --git a/test/helpers/fake-portal.js b/test/helpers/fake-portal.js index 2471e34f..15577d04 100644 --- a/test/helpers/fake-portal.js +++ b/test/helpers/fake-portal.js @@ -27,10 +27,16 @@ class FakePortal { return this.followedSiteId } - activateEditorProxy () {} + activateEditorProxy (editorProxy) { + this.activeEditorProxy = editorProxy + } removeEditorProxy () {} + getActiveEditorProxy () { + return this.activeEditorProxy + } + setDelegate (delegate) { this.delegate = delegate } diff --git a/test/host-portal-binding.test.js b/test/host-portal-binding.test.js index b6c684f4..934016d2 100644 --- a/test/host-portal-binding.test.js +++ b/test/host-portal-binding.test.js @@ -1,10 +1,11 @@ const assert = require('assert') const {Emitter, TextEditor} = require('atom') const {buildAtomEnvironment, destroyAtomEnvironments} = require('./helpers/atom-environments') -const {TeletypeClient} = require('@atom/teletype-client') +const {FollowState, TeletypeClient} = require('@atom/teletype-client') const HostPortalBinding = require('../lib/host-portal-binding') const FakeClipboard = require('./helpers/fake-clipboard') const FakePortal = require('./helpers/fake-portal') +const FakeEditorProxy = require('./helpers/fake-editor-proxy') suite('HostPortalBinding', () => { teardown(async () => { @@ -49,6 +50,33 @@ suite('HostPortalBinding', () => { assert(atomEnv.notifications.getNotifications()[0].message.includes('@site-3')) }) + test('switching the active editor to a remote editor that had been moved into a non-active pane', async () => { + const client = new TeletypeClient({pubSubGateway: {}}) + const portal = new FakePortal() + client.createPortal = () => portal + const atomEnv = buildAtomEnvironment() + const portalBinding = buildHostPortalBinding(client, atomEnv) + await portalBinding.initialize() + + const editor1 = await atomEnv.workspace.open() + const editorProxy1 = portal.getActiveEditorProxy() + + const editor2 = await atomEnv.workspace.open() + const editorProxy2 = portal.getActiveEditorProxy() + + const leftPane = atomEnv.workspace.getActivePane() + const rightPane = leftPane.splitRight({moveActiveItem: true}) + assert.equal(leftPane.getItems().length, 1) + assert.equal(rightPane.getItems().length, 1) + assert.equal(atomEnv.workspace.getActivePane(), rightPane) + + leftPane.activate() + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy2) + assert.equal(leftPane.getItems().length, 1) + assert.equal(rightPane.getItems().length, 1) + assert.equal(atomEnv.workspace.getActivePane(), rightPane) + }) + test('toggling site position components visibility when switching between shared and non-shared pane items', async () => { const client = new TeletypeClient({pubSubGateway: {}}) const portal = new FakePortal() From 4acaafce2b047a3c76bae380c20c51fcd5b9d1cd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2017 15:24:06 +0100 Subject: [PATCH 51/65] :shirt: Fix linter warnings --- test/host-portal-binding.test.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/host-portal-binding.test.js b/test/host-portal-binding.test.js index 934016d2..a8bcf9df 100644 --- a/test/host-portal-binding.test.js +++ b/test/host-portal-binding.test.js @@ -5,7 +5,6 @@ const {FollowState, TeletypeClient} = require('@atom/teletype-client') const HostPortalBinding = require('../lib/host-portal-binding') const FakeClipboard = require('./helpers/fake-clipboard') const FakePortal = require('./helpers/fake-portal') -const FakeEditorProxy = require('./helpers/fake-editor-proxy') suite('HostPortalBinding', () => { teardown(async () => { @@ -58,10 +57,8 @@ suite('HostPortalBinding', () => { const portalBinding = buildHostPortalBinding(client, atomEnv) await portalBinding.initialize() - const editor1 = await atomEnv.workspace.open() - const editorProxy1 = portal.getActiveEditorProxy() - - const editor2 = await atomEnv.workspace.open() + await atomEnv.workspace.open() + await atomEnv.workspace.open() const editorProxy2 = portal.getActiveEditorProxy() const leftPane = atomEnv.workspace.getActivePane() From d7e1db407d37b4d226fbfb84413671ab541e5cf0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2017 17:23:47 +0100 Subject: [PATCH 52/65] Relay active editor changes only if they're manually initiated by users This prevents a bug that was previously causing followers to disconnect their tether when the leader rapidly switched among tabs and destroyed them before followers had the chance to download the remote editors/buffers associated with them. In particular, followers would receive two messages indicating that the leader wanted to switch to a new tab and that they wanted to close the previous one. When trying to download the editor associated with the new tab, the guest would find out that such editor did not exist anymore and, therefore, would skip that message and only process the second message indicating that the old editor needed to be removed. In doing so, it would remove the old editor from the workspace and call `portal.activateEditorProxy` with the editor proxy associated with the newly active pane item in the workspace. In turn, this would cause the follower to disconnect their tether, even if the two peers did not really want to start browsing tabs on their own. With this commit we are explicitly preventing this problem from happening by not relaying active editor changes to `portal` when such changes happen as a result of an update to the workspace caused by the leader while the tether is retracted. --- lib/guest-portal-binding.js | 8 ++++++- test/guest-portal-binding.test.js | 39 +++++++++++++++++++++++++++++++ test/helpers/fake-portal.js | 17 ++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 834f13c1..af163c1c 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -20,6 +20,7 @@ class GuestPortalBinding { this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.lastEditorProxyChangePromise = Promise.resolve() + this.shouldRelayActiveEditorChanges = true } async initialize () { @@ -74,7 +75,10 @@ class GuestPortalBinding { await this.toggleEmptyPortalPaneItem() + const isRetracted = this.portal.resolveFollowState() === FollowState.RETRACTED + this.shouldRelayActiveEditorChanges = !isRetracted editorBinding.editor.destroy() + this.shouldRelayActiveEditorChanges = true }) return this.lastEditorProxyChangePromise @@ -98,7 +102,9 @@ class GuestPortalBinding { async _updateTether (followState, editorProxy, position) { if (followState === FollowState.RETRACTED) { const editor = this.findOrCreateEditorForEditorProxy(editorProxy) + this.shouldRelayActiveEditorChanges = false await this.openPaneItem(editor) + this.shouldRelayActiveEditorChanges = true await this.toggleEmptyPortalPaneItem() } else { this.editorBindingsByEditorProxy.forEach((b) => b.updateTether(followState)) @@ -237,7 +243,7 @@ class GuestPortalBinding { this.sitePositionsController.hide() } - this.portal.activateEditorProxy(editorProxy) + if (this.shouldRelayActiveEditorChanges) this.portal.activateEditorProxy(editorProxy) } } diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index ba902f56..0d289504 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -111,6 +111,45 @@ suite('GuestPortalBinding', () => { assert.equal(atomEnv.workspace.getActivePane(), rightPane) }) + test('relaying active editor changes', async () => { + const portal = new FakePortal() + const client = {joinPortal: () => portal} + const atomEnv = buildAtomEnvironment() + const portalBinding = buildGuestPortalBinding(client, atomEnv, 'some-portal') + await portalBinding.initialize() + + // Manually switching to another editor relays active editor changes to the client. + await atomEnv.workspace.open() + assert.equal(portal.activeEditorProxyChangeCount, 1) + + portal.setFollowState(FollowState.RETRACTED) + + // Updating tether and removing editor proxies while retracted doesn't relay + // active editor changes to the client. + const editorProxy1 = new FakeEditorProxy('editor-1') + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy1) + assert.equal(portal.activeEditorProxyChangeCount, 1) + + const editorProxy2 = new FakeEditorProxy('editor-2') + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy2) + assert.equal(portal.activeEditorProxyChangeCount, 1) + + const editorProxy3 = new FakeEditorProxy('editor-3') + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy3) + assert.equal(portal.activeEditorProxyChangeCount, 1) + + await portalBinding.removeEditorProxy(editorProxy3) + assert.equal(portal.activeEditorProxyChangeCount, 1) + assert(atomEnv.workspace.getActivePaneItem().getTitle().includes('editor-2')) + + portal.setFollowState(FollowState.DISCONNECTED) + + // Removing editor proxies while not retracted relays active editor changes to the client. + await portalBinding.removeEditorProxy(editorProxy2) + assert.equal(portal.activeEditorProxyChangeCount, 2) + assert(atomEnv.workspace.getActivePaneItem().getTitle().includes('editor-1')) + }) + test('toggling site position components visibility when switching tabs', async () => { const stubPubSubGateway = {} const client = new TeletypeClient({pubSubGateway: stubPubSubGateway}) diff --git a/test/helpers/fake-portal.js b/test/helpers/fake-portal.js index 15577d04..372db18f 100644 --- a/test/helpers/fake-portal.js +++ b/test/helpers/fake-portal.js @@ -1,5 +1,11 @@ +const {FollowState} = require('@atom/teletype-client') + module.exports = class FakePortal { + constructor () { + this.activeEditorProxyChangeCount = 0 + } + createBufferProxy () { return { dispose () {}, @@ -17,10 +23,20 @@ class FakePortal { follow (siteId) { this.followedSiteId = siteId + this.setFollowState(FollowState.RETRACTED) } unfollow () { this.followedSiteId = null + this.setFollowState(FollowState.DISCONNECTED) + } + + setFollowState (followState) { + this.followState = followState + } + + resolveFollowState () { + return this.followState } getFollowedSiteId () { @@ -29,6 +45,7 @@ class FakePortal { activateEditorProxy (editorProxy) { this.activeEditorProxy = editorProxy + this.activeEditorProxyChangeCount++ } removeEditorProxy () {} From 672bf256df38f628a949e801180183f6216976ab Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2017 17:32:22 +0100 Subject: [PATCH 53/65] :green_heart: Fix tests --- lib/host-portal-binding.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index fe6a3796..ae4e26d4 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -92,6 +92,8 @@ class HostPortalBinding { this._updateTether(followState, editorProxy, position) ) } + + return this.lastUpdateTetherPromise } // Private From c5cdec3ccb7f497e9b8d3b9ffeb7141f12bed1f9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 7 Dec 2017 13:49:50 +0100 Subject: [PATCH 54/65] Show site positions when moving from a local editor to empty portal item --- lib/guest-portal-binding.js | 17 ++++++++--------- test/guest-portal-binding.test.js | 21 ++++++++++++++++++--- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index af163c1c..f2ab4f5b 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -235,16 +235,15 @@ class GuestPortalBinding { } didChangeActivePaneItem (paneItem) { - if (paneItem !== this.getEmptyPortalPaneItem()) { - const editorProxy = this.editorProxiesByEditor.get(paneItem) - if (editorProxy) { - this.sitePositionsController.show() - } else { - this.sitePositionsController.hide() - } - - if (this.shouldRelayActiveEditorChanges) this.portal.activateEditorProxy(editorProxy) + const editorProxy = this.editorProxiesByEditor.get(paneItem) + + if (editorProxy || paneItem === this.getEmptyPortalPaneItem()) { + this.sitePositionsController.show() + } else { + this.sitePositionsController.hide() } + + if (this.shouldRelayActiveEditorChanges) this.portal.activateEditorProxy(editorProxy) } hasPaneItem (paneItem) { diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index 0d289504..d0bd9954 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -4,6 +4,7 @@ const {FollowState, TeletypeClient} = require('@atom/teletype-client') const FakePortal = require('./helpers/fake-portal') const FakeEditorProxy = require('./helpers/fake-editor-proxy') const GuestPortalBinding = require('../lib/guest-portal-binding') +const EmptyPortalPaneItem = require('../lib/empty-portal-pane-item') suite('GuestPortalBinding', () => { teardown(async () => { @@ -157,16 +158,30 @@ suite('GuestPortalBinding', () => { client.joinPortal = () => portal const atomEnv = buildAtomEnvironment() const portalBinding = buildGuestPortalBinding(client, atomEnv, 'some-portal') + await portalBinding.initialize() + assert.equal(portalBinding.sitePositionsController.visible, true) - await portalBinding.updateTether(FollowState.RETRACTED, new FakeEditorProxy('some-uri')) + const localPaneItem1 = await atomEnv.workspace.open() + assert.equal(portalBinding.sitePositionsController.visible, false) + + const editorProxy = new FakeEditorProxy('some-uri') + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy) const portalPaneItem = atomEnv.workspace.getActivePaneItem() assert.equal(portalBinding.sitePositionsController.visible, true) - await atomEnv.workspace.open() + await atomEnv.workspace.open(localPaneItem1) + assert.equal(portalBinding.sitePositionsController.visible, false) + + localPaneItem1.destroy() + assert.equal(portalBinding.sitePositionsController.visible, true) + + const localPaneItem2 = await atomEnv.workspace.open() assert.equal(portalBinding.sitePositionsController.visible, false) - await atomEnv.workspace.open(portalPaneItem) + await portalBinding.removeEditorProxy(editorProxy) + localPaneItem2.destroy() + assert(atomEnv.workspace.getActivePaneItem() instanceof EmptyPortalPaneItem) assert.equal(portalBinding.sitePositionsController.visible, true) }) From 5712447faac48c56a7ff583596dc185032674afa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 7 Dec 2017 13:58:19 +0100 Subject: [PATCH 55/65] :fire: Delete unnecessary test line --- test/guest-portal-binding.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index d0bd9954..06867675 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -167,7 +167,6 @@ suite('GuestPortalBinding', () => { const editorProxy = new FakeEditorProxy('some-uri') await portalBinding.updateTether(FollowState.RETRACTED, editorProxy) - const portalPaneItem = atomEnv.workspace.getActivePaneItem() assert.equal(portalBinding.sitePositionsController.visible, true) await atomEnv.workspace.open(localPaneItem1) From 0d96a82e167a78f362ca06c4ddf3d9d8469c502e Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 7 Dec 2017 11:18:35 -0500 Subject: [PATCH 56/65] Refollow host when they close last remote editor on guest --- lib/guest-portal-binding.js | 27 +++++++++++++++++---------- test/guest-portal-binding.test.js | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index f2ab4f5b..5458f2f4 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -71,14 +71,19 @@ class GuestPortalBinding { removeEditorProxy (editorProxy) { this.lastEditorProxyChangePromise = this.lastEditorProxyChangePromise.then(async () => { const editorBinding = this.editorBindingsByEditorProxy.get(editorProxy) - editorBinding.dispose() - - await this.toggleEmptyPortalPaneItem() - - const isRetracted = this.portal.resolveFollowState() === FollowState.RETRACTED - this.shouldRelayActiveEditorChanges = !isRetracted - editorBinding.editor.destroy() - this.shouldRelayActiveEditorChanges = true + if (editorBinding) { + editorBinding.dispose() + if (this.editorBindingsByEditorProxy.size === 0) { + this.portal.follow(1) + } + + await this.toggleEmptyPortalPaneItem() + + const isRetracted = this.portal.resolveFollowState() === FollowState.RETRACTED + this.shouldRelayActiveEditorChanges = !isRetracted + editorBinding.editor.destroy() + this.shouldRelayActiveEditorChanges = true + } }) return this.lastEditorProxyChangePromise @@ -137,8 +142,8 @@ class GuestPortalBinding { this.editorBindingsByEditorProxy.set(editorProxy, editorBinding) this.editorProxiesByEditor.set(editor, editorProxy) editorBinding.onDidDispose(() => { - this.editorBindingsByEditorProxy.delete(editorProxy) this.editorProxiesByEditor.delete(editor) + this.editorBindingsByEditorProxy.delete(editorProxy) }) this.sitePositionsController.addEditorBinding(editorBinding) @@ -243,7 +248,9 @@ class GuestPortalBinding { this.sitePositionsController.hide() } - if (this.shouldRelayActiveEditorChanges) this.portal.activateEditorProxy(editorProxy) + if (this.shouldRelayActiveEditorChanges && paneItem !== this.getEmptyPortalPaneItem()) { + this.portal.activateEditorProxy(editorProxy) + } } hasPaneItem (paneItem) { diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index 06867675..cbed2ff6 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -151,6 +151,30 @@ suite('GuestPortalBinding', () => { assert(atomEnv.workspace.getActivePaneItem().getTitle().includes('editor-1')) }) + test('host closing last remote editor on guest workspace', async () => { + const portal = new FakePortal() + const client = {joinPortal: () => portal} + const atomEnv = buildAtomEnvironment() + const portalBinding = buildGuestPortalBinding(client, atomEnv, 'some-portal') + + await portalBinding.initialize() + portal.setFollowState(FollowState.RETRACTED) + + const editorProxy1 = new FakeEditorProxy('editor-1') + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy1) + + const editorProxy2 = new FakeEditorProxy('editor-2') + portal.setFollowState(FollowState.DISCONNECTED) + await portalBinding.updateTether(FollowState.DISCONNECTED) + + await portalBinding.removeEditorProxy(editorProxy2) + assert.equal(portal.resolveFollowState(), FollowState.DISCONNECTED) + + await portalBinding.removeEditorProxy(editorProxy1) + assert.equal(portal.getFollowedSiteId(), 1) + assert.equal(portal.resolveFollowState(), FollowState.RETRACTED) + }) + test('toggling site position components visibility when switching tabs', async () => { const stubPubSubGateway = {} const client = new TeletypeClient({pubSubGateway: stubPubSubGateway}) From 3e755f264ad3e1384aa2fe5023e8dbacdaffbc2d Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 7 Dec 2017 12:04:10 -0500 Subject: [PATCH 57/65] =?UTF-8?q?=F0=9F=90=9B=20Gracefully=20handle=20atte?= =?UTF-8?q?mpt=20to=20update=20tether=20for=20destroyed=20editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/host-portal-binding.js | 2 +- test/helpers/fake-portal.js | 13 ++++++++++--- test/host-portal-binding.test.js | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index ae4e26d4..e76cfd58 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -102,11 +102,11 @@ class HostPortalBinding { if (followState === FollowState.RETRACTED) { await this.workspace.open(editorBinding.editor, {searchAllPanes: true}) + if (position) editorBinding.updateTether(followState, position) } else { this.editorBindingsByEditorProxy.forEach((b) => b.updateTether(followState)) } - if (position) editorBinding.updateTether(followState, position) } didDestroyPaneItem ({item}) { diff --git a/test/helpers/fake-portal.js b/test/helpers/fake-portal.js index 372db18f..0be5153e 100644 --- a/test/helpers/fake-portal.js +++ b/test/helpers/fake-portal.js @@ -9,14 +9,21 @@ class FakePortal { createBufferProxy () { return { dispose () {}, - setDelegate () {} + setDelegate () {}, + createCheckpoint () {}, + groupChangesSinceCheckpoint () {}, + applyGroupingInterval () {} } } createEditorProxy () { return { - dispose () {}, - setDelegate () {}, + dispose () { + this.delegate.dispose() + }, + setDelegate (delegate) { + this.delegate = delegate + }, updateSelections () {} } } diff --git a/test/host-portal-binding.test.js b/test/host-portal-binding.test.js index a8bcf9df..2cdec45d 100644 --- a/test/host-portal-binding.test.js +++ b/test/host-portal-binding.test.js @@ -74,6 +74,25 @@ suite('HostPortalBinding', () => { assert.equal(atomEnv.workspace.getActivePane(), rightPane) }) + test('gracefully handles attempt to update tether for destroyed editors', async () => { + const client = new TeletypeClient({pubSubGateway: {}}) + const portal = new FakePortal() + client.createPortal = () => portal + const atomEnv = buildAtomEnvironment() + const portalBinding = buildHostPortalBinding(client, atomEnv) + await portalBinding.initialize() + + const editor = await atomEnv.workspace.open() + editor.getBuffer().setTextInRange('Lorem ipsum dolor', [[0, 0], [0, 0]], {undo: 'skip'}) + const editorProxy = portal.getActiveEditorProxy() + + await portalBinding.updateTether(FollowState.RETRACTED, editorProxy, {row: 0, column: 3}) + assert.deepEqual(editor.getCursorBufferPosition(), {row: 0, column: 3}) + + editor.destroy() + await portalBinding.updateTether(FollowState.DISCONNECTED, editorProxy, {row: 0, column: 5}) + }) + test('toggling site position components visibility when switching between shared and non-shared pane items', async () => { const client = new TeletypeClient({pubSubGateway: {}}) const portal = new FakePortal() From 1aa4362081c564507df7b95d532b63fd56b3a099 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 7 Dec 2017 12:21:22 -0500 Subject: [PATCH 58/65] :shirt: Fix linter violation --- lib/host-portal-binding.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index e76cfd58..c2adb87b 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -106,7 +106,6 @@ class HostPortalBinding { } else { this.editorBindingsByEditorProxy.forEach((b) => b.updateTether(followState)) } - } didDestroyPaneItem ({item}) { From 247c95f9b79245c20ad7c6721787fce8ee75fe96 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 7 Dec 2017 12:30:07 -0500 Subject: [PATCH 59/65] :art: Use FakeEditorProxy in FakeEditorPortal --- test/helpers/fake-editor-proxy.js | 8 +++++++- test/helpers/fake-portal.js | 11 ++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/test/helpers/fake-editor-proxy.js b/test/helpers/fake-editor-proxy.js index 8a5160e2..19a9cf1f 100644 --- a/test/helpers/fake-editor-proxy.js +++ b/test/helpers/fake-editor-proxy.js @@ -11,11 +11,17 @@ class FakeEditorProxy { } } + dispose () { + if (this.delegate) this.delegate.dispose() + } + follow () {} didScroll () {} - setDelegate () {} + setDelegate (delegate) { + this.delegate = delegate + } updateSelections () {} } diff --git a/test/helpers/fake-portal.js b/test/helpers/fake-portal.js index 0be5153e..5ea4b865 100644 --- a/test/helpers/fake-portal.js +++ b/test/helpers/fake-portal.js @@ -1,4 +1,5 @@ const {FollowState} = require('@atom/teletype-client') +const FakeEditorProxy = require('./fake-editor-proxy') module.exports = class FakePortal { @@ -17,15 +18,7 @@ class FakePortal { } createEditorProxy () { - return { - dispose () { - this.delegate.dispose() - }, - setDelegate (delegate) { - this.delegate = delegate - }, - updateSelections () {} - } + return new FakeEditorProxy() } follow (siteId) { From b16a19b81fe7e76015a77cdee9fff923bf122d38 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 7 Dec 2017 17:36:49 -0500 Subject: [PATCH 60/65] =?UTF-8?q?=F0=9F=90=9B=20Deremotify=20Buffer=20when?= =?UTF-8?q?=20deremotifying=20its=20last=20remote=20Editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this change, if I host split an editor and then closed one of the two tabs for the editor, the remote editor on the host would have its title changed to "untitled". With this change, we keep track of how many remote editors we have for a remote buffer. When we deremotify the last editor for the buffer, we deremotify the buffer as well. --- lib/editor-binding.js | 18 +++++++++++++--- test/teletype-package.test.js | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/lib/editor-binding.js b/lib/editor-binding.js index 038d7456..9e533867 100644 --- a/lib/editor-binding.js +++ b/lib/editor-binding.js @@ -21,6 +21,9 @@ class EditorBinding { } dispose () { + if (this.disposed) return + + this.disposed = true this.subscriptions.dispose() this.markerLayersBySiteId.forEach((l) => l.destroy()) @@ -68,9 +71,13 @@ class EditorBinding { editor.copy = () => null editor.serialize = () => null editor.isRemote = true + + let remoteEditorCountForBuffer = buffer.remoteEditorCount || 0 + buffer.remoteEditorCount = ++remoteEditorCountForBuffer buffer.getPath = () => `${uriPrefix}:${bufferURI}` buffer.save = () => {} buffer.isModified = () => false + editor.element.classList.add('teletype-RemotePaneItem') } @@ -84,9 +91,14 @@ class EditorBinding { delete editor.copy delete editor.serialize delete editor.isRemote - delete buffer.getPath - delete buffer.save - delete buffer.isModified + + buffer.remoteEditorCount-- + if (buffer.remoteEditorCount === 0) { + delete buffer.remoteEditorCount + delete buffer.getPath + delete buffer.save + delete buffer.isModified + } editor.element.classList.remove('teletype-RemotePaneItem') editor.emitter.emit('did-change-title', editor.getTitle()) diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index c51d9710..92f3b81c 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -720,6 +720,45 @@ suite('TeletypePackage', function () { await editorsEqual(guestEditor1, hostEditor1) }) + test('remotifying and deremotifying guest editors and buffers when host splits an editor', async () => { + const hostEnv = buildAtomEnvironment() + const hostPackage = await buildPackage(hostEnv) + const portal = await hostPackage.sharePortal() + const guestEnv = buildAtomEnvironment() + const guestPackage = await buildPackage(guestEnv) + guestPackage.joinPortal(portal.id) + + const hostEditor1 = await hostEnv.workspace.open(path.join(temp.path(), 'a.txt')) + const guestEditor1 = await getNextActiveTextEditorPromise(guestEnv) + + hostEnv.workspace.paneForItem(hostEditor1).splitRight({copyActiveItem: true}) + const hostEditor2 = hostEnv.workspace.getActiveTextEditor() + const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) + + assert.deepEqual(getPaneItems(guestEnv), [guestEditor1, guestEditor2]) + assert(guestEditor1.isRemote) + assert(guestEditor1.getTitle().endsWith('a.txt')) + assert(guestEditor1.getBuffer().getPath().endsWith('a.txt')) + assert(guestEditor2.isRemote) + assert(guestEditor2.getTitle().endsWith('a.txt')) + assert(guestEditor2.getBuffer().getPath().endsWith('a.txt')) + + hostEditor2.destroy() + await condition(() => deepEqual(getPaneItems(guestEnv), [guestEditor1])) + + assert(guestEditor1.isRemote) + assert(guestEditor1.getTitle().endsWith('a.txt')) + assert(guestEditor1.getBuffer().getPath().endsWith('a.txt')) + + hostPackage.closeHostPortal() + + await condition(() => + !guestEditor1.isRemote && + guestEditor1.getTitle() === 'untitled' && + guestEditor1.getBuffer().getPath() === undefined + ) + }) + test('propagating nested marker layer updates that depend on text updates in a nested transaction', async () => { const hostEnv = buildAtomEnvironment() const hostPackage = await buildPackage(hostEnv) From b67f5c8cc1637aed616a95eb321af3edc43085d6 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 7 Dec 2017 17:48:42 -0500 Subject: [PATCH 61/65] =?UTF-8?q?=F0=9F=8E=A8=20Group=20the=20"split=20pan?= =?UTF-8?q?e"=20tests=20into=20a=20suite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/teletype-package.test.js | 139 +++++++++++++++++----------------- 1 file changed, 71 insertions(+), 68 deletions(-) diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index 92f3b81c..95ea729b 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -678,85 +678,88 @@ suite('TeletypePackage', function () { } }) - test('splitting editors', async () => { - const hostEnv = buildAtomEnvironment() - const hostPackage = await buildPackage(hostEnv) - const portal = await hostPackage.sharePortal() - - const guestEnv = buildAtomEnvironment() - const guestPackage = await buildPackage(guestEnv) - guestPackage.joinPortal(portal.id) - - const hostEditor1 = await hostEnv.workspace.open() - hostEditor1.setText('hello = "world"') - hostEditor1.setCursorBufferPosition([0, 0]) - hostEditor1.insertText('const ') - - hostEnv.workspace.paneForItem(hostEditor1).splitRight({copyActiveItem: true}) - const hostEditor2 = hostEnv.workspace.getActiveTextEditor() - hostEditor2.setCursorBufferPosition([0, 8]) - - assert.equal(hostEditor2.getBuffer(), hostEditor1.getBuffer()) - - const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) - guestEditor2.setCursorBufferPosition([0, Infinity]) - guestEditor2.insertText('\nconst goodbye = "moon"') - await editorsEqual(guestEditor2, hostEditor2) - await timeout(guestPackage.tetherDisconnectWindow) + suite('host splitting editors', async () => { + test('supporting distinct selections per editor with a shared undo stack for the buffer', async () => { + const hostEnv = buildAtomEnvironment() + const hostPackage = await buildPackage(hostEnv) + const portal = await hostPackage.sharePortal() - hostEditor2.undo() - assert.equal(hostEditor2.getText(), 'hello = "world"\nconst goodbye = "moon"') - assert.equal(hostEditor1.getText(), hostEditor2.getText()) - await editorsEqual(hostEditor2, guestEditor2) + const guestEnv = buildAtomEnvironment() + const guestPackage = await buildPackage(guestEnv) + guestPackage.joinPortal(portal.id) - hostEnv.workspace.paneForItem(hostEditor1).activate() - const guestEditor1 = await getNextActiveTextEditorPromise(guestEnv) - assert.equal(guestEditor1.getBuffer(), guestEditor2.getBuffer()) - await editorsEqual(guestEditor1, hostEditor1) + const hostEditor1 = await hostEnv.workspace.open() + hostEditor1.setText('hello = "world"') + hostEditor1.setCursorBufferPosition([0, 0]) + hostEditor1.insertText('const ') + + hostEnv.workspace.paneForItem(hostEditor1).splitRight({copyActiveItem: true}) + const hostEditor2 = hostEnv.workspace.getActiveTextEditor() + hostEditor2.setCursorBufferPosition([0, 8]) + + assert.equal(hostEditor2.getBuffer(), hostEditor1.getBuffer()) + + const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) + guestEditor2.setCursorBufferPosition([0, Infinity]) + guestEditor2.insertText('\nconst goodbye = "moon"') + await editorsEqual(guestEditor2, hostEditor2) + await timeout(guestPackage.tetherDisconnectWindow) + + hostEditor2.undo() + assert.equal(hostEditor2.getText(), 'hello = "world"\nconst goodbye = "moon"') + assert.equal(hostEditor1.getText(), hostEditor2.getText()) + await editorsEqual(hostEditor2, guestEditor2) + + hostEnv.workspace.paneForItem(hostEditor1).activate() + const guestEditor1 = await getNextActiveTextEditorPromise(guestEnv) + assert.equal(guestEditor1.getBuffer(), guestEditor2.getBuffer()) + await editorsEqual(guestEditor1, hostEditor1) + + guestEditor1.undo() + assert.equal(guestEditor1.getText(), 'hello = "world"') + assert.equal(guestEditor2.getText(), guestEditor1.getText()) + await editorsEqual(guestEditor1, hostEditor1) + }) - guestEditor1.undo() - assert.equal(guestEditor1.getText(), 'hello = "world"') - assert.equal(guestEditor2.getText(), guestEditor1.getText()) - await editorsEqual(guestEditor1, hostEditor1) - }) + test('remotifying and deremotifying guest editors and buffers', async () => { + const hostEnv = buildAtomEnvironment() + const hostPackage = await buildPackage(hostEnv) + const portal = await hostPackage.sharePortal() - test('remotifying and deremotifying guest editors and buffers when host splits an editor', async () => { - const hostEnv = buildAtomEnvironment() - const hostPackage = await buildPackage(hostEnv) - const portal = await hostPackage.sharePortal() - const guestEnv = buildAtomEnvironment() - const guestPackage = await buildPackage(guestEnv) - guestPackage.joinPortal(portal.id) + const guestEnv = buildAtomEnvironment() + const guestPackage = await buildPackage(guestEnv) + guestPackage.joinPortal(portal.id) - const hostEditor1 = await hostEnv.workspace.open(path.join(temp.path(), 'a.txt')) - const guestEditor1 = await getNextActiveTextEditorPromise(guestEnv) + const hostEditor1 = await hostEnv.workspace.open(path.join(temp.path(), 'a.txt')) + const guestEditor1 = await getNextActiveTextEditorPromise(guestEnv) - hostEnv.workspace.paneForItem(hostEditor1).splitRight({copyActiveItem: true}) - const hostEditor2 = hostEnv.workspace.getActiveTextEditor() - const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) + hostEnv.workspace.paneForItem(hostEditor1).splitRight({copyActiveItem: true}) + const hostEditor2 = hostEnv.workspace.getActiveTextEditor() + const guestEditor2 = await getNextActiveTextEditorPromise(guestEnv) - assert.deepEqual(getPaneItems(guestEnv), [guestEditor1, guestEditor2]) - assert(guestEditor1.isRemote) - assert(guestEditor1.getTitle().endsWith('a.txt')) - assert(guestEditor1.getBuffer().getPath().endsWith('a.txt')) - assert(guestEditor2.isRemote) - assert(guestEditor2.getTitle().endsWith('a.txt')) - assert(guestEditor2.getBuffer().getPath().endsWith('a.txt')) + assert.deepEqual(getPaneItems(guestEnv), [guestEditor1, guestEditor2]) + assert(guestEditor1.isRemote) + assert(guestEditor1.getTitle().endsWith('a.txt')) + assert(guestEditor1.getBuffer().getPath().endsWith('a.txt')) + assert(guestEditor2.isRemote) + assert(guestEditor2.getTitle().endsWith('a.txt')) + assert(guestEditor2.getBuffer().getPath().endsWith('a.txt')) - hostEditor2.destroy() - await condition(() => deepEqual(getPaneItems(guestEnv), [guestEditor1])) + hostEditor2.destroy() + await condition(() => deepEqual(getPaneItems(guestEnv), [guestEditor1])) - assert(guestEditor1.isRemote) - assert(guestEditor1.getTitle().endsWith('a.txt')) - assert(guestEditor1.getBuffer().getPath().endsWith('a.txt')) + assert(guestEditor1.isRemote) + assert(guestEditor1.getTitle().endsWith('a.txt')) + assert(guestEditor1.getBuffer().getPath().endsWith('a.txt')) - hostPackage.closeHostPortal() + hostPackage.closeHostPortal() - await condition(() => - !guestEditor1.isRemote && - guestEditor1.getTitle() === 'untitled' && - guestEditor1.getBuffer().getPath() === undefined - ) + await condition(() => + !guestEditor1.isRemote && + guestEditor1.getTitle() === 'untitled' && + guestEditor1.getBuffer().getPath() === undefined + ) + }) }) test('propagating nested marker layer updates that depend on text updates in a nested transaction', async () => { From 2905e6741f2c62dab299456fbc394732865d8fbf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 Dec 2017 09:18:33 +0100 Subject: [PATCH 62/65] Dispose editor binding on guests when the underlying editor is destroyed --- lib/editor-binding.js | 1 + test/editor-binding.test.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/editor-binding.js b/lib/editor-binding.js index 9e533867..3f9e692b 100644 --- a/lib/editor-binding.js +++ b/lib/editor-binding.js @@ -41,6 +41,7 @@ class EditorBinding { this.editor.onDidDestroy(() => this.editorProxy.dispose()) } else { this.monkeyPatchEditorMethods(this.editor, this.editorProxy) + this.editor.onDidDestroy(() => this.dispose()) } this.localCursorLayerDecoration = this.editor.decorateMarkerLayer( diff --git a/test/editor-binding.test.js b/test/editor-binding.test.js index 0473f87c..40a89e8b 100644 --- a/test/editor-binding.test.js +++ b/test/editor-binding.test.js @@ -301,6 +301,29 @@ suite('EditorBinding', function () { }) }) + suite('destroying the editor', () => { + test('on the host, disposes the underlying editor proxy', () => { + const editor = new TextEditor() + const binding = new EditorBinding({editor, isHost: true, portal: new FakePortal()}) + const editorProxy = new FakeEditorProxy(binding) + binding.setEditorProxy(editorProxy) + + editor.destroy() + assert(editorProxy.disposed) + }) + + test('on guests, disposes the editor binding', () => { + const editor = new TextEditor() + const binding = new EditorBinding({editor, isHost: false, portal: new FakePortal()}) + const editorProxy = new FakeEditorProxy(binding) + binding.setEditorProxy(editorProxy) + + editor.destroy() + assert(binding.disposed) + assert(!editorProxy.disposed) + }) + }) + suite('guest editor binding', () => { test('overrides the editor methods when setting the proxy, and restores them on dispose', () => { const buffer = new TextBuffer({text: SAMPLE_TEXT}) @@ -450,6 +473,11 @@ class FakeEditorProxy { this.bufferProxy = {uri: 'fake-buffer-proxy-uri'} this.selections = {} this.siteId = (siteId == null) ? 1 : siteId + this.disposed = false + } + + dispose () { + this.disposed = true } didScroll () {} From b6c9d15e7a6020b4cf578b5fcd38a633941c801f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 Dec 2017 09:43:29 +0100 Subject: [PATCH 63/65] Don't dispose buffer proxy on guest when underlying buffer is destroyed --- lib/buffer-binding.js | 15 +++++++++++++-- lib/guest-portal-binding.js | 1 + lib/host-portal-binding.js | 2 +- test/buffer-binding.test.js | 32 +++++++++++++++++++++++++++++++ test/helpers/fake-editor-proxy.js | 5 ++++- 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/lib/buffer-binding.js b/lib/buffer-binding.js index a3a78ed7..3c184edd 100644 --- a/lib/buffer-binding.js +++ b/lib/buffer-binding.js @@ -4,13 +4,18 @@ function doNothing () {} module.exports = class BufferBinding { - constructor ({buffer, didDispose}) { + constructor ({buffer, isHost, didDispose}) { this.buffer = buffer + this.isHost = isHost this.emitDidDispose = didDispose || doNothing this.pendingChanges = [] + this.disposed = false } dispose () { + if (this.disposed) return + + this.disposed = true this.buffer.restoreDefaultHistoryProvider(this.bufferProxy.getHistory(this.buffer.maxUndoEntries)) this.buffer = null if (this.bufferDestroySubscription) this.bufferDestroySubscription.dispose() @@ -24,7 +29,13 @@ class BufferBinding { this.pushChange(this.pendingChanges.shift()) } this.pendingChanges = null - this.bufferDestroySubscription = this.buffer.onDidDestroy(() => bufferProxy.dispose()) + this.bufferDestroySubscription = this.buffer.onDidDestroy(() => { + if (this.isHost) { + bufferProxy.dispose() + } else { + this.dispose() + } + }) } setText (text) { diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 5458f2f4..7beddecc 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -161,6 +161,7 @@ class GuestPortalBinding { buffer = new TextBuffer() bufferBinding = new BufferBinding({ buffer, + isHost: false, didDispose: () => this.bufferBindingsByBufferProxy.delete(bufferProxy) }) bufferBinding.setBufferProxy(bufferProxy) diff --git a/lib/host-portal-binding.js b/lib/host-portal-binding.js index c2adb87b..31a846d1 100644 --- a/lib/host-portal-binding.js +++ b/lib/host-portal-binding.js @@ -143,7 +143,7 @@ class HostPortalBinding { if (bufferBinding) { return bufferBinding.bufferProxy } else { - bufferBinding = new BufferBinding({buffer}) + bufferBinding = new BufferBinding({buffer, isHost: true}) const bufferProxy = this.portal.createBufferProxy({ uri: this.getBufferProxyURI(buffer), history: buffer.getHistory() diff --git a/test/buffer-binding.test.js b/test/buffer-binding.test.js index 69821911..77530aaf 100644 --- a/test/buffer-binding.test.js +++ b/test/buffer-binding.test.js @@ -42,10 +42,42 @@ suite('BufferBinding', function () { assert.equal(bufferProxy.text, 'hello\nworld') }) + suite('destroying the buffer', () => { + test('on the host, disposes the underlying buffer proxy', () => { + const buffer = new TextBuffer('') + const binding = new BufferBinding({buffer, isHost: true}) + const bufferProxy = new FakeBufferProxy(binding, buffer.getText()) + binding.setBufferProxy(bufferProxy) + + buffer.destroy() + assert(bufferProxy.disposed) + }) + + test('on guests, disposes the buffer binding', () => { + const buffer = new TextBuffer('') + const binding = new BufferBinding({buffer, isHost: false}) + const bufferProxy = new FakeBufferProxy(binding, buffer.getText()) + binding.setBufferProxy(bufferProxy) + + buffer.destroy() + assert(binding.disposed) + assert(!bufferProxy.disposed) + }) + }) + class FakeBufferProxy { constructor (delegate, text) { this.delegate = delegate this.text = text + this.disposed = false + } + + dispose () { + this.disposed = true + } + + getHistory () { + return {undoStack: [], redoStack: [], nextCheckpointId: 1} } setTextInRange (oldStart, oldEnd, newText) { diff --git a/test/helpers/fake-editor-proxy.js b/test/helpers/fake-editor-proxy.js index 19a9cf1f..3f486b73 100644 --- a/test/helpers/fake-editor-proxy.js +++ b/test/helpers/fake-editor-proxy.js @@ -7,7 +7,10 @@ class FakeEditorProxy { setDelegate () {}, createCheckpoint () {}, groupChangesSinceCheckpoint () {}, - applyGroupingInterval () {} + applyGroupingInterval () {}, + getHistory () { + return {undoStack: [], redoStack: [], nextCheckpointId: 1} + } } } From fe02c1afa50d95089525dd61f8e7aa922802cc06 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 11 Dec 2017 09:19:07 -0500 Subject: [PATCH 64/65] Leave portal when guest closes last remote editor When the guest closes the last remote editor or the portal's empty pane item, leave the portal. Prior to this change, if the guest closes the last remote editor, the guest would be in limbo: they're still in the portal, but they don't see any remote editors. Their only remedy was to manually leave and re-join the portal. In an upcoming release, we hope to integrate the guest's fuzzy finder with the portal. Once that functionality is in place: - If the guest closes the last remote editor, the guest could use the fuzzy finder to open any of the available remote editors, thus preventing the limbo state described above - Because of this, we could remove the behavior implemented in this commit (i.e., we would no longer have to leave the portal when the guest closes the last remote editor) --- lib/guest-portal-binding.js | 14 ++++++++ test/guest-portal-binding.test.js | 1 + test/helpers/fake-portal.js | 2 ++ test/teletype-package.test.js | 59 ++++++++++++++++++++++++------- 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/lib/guest-portal-binding.js b/lib/guest-portal-binding.js index 7beddecc..b26aa29c 100644 --- a/lib/guest-portal-binding.js +++ b/lib/guest-portal-binding.js @@ -35,6 +35,7 @@ class GuestPortalBinding { await this.toggleEmptyPortalPaneItem() this.subscriptions.add(this.workspace.onDidChangeActivePaneItem(this.didChangeActivePaneItem.bind(this))) + this.subscriptions.add(this.workspace.onDidDestroyPaneItem(this.didDestroyPaneItem.bind(this))) return true } catch (error) { this.didFailToJoin(error) @@ -81,7 +82,9 @@ class GuestPortalBinding { const isRetracted = this.portal.resolveFollowState() === FollowState.RETRACTED this.shouldRelayActiveEditorChanges = !isRetracted + this.lastDestroyedEditorWasRemovedByHost = true editorBinding.editor.destroy() + this.lastDestroyedEditorWasRemovedByHost = false this.shouldRelayActiveEditorChanges = true } }) @@ -254,6 +257,17 @@ class GuestPortalBinding { } } + didDestroyPaneItem () { + const emptyPortalItem = this.getEmptyPortalPaneItem() + const hasNoPortalPaneItem = this.workspace.getPaneItems().every((item) => ( + item !== emptyPortalItem && !this.editorProxiesByEditor.has(item) + )) + const lastDestroyedEditorWasClosedManually = !this.lastDestroyedEditorWasRemovedByHost + if (hasNoPortalPaneItem && lastDestroyedEditorWasClosedManually) { + this.leave() + } + } + hasPaneItem (paneItem) { return ( paneItem === this.getEmptyPortalPaneItem() || diff --git a/test/guest-portal-binding.test.js b/test/guest-portal-binding.test.js index cbed2ff6..c83a1da4 100644 --- a/test/guest-portal-binding.test.js +++ b/test/guest-portal-binding.test.js @@ -173,6 +173,7 @@ suite('GuestPortalBinding', () => { await portalBinding.removeEditorProxy(editorProxy1) assert.equal(portal.getFollowedSiteId(), 1) assert.equal(portal.resolveFollowState(), FollowState.RETRACTED) + assert(!portal.disposed) }) test('toggling site position components visibility when switching tabs', async () => { diff --git a/test/helpers/fake-portal.js b/test/helpers/fake-portal.js index 5ea4b865..d1db8ea9 100644 --- a/test/helpers/fake-portal.js +++ b/test/helpers/fake-portal.js @@ -7,6 +7,8 @@ class FakePortal { this.activeEditorProxyChangeCount = 0 } + dispose () {} + createBufferProxy () { return { dispose () {}, diff --git a/test/teletype-package.test.js b/test/teletype-package.test.js index 95ea729b..a10f645f 100644 --- a/test/teletype-package.test.js +++ b/test/teletype-package.test.js @@ -391,21 +391,54 @@ suite('TeletypePackage', function () { assert(errorNotification, 'Expected notifications to include "Portal not found" error') }) - test('guest leaving portal', async () => { - const hostEnv = buildAtomEnvironment() - const hostPackage = await buildPackage(hostEnv) - const guestEnv = buildAtomEnvironment() - const guestPackage = await buildPackage(guestEnv) - const portal = await hostPackage.sharePortal() - await guestPackage.joinPortal(portal.id) + suite('guest leaving portal', () => { + test('via explicit leave action', async () => { + const hostEnv = buildAtomEnvironment() + const hostPackage = await buildPackage(hostEnv) + const guestEnv = buildAtomEnvironment() + const guestPackage = await buildPackage(guestEnv) + const portal = await hostPackage.sharePortal() + await guestPackage.joinPortal(portal.id) - await hostEnv.workspace.open() - await hostEnv.workspace.open() - await hostEnv.workspace.open() - await condition(() => getPaneItems(guestEnv).length === 3) + await hostEnv.workspace.open() + await hostEnv.workspace.open() + await hostEnv.workspace.open() + await condition(() => getPaneItems(guestEnv).length === 3) - await guestPackage.leavePortal() - await condition(() => getPaneItems(guestEnv).length === 0) + await guestPackage.leavePortal() + await condition(() => getPaneItems(guestEnv).length === 0) + }) + + test('via closing last remote editor', async () => { + const hostEnv = buildAtomEnvironment() + const hostPackage = await buildPackage(hostEnv) + const hostPortal = await hostPackage.sharePortal() + await hostEnv.workspace.open(path.join(temp.path(), 'some-file')) + + const guestEnv = buildAtomEnvironment() + const guestPackage = await buildPackage(guestEnv) + const guestPortal = await guestPackage.joinPortal(hostPortal.id) + + await condition(() => getRemotePaneItems(guestEnv).length === 1) + const guestEditor = guestEnv.workspace.getActivePaneItem() + assert(guestEditor instanceof TextEditor) + guestEnv.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + assert(guestPortal.disposed) + }) + + test('via closing empty portal pane item', async () => { + const hostEnv = buildAtomEnvironment() + const hostPackage = await buildPackage(hostEnv) + const guestEnv = buildAtomEnvironment() + const guestPackage = await buildPackage(guestEnv) + const hostPortal = await hostPackage.sharePortal() + const guestPortal = await guestPackage.joinPortal(hostPortal.id) + + const guestEditor = getRemotePaneItems(guestEnv)[0] + assert(guestEditor instanceof EmptyPortalPaneItem) + guestEnv.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + assert(guestPortal.disposed) + }) }) test('host closing portal', async function () { From f25596a827e61b50116e3b1c4417622f58293c87 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 11 Dec 2017 10:30:58 -0500 Subject: [PATCH 65/65] :arrow_up: teletype-client@v0.29.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 94ffb2f7..0934237f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "temp": "^0.8.3" }, "dependencies": { - "@atom/teletype-client": "https://github.com/atom/teletype-client#multi-buffer-sharing", + "@atom/teletype-client": "^0.29.0", "etch": "^0.12.6" }, "consumedServices": {