compilation fixed in #45

compilation fixed in #45

--- src/server/models/ComputePersonal.coffee
+++ src/server/models/ComputePersonal.coffee
@@ -4,105 +4,103 @@
 GAME_RESULT = require('./constants').GAME_RESULT
 rating = require('./rating_formula_342')

-handleError(err)->
+module.exports = (playerName, done)->
+  handleError = (err)->
     if err
-        done(err, null)
-
-module.exports = (playerName, done)->
-    profile = {
-        "name": "",
-        "rating": 0,
-        "winsCitizen": 0,
-        "winsSheriff": 0,
-        "winsMafia": 0,
-        "winsDon": 0,
-        "gamesCitizen": 0,
-        "gamesSheriff": 0,
-        "gamesMafia": 0,
-        "gamesDon": 0,
-        "likesCitizen": 0,
-        "likesSheriff": 0,
-        "likesMafia": 0,
-        "likesDon": 0,
-        "bestPlayerCitizen": 0,
-        "bestPlayerSheriff": 0,
-        "bestPlayerMafia": 0,
-        "bestPlayerDon": 0,
-        "bestMoveAccuracy": 0.0,
-        "firstKilledNightCitizen": 0,
-        "firstKilledNightSheriff": 0,
-        "firstKilledNightMafia": 0,
-        "firstKilledNightDon": 0,
-        "firstKilledDayCitizen": 0,
-        "firstKilledDaySheriff": 0,
-        "firstKilledDayMafia": 0,
-        "firstKilledDayDon": 0,
-        "foulsCitizen": 0,
-        "foulsSheriff": 0,
-        "foulsMafia": 0,
-        "foulsDon": 0
-        "efficiency": []
-    }
-    db.Player.find({where: {name: playerName}}).success((player)->
-        db.PlayerGame.findAll({where: {PlayerId: player.id}}).success((playerGames)->
-            chainer = new Sequelize.Utils.QueryChainer
-            for playerGame in playerGames
-                chainer.add(playerGame, 'getGame', [])
-            chainer.runSerially().success((games)->
-                profile.name = playerName
-                bestMoveAttempts = 0
-                foundedMafiaOnBestMove = 0
-                for i in [0...playerGames.length]
-                    role = playerGames[i]
-                    result = games[i].result
-                    delta = 0
-                    if playerGame.role in [PLAYER_ROLES.CITIZEN, PLAYER_ROLES.SHERIFF]
-                        delta = if result == GAME_RESULT.CITIZENS_WIN then 1 else -1
-                    if playerGame.role in [PLAYER_ROLES.MAFIA, PLAYER_ROLES.DON]
-                        delta = if result == GAME_RESULT.MAFIA_WIN then 1 else -1
-                    profile.efficiency.push({
-                        gameID: games[i].id
-                        date: games[i].date
-                        winsMinusLosses: delta
-                    })
-                    profile.rating += rating(playerGame, result)
-                    profile.winsCitizen += 1 if result == GAME_RESULT.CITIZENS_WIN and playerGame.role == PLAYER_ROLES.CITIZEN
-                    profile.winsSheriff += 1 if result == GAME_RESULT.CITIZENS_WIN and playerGame.role == PLAYER_ROLES.SHERIFF
-                    profile.winsMafia += 1 if result == GAME_RESULT.MAFIA_WIN and playerGame.role == PLAYER_ROLES.MAFIA
-                    profile.winsDon += 1 if result == GAME_RESULT.MAFIA_WIN and playerGame.role == PLAYER_ROLES.DON
-                    profile.gamesCitizen += 1 if playerGame.role == PLAYER_ROLES.CITIZEN
-                    profile.gamesSheriff += 1 if playerGame.role == PLAYER_ROLES.SHERIFF
-                    profile.gamesMafia += 1 if playerGame.role == PLAYER_ROLES.MAFIA
-                    profile.gamesDon += 1 if playerGame.role == PLAYER_ROLES.DON
-                    profile.likesCitizen += playerGame.likes if playerGame.role == PLAYER_ROLES.CITIZEN
-                    profile.likesSheriff += playerGame.likes if playerGame.role == PLAYER_ROLES.SHERIFF
-                    profile.likesMafia += playerGame.likes if playerGame.role == PLAYER_ROLES.MAFIA
-                    profile.likesDon += playerGame.likes if playerGame.role == PLAYER_ROLES.DON
-                    profile.bestPlayerCitizen += 1 if playerGame.role == PLAYER_ROLES.CITIZEN and playerGame.is_best
-                    profile.bestPlayerSheriff += 1 if playerGame.role == PLAYER_ROLES.SHERIFF and playerGame.is_best
-                    profile.bestPlayerMafia += 1 if playerGame.role == PLAYER_ROLES.MAFIA and playerGame.is_best
-                    profile.bestPlayerDon += 1 if playerGame.role == PLAYER_ROLES.DON and playerGame.is_best
-                    bestMoveAttempts += 1 if playerGame.took_best_move
-                    foundedMafiaOnBestMove += playerGame.best_move_accuracy if playerGame.took_best_move
-                    profile.firstKilledNightCitizen += 1 if playerGame.role == PLAYER_ROLES.CITIZEN and playerGame.is_killed_first_at_night
-                    profile.firstKilledNightSheriff += 1 if playerGame.role == PLAYER_ROLES.SHERIFF and playerGame.is_killed_first_at_night
-                    profile.firstKilledNightMafia += 1 if playerGame.role == PLAYER_ROLES.MAFIA and playerGame.is_killed_first_at_night
-                    profile.firstKilledNightDon += 1 if playerGame.role == PLAYER_ROLES.DON and playerGame.is_killed_first_at_night
-                    profile.firstKilledDayCitizen += 1 if playerGame.role == PLAYER_ROLES.CITIZEN and playerGame.is_killed_first_by_day
-                    profile.firstKilledDaySheriff += 1 if playerGame.role == PLAYER_ROLES.SHERIFF and playerGame.is_killed_first_by_day
-                    profile.firstKilledDayMafia += 1 if playerGame.role == PLAYER_ROLES.MAFIA and playerGame.is_killed_first_by_day
-                    profile.firstKilledDayDon += 1 if playerGame.role == PLAYER_ROLES.DON and playerGame.is_killed_first_by_day
-                    profile.foulsCitizen += playerGame.fouls if playerGame.role == PLAYER_ROLES.CITIZEN
-                    profile.foulsSheriff += playerGame.fouls if playerGame.role == PLAYER_ROLES.SHERIFF
-                    profile.foulsMafia += playerGame.fouls if playerGame.role == PLAYER_ROLES.MAFIA
-                    profile.foulsDon += playerGame.fouls if playerGame.role == PLAYER_ROLES.DON
-                profile.bestMoveAccuracy = if bestMoveAttempts then foundedMafiaOnBestMove / bestMoveAttempts else 0.0
-                profile.efficiency.sort((a, b)->
-                    return if a.date > b.date then 1 else -1
-                )
-                for i in [1...playerGames.length]
-                    profile.efficiency[i].winsMinusLosses += profile.efficiency[i - 1].winsMinusLosses
-                done(null, profile)
-            ).error(handleError)
-        ).error(handleError)
+      done(err, null)
+  profile = {
+    "name": "",
+    "rating": 0,
+    "winsCitizen": 0,
+    "winsSheriff": 0,
+    "winsMafia": 0,
+    "winsDon": 0,
+    "gamesCitizen": 0,
+    "gamesSheriff": 0,
+    "gamesMafia": 0,
+    "gamesDon": 0,
+    "likesCitizen": 0,
+    "likesSheriff": 0,
+    "likesMafia": 0,
+    "likesDon": 0,
+    "bestPlayerCitizen": 0,
+    "bestPlayerSheriff": 0,
+    "bestPlayerMafia": 0,
+    "bestPlayerDon": 0,
+    "bestMoveAccuracy": 0.0,
+    "firstKilledNightCitizen": 0,
+    "firstKilledNightSheriff": 0,
+    "firstKilledNightMafia": 0,
+    "firstKilledNightDon": 0,
+    "firstKilledDayCitizen": 0,
+    "firstKilledDaySheriff": 0,
+    "firstKilledDayMafia": 0,
+    "firstKilledDayDon": 0,
+    "foulsCitizen": 0,
+    "foulsSheriff": 0,
+    "foulsMafia": 0,
+    "foulsDon": 0
+    "efficiency": []
+  }
+  db.Player.find({where: {name: playerName}}).success((player)->
+    db.PlayerGame.findAll({where: {PlayerId: player.id}}).success((playerGames)->
+      chainer = new Sequelize.Utils.QueryChainer
+      for playerGame in playerGames
+        chainer.add(playerGame, 'getGame', [])
+      chainer.runSerially().success((games)->
+        profile.name = playerName
+        bestMoveAttempts = 0
+        foundedMafiaOnBestMove = 0
+        for i in [0...playerGames.length]
+          result = games[i].result
+          delta = 0
+          if playerGame.role in [PLAYER_ROLES.CITIZEN, PLAYER_ROLES.SHERIFF]
+            delta = if result == GAME_RESULT.CITIZENS_WIN then 1 else -1
+          if playerGame.role in [PLAYER_ROLES.MAFIA, PLAYER_ROLES.DON]
+            delta = if result == GAME_RESULT.MAFIA_WIN then 1 else -1
+          profile.efficiency.push({
+            gameID: games[i].id
+            date: games[i].date
+            winsMinusLosses: delta
+          })
+          profile.rating += rating(playerGame, result)
+          profile.winsCitizen += 1 if result == GAME_RESULT.CITIZENS_WIN and playerGame.role == PLAYER_ROLES.CITIZEN
+          profile.winsSheriff += 1 if result == GAME_RESULT.CITIZENS_WIN and playerGame.role == PLAYER_ROLES.SHERIFF
+          profile.winsMafia += 1 if result == GAME_RESULT.MAFIA_WIN and playerGame.role == PLAYER_ROLES.MAFIA
+          profile.winsDon += 1 if result == GAME_RESULT.MAFIA_WIN and playerGame.role == PLAYER_ROLES.DON
+          profile.gamesCitizen += 1 if playerGame.role == PLAYER_ROLES.CITIZEN
+          profile.gamesSheriff += 1 if playerGame.role == PLAYER_ROLES.SHERIFF
+          profile.gamesMafia += 1 if playerGame.role == PLAYER_ROLES.MAFIA
+          profile.gamesDon += 1 if playerGame.role == PLAYER_ROLES.DON
+          profile.likesCitizen += playerGame.likes if playerGame.role == PLAYER_ROLES.CITIZEN
+          profile.likesSheriff += playerGame.likes if playerGame.role == PLAYER_ROLES.SHERIFF
+          profile.likesMafia += playerGame.likes if playerGame.role == PLAYER_ROLES.MAFIA
+          profile.likesDon += playerGame.likes if playerGame.role == PLAYER_ROLES.DON
+          profile.bestPlayerCitizen += 1 if playerGame.role == PLAYER_ROLES.CITIZEN and playerGame.is_best
+          profile.bestPlayerSheriff += 1 if playerGame.role == PLAYER_ROLES.SHERIFF and playerGame.is_best
+          profile.bestPlayerMafia += 1 if playerGame.role == PLAYER_ROLES.MAFIA and playerGame.is_best
+          profile.bestPlayerDon += 1 if playerGame.role == PLAYER_ROLES.DON and playerGame.is_best
+          bestMoveAttempts += 1 if playerGame.took_best_move
+          foundedMafiaOnBestMove += playerGame.best_move_accuracy if playerGame.took_best_move
+          profile.firstKilledNightCitizen += 1 if playerGame.role == PLAYER_ROLES.CITIZEN and playerGame.is_killed_first_at_night
+          profile.firstKilledNightSheriff += 1 if playerGame.role == PLAYER_ROLES.SHERIFF and playerGame.is_killed_first_at_night
+          profile.firstKilledNightMafia += 1 if playerGame.role == PLAYER_ROLES.MAFIA and playerGame.is_killed_first_at_night
+          profile.firstKilledNightDon += 1 if playerGame.role == PLAYER_ROLES.DON and playerGame.is_killed_first_at_night
+          profile.firstKilledDayCitizen += 1 if playerGame.role == PLAYER_ROLES.CITIZEN and playerGame.is_killed_first_by_day
+          profile.firstKilledDaySheriff += 1 if playerGame.role == PLAYER_ROLES.SHERIFF and playerGame.is_killed_first_by_day
+          profile.firstKilledDayMafia += 1 if playerGame.role == PLAYER_ROLES.MAFIA and playerGame.is_killed_first_by_day
+          profile.firstKilledDayDon += 1 if playerGame.role == PLAYER_ROLES.DON and playerGame.is_killed_first_by_day
+          profile.foulsCitizen += playerGame.fouls if playerGame.role == PLAYER_ROLES.CITIZEN
+          profile.foulsSheriff += playerGame.fouls if playerGame.role == PLAYER_ROLES.SHERIFF
+          profile.foulsMafia += playerGame.fouls if playerGame.role == PLAYER_ROLES.MAFIA
+          profile.foulsDon += playerGame.fouls if playerGame.role == PLAYER_ROLES.DON
+        profile.bestMoveAccuracy = if bestMoveAttempts then foundedMafiaOnBestMove / bestMoveAttempts else 0.0
+        profile.efficiency.sort((a, b)->
+          return if a.date > b.date then 1 else -1
+        )
+        for i in [1...playerGames.length]
+          profile.efficiency[i].winsMinusLosses += profile.efficiency[i - 1].winsMinusLosses
+        done(null, profile)
+      ).error(handleError)
     ).error(handleError)
+  ).error(handleError)


test(inventory-sync): add restockableInDays test

--- src/spec/sync/inventory-sync.spec.coffee
+++ src/spec/sync/inventory-sync.spec.coffee
@@ -14,6 +14,7 @@
       opts = [
         {type: 'quantity', group: 'white'}
         {type: 'expectedDelivery', group: 'black'}
+        {type: 'expectedRestockableInDays', group: 'black'}
       ]
       newInventory =
         id: '123'
@@ -24,6 +25,7 @@
         quantityOnStock: 10
         version: 1
       spyOn(@sync._utils, 'actionsMapExpectedDelivery').andReturn [{action: 'setExpectedDelivery', expectedDelivery: "2001-09-11T14:00:00.000Z"}]
+      spyOn(@sync._utils, 'actionsMapExpectedRestockableInDays').andReturn [{action: 'setRestockableInDays', expectedRestockableInDays: 7}]
       update = @sync.config(opts).buildActions(newInventory, oldInventory).getUpdatePayload()
       expected_update =
         actions: [
@@ -107,6 +109,52 @@
       expect(update).toBeDefined()
       expect(update.actions[0].action).toBe 'setExpectedDelivery'
       expect(update.actions[0].expectedDelivery).toBeUndefined()
+
+    it 'should add expectedRestockableInDays', ->
+      ieNew =
+        sku: 'ijk'
+        quantityOnStock: 5
+        expectedRestockableInDays: 7
+        version: 1
+      ieOld =
+        sku: 'ijk'
+        quantityOnStock: 5
+        version: 1
+      update = @sync.buildActions(ieNew, ieOld).getUpdatePayload()
+      expect(update).toBeDefined()
+      expect(update.actions[0].action).toBe 'setRestockableInDays'
+      expect(update.actions[0].expectedRestockableInDays).toBe 7
+
+    it 'should update expectedRestockableInDays', ->
+      ieNew =
+        sku: 'oik'
+        quantityOnStock: 0
+        expectedRestockableInDays: 3
+        version: 1
+      ieOld =
+        sku: 'oik'
+        quantityOnStock: 0
+        expectedRestockableInDays: 7
+        version: 1
+      update = @sync.buildActions(ieNew, ieOld).getUpdatePayload()
+      expect(update).toBeDefined()
+      expect(update.actions[0].action).toBe 'setRestockableInDays'
+      expect(update.actions[0].expectedRestockableInDays).toBe 3
+
+    it 'should remove expectedRestockableInDays', ->
+      ieNew =
+        sku: 'oik'
+        quantityOnStock: 0
+        version: 1
+      ieOld =
+        sku: 'oik'
+        quantityOnStock: 0
+        version: 1
+        expectedRestockableInDays: 10
+      update = @sync.buildActions(ieNew, ieOld).getUpdatePayload()
+      expect(update).toBeDefined()
+      expect(update.actions[0].action).toBe 'setRestockableInDays'
+      expect(update.actions[0].expectedRestockableInDays).toBeUndefined()

     describe 'actionsMapCustom', ->
       ieNew =


Added word boundaries in the applause.coffee

--- src/scripts/applause.coffee
+++ src/scripts/applause.coffee
@@ -63,6 +63,6 @@
   ]

 module.exports = (robot) ->
-  robot.hear /applau(d|se)|bravo|sarcastic applause|(slow|sarcastic) clap/i, (msg) ->
+  robot.hear /\b(applau(d|se)|bravo|sarcastic applause|(slow|sarcastic) clap)\b/i, (msg) ->
     type = if (/sarcastic/i).test(msg.message.text) then images.insincere else images.sincere
     msg.send msg.random type


Interpolate string

Interpolate string

--- src/scripts/whoisout.coffee
+++ src/scripts/whoisout.coffee
@@ -29,7 +29,7 @@
     thisDate = plugin.parseDate msg.match[1]?.trim()
     if thisDate
       plugin.save robot, thisDate, msg.message
-      msg.send 'ok, #{msg.user.name} is out on #{thisDate}'
+      msg.send "ok, #{msg.user.name} is out on #{thisDate}"
     else
       msg.send 'unable to save date'


Support more characters in selector id names.

This was just adding a test to verify that this was already working.

--- spec/CssSpec.coffee
+++ spec/CssSpec.coffee
@@ -81,6 +81,13 @@

     expect(css.parse(raw).lines).toEqual(stylesheet.lines)

+  it "should parse a ruleset with an id name containing all possible characters", ->
+    raw = "#abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_- { test: val; }"
+    stylesheet = new Stylesheet
+    stylesheet.addLine(new Ruleset(["#abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"], ["test:val;"]))
+
+    expect(css.parse(raw).lines).toEqual(stylesheet.lines)
+
   it "should parse a comment", ->
     raw = "/* Blah */"
     stylesheet = new Stylesheet


Tests for unscoped outports

Tests for unscoped outports

--- spec/Scoping.coffee
+++ spec/Scoping.coffee
@@ -98,6 +98,20 @@

     output.sendDone
       out: "1#{first}:2#{second}:#{c.nodeId}"
+
+processUnscope = ->
+  c = new noflo.Component
+  c.inPorts.add 'in',
+    datatype: 'string'
+  c.outPorts.add 'out',
+    datatype: 'string'
+    scoped: false
+
+  c.process (input, output) ->
+    data = input.getData 'in'
+    setTimeout ->
+      output.sendDone data + c.nodeId
+    , 1

 # Merge with an addressable port
 processMergeA = ->
@@ -133,6 +147,7 @@
       loader.registerComponent 'process', 'Async', processAsync
       loader.registerComponent 'process', 'Merge', processMerge
       loader.registerComponent 'process', 'MergeA', processMergeA
+      loader.registerComponent 'process', 'Unscope', processUnscope
       loader.registerComponent 'process', 'MergeUnscoped', processMergeUnscoped
       done()

@@ -457,7 +472,7 @@
       in1.post new noflo.IP 'data', 'one', scope: 'x'
       in1.post new noflo.IP 'closeBracket', 1, scope: 'x'

-  describe 'Process API with unscoped port and scopes', ->
+  describe 'Process API with unscoped inport and scopes', ->
     c = null
     in1 = null
     in2 = null
@@ -573,6 +588,122 @@
       in1.post new noflo.IP 'data', 'one', scope: 'x'
       in1.post new noflo.IP 'closeBracket', 1, scope: 'x'

+  describe 'Process API with unscoped outport and scopes', ->
+    c = null
+    in1 = null
+    in2 = null
+    out = null
+    before (done) ->
+      fbpData = "
+      INPORT=Pc1.IN:IN1
+      INPORT=Pc2.IN:IN2
+      OUTPORT=PcMerge.OUT:OUT
+      Pc1(process/Unscope) -> IN1 PcMerge(process/Merge)
+      Pc2(process/Unscope) -> IN2 PcMerge
+      "
+      noflo.graph.loadFBP fbpData, (err, g) ->
+        return done err if err
+        loader.registerComponent 'scope', 'MergeUnscopedOut', g
+        loader.load 'scope/MergeUnscopedOut', (err, instance) ->
+          return done err if err
+          c = instance
+          in1 = noflo.internalSocket.createSocket()
+          c.inPorts.in1.attach in1
+          in2 = noflo.internalSocket.createSocket()
+          c.inPorts.in2.attach in2
+          done()
+    beforeEach ->
+      out = noflo.internalSocket.createSocket()
+      c.outPorts.out.attach out
+    afterEach ->
+      c.outPorts.out.detach out
+      out = null
+    it 'should remove scopes as expected', (done) ->
+      expected = [
+        'null < 1'
+        'null DATA 1onePc1:2twoPc2:PcMerge'
+        'null >'
+      ]
+      received = []
+      brackets = []
+
+      out.on 'ip', (ip) ->
+        switch ip.type
+          when 'openBracket'
+            received.push "#{ip.scope} < #{ip.data}"
+            brackets.push ip.data
+          when 'data'
+            received.push "#{ip.scope} DATA #{ip.data}"
+          when 'closeBracket'
+            received.push "#{ip.scope} >"
+            brackets.pop()
+            return if brackets.length
+            chai.expect(received).to.eql expected
+            done()
+
+      in1.post new noflo.IP 'openBracket', 1, scope: 'x'
+      in1.post new noflo.IP 'data', 'one', scope: 'x'
+      in1.post new noflo.IP 'closeBracket', 1, scope: 'x'
+      in2.post new noflo.IP 'openBracket', 1, scope: 'y'
+      in2.post new noflo.IP 'data', 'two', scope: 'y'
+      in2.post new noflo.IP 'closeBracket', 1, scope: 'y'
+    it 'should forward packets without scopes', (done) ->
+      expected = [
+        'null < 1'
+        'null DATA 1onePc1:2twoPc2:PcMerge'
+        'null >'
+      ]
+      received = []
+      brackets = []
+
+      out.on 'ip', (ip) ->
+        switch ip.type
+          when 'openBracket'
+            received.push "#{ip.scope} < #{ip.data}"
+            brackets.push ip.data
+          when 'data'
+            received.push "#{ip.scope} DATA #{ip.data}"
+          when 'closeBracket'
+            received.push "#{ip.scope} >"
+            brackets.pop()
+            return if brackets.length
+            chai.expect(received).to.eql expected
+            done()
+      in1.post new noflo.IP 'openBracket', 1
+      in1.post new noflo.IP 'data', 'one'
+      in1.post new noflo.IP 'closeBracket'
+      in2.post new noflo.IP 'openBracket', 1
+      in2.post new noflo.IP 'data', 'two'
+      in2.post new noflo.IP 'closeBracket', 1
+    it 'should remove scopes also on unscoped packet', (done) ->
+      expected = [
+        'null < 1'
+        'null DATA 1onePc1:2twoPc2:PcMerge'
+        'null >'
+      ]
+      received = []
+      brackets = []
+
+      out.on 'ip', (ip) ->
+        switch ip.type
+          when 'openBracket'
+            received.push "#{ip.scope} < #{ip.data}"
+            brackets.push ip.data
+          when 'data'
+            received.push "#{ip.scope} DATA #{ip.data}"
+          when 'closeBracket'
+            received.push "#{ip.scope} >"
+            brackets.pop()
+            return if brackets.length
+            chai.expect(received).to.eql expected
+            done()
+      in1.post new noflo.IP 'openBracket', 1, scope: 'x'
+      in1.post new noflo.IP 'data', 'one', scope: 'x'
+      in1.post new noflo.IP 'closeBracket', 1, scope: 'x'
+      in2.post new noflo.IP 'openBracket', 1
+      in2.post new noflo.IP 'data', 'two'
+      in2.post new noflo.IP 'closeBracket', 1
+
   describe 'Process API with IIPs to addressable ports and scopes', ->
     c = null
     in1 = null


Activate packages asynchronously

Activate packages asynchronously

--- spec/background-tips-spec.coffee
+++ spec/background-tips-spec.coffee
@@ -1,6 +1,5 @@
 {WorkspaceView, $} = require 'atom'

-BackgroundTips = require '../lib/background-tips'
 BackgroundTipsView = require '../lib/background-tips-view'

 # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs.
@@ -15,9 +14,13 @@
     BackgroundTipsView.displayDuration = 50
     BackgroundTipsView.fadeDuration = 1

-  activatePackage = ->
-    backgroundTips = atom.packages.activatePackage('background-tips', immediate: true)
-    backgroundTipsView = backgroundTips.mainModule.backgroundTipsView
+  activatePackage = (callback) ->
+    waitsForPromise ->
+      atom.packages.activatePackage('background-tips').then ({mainModule}) ->
+        {backgroundTipsView} = mainModule
+
+    runs ->
+      callback()

   describe "when the package is activated when there is only one pane", ->
     beforeEach ->
@@ -27,29 +30,28 @@
     describe "when the pane is empty", ->
       it "attaches the view after a delay", ->
         expect(atom.workspaceView.getActivePane().getItems().length).toBe 0
-        activatePackage()

-        expect(backgroundTipsView.parent()).not.toExist()
-        advanceClock BackgroundTipsView.startDelay + 1
-        expect(backgroundTipsView.parent()).toExist()
+        activatePackage ->
+          expect(backgroundTipsView.parent()).not.toExist()
+          advanceClock BackgroundTipsView.startDelay + 1
+          expect(backgroundTipsView.parent()).toExist()

     describe "when the pane is not empty", ->
       it "does not attach the view", ->
         atom.workspaceView.getActivePane().activateItem($("item"))

-        activatePackage()
-
-        advanceClock BackgroundTipsView.startDelay + 1
-        expect(backgroundTipsView.parent()).not.toExist()
+        activatePackage ->
+          advanceClock BackgroundTipsView.startDelay + 1
+          expect(backgroundTipsView.parent()).not.toExist()

     describe "when a second pane is created", ->
       it "detaches the view", ->
-        activatePackage()
-        advanceClock BackgroundTipsView.startDelay + 1
-        expect(backgroundTipsView.parent()).toExist()
+        activatePackage ->
+          advanceClock BackgroundTipsView.startDelay + 1
+          expect(backgroundTipsView.parent()).toExist()

-        atom.workspaceView.getActivePane().splitRight()
-        expect(backgroundTipsView.parent()).not.toExist()
+          atom.workspaceView.getActivePane().splitRight()
+          expect(backgroundTipsView.parent()).not.toExist()

   describe "when the package is activated when there are multiple panes", ->
     beforeEach ->
@@ -58,23 +60,24 @@
       expect(atom.workspaceView.getPanes().length).toBe 2

     it "does not attach the view", ->
-      activatePackage()
-      advanceClock BackgroundTipsView.startDelay + 1
-      expect(backgroundTipsView.parent()).not.toExist()
+      activatePackage ->
+        advanceClock BackgroundTipsView.startDelay + 1
+        expect(backgroundTipsView.parent()).not.toExist()

     describe "when all but the last pane is destroyed", ->
       it "attaches the view", ->
-        activatePackage()
-        atom.workspaceView.getActivePane().remove()
-        advanceClock BackgroundTipsView.startDelay + 1
-        expect(backgroundTipsView.parent()).toExist()
+        activatePackage ->
+          atom.workspaceView.getActivePane().remove()
+          advanceClock BackgroundTipsView.startDelay + 1
+          expect(backgroundTipsView.parent()).toExist()

   describe "when the view is attached", ->
     beforeEach ->
       atom.workspaceView = new WorkspaceView
       expect(atom.workspaceView.getPanes().length).toBe 1
-      activatePackage()
-      advanceClock BackgroundTipsView.startDelay + 1
+
+      activatePackage ->
+        advanceClock BackgroundTipsView.startDelay + 1

     it "has text in the message", ->
       expect(backgroundTipsView.message.text()).toBeTruthy()


Add test

Add test

--- spec/language-csound-spec.coffee
+++ spec/language-csound-spec.coffee
@@ -1,5 +1,3 @@
-LanguageCsound = require '../lib/language-csound'
-
 describe 'language-csound', ->
   beforeEach ->
     waitsForPromise ->
@@ -45,6 +43,40 @@
       tokens = lines[2]
       expect(tokens[0]).toEqual value: 'endin', scopes: ['source.csound', 'meta.instrument-block.csound', 'keyword.other.csound']

+    it 'tokenizes user-defined opcodes', ->
+      # The Csound Orchestra grammar relies on the existence of an active text
+      # editor to tokenize user-defined opcodes.
+      waitsForPromise ->
+        atom.workspace.open().then (editor) ->
+          lines = grammar.tokenizeLines '''
+            opcode/**/aUDO,/**/0,/**/0//
+            aUDO
+            endop
+          '''
+
+          tokens = lines[0]
+          expect(tokens.length).toBe 14
+          expect(tokens[0]).toEqual value: 'opcode', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'keyword.function.csound']
+          expect(tokens[1]).toEqual value: '/*', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'comment.block.csound', 'punctuation.definition.comment.begin.csound']
+          expect(tokens[2]).toEqual value: '*/', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'comment.block.csound', 'punctuation.definition.comment.end.csound']
+          expect(tokens[3]).toEqual value: 'aUDO', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'meta.opcode-details.csound', 'entity.name.function.opcode.csound']
+          expect(tokens[4]).toEqual value: ',', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'meta.opcode-details.csound']
+          expect(tokens[5]).toEqual value: '/*', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'meta.opcode-details.csound', 'comment.block.csound', 'punctuation.definition.comment.begin.csound']
+          expect(tokens[6]).toEqual value: '*/', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'meta.opcode-details.csound', 'comment.block.csound', 'punctuation.definition.comment.end.csound']
+          expect(tokens[7]).toEqual value: '0', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'meta.opcode-details.csound', 'meta.opcode-argument-types.csound', 'storage.type.csound']
+          expect(tokens[8]).toEqual value: ',', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'meta.opcode-details.csound', 'meta.opcode-argument-types.csound']
+          expect(tokens[9]).toEqual value: '/*', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'meta.opcode-details.csound', 'comment.block.csound', 'punctuation.definition.comment.begin.csound']
+          expect(tokens[10]).toEqual value: '*/', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'meta.opcode-details.csound', 'comment.block.csound', 'punctuation.definition.comment.end.csound']
+          expect(tokens[11]).toEqual value: '0', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'meta.opcode-details.csound', 'meta.opcode-argument-types.csound', 'storage.type.csound']
+          expect(tokens[12]).toEqual value: '//', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound', 'meta.opcode-details.csound', 'meta.opcode-argument-types.csound', 'comment.line.csound', 'punctuation.definition.comment.line.csound']
+          expect(tokens[13]).toEqual value: '', scopes: ['source.csound', 'meta.opcode-definition.csound', 'meta.opcode-declaration.csound']
+
+          tokens = lines[1]
+          expect(tokens[0]).toEqual value: 'aUDO', scopes: ['source.csound', 'meta.opcode-definition.csound', 'entity.name.function.opcode.csound']
+
+          tokens = lines[2]
+          expect(tokens[0]).toEqual value: 'endop', scopes: ['source.csound', 'meta.opcode-definition.csound', 'keyword.other.csound']
+
     it 'tokenizes preprocessor directives', ->
       preprocessorDirectives = [
         '#else',
@@ -58,10 +90,9 @@
         '@ \t0'
         '@@ \t0'
       ]
