more cleaning

more cleaning

--- bai_3_modules/src/utils.coffee
+++ bai_3_modules/src/utils.coffee
@@ -1,178 +0,0 @@
-###
-  use it for coloring
-###
-
-#https://github.com/medikoo/cli-color
-cliColor = require 'cli-color'
-_ = require 'underscore'
-
-Logger = require './logger'
-logger = Logger.inject()
-
-class Utils
-	@_getDateTimeStr: ->
-		result = ''
-
-		date = new Date()
-		dateStr = date.getFullYear() + '/' + (date.getMonth()+1) + '/' + date.getDate()
-		timeStr = date.toLocaleTimeString()
-		result = dateStr + ' ' + timeStr
-
-		result
-
-	@writeLog: (msgs...) ->
-		messages = ''
-
-		for msg in msgs
-			messages += if _.isString(msg) then msg else JSON.stringify(msg, null, '\t') + ' '
-
-		#console.log cliColor.red.bgWhite.underline(@_getDateTimeStr()), messages
-		#logger.info @_getDateTimeStr() + messages
-		logger.info @_getDateTimeStr() + ' - ' + messages
-		return
-
-	@logInfo: (msgs...) ->
-		messages = ''
-		first = true
-		for msg in msgs
-			messages += if _.isString(msg) then msg else JSON.stringify(msg, null, '\t') + ' '
-
-		logger.info @_getDateTimeStr() + ' - ' + messages
-		return
-
-	@logDebug: (msgs...) ->
-		messages = ''
-
-		for msg in msgs
-			messages += if _.isString(msg) then msg else JSON.stringify(msg, null, '\t') + ' '
-
-		logger.warn @_getDateTimeStr() + ' - ' + messages
-		return
-
-	@logWarn: (msgs...) ->
-		messages = ''
-
-		for msg in msgs
-			messages += if _.isString(msg) then msg else JSON.stringify(msg, null, '\t') + ' '
-
-		logger.warn @_getDateTimeStr() + ' - ' + messages
-		return
-
-	@logError: (msgs...) ->
-		messages = ''
-
-		for msg in msgs
-			messages += if _.isString(msg) then msg else JSON.stringify(msg, null, '\t') + ' '
-
-		logger.error @_getDateTimeStr() + ' - ' + messages
-		return
-
-	@writeWarnLog: (msgs...) ->
-		messages = ''
-
-		for msg in msgs
-			messages += if _.isString(msg) then msg else JSON.stringify(msg, null, '\t') + ' '
-		logger.warn @_getDateTimeStr() + ' - ' + messages
-		return
-
-	@handleError: (message, error, res) ->
-		@writeLog message
-		@writeLog error
-
-		res.send error, 404
-
-		return
-
-	@handleResponse: (message, docs, res) ->
-		res.contentType 'application/json'
-
-		if docs instanceof Array
-			#Utils.writeLog "Found #{docs.length} docs, content:"
-			#console.log docs
-
-			if docs.length is 1
-				res.send docs[0]
-			else
-				res.send docs
-		else
-			#Utils.writeWarnLog "Found 1 doc, content:"
-			#console.log docs
-			res.send docs
-
-		@writeLog message
-
-		return
-
-	@checkReqParams: (req, res) ->
-		result = true
-
-		databaseName = req.params.database
-		collectionName = req.params.collection
-
-		if !databaseName?
-			@writeWarnLog 'Database name Undefined'
-
-			res.send 'database name Undefined', 404
-			result = false
-
-		if !collectionName?
-			@writeWarnLog 'Collection name Undefined'
-
-			res.send 'collection name Undefined', 404
-			result = false
-
-		@writeLog "Database name: #{databaseName}"
-		@writeLog "Collection name: #{collectionName}"
-
-		result
-
-	@checkReqParamsOk: (req) ->
-		result = ''
-
-		databaseName = req.params.database
-		collectionName = req.params.collection
-
-		if not databaseName?
-			result 'Database name Undefined'
-
-		if not collectionName?
-			result 'Collection name Undefined'
-
-		return result
-
-	@getParam: (req, parameter_name, defaultValue='') ->
-		parameter_value = defaultValue
-
-		if req.body[parameter_name]?
-			parameter_value = req.body[parameter_name]
-
-		else if req.query[parameter_name]?
-			parameter_value = req.query[parameter_name]
-
-		parameter_value
-
-	@firstUpperCase: (str) ->
-		str.substr(0,1).toUpperCase() + str.substr(1, str.length - 1)
-
-	@convertTableName: (str) =>
-		arrs = str.split '_'
-		#res = @firstUpperCase(item) for item in arrs
-		#res = res.join ''
-		res = ''
-		for item in arrs
-			res += @firstUpperCase(item)
-		return res
-
-	@getClientIp: (req) ->
-		x_ip = null
-		forwardedIps = req.headers['x-forwarded-for']
-
-		if forwardedIps
-			x_ip = forwardedIps.split(',')[0]
-
-		unless x_ip?
-			x_ip = req.connection.remoteAddress
-
-		x_ip
-
-exports.Utils = Utils


Call jQuery.noConflict() only when available

--- bokehjs/src/coffee/main.coffee
+++ bokehjs/src/coffee/main.coffee
@@ -24,7 +24,8 @@
   # Make sure that we don't clobber any existing definition of $ (most
   # likely a previous version of jQuery.
   _oldJQ = window.$
-  window.jQuery.noConflict()
+  if typeof(window.jQuery) != "undefined"
+    window.jQuery.noConflict()
   if typeof($) == "undefined"
     # if there was no previous definition of $, put our definition into window.$.
     window.$ = _oldJQ


Fix font style of selected text

Fix font style of selected text

