Update data attribute in specs

Update data attribute in specs

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -241,7 +241,7 @@
         it "should be moved to the right", ->
           expect(treeView).toBeVisible()
           atom.workspaceView.trigger 'tree-view:toggle-side'
-          expect(treeView).toMatchSelector('[data-toggle-side="true"]')
+          expect(treeView).toMatchSelector('[data-show-on-right-side="true"]')
       describe "when the tree view is on the right", ->
         beforeEach ->
           atom.workspaceView.trigger 'tree-view:toggle-side'
@@ -249,7 +249,7 @@
         it "should be moved to the left", ->
           expect(treeView).toBeVisible()
           atom.workspaceView.trigger 'tree-view:toggle-side'
-          expect(treeView).toMatchSelector('[data-toggle-side="false"]')
+          expect(treeView).toMatchSelector('[data-show-on-right-side="false"]')

   describe "when tree-view:toggle-focus is triggered on the root view", ->
     beforeEach ->


Don't use atom.workspaceView in specs

Don't use atom.workspaceView in specs

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -14,7 +14,7 @@
     causeFileToOpen()

 describe "TreeView", ->
-  [treeView, root, sampleJs, sampleTxt] = []
+  [treeView, root, sampleJs, sampleTxt, workspaceElement] = []

   beforeEach ->
     tempPath = fs.realpathSync(temp.mkdirSync('atom'))
@@ -22,14 +22,15 @@
     wrench.copyDirSyncRecursive(fixturesPath, tempPath, forceDelete: true)
     atom.project.setPaths([path.join(tempPath, 'tree-view')])

-    atom.workspaceView = new WorkspaceView
+    workspaceElement = atom.views.getView(atom.workspace)

     waitsForPromise ->
       atom.packages.activatePackage("tree-view")

     runs ->
-      atom.workspaceView.trigger 'tree-view:toggle'
-      treeView = atom.workspaceView.find(".tree-view").view()
+      atom.commands.dispatch(workspaceElement, 'tree-view:toggle')
+      treeView = $(atom.workspace.getLeftPanels()[0].getItemView()).view()
+
       root = $(treeView.root)
       sampleJs = treeView.find('.file:contains(tree-view.js)')
       sampleTxt = treeView.find('.file:contains(tree-view.txt)')
@@ -103,14 +104,14 @@
           atom.workspace.open(filePath)

         runs ->
-          atom.workspaceView.trigger 'tree-view:reveal-active-file'
+          atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file')
           expect(treeView.hasParent()).toBeFalsy()
           expect(treeView.root).not.toExist()

       describe "when the project is assigned a path because a new buffer is saved", ->
         it "creates a root directory view and attaches to the root view", ->
           waitsForPromise ->
-            atom.workspaceView.open()
+            atom.workspace.open()

           runs ->
             projectPath = temp.mkdirSync('atom-project')
@@ -174,13 +175,13 @@
         atom.packages.activatePackage("tree-view")

       runs ->
-        treeView = atom.workspaceView.find(".tree-view").view()
+        treeView = $(atom.workspace.getLeftPanels()[0].getItemView()).view()
         expect(treeView).toExist()
         expect(treeView.selectedEntry()).toMatchSelector(".file:contains(tree-view.js)")
         expect(treeView.find(".directory:contains(dir1)")).toHaveClass("expanded")

     it "restores the focus state of the tree view", ->
-      atom.workspaceView.attachToDom()
+      jasmine.attachToDOM(workspaceElement)
       treeView.focus()
       expect(treeView.list).toMatchSelector ':focus'
       atom.packages.deactivatePackage("tree-view")
@@ -189,46 +190,46 @@
         atom.packages.activatePackage("tree-view")

       runs ->
-        treeView = atom.workspaceView.find(".tree-view").view()
+        treeView = $(atom.workspace.getLeftPanels()[0].getItemView()).view()
         expect(treeView.list).toMatchSelector ':focus'

     it "restores the scroll top when toggled", ->
-      atom.workspaceView.height(5)
-      atom.workspaceView.attachToDom()
+      workspaceElement.style.height = '5px'
+      jasmine.attachToDOM(workspaceElement)
       expect(treeView).toBeVisible()
       treeView.focus()

       treeView.scrollTop(10)
       expect(treeView.scrollTop()).toBe(10)

-      runs -> atom.workspaceView.trigger 'tree-view:toggle'
+      runs -> atom.commands.dispatch(workspaceElement, 'tree-view:toggle')
       waitsFor -> treeView.is(':hidden')

-      runs -> atom.workspaceView.trigger 'tree-view:toggle'
+      runs -> atom.commands.dispatch(workspaceElement, 'tree-view:toggle')
       waitsFor -> treeView.is(':visible')

       runs -> expect(treeView.scrollTop()).toBe(10)

     it "restores the scroll left when toggled", ->
       treeView.width(5)
-      atom.workspaceView.attachToDom()
+      jasmine.attachToDOM(workspaceElement)
       expect(treeView).toBeVisible()
       treeView.focus()

       treeView.scroller.scrollLeft(5)
       expect(treeView.scroller.scrollLeft()).toBe(5)

-      runs -> atom.workspaceView.trigger 'tree-view:toggle'
+      runs -> atom.commands.dispatch(workspaceElement, 'tree-view:toggle')
       waitsFor -> treeView.is(':hidden')

-      runs -> atom.workspaceView.trigger 'tree-view:toggle'
+      runs -> atom.commands.dispatch(workspaceElement, 'tree-view:toggle')
       waitsFor -> treeView.is(':visible')

       runs -> expect(treeView.scroller.scrollLeft()).toBe(5)

   describe "when tree-view:toggle is triggered on the root view", ->
     beforeEach ->
-      atom.workspaceView.attachToDom()
+      jasmine.attachToDOM(workspaceElement)

     describe "when the tree view is visible", ->
       beforeEach ->
@@ -237,49 +238,46 @@
       describe "when the tree view is focused", ->
         it "hides the tree view", ->
           treeView.focus()
-          atom.workspaceView.trigger 'tree-view:toggle'
+          atom.commands.dispatch(workspaceElement, 'tree-view:toggle')
           expect(treeView).toBeHidden()

       describe "when the tree view is not focused", ->
         it "hides the tree view", ->
-          treeView.focus()
-          atom.workspaceView.trigger 'tree-view:toggle'
+          $(workspaceElement).focus()
+          atom.commands.dispatch(workspaceElement, 'tree-view:toggle')
           expect(treeView).toBeHidden()

     describe "when the tree view is hidden", ->
       it "shows and focuses the tree view", ->
         treeView.detach()
-        atom.workspaceView.trigger 'tree-view:toggle'
+        atom.commands.dispatch(workspaceElement, 'tree-view:toggle')
         expect(treeView.hasParent()).toBeTruthy()
         expect(treeView.list).toMatchSelector(':focus')

     describe "when tree-view:toggle-side is triggered on the root view", ->
-      beforeEach ->
-        atom.workspaceView.attachToDom()
-
       describe "when the tree view is on the left", ->
         it "should be moved to the right", ->
           expect(treeView).toBeVisible()
-          atom.workspaceView.trigger 'tree-view:toggle-side'
+          atom.commands.dispatch(workspaceElement, 'tree-view:toggle-side')
           expect(treeView).toMatchSelector('[data-show-on-right-side="true"]')

       describe "when the tree view is on the right", ->
         beforeEach ->
-          atom.workspaceView.trigger 'tree-view:toggle-side'
+          atom.commands.dispatch(workspaceElement, 'tree-view:toggle-side')

         it "should be moved to the left", ->
           expect(treeView).toBeVisible()
-          atom.workspaceView.trigger 'tree-view:toggle-side'
+          atom.commands.dispatch(workspaceElement, 'tree-view:toggle-side')
           expect(treeView).toMatchSelector('[data-show-on-right-side="false"]')

   describe "when tree-view:toggle-focus is triggered on the root view", ->
     beforeEach ->
-      atom.workspaceView.attachToDom()
+      jasmine.attachToDOM(workspaceElement)

     describe "when the tree view is hidden", ->
       it "shows and focuses the tree view", ->
         treeView.detach()
-        atom.workspaceView.trigger 'tree-view:toggle-focus'
+        atom.commands.dispatch(workspaceElement, 'tree-view:toggle-focus')
         expect(treeView.hasParent()).toBeTruthy()
         expect(treeView.list).toMatchSelector(':focus')

@@ -289,9 +287,9 @@
           atom.workspace.open() # When we call focus below, we want an editor to become focused

         runs ->
-          atom.workspaceView.focus()
+          $(workspaceElement).focus()
           expect(treeView).toBeVisible()
-          atom.workspaceView.trigger 'tree-view:toggle-focus'
+          atom.commands.dispatch(workspaceElement, 'tree-view:toggle-focus')
           expect(treeView).toBeVisible()
           expect(treeView.list).toMatchSelector(':focus')

@@ -303,7 +301,7 @@
           runs ->
             treeView.focus()
             expect(treeView).toBeVisible()
-            atom.workspaceView.trigger 'tree-view:toggle-focus'
+            atom.commands.dispatch(workspaceElement, 'tree-view:toggle-focus')
             expect(treeView).toBeVisible()
             expect(treeView.list).not.toMatchSelector(':focus')

@@ -318,7 +316,7 @@
           atom.workspace.open(path.join('dir1', 'file1'))

         runs ->
-          atom.workspaceView.trigger 'tree-view:reveal-active-file'
+          atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file')
           expect(treeView.hasParent()).toBeTruthy()
           expect(treeView.focus).toHaveBeenCalled()
           expect(treeView.selectedEntry().getPath()).toMatch new RegExp("dir1#{_.escapeRegExp(path.sep)}file1$")
@@ -330,14 +328,14 @@

         runs ->
           expect(atom.workspace.getActivePaneItem().getPath()).toBeUndefined()
-          atom.workspaceView.trigger 'tree-view:reveal-active-file'
+          atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file')
           expect(treeView.hasParent()).toBeTruthy()
           expect(treeView.focus).toHaveBeenCalled()

     describe "if there is no editor open", ->
       it "shows and focuses the tree view, but does not attempt to select a specific file", ->
         expect(atom.workspace.getActivePaneItem()).toBeUndefined()
-        atom.workspaceView.trigger 'tree-view:reveal-active-file'
+        atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file')
         expect(treeView.hasParent()).toBeTruthy()
         expect(treeView.focus).toHaveBeenCalled()

@@ -347,13 +345,13 @@
         atom.workspace.open() # When we trigger 'tool-panel:unfocus' below, we want an editor to become focused

       runs ->
-        atom.workspaceView.attachToDom()
+        jasmine.attachToDOM(workspaceElement)
         treeView.focus()
         expect(treeView.list).toMatchSelector(':focus')
         treeView.trigger 'tool-panel:unfocus'
         expect(treeView).toBeVisible()
         expect(treeView.list).not.toMatchSelector(':focus')
-        expect(atom.workspaceView.getActiveView().isFocused).toBeTruthy()
+        expect(atom.workspace.getActivePane().isActive()).toBe(true)

   describe "copy path commands", ->
     [pathToSelect, relativizedPath] = []
@@ -450,8 +448,11 @@
       expect(sampleJs).toHaveClass 'selected'

   describe "when a file is single-clicked", ->
+    beforeEach ->
+      jasmine.attachToDOM(workspaceElement)
+
     it "selects the files and opens it in the active editor, without changing focus", ->
-      expect(atom.workspaceView.getActiveView()).toBeUndefined()
+      treeView.focus()

       waitsForFileToOpen ->
         sampleJs.trigger clickEvent(originalEvent: { detail: 1 })
@@ -459,7 +460,7 @@
       runs ->
         expect(sampleJs).toHaveClass 'selected'
         expect(atom.workspace.getActivePaneItem().getPath()).toBe atom.project.resolve('tree-view.js')
-        expect(atom.workspaceView.getActiveView().isFocused).toBeFalsy()
+        expect(treeView.list).toHaveFocus()

       waitsForFileToOpen ->
         sampleTxt.trigger clickEvent(originalEvent: { detail: 1 })
@@ -468,25 +469,25 @@
         expect(sampleTxt).toHaveClass 'selected'
         expect(treeView.find('.selected').length).toBe 1
         expect(atom.workspace.getActivePaneItem().getPath()).toBe atom.project.resolve('tree-view.txt')
-        expect(atom.workspaceView.getActiveView().isFocused).toBeFalsy()
+        expect(treeView.list).toHaveFocus()

   describe "when a file is double-clicked", ->
     beforeEach ->
-      atom.workspaceView.attachToDom()
+      jasmine.attachToDOM(workspaceElement)

     it "selects the file and opens it in the active editor on the first click, then changes focus to the active editor on the second", ->