-      lines = grammar.tokenizeLines preprocessorDirectives.join('\n')
+      lines = grammar.tokenizeLines preprocessorDirectives.join '\n'
       for i in [0...lines.length]
-        tokens = lines[i]
-        expect(tokens[0]).toEqual value: preprocessorDirectives[i], scopes: ['source.csound', 'keyword.preprocessor.csound']
+        expect(lines[i][0]).toEqual value: preprocessorDirectives[i], scopes: ['source.csound', 'keyword.preprocessor.csound']

     it 'tokenizes macro definitions', ->
       lines = grammar.tokenizeLines '# \tdefine/**/MACRO(ARGUMENT)/**/#$ARGUMENT#'
@@ -86,10 +117,9 @@
         'nchnls_i',
         'sr'
       ]
-      lines = grammar.tokenizeLines headerGlobalVariables.join('\n')
+      lines = grammar.tokenizeLines headerGlobalVariables.join '\n'
       for i in [0...lines.length]
-        tokens = lines[i]
-        expect(tokens[0]).toEqual value: headerGlobalVariables[i], scopes: ['source.csound', 'variable.other.readwrite.global.csound']
+        expect(lines[i][0]).toEqual value: headerGlobalVariables[i], scopes: ['source.csound', 'variable.other.readwrite.global.csound']

     it 'tokenizes quoted strings', ->
       lines = grammar.tokenizeLines '"characters$MACRO."'