--- helper.coffee
+++ helper.coffee
@@ -8,7 +8,7 @@
   if options.showSelection
     stylesHTML += """
       .eager-typist-element .eager-typist-selected {
-        font-style: none;
+        font-style: normal;
         color: #fff;
         background: #000;
         color: #{ options.selectedColor };


Count finished tasks

Count finished tasks

--- src/ThreeBSP.coffee
+++ src/ThreeBSP.coffee
@@ -17,9 +17,8 @@

   check: =>
     return unless @started?
-    return unless @timeout?
     returning (elapsed = (Date.now() - @started)), =>
-      if elapsed >= @timeout
+      if elapsed >= @timeout ? Infinity
         throw new Error("Timeout reached: #{elapsed}/#{@timeout}")

   start: =>
@@ -32,8 +31,11 @@
     throw new Error("Finished more tasks than started") if @tasks? and @tasks < 1
     @tasks -= 1
     elapsed = @check()
+    @done ?= 0
+    @done += 1
     if @tasks == 0
-      @started = undefined
+      console.log "All #{@done} tasks finished. #{elapsed}/#{@timeout ? 'Inf'}ms"
+      @started = @done = undefined

   doTask: (block) =>
     do @start
@@ -57,7 +59,7 @@
     @matrix ?= new THREE.Matrix4()

     # Start a timer if one wasn't passed
-    @options.timer ?= new Timelimit(@options.timeout)
+    @options.timer ?= new Timelimit(@options.timer?.timeout ? @options.timeout)

     @tree   = @toTree treeIsh

@@ -252,6 +254,8 @@
     node.back     = @options.timer.doTask => @back?.clone()

+
+
   constructor: (polygons, @options={}) ->
     if polygons? and not (polygons instanceof Array)
       @options = polygons


minor style clean up

minor style clean up

--- src/app/game.coffee
+++ src/app/game.coffee
@@ -23,7 +23,7 @@
     @addPlayer data
     self = this
     client.on "disconnect", ->
-      data = id: this.id
+      data = id: @id
       self.removePlayer data
     client.on "move player", @onMovePlayer

@@ -36,13 +36,12 @@
   addPlayer: (player) =>
     @players[player.id] = player
     @socket.sockets.emit 'new player', player
-    console.log 'players=', @players

   removePlayer: (player) =>
     delete @players[player.id]
     @socket.sockets.emit 'remove player', player

   destructor: ->
-    # @socket.server.close()
+    @socket.server.close()

 module.exports = Game


map commit store explicitly! no auto-mapping anymore

--- source/server/module.coffee
+++ source/server/module.coffee
@@ -29,6 +29,7 @@

   singletons: [
     'Space.eventSourcing.CommitPublisher'
+    'Space.eventSourcing.CommitStore'
     'Space.eventSourcing.Repository'
     'Space.eventSourcing.Projector'
   ]


Clear the click event on the prompt modal

--- sources/tuktuk.modal.coffee
+++ sources/tuktuk.modal.coffee
@@ -63,6 +63,9 @@
     content = modal.find "#content"
     accept_button = modal.find "button.success"
     cancel_button = modal.find "button.alert"
+
+    accept_button = modal.unbind "click.Modal.prompt"
+    cancel_button = modal.unbind "click.Modal.prompt"

     text.html message
     accept_button.on "click.Modal.prompt", ->


Add returns in ComponentLoader spec

Add returns in ComponentLoader spec

--- spec/ComponentLoader.coffee
+++ spec/ComponentLoader.coffee
@@ -11,7 +11,7 @@
   root = 'noflo'
   urlPrefix = '/'

-describe 'ComponentLoader with no external packages installed', ->
+describe.only 'ComponentLoader with no external packages installed', ->
   l = new noflo.ComponentLoader root
   class Split extends noflo.Component
     constructor: ->
@@ -22,7 +22,9 @@
           out: {}
         process: (input, output) ->
           output.sendDone input.get 'in'
+          return
       super options
+      return
   Split.getComponent = -> new Split

   Merge = ->
@@ -31,172 +33,244 @@
     inst.outPorts.add 'out'
     inst.process (input, output) ->
       output.sendDone input.get 'in'
-    inst
+    return inst

   it 'should initially know of no components', ->
     chai.expect(l.components).to.be.null
+    return
   it 'should not initially be ready', ->
     chai.expect(l.ready).to.be.false
+    return
   it 'should not initially be processing', ->
     chai.expect(l.processing).to.be.false
+    return
   it 'should not have any packages in the checked list', ->
     chai.expect(l.checked).to.not.exist

+    return
   describe 'normalizing names', ->
     it 'should return simple module names as-is', ->
       normalized = l.getModulePrefix 'foo'
       chai.expect(normalized).to.equal 'foo'
+      return
     it 'should return empty for NoFlo core', ->
       normalized = l.getModulePrefix 'noflo'
       chai.expect(normalized).to.equal ''
+      return
     it 'should strip noflo-', ->
       normalized = l.getModulePrefix 'noflo-image'
       chai.expect(normalized).to.equal 'image'
+      return
     it 'should strip NPM scopes', ->
       normalized = l.getModulePrefix '@noflo/foo'
       chai.expect(normalized).to.equal 'foo'
+      return
     it 'should strip NPM scopes and noflo-', ->
       normalized = l.getModulePrefix '@noflo/noflo-image'
       chai.expect(normalized).to.equal 'image'
-
+      return
+    return
   it 'should be able to read a list of components', (done) ->
     @timeout 60 * 1000
     ready = false
     l.once 'ready', ->
       ready = true
       chai.expect(l.ready, 'should have the ready bit').to.equal true
+      return
     l.listComponents (err, components) ->
-      return done err if err
+      if err
+        done err
+        return
       chai.expect(l.processing, 'should have stopped processing').to.equal false
       chai.expect(l.components, 'should contain components').not.to.be.empty
       chai.expect(components, 'should have returned the full list').to.equal l.components
       chai.expect(l.ready, 'should have been set ready').to.equal true
       chai.expect(ready, 'should have emitted ready').to.equal true
       done()
+      return

     unless noflo.isBrowser()
       # Browser component registry can be synchronous
       chai.expect(l.processing, 'should have started processing').to.equal true

+    return
   describe 'calling listComponents twice simultaneously', ->
     it 'should return the same results', (done) ->
       loader = new noflo.ComponentLoader root
       received = []
       loader.listComponents (err, components) ->
-        return done err if err
+        if err
+          done err
+          return
         received.push components
         return unless received.length is 2
         chai.expect(received[0]).to.equal received[1]
         done()
+        return
       loader.listComponents (err, components) ->
-        return done err if err
+        if err
+          done err
+          return
         received.push components
         return unless received.length is 2
         chai.expect(received[0]).to.equal received[1]
         done()
-
+        return
+      return
+    return
   describe 'after listing components', ->
     it 'should have the Graph component registered', ->
       chai.expect(l.components.Graph).not.to.be.empty
-
+      return
+
+    return
   describe 'loading the Graph component', ->
     instance = null
     it 'should be able to load the component', (done) ->
       l.load 'Graph', (err, inst) ->
-        return done err if err
+        if err
+          done err
+          return
         chai.expect(inst).to.be.an 'object'
         chai.expect(inst.componentName).to.equal 'Graph'
         instance = inst
         done()
+        return
+      return
     it 'should contain input ports', ->
       chai.expect(instance.inPorts).to.be.an 'object'
       chai.expect(instance.inPorts.graph).to.be.an 'object'
+      return
     it 'should have "on" method on the input port', ->
       chai.expect(instance.inPorts.graph.on).to.be.a 'function'
+      return
     it 'it should know that Graph is a subgraph', ->
       chai.expect(instance.isSubgraph()).to.equal true
+      return
     it 'should know the description for the Graph', ->
       chai.expect(instance.getDescription()).to.be.a 'string'
+      return
     it 'should be able to provide an icon for the Graph', ->
       chai.expect(instance.getIcon()).to.be.a 'string'
       chai.expect(instance.getIcon()).to.equal 'sitemap'
+      return
     it 'should be able to load the component with non-ready ComponentLoader', (done) ->
       loader = new noflo.ComponentLoader root
       loader.load 'Graph', (err, inst) ->
-        return done err if err
+        if err
+          done err
+          return
         chai.expect(inst).to.be.an 'object'
         chai.expect(inst.componentName).to.equal 'Graph'
         instance = inst
         done()
+        return
+      return
+    return

   describe 'loading a subgraph', ->
     l = new noflo.ComponentLoader root
     file = "#{urlPrefix}spec/fixtures/subgraph.fbp"
     it 'should remove `graph` and `start` ports', (done) ->
       l.listComponents (err, components) ->
-        return done err if err
+        if err
+          done err
+          return
         l.components.Merge = Merge
         l.components.Subgraph = file
         l.components.Split = Split
         l.load 'Subgraph', (err, inst) ->
-          return done err if err
+          if err
+            done err
+            return
           chai.expect(inst).to.be.an 'object'
           inst.once 'ready', ->
             chai.expect(inst.inPorts.ports).not.to.have.keys ['graph','start']
             chai.expect(inst.inPorts.ports).to.have.keys ['in']
             chai.expect(inst.outPorts.ports).to.have.keys ['out']
             done()
+            return
+          return
+        return
+      return
     it 'should not automatically start the subgraph if there is no `start` port', (done) ->
       l.listComponents (err, components) ->
-        return done err if err
+        if err
+          done err
+          return
         l.components.Merge = Merge
         l.components.Subgraph = file
         l.components.Split = Split
         l.load 'Subgraph', (err, inst) ->
-          return done err if err
+          if err
+            done err
+            return
           chai.expect(inst).to.be.an 'object'
           inst.once 'ready', ->
             chai.expect(inst.started).to.equal(false)
             done()
+            return
+          return
+        return
+      return
     it 'should also work with a passed graph object', (done) ->
       noflo.graph.loadFile file, (err, graph) ->
-        return done err if err
+        if err
+          done err
+          return
         l.listComponents (err, components) ->
-          return done err if err
+          if err
+            done err
+            return
           l.components.Merge = Merge
           l.components.Subgraph = graph
           l.components.Split = Split
           l.load 'Subgraph', (err, inst) ->
-            return done err if err
+            if err
+              done err
+              return
             chai.expect(inst).to.be.an 'object'
             inst.once 'ready', ->
               chai.expect(inst.inPorts.ports).not.to.have.keys ['graph','start']
               chai.expect(inst.inPorts.ports).to.have.keys ['in']
               chai.expect(inst.outPorts.ports).to.have.keys ['out']
               done()
-
+              return
+            return
+          return
+        return
+      return
+    return
   describe 'loading the Graph component', ->
     instance = null
     it 'should be able to load the component', (done) ->
       l.load 'Graph', (err, graph) ->
-        return done err if err
+        if err
+          done err
+          return
         chai.expect(graph).to.be.an 'object'
         instance = graph
         done()
+        return
+      return
     it 'should have a reference to the Component Loader\'s baseDir', ->
       chai.expect(instance.baseDir).to.equal l.baseDir
-
+      return
+    return
   describe 'loading a component', ->
     loader = null
     before (done) ->
       loader = new noflo.ComponentLoader root
       loader.listComponents done
+      return
     it 'should return an error on an invalid component type', (done) ->
       loader.components['InvalidComponent'] = true
       loader.load 'InvalidComponent', (err, c) ->
         chai.expect(err).to.be.an 'error'
         chai.expect(err.message).to.equal 'Invalid type boolean for component InvalidComponent.'
         done()
+        return
+      return
     it 'should return an error on a missing component path', (done) ->
       loader.components['InvalidComponent'] = 'missing-file.js'
       if noflo.isBrowser()
@@ -207,7 +281,9 @@
         chai.expect(err).to.be.an 'error'
         chai.expect(err.message).to.contain str
         done()
-
+        return
+      return
+    return
   describe 'register a component at runtime', ->
     class FooSplit extends noflo.Component
       constructor: ->
@@ -223,46 +299,68 @@
     it 'should be available in the components list', ->
       l.registerComponent 'foo', 'Split', FooSplit
       chai.expect(l.components).to.contain.keys ['foo/Split', 'Graph']
+      return
     it 'should be able to load the component', (done) ->
       l.load 'foo/Split', (err, split) ->
-        return done err if err
+        if err
+          done err
+          return
         chai.expect(split).to.be.an 'object'
         instance = split
         done()
+        return
+      return
     it 'should have the correct ports', ->
       chai.expect(instance.inPorts.ports).to.have.keys ['in']
       chai.expect(instance.outPorts.ports).to.have.keys ['out']
+      return
     it 'should have inherited its icon from the library', ->
       chai.expect(instance.getIcon()).to.equal 'star'
+      return
     it 'should emit an event on icon change', (done) ->
       instance.once 'icon', (newIcon) ->
         chai.expect(newIcon).to.equal 'smile'
         done()
+        return
       instance.setIcon 'smile'
+      return
     it 'new instances should still contain the original icon', (done) ->
       l.load 'foo/Split', (err, split) ->
-        return done err if err
+        if err
+          done err
+          return
         chai.expect(split).to.be.an 'object'
         chai.expect(split.getIcon()).to.equal 'star'
         done()
+        return
+      return
     # TODO reconsider this test after full decaffeination
     it.skip 'after setting an icon for the Component class, new instances should have that', (done) ->
       FooSplit::icon = 'trophy'
       l.load 'foo/Split', (err, split) ->
-        return done err if err
+        if err
+          done err
+          return
         chai.expect(split).to.be.an 'object'
         chai.expect(split.getIcon()).to.equal 'trophy'
         done()
+        return
+      return
     it 'should not affect the original instance', ->
       chai.expect(instance.getIcon()).to.equal 'smile'
-
+      return
+    return
   describe 'reading sources', ->
     before ->
       # getSource not implemented in webpack loader yet
-      return @skip() if noflo.isBrowser()
+      if noflo.isBrowser()
+        @skip()
+        return
     it 'should be able to provide source code for a component', (done) ->
       l.getSource 'Graph', (err, component) ->
-        return done err if err
+        if err
+          done err
+          return
         chai.expect(component).to.be.an 'object'
         chai.expect(component.code).to.be.a 'string'
         chai.expect(component.code.indexOf('noflo.Component')).to.not.equal -1
@@ -271,36 +369,55 @@
         chai.expect(component.library).to.equal ''
         chai.expect(component.language).to.equal shippingLanguage
         done()
+        return
+      return
     it 'should return an error for missing components', (done) ->
       l.getSource 'foo/BarBaz', (err, src) ->
         chai.expect(err).to.be.an 'error'
         done()
+        return
+      return
     it 'should return an error for non-file components', (done) ->
       l.getSource 'foo/Split', (err, src) ->
         chai.expect(err).to.be.an 'error'
         done()
+        return
+      return
     it 'should be able to provide source for a graph file component', (done) ->
       file = "#{urlPrefix}spec/fixtures/subgraph.fbp"
       l.components.Subgraph = file
       l.getSource 'Subgraph', (err, src) ->
-        return done err if err
+        if err
+          done err
+          return
         chai.expect(src.code).to.not.be.empty
         chai.expect(src.language).to.equal 'json'
         done()
+        return
+      return
     it 'should be able to provide source for a graph object component', (done) ->
       file = "#{urlPrefix}spec/fixtures/subgraph.fbp"
       noflo.graph.loadFile file, (err, graph) ->
-        return done err if err
+        if err
+          done err
+          return
         l.components.Subgraph2 = graph
         l.getSource 'Subgraph2', (err, src) ->
-          return done err if err
+          if err
+            done err
+            return
           chai.expect(src.code).to.not.be.empty
           chai.expect(src.language).to.equal 'json'
           done()
+          return
+        return
+      return
     it 'should be able to get the source for non-ready ComponentLoader', (done) ->
       loader = new noflo.ComponentLoader root
       loader.getSource 'Graph', (err, component) ->
-        return done err if err
+        if err
+          done err
+          return
         chai.expect(component).to.be.an 'object'
         chai.expect(component.code).to.be.a 'string'
         chai.expect(component.code.indexOf('noflo.Component')).to.not.equal -1
@@ -309,7 +426,9 @@
         chai.expect(component.library).to.equal ''
         chai.expect(component.language).to.equal shippingLanguage
         done()
-
+        return
+      return
+    return
   describe 'writing sources', ->
     describe 'with working code', ->
       describe 'with ES5', ->
@@ -331,11 +450,17 @@
           unless noflo.isBrowser()
             workingSource = workingSource.replace "'noflo'", "'../src/lib/NoFlo'"
           l.setSource 'foo', 'RepeatData', workingSource, 'javascript', (err) ->
-            return done err if err
+            if err
+              done err
+              return
             done()
+            return
+          return
         it 'should be a loadable component', (done) ->
           l.load 'foo/RepeatData', (err, inst) ->
-            return done err if err
+            if err
+              done err
+              return
             chai.expect(inst).to.be.an 'object'
             chai.expect(inst.inPorts).to.contain.keys ['in']
             chai.expect(inst.outPorts).to.contain.keys ['out']
@@ -347,15 +472,22 @@
               chai.expect(ip.type).to.equal 'data'
               chai.expect(ip.data).to.equal 'ES5'
               done()
+              return
             ins.send 'ES5'
+            return
+          return
         it 'should be able to set the source for non-ready ComponentLoader', (done) ->
           @timeout 10000
           loader = new noflo.ComponentLoader root
           loader.setSource 'foo', 'RepeatData', workingSource, 'javascript', done
+          return
+        return
       describe 'with ES6', ->
         before ->
           # PhantomJS doesn't work with ES6
-          return @skip() if noflo.isBrowser()
+          if noflo.isBrowser()
+            @skip()
+            return
         workingSource = """
         const noflo = require('noflo');

@@ -374,11 +506,17 @@
           unless noflo.isBrowser()
             workingSource = workingSource.replace "'noflo'", "'../src/lib/NoFlo'"
           l.setSource 'foo', 'RepeatDataES6', workingSource, 'es6', (err) ->
-            return done err if err
+            if err
+              done err
+              return
             done()
+            return
+          return
         it 'should be a loadable component', (done) ->
           l.load 'foo/RepeatDataES6', (err, inst) ->