-      runs -> treeView.focus()
+      treeView.focus()

       waitsForFileToOpen ->
         sampleJs.trigger clickEvent(originalEvent: { detail: 1 })

       runs ->
         expect(sampleJs).toHaveClass 'selected'
-        expect(atom.workspace.getActivePaneItem().getPath()).toBe atom.project.resolve('tree-view.js')
-        expect(atom.workspaceView.getActiveView().isFocused).toBeFalsy()
+        item = atom.workspace.getActivePaneItem()
+        expect(item.getPath()).toBe atom.project.resolve('tree-view.js')

         sampleJs.trigger clickEvent(originalEvent: { detail: 2 })
-        expect(atom.workspaceView.getActiveView().isFocused).toBeTruthy()
+        expect(atom.views.getView(item)).toHaveFocus()

   describe "when a directory is single-clicked", ->
     it "is selected", ->
@@ -496,6 +497,9 @@

   describe "when a directory is double-clicked", ->
     it "toggles the directory expansion state and does not change the focus to the editor", ->
+      jasmine.attachToDOM(workspaceElement)
+      treeView.focus()
+
       subdir = null
       waitsForFileToOpen ->
         sampleJs.trigger clickEvent(originalEvent: { detail: 1 })
@@ -508,7 +512,7 @@
         subdir.trigger clickEvent(originalEvent: { detail: 2 })
         expect(subdir).toHaveClass 'selected'
         expect(subdir).not.toHaveClass 'expanded'
-        expect(atom.workspaceView.getActiveView().isFocused).toBeFalsy()
+        expect(treeView).toHaveFocus()

   describe "when an directory is alt-clicked", ->
     describe "when the directory is collapsed", ->
@@ -577,30 +581,29 @@
           sampleJs.click()

         runs ->
-          atom.workspaceView.getActivePaneView().activateItem($$ -> @div('hello'))
-          expect(atom.workspaceView.find('.selected')).not.toExist()
+          atom.workspace.getActivePane().activateItem($$ -> @div('hello'))
+          expect(treeView.find('.selected')).not.toExist()

   describe "when a different editor becomes active", ->
     beforeEach ->
-      atom.workspaceView.attachToDom()
+      jasmine.attachToDOM(workspaceElement)

     it "selects the file in that is open in that editor", ->
-      leftEditorView = null
-      rightEditorView = null
+      leftEditorPane = null

       waitsForFileToOpen ->
         sampleJs.click()

       runs ->
-        leftEditorView = atom.workspaceView.getActiveView()
-        rightEditorView = leftEditorView.splitRight()
+        leftEditorPane = atom.workspace.getActivePane()
+        leftEditorPane.splitRight()

       waitsForFileToOpen ->
         sampleTxt.click()

       runs ->
         expect(sampleTxt).toHaveClass('selected')
-        leftEditorView.focus()
+        leftEditorPane.activate()
         expect(sampleJs).toHaveClass('selected')

   describe "keyboard navigation", ->
@@ -908,7 +911,7 @@
     describe "tree-view:open-selected-entry", ->
       describe "when a file is selected", ->
         beforeEach ->
-          atom.workspaceView.attachToDom()
+          jasmine.attachToDOM(workspaceElement)

         it "opens the file in the editor and focuses it", ->
           waitsForFileToOpen ->
@@ -918,8 +921,9 @@
             root.trigger 'tree-view:open-selected-entry'

           runs ->
-            expect(atom.workspace.getActivePaneItem().getPath()).toBe atom.project.resolve('tree-view.js')
-            expect(atom.workspaceView.getActiveView().isFocused).toBeTruthy()
+            item = atom.workspace.getActivePaneItem()
+            expect(item.getPath()).toBe atom.project.resolve('tree-view.js')
+            expect(atom.views.getView(item)).toHaveFocus()

       describe "when a directory is selected", ->
         it "expands or collapses the directory", ->
@@ -936,7 +940,7 @@
       describe "when nothing is selected", ->
         it "does nothing", ->
           root.trigger 'tree-view:open-selected-entry'
-          expect(atom.workspaceView.getActiveView()).toBeUndefined()
+          expect(atom.workspace.getActivePaneItem()).toBeUndefined()

   describe "file modification", ->
     [dirView, fileView, dirView2, fileView2, fileView3, rootDirPath, dirPath, filePath, dirPath2, filePath2, filePath3] = []
@@ -966,8 +970,8 @@
         atom.packages.activatePackage('tree-view')

       runs ->
-        atom.workspaceView.trigger 'tree-view:toggle'
-        treeView = atom.workspaceView.find(".tree-view").view()
+        atom.commands.dispatch(workspaceElement, 'tree-view:toggle')
+        treeView = $(atom.workspace.getLeftPanels()[0].getItemView()).view()
         root = $(treeView.root)
         dirView = $(treeView.root.entries).find('.directory:contains(test-dir):first')
         dirView[0].expand()
@@ -980,7 +984,7 @@
     describe "tree-view:copy", ->
       LocalStorage = window.localStorage
       beforeEach ->
-        atom.workspaceView.attachToDom()
+        jasmine.attachToDOM(workspaceElement)
         LocalStorage.clear()

         waitsForFileToOpen ->
@@ -1012,7 +1016,7 @@
     describe "tree-view:cut", ->
       LocalStorage = window.localStorage
       beforeEach ->
-        atom.workspaceView.attachToDom()
+        jasmine.attachToDOM(workspaceElement)
         LocalStorage.clear()

         waitsForFileToOpen ->
@@ -1045,7 +1049,7 @@
     describe "tree-view:paste", ->
       LocalStorage = window.localStorage
       beforeEach ->
-        atom.workspaceView.attachToDom()
+        jasmine.attachToDOM(workspaceElement)
         LocalStorage.clear()

       describe "when attempting to paste a directory into itself", ->
@@ -1233,14 +1237,14 @@
       addDialog = null

       beforeEach ->
-        atom.workspaceView.attachToDom()
+        jasmine.attachToDOM(workspaceElement)

         waitsForFileToOpen ->
           fileView.click()

         runs ->
           treeView.trigger "tree-view:add-file"
-          addDialog = atom.workspaceView.find(".tree-view-dialog").view()
+          addDialog = $(atom.workspace.getModalPanels()[0].getItemView()).view()

       describe "when a file is selected", ->
         it "opens an add dialog with the file's current directory path populated", ->
@@ -1309,10 +1313,10 @@

         describe "when the add dialog's editor loses focus", ->
           it "removes the dialog and focuses root view", ->
-            atom.workspaceView.attachToDom()
-            atom.workspaceView.focus()
+            jasmine.attachToDOM(workspaceElement)
+            $(workspaceElement).focus()
             expect(addDialog.parent()).not.toExist()
-            expect(atom.workspaceView.getActiveView().isFocused).toBeTruthy()
+            expect(atom.views.getView(atom.workspace.getActivePane())).toHaveFocus()

         describe "when the path ends with whitespace", ->
           it "removes the trailing whitespace before creating the file", ->
@@ -1331,21 +1335,21 @@
           addDialog.cancel()
           dirView.click()
           treeView.trigger "tree-view:add-file"
-          addDialog = atom.workspaceView.find(".tree-view-dialog").view()
+          addDialog = $(atom.workspace.getModalPanels()[0].getItemView()).view()

           expect(addDialog).toExist()
           expect(addDialog.promptText.text()).toBeTruthy()
           expect(atom.project.relativize(dirPath)).toMatch(/[^\\\/]$/)
           expect(addDialog.miniEditor.getText()).toBe(atom.project.relativize(dirPath) + path.sep)
           expect(addDialog.miniEditor.getEditor().getCursorBufferPosition().column).toBe addDialog.miniEditor.getText().length
-          expect(addDialog.miniEditor.isFocused).toBeTruthy()
+          expect(addDialog.miniEditor).toHaveFocus()

       describe "when the root directory is selected", ->
         it "opens an add dialog with no path populated", ->
           addDialog.cancel()
           root.click()
           treeView.trigger "tree-view:add-file"
-          addDialog = atom.workspaceView.find(".tree-view-dialog").view()
+          addDialog = $(atom.workspace.getModalPanels()[0].getItemView()).view()

           expect(addDialog.miniEditor.getText().length).toBe 0

@@ -1356,7 +1360,7 @@
           root.removeClass('selected')
           expect(treeView.selectedEntry()).toBeNull()
           treeView.trigger "tree-view:add-file"
-          addDialog = atom.workspaceView.find(".tree-view-dialog").view()
+          addDialog = $(atom.workspace.getModalPanels()[0].getItemView()).view()

           expect(addDialog.miniEditor.getText().length).toBe 0

@@ -1364,14 +1368,14 @@
       addDialog = null

       beforeEach ->
-        atom.workspaceView.attachToDom()
+        jasmine.attachToDOM(workspaceElement)

         waitsForFileToOpen ->
           fileView.click()

         runs ->
           treeView.trigger "tree-view:add-folder"
-          addDialog = atom.workspaceView.find(".tree-view-dialog").view()
+          addDialog = $(atom.workspace.getModalPanels()[0].getItemView()).view()

       describe "when a file is selected", ->
         it "opens an add dialog with the file's current directory path populated", ->
@@ -1393,7 +1397,6 @@
               expect(addDialog.parent()).not.toExist()
               expect(atom.workspace.getActivePaneItem().getPath()).not.toBe newPath
               expect(treeView.find(".tree-view")).toMatchSelector(':focus')
-              expect(atom.workspaceView.getActiveView().isFocused).toBeFalsy()
               expect(dirView.find('.directory.selected:contains(new)').length).toBe 1

         describe "when the path with a trailing '#{path.sep}' is changed and confirmed", ->
@@ -1407,7 +1410,6 @@
               expect(addDialog.parent()).not.toExist()
               expect(atom.workspace.getActivePaneItem().getPath()).not.toBe newPath
               expect(treeView.find(".tree-view")).toMatchSelector(':focus')
-              expect(atom.workspaceView.getActiveView().isFocused).toBeFalsy()
               expect(dirView.find('.directory.selected:contains(new)').length).toBe(1)

             it "selects the created directory and does not change the expansion state of existing directories", ->
@@ -1426,7 +1428,6 @@
               expect(addDialog.parent()).not.toExist()
               expect(atom.workspace.getActivePaneItem().getPath()).not.toBe newPath
               expect(treeView.find(".tree-view")).toMatchSelector(':focus')
-              expect(atom.workspaceView.getActiveView().isFocused).toBeFalsy()
               expect(dirView.find('.directory.selected:contains(new2)').length).toBe(1)
               expect(treeView.entryForPath(expandedPath).isExpanded).toBeTruthy()

@@ -1446,14 +1447,14 @@
         moveDialog = null

         beforeEach ->
-          atom.workspaceView.attachToDom()
+          jasmine.attachToDOM(workspaceElement)

           waitsForFileToOpen ->
             fileView.click()

           runs ->
             treeView.trigger "tree-view:move"
-            moveDialog = atom.workspaceView.find(".tree-view-dialog").view()
+            moveDialog = $(atom.workspace.getModalPanels()[0].getItemView()).view()

         afterEach ->
           waits 50 # The move specs cause too many false positives because of their async nature, so wait a little bit before we cleanup
@@ -1523,10 +1524,10 @@

         describe "when the move dialog's editor loses focus", ->
           it "removes the dialog and focuses root view", ->
-            atom.workspaceView.attachToDom()
-            atom.workspaceView.focus()
+            jasmine.attachToDOM(workspaceElement)
+            $(workspaceElement).focus()
             expect(moveDialog.parent()).not.toExist()
-            expect(atom.workspaceView.getActiveView().isFocused).toBeTruthy()
+            expect(atom.views.getView(atom.workspace.getActivePane())).toHaveFocus()

       describe "when a file is selected that's name starts with a '.'", ->
         [dotFilePath, dotFileView, moveDialog] = []
@@ -1543,7 +1544,7 @@

           runs ->
             treeView.trigger "tree-view:move"
-            moveDialog = atom.workspaceView.find(".tree-view-dialog").view()
+            moveDialog = $(atom.workspace.getModalPanels()[0].getItemView()).view()

         it "selects the entire file name", ->
           expect(moveDialog).toExist()
@@ -1554,21 +1555,21 @@
         it "doesn't display the move dialog", ->
           treeView.root.click()
           treeView.trigger "tree-view:move"
-          expect(atom.workspaceView.find(".tree-view-dialog")).not.toExist()
+          expect(atom.workspace.getModalPanels().length).toBe(0)

     describe "tree-view:duplicate", ->
       describe "when a file is selected", ->
         copyDialog = null

         beforeEach ->
-          atom.workspaceView.attachToDom()
+          jasmine.attachToDOM(workspaceElement)

           waitsForFileToOpen ->
             fileView.click()

           runs ->
             treeView.trigger "tree-view:duplicate"