@@ -157,7 +187,6 @@
         'until',
         'while'
       ]
-      lines = grammar.tokenizeLines keywords.join('\n')
+      lines = grammar.tokenizeLines keywords.join '\n'
       for i in [0...lines.length]
-        tokens = lines[i]
-        expect(tokens[0]).toEqual value: keywords[i], scopes: ['source.csound', 'keyword.control.csound']
+        expect(lines[i][0]).toEqual value: keywords[i], scopes: ['source.csound', 'keyword.control.csound']


Move reset spec setup to `beforeEach`

Move reset spec setup to `beforeEach`

For use in upcoming context

--- spec/storage-manager-spec.coffee
+++ spec/storage-manager-spec.coffee
@@ -55,10 +55,11 @@
       expect(manager.storage('time_entries').firstFetchOptions).toEqual({})

   describe "reset", ->
-    it "should clear all storage and sort lengths", ->
+    beforeEach ->
       buildAndCacheTask()
       buildAndCacheProject()

+    it "should clear all storage and sort lengths", ->
       expect(manager.storage("projects").length).toEqual 1
       expect(manager.storage("tasks").length).toEqual 1


SocialWorker: remove dasiy usage

SocialWorker: remove dasiy usage

--- workers/social/lib/social/traits/taggable.coffee
+++ workers/social/lib/social/traits/taggable.coffee
@@ -1,7 +1,8 @@
 module.exports = class Taggable

-  { ObjectRef, daisy, secure } = require 'bongo'
-  { Relationship } = require 'jraphical'
+  async                 = require 'async'
+  { Relationship }      = require 'jraphical'
+  { ObjectRef, secure } = require 'bongo'

   getTaggedContentRole  : -> @constructor.taggedContentRole or 'tagged'
   getTagRole            : -> @constructor.tagRole           or 'tag'
@@ -22,22 +23,23 @@
       else unless tags.length then callback null
       else
         JTag.handleFreetags client, tags, (err, tag) =>
-          if err then callback err
-          else
-            daisy queue = [
-              =>
-                @addTag tag, (err) ->
-                  if err then callback err
-                  else do queue.next
-              =>
-                incCount = {}
-                incCount["counts.#{taggedContentRole}"] = taggedCount
-                tag.update { $set: incCount }, (err) =>
-                  if err then callback err
-                  else if ++tagCount is tags.length
-                    @emit 'TagsChanged', tags unless options.silent
-                    callback null
-            ]
+          return callback err  if err
+
+          async.series [
+
+            (next) =>
+              @addTag tag, next
+
+            (next) =>
+              incCount = {}
+              incCount["counts.#{taggedContentRole}"] = taggedCount
+              tag.update { $set: incCount }, (err) =>
+                return next err  if err
+                if ++tagCount is tags.length
+                  @emit 'TagsChanged', tags unless options.silent
+                next()
+
+          ], callback

   removeAllTags: secure (client, options, callback) ->
     [callback, options] = [options, callback] unless callback


Added comments.

Added comments.

--- src/backbone-articulation.coffee
+++ src/backbone-articulation.coffee
@@ -43,7 +43,7 @@
 ##################################
 class Articulation.Model extends Backbone.Model
   @extend = Backbone.Model.extend
-  __bba_super: Backbone.Model
+  __bba_super: Backbone.Model  # provide the super class to mixin functions

   # Converts a model attributes from objects to plain old JSON (if needed).
   toJSON: ->
@@ -137,9 +137,8 @@
 ##################################
 class Articulation.Collection extends Backbone.Collection
   @extend = Backbone.Collection.extend
-  __bba_super: Backbone.Collection
-
-  model: Articulation.Model
+  __bba_super: Backbone.Collection  # provide the super class to mixin functions
+  model: Articulation.Model # default model type

   # Converts all of its models to plain old JSON (if needed) using JSONS.serialize.
   toJSON: ->


Update sqlformat.coffee

Update sqlformat.coffee

apply Glavin001's suggestion.