-            return done err if err
+            if err
+              done err
+              return
             chai.expect(inst).to.be.an 'object'
             chai.expect(inst.inPorts).to.contain.keys ['in']
             chai.expect(inst.outPorts).to.contain.keys ['out']
@@ -390,12 +528,18 @@
               chai.expect(ip.type).to.equal 'data'
               chai.expect(ip.data).to.equal 'ES6'
               done()
+              return
             ins.send 'ES6'
+            return
+          return
+        return
       describe 'with CoffeeScript', ->
         before ->
           # CoffeeScript tests work in browser only if we have CoffeeScript
           # compiler loaded
-          return @skip() if noflo.isBrowser() and not window.CoffeeScript
+          if noflo.isBrowser() and not window.CoffeeScript
+            @skip()
+          return
         workingSource = """
         noflo = require 'noflo'
         exports.getComponent = ->
@@ -411,11 +555,17 @@
           unless noflo.isBrowser()
             workingSource = workingSource.replace "'noflo'", "'../src/lib/NoFlo'"
           l.setSource 'foo', 'RepeatDataCoffee', workingSource, 'coffeescript', (err) ->
-            return done err if err
+            if err
+              done err
+              return
             done()
+            return
+          return
         it 'should be a loadable component', (done) ->
           l.load 'foo/RepeatDataCoffee', (err, inst) ->
-            return done err if err
+            if err
+              done err
+              return
             chai.expect(inst).to.be.an 'object'
             chai.expect(inst.inPorts).to.contain.keys ['in']
             chai.expect(inst.outPorts).to.contain.keys ['out']
@@ -427,8 +577,12 @@
               chai.expect(ip.type).to.equal 'data'
               chai.expect(ip.data).to.equal 'CoffeeScript'
               done()
+              return
             ins.send 'CoffeeScript'
-
+            return
+          return
+        return
+      return
     describe 'with non-working code', ->
       describe 'without exports', ->
         nonWorkingSource = """
@@ -456,11 +610,16 @@
             chai.expect(err).to.be.an 'error'
             chai.expect(err.message).to.contain 'runnable component'
             done()
+            return
+          return
         it 'should not be a loadable component', (done) ->
           l.load 'foo/NotWorking', (err, inst) ->
             chai.expect(err).to.be.an 'error'
             chai.expect(inst).to.be.an 'undefined'
             done()
+            return
+          return
+        return
       describe 'with non-existing import', ->
         nonWorkingSource = """
         var noflo = require('noflo');
@@ -488,11 +647,16 @@
           l.setSource 'foo', 'NotWorking', nonWorkingSource, 'js', (err) ->
             chai.expect(err).to.be.an 'error'
             done()
+            return
+          return
         it 'should not be a loadable component', (done) ->
           l.load 'foo/NotWorking', (err, inst) ->
             chai.expect(err).to.be.an 'error'
             chai.expect(inst).to.be.an 'undefined'
             done()
+            return
+          return
+        return
       describe 'with deprecated process callback', ->
         nonWorkingSource = """
         var noflo = require('noflo');
@@ -518,137 +682,192 @@
           unless noflo.isBrowser()
             nonWorkingSource = nonWorkingSource.replace "'noflo'", "'../src/lib/NoFlo'"
           l.setSource 'foo', 'NotWorkingProcess', nonWorkingSource, 'js', done
+          return
         it 'should not be a loadable component', (done) ->
           l.load 'foo/NotWorkingProcess', (err, inst) ->
             chai.expect(err).to.be.an 'error'
             chai.expect(err.message).to.contain 'process callback is deprecated'
             chai.expect(inst).to.be.an 'undefined'
             done()
-
+            return
+          return
+        return
+      return
+    return
+  return
 describe 'ComponentLoader with a fixture project', ->
   l = null
   before ->
-    return @skip() if noflo.isBrowser()
+    if noflo.isBrowser()
+      @skip()
+      return
   it 'should be possible to instantiate', ->
     l = new noflo.ComponentLoader path.resolve __dirname, 'fixtures/componentloader'
+    return
   it 'should initially know of no components', ->
     chai.expect(l.components).to.be.a 'null'
+    return
   it 'should not initially be ready', ->
     chai.expect(l.ready).to.be.false
+    return
   it 'should be able to read a list of components', (done) ->
     ready = false
     l.once 'ready', ->
       chai.expect(l.ready).to.equal true
       ready = l.ready
+      return
     l.listComponents (err, components) ->
-      return done err if err
+      if err
+        done err
+        return
       chai.expect(l.processing).to.equal false
       chai.expect(l.components).not.to.be.empty
       chai.expect(components).to.equal l.components
       chai.expect(l.ready).to.equal true
       chai.expect(ready).to.equal true
       done()
+      return
     chai.expect(l.processing).to.equal true
+    return
   it 'should be able to load a local component', (done) ->
     l.load 'componentloader/Output', (err, instance) ->
       chai.expect(err).to.be.a 'null'
       chai.expect(instance.description).to.equal 'Output stuff'
       chai.expect(instance.icon).to.equal 'cloud'
       done()
+      return
+    return
   it 'should be able to load a component from a dependency', (done) ->
     l.load 'example/Forward', (err, instance) ->
       chai.expect(err).to.be.a 'null'
       chai.expect(instance.description).to.equal 'Forward stuff'
       chai.expect(instance.icon).to.equal 'car'
       done()
+      return
+    return
   it 'should be able to load a dynamically registered component from a dependency', (done) ->
     l.load 'example/Hello', (err, instance) ->
       chai.expect(err).to.be.a 'null'
       chai.expect(instance.description).to.equal 'Hello stuff'
       chai.expect(instance.icon).to.equal 'bicycle'
       done()
+      return
+    return
   it 'should be able to load core Graph component', (done) ->
     l.load 'Graph', (err, instance) ->
       chai.expect(err).to.be.a 'null'
       chai.expect(instance.icon).to.equal 'sitemap'
       done()
+      return
+    return
   it 'should fail loading a missing component', (done) ->
     l.load 'componentloader/Missing', (err, instance) ->
       chai.expect(err).to.be.an 'error'
       done()
-
+      return
+    return
+  return
 describe 'ComponentLoader with a fixture project and caching', ->
   l = null
   fixtureRoot = null
   before ->
-    return @skip() if noflo.isBrowser()
+    if noflo.isBrowser()
+      @skip()
+      return
     fixtureRoot = path.resolve __dirname, 'fixtures/componentloader'
+    return
   after (done) ->
-    return done() if noflo.isBrowser()
+    if noflo.isBrowser()
+      done()
+      return
     manifestPath = path.resolve fixtureRoot, 'fbp.json'
     { unlink } = require 'fs'
     unlink manifestPath, done
+    return
   it 'should be possible to pre-heat the cache file', (done) ->
     @timeout 8000
     { exec } = require 'child_process'
     exec "node #{path.resolve(__dirname, '../bin/noflo-cache-preheat')}",
       cwd: fixtureRoot
     , done
+    return
   it 'should have populated a fbp-manifest file', (done) ->
     manifestPath = path.resolve fixtureRoot, 'fbp.json'
     { stat } = require 'fs'
     stat manifestPath, (err, stats) ->
-      return done err if err
+      if err
+        done err
+        return
       chai.expect(stats.isFile()).to.equal true
       done()
+      return
+    return
   it 'should be possible to instantiate', ->
     l = new noflo.ComponentLoader fixtureRoot,
       cache: true
+    return
   it 'should initially know of no components', ->
     chai.expect(l.components).to.be.a 'null'
+    return
   it 'should not initially be ready', ->
     chai.expect(l.ready).to.be.false
+    return
   it 'should be able to read a list of components', (done) ->
     ready = false
     l.once 'ready', ->
       chai.expect(l.ready).to.equal true
       ready = l.ready
+      return
     l.listComponents (err, components) ->
-      return done err if err
+      if err
+        done err
+        return
       chai.expect(l.processing).to.equal false
       chai.expect(l.components).not.to.be.empty
       chai.expect(components).to.equal l.components
       chai.expect(l.ready).to.equal true
       chai.expect(ready).to.equal true
       done()
+      return
     chai.expect(l.processing).to.equal true
+    return
   it 'should be able to load a local component', (done) ->
     l.load 'componentloader/Output', (err, instance) ->
       chai.expect(err).to.be.a 'null'
       chai.expect(instance.description).to.equal 'Output stuff'
       chai.expect(instance.icon).to.equal 'cloud'
       done()
+      return
+    return
   it 'should be able to load a component from a dependency', (done) ->
     l.load 'example/Forward', (err, instance) ->
       chai.expect(err).to.be.a 'null'
       chai.expect(instance.description).to.equal 'Forward stuff'
       chai.expect(instance.icon).to.equal 'car'
       done()
+      return
+    return
   it 'should be able to load a dynamically registered component from a dependency', (done) ->
     l.load 'example/Hello', (err, instance) ->
       chai.expect(err).to.be.a 'null'
       chai.expect(instance.description).to.equal 'Hello stuff'
       chai.expect(instance.icon).to.equal 'bicycle'
       done()
+      return
+    return
   it 'should be able to load core Graph component', (done) ->
     l.load 'Graph', (err, instance) ->
       chai.expect(err).to.be.a 'null'
       chai.expect(instance.icon).to.equal 'sitemap'
       done()
+      return
+    return
   it 'should fail loading a missing component', (done) ->
     l.load 'componentloader/Missing', (err, instance) ->
       chai.expect(err).to.be.an 'error'
       done()
+      return
+    return
   it 'should fail with missing manifest without discover option', (done) ->
     l = new noflo.ComponentLoader fixtureRoot,
       cache: true
@@ -657,6 +876,8 @@
     l.listComponents (err) ->
       chai.expect(err).to.be.an 'error'
       done()
+      return
+    return
   it 'should be able to use a custom manifest file', (done) ->
     @timeout 8000
     manifestPath = path.resolve fixtureRoot, 'fbp2.json'
@@ -665,11 +886,17 @@
       discover: true
       manifest: 'fbp2.json'
     l.listComponents (err, components) ->
-      return done err if err
+      if err
+        done err
+        return
       chai.expect(l.processing).to.equal false
       chai.expect(l.components).not.to.be.empty
       done()
+      return
+    return
   it 'should have saved the new manifest', (done) ->
     manifestPath = path.resolve fixtureRoot, 'fbp2.json'
     { unlink } = require 'fs'
     unlink manifestPath, done
+    return
+  return


Move all mouse event specs under one describe block

--- spec/app/editor-spec.coffee
+++ spec/app/editor-spec.coffee
@@ -874,203 +874,104 @@
         editor.setFontSize(10)
         expect(editor.renderedLines.find(".line").length).toBeGreaterThan originalLineCount

-  describe "when a mousedown event occurs in the editor", ->
+  describe "mouse events", ->
     beforeEach ->
       editor.attachToDom()
       editor.css(position: 'absolute', top: 10, left: 10)

-      describe "when it is a single click", ->
-        it "re-positions the cursor to the clicked row / column", ->
+    describe "single-click", ->
+      it "re-positions the cursor to the clicked row / column", ->
+        expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
+
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 10])
+        expect(editor.getCursorScreenPosition()).toEqual(row: 3, column: 10)
+
+      describe "when the lines are scrolled to the right", ->
+        it "re-positions the cursor on the clicked location", ->
+          setEditorWidthInChars(editor, 30)
           expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