-            copyDialog = atom.workspaceView.find(".tree-view-dialog").view()
+            copyDialog = $(atom.workspace.getModalPanels()[0].getItemView()).view()

         afterEach ->
           waits 50 # The copy specs cause too many false positives because of their async nature, so wait a little bit before we cleanup
@@ -1643,10 +1644,10 @@

         describe "when the duplicate dialog's editor loses focus", ->
           it "removes the dialog and focuses root view", ->
-            atom.workspaceView.attachToDom()
-            atom.workspaceView.focus()
+            jasmine.attachToDOM(workspaceElement)
+            $(workspaceElement).focus()
             expect(copyDialog.parent()).not.toExist()
-            expect(atom.workspaceView.getActiveView().isFocused).toBeTruthy()
+            expect(atom.views.getView(atom.workspace.getActivePane())).toHaveFocus()

       describe "when a file is selected that's name starts with a '.'", ->
         [dotFilePath, dotFileView, copyDialog] = []
@@ -1663,7 +1664,7 @@

           runs ->
             treeView.trigger "tree-view:duplicate"
-            copyDialog = atom.workspaceView.find(".tree-view-dialog").view()
+            copyDialog = $(atom.workspace.getModalPanels()[0].getItemView()).view()

         it "selects the entire file name", ->
           expect(copyDialog).toExist()
@@ -1674,7 +1675,7 @@
         it "doesn't display the copy dialog", ->
           treeView.root.click()
           treeView.trigger "tree-view:duplicate"
-          expect(atom.workspaceView.find(".tree-view-dialog")).not.toExist()
+          expect(atom.workspace.getModalPanels().length).toBe(0)

       describe "when the editor has focus", ->
         copyDialog = null
@@ -1684,9 +1685,9 @@
             atom.workspace.open('tree-view.js')

           runs ->
-            editorView = atom.workspaceView.getActiveView()
-            editorView.trigger "tree-view:duplicate"
-            copyDialog = atom.workspaceView.find(".tree-view-dialog").view()
+            editorElement = atom.views.getView(atom.workspace.getActivePaneItem())
+            atom.commands.dispatch(editorElement, "tree-view:duplicate")
+            copyDialog = $(atom.workspace.getModalPanels()[0].getItemView()).view()

         it "duplicates the current file", ->
           expect(copyDialog.miniEditor.getText()).toBe('tree-view.js')
@@ -1694,7 +1695,7 @@
     describe "tree-view:remove", ->
       it "won't remove the root directory", ->
         spyOn(atom, 'confirm')
-        atom.workspaceView.attachToDom()
+        jasmine.attachToDOM(workspaceElement)
         treeView.show()
         treeView.root.click()
         treeView.trigger 'tree-view:remove'
@@ -1896,8 +1897,8 @@
         atom.packages.activatePackage('tree-view')

       runs ->
-        atom.workspaceView.trigger 'tree-view:toggle'
-        treeView = atom.workspaceView.find(".tree-view").view()
+        atom.commands.dispatch(workspaceElement, 'tree-view:toggle')
+        treeView = $(atom.workspace.getLeftPanels()[0].getItemView()).view()
         dirView = $(treeView.root.entries).find('.directory:contains(test-dir)')
         dirView[0].expand()
         fileView1 = treeView.find('.file:contains(test-file1.txt)')


Add failing spec

Add failing spec

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -2657,6 +2657,21 @@
         expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Opening folder in Finder failed'
         expect(atom.notifications.getNotifications()[0].getDetail()).toContain 'ENOENT'

+  describe "when reloading a directory with deletions and additions", ->
+    it "does not throw an error (regression)", ->
+      projectPath = temp.mkdirSync('atom-project')
+      entriesPath = path.join(projectPath, 'entries')
+
+      fs.mkdirSync(entriesPath)
+      atom.project.setPaths([projectPath])
+      treeView.roots[0].expand()
+
+      fs.removeSync(entriesPath)
+      treeView.roots[0].reload()
+
+      fs.mkdirSync(path.join(projectPath, 'other'))
+      treeView.roots[0].reload()
+
   describe "Dragging and dropping files", ->
     beforeEach ->
       rootDirPath = fs.absolute(temp.mkdirSync('tree-view'))


Add paid access end date to HoC approved survey mail

--- server/trial_requests/TrialRequest.coffee
+++ server/trial_requests/TrialRequest.coffee
@@ -42,10 +42,14 @@
     msg = "Trial Request submitted by #{doc.get('properties')?.email}"
     hipchat.sendHipChatMessage msg, ['tower']
   else if doc.get('status') is 'approved'
+    endDate = new Date()
+    endDate.setUTCMonth(endDate.getUTCMonth() + 2)
     emailParams =
       recipient:
         address: doc.get('properties')?.email
       email_id: sendwithus.templates.teacher_free_trial_hoc
+      email_data:
+        endDate: endDate
     sendwithus.api.send emailParams, (err, result) =>
       log.error "sendwithus trial request approved error: #{err}, result: #{result}" if err


We have to manually stringify/parse the JSON column for knex - fixes containers restarting every 6 minutes.

--- src/application.coffee
+++ src/application.coffee
@@ -64,9 +64,11 @@
 	.then ->
 		console.log("Creating container:", app.imageId)
 		ports = {}
-		if app.env.PORT?
-			maybePort = parseInt(app.env.PORT, 10)
-			if parseFloat(app.env.PORT) is maybePort and maybePort > 0 and maybePort < 65535
+		# Parse the env vars before trying to access them, that's because they have to be stringified for knex..
+		env = JSON.parse(app.env)
+		if env.PORT?
+			maybePort = parseInt(env.PORT, 10)
+			if parseFloat(env.PORT) is maybePort and maybePort > 0 and maybePort < 65535
 				port = maybePort
 		if port?
 			ports[port + '/tcp'] = {}
@@ -75,7 +77,7 @@
 			Cmd: ['/bin/bash', '-c', '/start']
 			Volumes:
 				'/dev': {}
-			Env: _.map app.env, (v, k) -> k + '=' + v
+			Env: _.map env, (v, k) -> k + '=' + v
 			ExposedPorts: ports
 		)
 		.then (container) ->
@@ -136,7 +138,7 @@
 						env[envVar.name] = envVar.value
 				return {
 					imageId: "#{process.env.REGISTRY_ENDPOINT}/#{path.basename(app.git_repository, '.git')}/#{app.commit}"
-					env: env
+					env: JSON.stringify(env) # The env has to be stored as a JSON string for knex
 				}

 			remoteApps = _.indexBy(remoteApps, 'imageId')


clean up and added more tests

clean up and added more tests

--- spec/lib/receiver-spec.coffee
+++ spec/lib/receiver-spec.coffee
@@ -1,30 +1,16 @@
 EventEmitter = require('events').EventEmitter
+Common = require 'bus.io-common'
+Message = Common.Message

 describe 'Receiver', ->

-  Given ->
-    @Router = class Router extends EventEmitter
-      constructor: ->
-        if not (@ instanceof Router)
-          return new Router
-      route: ->
-
-  Given ->
-    @Message = class Message
-      constructor: ->
-        if not (@ instanceof Message)
-          return new Message
-        @data =
-          actor: 'me'
-          action: 'say'
-          content: 'hello'
-          target: 'you'
-          created: new Date
-          reference: null
-          id: 1
-
-      clone: ->
-        return new Message
+  Given -> @Route = requireSubject 'lib/route', {
+    'bus.io-common': Common
+  }
+  Given -> @Router = requireSubject 'lib/router', {
+    'bus.io-common': Common
+    './route': @Route
+  }

   Given -> @handlers = [
     (message, socket, next) ->
@@ -41,67 +27,90 @@
       next()
   ]