--- src/beautifiers/sqlformat.coffee
+++ src/beautifiers/sqlformat.coffee
@@ -17,7 +17,7 @@
   beautify: (text, language, options) ->
     @run("sqlformat", [
       @tempFile("input", text)
-      "--reindent" if (options.reindent? && options.reindent == true)
+      "--reindent" if options.reindent is true
       "--indent_width=#{options.indent_size}" if options.indent_size?
       "--keywords=#{options.keywords}" if (options.keywords? && options.keywords != 'unchanged')
       "--identifiers=#{options.identifiers}" if (options.identifiers? && options.identifiers != 'unchanged')


removed redundant exports

removed redundant exports

--- src/scripts/passivetotal.coffee
+++ src/scripts/passivetotal.coffee
@@ -60,14 +60,12 @@
     else
       msg.send "PassiveTotal API key not configured."

-module.exports = (robot) ->
   robot.respond /ptotal classify (targeted|crime|multiple|benign) (.*)/i, (msg) ->

     if PASSIVETOTAL_KEY?
       classification =  msg.match[1].toLowerCase()
       value = msg.match[2].toLowerCase()
       data = "apikey=#{encodeURIComponent PASSIVETOTAL_KEY}&classification=#{classification}&value=#{encodeURIComponent value}"
-

       robot.http(PT_CLASSIFY_URL)
         .post(data) (err, res, body) ->


Add comment

Add comment

--- src/scripts/modules/media/title/title.coffee
+++ src/scripts/modules/media/title/title.coffee
@@ -12,6 +12,7 @@
       title = @model.get('title')
       currentPage = @model.get('currentPage')
       if currentPage
+        # Set information used for social media links
         share =
           url: Backbone.history.fragment
           source: @model.get('source') or currentPage.get('source') or 'OpenStax College'


project avatars: clean up after some incomplete renaming

--- src/smc-webapp/editor_chat.cjsx
+++ src/smc-webapp/editor_chat.cjsx
@@ -70,7 +70,7 @@
 underscore = require('underscore')

 # SMC libraries
-{Avatar, UsersViewingDocument} = require('./profile')
+{Avatar, UsersViewing} = require('./profile')
 misc = require('smc-util/misc')
 misc_page = require('./misc_page')
 {defaults, required} = misc
@@ -926,7 +926,7 @@
                         </Button>
                     </Col>
                     <Col xs={4} md={4} style={padding:'0px'}>
-                        <UsersViewingDocument
+                        <UsersViewing
                               file_use_id = {@props.file_use_id}
                               file_use    = {@props.file_use}
                               account_id  = {@props.account_id}
@@ -989,7 +989,7 @@
             <Grid>
                 <Row style={marginBottom:'5px'}>
                     <Col xs={3} style={padding:'0px'}>
-                        <UsersViewingDocument
+                        <UsersViewing
                               file_use_id = {@props.file_use_id}
                               file_use    = {@props.file_use}
                               account_id  = {@props.account_id}


webapp: do not randomize data image urls -- #3141

--- src/smc-webapp/misc_page.coffee
+++ src/smc-webapp/misc_page.coffee
@@ -122,10 +122,14 @@
 require('./jquery-plugins/katex')

 # Force reload all images by appending random query param to their src URL.
+# But not for base64 data images -- https://github.com/sagemathinc/cocalc/issues/3141
 $.fn.reload_images = (opts) ->
     @each ->
         for img in $(this).find('img')
-            $(img).attr('src', $(img).attr('src')+'?'+Math.random())
+            src = $(img).attr('src')
+            if misc.startswith(src, 'data:')
+                continue
+            $(img).attr('src', src + '?' + Math.random())

 # Highlight all code blocks that have CSS class language-r, language-python.
 # TODO: I just put in r and python for now, since this is mainly


Remove extra newline

Remove extra newline

--- src/trix/controllers/input_controller.coffee
+++ src/trix/controllers/input_controller.coffee
@@ -141,7 +141,6 @@
         event.preventDefault()
         @responder?.adjustPositionInDirection("backward")

-
     right: (event) ->
       if @selectionIsInCursorTarget()
         event.preventDefault()


Fixes typo

Fixes typo

--- src/api-mock.coffee
+++ src/api-mock.coffee
@@ -20,13 +20,12 @@

     if configuration.options['ssl-enable']
       sslSupport = new SslSupport(
-        @app, {
+        @app,
             port: configuration.options['ssl-port'],
             host: configuration.options['ssl-host'],
             cert: configuration.options['ssl-cert'],
             key: configuration.options['ssl-key']
-        }
-    )
+      )

     if !configuration.options['cors-disable']
       corsSupport = new CorsSupport @app


修复DropDown 在界面滚动后显示位置错误问题

修复DropDown 在界面滚动后显示位置错误问题

--- src/widget/edit/dropdown.coffee
+++ src/widget/edit/dropdown.coffee
@@ -373,7 +373,8 @@
 		rect = $fly(dropdownDom).offset()
 		clientWidth = document.body.offsetWidth
 		clientHeight = document.body.clientHeight
-		bottomSpace = clientHeight - rect.top - dropdownDom.clientHeight
+		scrollTop = document.body.scrollTop
+		bottomSpace = clientHeight - rect.top - dropdownDom.clientHeight - scrollTop

 		if bottomSpace >= boxHeight
 			direction = "down"


改进DropDown

改进DropDown

--- src/widget/edit/dropdown.coffee
+++ src/widget/edit/dropdown.coffee
@@ -141,8 +141,8 @@
 					})
 					dom.appendChild(clearButton)

-				$fly(clearButton).toggleClass("disabled", !@_value)
-		).on("click", (evt)=>
+				$fly(clearButton).toggleClass("disabled", !@_doms.input.value)
+		).on("click", () =>
 			if @_disabled then return;
 			if @_openOnActive
 				if @_opened
@@ -150,6 +150,7 @@
 					if input.readOnly then @close()
 				else
 					@open()
+			return
 		)

 		$(@_doms.input).on("input", (evt) =>
@@ -189,7 +190,8 @@
 		return $.xCreate(
 			tagName: "input"
 			type: "text"
-			click: () => this.focus()
+			click: (evt) =>
+				this.focus()
 		)

 	_isEditorDom: (node) ->
@@ -204,7 +206,6 @@
 		$inputDom.prop("readonly", @_finalReadOnly or @_isEditorReadOnly() or @_disabled)
 		@get("actionButton")?.set("disabled", @_finalReadOnly)
 		@_setValueContent()
-		@_refreshInputValue(@_value)
 		return

 	_setValue: (value) ->
@@ -263,11 +264,16 @@
 				text = cola.Entity._evalDataPath(item, @_textProperty or @_valueProperty or "value")
 			else
 				text = item
-			input.value = text or ""
+
+			if not @_useValueContent
+				input.value = text or ""
+
 			input.placeholder = ""
 			@get$Dom().removeClass("placeholder")
 		else
-			input.value = ""
+			if not @_useValueContent
+				input.value = ""
+
 			input.placeholder = @_placeholder or ""
 			@get$Dom().addClass("placeholder")

@@ -426,7 +432,7 @@
 		container?.hide?(callback)
 		return

-	_closeDropdown:()->
+	_closeDropdown: ()->
 		container = @_getContainer(true)
 		container?.hide?()

@@ -452,25 +458,29 @@
 					pair = part.split("=")
 					targetProp = pair[0]
 					sourceProp = pair[1] or targetProp
-					if item instanceof cola.Entity
-						value = item.get(sourceProp)
+					if item
+						if item instanceof cola.Entity
+							value = item.get(sourceProp)
+						else
+							value = item[sourceProp]
 					else
-						value = item[sourceProp]
+						value = null
 					bindEntity.set(targetProp, value)
 					return
 				)
-			else
-				value = @_getItemValue(item)
-				@set("value", value)
+
+			value = @_getItemValue(item)
+			@set("value", value)

 		@_skipFindCurrentItem = false
 		@refresh()
 		return

-	_doRefreshDom:() ->
+	_doRefreshDom: () ->
 		super()
 		if not @_dom then return
 		$(@_dom).toggleClass("disabled", @_disabled);
+		return

 cola.Element.mixin(cola.AbstractDropdown, cola.TemplateSupport)

@@ -520,11 +530,11 @@

 		if height then $dom.css("height", height)
 		$dom.removeClass(if direction == "down" then "direction-up" else "direction-down")
-		.addClass("direction-" + direction)
-		.toggleClass("x-over", boxWidth > dropdownDom.offsetWidth)
-		.css("left", left).css("top", top)
-		.css("min-width", dropdownDom.offsetWidth)
-		.css("max-width", document.body.clientWidth)
+			.addClass("direction-" + direction)
+			.toggleClass("x-over", boxWidth > dropdownDom.offsetWidth)
+			.css("left", left).css("top", top)
+			.css("min-width", dropdownDom.offsetWidth)
+			.css("max-width", document.body.clientWidth)

 		@_animation = "fade"

@@ -627,13 +637,37 @@
 				return true

 	_initDom: (dom)->
-		if @_filterable then $fly(dom).addClass("filterable")
 		@_regDefaultTemplates()

 		inputDom = @_doms.input
 		$fly(inputDom).on("input", () => @_onInput(inputDom.value))

 		super(dom)
+
+		if @_filterable and @_useValueContent
+			$fly(dom).addClass("filterable").xAppend(
+				contextKey: "filterInput"
+				tagName: "input"
+				text: "input"
+				class: "filter-input"
+				focus: () => @_doFocus()
+				blur: () => @_doBlur()
+				input: (evt) =>
+					if @_useValueContent
+						$valueContent = $fly(@_doms.valueContent)
+						if evt.target.value
+							$valueContent.hide()
+						else
+							$valueContent.show()
+
+					@_onInput(@_doms.filterInput.value)
+					return
+			, @_doms)
+		return
+
+	_refreshInputValue: (value) ->
+		if not @_useValueContent then super(value)
+		return

 	open: () ->
 		if super()
@@ -708,6 +742,7 @@
 	_doBlur: ()->
 		if @_inputDirty
 			@close(@_list?.get("currentItem") or null)
+		@_doms.filterInput?.value = ""
 		return super()

 	_getDropdownContent: () ->


reply when it makes sense

reply when it makes sense

--- src/bonusly.coffee
+++ src/bonusly.coffee
@@ -23,11 +23,11 @@
   service = 'https://bonus.ly'

   unless token
-    msg.send 'The Bonusly API token is not set. Navigate to https://bonus.ly/api as an _admin_ user (important), grab the access token and set the HUBOT_BONUSLY_ADMIN_API_TOKEN environment variable.'
+    msg.reply 'The Bonusly API token is not set. Navigate to https://bonus.ly/api as an _admin_ user (important), grab the access token and set the HUBOT_BONUSLY_ADMIN_API_TOKEN environment variable.'
     return

   robot.respond /bonuses/i, (msg) ->
-    msg.send "o.k. I'm grabbing 10 recent bonuses ..."
+    msg.reply "o.k. I'm grabbing 10 recent bonuses ..."
     path="/api/v1/bonuses?access_token=#{token}&limit=10"
     msg.http(service)
       .path(path)
@@ -39,7 +39,7 @@
             bonuses_text = ("From #{bonus.giver.short_name} to #{bonus.receiver.short_name}: #{bonus.amount_with_currency} #{bonus.reason}" for bonus in bonuses).join('\n')
             msg.send bonuses_text
           else
-            msg.send "Request (#{service}#{path}) failed (#{res.statusCode})."
+            msg.reply "Request (#{service}#{path}) failed (#{res.statusCode})."

   robot.respond /(give) ?(.*)?/i, (msg) ->
@@ -47,10 +47,10 @@
     text = msg.match[2]

     unless text?
-      msg.send "Usage: give <amount> to <name|email> for <reason> <#hashtag>"
+      msg.reply "Usage: give <amount> to <name|email> for <reason> <#hashtag>"
       return

-    msg.send "o.k. I'll try to give that bonus ..."
+    msg.reply "o.k. I'll try to give that bonus ..."

     path = '/api/v1/bonuses/create_from_text'
     post = "access_token=#{token}&giver=#{giver}&client=#{adapter}&text=#{text}"
@@ -63,4 +63,4 @@
           when 200
             msg.send body
           else
-            msg.send "Failed to give: (#{res.statusCode}). Tried to post (#{post}) to (#{service}#{path})"
+            msg.reply "Failed to give: (#{res.statusCode}). Tried to post (#{post}) to (#{service}#{path})"


syntax sugar

syntax sugar

--- src/browser.coffee
+++ src/browser.coffee
@@ -59,9 +59,8 @@
         return this

     listen: () ->
-        for event in EVENTS
-            if (listener = this["on#{event}"])?
-                @template.on(event, listener.bind(this))
+        for event in EVENTS when (listener = this["on#{event}"])?
+            @template.on(event, listener.bind(this))
         return this

     make: -> # create a dom object


cooldown finilazation

cooldown finilazation

--- src/cooldown.coffee
+++ src/cooldown.coffee
@@ -3,7 +3,7 @@
 GameObject's properties.

-# Health regeneration
+
 player = GameObject
   health: 50


properly set group when guests convert to members

--- workers/social/lib/social/models/vm.coffee
+++ workers/social/lib/social/models/vm.coffee
@@ -596,7 +596,13 @@
             webHome   : user.username
             groups    : wrapGroup group
           }
-        else unless group.slug is 'koding'
+        else if group.slug is 'koding'
+          member.fetchVms (err, vms)->
+            if err then handleError err
+            else
+              vms.forEach (vm) ->
+                vm.update $set: groups: [id: group.getId()], handleError
+        else
           member.checkPermission group, 'sudoer', (err, hasPermission)->
             if err then handleError err
             else


include jquery to widget js file

include jquery to widget js file

--- app/assets/javascripts/uploadcare/core/jquery.js.coffee
+++ app/assets/javascripts/uploadcare/core/jquery.js.coffee
@@ -1,37 +1,12 @@
+# = require uploadcare/core/jquery.1.7.2.js
+
 {namespace} = uploadcare

 namespace 'uploadcare', (ns) ->
-  fns = []
-  ns.whenReady = (callback) -> fns.push(callback)
+
+  ns.jQuery = jQuery
+  jQuery.noConflict(true)
+
+  # back compatibility
+  ns.whenReady = (fn) -> fn()
   ns.expose 'whenReady'
-
-  ns.ready = ->
-    fn() for fn in fns
-    ns.whenReady = (fn) -> fn()
-    ns.expose 'whenReady'
-
-
-
-jQueryVersion = '1.7.2'
-
-jquerySrc = "https://ajax.googleapis.com/ajax/libs/jquery/#{jQueryVersion}/jquery.min.js"
-
-if jQuery? and jQuery().jquery > jQueryVersion
-  uploadcare.jQuery = jQuery
-  return uploadcare.jQuery -> uploadcare.ready()
-
-script = document.createElement('script')
-script.setAttribute('src', jquerySrc)
-
-load = ->
-  uploadcare.jQuery = jQuery.noConflict(true)
-  uploadcare.jQuery -> uploadcare.ready()
-
-if document.addEventListener?
-  script.addEventListener('load', load, false)
-else
-  script.attachEvent 'onreadystatechange', ->
-    load() if script.readyState == 'loaded' || script.readyState == 'complete'
-
-first = document.getElementsByTagName('script')[0]
-first.parentNode.insertBefore(script, first)