-
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 10])
-          expect(editor.getCursorScreenPosition()).toEqual(row: 3, column: 10)
-
-        describe "when the lines are scrolled to the right", ->
-          it "re-positions the cursor on the clicked location", ->
-            setEditorWidthInChars(editor, 30)
-            expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
-            editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 30]) # scrolls lines to the right
-            editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 50])
-            expect(editor.getCursorBufferPosition()).toEqual(row: 3, column: 50)
-
-      describe "when it is a double click", ->
-        it "selects the word under the cursor", ->
-          expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 8], originalEvent: {detail: 1})
-          editor.renderedLines.trigger 'mouseup'
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 8], originalEvent: {detail: 2})
-          editor.renderedLines.trigger 'mouseup'
-          expect(editor.getSelectedText()).toBe "quicksort"
-
-      describe "when it is clicked more then twice (triple, quadruple, etc...)", ->
-        it "selects the line under the cursor", ->
-          expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
-
-          # Triple click
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 8], originalEvent: {detail: 1})
-          editor.renderedLines.trigger 'mouseup'
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 8], originalEvent: {detail: 2})
-          editor.renderedLines.trigger 'mouseup'
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 8], originalEvent: {detail: 3})
-          editor.renderedLines.trigger 'mouseup'
-          expect(editor.getSelectedText()).toBe "  var sort = function(items) {"
-
-          # Quad click
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 1})
-          editor.renderedLines.trigger 'mouseup'
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 2})
-          editor.renderedLines.trigger 'mouseup'
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 3})
-          editor.renderedLines.trigger 'mouseup'
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 4})
-          editor.renderedLines.trigger 'mouseup'
-          expect(editor.getSelectedText()).toBe "    if (items.length <= 1) return items;"
-
-  describe "when the cursor moves", ->
-    charWidth = null
-
-    beforeEach ->
-      editor.attachToDom()
-      editor.vScrollMargin = 3
-      editor.hScrollMargin = 5
-      {charWidth} = editor
-
-    it "repositions the cursor's view on screen", ->
-      editor.attachToDom()
-      editor.setCursorScreenPosition(row: 2, column: 2)
-      expect(editor.getCursorView().position()).toEqual(top: 2 * editor.lineHeight, left: 2 * editor.charWidth)
-
-    describe "auto-scrolling", ->
-      it "only auto-scrolls when the last cursor is moved", ->
-        editor.setCursorBufferPosition([11,0])
-        editor.addCursorAtBufferPosition([6,50])
-        [cursor1, cursor2] = editor.getCursors()
-
-        spyOn(editor, 'scrollTo')
-        cursor1.setScreenPosition([10, 10])
-        expect(editor.scrollTo).not.toHaveBeenCalled()
-
-        cursor2.setScreenPosition([11, 11])
-        expect(editor.scrollTo).toHaveBeenCalled()
-
-      describe "when the last cursor exceeds the upper or lower scroll margins", ->
-        describe "when the editor is taller than twice the vertical scroll margin", ->
-          it "sets the scrollTop so the cursor remains within the scroll margin", ->
-            setEditorHeightInLines(editor, 10)
-
-            _.times 6, -> editor.moveCursorDown()
-            expect(editor.scrollTop()).toBe(0)
-
-            editor.moveCursorDown()
-            expect(editor.scrollTop()).toBe(editor.lineHeight)
-
-            editor.moveCursorDown()
-            expect(editor.scrollTop()).toBe(editor.lineHeight * 2)
-
-            _.times 3, -> editor.moveCursorUp()
-
-            editor.moveCursorUp()
-            expect(editor.scrollTop()).toBe(editor.lineHeight)
-
-            editor.moveCursorUp()
-            expect(editor.scrollTop()).toBe(0)
-
-        describe "when the editor is shorter than twice the vertical scroll margin", ->
-          it "sets the scrollTop based on a reduced scroll margin, which prevents a jerky tug-of-war between upper and lower scroll margins", ->
-            setEditorHeightInLines(editor, 5)
-
-            _.times 3, -> editor.moveCursorDown()
-
-            expect(editor.scrollTop()).toBe(editor.lineHeight)
-
-            editor.moveCursorUp()
-            expect(editor.renderedLines.css('top')).toBe "0px"
-
-      describe "when the last cursor exceeds the right or left scroll margins", ->
-        describe "when soft-wrap is disabled", ->
-          describe "when the editor is wider than twice the horizontal scroll margin", ->
-            it "sets the scrollView's scrollLeft so the cursor remains within the scroll margin", ->
-              setEditorWidthInChars(editor, 30)
-
-              # moving right
-              editor.setCursorScreenPosition([2, 24])
-              window.advanceClock()
-              expect(editor.scrollView.scrollLeft()).toBe 0
-
-              editor.setCursorScreenPosition([2, 25])
-              window.advanceClock()
-              expect(editor.scrollView.scrollLeft()).toBe charWidth
-
-              editor.setCursorScreenPosition([2, 28])
-              window.advanceClock()
-              expect(editor.scrollView.scrollLeft()).toBe charWidth * 4
-
-              # moving left
-              editor.setCursorScreenPosition([2, 9])
-              window.advanceClock()
-              expect(editor.scrollView.scrollLeft()).toBe charWidth * 4
-
-              editor.setCursorScreenPosition([2, 8])
-              window.advanceClock()
-              expect(editor.scrollView.scrollLeft()).toBe charWidth * 3
-
-              editor.setCursorScreenPosition([2, 5])
-              window.advanceClock()
-              expect(editor.scrollView.scrollLeft()).toBe 0
-
-          describe "when the editor is narrower than twice the horizontal scroll margin", ->
-            it "sets the scrollView's scrollLeft based on a reduced horizontal scroll margin, to prevent a jerky tug-of-war between right and left scroll margins", ->
-              editor.hScrollMargin = 6
-              setEditorWidthInChars(editor, 7)
-
-              editor.setCursorScreenPosition([2, 3])
-              window.advanceClock()
-              expect(editor.scrollView.scrollLeft()).toBe(0)
-
-              editor.setCursorScreenPosition([2, 4])
-              window.advanceClock()
-              expect(editor.scrollView.scrollLeft()).toBe(charWidth)
-
-              editor.setCursorScreenPosition([2, 3])
-              window.advanceClock()
-              expect(editor.scrollView.scrollLeft()).toBe(0)
-
-        describe "when soft-wrap is enabled", ->
-          beforeEach ->
-            editor.setSoftWrap(true)
-
-          it "does not scroll the buffer horizontally", ->
-            editor.width(charWidth * 30)
-
-            # moving right
-            editor.setCursorScreenPosition([2, 24])
-            expect(editor.scrollView.scrollLeft()).toBe 0
-
-            editor.setCursorScreenPosition([2, 25])
-            expect(editor.scrollView.scrollLeft()).toBe 0
-
-            editor.setCursorScreenPosition([2, 28])
-            expect(editor.scrollView.scrollLeft()).toBe 0
-
-            # moving left
-            editor.setCursorScreenPosition([2, 9])
-            expect(editor.scrollView.scrollLeft()).toBe 0
-
-            editor.setCursorScreenPosition([2, 8])
-            expect(editor.scrollView.scrollLeft()).toBe 0
-
-            editor.setCursorScreenPosition([2, 5])
-            expect(editor.scrollView.scrollLeft()).toBe 0
-
-
-  describe "selection", ->
-    selection = null
-
-    beforeEach ->
-      selection = editor.getSelection()
-
-    describe "when the mouse is dragged across the text", ->
+          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 30]) # scrolls lines to the right
+          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 50])
+          expect(editor.getCursorBufferPosition()).toEqual(row: 3, column: 50)
+
+    describe "double-click", ->
+      it "selects the word under the cursor", ->
+        expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 8], originalEvent: {detail: 1})
+        editor.renderedLines.trigger 'mouseup'
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 8], originalEvent: {detail: 2})
+        editor.renderedLines.trigger 'mouseup'
+        expect(editor.getSelectedText()).toBe "quicksort"
+
+    describe "triple/quardruple/etc-click", ->
+      it "selects the line under the cursor", ->
+        expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
+
+        # Triple click
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 8], originalEvent: {detail: 1})
+        editor.renderedLines.trigger 'mouseup'
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 8], originalEvent: {detail: 2})
+        editor.renderedLines.trigger 'mouseup'
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 8], originalEvent: {detail: 3})
+        editor.renderedLines.trigger 'mouseup'
+        expect(editor.getSelectedText()).toBe "  var sort = function(items) {"
+
+        # Quad click
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 1})
+        editor.renderedLines.trigger 'mouseup'
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 2})
+        editor.renderedLines.trigger 'mouseup'
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 3})
+        editor.renderedLines.trigger 'mouseup'
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [2, 3], originalEvent: {detail: 4})
+        editor.renderedLines.trigger 'mouseup'
+        expect(editor.getSelectedText()).toBe "    if (items.length <= 1) return items;"
+
+    describe "shift-click", ->
+      it "selects from the cursor's current location to the clicked location", ->
+        editor.setCursorScreenPosition([4, 7])
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true)
+        expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]]
+
+    describe "shift-double-click", ->
+      it "expands the selection to include the double-clicked word", ->
+        editor.setCursorScreenPosition([4, 7])
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 1 })
+        editor.renderedLines.trigger 'mouseup'
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 2 })
+        editor.renderedLines.trigger 'mouseup'
+        expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 27]]
+
+    describe "shift-triple-click", ->
+      it "expands the selection to include the triple-clicked line", ->
+        editor.setCursorScreenPosition([4, 7])
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 1 })
+        editor.renderedLines.trigger 'mouseup'
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 2 })
+        editor.renderedLines.trigger 'mouseup'
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 3 })
+        editor.renderedLines.trigger 'mouseup'
+        expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 30]]
+
+    describe "meta-click", ->
+      it "places an additional cursor", ->
+        editor.attachToDom()
+        setEditorHeightInLines(editor, 5)
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 0])
+        editor.scrollTop(editor.lineHeight * 6)
+
+        spyOn(editor, "scrollTo").andCallThrough()
+
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [6, 0], metaKey: true)
+        expect(editor.scrollTo.callCount).toBe 1
+
+        [cursor1, cursor2] = editor.getCursorViews()
+        expect(cursor1.position()).toEqual(top: 3 * editor.lineHeight, left: 0)
+        expect(cursor1.getBufferPosition()).toEqual [3, 0]
+        expect(cursor2.position()).toEqual(top: 6 * editor.lineHeight, left: 0)
+        expect(cursor2.getBufferPosition()).toEqual [6, 0]
+
+    describe "click and drag", ->
       it "creates a selection from the initial click to mouse cursor's location ", ->
         editor.attachToDom()
         editor.css(position: 'absolute', top: 10, left: 10)
@@ -1097,6 +998,7 @@
         expect(range.end).toEqual({row: 5, column: 27})
         expect(editor.getCursorScreenPosition()).toEqual(row: 5, column: 27)

+    describe "double-click and drag", ->
       it "creates a selection from the word underneath an initial double click to mouse's new location ", ->
         editor.attachToDom()
         editor.css(position: 'absolute', top: 10, left: 10)
@@ -1125,6 +1027,7 @@
         expect(range.end).toEqual({row: 5, column: 27})
         expect(editor.getCursorScreenPosition()).toEqual(row: 5, column: 27)

+    describe "trip-click and drag", ->
       it "creates a selection from the line underneath an initial triple click to mouse's new location ", ->
         editor.attachToDom()
         editor.css(position: 'absolute', top: 10, left: 10)
@@ -1155,64 +1058,157 @@
         expect(range.end).toEqual({row: 5, column: 27})
         expect(editor.getCursorScreenPosition()).toEqual(row: 5, column: 27)