-  Given -> @MessageReceiver = requireSubject 'lib/receiver', {
+  Given -> @Receiver = requireSubject 'lib/receiver', {
     './router': @Router
-    './bus.io-common':
-      Message: @Message
+    'bus.io-common': Common
   }

-  Given -> @instance = @MessageReceiver()
+  Then -> expect(typeof @Receiver).toBe 'function'

-  describe '#use (fns:Array)', ->
+  describe '#', ->

-    Given -> spyOn(@instance.router(),['on']).andCallThrough()
-    When -> @instance.use @handlers
-    Then -> expect(@instance.router().on).toHaveBeenCalled()
-    And -> expect(@instance.router().on.argsForCall[0]).toEqual ['*', @handlers[0]]
-    And -> expect(@instance.router().on.argsForCall[1]).toEqual ['*', @handlers[1]]
-    And -> expect(@instance.router().on.argsForCall[2]).toEqual ['*', @handlers[2]]
+    When -> @res = @Receiver()
+    Then -> expect(@res instanceof @Receiver).toBe true
+    Then -> expect(@res instanceof EventEmitter).toBe true

-  describe '#onReceive (message:Message)', ->
+  describe 'prototype', ->

-    Given -> @socket = new EventEmitter
-    Given -> spyOn(@socket,['emit']).andCallThrough()
-    Given -> @instance.use @handlers
-    Given -> @message = new @Message
-    Given -> @content = @message.data.content + 'ABC'
-    When (done)-> @instance.onReceive @message, @socket, done
-    And -> expect(@socket.emit).toHaveBeenCalledWith @message.data.action, @message.data.actor, @content, @message.data.target, @message.data.created
-
-  describe '#router', ->
+    Given -> @instance = @Receiver()

-    When -> @res = @instance.router()
-    Then -> expect(@res instanceof @Router).toBe true
-    And -> expect(@res.listeners('next')[0]).toBe @instance.onReceived
-    And -> expect(@res.listeners('deliver')[0]).toBe @instance.onReceived
-    And -> expect(@res.listeners('respond')[0]).toBe @instance.onReceived
-    And -> expect(@res.listeners('error')[0]).toBe @instance.onError
-    And -> expect(@res.listeners('consume')[0]).toBe @instance.onConsumed
+    describe '#use (fns:Array)', ->

-  describe '#onReceived', ->
+      Given -> spyOn(@instance.router(),['on']).andCallThrough()
+      When -> @instance.use @handlers
+      Then -> expect(@instance.router().on).toHaveBeenCalled()
+      And -> expect(@instance.router().on.argsForCall[0]).toEqual ['*', @handlers[0]]
+      And -> expect(@instance.router().on.argsForCall[1]).toEqual ['*', @handlers[1]]
+      And -> expect(@instance.router().on.argsForCall[2]).toEqual ['*', @handlers[2]]

-    Given -> @message = @Message()
-    Given -> @socket = new EventEmitter
-    Given -> spyOn(EventEmitter.prototype.emit,['apply']).andCallThrough()
-    When -> @instance.onReceived @message, @socket
-    Then -> expect(EventEmitter.prototype.emit.apply).toHaveBeenCalledWith @instance, ['received', @message, @socket]
+    describe '#onReceive (message:Message, socket:Scoket)', ->

-  describe '#onConsumed', ->
+      Given -> @socket = new EventEmitter
+      Given -> spyOn(@socket,['emit']).andCallThrough()
+      Given -> @instance.use @handlers
+      Given -> @message = Message()
+      Given -> @content = @message.data.content + 'ABC'
+      When (done)-> @instance.onReceive @message, @socket, done
+      And -> expect(@socket.emit).toHaveBeenCalledWith @message.data.action, @message.data.actor, @content, @message.data.target, @message.data.created
+
+    describe '#router', ->

-    Given -> @message = @Message()
-    Given -> @socket = new EventEmitter
-    Given -> spyOn(EventEmitter.prototype.emit,['apply']).andCallThrough()
-    When -> @instance.onConsumed @message, @socket
-    Then -> expect(EventEmitter.prototype.emit.apply).toHaveBeenCalledWith @instance, ['consumed', @message, @socket]
+      When -> @res = @instance.router()
+      Then -> expect(@res instanceof @Router).toBe true
+      And -> expect(@res.listeners('next')[0]).toBe @instance.onReceived
+      And -> expect(@res.listeners('deliver')[0]).toBe @instance.onReceived
+      And -> expect(@res.listeners('respond')[0]).toBe @instance.onReceived
+      And -> expect(@res.listeners('error')[0]).toBe @instance.onError
+      And -> expect(@res.listeners('consume')[0]).toBe @instance.onConsumed
+
+    describe '#onReceived (message:Message, socket:Socket)', ->
+
+      Given -> @message = Message()
+      Given -> @socket = new EventEmitter
+      Given -> spyOn(EventEmitter.prototype.emit,['apply']).andCallThrough()
+      When -> @instance.onReceived @message, @socket
+      Then -> expect(EventEmitter.prototype.emit.apply).toHaveBeenCalledWith @instance, ['received', @message, @socket]
+
+    describe '#onReceived (message:Message)', ->
+
+      Given -> @message = Message()
+      Given -> spyOn(EventEmitter.prototype.emit,['apply']).andCallThrough()
+      When -> @instance.onReceived @message
+      Then -> expect(EventEmitter.prototype.emit.apply).toHaveBeenCalledWith @instance, ['received', @message]
+
+    describe '#onConsumed (message:Message, socket:Socket)', ->
+
+      Given -> @message = Message()
+      Given -> @socket = new EventEmitter
+      Given -> spyOn(EventEmitter.prototype.emit,['apply']).andCallThrough()
+      When -> @instance.onConsumed @message, @socket
+      Then -> expect(EventEmitter.prototype.emit.apply).toHaveBeenCalledWith @instance, ['consumed', @message, @socket]
+
+    describe '#onConsumed (message:Message)', ->
+
+      Given -> @message = Message()
+      Given -> spyOn(EventEmitter.prototype.emit,['apply']).andCallThrough()
+      When -> @instance.onConsumed @message
+      Then -> expect(EventEmitter.prototype.emit.apply).toHaveBeenCalledWith @instance, ['consumed', @message]

-  describe '#onError', ->
+    describe '#onError', ->

-    Given -> @error = 'test'
-    Given -> @message = @Message()
-    Given -> @socket = new EventEmitter
-    Given -> spyOn(EventEmitter.prototype.emit,['apply'])
-    When -> @instance.onError @error, @message, @socket
-    Then -> expect(EventEmitter.prototype.emit.apply).toHaveBeenCalledWith @instance, ['error', @error, @message, @socket]
+      Given -> @error = 'test'
+      Given -> @message = Message()
+      Given -> @socket = new EventEmitter
+      Given -> spyOn(EventEmitter.prototype.emit,['apply'])
+      When -> @instance.onError @error, @message, @socket
+      Then -> expect(EventEmitter.prototype.emit.apply).toHaveBeenCalledWith @instance, ['error', @error, @message, @socket]

-
+


spec(spellchecker): handle some weird Linux cases

Linux returns ['asked', 'acidify', 'Assad'] for the string "asdfk"
which seems normal to me. Why do OS X and/or Windows return [] for
that string? That seems weird, so I just broke the spec to run different
specs on different platforms.

--- spec/spellchecker-spec.coffee
+++ spec/spellchecker-spec.coffee
@@ -87,7 +87,12 @@

     it "provides options for misspelled words", ->
       expect(@spellchecker.getCorrectionsForMisspelling("")).toEqual []
-      expect(@spellchecker.getCorrectionsForMisspelling("asdfk")).toEqual []
+      # TODO: Determine the reason OS X and/or Windows return an empty array
+      # for the string. -mbilker
+      if process.platform is 'linux'
+        expect(@spellchecker.getCorrectionsForMisspelling("asdfk")).toEqual ['asked', 'acidify', 'Assad']
+      else
+        expect(@spellchecker.getCorrectionsForMisspelling("asdfk")).toEqual []

     it "still provides options for correctly spelled workds", ->
       expect(@spellchecker.getCorrectionsForMisspelling("hello").length).toBeGreaterThan 1


Fix spec name

Fix spec name

--- spec/supremo/init_spec.coffee
+++ spec/supremo/init_spec.coffee
@@ -44,7 +44,7 @@
       window.App = Supremo.init(data)
       window.App.Views.Closured().should.equal(422)

-    it "works without string instead of hash", ->
+    it "works with string instead of hash", ->
       data =
         Answer: "../support/simple.js"
         Complex: "../support/complex.js"


Tests for catching component errors in asCallback

--- spec/AsCallback.coffee
+++ spec/AsCallback.coffee
@@ -34,6 +34,16 @@
     c.process (input, output) ->
       data = input.getData 'in'
       output.done new Error "Received #{data}"
+  processValues = ->
+    c = new noflo.Component
+    c.inPorts.add 'in',
+      datatype: 'string'
+      values: ['green', 'blue']
+    c.outPorts.add 'out',
+      datatype: 'string'
+    c.process (input, output) ->
+      data = input.getData 'in'
+      output.sendDone data
   neverSend = ->
     c = new noflo.Component
     c.inPorts.add 'in',
@@ -64,6 +74,7 @@
       return done err if err
       loader.registerComponent 'process', 'Async', processAsync
       loader.registerComponent 'process', 'Error', processError
+      loader.registerComponent 'process', 'Values', processValues
       loader.registerComponent 'process', 'NeverSend', neverSend
       loader.registerComponent 'process', 'Streamify', streamify
       done()
@@ -188,11 +199,44 @@
         chai.expect(err).to.be.an 'error'
         chai.expect(err.message).to.contain expected
         done()
+  describe 'with a component supporting only certain values', ->
+    wrapped = null
+    before ->
+      wrapped = noflo.asCallback 'process/Values',
+        loader: loader
+    it 'should execute network with input map and provide output map', (done) ->
+      expected ='blue'
+      wrapped
+        in: expected
+      , (err, out) ->
+        return done err if err
+        chai.expect(out.out).to.eql expected
+        done()
+    it 'should execute network with simple input and provide simple output', (done) ->
+      expected = 'blue'
+      wrapped expected, (err, out) ->
+        return done err if err
+        chai.expect(out).to.eql expected
+        done()
+    it 'should execute network with wrong map and provide error', (done) ->
+      expected = 'red'
+      wrapped
+        in: 'red'
+      , (err) ->
+        chai.expect(err).to.be.an 'error'
+        chai.expect(err.message).to.contain 'Invalid data=\'red\' received, not in [green,blue]'
+        done()
+    it 'should execute network with wrong input and provide error', (done) ->
+      wrapped 'red', (err) ->
+        chai.expect(err).to.be.an 'error'
+        chai.expect(err.message).to.contain 'Invalid data=\'red\' received, not in [green,blue]'
+        done()
   describe 'with a component sending streams', ->
     wrapped = null
     before ->
       wrapped = noflo.asCallback 'process/Streamify',
         loader: loader
+      return
     it 'should execute network with input map and provide output map with streams as arrays', (done) ->
       wrapped
         in: 'hello world'
@@ -202,6 +246,7 @@
           ['w','o','r','l','d']
         ]
         done()
+      return
     it 'should execute network with simple input and and provide simple output with streams as arrays', (done) ->
       wrapped 'hello there', (err, out) ->
         chai.expect(out).to.eql [
@@ -209,6 +254,7 @@
           ['t','h','e','r','e']
         ]
         done()
+      return
     describe 'with the raw option', ->
       it 'should execute network with input map and provide output map with IP objects', (done) ->
         wrappedRaw = noflo.asCallback 'process/Streamify',
@@ -267,3 +313,44 @@
           ['t','h','e','r','e']
         ]
         done()
+  describe 'with a graph containing a component supporting only certain values', ->
+    graph = null
+    wrapped = null
+    before (done) ->
+      noflo.graph.loadFBP """
+      INPORT=Async.IN:IN
+      OUTPORT=Values.OUT:OUT
+      Async(process/Async) OUT -> IN Values(process/Values)
+      """, (err, g) ->
+        return done err if err
+        graph = g
+        wrapped = noflo.asCallback graph,
+          loader: loader
+        done()
+    it 'should execute network with input map and provide output map', (done) ->
+      expected ='blue'
+      wrapped
+        in: expected
+      , (err, out) ->
+        return done err if err
+        chai.expect(out.out).to.eql expected
+        done()
+    it 'should execute network with simple input and provide simple output', (done) ->
+      expected = 'blue'
+      wrapped expected, (err, out) ->
+        return done err if err
+        chai.expect(out).to.eql expected
+        done()
+    it 'should execute network with wrong map and provide error', (done) ->
+      expected = 'red'
+      wrapped
+        in: 'red'
+      , (err) ->
+        chai.expect(err).to.be.an 'error'
+        chai.expect(err.message).to.contain 'Invalid data=\'red\' received, not in [green,blue]'
+        done()
+    it 'should execute network with wrong input and provide error', (done) ->
+      wrapped 'red', (err) ->
+        chai.expect(err).to.be.an 'error'
+        chai.expect(err.message).to.contain 'Invalid data=\'red\' received, not in [green,blue]'
+        done()


Improve specs by using latex::getBuilder method

By swapping out the real `latexmk` builder with a fake builder, we've
drastically improved spec timings. The actual `latexmk` builder spec still uses
the real implementation to ensure everything works end-to-end.

--- spec/latex-spec.coffee
+++ spec/latex-spec.coffee
@@ -18,6 +18,18 @@
   prependRight: (view) ->
     @rightPanel.append(view)

+fakeBuilder =
+  executable: ""  # TODO: Remove this from builder signature.
+
+  run: (args, callback) ->
+    callback(0)
+
+  constructArgs: (filePath) ->
+    return []
+
+  constructPath: ->
+    return ""
+
 describe "Latex", ->
   beforeEach ->
     tempPath = fs.realpathSync(temp.mkdirSync("atom-latex"))
@@ -34,6 +46,8 @@
     atom.config.set("latex.texPath", "$PATH:/usr/texbin")
     atom.config.set("latex.outputDirectory", "output")
     atom.config.set("latex.enableShellEscape", false)
+
+    spyOn(latex, "getBuilder").andReturn(fakeBuilder)

   describe "build", ->
     it "does nothing for new, unsaved files", ->


tab className attribute becomes tabClassName

--- src/components/course-settings/roster.cjsx
+++ src/components/course-settings/roster.cjsx
@@ -62,7 +62,7 @@
           {period.name}
         </BS.OverlayTrigger>

-      <BS.Tab key={period.id} eventKey={index} title={name} className={className}>
+      <BS.Tab key={period.id} eventKey={index} title={name} tabClassName={className}>
         <PeriodRoster
         period={period}
         courseId={@props.courseId}


tests: Check for error

tests: Check for error

--- spec/ComponentLoader.coffee
+++ spec/ComponentLoader.coffee
@@ -25,10 +25,7 @@
             ComponentLoader loader, (err) ->
                 chai.expect(err).to.equal null
                 loader.listComponents (err, components) ->
-                    if not components
-                        # NoFlo <0.6 compat
-                        components = err
-                        err = null
+                    chai.expect(err).to.not.exist
                     names = Object.keys(components).filter (c) -> return c.indexOf('runtime/') == 0
                     chai.expect(names).to.have.length 1
                     chai.expect(names[0]).to.equal compName


Remove unneeded spyOn calls

Remove unneeded spyOn calls

--- spec/app/editor-spec.coffee
+++ spec/app/editor-spec.coffee
@@ -1396,7 +1396,6 @@
     describe "when lines are added", ->
       beforeEach ->
         editor.attachToDom(heightInLines: 5)
-        spyOn(editor, "scrollToPixelPosition")

       describe "when the change precedes the first rendered row", ->
         it "inserts and removes rendered lines to account for upstream change", ->
@@ -1448,7 +1447,6 @@
     describe "when lines are removed", ->
       beforeEach ->
         editor.attachToDom(heightInLines: 5)
-        spyOn(editor, "scrollToPixelPosition")

       it "sets the rendered screen line's width to either the max line length or the scollView's width (whichever is greater)", ->
         maxLineLength = editor.maxScreenLineLength()
@@ -1614,7 +1612,6 @@

     describe "when lines are inserted", ->
       it "re-renders the correct line number range in the gutter", ->
-        spyOn(editor, 'scrollToPixelPosition')
         editor.scrollTop(3 * editor.lineHeight)
         expect(editor.gutter.find('.line-number:first').text()).toBe '2'
         expect(editor.gutter.find('.line-number:last').text()).toBe '11'


Enable all performance specs.

Enable all performance specs.

--- spec/performanceSpec.coffee
+++ spec/performanceSpec.coffee
@@ -1,11 +1,11 @@
 describe "Transparency performance", ->

-  xdescribe "with cached templates", ->
+  describe "with cached templates", ->

     describe "with one todo item", ->

       it "should be on the same ballpark with Handlebars", ->