Use correct flag for determining mini panel visibility

--- services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee
+++ services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee
@@ -148,7 +148,7 @@
 			entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {}
 			Object.keys(entries).length
 		), (nEntries) ->
-			$scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChanges
+			$scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChangesVisible

 		$scope.$watch "ui.reviewPanelOpen", (reviewPanelOpen) ->
 			return if !reviewPanelOpen?


Fix error after rendering when there's no selection

--- src/trix/models/selection_manager.coffee
+++ src/trix/models/selection_manager.coffee
@@ -46,7 +46,7 @@
     if --@lockCount is 0
       lockedLocationRange = @lockedLocationRange
       delete @lockedLocationRange
-      @setLocationRange(lockedLocationRange)
+      @setLocationRange(lockedLocationRange) if lockedLocationRange?

   preserveSelection: (block) ->
     point = @getPointAtEndOfSelection()


Ensure location range is current when locking

--- src/trix/models/selection_manager.coffee
+++ src/trix/models/selection_manager.coffee
@@ -63,6 +63,7 @@

   lock: ->
     if @lockCount++ is 0
+      @updateCurrentLocationRange()
       @lockedLocationRange = @getLocationRange()

   unlock: ->


Replace recursive CodeCat instantation with "Relative" class setup in constructor

--- src/codecat.coffee
+++ src/codecat.coffee
@@ -15,8 +15,6 @@
 		Object.defineProperties this,
 			source:
 				value: source
-			options:
-				value: options
 			prefix:
 				value: prefix
 			encoding:
@@ -25,9 +23,15 @@
 				value: getDirectiveRegExp('prepend')
 			appendRegexp:
 				value: getDirectiveRegExp('append')
+			Relative:
+				value: class extends CodeCat
+					constructor: (src) ->
+						sourceDir = Path.dirname(source)
+						relSrc = Path.join(sourceDir, src)
+						super(relSrc, options)

 	findConcats: (options, callback) ->
-		[{relative = false}, callback] = discernOptions(options, callback)
+		[options, callback] = discernOptions(options, callback)
 		{prepend, append} = concats = {prepend: [], append: []}
 		lineReader = Readline.createInterface
 			input: createReadStream(@source, encoding: @encoding)
@@ -38,9 +42,7 @@
 			else if match = line.match(@appendRegexp)?[2]
 				append.push(match)

-		lineReader.on 'close', =>
-			concats = mapConcats(concats, @getRelativePath, this) if relative
-			callback?(concats)
+		lineReader.on 'close', -> callback?(concats)

 	concat: (options, callback) ->
 		[options, callback] = discernOptions(options, callback)
@@ -55,15 +57,11 @@
 		[options, callback] = discernOptions(options, callback)
 		{recursive = true} = options
 		ensureStream dest, defaultEncoding: @encoding, (stream, end) =>
-			@findConcats relative: true, (concats) =>
+			@findConcats (concats) =>
 				error = null
-				concats = mapConcats(concats, (c) => new CodeCat(c, @options)) if recursive
+				concats = mapConcats(concats, (c) => new @Relative(c)) if recursive
 				paths = [concats.prepend..., @source, concats.append...]
 				joinFiles paths, @encoding, stream, options, -> callback?(error)
-
-	getRelativePath: (file) ->
-		sourceDir = Path.dirname(@source)
-		Path.join(sourceDir, file)

 	@Commenters =
 		'': '//'


Add file type case insensitive.

Add file type case insensitive.

--- src/dropzone.coffee
+++ src/dropzone.coffee
@@ -1192,7 +1192,7 @@
   for validType in acceptedFiles
     validType = validType.trim()
     if validType.charAt(0) == "."
-      return yes if file.name.indexOf(validType, file.name.length - validType.length) != -1
+      return yes if file.name.toLowerCase().indexOf(validType.toLowerCase(), file.name.length - validType.length) != -1
     else if /\/\*$/.test validType
       # This is something like a image/* mime type
       return yes if baseMimeType == validType.replace /\/.*$/, ""


Allowed for multiple --eval or -e arguments

--- src/command.coffee
+++ src/command.coffee
@@ -136,19 +136,26 @@
 opts.welcome = argv.welcome if argv.welcome?

 if argv.eval
-    isJs = false
-    if fs.existsSync argv.eval
-        isJs = argv.eval[-3..] is '.js'
-        opts.evalData = fs.readFileSync argv.eval, 'utf-8'
-    else
-        opts.evalData = argv.eval
+    evalArgs = if argv.eval instanceof Array then argv.eval else [argv.eval]
+
+    evalScripts = for evalArg in evalArgs
+        isJs = false
+        evalScript = if fs.existsSync evalArg
+            isJs = evalArg[-3..] is '.js'
+            fs.readFileSync evalArg, 'utf-8'
+        else
+            evalArg

-    # If we are evaluating code, and it's either from a string or
-    # from a file not ending in `.js`, and a non-js language is
-    # set, then we need to compile it to js first.
-    if not isJs and nesh.compile
-        nesh.log.debug 'Compiling eval data'
-        opts.evalData = nesh.compile opts.evalData
+        # If we are evaluating code, and it's either from a string or
+        # from a file not ending in `.js`, and a non-js language is
+        # set, then we need to compile it to js first.
+        if not isJs and nesh.compile
+            nesh.log.debug 'Compiling eval data'
+            evalScript = nesh.compile evalScript
+
+        evalScript
+
+    opts.evalData = evalScripts.join ";\n"

 # Initialize and autoload plugins
 nesh.init true, (err) ->


Fix for progress bar

Fix for progress bar

--- src/command.coffee
+++ src/command.coffee
@@ -31,7 +31,7 @@
         }

       getbot.on 'data', (data, rate) ->
-        bar.tick(data.length, {'rate': rate})
+        bar.tick(data, {'rate': rate})
         #console.log rate
       return
     else


IE7 compatibility fix when setting tag class

--- src/compile.coffee
+++ src/compile.coffee
@@ -71,8 +71,9 @@
         classes = node.ast.classes
         classes = classes.concat(value) unless value is undefined
         if classes.length
-          element.setAttribute(ast.name, classes.join(' '))
+          element.className = classes.join(' ')
         else
+          element.className = '' # IE7-compat
           element.removeAttribute(ast.name)
       else if value is undefined
         element.removeAttribute(ast.name)


logger can be turned off now

logger can be turned off now

--- lib/Logger.coffee
+++ lib/Logger.coffee
@@ -3,10 +3,13 @@

 class Logger
   logFile: "log.log"
+  loggerActive: true
+
   logText: (str) ->
-    fs.appendFile @logFile, str, (err) ->
-      if err
-        throw err
+    if @loggerActive
+      fs.appendFile @logFile, str, (err) ->
+        if err
+          throw err

   formatCommand: (cmd) ->
     utils.formatObj cmd


Added refCount helper

Added refCount helper

--- src/core/kb.coffee
+++ src/core/kb.coffee
@@ -146,6 +146,8 @@
     obj.__kb_ref_count++
     return obj

+  @refCount: (obj) -> if typeof(obj.refCount) is 'function' then obj.refCount() else (obj.__kb_ref_count or 0)
+
   # Renders a template and binds a callback to the node that releases the view model when the node is removed using ko.removeNode.
   #
   # NOTE: if you provide an afterRender method on the View Model and do not provide afterRender in the options, afterRender will be called with the following signature: afterRender(element) which differs from the Knockout signture of afterRender(elements)


Rename link from Scores Report to Student Scores

--- src/components/scores/index.cjsx
+++ src/components/scores/index.cjsx
@@ -187,7 +187,7 @@
       @renderStudentRow(data.rows[rowIndex])

-      Scores Report
+      Student Scores
       <ScoresExport courseId={courseId} className='pull-right'/>
       <CoursePeriodsNavShell
         handleSelect={@selectPeriod}


增加css

增加css

--- src/config/routers/static.coffee
+++ src/config/routers/static.coffee
@@ -9,7 +9,7 @@
     to: '../static/index.html'
   }
   {
-    path: /^\/(js|package|fonts|images)\/(.+)$/i
+    path: /^\/(js|package|fonts|images|css)\/(.+)$/i
     to: '../static/$1/$2'
   }
 ]


remove excessive binds

remove excessive binds

--- lib/Cycler.coffee
+++ lib/Cycler.coffee
@@ -8,7 +8,7 @@
     #@source ?= ['localhost']
     if @source?
       exec "ifconfig eth0 0.0.0.0"
-      exec "ifconfig eth0:#{idx} #{ip} netmask 255.255.255.248 up" for ip, idx in @source[0..50]
+      exec "ifconfig eth0:#{idx} #{ip}" for ip, idx in @source
     @range = 8 - @prefix.split(':').length if @prefix?

   # TODO: Support for ipv4 ranges


- fixes

- fixes


Check for the correct dictionary name

Check for the correct dictionary name

--- src/feedback.coffee
+++ src/feedback.coffee
@@ -89,7 +89,7 @@
           'This is a very common password'
       else if match.guesses_log10 <= 4
         'This is similar to a commonly used password'
-    else if match.dictionary_name == 'english'
+    else if match.dictionary_name == 'english_wikipedia'
       if is_sole_match
         'A word by itself is easy to guess'
     else if match.dictionary_name in ['surnames', 'male_names', 'female_names']


adding submit queue, fixes #2

adding submit queue, fixes #2

--- lib/client.coffee
+++ lib/client.coffee
@@ -96,6 +96,8 @@

 class Client extends Gearman
   constructor: (@options) ->
+    @queue = []
+    @submitting = false
     @options = _.defaults (@options or {}),
       host: 'localhost'
       port: 4730
@@ -113,11 +115,21 @@
       delete @jobs[handle]
     @connect()

+  push: () =>
+    @queue.push arguments
+  pop: () =>
+    args = @queue.pop arguments
+    @submitJob.apply @, args[0]
+
   submitJob: (name, payload) =>
+    return @push arguments if @submitting
+    @submitting = true
     job = new EventEmitter
     @once 'JOB_CREATED', (handle) =>
       @jobs[handle] = job
       job.emit 'created', handle
+      @submitting = false
+      process.nextTick @pop if @queue.length
     @sendCommand "SUBMIT_JOB", name, false, payload
     job


I guess I'll be finding these for a while.

--- app/views/editor/modal/VersionsModal.coffee
+++ app/views/editor/modal/VersionsModal.coffee
@@ -29,7 +29,7 @@

   constructor: (options, @ID, @model) ->
     super options
-    @original = new model(_id: @ID)
+    @original = new @model(_id: @ID)
     @original = @supermodel.loadModel(@original, 'document').model
     @listenToOnce(@original, 'sync', @onViewSync)


bump version

bump version

--- src/mailButler.appsscript.coffee
+++ src/mailButler.appsscript.coffee
@@ -58,7 +58,7 @@

 class MailButler

-  @VERSION:             1.137
+  @VERSION:             1.138
   @SUPPORT_EMAIL:       'support@mailfred.de'
   @LABEL_BASE:          'MailFred'
   @LABEL_OUTBOX:        MailButler.LABEL_BASE + '/' + 'Scheduled'


fix @

fix @

--- app/assets/javascripts/messages.coffee
+++ app/assets/javascripts/messages.coffee
@@ -436,10 +436,6 @@
       , 100)
       return

-    "textComplete:hide": ->
-      clearInterval set_top  if set_top
-      return
-
   $(".friend").click ->
     sender = $(this)
     unless self.location.toString().indexOf("persons/") is -1


progress zooms to bounds

progress zooms to bounds

--- app/assets/javascripts/progress.coffee
+++ app/assets/javascripts/progress.coffee
@@ -85,8 +85,14 @@
 			yes_json.addTo(progress.map)
 			no_json.addTo(progress.map)
 			fix_json.addTo(progress.map)
+
 			progress.map.addLayer(markers)
-			progress.map.setZoom(17)
+
+			bounds = yes_json.getBounds()
+			.extend(no_json.getBounds())
+			.extend(fix_json.getBounds())
+
+			progress.map.fitBounds(bounds)
 		)

 	addMarker: (markers, data, icon) ->


mini fix

mini fix

--- app/assets/javascripts/projects.coffee
+++ app/assets/javascripts/projects.coffee
@@ -76,7 +76,7 @@

       setTimeout ( ->
         $('#invited_users > span').addClass('col-md-12')
-        $('input[type=invitationEmail').addClass('form-control email-invite col-md-2').attr('readonly', true)
+        $('input[type=invitationEmail]').addClass('form-control email-invite col-md-2').attr('readonly', true)
         $('input[type=RemoveButton]').addClass('btn btn-default removeButton')

       ), 5


Fix date parsing again

Fix date parsing again

--- scanblog/static/js/moderation/common.coffee
+++ scanblog/static/js/moderation/common.coffee
@@ -6,7 +6,13 @@
     if _.isString dateOrStr
         parts = [parseInt(part, 10) for part in dateOrStr.split(/[^0-9]+/g)][0]
         # Months are rendered as 0-11 in javascript