-    describe "shift-click", ->
-      beforeEach ->
-        editor.attachToDom()
-        editor.css(position: 'absolute', top: 10, left: 10)
-        editor.setCursorScreenPosition([4, 7])
-
-      it "selects from the cursor's current location to the clicked location", ->
-        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true)
-        expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]]
-
-      describe "when it is a double-click", ->
-        it "expands the selection to include the double-clicked word", ->
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 1 })
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 2 })
-          expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 27]]
-
-      describe "when it is a triple-click", ->
-        it "expands the selection to include the triple-clicked line", ->
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 1 })
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 2 })
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [5, 24], shiftKey: true, originalEvent: { detail: 3 })
-          expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 30]]
-
-  describe "multiple cursors", ->
-    it "places multiple cursors with meta-click", ->
+    describe "meta-click and drag", ->
+      it "adds an additional selection", ->
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [4, 10])
+        editor.renderedLines.trigger mousemoveEvent(editor: editor, point: [5, 27])
+        editor.renderedLines.trigger 'mouseup'
+
+        editor.renderedLines.trigger mousedownEvent(editor: editor, point: [6, 10], metaKey: true)
+        editor.renderedLines.trigger mousemoveEvent(editor: editor, point: [8, 27], metaKey: true)
+        editor.renderedLines.trigger 'mouseup'
+
+        selections = editor.getSelections()
+        expect(selections.length).toBe 2
+        [selection1, selection2] = selections
+        expect(selection1.getScreenRange()).toEqual [[4, 10], [5, 27]]
+        expect(selection2.getScreenRange()).toEqual [[6, 10], [8, 27]]
+
+  describe "when the cursor moves", ->
+    charWidth = null
+
+    beforeEach ->
       editor.attachToDom()
-      setEditorHeightInLines(editor, 5)
-      editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 0])
-      editor.scrollTop(editor.lineHeight * 6)
-
-      spyOn(editor, "scrollTo").andCallThrough()
-
-      editor.renderedLines.trigger mousedownEvent(editor: editor, point: [6, 0], metaKey: true)
-      expect(editor.scrollTo.callCount).toBe 1
-
-      [cursor1, cursor2] = editor.find('.cursor').map -> $(this).view()
-      expect(cursor1.position()).toEqual(top: 3 * editor.lineHeight, left: 0)
-      expect(cursor1.getBufferPosition()).toEqual [3, 0]
-      expect(cursor2.position()).toEqual(top: 6 * editor.lineHeight, left: 0)
-      expect(cursor2.getBufferPosition()).toEqual [6, 0]
-
-    describe "selections", ->
-      describe "upon clicking and dragging with the meta-key held down", ->
-        it "adds an additional selection upon clicking and dragging with the meta-key held down", ->
-          editor.attachToDom()
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [4, 10])
-          editor.renderedLines.trigger mousemoveEvent(editor: editor, point: [5, 27])
-          editor.renderedLines.trigger 'mouseup'
-
-          editor.renderedLines.trigger mousedownEvent(editor: editor, point: [6, 10], metaKey: true)
-          editor.renderedLines.trigger mousemoveEvent(editor: editor, point: [8, 27], metaKey: true)
-          editor.renderedLines.trigger 'mouseup'
-
-          selections = editor.getSelections()
-          expect(selections.length).toBe 2
-          [selection1, selection2] = selections
-          expect(selection1.getScreenRange()).toEqual [[4, 10], [5, 27]]
-          expect(selection2.getScreenRange()).toEqual [[6, 10], [8, 27]]
+      editor.vScrollMargin = 3
+      editor.hScrollMargin = 5
+      {charWidth} = editor
+
+    it "repositions the cursor's view on screen", ->
+      editor.attachToDom()
+      editor.setCursorScreenPosition(row: 2, column: 2)
+      expect(editor.getCursorView().position()).toEqual(top: 2 * editor.lineHeight, left: 2 * editor.charWidth)
+
+    describe "auto-scrolling", ->
+      it "only auto-scrolls when the last cursor is moved", ->
+        editor.setCursorBufferPosition([11,0])
+        editor.addCursorAtBufferPosition([6,50])
+        [cursor1, cursor2] = editor.getCursors()
+
+        spyOn(editor, 'scrollTo')
+        cursor1.setScreenPosition([10, 10])
+        expect(editor.scrollTo).not.toHaveBeenCalled()
+
+        cursor2.setScreenPosition([11, 11])
+        expect(editor.scrollTo).toHaveBeenCalled()
+
+      describe "when the last cursor exceeds the upper or lower scroll margins", ->
+        describe "when the editor is taller than twice the vertical scroll margin", ->
+          it "sets the scrollTop so the cursor remains within the scroll margin", ->
+            setEditorHeightInLines(editor, 10)
+
+            _.times 6, -> editor.moveCursorDown()
+            expect(editor.scrollTop()).toBe(0)
+
+            editor.moveCursorDown()
+            expect(editor.scrollTop()).toBe(editor.lineHeight)
+
+            editor.moveCursorDown()
+            expect(editor.scrollTop()).toBe(editor.lineHeight * 2)
+
+            _.times 3, -> editor.moveCursorUp()
+
+            editor.moveCursorUp()
+            expect(editor.scrollTop()).toBe(editor.lineHeight)
+
+            editor.moveCursorUp()
+            expect(editor.scrollTop()).toBe(0)
+
+        describe "when the editor is shorter than twice the vertical scroll margin", ->
+          it "sets the scrollTop based on a reduced scroll margin, which prevents a jerky tug-of-war between upper and lower scroll margins", ->
+            setEditorHeightInLines(editor, 5)
+
+            _.times 3, -> editor.moveCursorDown()
+
+            expect(editor.scrollTop()).toBe(editor.lineHeight)
+
+            editor.moveCursorUp()
+            expect(editor.renderedLines.css('top')).toBe "0px"
+
+      describe "when the last cursor exceeds the right or left scroll margins", ->
+        describe "when soft-wrap is disabled", ->
+          describe "when the editor is wider than twice the horizontal scroll margin", ->
+            it "sets the scrollView's scrollLeft so the cursor remains within the scroll margin", ->
+              setEditorWidthInChars(editor, 30)
+
+              # moving right
+              editor.setCursorScreenPosition([2, 24])
+              window.advanceClock()
+              expect(editor.scrollView.scrollLeft()).toBe 0
+
+              editor.setCursorScreenPosition([2, 25])
+              window.advanceClock()
+              expect(editor.scrollView.scrollLeft()).toBe charWidth
+
+              editor.setCursorScreenPosition([2, 28])
+              window.advanceClock()
+              expect(editor.scrollView.scrollLeft()).toBe charWidth * 4
+
+              # moving left
+              editor.setCursorScreenPosition([2, 9])
+              window.advanceClock()
+              expect(editor.scrollView.scrollLeft()).toBe charWidth * 4
+
+              editor.setCursorScreenPosition([2, 8])
+              window.advanceClock()
+              expect(editor.scrollView.scrollLeft()).toBe charWidth * 3
+
+              editor.setCursorScreenPosition([2, 5])
+              window.advanceClock()
+              expect(editor.scrollView.scrollLeft()).toBe 0
+
+          describe "when the editor is narrower than twice the horizontal scroll margin", ->
+            it "sets the scrollView's scrollLeft based on a reduced horizontal scroll margin, to prevent a jerky tug-of-war between right and left scroll margins", ->
+              editor.hScrollMargin = 6
+              setEditorWidthInChars(editor, 7)
+
+              editor.setCursorScreenPosition([2, 3])
+              window.advanceClock()
+              expect(editor.scrollView.scrollLeft()).toBe(0)
+
+              editor.setCursorScreenPosition([2, 4])
+              window.advanceClock()
+              expect(editor.scrollView.scrollLeft()).toBe(charWidth)
+
+              editor.setCursorScreenPosition([2, 3])
+              window.advanceClock()
+              expect(editor.scrollView.scrollLeft()).toBe(0)
+
+        describe "when soft-wrap is enabled", ->
+          beforeEach ->
+            editor.setSoftWrap(true)
+
+          it "does not scroll the buffer horizontally", ->
+            editor.width(charWidth * 30)
+
+            # moving right
+            editor.setCursorScreenPosition([2, 24])
+            expect(editor.scrollView.scrollLeft()).toBe 0
+
+            editor.setCursorScreenPosition([2, 25])
+            expect(editor.scrollView.scrollLeft()).toBe 0
+
+            editor.setCursorScreenPosition([2, 28])
+            expect(editor.scrollView.scrollLeft()).toBe 0
+
+            # moving left
+            editor.setCursorScreenPosition([2, 9])
+            expect(editor.scrollView.scrollLeft()).toBe 0
+
+            editor.setCursorScreenPosition([2, 8])
+            expect(editor.scrollView.scrollLeft()).toBe 0
+
+            editor.setCursorScreenPosition([2, 5])
+            expect(editor.scrollView.scrollLeft()).toBe 0

   describe "buffer manipulation", ->
     beforeEach ->


:construction: Last attempt to fix travis build

--- spec/minimap-v4-spec.coffee
+++ spec/minimap-v4-spec.coffee
@@ -6,6 +6,7 @@

   beforeEach ->
     atom.config.set 'minimap.v4Preview', true
+    atom.config.set 'minimap.autoToggle', true

     workspaceElement = atom.views.getView(atom.workspace)


Added ability to log out from plugin

Added ability to log out from plugin

--- src/annotator.readmill.coffee
+++ src/annotator.readmill.coffee
@@ -19,6 +19,7 @@
     @client = new Readmill.Client @options

     @view.subscribe "connect", @connect
+    @view.subscribe "disconnect", @disconnect

     token = options.accessToken || @store.get "access-token"
     @connected(token, silent: true) if token
@@ -58,6 +59,10 @@

     unless options?.silent is true
       Annotator.showNotification "Successfully connected to Readmill"
+
+  disconnect: =>
+    @client.deauthorize()
+    @store.remove "access-token"

   error: (message) ->
     Annotator.showNotification message, Annotator.Notification.ERROR
@@ -133,29 +138,27 @@
     loggedIn: "annotator-readmill-logged-in"

   template: """
-

-
-
-
-

-
-
-

-

-

-      Connect with Readmill
-

-

-      Log Out
-

+
+
+
+

+
+
+

+

+

+    Connect with Readmill
+

+

+    Log Out

   """

   constructor: () ->