-        transparency = new Benchmark 'transparency - cached tempate, one todo',
+        transparency = new Benchmark 'transparency - cached template, one todo',
           setup: ->
             template = $('

')[0]
             index    = 0
@@ -40,7 +40,7 @@
     describe "with hundred todo items", ->

       it "should be on the same ballpark with Handlebars", ->
-        transparency = new Benchmark 'transparency - cached tempate, 100 todos',
+        transparency = new Benchmark 'transparency - cached template, 100 todos',
           setup: ->
             template = $('

')[0]
             index    = 0
@@ -78,7 +78,7 @@

   describe "on first render call", ->

-    xdescribe "with one todo item", ->
+    describe "with one todo item", ->

       it "should be on the same ballpark with Handlebars", ->
         transparency = new Benchmark 'transparency - unused template, one todo',


:white_check_mark: Add tests for complete redraw

--- spec/table-view-spec.coffee
+++ spec/table-view-spec.coffee
@@ -111,3 +111,22 @@
       expect(rows.length).toEqual(18)
       expect(rows.first().data('row-id')).toEqual(1)
       expect(rows.last().data('row-id')).toEqual(18)
+
+  describe 'when scrolled by 300px', ->
+    beforeEach ->
+      tableView.scrollTop(300)
+      nextAnimationFrame()
+
+    describe '::getFirstVisibleRow', ->
+      it 'returns 15', ->
+        expect(tableView.getFirstVisibleRow()).toEqual(15)
+
+    describe '::getLastVisibleRow', ->
+      it 'returns 23', ->
+        expect(tableView.getLastVisibleRow()).toEqual(23)
+
+    it 'renders new rows', ->
+      rows = tableView.find('.table-edit-row')
+      expect(rows.length).toEqual(28)
+      expect(rows.first().data('row-id')).toEqual(6)
+      expect(rows.last().data('row-id')).toEqual(33)


Fixed __and__ for set.coffee to do inplace or on the intersect set, and to look at the objects(not hashes) returned from the underlying dictionary's keys()

--- src/builtin/set.coffee
+++ src/builtin/set.coffee
@@ -8,7 +8,7 @@

   __and__: (otherSet) ->
     intersect = new Set()
-    intersect.__or__(item) for item of @value.value when otherSet.__contains__(item).value
+    intersect.__ior__(item) for item of @value.keys().value when otherSet.__contains__(item).value
     return intersect

   __cmp__: (otherSet) ->


Use third package view instead of first

Use third package view instead of first

--- spec/app/package-config-panel-spec.coffee
+++ spec/app/package-config-panel-spec.coffee
@@ -88,5 +88,5 @@
       expect(p2View.issues.find('a').attr('href')).toBe 'http://github.com/atom/p2/issues'

       p3View.dropdownButton.click()
-      expect(p1View.homepage).toBeHidden()
-      expect(p1View.issues).toBeHidden()
+      expect(p3View.homepage).toBeHidden()
+      expect(p3View.issues).toBeHidden()


[grunt] sort path properties

[grunt] sort path properties

--- gruntfile.coffee
+++ gruntfile.coffee
@@ -34,11 +34,11 @@
         fonts: '<%= path.source.root %>/fonts'
         icons: '<%= path.source.root %>/icons'
         images: '<%= path.source.root %>/images'
-        styles: '<%= path.source.root %>/styles'
         locales: '<%= path.source.root %>/locales'
         scripts: '<%= path.source.root %>/scripts'
         sprites: '<%= path.source.root %>/sprites'
         static: '<%= path.source.root %>/static'
+        styles: '<%= path.source.root %>/styles'
         templates: '<%= path.source.root %>/templates'

       temp:
@@ -50,9 +50,9 @@
         assets: '<%= path.build.root %>/assets'
         fonts: '<%= path.build.assets %>/fonts'
         images: '<%= path.build.assets %>/images'
-        styles: '<%= path.build.assets %>/styles'
         scripts: '<%= path.build.assets %>/scripts'
         sprites: '<%= path.build.assets %>/sprites'
+        styles: '<%= path.build.assets %>/styles'
         thumbnails: '<%= path.build.images %>/thumbnails'

     # Specify files
@@ -92,8 +92,8 @@
         # Remove `build/` part from path
         fonts: '<%= grunt.template.process(path.build.fonts).replace(path.build.root + \'/\', \'\') %>'
         images: '<%= grunt.template.process(path.build.images).replace(path.build.root + \'/\', \'\') %>'
+        scripts: '<%= grunt.template.process(path.build.scripts).replace(path.build.root + \'/\', \'\') %>'
         styles: '<%= grunt.template.process(path.build.styles).replace(path.build.root + \'/\', \'\') %>'
-        scripts: '<%= grunt.template.process(path.build.scripts).replace(path.build.root + \'/\', \'\') %>'
         thumbnails: '<%= grunt.template.process(path.build.thumbnails).replace(path.build.root + \'/\', \'\') %>'
         source: '<%= path.source %>'
       site:


Added method for sanitizing before inserting and updating. Refactored structure of Collections into headings.

--- src/Collections.coffee
+++ src/Collections.coffee
@@ -1,6 +1,10 @@
 global = @

 Collections =
+
+####################################################################################################
+# COLLECTIONS
+####################################################################################################

   allow: -> true
   allowAll: -> insert: @allow, update: @allow, remove: @allow
@@ -50,7 +54,7 @@

   # @param {String|Meteor.Collection|Cursor} arg
   # @returns {Meteor.Collection|Cursor} Either a Meteor collection, a cursor, or null if none is
-  # found.
+  #     found.
   resolve: (arg) ->
     if Types.isString(arg)
       # Collection name.
@@ -84,6 +88,31 @@
       return arg.fetch()
     return []

+  # @param {Array.<Meteor.Collection>} collections
+  # @returns {Object.<String, Meteor.Collection>} A map of collection name to object for the given
+  #      collections.
+  getMap: (collections) ->
+    collectionMap = {}
+    _.each collections, (collection) =>
+      name = @getName(collection)
+      collectionMap[name] = collection
+    collectionMap
+
+  # @param {Object.<String, String>} map - A map of IDs to names of the items.
+  # @returns A temporary collection with items created from the given map.
+  fromNameMap: (map, args) ->
+    args = _.extend({
+    }, args)
+    collection = Collections.createTemporary()
+    callback = args.callback
+    _.each map, (item, id) ->
+      if callback
+        name = callback(item, id)
+      else
+        name = item
+      collection.insert(_id: id, name: name)
+    collection
+
   createTemporary: (docs) ->
     collection = new Meteor.Collection(null)
     @insertAll(docs, collection)
@@ -91,31 +120,18 @@

   isTemporary: (collection) -> !@getName(collection)

+  # @returns {String} Generates a MongoDB ObjectID hex string.
+  generateId: -> new Mongo.ObjectID().toHexString()
+
   insertAll: (docs, collection) ->
     _.each docs, (doc) -> collection.insert(doc)

-  moveDoc: (id, sourceCollection, destCollection) ->
-    order = sourceCollection.findOne(id)
-    unless order
-      throw new Error('Could not find doc with id ' + id + ' in collection ' + sourceCollection)
-    destCollection.insert order, (err, result) ->
-      if err
-        throw new Error('Failed to insert into destination collection when moving')
-      else
-        sourceCollection.remove id, (err2, result2) ->
-          if err2
-            throw new Error('Failed to remove from source collection when moving')
-
-  duplicateDoc: (docOrId, collection) ->
-    df = Q.defer()
-    doc = if Types.isObject(docOrId) then docOrId else collection.findOne(docOrId)
-    delete doc._id
-    collection.insert doc, (err, result) -> if err then df.reject(err) else df.resolve(result)
-    df.promise
-
-  removeAllDocs: (collection) ->
-    _.each collection.find().fetch(), (order) ->
-      collection.remove(order._id)
+  intersection: (a, b) ->
+    result = @createTemporary()
+    a.find().forEach (item) ->
+      if b.findOne(item._id)
+        result.insert(item)
+    result

   # @param {Meteor.Collection|Cursor|String} collection
   # @param {Object|Function} args - If given as a function, it takes precendence as the callback
@@ -209,33 +225,32 @@
             delete idMap[id]
     Q.all(insertPromises).then -> dest

-  # @param {Array.<Meteor.Collection>} collections
-  # @returns {Object.<String, Meteor.Collection>} A map of collection name to object for the given
-  # collections.
-  getMap: (collections) ->
-    collectionMap = {}
-    _.each collections, (collection) =>
-      name = @getName(collection)
-      collectionMap[name] = collection
-    collectionMap
-
-  # @param {Object.<String, String>} map - A map of IDs to names of the items.
-  # @returns A temporary collection with items created from the given map.
-  fromNameMap: (map, args) ->
-    args = _.extend({
-    }, args)
-    collection = Collections.createTemporary()
-    callback = args.callback
-    _.each map, (item, id) ->
-      if callback
-        name = callback(item, id)
+####################################################################################################
+# DOCS
+####################################################################################################
+
+  moveDoc: (id, sourceCollection, destCollection) ->
+    order = sourceCollection.findOne(id)
+    unless order
+      throw new Error('Could not find doc with id ' + id + ' in collection ' + sourceCollection)
+    destCollection.insert order, (err, result) ->
+      if err
+        throw new Error('Failed to insert into destination collection when moving')
       else
-        name = item
-      collection.insert(_id: id, name: name)
-    collection
-
-  # @returns {String} Generates a MongoDB ObjectID hex string.
-  generateId: -> new Mongo.ObjectID().toHexString()
+        sourceCollection.remove id, (err2, result2) ->
+          if err2
+            throw new Error('Failed to remove from source collection when moving')
+
+  duplicateDoc: (docOrId, collection) ->
+    df = Q.defer()
+    doc = if Types.isObject(docOrId) then docOrId else collection.findOne(docOrId)
+    delete doc._id
+    collection.insert doc, (err, result) -> if err then df.reject(err) else df.resolve(result)
+    df.promise
+
+  removeAllDocs: (collection) ->
+    _.each collection.find().fetch(), (order) ->
+      collection.remove(order._id)

   # @param {Object} doc
   # @param {Object} modifier - A MongoDB modifier object.
@@ -248,10 +263,14 @@
     tmpCollection.update(insertedId, modifier)
     tmpCollection.findOne(insertedId)

+####################################################################################################
+# VALIDATION
+####################################################################################################
+
   # @param {Meteor.Collection} collection
   # @param {Function} validate - A validation method which returns a string on failure or throws
-  # an exception, which causes validation to fail and prevents insert() or update() on the collection
-  # from completing.
+  #      an exception, which causes validation to fail and prevents insert() or update() on the
+  #      collection from completing.
   addValidation: (collection, validate) ->
     collection.before.insert (userId, doc, options) =>
       return if options?.validate == false
@@ -271,9 +290,33 @@
     else
       handle(result)

-  intersection: (a, b) ->
-    result = @createTemporary()
-    a.find().forEach (item) ->
-      if b.findOne(item._id)
-        result.insert(item)
-    result
+####################################################################################################
+# SANITIZATION
+####################################################################################################
+
+  # @param {Meteor.Collection} collection
+  # @param {Function} sanitize - A sanitization method which is passed a document before insertions
+  #      or updates occur for the given collection. If a MongoDB modifier is returned, it is applied
+  #      to the document before the operation takes place, allowing for any final changes.
+  addSanitization: (collection, sanitize) ->
+    collection.before.insert (userId, doc) =>
+      context = {userId: userId}
+      modifier = sanitize.call(context, doc)
+      if modifier
+        # TODO(aramk) Apply the change directly on the doc for better performance.
+        updatedDoc = @simulateModifierUpdate(doc, modifier)
+        Setter.merge(doc, updatedDoc)
+
+    collection.before.update (userId, doc, fieldNames, modifier) ->
+      updatedDoc = Collections.simulateModifierUpdate(doc, modifier)
+      context = {userId: userId}
+      # sanitizedDoc = Setter.clone(updatedDoc)
+      sanitizeModifier = sanitize.call(context, updatedDoc)
+      return unless sanitizeModifier
+      # docDiff = Objects.diff(updatedDoc, sanitizedDoc)
+      Setter.merge(modifier, sanitizeModifier)
+      # Ensure no fields exist in $unset from $set.
+      $unset = modifier.$unset
+      if $unset
+        _.each $set, (value, key) ->
+          delete $unset[key]


FIX: error in achievement box plot name

FIX: error in achievement box plot name

--- src/app/visualisations/partials/directives/achievement-box-plot.coffee
+++ src/app/visualisations/partials/directives/achievement-box-plot.coffee
@@ -59,7 +59,7 @@
         enabled: $scope.showLegend
       maxBoxWidth: 75
       yDomain: [0, 1]
-    }, {}, Achievement Box Plot
+    }, {}, 'Achievement Box Plot'

     # $scope.$watch 'rawData', refreshData($scope.rawData)
     $scope.$watch 'pctHolder.pct', (newData, oldData) -> if newData != oldData then refreshData($scope.rawData)


Remove some TODO that were done

Remove some TODO that were done

--- admin/static/coffee/log_view.coffee
+++ admin/static/coffee/log_view.coffee
@@ -426,7 +426,6 @@
                                             database_id: database_id
                                             database_id_trunked: database_id.slice 24