-        return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5])
+        return new Date(
+          parts[0],
+          parts[1] - 1,
+          parts[2],
+          parts[3] or 0,
+          parts[4] or 0,
+          parts[5] or 0)
     return dateOrStr

 btb.formatDate = (dateOrStr) ->


fix issue #3631 - toggle backlog filter text

--- app/coffee/modules/backlog/main.coffee
+++ app/coffee/modules/backlog/main.coffee
@@ -752,7 +752,7 @@
         hideText = $translate.instant("BACKLOG.FILTERS.HIDE")
         showText = $translate.instant("BACKLOG.FILTERS.SHOW")

-        toggleText(target.find(".text"), [hideText, showText])
+        toggleText(target, [hideText, showText])

         if !sidebar.hasClass("active")
             $ctrl.resetFilters()


Minor offset correction in script tag closure detection

--- src/htmldiff.coffee
+++ src/htmldiff.coffee
@@ -22,7 +22,7 @@
       when 'script'
         if is_end_of_tag char
           current_word += '>'
-          end = current_word.substr current_word.length - 8
+          end = current_word.substr current_word.length - 9
           if end is '</script>'
             words.push current_word
             current_word = ''


Remove repetitive error email

Remove repetitive error email

lab80/hellomoney#1908
- Only email.send disabled for certain case

--- error_logger_server.coffee
+++ error_logger_server.coffee
@@ -1,3 +1,5 @@
+REPETITIVE_ERRORS =
+  "Connection timeout. No sockjs heartbeat received."
 LOG_ROUTE = '/errorlog'
 ERROR_FIELDS = ['errorType', 'ipAddress', 'location', 'browser', 'details']
 ERROR_TYPE_TO_SUBJECT_GETTER =
@@ -5,6 +7,9 @@
   MANUAL: (error) -> error.details.message
   AJAX: (error) -> error.details.xhrStatusText
   METEOR: (error) -> error.details.message
+
+_isRepetitiveError = (error) ->
+  _.has(REPETITIVE_ERRORS, error.details.message)

 ErrorLogger =
   _collection: new Mongo.Collection('errorlogs')
@@ -23,13 +28,15 @@

     ErrorLogger._collection.insert(error)

-    from = process.env.ERROR_EMAIL_FROM
-    to = process.env.ERROR_EMAIL_TO
-    if from and to
-      text = EJSON.stringify(error, indent: true)
-      subject = ERROR_TYPE_TO_SUBJECT_GETTER[error.errorType](error)
-      subject = "[Front error] #{error.errorType}: #{subject}"
-      Email.send(from: from, to: to, subject: subject, text: text)
+    unless _isRepetitiveError(error)
+      from = process.env.ERROR_EMAIL_FROM
+      to = process.env.ERROR_EMAIL_TO
+      if from and to
+        text = EJSON.stringify(error, indent: true)
+        subject = ERROR_TYPE_TO_SUBJECT_GETTER[error.errorType](error)
+        subject = "[Front error] #{error.errorType}: #{subject}"
+        Email.send(from: from, to: to, subject: subject, text: text)
+
     "#{error.errorType} OK"

 bodyParser = Npm.require('body-parser')


Switch order of args

Switch order of args

--- services/real-time/app/coffee/Router.coffee
+++ services/real-time/app/coffee/Router.coffee
@@ -79,26 +79,23 @@
 					if err?
 						Router._handleError null, err, client, "leaveProject"

-			# Variadic. The possible options:
+			# Variadic. The possible arguments:
 			# doc_id, callback
 			# doc_id, fromVersion, callback
 			# doc_id, options, callback
-			# doc_id, options, fromVersion, callback
-			client.on "joinDoc", (doc_id, options, fromVersion, callback) ->
-				# options is optional
+			# doc_id, fromVersion, options, callback
+			client.on "joinDoc", (doc_id, fromVersion, options, callback) ->
+				if typeof fromVersion == "function"
+					fromVersion = -1
+					options = {}
+					callback = fromVersion
+				else if typeof fromVersion == "object"
+					fromVersion = -1
+					options = fromVersion
+					callback = options
 				if typeof options == "function"
 					options = {}
 					callback = options
-					fromVersion = -1
-				else if typeof options == "number"
-					options = {}
-					fromVersion = options
-					callback = fromVersion
-
-				# fromVersion is optional
-				if typeof fromVersion == "function"
-					callback = fromVersion
-					fromVersion = -1

 				WebsocketController.joinDoc client, doc_id, options, fromVersion, (err, args...) ->
 					if err?


Limit vote amount to 5 decimals, addresses voting part of issue #681

--- app/js/controllers/account_vote.coffee
+++ app/js/controllers/account_vote.coffee
@@ -46,6 +46,9 @@
         Observer.unregisterObserver(account_votes_observer)

     yesSend = ->
+        console.log("balMinusFee:", balMinusFee);
+        balMinusFee = Math.floor(balMinusFee * 100000) / 100000;
+        console.log("balMinusFee:", balMinusFee);
         WalletAPI.transfer(balMinusFee, Info.symbol, $scope.account_name, $scope.account_name, $scope.data.vote, $scope.data.vote).then (response) ->
             Growl.notice "", "Transfer transaction broadcasted"
             $scope.t_active = true


allow changing stepper default open style

--- lib/debugger/stepper.coffee
+++ lib/debugger/stepper.coffee
@@ -7,10 +7,11 @@

 module.exports =
 class Stepper
-  constructor: ({@buttons}) ->
+  constructor: ({@buttons, @pending}) ->
     @views = []
     @bars = []
     @text = "Grand Steppin'"
+    @pending ?= true
     @buttons ?= [
       {icon: 'arrow-down'}
       {icon: 'link-external'}
@@ -53,7 +54,7 @@
       Promise.resolve()
     else
       focusEditorPane()
-      open(file, line, {pending: true})
+      open(file, line, {pending: @pending})

   goto: (file, @line) ->
     @listener ?= atom.workspace.observeTextEditors (ed) =>


Duplicate applyDefaultAccess definition.

--- lib/documents/person.coffee
+++ lib/documents/person.coffee
@@ -192,9 +192,6 @@

     document

-  @applyDefaultAccess: (personId, document) ->
-    document
-
 Meteor.person = (userId) ->
   # Meteor.userId is reactive
   userId ?= Meteor.userId()


Don't throw exceptions when emitting on a disposed emitter

--- src/emitter.coffee
+++ src/emitter.coffee
@@ -22,10 +22,7 @@
     new Disposable(@off.bind(this, eventName, handler))

   emit: (eventName, value) ->
-    if @isDisposed
-      throw new Error("Emitter has been disposed")
-
-    if handlers = @handlersByEventName[eventName]
+    if handlers = @handlersByEventName?[eventName]
       handler(value) for handler in handlers

   dispose: ->


convenience functions for bokehview, and fixed some tag based functions for bokehview

--- src/js/bokeh.coffee
+++ src/js/bokeh.coffee
@@ -14,17 +14,25 @@
 # Regular backbone view, except, it gets assigned an id.
 # this id can be used to auto-create html ids, and pull out
 # d3, and jquery nodes based on those dom elements
+
 class BokehView extends Backbone.View
   initialize : (options) ->
     if not _.has(options, 'id')
       this.id = _.uniqueId('BokehView')
   tag_id : (tag) ->
-    "tag" + "-" + this.id
+    tag + "-" + this.id
   tag_el : (tag) ->
-    $("#" + this.tag_id())
+    @$el.find("#" + this.tag_id(tag))
   tag_d3 : (tag) ->
-    d3.select("#" + this.tag_id())
-
+    val = d3.select(this.el).select("#" + this.tag_id(tag))
+    if val[0][0] == null
+      return null
+    else
+      return val
+  mget : (fld)->
+    return @model.get(fld)
+  mget_ref : (fld) ->
+    return @model.get_ref(fld)
 # HasReference
 # Backbone model, which can output a reference (combination of type, and id)
 # also auto creates an id on init, if one isn't passed in.


:bug: Update markers of all newly created editor linters

--- lib/linter.coffee
+++ lib/linter.coffee
@@ -104,6 +104,7 @@
       @linters.lint({onChange, editorLinter})
     editorLinter.onDidDestroy =>
       @messages.deleteEditorMessages(editor)
+    editorLinter.updateMarkers({removed: [], added: @views.messages})

   deactivate: ->
     @subscriptions.dispose()


Support for the rental lights

Support for the rental lights

--- lib/enttec.coffee
+++ lib/enttec.coffee
@@ -20,11 +20,17 @@

   set: (r, g, b) ->
-    @_buffer[0] = 0
-    @_buffer[1] = r
-    @_buffer[2] = g
-    @_buffer[3] = b
+    @_buffer[0] = r
+    @_buffer[1] = g
+    @_buffer[2] = b
+    @_buffer[3] = 255
     @_buffer[4] = 0
+
+    # @_buffer[0] = 0
+    # @_buffer[1] = r
+    # @_buffer[2] = g
+    # @_buffer[3] = b
+    # @_buffer[4] = 0

   execute: (cmd, cb) ->
     # TODO: assert values


:fire: comment

:fire: comment

--- lib/motion.coffee
+++ lib/motion.coffee
@@ -727,7 +727,6 @@
     @setBufferPositionSafely(cursor, point)
     unless @isRepeated()
       globalState.currentFind = this
-    # @editor.getCursorBufferPosition().toString()

 # keymap: F
 class FindBackwards extends Find
@@ -1051,7 +1050,6 @@
 class SearchCurrentLine extends Search
   @extend()
   quiet: true
-  # updateSearchHistory: false
   getScanRanges: ->
     [@editor.getLastCursor().getCurrentLineBufferRange()]


Use Unicode on PDFObject.s for CJK metadata

As a Chinese user,
I want Use Unicode to store PDF metadata,
So that I can read a Chinese Title&Author...

Not so familiar with coffeescript re-organize may be required

--- lib/object.coffee
+++ lib/object.coffee
@@ -42,6 +42,20 @@
             '' + object

     @s: (string) ->
+
+        # Convert Big-endian UCS-2 to Little-endian to support most PDFRreaders
+        swapBytes = (buff) ->
+            l = buff.length
+            if l & 0x01
+                throw new Error("Buffer length must be even")
+            else
+                for i in [0...l - 1] by 2
+                    a = buff[i]
+                    buff[i] = buff[i+1]
+                    buff[i+1] = a
+            buff
+        string = swapBytes(new Buffer('\ufeff' + string, 'ucs-2')).toString('binary')
+
         string = string.replace(/\\/g, '\\\\\\\\')
             .replace(/\(/g, '\\(')
             .replace(/\)/g, '\\)')


runner will print the current test name

runner will print the current test name

--- lib/runner.coffee
+++ lib/runner.coffee
@@ -43,7 +43,10 @@

     do @_createNewContext if @_config.runner.startTestsWithNewBrowser

-    @testCase = new (require @_files[@_fileIndex]) @_wd, @_browser, @_config, @errorHandler
+    testFile = @_files[@_fileIndex]
+    console.log 'Started: ' + testFile.replace @_config.root, ''
+
+    @testCase = new (require testFile) @_wd, @_browser, @_config, @errorHandler
     @_context = do @testCase
       .setContext @_context
       .test()


loop for cargo does not seems to be closing, forcing all cases

--- lib/server.coffee
+++ lib/server.coffee
@@ -451,8 +451,7 @@
 				if !r.ready or os.freemem() < config.minMem * 1048576
 					setImmediate ->
 						getDocsCargo.push couch_toprocess
-						return
-					callback()
+						callback()
 				else
 					keys = []
 					keys.push key for key in couch_toprocess
@@ -467,7 +466,7 @@
 							console.log 'error while getting DB data', bulkerr
 							setImmediate ->
 								getDocsCargo.push couch_toprocess
-								return
+								callback()
 						else
 							for item in bulkbody.rows
 								if item?.doc?.exec?
@@ -490,7 +489,8 @@
 										console.log "invalid doc",item.id
 										index = findplace(item.id)
 										workque[index].status = "dropped"
-						callback()
+								callback()
+
 			), config.readBatchsize)
 			syncJobs = (host, jobs) ->
 				if host? and Array.isArray(jobs)


Minor change.

Minor change.

--- lib/app/routes.coffee
+++ lib/app/routes.coffee
@@ -9,10 +9,10 @@

     assetsUrl = if process.env.PRODUCTION?
         console.log 'Using the production assetsUrl'
-        'http://songdown.herokuapp.com/static'
+        'https://songdown.herokuapp.com/static'
     else
         console.log 'Using the development assetsUrl'
-        "http://localhost:#{app.get 'port'}/static"
+        "https://localhost:#{app.get 'port'}/static"

     songDir = app.get 'songDir'


Dont autoclose JSX tags if multiple cursors are present. fixes #93

--- lib/atom-react.coffee
+++ lib/atom-react.coffee
@@ -213,6 +213,9 @@
     return if not @isReactEnabledForEditor(editor) or editor != atom.workspace.getActiveTextEditor()

     if eventObj?.newText is '>' and !eventObj.oldText
+      # auto closing multiple cursors is a little bit tricky so lets disable it for now
+      return if editor.getCursorBufferPositions().length > 1;
+
       tokenizedLine = editor.displayBuffer?.tokenizedBuffer?.tokenizedLineForRow(eventObj.newRange.end.row)
       return if not tokenizedLine?