-    super jQuery(@template)
+    super jQuery("
").html(@template)

   connect: ->
-    this.publish "connect", this
+    @publish "connect", [this]

   login: (user) ->
     @updateUser(user) if user
@@ -163,15 +166,12 @@
     this

   logout: ->
-    newElement = jQuery(@template)
-
-    @element.replaceWith newElement
-    @element = newElement
+    @element.removeClass(@classes.loggedIn).html(@template)

     @user = null
-    @updateBook @book
-
-    this.publish "disconnect", this
+    @updateBook()
+
+    @publish "disconnect", [this]

   updateUser: (@user=@user) ->
     if @user
@@ -271,6 +271,8 @@

   authorize: (@accessToken) ->

+  deauthorize: -> @accessToken = null
+
   isAuthorized: -> !!@accessToken

 class Store


:lipstick:

:lipstick:

--- src/app/display-buffer.coffee
+++ src/app/display-buffer.coffee
@@ -35,7 +35,8 @@

   copy: ->
     newDisplayBuffer = new DisplayBuffer(@buffer, tabLength: @getTabLength())
-    @findMarkers(displayBufferId: @id).map (marker) -> marker.copy(displayBufferId: newDisplayBuffer.id)
+    for marker in @findMarkers(displayBufferId: @id)
+      marker.copy(displayBufferId: newDisplayBuffer.id)
     newDisplayBuffer

   updateAllScreenLines: ->
@@ -404,8 +405,7 @@
   #
   # Returns the {DisplayBufferMarker} (if it exists).
   getMarker: (id) ->
-    marker = @markers[id]
-    unless marker?
+    unless marker = @markers[id]
       if bufferMarker = @buffer.getMarker(id)
         marker = new DisplayBufferMarker({bufferMarker, displayBuffer: this})
         @markers[id] = marker


offtopic: improve error message on required transform

--- src/coffee/transformer.coffee
+++ src/coffee/transformer.coffee
@@ -134,7 +134,7 @@
     if @_disabled or util.nonEmpty(value)
       Q(value)
     else
-      Q.reject new Error("Value is empty.")
+      Q.reject new Error("Required Value is empty.")

 class UpperCaseTransformer extends ValueTransformer
   @create: (transformers, options) ->


give non-users a signup url

give non-users a signup url

--- js/app.coffee
+++ js/app.coffee
@@ -125,9 +125,11 @@
     @signupUrl = @komp =>
       CI.github.authUrl(@ab().oauth_scopes())

-    # TODO: try to give non-users a signupUrl
     @loginUrl = @komp =>
-      CI.github.authUrl(false)
+      if @is_existing_user()
+        CI.github.authUrl(false)
+      else
+        @signupUrl()

   refreshBuildState: () =>


Shader fixes.

Shader fixes.

--- packages/landsofillusions-assets/engine/mesh/rampmaterial.coffee
+++ packages/landsofillusions-assets/engine/mesh/rampmaterial.coffee
@@ -43,23 +43,29 @@
 void main()	{
   vec3 sourceColor = shades[#{@options.shadeIndex}];

-  // Shade color based on the normal.
-  float lightIntensity = 0.0;
+  // Accumulate directional lights.
+  float totalLightIntensity = 0.0;

   DirectionalLight directionalLight;
+  float lightIntensity;

   for (int i = 0; i < NUM_DIR_LIGHTS; i++) {
     directionalLight = directionalLights[i];

     // Shade using Lambert cosine law.
-    lightIntensity += dot(directionalLight.direction, normalEye);
+    lightIntensity = dot(directionalLight.direction, normalEye);

     // Apply shadow map.
     lightIntensity *= getShadow(directionalShadowMap[i], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[0]);
+
+    // Add to total intensity.
+    totalLightIntensity += lightIntensity;
   }

-  float shadeFactor = ambientLightColor.r + 0.6 * clamp(lightIntensity, 0.0, 1.0);
+  // Shade from ambient to full light based on intensity.
+  float shadeFactor = mix(ambientLightColor.r, 1.0, lightIntensity);

+  // Dim the color of the cluster by the shade factor.
   vec3 shadedColor = sourceColor * shadeFactor;

   // Find the nearest color from the palette to represent the shaded color.
@@ -84,6 +90,7 @@
     }
   }

+  // Color the pixel with the best match from the palette.
   gl_FragColor = vec4(bestColor, 1);
 }
 """


Don't use deprecated APIs

Don't use deprecated APIs

--- spec/status-bar-spec.coffee
+++ spec/status-bar-spec.coffee
@@ -10,13 +10,17 @@
   beforeEach ->
     atom.workspaceView = new WorkspaceView
     atom.workspace = atom.workspaceView.model
-    atom.workspaceView.openSync('sample.js')
-    atom.workspaceView.simulateDomAttachment()
-    StatusBar.activate()
-    editorView = atom.workspaceView.getActiveView()
-    editor = editorView.getEditor()
-    statusBar = atom.workspaceView.find('.status-bar').view()
-    buffer = editor.getBuffer()
+
+    waitsForPromise ->
+      atom.workspace.open('sample.js')
+
+    runs ->
+      atom.workspaceView.simulateDomAttachment()
+      StatusBar.activate()
+      editorView = atom.workspaceView.getActiveView()
+      editor = editorView.getEditor()
+      statusBar = atom.workspaceView.find('.status-bar').view()
+      buffer = editor.getBuffer()

   describe "@initialize", ->
     it "appends only one status bar", ->
@@ -32,11 +36,14 @@

     describe "when associated with an unsaved buffer", ->
       it "displays 'untitled' instead of the buffer's path, but still displays the buffer position", ->
-        atom.workspaceView.openSync()
-        StatusBar.activate()
-        statusBar = atom.workspaceView.find('.status-bar').view()
-        expect(StatusBar.fileInfo.currentPath.text()).toBe 'untitled'
-        expect(StatusBar.cursorPosition.text()).toBe '1,1'
+        waitsForPromise ->
+          atom.workspace.open()
+
+        runs ->
+          StatusBar.activate()
+          statusBar = atom.workspaceView.find('.status-bar').view()
+          expect(StatusBar.fileInfo.currentPath.text()).toBe 'untitled'
+          expect(StatusBar.cursorPosition.text()).toBe '1,1'

   describe ".deactivate()", ->
     it "removes the StatusBarView", ->
@@ -56,8 +63,11 @@

   describe "when the associated editor's path changes", ->
     it "updates the path in the status bar", ->
-      atom.workspaceView.openSync('sample.txt')
-      expect(StatusBar.fileInfo.currentPath.text()).toBe 'sample.txt'
+      waitsForPromise ->
+        atom.workspace.open('sample.txt')
+
+      runs ->
+        expect(StatusBar.fileInfo.currentPath.text()).toBe 'sample.txt'

   describe "when the associated editor's buffer's content changes", ->
     it "enables the buffer modified indicator", ->
@@ -71,14 +81,18 @@
     it "disables the buffer modified indicator on save", ->
       filePath = path.join(os.tmpdir(), "atom-whitespace.txt")
       fs.writeFileSync(filePath, "")
-      atom.workspaceView.openSync(filePath)
-      editor = atom.workspace.getActivePaneItem()
-      expect(StatusBar.fileInfo.bufferModified.text()).toBe ''
-      editor.insertText("\n")
-      advanceClock(buffer.stoppedChangingDelay)
-      expect(StatusBar.fileInfo.bufferModified.text()).toBe '*'
-      editor.getBuffer().save()
-      expect(StatusBar.fileInfo.bufferModified.text()).toBe ''
+
+      waitsForPromise ->
+        atom.workspace.open(filePath)
+
+      runs ->
+        editor = atom.workspace.getActiveEditor()
+        expect(StatusBar.fileInfo.bufferModified.text()).toBe ''
+        editor.insertText("\n")
+        advanceClock(buffer.stoppedChangingDelay)
+        expect(StatusBar.fileInfo.bufferModified.text()).toBe '*'
+        editor.getBuffer().save()
+        expect(StatusBar.fileInfo.bufferModified.text()).toBe ''

     it "disables the buffer modified indicator if the content matches again", ->
       expect(StatusBar.fileInfo.bufferModified.text()).toBe ''
@@ -101,19 +115,27 @@
   describe "when the buffer changes", ->
     it "updates the buffer modified indicator for the new buffer", ->
       expect(StatusBar.fileInfo.bufferModified.text()).toBe ''
-      atom.workspaceView.openSync('sample.txt')
-      editor = atom.workspace.getActivePaneItem()
-      editor.insertText("\n")
-      advanceClock(buffer.stoppedChangingDelay)
-      expect(StatusBar.fileInfo.bufferModified.text()).toBe '*'
+
+      waitsForPromise ->
+        atom.workspace.open('sample.txt')
+
+      runs ->
+        editor = atom.workspace.getActiveEditor()
+        editor.insertText("\n")
+        advanceClock(buffer.stoppedChangingDelay)
+        expect(StatusBar.fileInfo.bufferModified.text()).toBe '*'

     it "doesn't update the buffer modified indicator for the old buffer", ->
       oldBuffer = editor.getBuffer()
       expect(StatusBar.fileInfo.bufferModified.text()).toBe ''
-      atom.workspaceView.openSync('sample.txt')
-      oldBuffer.setText("new text")
-      advanceClock(buffer.stoppedChangingDelay)
-      expect(StatusBar.fileInfo.bufferModified.text()).toBe ''
+
+      waitsForPromise ->
+        atom.workspace.open('sample.txt')
+
+      runs ->
+        oldBuffer.setText("new text")
+        advanceClock(buffer.stoppedChangingDelay)
+        expect(StatusBar.fileInfo.bufferModified.text()).toBe ''

   describe "when the associated editor's cursor position changes", ->
     it "updates the cursor position in the status bar", ->
@@ -129,26 +151,37 @@

     it "displays the current branch for files in repositories", ->
       atom.project.setPath(atom.project.resolve('git/master.git'))
-      atom.workspaceView.openSync('HEAD')
-      expect(StatusBar.git.branchArea).toBeVisible()
-      expect(StatusBar.git.branchLabel.text()).toBe 'master'
-
-      atom.workspaceView.getActivePaneView().destroyItems()
-      expect(StatusBar.git.branchArea).toBeVisible()
-      expect(StatusBar.git.branchLabel.text()).toBe 'master'
-
-      view = $$ -> @div id: 'view', tabindex: -1, 'View'
-      atom.workspaceView.getActivePaneView().showItem(view)
+
+      waitsForPromise ->
+        atom.workspace.open('HEAD')
+
+      runs ->
+        expect(StatusBar.git.branchArea).toBeVisible()
+        expect(StatusBar.git.branchLabel.text()).toBe 'master'
+
+        atom.workspaceView.getActivePaneView().destroyItems()
+        expect(StatusBar.git.branchArea).toBeVisible()
+        expect(StatusBar.git.branchLabel.text()).toBe 'master'
+
+      view = $$ -> @div id: 'view', tabindex: -1, 'View'
+      atom.workspaceView.getActivePaneView().activateItem(view)
       expect(StatusBar.git.branchArea).not.toBeVisible()

     it "doesn't display the current branch for a file not in a repository", ->
       atom.project.setPath(os.tmpdir())
-      atom.workspaceView.openSync(path.join(os.tmpdir(), 'temp.txt'))
-      expect(StatusBar.git.branchArea).toBeHidden()
+
+      waitsForPromise ->
+        atom.workspace.open(path.join(os.tmpdir(), 'temp.txt'))
+
+      runs ->
+        expect(StatusBar.git.branchArea).toBeHidden()

     it "doesn't display the current branch for a file outside the current project", ->
-      atom.workspaceView.openSync(path.join(os.tmpdir(), 'atom-specs', 'not-in-project.txt'))
-      expect(StatusBar.git.branchArea).toBeHidden()
+      waitsForPromise ->
+        atom.workspace.open(path.join(os.tmpdir(), 'atom-specs', 'not-in-project.txt'))
+
+      runs ->
+        expect(StatusBar.git.branchArea).toBeHidden()

   describe "git status label", ->
     [repo, filePath, originalPathText, newPath, ignorePath, ignoredPath, projectPath] = []
@@ -179,49 +212,76 @@
     it "displays the modified icon for a changed file", ->
       fs.writeFileSync(filePath, "i've changed for the worse")
       atom.project.getRepo().getPathStatus(filePath)
-      atom.workspaceView.openSync(filePath)
-      expect(StatusBar.git.gitStatusIcon).toHaveClass('icon-diff-modified')
+
+      waitsForPromise ->
+        atom.workspace.open(filePath)
+
+      runs ->
+        expect(StatusBar.git.gitStatusIcon).toHaveClass('icon-diff-modified')

     it "doesn't display the modified icon for an unchanged file", ->
-      atom.workspaceView.openSync(filePath)
-      expect(StatusBar.git.gitStatusIcon).toHaveText('')
+      waitsForPromise ->
+        atom.workspace.open(filePath)
+
+      runs ->
+        expect(StatusBar.git.gitStatusIcon).toHaveText('')

     it "displays the new icon for a new file", ->
-      atom.workspaceView.openSync(newPath)
-      expect(StatusBar.git.gitStatusIcon).toHaveClass('icon-diff-added')
+      waitsForPromise ->
+        atom.workspace.open(newPath)
+
+      runs ->
+        expect(StatusBar.git.gitStatusIcon).toHaveClass('icon-diff-added')

     it "displays the ignored icon for an ignored file", ->
-      atom.workspaceView.openSync(ignoredPath)
-      expect(StatusBar.git.gitStatusIcon).toHaveClass('icon-diff-ignored')
+      waitsForPromise ->
+        atom.workspace.open(ignoredPath)
+
+      runs ->
+        expect(StatusBar.git.gitStatusIcon).toHaveClass('icon-diff-ignored')

     it "updates when a status-changed event occurs", ->
       fs.writeFileSync(filePath, "i've changed for the worse")
       atom.project.getRepo().getPathStatus(filePath)
-      atom.workspaceView.openSync(filePath)
-      expect(StatusBar.git.gitStatusIcon).toHaveClass('icon-diff-modified')
-      fs.writeFileSync(filePath, originalPathText)
-      atom.project.getRepo().getPathStatus(filePath)
-      expect(StatusBar.git.gitStatusIcon).not.toHaveClass('icon-diff-modified')
+
+      waitsForPromise ->
+        atom.workspace.open(filePath)
+
+      runs ->
+        expect(StatusBar.git.gitStatusIcon).toHaveClass('icon-diff-modified')
+        fs.writeFileSync(filePath, originalPathText)
+        atom.project.getRepo().getPathStatus(filePath)
+        expect(StatusBar.git.gitStatusIcon).not.toHaveClass('icon-diff-modified')

     it "displays the diff stat for modified files", ->
       fs.writeFileSync(filePath, "i've changed for the worse")
       atom.project.getRepo().getPathStatus(filePath)
-      atom.workspaceView.openSync(filePath)
-      expect(StatusBar.git.gitStatusIcon).toHaveText('+1')
+
+      waitsForPromise ->
+        atom.workspace.open(filePath)
+
+      runs ->
+        expect(StatusBar.git.gitStatusIcon).toHaveText('+1')

     it "displays the diff stat for new files", ->
-      atom.workspaceView.openSync(newPath)
-      expect(StatusBar.git.gitStatusIcon).toHaveText('+1')
+      waitsForPromise ->
+        atom.workspace.open(newPath)
+
+      runs ->
+        expect(StatusBar.git.gitStatusIcon).toHaveText('+1')

     it "does not display for files not in the current project", ->
-      atom.workspaceView.openSync('/tmp/atom-specs/not-in-project.txt')
-      expect(StatusBar.git.gitStatusIcon).toBeHidden()
+      waitsForPromise ->
+        atom.workspace.open('/tmp/atom-specs/not-in-project.txt')
+
+      runs ->
+        expect(StatusBar.git.gitStatusIcon).toBeHidden()

   describe "when the active item view does not implement getCursorBufferPosition()", ->
     it "hides the cursor position view", ->
       atom.workspaceView.attachToDom()
       view = $$ -> @div id: 'view', tabindex: -1, 'View'
-      editorView.getPane().showItem(view)
+      editorView.getPane().activateItem(view)
       expect(StatusBar.cursorPosition).toBeHidden()

   describe "when the active item implements getTitle() but not getPath()", ->
@@ -229,7 +289,7 @@
       atom.workspaceView.attachToDom()
       view = $$ -> @div id: 'view', tabindex: -1, 'View'
       view.getTitle = => 'View Title'
-      editorView.getPane().showItem(view)
+      editorView.getPane().activateItem(view)
       expect(StatusBar.fileInfo.currentPath.text()).toBe 'View Title'
       expect(StatusBar.fileInfo.currentPath).toBeVisible()

@@ -237,7 +297,7 @@
     it "hides the path view", ->
       atom.workspaceView.attachToDom()
       view = $$ -> @div id: 'view', tabindex: -1, 'View'
-      editorView.getPane().showItem(view)
+      editorView.getPane().activateItem(view)
       expect(StatusBar.fileInfo.currentPath).toBeHidden()

   describe "when the active item's title changes", ->
@@ -245,7 +305,7 @@
       atom.workspaceView.attachToDom()
       view = $$ -> @div id: 'view', tabindex: -1, 'View'
       view.getTitle = => 'View Title'
-      editorView.getPane().showItem(view)
+      editorView.getPane().activateItem(view)
       expect(StatusBar.fileInfo.currentPath.text()).toBe 'View Title'
       view.getTitle = => 'New Title'
       view.trigger 'title-changed'


Fix specs to work with latest editor component

The status bar should really be subscribing to events on the model, not
the view. In order for 'selection:changed' to be emitted by the new
editor, it needs to be on the DOM so the selection lands in the visible
row range.

--- spec/status-bar-spec.coffee
+++ spec/status-bar-spec.coffee
@@ -153,14 +153,16 @@
     it "updates the cursor position in the status bar", ->
       atom.workspaceView.attachToDom()
       editor.setCursorScreenPosition([1, 2])
-      editorView.updateDisplay()
       expect(StatusBar.cursorPosition.text()).toBe '2,3'

   describe "when the associated editor's selection changes", ->
     it "updates the selection count in the status bar", ->
       atom.workspaceView.attachToDom()
+      editorView.height(100)
+      editorView.component.checkForVisibilityChange()
+
+      atom.workspaceView.attachToDom()
       editor.setSelectedBufferRange([[0, 0], [0, 2]])
-      editorView.updateDisplay()
       expect(StatusBar.selectionCount.text()).toBe '(2)'

   describe "git branch label", ->


Give specs better names. Remove unnecessary specs.

--- spec/whitespace-spec.coffee
+++ spec/whitespace-spec.coffee
@@ -52,7 +52,7 @@
     editor.save()
     expect(editor.getText()).toBe "don't trim me \n"

-  describe "whitespace.ignoreWhitespaceOnCurrentLine config", ->
+  describe "when 'whitespace.ignoreWhitespaceOnCurrentLine' is true", ->
     [originalConfigValue] = []
     beforeEach ->
       originalConfigValue = atom.config.get("whitespace.ignoreWhitespaceOnCurrentLine")
@@ -61,31 +61,27 @@
     afterEach ->
       atom.config.set("whitespace.ignoreWhitespaceOnCurrentLine", originalConfigValue)

-    it "doesn't trim whitespace on current line", ->
+    it "removes the whitespace from all lines, excluding the current line", ->
       editor.insertText "  \n-  \n"
       editor.setCursorBufferPosition([0,6])
       editor.save()
       expect(editor.getText()).toBe "\n-  \n"

-    it "doesn't trim whitespace on current line when just whitespace", ->
-      editor.insertText "  "
-      editor.setCursorBufferPosition([0,2])
-      editor.save()
-      expect(editor.getText()).toBe "  \n"
-
-    it "does trim whitespace on other lines", ->
-      editor.insertText "  \n  "
-      editor.setCursorBufferPosition([0,2])
-      editor.save()
-      expect(editor.getText()).toBe "  \n"
-
-    it "trims whitespace on current line if ignoreWhitespaceOnCurrentLine is false", ->
+  describe "when 'whitespace.ignoreWhitespaceOnCurrentLine' is false", ->
+    [originalConfigValue] = []
+    beforeEach ->
+      originalConfigValue = atom.config.get("whitespace.ignoreWhitespaceOnCurrentLine")
+      expect(originalConfigValue).toBe true
       atom.config.set("whitespace.ignoreWhitespaceOnCurrentLine", false)

-      editor.insertText "  "
-      editor.setCursorBufferPosition([0,2])
+    afterEach ->
+      atom.config.set("whitespace.ignoreWhitespaceOnCurrentLine", originalConfigValue)
+
+    it "removes the whitespace from all lines, including the current line", ->
+      editor.insertText "  \n-  \n"
+      editor.setCursorBufferPosition([0,6])
       editor.save()
-      expect(editor.getText()).toBe "\n"
+      expect(editor.getText()).toBe "\n-\n"

   describe "whitespace.ensureSingleTrailingNewline config", ->
     [originalConfigValue] = []


DevTools: add ToggleLiveReloadMenuView for context menu

--- client/DevTools/AppView.coffee
+++ client/DevTools/AppView.coffee
@@ -8,6 +8,24 @@
     @storage = KD.singletons.localStorageController.storage "DevTools"
     @liveMode = @storage.getAt 'liveMode'

+  getToggleLiveReloadMenuView: (item, menu)->
+
+    itemLabel = "#{if @liveMode then 'Disable' else 'Enable'} live compile"
+
+    toggleLiveReload = new KDView
+      partial : "<span>#{itemLabel}</span>"
+      click   : =>
+        @liveMode = !@liveMode
+        @storage.setValue 'liveMode', @liveMode
+
+        menu.contextMenu.destroy()
+
+        if @liveMode
+          KD.utils.defer =>
+            @previewApp yes; @previewCss yes
+
+    toggleLiveReload.on "viewAppended", ->
+      toggleLiveReload.parent.setClass "default"
   viewAppended:->

     @addSubView @workspace      = new CollaborativeWorkspace


use moment.js in intercomSettings code, to avoid Safari 5 ISO data parsing issues

--- client/code/router/main.coffee
+++ client/code/router/main.coffee
@@ -60,6 +60,7 @@
   trackIntercom: ->
     if 'real' of window.user and window.intercomUserHash != ''
       @getIntercomSettings (intercomSettings) ->
+        console.log intercomSettings
         if window.intercomBooted?
           # tell Intercom we're still here
           window.Intercom 'update', intercomSettings
@@ -89,7 +90,7 @@
               user_id: real.shortName
               name: real.displayName
               email: real.email[0]
-              created_at: Date.parse(real.created) / 1000
+              created_at: moment(real.created, 'YYYY-MM-DD HH:mm:ssZ').unix()
               accountLevel: real.accountLevel
               datahub_id: effective.shortName
               datahub_name: effective.displayName
@@ -103,7 +104,7 @@
               cb_datasets: 0

             _.each datasets, (dataset) ->
-              date = Date.parse(dataset.createdDate) / 1000
+              date = moment(dataset.createdDate, 'YYYY-MM-DD HH:mm:ssZ').unix()
               settings.dataset_created_at = Math.max date, settings.dataset_created_at
               if dataset.tool == 'table-xtract'
                 settings.tx_created_at = Math.max date, settings.tx_created_at


外部リソースすべてをコピーできるようにする

外部リソースすべてをコピーできるようにする

--- gulp/tasks/compile.coffee
+++ gulp/tasks/compile.coffee
@@ -45,7 +45,7 @@
   .pipe(gulp.dest(config.dest.package))

 gulp.task 'copy:resource', ->
-  gulp.src('resource/*') #, { base: 'resource' })
+  gulp.src('resource/**/*') #, { base: 'resource' })
   .pipe(gulp.dest(config.dest.package))

 gulp.task 'copy:css', ->


payment: update includes to reflect refactoring

--- client/landing/includes.coffee
+++ client/landing/includes.coffee
@@ -588,8 +588,9 @@
   "app/Applications/Dashboard.kdapplication/views/productsettingsview.coffee",
   "app/Applications/Dashboard.kdapplication/views/productsectionview.coffee",
   "app/Applications/Dashboard.kdapplication/views/productcreateform.coffee",
+  "app/Applications/Dashboard.kdapplication/views/embedcodeview.coffee",
   "app/Applications/Dashboard.kdapplication/views/productlistitem.coffee",
-  "app/Applications/Dashboard.kdapplication/views/productlistcontroller.coffee",
+  "app/Applications/Dashboard.kdapplication/views/productsectionlistcontroller.coffee",

   # CONTENT DISPLAY VIEWS


Add callback argument to message

Add callback argument to message

--- src/flowdock.coffee
+++ src/flowdock.coffee
@@ -65,12 +65,13 @@
       callback res if callback

   # Send a chat message to flowdock
-  message: (flow, message, tags) ->
+  message: (flow, message, tags, callback) ->
     data =
       event: 'message'
       content: message
       tags: tags || []
-    @send flow, data
+    @send flow, data, callback
+

   # Change status on Flowdock
   status: (flow, status) ->


Define totalIntensity

Define totalIntensity

Fixes #5

--- src/forecast.coffee
+++ src/forecast.coffee
@@ -167,6 +167,7 @@
       # in the weather

       mostIntenseDataPoint = {}
+      totalIntensity = 0

       dataPoints = json['minutely']['data']


Fix landing chart alignment

Fix landing chart alignment

Ensure no blank space at the bottom

--- resources/assets/coffee/_classes/landing-user-stats.coffee
+++ resources/assets/coffee/_classes/landing-user-stats.coffee
@@ -41,7 +41,8 @@

     @svg = @svgContainerInner
       .append 'g'
-      .attr 'transform', "translate(#{@margin.left}, #{@margin.top})"
+      # Ensure no blank space at the bottom at certain zoom level in Firefox.
+      .attr 'transform', "translate(#{@margin.left}, #{@margin.top + 1})"

     @svgArea = @svg
       .append 'path'


Return local package version instead of latest on npm

--- src/aotw.coffee
+++ src/aotw.coffee
@@ -49,11 +49,11 @@
         when "about"
           try
             @exec = require('child_process').exec
-            @exec 'npm view hubot-aotw version', (error, stdout, stderr) ->
+            @exec 'npm list hubot-aotw', (error, stdout, stderr) ->
               if error
                 msg.send "Unable to get version: " + stderr
               else output = stdout+''
-              msg.send "hubot-aotw v#{output}"
+              msg.send "#{output}"
           catch error
               msg.send "Unable to get version: " + error
         when "data"


avoid emitting when client not connected

the emit is happening asynchronously after the client list is computed,
so clients may have disconnected in the intervening time.

--- services/real-time/app/coffee/WebsocketLoadBalancer.coffee
+++ services/real-time/app/coffee/WebsocketLoadBalancer.coffee
@@ -96,6 +96,9 @@
 					, (client, cb) ->
 						Utils.getClientAttributes client, ['is_restricted_user'], (err, {is_restricted_user}) ->
 							return cb(err) if err?
+							if !client.connected
+								logger.warn {channel:channel, client: client.id}, "skipping emit, client not connected"
+								return cb()
 							if !seen[client.id]
 								seen[client.id] = true
 								if !(is_restricted_user && message.message not in RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST)


Fix slide adjustment on resize

Fix slide adjustment on resize

--- src/jquery.superslides.coffee
+++ src/jquery.superslides.coffee
@@ -45,7 +45,7 @@
       left: -width

   setupChildren = =>
-    $children.not('.current').css
+    $children.css
       display: 'none'
       position: 'absolute'
       overflow: 'hidden'
@@ -97,8 +97,8 @@
     $el.each (i) ->
       $(this).width(width).height(height)

-      # $(this).css
-      #   left: width
+      $(this).css
+        left: width

       adjustImagePosition $('img', this).not('.keep-original')


Update timeout to 5 seconds

Update timeout to 5 seconds

--- configs/test/jest.setup.coffee
+++ configs/test/jest.setup.coffee
@@ -25,6 +25,9 @@
 # Combine both jest and chai matchers on expect
 originalExpect = global.expect

+# bump up timeout to 5 seconds
+global.jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000
+
 global.expect = (actual) ->
   originalMatchers = originalExpect(actual)
   chaiMatchers = chai.expect(actual)


Remove getUserAgent. just use navigator.userAgent

--- src/atom.coffee
+++ src/atom.coffee
@@ -352,10 +352,6 @@
   getVersion: ->
     app.getVersion()

-  # Public: Gets the user agent of the atom instance.
-  getUserAgent: ->
-    "GitHubAtom/#{app.getVersion()}"
-
   # Public: Get the directory path to Atom's configuration area.
   #
   # Returns the absolute path to ~/.atom


Store included grammar scopes

Store included grammar scopes

--- src/atom.coffee
+++ src/atom.coffee
@@ -252,9 +252,19 @@

   storeGrammarsForOpenEditors: ->
     packageNames = []
-    for editor in @workspace.getEditors()
-      {packageName} = editor.getGrammar()
-      packageNames.push(packageName) if packageName
+    addGrammar = (grammar={}) ->
+      {includedGrammarScopes, packageName} = grammar
+      return unless packageName
+
+      # Prevent cycles
+      return if packageNames.indexOf(packageName) isnt -1
+
+      packageNames.push(packageName)
+      for scopeName in includedGrammarScopes ? []
+        console.log scopeName
+        addGrammar(atom.syntax.grammarForScopeName(scopeName))
+
+    addGrammar(editor.getGrammar()) for editor in @workspace.getEditors()
     @state.packagesWithActiveGrammars = _.uniq(packageNames)

   # Public: Get the load settings for the current window.


Use instance method

Use instance method

--- src/atom.coffee
+++ src/atom.coffee
@@ -226,7 +226,7 @@
       @center()

   storeDefaultWindowDimensions: ->
-    dimensions = JSON.stringify(atom.getWindowDimensions())
+    dimensions = JSON.stringify(@getWindowDimensions())
     localStorage.setItem("defaultWindowDimensions", dimensions)

   getDefaultWindowDimensions: ->


Integration tests for cobalt unix socket

--- integration_test/cobalt.coffee
+++ integration_test/cobalt.coffee
@@ -14,25 +14,33 @@
 boxname = 'cotest/' + String(Math.random()).replace(/\./,'')
 sshkey_pub_path =  "../swops-secret/cotest-rsa.pub"
 sshkey_prv_path =  "../swops-secret/cotest-rsa"
+sshkey_prv_path_root = "../swops-secret/id_dsa"
 fs.chmodSync sshkey_prv_path, 0o600

 ssh_args = [
-  "-o User=#{boxname}",
   "-o LogLevel=ERROR",
   "-o UserKnownHostsFile=/dev/null",
   "-o StrictHostKeyChecking=no",
   "-o IdentitiesOnly=yes",
   "-o PreferredAuthentications=publickey",
-  "-i #{sshkey_prv_path}"
 ]

+ssh_root_args = [ "-o User=root", "-i #{sshkey_prv_path_root}" ].concat(ssh_args)
+
+ssh_user_args = [ "-o User=#{boxname}", "-i #{sshkey_prv_path}" ].concat(ssh_args)
+
 ssh_cmd = (cmd, callback) ->
-  ssh = "ssh #{ssh_args.join ' '} #{host} '#{cmd}'"
+  ssh = "ssh #{ssh_user_args.join ' '} #{host} '#{cmd}'"
   exec ssh, (err, stdout, stderr) ->
    callback err, stdout, stderr

+ssh_cmd_root = (cmd, callback) ->
+  ssh = "ssh #{ssh_root_args.join ' '} #{host} '#{cmd}'"
+  exec ssh, (err, stdout, stderr) ->
+   callback err, stdout, stderr
+
 scp_cmd = (file_path, file_name, callback) ->
-  scp = "scp #{ssh_args.join ' '} #{file_path} #{host}:#{file_name}"
+  scp = "scp #{ssh_user_args.join ' '} #{file_path} #{host}:#{file_name}"
   exec scp, (err, stdout, stderr) ->
     callback err, stdout, stderr

@@ -267,3 +275,20 @@
         request.get { uri: "#{baseurl}/#{boxname}/sqlite/", qs: options.qs }, (err, r, body) ->
           r.should.have.status 200
           done()
+
+
+  describe 'When Cobalt has started', ->
+    before (done) ->
+      ssh_cmd_root "service cobalt stop && rm -f /var/run/cobalt.socket && service cobalt start", done
+
+    describe "the created unix socket", ->
+
+      it "is listened to by Cobalt", (done) ->
+        ssh_cmd_root "netstat -a | grep cobalt", (err, stdout, stderr) ->
+          stdout.should.include "/var/run/cobalt.socket"
+          done()
+
+      it "has the right permissions", (done) ->
+        ssh_cmd_root "stat -c \"%U %a\" /var/run/cobalt.socket", (err, stdout, stderr) ->
+          stdout.should.equal "www-data 600\n"
+          done()


automatic updatedAt and createdAt fields

--- src/base.coffee
+++ src/base.coffee
@@ -173,6 +173,52 @@
         obj.name.should.eql 'jakob'
         obj.age.should.eql 28
         obj.should.not.eql input
+      ).then ->
+        api.close(done)
+
+
+
+    it "should set 'createdAt' automatically, if requested", (done) ->
+      api = manikin.create()
+
+      model =
+        stuffz:
+          createdAtField: 'created__at__123'
+          fields:
+            name: 'string'
+            age: 'number'
+
+      saved = {}
+
+      now = new Date().getTime()
+
+      promise(api).connect(connectionData, model, noErr())
+      .post('stuffz', { name: 'jakob', age: 28 }, noErr())
+      .list('stuffz', {}, noErr (list) ->
+        list.map((x) -> x.created__at__123).should.be.between(now, now + 1000)
+      ).then ->
+        api.close(done)
+
+
+
+    it "should set 'updatedAt' automatically, if requested", (done) ->
+      api = manikin.create()
+
+      model =
+        stuffz:
+          updatedAtField: 'upd'
+          fields:
+            name: 'string'
+            age: 'number'
+
+      saved = {}
+
+      now = new Date().getTime()
+
+      promise(api).connect(connectionData, model, noErr())
+      .post('stuffz', { name: 'jakob', age: 28 }, noErr())
+      .list('stuffz', {}, noErr (list) ->
+        list.map((x) -> x.upd).should.be.between(now, now + 1000)
       ).then ->
         api.close(done)


changing allowed file types

changing allowed file types

--- services/web/public/coffee/ide/files/services/files.coffee
+++ services/web/public/coffee/ide/files/services/files.coffee
@@ -8,7 +8,7 @@
 			getTeXFiles: () ->
 				texFiles = []
 				ide.fileTreeManager.forEachEntity (entity, folder, path) ->
-					if entity.type == 'doc' && entity?.name?.match?(/.*\.(tex|sty|cls|dtx|ltx|def)/)
+					if entity.type == 'doc' && entity?.name?.match?(/.*\.(tex|txt|md)/)
 						cloned = _.clone(entity)
 						cloned.path = path
 						texFiles.push cloned


cleanup level

cleanup level

--- src/game.coffee
+++ src/game.coffee
@@ -46,10 +46,10 @@

 exports.game = (rx, query) ->
   game_dict = find_game query.name
-  level = query.level or 0
   my.assert game_dict.levels and _.isArray(game_dict.levels)
   world = god(rx, game_dict)

+  level = query.level or 0
   level_dict = create_level(game_dict, level)
   level_world = world.add_child level_dict
   for child in layout


Remove unnecessary line

Remove unnecessary line

--- src/property_validator.coffee
+++ src/property_validator.coffee
@@ -24,7 +24,6 @@
     while result = termRegex.exec(configString)
       term = result[0]
       if term == 'optional'
-        @isOptional = true
         @parent.removeRequiredProperty(@property)
       else if term.indexOf('array of ') == 0
         @validators.push('array')


fix save

fix save

--- src/notebook.coffee
+++ src/notebook.coffee
@@ -47,8 +47,6 @@
     is_clean = (cell) -> cell.editor.getValue() is '' and not cell.used
     visible = (cell) -> cell.visible
     identity = (cell) -> true
-
-

     DocumentComponent = React.createClass
       mixins: [React.ComponentListMixin]
@@ -298,7 +296,7 @@
         run cell
       # TODO does it make sense to use output cells here?
       previously_run: -> input_cell_at_offset(cell, -1).editor.getValue()
-      export_notebook: -> export_notebook cell
+      export_notebook: -> export_notebook notebook, cell
       get_input_value: (number) ->
         get_input_cell_by_number(notebook, number)?.editor.getValue()


Fixes #27

Fixes #27

--- src/jobs.coffee
+++ src/jobs.coffee
@@ -1,6 +1,8 @@
 mongo = require 'mongodb'
 path = require 'path'
-db = new mongo.Db "concrete_#{path.basename(process.cwd()).replace(/\./, "-")}", new mongo.Server('localhost', mongo.Connection.DEFAULT_PORT, {auto_reconnect: true}), {}
+dbName = path.basename(process.cwd()).replace(/\./, "-")
+
+db = new mongo.Db "concrete_#{ dbName }", new mongo.Server('localhost', mongo.Connection.DEFAULT_PORT, {auto_reconnect: true}), {}
 db.open (error) ->
     if error
       console.log 'There was an error creating a connection with the Mongo database. Please check that MongoDB is properly installed and running.'.red


fix pointer bug

fix pointer bug

--- src/leaf.coffee
+++ src/leaf.coffee
@@ -13,7 +13,8 @@
     return node.childNodes.length == 1 && node.firstChild.nodeType == node.TEXT_NODE || node.tagName == 'BR'

-  constructor: (@line, @node, @attributes) ->
+  constructor: (@line, @node, attributes) ->
+    @attributes = _.clone(attributes)
     @text = @node.textContent
     @length = @text.length