-                    #TODO logs for databases
                     else
                         msg += "We were unable to parse this log. Click on 'More details' to see the raw log"
                 return {
@@ -546,8 +545,6 @@

                     else
                         msg += "We were unable to parse this log. Click on 'More details' to see the raw log"
-
-                        #TODO logs for databases
                 return {
                     msg: msg
                     raw_data: JSON.stringify $.parseJSON(data), undefined, 2


Add apple feature flag test

Add apple feature flag test

--- src/desktop/apps/user/components/linked_accounts/test/view.test.coffee
+++ src/desktop/apps/user/components/linked_accounts/test/view.test.coffee
@@ -5,6 +5,9 @@
 { fabricate } = require '@artsy/antigravity'
 CurrentUser = require '../../../../../models/current_user'
 LinkedAccountsView = benv.requireWithJadeify require.resolve('../view'), ['template']
+LinkedAccountsView.__set__ 'sd', AP:
+  applePath: '/users/auth/apple'
+  facebookPath: '/users/auth/facebook'

 describe "LinkedAccountsView", ->
   before (done) ->
@@ -21,10 +24,23 @@

     @user = new CurrentUser fabricate 'user'
     @view = new LinkedAccountsView user: @user
+
     @view.render()

   afterEach ->
     Backbone.sync.restore()
+
+  describe 'apple feature flag', ->
+    it 'does not render apple by default', ->
+      @view.$('#apple-svg-icon').length.should.eql 0
+
+    it 'renders apple when feature flag is enabled', ->
+      sd = LinkedAccountsView.__get__ 'sd'
+      LinkedAccountsView.__set__ 'sd', extend {}, sd,
+        ENABLE_SIGN_IN_WITH_APPLE: true
+      enabled_view = new LinkedAccountsView user: @user
+      enabled_view.render()
+      enabled_view.$('#apple-svg-icon').length.should.eql 1

   describe '#toggleService', ->
     for provider in ['apple', 'facebook']


Also guard against repeated GB ID Fusion Tables queries (see #19)

--- js/book-aligner.coffee
+++ js/book-aligner.coffee
@@ -266,12 +266,14 @@
   gb_query(gb_id)

 gb_query = (gb_id, level = 0) ->
-  fusion_tables_query "SELECT ia_identifier FROM #{IA_GB_TABLE_ID} WHERE gb_identifier = #{fusion_tables_escape(gb_id)}",
-    (data) ->
-      if data.rows?
-        process_ia_id(row[0],(1 - level)*100) for row in data.rows.reverse()
-        if level == 0
-          ia_query(row[0],1) for row in data.rows.reverse()
+  if gb_id not in QUERIED_IDS['gb']
+    QUERIED_IDS['gb'].push gb_id
+    fusion_tables_query "SELECT ia_identifier FROM #{IA_GB_TABLE_ID} WHERE gb_identifier = #{fusion_tables_escape(gb_id)}",
+      (data) ->
+        if data.rows?
+          process_ia_id(row[0],(1 - level)*100) for row in data.rows.reverse()
+          if level == 0
+            ia_query(row[0],1) for row in data.rows.reverse()

 gb_url = (gb_id) ->
   "https://books.google.com/books?id=#{gb_id}"
@@ -286,6 +288,7 @@
     'oclc': [],
     'lccn': [],
     'ht': [],
+    'gb': [],
     'ia': []
   }


organize

organize

--- src/Bacon.JQuery.Bindings.coffee
+++ src/Bacon.JQuery.Bindings.coffee
@@ -12,6 +12,7 @@
   idCounter = 1

   Model = Bacon.Model = Bacon.$.Model = (initValue) ->
+    myModCount = 0
     modificationBus = new Bacon.Bus()
     syncBus = new Bacon.Bus()
     valueWithSource = modificationBus.scan(
@@ -19,7 +20,6 @@
       ({value}, {source, f}) -> {source, value: f(value)}
     ).changes().merge(syncBus).toProperty()
     model = valueWithSource.map(".value").skipDuplicates()
-    myModCount = 0
     model.id = idCounter++
     model.addSyncSource = (syncEvents) ->
       syncBus.plug(syncEvents.filter((e) ->


Changing default keystroke for Go to Definition to alt-down

--- keymaps/atom-elixir.cson
+++ keymaps/atom-elixir.cson
@@ -14,7 +14,7 @@
   'enter': 'autocomplete-plus:confirm'

 'atom-text-editor:not(mini)[data-grammar^="source elixir"]':
-  'f3': 'atom-elixir:goto-declaration'
+  'alt-down': 'atom-elixir:goto-declaration'
   'f2': 'atom-elixir:show-elixir-docs'
   'ctrl-shift-t': 'atom-elixir:quote-selected-text'
   'ctrl-shift-x': 'atom-elixir:expand-selected-text'


Fix result ordering

Fix result ordering


allow to not change history after a form submission

--- lib/assets/javascripts/up/form.js.coffee
+++ lib/assets/javascripts/up/form.js.coffee
@@ -78,7 +78,9 @@

     successUrl = (xhr) ->
       url = if historyOption
-        if u.isString(historyOption)
+        if historyOption == 'false'
+          false
+        else if u.isString(historyOption)
           historyOption
         else if currentLocation = u.locationFromXhr(xhr)
           currentLocation


Empty expression interpolations evaluate as empty strings now.

--- lexer.coffee
+++ lexer.coffee
@@ -349,12 +349,15 @@
           tokens.push ['IDENTIFIER', interp]
           i += group.length - 1
           pi: i + 1
-        else if (expr: @balanced_string str.substring(i), ['${', '}']) and expr.length > 3
+        else if (expr: @balanced_string str.substring(i), ['${', '}'])
+          tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i
           inner: expr.substring(2, expr.length - 1)
-          nested: lexer.tokenize "($inner)", {rewrite: no, line: @line}
-          nested.pop()
-          tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i
-          tokens.push ['TOKENS', nested]
+          if inner.length
+            nested: lexer.tokenize "($inner)", {rewrite: no, line: @line}
+            nested.pop()
+            tokens.push ['TOKENS', nested]
+          else
+            tokens.push ['STRING', "$quote$quote"]
           i += expr.length - 1
           pi: i + 1
         i += 1


Escape newlines in heredocs with backslashes

--- lexer.coffee
+++ lexer.coffee
@@ -691,18 +691,20 @@

   # Converts newlines for string literals.
   escapeLines: (str, heredoc) ->
+    # Ignore escaped backslashes and remove escaped newlines
+    str = str.replace /\\[^\S\n]*(\n|\\)\s*/g, (escaped, character) ->
+      if character is '\n' then '' else escaped
     if heredoc
       str.replace MULTILINER, '\\n'
     else
-      str.replace(/((^|[^\\])(\\\\)+)\n/g, '$1 \\\n') # escaped backslashes
-         .replace(/\\\s*\n\s*/g, '') # backslash at EOL
-         .replace(/\s*\n\s*/g, ' ')
+      str.replace /\s*\n\s*/g, ' '

   # Constructs a string token by escaping quotes and newlines.
   makeString: (body, quote, heredoc) ->
     return quote + quote unless body
-    body = body.replace /\\([\s\S])/g, (match, contents) ->
-      if contents is quote or heredoc and contents is '\n' then contents else match
+    # Ignore escaped backslashes and unescape quotes
+    body = body.replace /// \\( #{quote} | \\ ) ///g, (match, contents) ->
+      if contents is quote then contents else match
     body = body.replace /// #{quote} ///g, '\\$&'
     quote + @escapeLines(body, heredoc) + quote


lexer: improved consistency, preferring `charAt`, `slice` and single quotes

--- lexer.coffee
+++ lexer.coffee
@@ -105,7 +105,7 @@
   # Be careful not to interfere with ranges-in-progress.
   numberToken: ->
     return false unless number = @match NUMBER
-    return false if @tag() is '.' and starts number, '.'
+    return false if @tag() is '.' and number.charAt(0) is '.'
     @i += number.length
     @token 'NUMBER', number
     true
@@ -113,12 +113,12 @@
   # Matches strings, including multi-line strings. Ensures that quotation marks
   # are balanced within the string's contents, and within nested interpolations.
   stringToken: ->
-    return false unless starts(@chunk, '"') or starts(@chunk, "'")
+    return false unless @chunk.charAt(0) in ["'", '"']
     return false unless string =
       @balancedToken(['"', '"'], ['#{', '}']) or
       @balancedToken ["'", "'"]
     @interpolateString string.replace MULTILINER, '\\\n'
-    @line += count string, "\n"
+    @line += count string, '\n'
     @i += string.length
     true

@@ -148,7 +148,7 @@

   # Matches JavaScript interpolated directly into the source via backticks.
   jsToken: ->
-    return false unless starts @chunk, '`'
+    return false unless @chunk.charAt(0) is '`'
     return false unless script = @balancedToken ['`', '`']
     @token 'JS', script.slice 1, -1
     @i += script.length
@@ -251,13 +251,13 @@

   # Generate a newline token. Consecutive newlines get merged together.
   newlineToken: (newlines) ->
-    @token 'TERMINATOR', "\n" unless @tag() is 'TERMINATOR'
+    @token 'TERMINATOR', '\n' unless @tag() is 'TERMINATOR'
     true

   # Use a `\` at a line-ending to suppress the newline.
   # The slash is removed here once its job is done.
   suppressNewlines: ->
-    @tokens.pop() if @value() is "\\"
+    @tokens.pop() if @value() is '\\'
     true

   # We treat all other single characters as a token. Eg.: `( ) , . !`
@@ -327,10 +327,10 @@
         attempt = if match[1]? then match[1] else match[2]
         indent = attempt if not indent? or 0 < attempt.length < indent.length
     indent or= ''
-    doc = doc.replace(new RegExp("^" + indent, 'gm'), '')
+    doc = doc.replace(new RegExp('^' + indent, 'gm'), '')
     return doc if options.herecomment
-    doc = doc.replace(/^\n/, '')
-    doc.replace(MULTILINER, "\\n")
+    doc.replace(/^\n/, '')
+       .replace(MULTILINER, '\\n')
        .replace(new RegExp(options.quote, 'g'), "\\#{options.quote}")

   # A source of ambiguity in our grammar used to be parameter lists in function
@@ -372,8 +372,9 @@
     slash = delimited[0][0] is '/'
     levels = []
     i = 0
-    while i < str.length
-      if levels.length and starts str, '\\', i
+    slen = str.length
+    while i < slen
+      if levels.length and str.charAt(i) is '\\'
         i += 1
       else
         for pair in delimited
@@ -387,12 +388,12 @@
             levels.push(pair)
             i += open.length - 1
             break
-      break if not levels.length or slash and starts str, '\n', i
+      break if not levels.length or slash and str.charAt(i) is '\n'
       i += 1
     if levels.length
       return false if slash
       throw new Error "SyntaxError: Unterminated #{levels.pop()[0]} starting on line #{@line + 1}"
-    if not i then false else str.substring(0, i)
+    if not i then false else str[0...i]

   # Expand variables and expressions inside double-quoted strings using
   # [ECMA Harmony's interpolation syntax](http://wiki.ecmascript.org/doku.php?id=strawman:string_interpolation)
@@ -405,19 +406,20 @@
   # token stream.
   interpolateString: (str, options) ->
     options or= {}
-    if str.length < 3 or not starts str, '"'
+    if str.length < 3 or str.charAt(0) isnt '"'
       @token 'STRING', str
     else
       lexer   = new Lexer
       tokens  = []
-      quote   = str.substring 0, 1
+      quote   = str.charAt 0
       [i, pi] = [1, 1]
-      while i < str.length - 1
-        if starts str, '\\', i
+      end = str.length - 1
+      while i < end
+        if str.charAt(i) is '\\'
           i += 1
-        else if expr = @balancedString(str.substring(i), [['#{', '}']])
-          tokens.push ['STRING', quote + str.substring(pi, i) + quote] if pi < i
-          inner = expr.substring(2, expr.length - 1)
+        else if expr = @balancedString str[i..], [['#{', '}']]
+          tokens.push ['STRING', quote + str[pi...i] + quote] if pi < i
+          inner = expr.slice 2, -1
           if inner.length
             inner = inner.replace new RegExp('\\\\' + quote, 'g'), quote if options.heredoc
             nested = lexer.tokenize "(#{inner})", line: @line
@@ -429,7 +431,7 @@
           i += expr.length - 1
           pi = i + 1
         i += 1
-      tokens.push ['STRING', quote + str.substring(pi, i) + quote] if pi < i and pi < str.length - 1
+      tokens.push ['STRING', quote + str[pi...i] + quote] if i > pi < str.length - 1
       tokens.unshift ['STRING', '""'] unless tokens[0][0] is 'STRING'
       interpolated = tokens.length > 1
       @token '(', '(' if interpolated
@@ -438,7 +440,7 @@
         if tag is 'TOKENS'
           @tokens = @tokens.concat value
         else if tag is 'STRING' and options.escapeQuotes
-          escaped = value.substring(1, value.length - 1).replace(/"/g, '\\"')
+          escaped = value.slice(1, -1).replace(/"/g, '\\"')
           @token tag, "\"#{escaped}\""
         else
           @token tag, value
@@ -472,48 +474,48 @@
   # Attempt to match a string against the current chunk, returning the indexed
   # match if successful, and `false` otherwise.
   match: (regex, index) ->
-    return false unless m = @chunk.match regex
-    if m then m[index] else false
+    if m = @chunk.match regex then m[index or 0] else false

   # Are we in the midst of an unfinished expression?
   unfinished: ->
-    prev = @prev(2)
-    @value() and @value().match and @value().match(NO_NEWLINE) and
-      prev and (prev[0] isnt '.') and not @value().match(CODE) and
-      not @chunk.match ASSIGNED
+    prev  = @prev 2
+    value = @value()
+    value and NO_NEWLINE.test(value) and
+      prev and prev[0] isnt '.' and not CODE.test(value) and
+      not ASSIGNED.test(@chunk)

 # Constants
 # ---------

 # Keywords that CoffeeScript shares in common with JavaScript.
 JS_KEYWORDS = [
-  "if", "else",
-  "true", "false",
-  "new", "return",
-  "try", "catch", "finally", "throw",
-  "break", "continue",
-  "for", "in", "while",
-  "delete", "instanceof", "typeof",
-  "switch", "super", "extends", "class",
-  "this", "null", "debugger"
+  'if', 'else'
+  'true', 'false'
+  'new', 'return'
+  'try', 'catch', 'finally', 'throw'
+  'break', 'continue'
+  'for', 'in', 'while'
+  'delete', 'instanceof', 'typeof'
+  'switch', 'super', 'extends', 'class'
+  'this', 'null', 'debugger'
 ]

 # CoffeeScript-only keywords, which we're more relaxed about allowing. They can't
 # be used standalone, but you can reference them as an attached property.
-COFFEE_ALIASES =  ["and", "or", "is", "isnt", "not"]
+COFFEE_ALIASES =  ['and', 'or', 'is', 'isnt', 'not']
 COFFEE_KEYWORDS = COFFEE_ALIASES.concat [
-  "then", "unless", "until", "loop",
-  "yes", "no", "on", "off",
-  "of", "by", "where", "when"
+  'then', 'unless', 'until', 'loop'
+  'yes', 'no', 'on', 'off'
+  'of', 'by', 'where', 'when'
 ]

 # The list of keywords that are reserved by JavaScript, but not used, or are
 # used by CoffeeScript internally. We throw an error when these are encountered,
 # to avoid having a JavaScript error at runtime.
 RESERVED = [
-  "case", "default", "do", "function", "var", "void", "with",
-  "const", "let", "enum", "export", "import", "native",
-  "__hasProp", "__extends", "__slice"
+  'case', 'default', 'do', 'function', 'var', 'void', 'with'
+  'const', 'let', 'enum', 'export', 'import', 'native'
+  '__hasProp', '__extends', '__slice'
 ]

 # The superset of both JavaScript keywords and reserved words, none of which may


Missing parentheses

Missing parentheses

--- lexer.coffee
+++ lexer.coffee
@@ -196,7 +196,7 @@
         if 0 < string.indexOf '#{', 1
           @interpolateString string[1...-1], strOffset: 1, lexedLength: string.length
         else
-          @token 'STRING', @escapeLines string, 0, string.length
+          @token 'STRING', @escapeLines(string), 0, string.length
       else
         return 0
     if octalEsc = /^(?:\\.|[^\\])*\\(?:0[0-7]|[1-7])/.test string


Fix scopes and tokenisation of 4e2cf9e

Fix scopes and tokenisation of 4e2cf9e

--- grammars/viml.cson
+++ grammars/viml.cson
@@ -46,19 +46,19 @@
 limitLineLength: no
 patterns: [{
 	# VimBall archive
-	name:  "meta.file-archive.vimball.viml"
+	name:  "meta.file-archive.vimball"
 	begin: '\\A(?=" Vimball Archiver)'
 	end:   "(?=A)B"
 	patterns: [{
 		# Help file
-		name:  "meta.file-record.$1.vimball.viml"
-		begin: "^(.*?\\S.*?\\.txt)(\\t)((\\[){3}1)(?=$)"
+		name:  "meta.file-record.help-file.vimball"
+		begin: "^(.*?\\S.*?\\.txt)(\\t)(\\[{3}1)(?=$)"
 		end:   "(?!\\G)(?=^.*?\\S.*?\\t\\[{3}1$)"
 		beginCaptures:
-			0: name: "markup.heading.1.viml"
-			1: name: "entity.name.file.path.viml"
-			2: name: "punctuation.whitespace.tab.separator.viml"
-			3: name: "meta.separator.marker.viml"
+			0: name: "markup.heading.1.vimball"
+			1: name: "entity.name.file.path.vimball"
+			2: name: "punctuation.whitespace.tab.separator.vimball"
+			3: name: "punctuation.definition.header.vimball"
 		contentName: "text.embedded.vim-help"
 		patterns: [{
 			# Line count
@@ -70,14 +70,14 @@
 		}, include: "text.vim-help"]
 	},{
 		# Vim script file
-		name:  "meta.file-record.$1.vimball.viml"
-		begin: "^(.*?\\S.*?[ \\t]*?)(\\t)((\\[){3}1)(?=$)"
+		name:  "meta.file-record.vimball.viml"
+		begin: "^(.*?\\S.*?[ \\t]*?)(\\t)(\\[{3}1)(?=$)"
 		end:   "(?!\\G)(?=^.*?\\S.*?\\t\\[{3}1$)"
 		beginCaptures:
-			0: name: "markup.heading.1.viml"
-			1: name: "entity.name.file.viml"
-			2: name: "punctuation.whitespace.tab.separator.viml"
-			3: name: "meta.separator.marker.viml"
+			0: name: "markup.heading.1.vimball"
+			1: name: "entity.name.file.path.vimball"
+			2: name: "punctuation.whitespace.tab.separator.vimball"
+			3: name: "punctuation.definition.header.vimball"
 		patterns: [{
 			# Line count
 			begin: "\\G"


client: log when cpu limit reached error happens

--- client/Terminal/AppView.coffee
+++ client/Terminal/AppView.coffee
@@ -407,11 +407,13 @@
     console.error err

     try
-      err   = JSON.parse err.message
-      title = err.message  if err.message
+      err         = JSON.parse err.message
+      title       = err.message  if err.message
+      numberOfVms = Object.keys(KD.singletons.vmController.vmsInfo).length

     if /CPU limit reached/.test title
       title = "You've exceeded the allowed number of concurrent vms. Please upgrade."
+      KD.remote.api.JErrorLog.create { error : "cpu_limit_reached", numberOfVms }, ->
     else
       title = "Your vm failed to start. Please try again later."


Avoid highlighting "is" or "isnot" inside words


Fix #2944

Fix #2944

--- lexer.coffee
+++ lexer.coffee
@@ -846,11 +846,11 @@
 # See: http://www.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions
 #
 # Our list is shorter, due to sans-parentheses method calls.
-NOT_REGEX = ['NUMBER', 'REGEX', 'BOOL', 'NULL', 'UNDEFINED', '++', '--', ']']
+NOT_REGEX = ['NUMBER', 'REGEX', 'BOOL', 'NULL', 'UNDEFINED', '++', '--']

 # If the previous token is not spaced, there are more preceding tokens that
 # force a division parse:
-NOT_SPACED_REGEX = NOT_REGEX.concat ')', '}', 'THIS', 'IDENTIFIER', 'STRING'
+NOT_SPACED_REGEX = NOT_REGEX.concat ')', '}', 'THIS', 'IDENTIFIER', 'STRING', ']'

 # Tokens which could legitimately be invoked or indexed. An opening
 # parentheses or bracket following these tokens will be recorded as the start


one step at a time

one step at a time

--- lexer.coffee
+++ lexer.coffee
@@ -94,13 +94,13 @@
   identifier_token: ->
     return false unless id: @match IDENTIFIER, 1
     @name_access_type()
-    space: @prev() and @prev().spaced
+    accessed: include ACCESSORS, @tag(0)
     tag: 'IDENTIFIER'
-    tag: id.toUpperCase()     if include(KEYWORDS, id) and not (include(ACCESSORS, @tag(0)) and not space)
+    tag: id.toUpperCase()     if not accessed and include(KEYWORDS, id)
     @identifier_error id      if include RESERVED, id
     tag: 'LEADING_WHEN'       if tag is 'WHEN' and include LINE_BREAK, @tag()
     @i += id.length
-    tag: id: CONVERSIONS[id]  if space and include(COFFEE_ALIASES, id)
+    tag: id: CONVERSIONS[id]  if not accessed and include(COFFEE_ALIASES, id)
     @token(tag, id)
     true


Better way to do method binding in coffee script

--- app/assets/javascripts/c4.js.coffee
+++ app/assets/javascripts/c4.js.coffee
@@ -7,9 +7,9 @@
     @context = @canvas[0].getContext '2d'
     @overlayContext = @overlayCanvas[0].getContext '2d'
     @reset()
-    @overlayCanvas.click $.proxy(this.clicked, this)
-    @overlayCanvas.mousemove $.proxy(this.mouseMove, this)
-    @overlayCanvas.mouseleave $.proxy(this.mouseLeave, this)
+    @overlayCanvas.click @clicked
+    @overlayCanvas.mousemove @mouseMove
+    @overlayCanvas.mouseleave @mouseLeave
     @drawFrame()
     this.initialiseGameState() if @options.grid && @options.moves

@@ -28,10 +28,10 @@
   activate: (active) ->
     @active = active

-  clicked: (e) ->
+  clicked: (e) =>
     @move Math.floor(e.offsetX/@options.cellSize) if @active

-  mouseMove: (e) ->
+  mouseMove: (e) =>
     if @active
       highlightColumn = Math.floor e.offsetX/@options.cellSize
       if highlightColumn != @highlightColumn
@@ -54,7 +54,7 @@
         ((@options.gridHeight - @grid[@highlightColumn].length) * @options.cellSize) - (@options.cellSize/2),
         (@options.cellSize * 4/10) + 1)

-  mouseLeave: (e) ->
+  mouseLeave: (e) =>
     @highlight(-1)

   drawFrame: ->
@@ -108,7 +108,7 @@
         clearInterval(interval)
     , delay)

-  start: (counter, column, colour) ->
+  start: (counter, column, colour) =>
     @context.globalCompositeOperation = 'copy'
     @context.fillStyle = 'rgba(0,0,0,0)'
     this.fillEllipse(((column + 1) * @options.cellSize) - (@options.cellSize/2),
@@ -129,6 +129,6 @@
     @grid[column].push colour
     @moves.push [colour, column]
     @options.afterMove(@grid) if @options.afterMove
-    this.animateMove($.proxy(this.start, this), @options.cellSize/4, (7 - countersInColumn) * 4, column, colour)
+    this.animateMove(@start, @options.cellSize/4, (7 - countersInColumn) * 4, column, colour)


add reference

add reference

--- app/assets/javascripts/calls.coffee
+++ app/assets/javascripts/calls.coffee
@@ -1,6 +1,9 @@
 # Place all the behaviors and hooks related to the matching controller here.
 # All this logic will automatically be available in application.js.
 # You can use CoffeeScript in this file: http://coffeescript.org/
+
+# http://stackoverflow.com/questions/1307705/jquery-ui-sortable-with-table-and-tr-width/1372954#1372954
+# http://benw.me/posts/sortable-bootstrap-tables/

 jQuery ->


hide/show the locations forms according to parent location selection

--- app/assets/javascripts/forms.coffee
+++ app/assets/javascripts/forms.coffee
@@ -124,6 +124,19 @@
   $(document).on 'change', 'input.localization_main', (event) ->
     $('input.localization_main').not(this).prop('checked', false)

+  disableLocations = (el) ->
+    if el.selectedIndex == 0
+      $('.localization-form').each ->
+        $(this).show()
+        return
+      $('.add_location').show()
+    else
+      $('.localization-form').each ->
+        $(this).hide()
+        return
+      $('.add_location').hide()
+    return
+
   addChosen = ->
     # enable chosen js
     $('.chosen-select').chosen
@@ -135,6 +148,10 @@
   $(document).on 'ready', () ->
     addChosen()

+    if $('.parent-location').length
+      parentSelect = $('.parent-location')[0]
+      disableLocations parentSelect
+
   $('form').submit ->
     $('#items_list .table-container').html('
Loading...
')
     return
@@ -142,3 +159,6 @@
   $('.tags-select').select2
     tags: true
     tokenSeparators: [',']
+
+  $(document).on 'change', '.parent-location', (event) ->
+    disableLocations this


-  Add stdout

-  Add stdout

--- kites/sharedHosting/executecommand.coffee
+++ kites/sharedHosting/executecommand.coffee
@@ -79,7 +79,7 @@
           cmd = exec "chown #{username} #{tmpFile};su -l #{username} -c 'sh #{tmpFile}'",(err,stdout,stderr)->
             if err?
               log.error "[ERROR] can't execute command \"#{command}\" for user #{username}: #{stderr}"
-              callback? stderr
+              callback? stderr,stdout
             else
               log.info "[OK] command \"#{command}\" executed for user #{username}"
               callback? null,stdout


[✔] WebGL is broken in Safari (desktop and mobile), fall back to Canvas for the moment.

--- vendor/assets/javascripts/jquery/plugins/shadify/shady.js.coffee
+++ vendor/assets/javascripts/jquery/plugins/shadify/shady.js.coffee
@@ -320,10 +320,11 @@
     return

   findRenderer = ->
-    ua     = detect.parse navigator.userAgent
-    device = ua.device
-
-    if Modernizr.webgl and device.family isnt 'iPhone'
+    ua      = detect.parse navigator.userAgent
+    browser = ua.browser
+    safari  = browser.name.indexOf('Safari') > -1
+
+    if Modernizr.webgl and not safari
       Shady.RENDERER.WEBGL
     else if Modernizr.canvas
       Shady.RENDERER.CANVAS


Remove check for clientID and role name

Remove check for clientID and role name

This commit removes a check for same role name and clientID. This check introduced a bug
and hence the need to removes it.

--- src/api/clients.coffee
+++ src/api/clients.coffee
@@ -103,12 +103,6 @@
   # Ignore _id if it exists, a user shouldn't be able to update the internal id
   delete clientData._id if clientData._id

-  if clientData.clientID
-    chResult = yield Channel.find({allow: {$in: [clientData.clientID]}}, {name: 1 }).exec()
-    clResult = yield Client.find({roles: {$in: [clientData.clientID]}}, {clientID: 1 }).exec()
-    if chResult?.length > 0 or clResult?.length > 0
-      return utils.logAndSetResponse this, 409, "A role name conflicts with clientID '#{clientData.clientID}'. A role name cannot be the same as a clientID.", 'info'
-
   try
     yield Client.findByIdAndUpdate(clientId, clientData).exec()
     logger.info "User #{this.authenticated.email} updated client with id #{clientId}"


Enclose publish queuing vars in their own closure

--- src/application.coffee
+++ src/application.coffee
@@ -23,20 +23,20 @@

 # Queue up any calls to publish while we wait for the uuid to return from
 # the sqlite db
-publishQueue = []
-
-publish = ->
-	publishQueue.push(arguments)
-
-knex('config').select('value').where(key: 'uuid').then ([uuid]) ->
-	uuid = uuid.value
-	channel = "device-#{uuid}-logs"
-
-	publish = (message) ->
-		pubnub.publish({channel, message})
-
-	# Replay queue now that we have initialised the publish function
-	publish.apply(null, args) for args in publishQueue
+publish = do ->
+	publishQueue = []
+
+	knex('config').select('value').where(key: 'uuid').then ([uuid]) ->
+		uuid = uuid.value
+		channel = "device-#{uuid}-logs"
+
+		publish = (message) ->
+			pubnub.publish({channel, message})
+
+		# Replay queue now that we have initialised the publish function
+		publish(args...) for args in publishQueue
+
+	return -> publishQueue.push(arguments)

 exports.kill = kill = (app) ->
 	docker.listContainersAsync(all: 1)


Updated currentUserService mocks for CreateProjectFormCtrl tests

--- app/modules/projects/create/create-project-form/create-project-form.controller.spec.coffee
+++ app/modules/projects/create/create-project-form/create-project-form.controller.spec.coffee
@@ -32,7 +32,8 @@
     _mockCurrentUserService = ->
         mocks.currentUserService = {
             canCreatePublicProjects: sinon.stub().returns({valid: true}),
-            canCreatePrivateProjects: sinon.stub().returns({valid: true})
+            canCreatePrivateProjects: sinon.stub().returns({valid: true}),
+            loadProjects: sinon.stub()
         }

         $provide.value("tgCurrentUserService", mocks.currentUserService)


Abstract building the query string

Abstract building the query string

This was repeated in a few places

--- js/isvalid.coffee
+++ js/isvalid.coffee
@@ -156,7 +156,7 @@

 		percents

-	permalink = "http://#{window.location.host}?" + $.param( getPermalinkQuery query )
+	permalink = "http://#{window.location.host}?" + getQueryString query

 	# Control
 	results.push $.extend
@@ -196,7 +196,7 @@
 		$(this).hide().delay( 300 ).html( html ).fadeIn options.speed

 pushState = ( query ) ->
-	url = '?' + $.param( getPermalinkQuery query )
+	url = '?' + getQueryString query
 	history.pushState query, '', url if $.support.history

 isEmbed = ->
@@ -205,9 +205,12 @@
 isNewQuery = ( queryString ) ->
 	return queryString != window.location.search

+getQueryString = ( query ) ->
+	$.param( getPermalinkQuery query )
+
 getResults = ( query, options = {} ) ->
 	lastQuery = window.location.search
-	newQuery  = '?' + $.param( getPermalinkQuery query )
+	newQuery  = '?' + getQueryString query

 	# Maintain state
 	pushState query if ! isEmbed() && ( isNewQuery( newQuery ) || ! history.state )


JMachine::one: fetcher for machines

JMachine::one: fetcher for machines

--- workers/social/lib/social/models/computeproviders/machine.coffee
+++ workers/social/lib/social/models/computeproviders/machine.coffee
@@ -1,8 +1,14 @@
-{ Module } = require 'jraphical'
+{ Module }     = require 'jraphical'
+{ revive }     = require './computeutils'

 module.exports = class JMachine extends Module

-  { ObjectId } = require 'bongo'
+  { ObjectId, signature } = require 'bongo'
+
+  @trait __dirname, '../../traits/protected'
+  {permit} = require '../group/permissionset'
+
+  @share()

   @set

@@ -12,6 +18,14 @@
     sharedEvents        :
       static            : [ ]
       instance          : [ ]
+
+    sharedMethods       :
+      static            :
+        one             :
+          (signature String, Function)
+
+    permissions         :
+      'list machines'   : ['member']

     schema              :

@@ -40,3 +54,23 @@
         default         : "not-initialized"

       meta              : Object
+
+
+  @one$: permit 'list machines',
+
+    success: revive
+
+      shouldReviveClient   : yes
+      shouldReviveProvider : no
+
+    , (client, machineId, callback)->
+
+      { r: { group, user } } = client
+
+      selector =
+        _id      : machineId
+        users    : $elemMatch: id: user.getId()
+        groups   : $elemMatch: id: group.getId()
+
+      JMachine.one selector, (err, machine)->
+        callback err, machine


use is -1

use is -1

--- src/Commander.coffee
+++ src/Commander.coffee
@@ -179,7 +179,7 @@
       for varid in removes
         if subqueries = _subqueries[varid]
           for subquery in subqueries
-            if removes.indexOf(subquery) == -1
+            if removes.indexOf(subquery) is -1
               removes.push subquery
           delete _subqueries[varid]
     _subtrackers = @selectorKeysByTracker
@@ -189,7 +189,7 @@
       if _subtrackers
         if subtrackers = _subtrackers[varid]
           for subtracked in subtrackers
-            if removes.indexOf(subtracked) == -1
+            if removes.indexOf(subtracked) is -1
               removes.push subtracked
           delete _subtrackers[varid]
     @engine.registerCommand ['remove', removes...]


Fix figure links

Fix figure links

--- src/scripts/modules/media/body/body.coffee
+++ src/scripts/modules/media/body/body.coffee
@@ -111,7 +111,7 @@
           $temp.find('figcaption').each (i, el) ->
             $(el).parent().append(el)

-          # Convert figure and table links to show the proper name
+          # Convert figure, table, exercise, problem and note links to show the proper name
           $temp.find('a:not([data-type=footnote-number])').each (i, el) ->
             $el = $(el)
             href = $el.attr('href')
@@ -119,7 +119,12 @@
             if href.substr(0, 1) is '#' and href.length > 1 and $el.data('type') isnt 'footnote-ref'
               try
                 $target = $temp.find(href)
-                tag = $target?.attr('data-type')?.toLowerCase()
+
+                tag = $target?.prop('tagName')?.toLowerCase()
+
+                if $target?.attr('data-type') isnt undefined
+                  tag = $target?.attr('data-type')?.toLowerCase()
+
                 if $el.text() is '[link]' and tag
                   tag = tag.charAt(0).toUpperCase() + tag.substring(1)
                   $el.text("#{tag}") if tag isnt 'undefined'


JStackTemplate: clone: clone all custom credentials along the stack template itself

--- workers/social/lib/social/models/computeproviders/stacktemplate.coffee
+++ workers/social/lib/social/models/computeproviders/stacktemplate.coffee
@@ -1,6 +1,6 @@
-{ ObjectId, signature }  = require 'bongo'
-{ Module, Relationship } = require 'jraphical'
-KodingError              = require '../../error'
+{ ObjectId, signature, daisy }  = require 'bongo'
+{ Module, Relationship }        = require 'jraphical'
+KodingError                     = require '../../error'

 module.exports = class JStackTemplate extends Module
@@ -245,6 +245,30 @@
       @update { $set: data }, (err) => callback err, this

+  cloneCustomCredentials = (client, credentials, callback) ->
+
+    JCredential = require './credential'
+    clonedCreds = []
+    queue       = []
+
+    credentials.forEach (identifier) -> queue.push ->
+      JCredential.fetchByIdentifier client, identifier, (err, credential) ->
+        if not err and credential
+          credential.clone client, (err, cloneCredential) ->
+            if err
+              console.warn "Clone failed:", err
+            else
+              clonedCreds.push cloneCredential.identifier
+            queue.next()
+        else
+          queue.next()
+
+    queue.push ->
+      callback null, clonedCreds
+
+    daisy queue
+
+
   clone: permit

     advanced: [
@@ -269,11 +293,16 @@
       cloneData.config           ?= {}
       cloneData.config.clonedFrom = @getId()

-      # TODO make sure to clone custom credentials as well ~ GG
-
-      JStackTemplate.create client, cloneData, (err, cloneStackTemplate) ->
-        if err then callback new KodingError 'Clone failed', err
-        else callback null, cloneStackTemplate
+      { custom } = cloneData.credentials or {}
+      custom    ?= []
+
+      if custom.length > 0
+        cloneCustomCredentials client, custom, (err, creds) ->
+          return callback new KodingError 'Failed to clone credentials'  if err
+          cloneData.credentials.custom = creds
+          JStackTemplate.create client, cloneData, callback
+      else
+        JStackTemplate.create client, cloneData, callback

 # Base StackTemplate example for koding group
@@ -361,5 +390,3 @@
 }

 ###
-
-


Prevent auto focus of new message input in mobile devices

--- client/routes/roomRoute.coffee
+++ client/routes/roomRoute.coffee
@@ -31,9 +31,10 @@
 				Meteor.call 'readMessages', params._id if Meteor.userId()?
 				# KonchatNotification.removeRoomNotification(params._id)

-				setTimeout ->
-					$('.message-form .input-message').focus()
-				, 100
+				if Meteor.Device.isDesktop()
+					setTimeout ->
+						$('.message-form .input-message').focus()
+					, 100

 	triggersExit: [
 		->


reduce the "show done after clicking done" thresh

--- src/smc-webapp/tasks/update-visible.coffee
+++ src/smc-webapp/tasks/update-visible.coffee
@@ -10,7 +10,8 @@

 {SORT_INFO, HEADINGS, HEADINGS_DIR} = require('./headings')

-DONE_CUTOFF_MS = 45000
+# Show tasks for a few seconds, even after marked done:
+DONE_CUTOFF_MS = 5000

 exports.update_visible = (tasks, local_tasks, view, counts, current_task_id) ->
     show_deleted    = !!view.get('show_deleted')


tweak regex a tiny bit

tweak regex a tiny bit

--- js/libs/string-helpers.coffee
+++ js/libs/string-helpers.coffee
@@ -23,9 +23,9 @@

     if project_name
       #issueNum
-      issuePattern = /\W#(\d+)\b/g
-      issueUrl = "https://github.com/#{project_name}/issues/$1"
+      issuePattern = /(^|\s)#(\d+)\b/g
+      issueUrl = "https://github.com/#{project_name}/issues/$2"

-      text = text.replace(issuePattern, "#$1")
+      text = text.replace(issuePattern, "$1#$2")

     text