Add trailing newline to Users collection input test fixture

--- test/fixtures/input/collections/users.coffee
+++ test/fixtures/input/collections/users.coffee
@@ -1,2 +1,2 @@
 class RootNamespace.Collections.Users extends Backbone.Collection
-  model: RootNamespace.Models.User+  model: RootNamespace.Models.User


added to navbar update a backButton param for a custom backbutton

--- testApp/app/controllers/navigationbar.coffee
+++ testApp/app/controllers/navigationbar.coffee
@@ -150,11 +150,11 @@
     }

   @testSetBackButtonImage: ->
-      customBack = new steroids.buttons.NavigationBarButton
-      customBack.imagePath = "/icons/pill@2x.png"
-      steroids.view.navigationBar.update {
-        backButton: customBack
-      }
+    customBack = new steroids.buttons.NavigationBarButton
+    customBack.imagePath = "/icons/pill@2x.png"
+    steroids.view.navigationBar.update {
+      backButton: customBack
+    }

   @testUpdateButtonsWithoutBackButton: ->
     button1 = new steroids.buttons.NavigationBarButton


fixed broken test on phantomjs

fixed broken test on phantomjs

--- tests/batman/view/filter_parsing_test.coffee
+++ tests/batman/view/filter_parsing_test.coffee
@@ -434,8 +434,7 @@
     foo: spy = createSpy()

   helpers.render '<a data-event-click="foo | withArguments 2">', context, (node) ->
-    node[0].click()
-
+    helpers.triggerClick(node[0])
     ok spy.called
     equal spy.lastCallArguments[0], 2
     QUnit.start()


Prefix the function with a space

Prefix the function with a space

The underlying bug is in the sexp-formatter which
always treats strings starting with : as symbol.
This breaks clauses like ":|:" since they are
not being quoted

This fixes #185

--- lib/idris-controller.coffee
+++ lib/idris-controller.coffee
@@ -238,11 +238,13 @@
     @saveFile editor
     uri = editor.getURI()
     line = editor.getLastCursor().getBufferRow()
-    word = editorHelper.getWordUnderCursor editor
+    # by adding a clause we make sure that the word is
+    # not treated as a symbol
+    word = ' ' + editorHelper.getWordUnderCursor editor

     @clearMessagePanel 'Idris: Add clause ...'

-    successHandler = ({ responseType, msg }) =>
+    successHandler =  ({ responseType, msg }) =>
       [clause] = @prefixLiterateClause msg

       @hideAndClearMessagePanel()
@@ -327,7 +329,7 @@
         .filter ({ responseType }) -> responseType == 'return'
         .flatMap => @model.makeWith line + 1, word
         .subscribe successHandler, @displayErrors
-    else
+    else
       @clearMessagePanel "Idris: Illegal position to make a with view"

   # construct a lemma from a hole


log message after successful steroids-configure

--- tasks/steroids-configure.coffee
+++ tasks/steroids-configure.coffee
@@ -4,16 +4,19 @@
 module.exports = (grunt)->

   grunt.registerTask "steroids-configure", "Read XML configuration files from www/ and output a JSON to dist/", ->
-    configPath = "www/config.xml"
-    if not grunt.file.isFile configPath
-      grunt.log.writeln "No file found at #{configPath}, skipping."
+    configXmlPath = "www/config.xml"
+    distConfigJsonPath = "dist/config.json"
+
+    if not grunt.file.isFile configXmlPath
+      grunt.log.writeln "No file found at #{configXmlPath}, skipping."
       return

     done = @async()

-    configXml = grunt.file.read configPath
+    configXml = grunt.file.read configXmlPath
     SteroidsConfigure.fromXml configXml, (err, json) ->
       throw new Error err if err?
-      grunt.file.write "dist/config.json", json
+      grunt.file.write distConfigJsonPath, json
+      grunt.log.writeln "Parsed #{configXmlPath} and wrote corresponding data to #{distConfigJsonPath}."

       done()


Fix KDOnOffSwitch make it fast.

Fix KDOnOffSwitch make it fast.

--- client/Framework/components/inputs/KDOnOffSwitch.coffee
+++ client/Framework/components/inputs/KDOnOffSwitch.coffee
@@ -5,76 +5,68 @@
 class KDOnOffSwitch extends KDInputView
   constructor:(options = {}, data)->

-    options.type   or= "switch"       # mandatory
-    options.size   or= "small"        # a String tiny/small/big
-    options.labels or= ["ON", "OFF"]
+    options.title        or= ""
+    options.type         or= "switch"       # mandatory
+    options.size         or= "small"        # a String tiny/small/big
+    options.labels       or= ["ON", "OFF"]
+    options.defaultValue  ?= off
     super options, data

     @setClass options.size
     @setPartial "<input class='checkbox hidden no-kdinput' type='checkbox' name='#{@getName()}'/>"

-    if @getOptions().defaultValue?
-      @setDefaultValue @getOptions().defaultValue
+    @setDefaultValue options.defaultValue

   setDomElement:(cssClass)->
     {title, labels, name} = @getOptions()
-    @inputName = name
-    title or= ""
-
+    title       = "#{title}" if title
+    @inputName  = name
     @domElement = $ """

-        #{title}
+        #{title}
         #{labels[0]}#{labels[1]}

 """

+  getValue:-> @$("input").attr("checked") is "checked"
+  setValue:(value, wCallback = yes)->
+    switch value
+      when on   then @setOn wCallback
+      when off  then @setOff wCallback
+
   setDefaultValue:(value) ->
     switch value
-      when on,"on","true","yes",1 then @_setDefaultValue on
-      else @_setDefaultValue off
+      when on,"on","true","yes",1 then @setValue on, no
+      else @setValue off, no

-  getDefaultValue:()-> @inputDefaultValue
+  setOff:(wCallback = yes)->
+    return if not @getValue() and wCallback

-  getValue:()-> @getDomElement().find("input").eq(0).is ":checked"
-  setValue:(value)->
-    switch value
-      when on   then @_setOn()
-      when off  then @_setOff()
+    @$("input").attr "checked", no

-  _setDefaultValue: (val) ->
-    setTimeout =>
-      val = !!val
-      if val then @_setOn no else @_setOff no
-    , 0
-
-  _setOff:(wCallback = yes)->
-    return if not @getValue() and wCallback
-    @inputDefaultValue = off
-    @getDomElement().find("input").eq(0).attr "checked",no
     @$('a.on').removeClass('active')
     @$('a.off').addClass('active')
+
     @switchStateChanged() if wCallback

-  _setOn:(wCallback = yes)->
+  setOn:(wCallback = yes)->
     return if @getValue() and wCallback
-    @inputDefaultValue = on
-    @getDomElement().find("input").eq(0).attr "checked",yes
+
+    @$("input").attr "checked", yes
+
     @$('a.off').removeClass('active')
     @$('a.on').addClass('active')
+
     @switchStateChanged() if wCallback

-  switchStateChanged:()->
-    # log "new state of #{@getName()} is #{@getValue()}",@getCallback()?
+  switchStateChanged:->
     @emit 'SwitchStateChanged', @getValue()
-    @getCallback().call @,@getValue() if @getCallback()?
+    @getCallback().call @, @getValue() if @getCallback()?

   mouseDown:(event)->
     if $(event.target).is('a.on')
       @setValue on
     else if $(event.target).is('a.off')
       @setValue off
-    # no
-
-

 class KDCheckBox extends KDInputView
   constructor:(options = {}, data)->


account/shortcuts: cancel out current recording session when clicked on x

--- client/account/lib/views/accounteditshortcutsrow.coffee
+++ client/account/lib/views/accounteditshortcutsrow.coffee
@@ -3,6 +3,7 @@
 facade = require('./accounteditshortcutsfacade')
 EventType = require './accounteditshortcutseventtype'
 globals = require 'globals'
+recorder = require 'record-shortcuts'

 # On Mac we display corresponding unicode chars for the following keys.
 # See: http://macbiblioblog.blogspot.nl/2005/05/special-key-symbols.html
@@ -63,6 +64,9 @@
   # Class name to set for duplicate/colliding items.
   DUP_CLASS_NAME = 'collides'

+  # Class name that we use to catch cancel click.
+  CANCEL_CLASS_NAME = 'cancel'
+
   # The maximum description string length.
   DESCRIPTION_TRUNC_LEN = 30

@@ -74,6 +78,8 @@
   CLASS_NAME = 'row'

   TOGGLE_PARTIAL = '
'
+
+  BINDING_ACTIVE_PARTIAL = ''

   constructor: (options, data) ->

@@ -126,13 +132,15 @@

     # Handles dom click events.
     #
-    clickHandler = =>
-      return  if @_active
+    clickHandler = (e) =>
+      if @_active
+        recorder.cancel()  if e.target.className is CANCEL_CLASS_NAME
+        return

       @setClass ACTIVE_CLASS_NAME
       @unsetClass DUP_CLASS_NAME
       @_active = yes
-      binding.updatePartial ''
+      binding.updatePartial BINDING_ACTIVE_PARTIAL

       # Start a recording session.
       @emit EventType.Item.SELECTED,


Resurrection: group switcher popup removed from first menu

--- client/app/MainApp/avatararea/avatarareaiconmenu.coffee
+++ client/app/MainApp/avatararea/avatarareaiconmenu.coffee
@@ -6,42 +6,28 @@

     @setClass "account-menu"

-    sidebar  = @getDelegate()
-
     @notificationsPopup = new AvatarPopupNotifications
       cssClass : "notifications"
-      delegate : sidebar

     @messagesPopup = new AvatarPopupMessages
       cssClass : "messages"
-      delegate : sidebar
-
-    @groupSwitcherPopup = new AvatarPopupGroupSwitcher
-      cssClass : "group-switcher"
-      delegate : sidebar

     @notificationsIcon = new AvatarAreaIconLink
-      cssClass   : 'notifications'
+      cssClass   : 'notifications acc-dropdown-icon'
       attributes :
         title    : 'Notifications'
       delegate   : @notificationsPopup

     @messagesIcon = new AvatarAreaIconLink
-      cssClass   : 'messages'
+      cssClass   : 'messages acc-dropdown-icon'
       testPath   : "avatararea-messages-icon"
       attributes :
         title    : 'Messages'
       delegate   : @messagesPopup

-    @groupsSwitcherIcon = new AvatarAreaIconLink
-      cssClass   : 'group-switcher hidden'
-      attributes :
-        title    : 'Your groups'
-      delegate   : @groupSwitcherPopup
-
     @accountIcon = new KDCustomHTMLView
       tagName    : "a"
-      cssClass   : 'account'
+      cssClass   : 'account acc-dropdown-icon'
       attributes :
         title    : 'Account Settings'
         href     : "#"
@@ -50,13 +36,14 @@
         KD.getSingleton('router').handleRoute '/Account'
       partial    : ""

+
   pistachio:->
     """
     {{> @accountIcon}}
     {{> @messagesIcon}}
     {{> @notificationsIcon}}
-    {{> @groupsSwitcherIcon}}
     """
+

   viewAppended:->

@@ -66,9 +53,9 @@

     mainView.addSubView @notificationsPopup
     mainView.addSubView @messagesPopup
-    mainView.addSubView @groupSwitcherPopup

     @attachListeners()
+

   attachListeners:->
     KD.getSingleton('notificationController').on 'NotificationHasArrived', ({event})=>
@@ -93,22 +80,13 @@
       else @messagesPopup.noMessage.show()
       @messagesIcon.updateCount count

-    @groupSwitcherPopup.listControllerPending.on 'PendingGroupsCountDidChange', (count)=>
-      if count > 0
-        @groupSwitcherPopup.invitesHeader.show()
-        @groupSwitcherPopup.switchToTitle.unsetClass 'top'
-      else
-        @groupSwitcherPopup.invitesHeader.hide()
-        @groupSwitcherPopup.switchToTitle.setClass 'top'
-      @groupsSwitcherIcon.updateCount count

   accountChanged:(account)->

-    {notificationsPopup, messagesPopup, groupSwitcherPopup} = @
+    {notificationsPopup, messagesPopup} = this

     messagesPopup.listController.removeAllItems()
     notificationsPopup.listController.removeAllItems()
-    groupSwitcherPopup.listController.removeAllItems()

     if KD.isLoggedIn()

@@ -118,8 +96,3 @@

       # Fetch Private Messages
       messagesPopup.listController.fetchMessages()
-
-      # Fetch Groups
-      groupSwitcherPopup.populateGroups()
-      groupSwitcherPopup.populatePendingGroups()
-


improve paginator

improve paginator

--- client/app/components/pagination_links_component.coffee
+++ client/app/components/pagination_links_component.coffee
@@ -22,18 +22,23 @@
     if pEnd < 1
       pEnd = 1

+    pStart -= needAddStart
+    pEnd += needAddEnd
+    pStart = 1 if pStart < 1
+    pEnd = pagesCount if pEnd > pagesCount
+
+
     pagesArray = for pageNum in [pStart..pEnd]
       {pageNum, active: pageNum==curPageNum}
-
     pPrev = no
     if pStart > 1
-      pPrev = curPageNum - 11
+      pPrev = pStart - 6
       if pPrev < 1
         pPrev = 1

     pNext = no
     if pEnd < pagesCount
-      pNext = curPageNum + 11
+      pNext = pEnd + 6
       if pNext > pagesCount
         pNext = pagesCount


component-lab: fix require path for TrialEndedMemberModal

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/component-lab/TrialEndedMemberModal/index.coffee
+++ client/component-lab/TrialEndedMemberModal/index.coffee
@@ -1 +1 @@
-module.exports = require './TrialEndedModal'
+module.exports = require './TrialEndedMemberModal'


analytics: add signup event

analytics: add signup event

--- client/landing/site.landing/coffee/login/AppView.coffee
+++ client/landing/site.landing/coffee/login/AppView.coffee
@@ -505,6 +505,13 @@
       xhrFields   : withCredentials : yes
       success     : ->
         document.cookie = 'newRegister=true'
+
+        KD.utils.defer ->
+          KD.utils.trackEvent 'Signup, success',
+            category : 'userInteraction'
+            action   : 'microConversions'
+            label    : 'userSignup'
+
         return location.replace "/#{redirectTo}#{query}"

       error       : (xhr) =>


fix bug where banned users weren't shown modal during login

--- client/landing/site.landing/coffee/login/AppView.coffee
+++ client/landing/site.landing/coffee/login/AppView.coffee
@@ -562,13 +562,13 @@
       error       : (xhr) =>
         {responseText} = xhr
         if /suspended/i.test responseText
-        then @handleBanned()
+        then @handleBanned responseText
         else new KDNotificationView title : responseText

         @loginForm.button.hideLoader()

-  handleBanned: ->
+  handleBanned: (responseText)->
     new KDModalView
       title        : "You've been banned!"
       content      : responseText


fix for tabs failing to be temp in latest Atom

--- lib/sublime-tab-view.coffee
+++ lib/sublime-tab-view.coffee
@@ -10,8 +10,7 @@
     super(@item, @pane)
     return unless considerTemporary

-    if @item.constructor.name is 'Editor' ||
-       @item.constructor.name is 'ImageEditor'
+    if @item.constructor.name in ['Editor', 'ImageEditor', 'TextEditor']
       if @item.getPath() in openPermanent
         _.remove(openPermanent, @item.getPath())
       else


making 'no such task' errors a little nicer

--- cake.coffee
+++ cake.coffee
@@ -5,6 +5,10 @@
 coffee:   require 'coffee-script'

 tasks: {}
+
+no_such_task: (task) ->
+  process.stdio.writeError('No such task: "' + task + '"\n')
+  process.exit(1)

 # Mixin the Cake functionality.
 process.mixin {
@@ -15,6 +19,7 @@

   # Invoke another task in the Cakefile.
   invoke: (name) ->
+    no_such_task name unless tasks[name]
     tasks[name].action()
 }

@@ -35,6 +40,6 @@
       eval coffee.compile source
       return print_tasks() unless args.length
       for arg in args
-        throw new Error('No such task: "' + arg + '"') unless tasks[arg]
+        no_such_task arg unless tasks[arg]
         tasks[arg].action()


Cleaned up tests for type resolution. Added tests for implicit arrays.

--- test/typeresolution-test.coffee
+++ test/typeresolution-test.coffee
@@ -33,8 +33,6 @@
   describe "on Named Type node", ->
     testTypeResolution "if given with explicit type",
       astTreeNode:
-        name:
-          name: 'Person'
         base:
           typeSpecification:
             name:
@@ -43,8 +41,6 @@

     testTypeResolution "if given with ‘array’ type and some nested types",
       astTreeNode:
-        name:
-          name: 'Some Elements'
         base:
           typeSpecification:
             name:
@@ -59,8 +55,6 @@

     testTypeResolution "if given with ‘enum’ type and some nested types",
       astTreeNode:
-        name:
-          name: 'Some Elements'
         base:
           typeSpecification:
             name:
@@ -75,8 +69,6 @@

     testTypeResolution "if given with nested types for type other than ‘array’ or ‘enum’",
       astTreeNode:
-        name:
-          name: 'Some Elements'
         base:
           typeSpecification:
             name:
@@ -90,8 +82,6 @@

     testTypeResolution "if given with member sections and with no explicit type",
       astTreeNode:
-        name:
-          name: 'Person'
         sections: [
             type: 'member'
           ,
@@ -100,15 +90,11 @@
       typeName: 'object'

     testTypeResolution "if given with no explicit type",
-      astTreeNode:
-        name:
-          name: 'Person'
+      astTreeNode: {}
       typeName: 'string'

     testTypeResolution "if given with type, which is not primitive",
       astTreeNode:
-        name:
-          name: 'Person'
         base:
           typeSpecification:
             name:
@@ -171,11 +157,6 @@

     testTypeResolution "if given with member sections and with no explicit type",
       astTreeNode:
-        valueDefinition:
-          typeDefinition:
-            typeSpecification:
-              name:
-                name: null
         sections: [
             type: 'member'
           ,
@@ -184,13 +165,31 @@
       typeName: 'object'

     testTypeResolution "if given with no explicit type",
-      astTreeNode:
-        valueDefinition:
-          typeDefinition:
-            typeSpecification:
-              name:
-                name: null
+      astTreeNode: {}
       typeName: 'string'
+
+    testTypeResolution "if given with multiple values and with no explicit type",
+      astTreeNode:
+        valueDefinition:
+          values: [
+              literal: 'home'
+            ,
+              literal: 'green'
+          ]
+      typeName: 'array'
+
+    testTypeResolution "if given with member sections and multiple values and with no explicit type",
+      astTreeNode:
+        valueDefinition:
+          values: [
+              literal: 'home'
+            ,
+              literal: 'green'
+          ]
+        sections: [
+          type: 'member'
+        ]
+      errContains: 'ambiguous'

     testTypeResolution "if given with type, which is not primitive",
       astTreeNode:


crunchtask.js-7 Provide out of the box `for` loop - Provided tests

--- test/unit/crunchtaskSpec.coffee
+++ test/unit/crunchtaskSpec.coffee
@@ -565,7 +565,7 @@
         result2 = task.run(2)

         setTimeout ->
-          console.log 'after ' + timeoutAmount + 'ms'
+#          console.log 'after ' + timeoutAmount + 'ms'
           expect(foo.bar.calls.count()).toEqual(2)
           expect(memo[1]).toEqual(1)
           expect(memo[2]).toEqual(1)
@@ -573,7 +573,7 @@
         , timeoutAmount + safetyMargin

         setTimeout ->
-          console.log 'after ' + 2 * timeoutAmount + 'ms'
+#          console.log 'after ' + 2 * timeoutAmount + 'ms'
           expect(foo.bar.calls.count()).toEqual(3)
           expect(memo[1]).toEqual(1)
           expect(memo[2]).toEqual(2)
@@ -581,7 +581,7 @@
         , 2 * timeoutAmount + safetyMargin

         setTimeout ->
-          console.log 'after ' + 3 * timeoutAmount + 'ms'
+#          console.log 'after ' + 3 * timeoutAmount + 'ms'
           expect(foo.bar.calls.count()).toEqual(5)
           expect(memo[1]).toEqual(2)
           expect(memo[2]).toEqual(3)
@@ -641,7 +641,6 @@
             do resolve
           return
         task.onIdle ()->
-          debugger
           expect(task.isIdle()).toBe(true)
           done()

@@ -652,6 +651,126 @@

         return

+      return
+
+    describe 'Convenience method `for`', ->
+
+      foo = {}
+
+      beforeEach(()->
+        foo.bar = (() ->
+        )
+      )
+
+      it 'declares static `for` method', ()->
+        expect(CrunchTask.for).toBeDefined()
+        expect(type(CrunchTask.for)).toEqual('function')
+        return
+
+      it 'once called, the `for` method creates a CrunchTask instance', ()->
+        forloop = CrunchTask.for(()->)
+        expect(forloop instanceof CrunchTask).toBe(true);
+        return
+
+      it '`for` method accepts loop control numbers and a worker function or a task', (done)->
+
+        spyOn(foo, 'bar').and.callThrough()
+
+        forloop = CrunchTask.for(0, 2, foo.bar)
+
+        expect(foo.bar).not.toHaveBeenCalled()
+
+        forloop.run()
+
+        setTimeout(()->
+          expect(foo.bar).toHaveBeenCalled()
+          expect(foo.bar.calls.count()).toEqual(2)
+          expect(foo.bar.calls.argsFor(0)).toEqual([0])
+          done()
+        , 10)
+
+        return
+
+      it '`run` methods supercedes loop control values given to the `for` method', (done)->
+
+        spyOn(foo, 'bar').and.callThrough()
+
+        forloop = CrunchTask.for(0, 2, foo.bar)
+
+        expect(foo.bar).not.toHaveBeenCalled()
+
+        forloop.run(0, 5)
+
+        setTimeout(()->
+          expect(foo.bar).toHaveBeenCalled()
+          expect(foo.bar.calls.count()).toEqual(5)
+          expect(foo.bar.calls.argsFor(0)).toEqual([0])
+          expect(foo.bar.calls.argsFor(1)).toEqual([1])
+          expect(foo.bar.calls.argsFor(2)).toEqual([2])
+          expect(foo.bar.calls.argsFor(3)).toEqual([3])
+          expect(foo.bar.calls.argsFor(4)).toEqual([4])
+          done()
+        , 10)
+
+        return
+
+      it 'loop-control values may be supplied in groups, as arrays of 1..3 elements', (done)->
+
+        spyOn(foo, 'bar').and.callThrough()
+
+        forloop = CrunchTask.for([0, 2], [0, 2], foo.bar)
+
+        expect(foo.bar).not.toHaveBeenCalled()
+
+        forloop.done( ()->
+          expect(foo.bar).toHaveBeenCalled()
+          expect(foo.bar.calls.count()).toEqual(4)
+          expect(foo.bar.calls.argsFor(0)).toEqual([0, 0])
+          expect(foo.bar.calls.argsFor(1)).toEqual([1, 0])
+          expect(foo.bar.calls.argsFor(2)).toEqual([0, 1])
+          expect(foo.bar.calls.argsFor(3)).toEqual([1, 1])
+          done()
+        )
+
+        forloop.run()
+        return
+
+      it 'loop-control values in the group may initiate a countdown loop..', (done)->
+
+        spyOn(foo, 'bar').and.callThrough()
+
+        forloop = CrunchTask.for([1, -1], [1, -1], foo.bar)
+
+        expect(foo.bar).not.toHaveBeenCalled()
+
+        forloop.done( ()->
+          expect(foo.bar).toHaveBeenCalled()
+          expect(foo.bar.calls.count()).toEqual(4)
+          expect(foo.bar.calls.argsFor(0)).toEqual([1, 1])
+          expect(foo.bar.calls.argsFor(1)).toEqual([0, 1])
+          expect(foo.bar.calls.argsFor(2)).toEqual([1, 0])
+          expect(foo.bar.calls.argsFor(3)).toEqual([0, 0])
+          done()
+        )
+
+        forloop.run()
+        return
+
+      it 'if no function is supplied, the loop resolves automatically when started', (done)->
+        spyOn(foo, 'bar')
+        forloop = CrunchTask.for()
+        forloop.done(foo.bar)
+
+        expect(foo.bar.calls.any()).toEqual(false)
+
+        forloop.run()
+
+        setTimeout(()->
+          expect(foo.bar.calls.any()).toEqual(true)
+          done()
+        , 10)
+
+        return
       return

     describe 'Examples in the readme.md file', ->


add tag for inversions

add tag for inversions

--- lib/fingerings.coffee
+++ lib/fingerings.coffee
@@ -24,7 +24,7 @@

   @cached_getter 'chordName', ->
     name = @chord.name
-    name += " / #{getPitchClassName(@instrument.pitchAt(@positions[0]))}" unless @inversion == 0
+    name += " / #{getPitchClassName(@instrument.pitchAt(@positions[0]))}" if @inversion > 0
     return name

   # @cached_getter 'pitches', ->
@@ -35,6 +35,10 @@

   @cached_getter 'inversion', ->
     @chord.pitchClasses.indexOf intervalClassDifference(@chord.rootPitch, @instrument.pitchAt(@positions[0]))
+
+  @cached_getter 'inversionLetter', ->
+    return unless @inversion > 0
+    return String.fromCharCode(96 + @inversion)

 #
@@ -227,7 +231,7 @@
     root: isRootPosition
     barres: (f) -> f.barres.length
     fingers: getFingerCount
-    inverted: (f) -> not isRootPosition(f)
+    inversion: (f) -> f.inversionLetter
     skipping: /\dx\d/
     muting: /\dx/
     open: /0/


:bug: restoreEditorState might not be defined.

--- lib/motion.coffee
+++ lib/motion.coffee
@@ -757,7 +757,7 @@
     @vimState.reset()
     @subscriptions?.dispose()
     @subscriptions = null
-    @restoreEditorState()
+    @restoreEditorState?()

   onDidChange: (@input) =>
     for c in @editor.getCursors()