Test names for the rest of Mundle install tests

Testing path resolution when not explicitly declared in package.json
Testing path resolution for nested (synchronous) calls
Test error if path to a single file cannot be resolved

--- tests/main.coffee
+++ tests/main.coffee
@@ -116,6 +116,7 @@
 ###
 	Currently we're not supporting versioning. But some of the client-side work for it was done already, so the tests exercise those cases.
 ###
+# 'nested' refers to parsed synchronous require calls in a file vs. top level, i.e. an asynchronous require
 exports.testMundle =
 	standardPackage : (test)->
 		test.expect 2
@@ -148,6 +149,18 @@
 			test.ifError errors
 			test.equal results['/m/underscore/0.0.0/underscore-min.js'], contents, 'Should get file contents of main js file'
 			test.done()
+
+	nestedDependencies : (test)->
+		test.expect 1
+		test.done()
+
+	noPackageJsonWithIndexJs : (test)->
+		test.expect 1
+		test.done()
+
+	noPackageJsonWithMainJs : (test)->
+		test.expect 1
+		test.done()

 # 'nested' refers to parsed synchronous require calls in a file vs. a top level error in an asynchronous require
 exports.testErrorFormatting =
@@ -198,6 +211,10 @@
 			test.equal errors['/b/something/Hidden1.js'].message, 'Unable to read file'
 			test.equal errors['/b/something/Hidden1.js'].path, '../something/Hidden1.js'
 			test.done()
+
+	mundleCouldntResolveFile : (test)->
+		test.expect 1
+		test.done()

 exports.testCache =
 	setUp : (callback)->


Animationframe is no longer needed

Animationframe is no longer needed

--- src/core/animationframe.coffee
+++ src/core/animationframe.coffee
@@ -1,29 +0,0 @@
-define "requestAnimationFrame", ["root"], (root) ->
-
-  # frameRate is only used if requestAnimationFrame is not available
-  frameRate = 60
-  requestAnimationFrame = root.requestAnimationFrame
-  vendors = ["ms", "moz", "webkit", "o"]
-  x = 0
-
-  while x < vendors.length and not window.requestAnimationFrame
-    requestAnimationFrame = root[vendors[x] + "RequestAnimationFrame"]
-    break  if requestAnimationFrame
-    ++x
-  unless requestAnimationFrame
-    requestAnimationFrame = (callback) ->
-      window.setTimeout callback, ~~(1000 / window.frameRate)
-  requestAnimationFrame
-
-define "cancelAnimationFrame", ["root"], (root) ->
-  cancelAnimationFrame = root.cancelAnimationFrame
-  vendors = ["ms", "moz", "webkit", "o"]
-  x = 0
-
-  while x < vendors.length and not window.requestAnimationFrame
-    cancelRequestAnimationFrame = root[vendors[x] + "CancelRequestAnimationFrame"]
-    break  if cancelAnimationFrame
-    ++x
-  unless cancelAnimationFrame
-    cancelAnimationFrame = (id) ->
-      clearTimeout id


Use status code 403 forbidden instead of 401 unauthorized

Also returns a pretty JSON

--- packages/rocketchat-api/server/api.coffee
+++ packages/rocketchat-api/server/api.coffee
@@ -28,7 +28,7 @@

 	unauthorized: (msg) ->
 		return {} =
-			statusCode: 401
+			statusCode: 403
 			body:
 				success: false
 				error: msg or 'unauthorized'
@@ -40,7 +40,7 @@
 RocketChat.API.v1 = new API
 	version: 'v1'
 	useDefaultAuth: true
-	prettyJson: false
+	prettyJson: true
 	enableCors: false
 	auth:
 		token: 'services.resume.loginTokens.hashedToken'


Don't error when tether is disabled

Don't error when tether is disabled

--- coffee/drop.coffee
+++ coffee/drop.coffee
@@ -212,7 +212,7 @@
       setTimeout =>
         addClass @drop, "#{ drop.classPrefix }-after-open"

-      @tether.position()
+      @tether?.position()

       @trigger 'open'


Fix typo (src -> @src)

Fix typo (src -> @src)

--- coffee/hmms.coffee
+++ coffee/hmms.coffee
@@ -7,7 +7,7 @@
     @sunday_holiday = 0

     # 時刻表データを読み込む
-    rowData = @loadTimeTable(src).split(String.fromCharCode(10))
+    rowData = @loadTimeTable(@src).split(String.fromCharCode(10))
     for i in [0...rowData.length]
       @tt[i] = rowData[i].split(',')


Don't call code that doesn't exist

Don't call code that doesn't exist

--- coffee/main.coffee
+++ coffee/main.coffee
@@ -167,7 +167,6 @@
     @ctx.restore()

   clear: ->
-    @undoStack.push(@shapes)
     @shapes = []
     @repaint()


stray space

stray space

--- coffee/main.coffee
+++ coffee/main.coffee
@@ -1,6 +1,6 @@
 URL_BASE = 'http://ireproof.org'

-linkClicked = (event) ->
+linkClicked = (event) ->
     linkText = $(event.target).text()
     internalLink = true
     url = '#'


add argument type checking

add argument type checking

--- src/celerity.coffee
+++ src/celerity.coffee
@@ -6,12 +6,18 @@
     return ('number' is typeof n) and (n % 1 is 0)

 checkConfig = (config) ->
+    unless config?
+        throw new Error 'config argument is missing'
     unless 'object' is typeof config
         throw new Error 'config argument must be an object'
     unless 'object' is typeof config.redis
         throw new Error 'config.redis argument must be an object'
+    unless config.timespanMs?
+        throw new Error 'config.timespanMs argument is missing'
     unless isInt config.timespanMs
         throw new Error 'config.timespanMs argument must be an integer'
+    unless config.timespanMs?
+        throw new Error 'config.bucketCount argument is missing'
     unless isInt config.bucketCount
         throw new Error 'config.bucketCount argument must be an integer'
     if config.prefix? and 'string' isnt typeof config.prefix
@@ -39,28 +45,44 @@

 module.exports =

-    increment: (config, name, n, cb) ->
+    increment: (config, name, increment, cb) ->
         checkConfig config
+        unless 'string' is typeof name
+            throw new Error 'name argument must be a string'
+        unless isInt increment
+            throw new Error 'increment argument must be an integer'
+        unless 'function' is typeof cb
+            throw new Error 'cb argument must be a function'

         config.redis.eval lua.increment, 1,
             getBucketKey(config, name, Date.now()),
-            n,
+            increment,
             getExpire(config),
             cb

-    incrementAndRead: (config, name, n, cb) ->
+    incrementAndRead: (config, name, increment, cb) ->
         checkConfig config
+        unless 'string' is typeof name
+            throw new Error 'name argument must be a string'
+        unless isInt increment
+            throw new Error 'increment argument must be an integer'
+        unless 'function' is typeof cb
+            throw new Error 'cb argument must be a function'

         config.redis.eval lua.incrementAndRead, 2,
             getKey(config, name),
             getBucketKey(config, name, Date.now()),
-            n,
+            increment,
             getExpire(config),
             config.bucketCount,
             cb

     read: (config, name, cb) ->
         checkConfig config
+        unless 'string' is typeof name
+            throw new Error 'name argument must be a string'
+        unless 'function' is typeof cb
+            throw new Error 'cb argument must be a function'

         config.redis.eval lua.read, 1,
             getKey(config, name),


Update browser test.

Update browser test.

--- test/browser/account.coffee
+++ test/browser/account.coffee
@@ -23,7 +23,7 @@
       catch err

       err.status.should.eq 400
-      err.message.should.eq 'Email is not valid'
+      err.message.should.eq "Email '#{firstName}' is not valid"

     it 'should not allow firstName to be blank', ->
       try


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -10,6 +10,7 @@

   equals engine.find('.findable').length, 1

-  engine.
+  engine.add
+    find

 module()


Added EnterText to PhantomJS CoffeeScript commands

--- FluentAutomation.PhantomJS/PhantomDriver.coffee
+++ FluentAutomation.PhantomJS/PhantomDriver.coffee
@@ -285,8 +285,24 @@

 	Type: (text) ->
 		@owner.throw "NotImplementedException"
+
+	EnterText: (command) ->
+		selector = command.selector if command.selector?
+		text = command.text if command.text?
+
+		fn = ->
+			opt = $('SELECTOR')
+			opt.val('VALUE')
+			if opt.length > 0 then true else false
+
+		success = page.evaluate fn.toString().replace( 'SELECTOR', selector ).replace( 'VALUE', text )
+
+		if !success
+			@owner.throw "Could not find element '" + selector + "'"
+
+		@CompleteAction()

 	IncludeJQuery: () ->
 		page.includeJs "http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"

-new PhantomDriver()+new PhantomDriver()


Added delayed response support to diagnostic commands

--- src/scripts/diagnostics.coffee
+++ src/scripts/diagnostics.coffee
@@ -108,23 +108,33 @@
     body = req.body
     if body.token is process.env.SLASH_PAYROLL_TOKEN and
        isAdminUser body.user_name
-      comps = body.text || []
-      start = if comps[0] then moment comps[0] else moment().subtract 2, 'weeks'
-      end = if comps[1] then moment comps[1] else moment()
-      Organization.generateReport(start, end)
-      .catch((err) ->
+      response_url = body.response_url
+      if response_url
+        comps = body.text || []
+        start = if comps[0] then moment comps[0] else moment().subtract 2, 'weeks'
+        end = if comps[1] then moment comps[1] else moment()
+        Organization.generateReport(start, end)
+        .catch(
+          (err) ->
+            robot.http(response_url)
+            .header('Content-Type', 'application/json')
+            .post({
+              "text": "Failed to produce a salary report"
+            })
+        )
+        .done(
+          (numberDone) ->
+            robot.http(response_url)
+            .header('Content-Type', 'application/json')
+            .post({
+                "text": "Salary report generated for #{numberDone} employees"
+            })
+        )
+      else
         res.status 500
         res.json {
-          "text": "Failed to produce a salary report"
+          "text": "No return url provided by Slack"
         }
-      )
-      .done(
-        (numberDone) ->
-          res.status 200
-          res.json {
-            "text": "Salary report generated for #{numberDone} employees"
-          }
-      )
     else
       res.status 401
       res.json {
@@ -134,19 +144,30 @@
   robot.router.post '/ibizan/diagnostics/sync', (req, res) ->
     body = req.body
     if body.token is process.env.SLASH_SYNC_TOKEN
-      Organization.sync()
-      .catch((err) ->
+      response_url = body.response_url
+      if response_url
+        Organization.sync()
+        .catch(
+          (err) ->
+            robot.http(response_url)
+            .header('Content-Type', 'application/json')
+            .post({
+              "text": "Failed to resync"
+            })
+        )
+        .done(
+          (status) ->
+            robot.http(response_url)
+            .header('Content-Type', 'application/json')
+            .post({
+              "text": "Re-synced with spreadsheet"
+            })
+        )
+      else
         res.status 500
         res.json {
-          "text": "Failed to resync"
+          "text": "No return url provided by Slack"
         }
-      )
-      .done((status) ->
-        res.status 200
-        res.json {
-          "text": "Re-synced with spreadsheet"
-        }
-      )
     else
       res.status 401
       res.json {


major rewrite adds HbA1C estimation

major rewrite adds HbA1C estimation

--- src/diabetes.coffee
+++ src/diabetes.coffee
@@ -5,21 +5,63 @@
 #   None
 #
 # Configuration:
-#   None
+#   HUBOT_GLUCOSE_UNIT_THRESHOLD
 #
 # Commands:
-#   None
+#   hubot estimate a1c [from average] <glucose level> - Estimates average HbA1C
 #
 # Author:
 #   awaxa

+conversionratio = 18.0182
+
+mgdlToMmol = (n) ->
+  return n / conversionratio
+
+mmolToMgdl = (n) ->
+  return n * conversionratio
+
+mgdlToDcct = (n) ->
+  return (parseInt(n, 10)+46.7)/28.7
+
+mgdlToIfcc = (n) ->
+  return (mgdlToDcct(n)-2.15)*10.929
+
+tenths = (n) ->
+  Math.round(10*n)/10
+
 module.exports = (robot) ->
-  mmol = /^(\d{1,2}\.\d)$/
-  mgdl = /^(\d{2,3})$/
-  ratio = 18.0182
+  range = ///^( # begin a capture group anchored to the beginning of the string
+    \d{2,3}     # numbers that fit mg/dL
+    |           # or
+    \d{1,2}\.\d # numbers that fit mmol/L
+    )$///       # end the capture group anchored to the end of the string

-  robot.hear mgdl, (msg) ->
-    msg.send msg.match[1] + ' mg/dL is ' + Math.round(10*msg.match[1]/ratio)/10 + ' mmol/L'
+  options =
+    threshold: process.env.HUBOT_GLUCOSE_UNIT_THRESHOLD

-  robot.hear mmol, (msg) ->
-    msg.send msg.match[1] + ' mmol/L is ' + Math.round(msg.match[1]*ratio) + ' mg/dL'
+  unless options.threshold
+    options.threshold = 30
+
+  robot.respond /estimate a1c (from average )?(.*)/i, (msg) ->
+    bg = msg.match[2]
+    if bg >= options.threshold
+      reply = 'an average of ' + bg + ' mg/dL or '
+      reply = reply + tenths(mgdlToMmol(bg)) + ' mmol/L'
+      reply = reply + ' is approximately equivalent to '
+      reply = reply + tenths(mgdlToDcct(bg)) + '% (DCCT) or '
+      reply = reply + tenths(mgdlToIfcc(bg)) + ' mmol/mol (IFCC)'
+    else
+      reply = 'an average of ' + bg + ' mmol/L or '
+      reply = reply + tenths(mmolToMgdl(bg)) + ' mg/dL'
+      reply = reply + ' is approximately equivalent to '
+      reply = reply + tenths(mgdlToDcct(mmolToMgdl(bg))) + '% (DCCT) or '
+      reply = reply + tenths(mgdlToIfcc(mmolToMgdl(bg))) + ' mmol/mol (IFCC)'
+    msg.send reply
+
+  robot.hear range, (msg) ->
+    bg = msg.match[1]
+    if bg >= options.threshold
+      msg.send bg + ' mg/dL is ' + tenths(mgdlToMmol(bg)) + ' mmol/L'
+    else
+      msg.send bg + ' mmol/L is ' + tenths(mmolToMgdl(bg)) + ' mg/dL'


Update competition text per https://github.com/zooniverse/Penguins/issues/65

--- competition.coffee
+++ competition.coffee
@@ -7,7 +7,7 @@

By marking penguins today, you could earn the chance to win the trip of a lifetime to see these amazing little creatures in their natural habitat!

-
Between April 25 and May 25, every day that you classify at least 10 photos, you can receive an entry into a prize-drawing to win an 11-day Antarctic Explorer: Discovering the 7th Continent expedition from Quark Expeditions®. Click on "Enter Competition" below to register, and "Learn More" to read up on the competition details.
+
Between April 25 and May 25, every day that you classify at least 10 photos, you will receive an entry into a prize-drawing to win an 11-day Antarctic Explorer: Discovering the 7th Continent expedition from Quark Expeditions®. Click on "Enter Competition" below to register, and "Learn More" to read up on the competition details.
         Note: You must be a registered Zooniverse user to participate; if not already registered with Zooniverse, you can do so with the link at the top right of your screen.


share/analytics: test on dev namespace (change this later!)

--- src/smc-webapp/share/page.cjsx
+++ src/smc-webapp/share/page.cjsx
@@ -65,7 +65,7 @@
         ]

     render_cocalc_analytics: ->
-        <script async={true} src={"https://www.cocalc.com/static/analytics.js"}></script>
+        <script async={true} src={"https://dev.cocalc.com/static/analytics.js"}></script>

     render: ->
         <html lang="en">


Allow smart quotes in who's on call command

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -382,7 +382,7 @@

   # who is on call?
-  robot.respond /who('s|s| is|se)? (on call|oncall|on-call)( (?:for )?(.+))?/i, (msg) ->
+  robot.respond /who(’s|'s|s| is|se)? (on call|oncall|on-call)( (?:for )?(.+))?/i, (msg) ->
     scheduleName = msg.match[4]

     displaySchedule = (s) ->


Swapped out hardcoded bot name for robot.name

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -16,7 +16,7 @@
 #
 # Dependencies:
 #  "moment": "1.6.2"
-#
+#
 # Configuration:
 #
 #   HUBOT_PAGERDUTY_API_KEY - API Access Key
@@ -43,7 +43,7 @@
     emailNote = if msg.message.user.pagerdutyEmail
                   "You've told me your PagerDuty email is #{msg.message.user.pagerdutyEmail}"
                 else if msg.message.user.email_address
-                  "I'm assuming your PagerDuty email is #{msg.message.user.email_address}. Change it with `hubot pager me as you@yourdomain.com`"
+                  "I'm assuming your PagerDuty email is #{msg.message.user.email_address}. Change it with `#{robot.name} pager me as you@yourdomain.com`"
                 else
                   "I don't know your PagerDuty email. Change it with `#{robot.name} pager me as you@yourdomain.com`"

@@ -158,19 +158,19 @@
     unless email
       msg.send "Sorry, I can't figure out your email address :( Can you tell me with `#{robot.name} pager me as you@yourdomain.com`?"
       return
-
+
     user = users[email]
-
+
     unless user
       msg.send "Sorry, I couldn't find a PagerDuty user for #{email}. Double check you have a user, and that I know your PagerDuty email with `#{robot.name} pager me as you@yourdomain.com`"
       return
-
+
     users[email].id
-
+
   pagerDutyGet = (msg, url, query, cb) ->
     if missingEnvironmentForApi(msg)
       return
-
+
     auth = "Token token=#{pagerDutyApiKey}"
     msg.http(pagerDutyBaseUrl + url)
       .query(query)
@@ -184,11 +184,11 @@
             console.log body
             json_body = null
         cb json_body
-
+
   pagerDutyPut = (msg, url, data, cb) ->
     if missingEnvironmentForApi(msg)
       return
-
+
     json = JSON.stringify(data)
     auth = "Token token=#{pagerDutyApiKey}"
     msg.http(pagerDutyBaseUrl + url)
@@ -204,11 +204,11 @@
             console.log body
             json_body = null
         cb json_body
-
+
   pagerDutyPost = (msg, url, data, cb) ->
     if missingEnvironmentForApi(msg)
       return
-
+
     json = JSON.stringify(data)
     auth = "Token token=#{pagerDutyApiKey}"
     msg.http(pagerDutyBaseUrl + url)
@@ -224,11 +224,11 @@
             console.log body
             json_body = null
         cb json_body
-
+
   withCurrentOncall = (msg, cb) ->
     oneHour = moment().add('hours', 1).format()
     now = moment().format()
-
+
     query = {
       since: now,
       until: oneHour,
@@ -237,7 +237,7 @@
     pagerDutyGet msg, "/schedules/#{pagerDutyScheduleId}/entries", query, (json) ->
       if json.entries and json.entries.length > 0
         cb(json.entries[0].user.name)
-
+
   withPagerDutyUsers = (msg, cb) ->
     if pagerDutyUsers['loaded'] != true
       pagerDutyGet msg, "/users", {}, (json) ->
@@ -249,27 +249,27 @@
         cb(pagerDutyUsers)
     else
       cb(pagerDutyUsers)
-
+
   pagerDutyIncidents = (msg, cb) ->
     query =
       status:  "triggered,acknowledged"
       sort_by: "incident_number:asc"
     pagerDutyGet msg, "/incidents", query, (json) ->
       cb(json.incidents)
-
+
   pagerDutyIntegrationAPI = (msg, cmd, args, cb) ->
     unless pagerDutyServiceApiKey?
       msg.send "PagerDuty API service key is missing."
       msg.send "Ensure that HUBOT_PAGERDUTY_SERVICE_API_KEY is set."
       return
-
+
     data = null
     switch cmd
       when "trigger"
         data = JSON.stringify { service_key: pagerDutyServiceApiKey, event_type: "trigger", description: "#{args}"}
         pagerDutyIntergrationPost msg, data, (json) ->
           cb(json)
-
+
   formatIncident = (inc) ->
      # { pd_nagios_object: 'service',
      #   HOSTNAME: 'fs1a',
@@ -285,7 +285,7 @@
          "#{inc.incident_number}: #{inc.trigger_summary_data.HOSTNAME}/#{inc.trigger_summary_data.HOSTSTATE} - assigned to #{inc.assigned_to_user.name}\n"
     else
       ""
-
+
   updateIncident = (msg, incident_number, status) ->
     withPagerDutyUsers msg, (users) ->
       userId = pagerDutyUserId(msg, users)
@@ -313,8 +313,8 @@
                 msg.reply "Problem updating incident #{incident_number}"
         if foundIncidents.length == 0
           msg.reply "Couldn't find incident #{incident_number}"
-
-
+
+
   pagerDutyIntergrationPost = (msg, json, cb) ->
     msg.http('https://events.pagerduty.com/generic/2010-04-15/create_event.json')
       .header("content-type","application/json")


fixed the match array index for the scheduleName

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -556,7 +556,7 @@
     if pagerduty.missingEnvironmentForApi(msg)
       return

-    scheduleName = msg.match[4]
+    scheduleName = msg.match[1]

     renderSchedule = (s, cb) ->
       withCurrentOncall msg, s, (username, schedule) ->


KodingKite::tell: catch token expired errors and request new token when needed

--- client/Main/kite/kodingkite.coffee
+++ client/Main/kite/kodingkite.coffee
@@ -67,14 +67,25 @@

           .then (args)=>
             KiteLogger.started name, rpcMethod
-            resolve (
-              @transport?.tell args...
-                .then (res)->
-                  KiteLogger.success name, rpcMethod
-                  return res
-                .catch (err)->
-                  KiteLogger.failed name, rpcMethod
-                  throw err
+            resolve (@transport?.tell args...
+
+              .then (res)->
+
+                KiteLogger.success name, rpcMethod
+                return res
+
+              .catch (err)=>
+
+                if err.name is 'KiteError' and \
+                   err.message is 'token is expired'
+
+                  return new Promise (resolve, reject)=>
+                    @transport?.expireToken =>
+                      resolve @transport.tell args...
+
+                KiteLogger.failed name, rpcMethod
+                throw err
+
             )

           .catch =>


Pane: Make header.title as a separate object

--- client/Workspace/panes/pane.coffee
+++ client/Workspace/panes/pane.coffee
@@ -20,7 +20,12 @@
     @header    = new KDCustomHTMLView
       tagName  : "span"  if title is ''
       cssClass : "ws-header inner-header"
-      partial  : "<h4>#{title}</h4>"
+
+    @header.title = new KDCustomHTMLView
+      partial : "#{title}"
+      tagName : "h4"
+
+    @header.addSubView @header.title

   createButtons: ->
     # TODO: c/p from panel, should refactor both of them.


fixed positioning of notes popup

fixed positioning of notes popup

--- viewer/app.coffee
+++ viewer/app.coffee
@@ -80,7 +80,7 @@

     tag_html += """
-
+

@@ -112,7 +112,7 @@
     """

-    # Add gaug.es tracking code
+    # Add gaug.es tracking code
     if gist_data.public and process.env.GAUGES
         tag_html += """


client: stacks: stacks become no stack home page

--- client/app/lib/kodingrouter.coffee
+++ client/app/lib/kodingrouter.coffee
@@ -97,11 +97,7 @@

     if groupsController.currentGroupHasStack()
     then return '/IDE'
-    else
-      if groupsController.currentGroupIsNew()
-      then return '/Welcome'
-      else return '/Activity'
-
+    else return '/Stacks'

   setPageTitle: (title = 'Koding') -> kd.singletons.pageTitle.update title


Fixed: popover positionning

Fixed: popover positionning

--- client/app/lib/popover_view.coffee
+++ client/app/lib/popover_view.coffee
@@ -117,57 +117,40 @@
                 display: 'block'
                 top: 'auto'
                 left: 'auto'
-                left: 'auto'

         # Append the popover to the DOM so it has height/width/offset.
         @$popover.appendTo @container

-        # The popover's height must be retrieved when the popover is expanded.
-        hiddenElements = @$popover.find '[aria-hidden="true"]'
-        hiddenElements.attr 'aria-hidden', false
-
         # Define everything that will be needed for positionning.
-        popoverHeight = @$popover.innerHeight()
         popoverWidth = @$popover.innerWidth()

         # Target is the calendar's cell clicked.
         targetOffset = @target.offset()
         targetWidth = @target.width()
-        targetHeight = @target.height()

         # Container is the screen, except the sidebar.
-        containerOffset = @container.offset()
         containerHeight = @container.innerHeight()
         containerWidth = @container.innerWidth()

-        # `popoverHeight` has been computed based on the expanded popover, but
-        # it's collapsed by default.
-        hiddenElements.attr 'aria-hidden', true
+        # Cell is on the left side
+        if targetOffset.left < (containerWidth / 2)
+            left = targetOffset.left + 50

-        # Define default position.
-        position =
-            top: targetOffset.top
-            left: targetOffset.left + 50
+        # Cell is on the right side
+        else
+            left = targetOffset.left - popoverWidth - targetWidth + 25

-        # Check if popover is within viewport's width
-        maxXPosition = position.left + targetWidth + popoverWidth
-        viewportMaxXPosition = containerOffset.left + containerWidth
-        fitRight = maxXPosition < viewportMaxXPosition
+        # Cell is on the upper side
+        if targetOffset.top < (containerHeight / 2)
+            top = '10vh'
+            bottom = 'auto'

-        # Check if popover is within viewport's height
-        maxYposition = position.top + popoverHeight
-        viewportMaxYPosition = containerOffset.top + containerHeight
-        fitBottom = maxYposition < viewportMaxYPosition
+        # Cell is on the lower side
+        else
+            top = 'auto'
+            bottom = '5vh'

-        # If it's not in viewport's height, position relatively to bottom.
-        unless fitBottom
-            position.bottom = containerHeight - targetOffset.top
-            position.top = 'auto'
-
-        # If it's not in viewport's width, position relatively to right.
-        unless fitRight
-            position.right = containerWidth - targetOffset.left + targetWidth
-            position.left = 'auto'
+        position = {top, bottom, left}

         # Position the element into the DOM
         @$popover.css position


Remove space-pen require from text-editor-element

--- src/text-editor-element.coffee
+++ src/text-editor-element.coffee
@@ -1,5 +1,4 @@
 {Emitter} = require 'event-kit'
-{View, $, callRemoveHooks} = require 'space-pen'
 Path = require 'path'
 {defaults} = require 'underscore-plus'
 TextBuffer = require 'text-buffer'
@@ -119,7 +118,6 @@
       inputNode.addEventListener 'blur', => @dispatchEvent(new FocusEvent('blur', bubbles: false))

   unmountComponent: ->
-    callRemoveHooks(this)
     if @component?
       @component.destroy()
       @component.getDomNode().remove()


Removed unnecessary delete from user model spec

--- coach/specs/user/model.spec.coffee
+++ coach/specs/user/model.spec.coffee
@@ -52,7 +52,6 @@
     # test that it doesn't blowup if role latest_enrollment_at is missing
     delete matches[0].roles[0].latest_enrollment_at
     matches[1].roles[0].latest_enrollment_at = "2017-01-06T20:00:00.000Z"
-    delete matches[0].roles[0].latest_enrollment_at
     expect(User.getCourse(TWO_COURSE_ECOSYSTEM_ID).id).to.equal(matches[1].id)

     undefined


enable update count after startup

enable update count after startup

--- coffee/controllers/mainCtrl.coffee
+++ coffee/controllers/mainCtrl.coffee
@@ -33,9 +33,8 @@
     DataAdapter.addEventListener DataAdapter.ACCOUNT_ADDED, _loadRedmine
     DataAdapter.addEventListener DataAdapter.TICKETS_CHANGED, () ->
       Ticket.set(DataAdapter.tickets)
-    Option.onChanged('reportUsage', (e) ->
-      Analytics.setPermission e
-      Log.info("GoogleAnalytics enable: " + Option.getOptions().reportUsage))
+    Option.onChanged('reportUsage', (e) -> Analytics.setPermission(e) )
+    Option.onChanged('hideNonTicketProject',  _toggleProjectHidden)
     Log.debug "finish initialize Event."

   ###
@@ -162,6 +161,16 @@
     $q.all(promises).then(() -> DataAdapter.updateProjects())

   ###
+   count project's issues count on all accounts.
+  ###
+  _toggleProjectHidden = (enableHide) ->
+    if not enableHide
+      DataAdapter.updateProjects()
+    else
+      for a in Account.getAccounts()
+        _loadIssueCount(a)()
+
+  ###
    request a setup of redmine account to user.
   ###
   _requestAddAccount = () ->


Failing spec for newline delimiters

Failing spec for newline delimiters

--- coffee/spec/calculator.spec.coffee
+++ coffee/spec/calculator.spec.coffee
@@ -15,3 +15,6 @@
   it "should return 3 for '0,1,2'", ->
     expect(calculator.add '0,1,2').toEqual 3

+  it "should return 6 for '1\n2,3'", ->
+    expect(calculator.add '1\n2,3').toEqual 6
+


fix terminal session fails to re-open bug

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -3433,6 +3433,7 @@
                     if @element.is(":visible")
                         @show()
                     @console.set_session(session)
+                    @opts.session_uuid = session.session_uuid
                     salvus_client.write_text_file_to_project
                         project_id : @editor.project_id
                         path       : @filename
@@ -3449,9 +3450,13 @@

     _get: () =>  # TODO
-        return 'history saving not yet implemented'
+        return @opts.session_uuid ? ''

     _set: (content) =>  # TODO
+
+    save: (cb) =>
+        # DO nothing -- a no-op for now (no notion of history... YET!)
+        cb?()

     focus: () =>
         @console?.focus()


fix https://github.com/sagemathinc/smc/issues/1022, which is more fallout from the side-chat/react patchbomb

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -1555,7 +1555,7 @@
         if filename.length > 30
             filename = "…" + filename.slice(filename.length-30)

-        @chat_filename = "." + @filename + ".sage-chat"
+        @chat_filename = misc.meta_file(@filename, 'chat')

         # not really needed due to highlighted tab; annoying.
         #@element.find(".salvus-editor-codemirror-filename").text(filename)


fixes #1722: operator precedence in unbounded slice compilation

--- nodes.coffee
+++ nodes.coffee
@@ -728,14 +728,14 @@
   compileNode: (o) ->
     {to, from} = @range
     fromStr    = from and from.compile(o, LEVEL_PAREN) or '0'
-    compiled   = to and to.compile o, LEVEL_PAREN
+    compiled   = to and to.compile o, LEVEL_ACCESS
     if to and not (not @range.exclusive and +compiled is -1)
       toStr = ', ' + if @range.exclusive
         compiled
       else if SIMPLENUM.test compiled
         (+compiled + 1).toString()
       else
-        "(#{compiled} + 1) || 9e9"
+        "#{compiled} + 1 || 9e9"
     ".slice(#{ fromStr }#{ toStr or '' })"

 #### Obj


Remove dead code.

Remove dead code.

--- tests/scripts/fuzzer.coffee
+++ tests/scripts/fuzzer.coffee
@@ -18,9 +18,6 @@

     get: (ref) ->
       window.Fuzzer[ref]
-
-    toStr: (obj) ->
-      JSON.stringify(obj)

     getEditorDelta: ->
       window.editor.getDelta()


Fixed linux keybindings

Fixed linux keybindings

--- keymaps/latextools.cson
+++ keymaps/latextools.cson
@@ -27,25 +27,25 @@
   '\'': 'latextools:quote'
   '"' : 'latextools:double-quote'

-  ".platform-linux atom-text-editor[data-grammar='text tex latex']":
-    'ctrl-l ctrl-l': 'editor:select-line'
-    'ctrl-l c': 'latextools:clear-log'
-    'ctrl-l escape': 'latextools:hide-ltconsole'
-    'ctrl-l shift-escape': 'latextools:show-ltconsole'
-    'ctrl-alt-b': 'latextools:build'
-    'ctrl-l j': 'latextools:jump-to-pdf'
-    'ctrl-l x': 'latextools:ref-cite-complete'
-    'ctrl-l ctrl-c': 'latextools:wrap-in-command'
-    'ctrl-l ctrl-n': 'latextools:wrap-in-environment'
-    'ctrl-l e': 'latextools:wrap-in-emph'
-    'ctrl-l b': 'latextools:wrap-in-bold'
-    'ctrl-l u': 'latextools:wrap-in-underline'
-    'ctrl-l m': 'latextools:wrap-in-monospace'
-    'ctrl-l .': 'latextools:close-environment'
-    '$' : 'latextools:dollar-sign'
-    '`' : 'latextools:backquote'
-    '\'': 'latextools:quote'
-    '"' : 'latextools:double-quote'
+".platform-linux atom-text-editor[data-grammar='text tex latex']":
+  'ctrl-l ctrl-l': 'editor:select-line'
+  'ctrl-l c': 'latextools:clear-log'
+  'ctrl-l escape': 'latextools:hide-ltconsole'
+  'ctrl-l shift-escape': 'latextools:show-ltconsole'
+  'ctrl-alt-b': 'latextools:build'
+  'ctrl-l j': 'latextools:jump-to-pdf'
+  'ctrl-l x': 'latextools:ref-cite-complete'
+  'ctrl-l ctrl-c': 'latextools:wrap-in-command'
+  'ctrl-l ctrl-n': 'latextools:wrap-in-environment'
+  'ctrl-l e': 'latextools:wrap-in-emph'
+  'ctrl-l b': 'latextools:wrap-in-bold'
+  'ctrl-l u': 'latextools:wrap-in-underline'
+  'ctrl-l m': 'latextools:wrap-in-monospace'
+  'ctrl-l .': 'latextools:close-environment'
+  '$' : 'latextools:dollar-sign'
+  '`' : 'latextools:backquote'
+  '\'': 'latextools:quote'
+  '"' : 'latextools:double-quote'

 ".platform-darwin atom-text-editor[data-grammar='text tex latex']":
   'cmd-l cmd-l': 'editor:select-line'


calculate charges and distances; fix links

--- layout.coffee
+++ layout.coffee
@@ -2,14 +2,15 @@
     node = {
         name: state.name
         cell: cell
-        children: []
+        size: 1000
     }

     if state.children?
         g = cell.append('g').attr('class', 'children')
         node.children = drawChildren(g, state.children)
+        node.size += d3.sum(node.children, (d) -> d.size) * 4

-    r = 20
+    r = Math.sqrt(node.size)

     cell.insert('rect', ':first-child')
         .attr('class', 'border')
@@ -36,27 +37,24 @@
     for state in childStates
         cell = g.append('g').attr('class', 'cell')
         child = drawState(cell, state)
-        child._idx = children.length
         children.push(child)
         childMap[child.name] = child

-    for child in children
+    for state in childStates
+        child = childMap[state.name]
         for tr in state.transitions or []
             target = childMap[tr.target]
-            links.push({source: child._idx, target: target._idx})
+            links.push({source: child, target: target})

-    link = linkGroup
-        .selectAll('.link')
+    link = linkGroup.selectAll('.link')
         .data(links)
       .enter().append('line')
         .attr('class', 'link')
         .style('stroke-width', (d) -> Math.sqrt(d.value))

-    r = 10
-
     force = d3.layout.force()
-        .charge(-r * 6)
-        .linkDistance(r * 1.5)
+        .charge((d) -> -Math.sqrt(d.size) * 5)
+        .linkDistance((d) -> Math.sqrt(d.source.size) + Math.sqrt(d.target.size))

     force
         .nodes(children)


check object crypto service before decrypting

--- js/src/service/StorageClient.coffee
+++ js/src/service/StorageClient.coffee
@@ -223,6 +223,9 @@
         objectTreePagedResponse: @objectApi.getObjectTreeByTypeAndLoadLevelPaged(objectTreePagedRequest)
       })
       .then ({ objectCryptoService, objectTreePagedResponse }) ->
+
+        if not _.isObject(objectCryptoService)
+          return null

         objectCryptoService.decryptObjectMetadataTree(objectTreePagedResponse.objectMetadataTree)
         return objectTreePagedResponse


Don't crash when missing an error message

--- lib/commands/run.coffee
+++ lib/commands/run.coffee
@@ -917,7 +917,7 @@

   .catch (err) ->
     if err? and err isnt '' and typeof err is 'string' or err.json?
-      message = err.json?.message.trim() or ((err if typeof err is 'string') or err.message or 'Unknown error').trim()
+      message = err.json?.message?.trim() or ((err if typeof err is 'string') or err.message or 'Unknown error').trim()
       message = message.replace /^Error: /, ''
       options.reporter.error chalk.bold('Error:') + ' ' + message


Recursion for non-unix path, and no actual path-change

--- src/jadelint.coffee
+++ src/jadelint.coffee
@@ -23,14 +23,15 @@
     getDefaultConf = () ->
         def = {}
         dir = process.cwd()
-        while '/' isnt fs.realpathSync dir
+        lastDir = ''
+        while dir isnt lastDir
             try
                 for prop, val of JSON.parse fs.readFileSync "#{dir}/.jadelintrc"
                     def[prop] ?= val

-            dir += '/..'
-
-        def
+            lastDir = dir
+            dir = path.join dir, '..'
+        return def

     conf ?= getDefaultConf()


Indicate strings using quotes. #206

Indicate strings using quotes. #206

--- lib/Components/CallStackPane.coffee
+++ lib/Components/CallStackPane.coffee
@@ -98,7 +98,8 @@
       switch(type)
         when 'string', 'boolean', 'number', 'undefined', 'null'
           value = value.value.value
-          TreeViewItem("#{name} : #{value}", handlers: handlers)
+          title = if type is 'string' then "#{name} : \"#{value}\"" else "#{name} : #{value}"
+          TreeViewItem(title, handlers: handlers)
         when 'function'
           TreeViewItem("#{name} : function() { ... }", handlers: handlers)
         when 'object'


修正焦点获取的问题

修正焦点获取的问题

延迟上 200 ms 这个问题就解决了。

--- src/js/popup.coffee
+++ src/js/popup.coffee
@@ -107,4 +107,5 @@
     $timeout ->
       $scope.isReady = true
       angular.element('#source').focus().select()
+    , 200


Change batman.rails to use pathToHTML instead of viewPrefix

--- src/extras/batman.rails.coffee
+++ src/extras/batman.rails.coffee
@@ -1,4 +1,4 @@
-Batman.config.viewPrefix = '/assets/batman/html'
+Batman.config.pathToHTML = '/assets/batman/html'

 numericKeys = [1, 2, 3, 4, 5, 6, 7, 10, 11]
 date_re = ///


made ready chainable

made ready chainable

--- src/xml.coffee
+++ src/xml.coffee
@@ -240,8 +240,11 @@
         this

     ready: (callback) =>
-        return callback?.call(this) if @isready
+        if @isready
+            callback?.call(this)
+            return this
         @once 'ready', callback
+        this

 class Builder extends EventEmitter


:bug: Fix broken scroll when clicking the minimap

Fixes #171

--- lib/minimap-view.coffee
+++ lib/minimap-view.coffee
@@ -493,11 +493,13 @@
     @isClicked = true
     e.preventDefault()
     e.stopPropagation()
-    # VisibleArea's center-y
+
     y = e.pageY - @offsetTop
-    top = @indicator.computeFromCenterY(y) / @scaleY
-    # @note: currently, no animation.
-    @editor.scrollToScreenPosition({top, left: 0})
+    top = (y + @renderView.scrollTop()) / @scaleY
+
+    position = @editor.displayBuffer.screenPositionForPixelPosition({top, left: 0})
+    @editor.scrollToScreenPosition(position, center: true)
+
     # Fix trigger `mousewheel` event.
     setTimeout =>
       @isClicked = false


Adds back `activeViewSupportMinimap` method

--- lib/minimap-view.coffee
+++ lib/minimap-view.coffee
@@ -72,6 +72,7 @@
     @paneView.removeClass('with-minimap')
     @detachFromPaneView()

+  activeViewSupportMinimap: -> @getEditor()?
   minimapIsAttached: -> @paneView.find('.minimap').length is 1

   # EDITOR VIEW MANAGEMENT
@@ -162,7 +163,7 @@
     return if item is @activeItem
     @activeItem = item

-    if @getEditor()
+    if @activeViewSupportMinimap()
       @log 'minimap is supported by the current tab'
       @activatePaneViewMinimap() unless @minimapIsAttached()
       @storeActiveEditor()


menus: Advanced: give the "Version" row a dummy action

--- src/js/menus.coffee
+++ src/js/menus.coffee
@@ -463,6 +463,7 @@
               console.log "Local storage cleared"
           }, {
             title: "Version: #{Appinfo.versionLabel}"
+            action: ->
           }
         ]
       ]


panelframe: use margin.inner

panelframe: use margin.inner

--- src/panelframe.coffee
+++ src/panelframe.coffee
@@ -5,7 +5,7 @@
     # chartOpts start
     width = chartOpts?.width ? 800 # overall height of chart in pixels
     height = chartOpts?.height ? 500 # overall width of chart in pixels
-    margin = chartOpts?.margin ? {left:60, top:40, right:40, bottom: 40, inner:5} # margins in pixels (left, top, right, bottom, inner)
+    margin = chartOpts?.margin ? {left:60, top:40, right:40, bottom: 40, inner:3} # margins in pixels (left, top, right, bottom, inner)
     axispos = chartOpts?.axispos ? {xtitle:25, ytitle:45, xlabel:5, ylabel:5} # position of axis labels in pixels (xtitle, ytitle, xlabel, ylabel)
     titlepos = chartOpts?.titlepos ? 20 # position of chart title in pixels
     title = chartOpts?.title ? "" # chart title
@@ -113,8 +113,8 @@
              .attr("transform", if rotate_ylab then "rotate(270,#{ylabpos_x},#{ylabpos_y})" else "")

         # scales (ignoring NA business)
-        xscale = d3.scale.linear().domain(xlim).range(xrange)
-        yscale = d3.scale.linear().domain(ylim).range(yrange)
+        xscale = d3.scale.linear().domain(xlim).range([xrange[0]+margin.inner, xrange[1]-margin.inner])
+        yscale = d3.scale.linear().domain(ylim).range([yrange[0]-margin.inner, yrange[1]+margin.inner])
         # scales (handling nulls)
         xscale_wnull = (val) ->
             return xNA_xpos unless val?


Fix failing test

Fix failing test

--- lib/python-tools.coffee
+++ lib/python-tools.coffee
@@ -128,23 +128,35 @@
         new Point(bufferPosition.row, end),
       ))
     else
-      #  We can use a scope lookup on the delimiters position to determine
-      #  if I am at the end or beginning of the string block
-
       start = end = bufferPosition.row
       start_index = end_index = -1

-      line = editor.lineTextForBufferRow(start)
-
-      while start_index == -1
-        start = start - 1
-        line = editor.lineTextForBufferRow(start)
-        start_index = line.indexOf(delimiter)
-
-      while end_index == -1
-        end = end + 1
-        line = editor.lineTextForBufferRow(end)
-        end_index = line.indexOf(delimiter)
+      # TODO: This could do with some documentation and cleanup!
+      delim_index = line.indexOf(delimiter)
+      if delim_index != -1
+        scopes = editor.scopeDescriptorForBufferPosition(new Point(start, delim_index))
+        scopes = scopes.getScopesArray()
+        if regexPatternIn(/punctuation.definition.string.begin.*/, scopes)
+          start_index = line.indexOf(delimiter)
+          while end_index == -1
+            end = end + 1
+            line = editor.lineTextForBufferRow(end)
+            end_index = line.indexOf(delimiter)
+        else if regexPatternIn(/punctuation.definition.string.end.*/, scopes)
+          end_index = line.indexOf(delimiter)
+          while start_index == -1
+            start = start - 1
+            line = editor.lineTextForBufferRow(start)
+            start_index = line.indexOf(delimiter)
+      else
+        while end_index == -1
+          end = end + 1
+          line = editor.lineTextForBufferRow(end)
+          end_index = line.indexOf(delimiter)
+        while start_index == -1
+          start = start - 1
+          line = editor.lineTextForBufferRow(start)
+          start_index = line.indexOf(delimiter)

       editor.setSelectedBufferRange(new Range(
         new Point(start, start_index + delimiter.length),


Call the setGraph method so we get errors passed at loading stage

--- src/lib/ComponentLoader.coffee
+++ src/lib/ComponentLoader.coffee
@@ -126,12 +126,13 @@
       graphSocket = internalSocket.createSocket()
       graph.loader = @
       graph.baseDir = @baseDir
-      graph.inPorts.graph.attach graphSocket
-      graphSocket.send component
-      graphSocket.disconnect()
       graph.inPorts.remove 'graph'
-      @setIcon name, graph
-      callback null, graph
+      graph.setGraph component, (err) =>
+        return callback err if err
+        @setIcon name, graph
+        callback null, graph
+      return
+    return

   setIcon: (name, instance) ->
     # See if component has an icon


Update upi consumer

Update upi consumer

--- lib/upi-consumer.coffee
+++ lib/upi-consumer.coffee
@@ -53,23 +53,25 @@

   constructor: (service, @process) ->
     @disposables = new CompositeDisposable
-    @disposables.add service.consume
+
+    @upi = service.register
       name: 'haskell-ghc-mod'
       menu: @mainMenu
       messageTypes: @messageTypes
-      tooltipEvent: @shouldShowTooltip
+      tooltip: @shouldShowTooltip
       events:
         onDidSaveBuffer: (buffer) =>
           @checkLint buffer, 'Save'
         onDidStopChanging: (buffer) =>
           @checkLint buffer, 'Change', true
-      consumer: (@upi) =>
+
+    @disposables.add(@upi)

     @disposables.add @process.onBackendActive =>
-      @upi.messages.status status: 'progress'
+      @upi.setStatus status: 'progress'

     @disposables.add @process.onBackendIdle =>
-      @upi.messages.status status: 'ready'
+      @upi.setStatus status: 'ready'

     @disposables.add atom.commands.add @contextScope, @contextCommands()
     cm = {}
@@ -102,7 +104,7 @@

   tooltipCommand: (tooltipfun) =>
     ({currentTarget, detail}) =>
-      @upi.tooltips.show
+      @upi.showTooltip
         editor: currentTarget.getModel()
         detail: detail
         tooltip: (crange) ->
@@ -111,91 +113,91 @@
   insertTypeCommand: ({currentTarget, detail}) =>
     Util = require './util'
     editor = currentTarget.getModel()
-    @upi.utils.withEventRange {editor, detail}, ({crange, pos}) =>
-      @process.getTypeInBuffer(editor.getBuffer(), crange)
-      .then (o) ->
-        {type} = o
-        {scope, range, symbol} = Util.getSymbolAtPoint editor, pos
-        if editor.getTextInBufferRange(o.range).match(/[=]/)?
-          indent = editor.getTextInBufferRange([[o.range.start.row, 0], o.range.start])
-          symbol = "(#{symbol})" if scope is 'keyword.operator.haskell'
-          birdTrack = ''
-          if 'meta.embedded.haskell' in editor.scopeDescriptorForBufferPosition(pos).getScopesArray()
-            birdTrack = indent.slice 0, 2
-            indent = indent.slice(2)
-          if indent.match(/\S/)?
-            indent = indent.replace /\S/g, ' '
-          editor.setTextInBufferRange [o.range.start, o.range.start],
-            "#{symbol} :: #{type}\n#{birdTrack}#{indent}"
-        else if not scope? #neither operator nor infix
-          editor.setTextInBufferRange o.range,
-            "(#{editor.getTextInBufferRange(o.range)} :: #{type})"
+    {crange, pos} = @upi.getEventRange(editor, detail)
+    @process.getTypeInBuffer(editor.getBuffer(), crange)
+    .then (o) ->
+      {type} = o
+      {scope, range, symbol} = Util.getSymbolAtPoint editor, pos
+      if editor.getTextInBufferRange(o.range).match(/[=]/)?
+        indent = editor.getTextInBufferRange([[o.range.start.row, 0], o.range.start])
+        symbol = "(#{symbol})" if scope is 'keyword.operator.haskell'
+        birdTrack = ''
+        if 'meta.embedded.haskell' in editor.scopeDescriptorForBufferPosition(pos).getScopesArray()
+          birdTrack = indent.slice 0, 2
+          indent = indent.slice(2)
+        if indent.match(/\S/)?
+          indent = indent.replace /\S/g, ' '
+        editor.setTextInBufferRange [o.range.start, o.range.start],
+          "#{symbol} :: #{type}\n#{birdTrack}#{indent}"
+      else if not scope? #neither operator nor infix
+        editor.setTextInBufferRange o.range,
+          "(#{editor.getTextInBufferRange(o.range)} :: #{type})"

   caseSplitCommand: ({currentTarget, detail}) =>
     editor = currentTarget.getModel()
-    @upi.utils.withEventRange {editor, detail}, ({crange}) =>
-      @process.doCaseSplit(editor.getBuffer(), crange)
-      .then (res) ->
-        res.forEach ({range, replacement}) ->
-          editor.setTextInBufferRange(range, replacement)
+    {crange} = @upi.getEventRange(editor, detail)
+    @process.doCaseSplit(editor.getBuffer(), crange)
+    .then (res) ->
+      res.forEach ({range, replacement}) ->
+        editor.setTextInBufferRange(range, replacement)

   sigFillCommand: ({currentTarget, detail}) =>
     editor = currentTarget.getModel()
-    @upi.utils.withEventRange {editor, detail}, ({crange}) =>
-      @process.doSigFill(editor.getBuffer(), crange)
-      .then (res) ->
-        res.forEach ({type, range, body}) ->
-          sig = editor.getTextInBufferRange(range)
-          indent = editor.indentLevelForLine(sig)
-          pos = range.end
-          text = "\n#{body}"
-          editor.transact ->
-            if type is 'instance'
-              indent += 1
-              unless sig.endsWith ' where'
-                editor.setTextInBufferRange([range.end, range.end], ' where')
-            newrange = editor.setTextInBufferRange([pos, pos], text)
-            for row in newrange.getRows().slice(1)
-              editor.setIndentationForBufferRow row, indent
+    {crange} = @upi.getEventRange(editor, detail)
+    @process.doSigFill(editor.getBuffer(), crange)
+    .then (res) ->
+      res.forEach ({type, range, body}) ->
+        sig = editor.getTextInBufferRange(range)
+        indent = editor.indentLevelForLine(sig)
+        pos = range.end
+        text = "\n#{body}"
+        editor.transact ->
+          if type is 'instance'
+            indent += 1
+            unless sig.endsWith ' where'
+              editor.setTextInBufferRange([range.end, range.end], ' where')
+          newrange = editor.setTextInBufferRange([pos, pos], text)
+          for row in newrange.getRows().slice(1)
+            editor.setIndentationForBufferRow row, indent

   goToDeclCommand: ({currentTarget, detail}) =>
     editor = currentTarget.getModel()
-    @upi.utils.withEventRange {editor, detail}, ({crange}) =>
-      @process.getInfoInBuffer(editor, crange)
-      .then ({range, info}) =>
-        res = /.*-- Defined at (.+):(\d+):(\d+)/.exec info
-        return unless res?
-        [_, fn, line, col] = res
-        rootDir = @process.getRootDir(editor.getBuffer())
-        atom.workspace.open (try rootDir.getFile(fn).getPath() ? fn),
-          initialLine: parseInt(line) - 1
-          initialColumn: parseInt(col) - 1
+    {crange} = @upi.getEventRange(editor, detail)
+    @process.getInfoInBuffer(editor, crange)
+    .then ({range, info}) =>
+      res = /.*-- Defined at (.+):(\d+):(\d+)/.exec info
+      return unless res?
+      [_, fn, line, col] = res
+      rootDir = @process.getRootDir(editor.getBuffer())
+      atom.workspace.open (try rootDir.getFile(fn).getPath() ? fn),
+        initialLine: parseInt(line) - 1
+        initialColumn: parseInt(col) - 1

   insertImportCommand: ({currentTarget, detail}) =>
     editor = currentTarget.getModel()
     buffer = editor.getBuffer()
-    @upi.utils.withEventRange {editor, detail}, ({crange}) =>
-      @process.findSymbolProvidersInBuffer editor, crange
-      .then (lines) ->
-        new ImportListView
-          items: lines
-          onConfirmed: (mod) ->
-            piP = new Promise (resolve) ->
-              buffer.backwardsScan /^(\s*)(import|module)/, ({match, range, stop}) ->
-                resolve
-                  pos: buffer.rangeForRow(range.start.row).end
-                  indent:
-                    switch match[2]
-                      when "import"
-                        "\n" + match[1]
-                      when "module"
-                        "\n\n" + match[1]
+    {crange} = @upi.getEventRange(editor, detail)
+    @process.findSymbolProvidersInBuffer editor, crange
+    .then (lines) ->
+      new ImportListView
+        items: lines
+        onConfirmed: (mod) ->
+          piP = new Promise (resolve) ->
+            buffer.backwardsScan /^(\s*)(import|module)/, ({match, range, stop}) ->
               resolve
-                pos: buffer.getFirstPosition()
-                indent: ""
-                end: "\n"
-            piP.then (pi) ->
-              editor.setTextInBufferRange [pi.pos, pi.pos], "#{pi.indent}import #{mod}#{pi.end ? ''}"
+                pos: buffer.rangeForRow(range.start.row).end
+                indent:
+                  switch match[2]
+                    when "import"
+                      "\n" + match[1]
+                    when "module"
+                      "\n\n" + match[1]
+            resolve
+              pos: buffer.getFirstPosition()
+              indent: ""
+              end: "\n"
+          piP.then (pi) ->
+            editor.setTextInBufferRange [pi.pos, pi.pos], "#{pi.indent}import #{mod}#{pi.end ? ''}"

   typeTooltip: (e, p) =>
     @process.getTypeInBuffer(e.getBuffer(), p)
@@ -263,7 +265,7 @@
       (m) -> m

   setMessages: (messages, types) ->
-    @upi.messages.set messages.map(@setHighlighter()), types
+    @upi.setMessages messages.map(@setHighlighter())

   checkLint: (buffer, opt, fast) ->
     if atom.config.get("haskell-ghc-mod.on#{opt}Check") and


Catch global exceptions and kill workers

--- src/pow/server.coffee
+++ src/pow/server.coffee
@@ -12,10 +12,19 @@
     @server = http.createServer (req, res) =>
       @onRequest req, res

+    process.on 'uncaughtException', (err) =>
+      @close()
+
+      process.nextTick () ->
+        throw err
+
   listen: (port) ->
     @server.listen port

   close: ->
+    for config, app of @applications
+      app.quit()
+
     @server.close()

   createApplicationPool: (config) ->


format this

format this

--- test/google_images_test.coffee
+++ test/google_images_test.coffee
@@ -8,9 +8,10 @@
 danger = Tests.danger helper, (req, res, url) ->
   res.writeHead 200
   res.end JSON.stringify(
-    {responseData: {results: [
-      {unescapedUrl: "(#{url.query.q})"}
-    ]}}
+    responseData:
+      results: [
+        unescapedUrl: "(#{url.query.q})"
+      ]
   )

 # callbacks for when hubot sends messages


Spec fixes

Spec fixes

--- test/lib/generator_spec.coffee
+++ test/lib/generator_spec.coffee
@@ -19,7 +19,7 @@
       date = new Date()
       expect(generator.defaults).to.eql({
         bozonVersion: "0.3.8",
-        electronVersion: "1.2.0",
+        electronVersion: "1.2.1",
         name: 'TestApp',
         platforms: ['linux-x64', 'darwin-x64', 'win32-ia32'],
         platformsStr: "[\"linux-x64\", \"darwin-x64\", \"win32-ia32\"]",
@@ -72,8 +72,8 @@
     it 'package.json should contain actual information', ->
       expect($.fileContains('package.json', '"name": "TestApp"')).to.be.true
       expect($.fileContains('package.json', '"version": "0.1.0"')).to.be.true
-      expect($.fileContains('package.json', '"electron-prebuilt": "1.2.0"')).to.be.true
-      expect($.fileContains('package.json', '"version": "1.2.0"')).to.be.true
+      expect($.fileContains('package.json', '"electron-prebuilt": "1.2.1"')).to.be.true
+      expect($.fileContains('package.json', '"version": "1.2.1"')).to.be.true
       expect($.fileContains('package.json', '"platforms": ["linux-x64", "darwin-x64", "win32-ia32"]')).to.be.true

     it 'app package.json should contain actual information', ->


fixing test to something more along the lines of what actually happens when someone gets deleted

--- src/test-xfuse.coffee
+++ src/test-xfuse.coffee
@@ -11,6 +11,9 @@

 testUser = ''

+###
+Because DELETE is a soft-delete some tests may fail when the randUID has been used before
+###
 randUID = Math.floor Math.random() * 501

 ###
@@ -105,23 +108,22 @@
                     ###
                     testUser = res.user.id
                     return
-            "deleting test user" :
-                topic : () ->
-                    xfuse.del "/user/" + testUser, @callback
-                    return
-                "should not get any errors" : (err, res) ->
-                    assert.isNull err
-                    assert.include res, "success"
-                    return
-                "and the user should no longer exist" :
+                "deleting test user" :
                     topic : () ->
-                        xfuse.get "/user/" + testUser, @callback
+                        xfuse.del "/user/" + testUser, @callback
                         return
-                    "should get user doesn't exist" : (err, res) ->
-                        assert.isNull res
-                        assert.isNotNull err
-                        assert.include err, "error"
-                        assert.include err.error, "message"
-                        assert.include err.error, "error_id"
+                    "should not get any errors" : (err, res) ->
+                        assert.isNull err
+                        assert.include res, "user"
+                        assert.equal res.user.message, "Deleted the user successfully."
                         return
+                    "and the user should no longer exist" :
+                        topic : () ->
+                            xfuse.get "/user/" + testUser, @callback
+                            return
+                        "should get user doesn't exist" : (err, res) ->
+                            assert.isNull res
+                            assert.isNotNull err
+                            assert.include err, "message"
+                            return
 .export(module)


remove some dead code

remove some dead code

--- src/types/json.coffee
+++ src/types/json.coffee
@@ -162,33 +162,6 @@
 exports.normalize = (op) ->
 	op

-
-# TODO: paths sometimes have string offsets?
-# perhaps instead of {p:['foo',1,3], sd:'bar'} we should have
-#                    {p:['foo'], sd:[1,3,'bar']} ?
-atPath = (obj, path) ->
-	obj = obj[k] for k in path
-	obj
-
-setPath = (obj, path, val) ->
-	obj = obj[k] for k in path[..-2]
-	obj[path[path.length-1]] = val
-
-# true if p1 is beneath (or identical to, if strict is false) p2
-pathIsBeneathPath = (p1, p2, strict) ->
-	return false if p2.length > p1.length
-	p1 = p1.slice()
-	p2 = p2.slice()
-
-	while p2.length > 0
-		if p1.shift() != p2.shift()
-			return false
-
-	return !strict || p1.length > 0
-# pbp [] [] s = not s
-# pbp _ [] s = true
-# pbp (p1:p1s) (p2:p2s) s = p1 == p2 && pbp p1s p2s s
-
 # hax, copied from test/types/json
 clone = (o) -> JSON.parse(JSON.stringify o)

@@ -255,8 +228,8 @@
 			c.od = apply clone(c.od), [oc]

-	commonOperand = common? and cplength == otherCplength
 	if common?
+		commonOperand = cplength == otherCplength
 		# transform based on otherC
 		if otherC.na != undefined
 			null


Use async.reduce for executing middleware

--- src/listener.coffee
+++ src/listener.coffee
@@ -52,7 +52,7 @@
           @callback response
           done()
         catch err
-          @emit('error', err, new @Response(@, message, []))
+          @emit('error', err, response)

       # When everything is finished (down the middleware stack and back up),
       # pass control back to the robot
@@ -74,33 +74,29 @@
   #
   # response - Response object to eventually pass to the Listener callback
   #
-  # next     - Called when all middleware is complete (assuming all continued
-  #            by calling respective 'next' functions)
+  # next(response, done) - Called when all middleware is complete (assuming
+  #     all continued by calling respective 'next' functions)
   #
-  # done     - Initial (final) completion callback. May be wrapped by
-  #            executed middleware.
+  # done() - Initial (final) completion callback. May be wrapped by
+  #     executed middleware.
   #
   # Returns nothing
   executeAllMiddleware: (response, next, done) ->
     allMiddleware = @robot.middleware

-    # When a middleware finishes, call the next one with the latest
-    # completion callback (each middleware may wrap the old 'done' callback
-    # with additional logic)
-    iterate = (idx, done) ->
-      if idx < allMiddleware.length
-        # Execute the indicated middleware
-        executeSingleMiddleware(idx, done)
-      else
-        # All done with middleware!
-        next(response, done)
+    # Execute a single piece of middleware and update the completion callback
+    # (each piece of middleware can wrap the 'done' callback with additional
+    # logic).
+    executeMiddleware = (doneFunc, middlewareFunc, cb) =>
+      nextFunc = (newDoneFunc) -> cb(null, newDoneFunc)
+      middlewareFunc.call(undefined, @robot, @, response, nextFunc, doneFunc)

-    # Execute a single middleware and return to #iterate when it continues
-    executeSingleMiddleware = (idx, done) =>
-      myIterate = (newDone) -> iterate(idx + 1, newDone)
-      allMiddleware[idx].call(undefined, @robot, @, response, myIterate, done)
+    # Executed when the middleware stack is finished
+    allDone = (err, finalDoneFunc) -> next(response, finalDoneFunc)

-    iterate(0, done)
+    # Execute each piece of middleware, collecting the latest 'done' callback
+    # at each step.
+    async.reduce(allMiddleware, done, executeMiddleware, allDone)

 class TextListener extends Listener
   # TextListeners receive every message from the chat source and decide if they


a notebook isn't loaded until it's modules init promises have been fulfilled

--- src/notebook.coffee
+++ src/notebook.coffee
@@ -54,11 +54,12 @@
     if module_names.length > 0
       loaded = Q.defer()
       require module_names, (imported_modules...) ->
-        _.each imported_modules, (module) -> module.init?()
         loaded.resolve _.object module_names, imported_modules
       , (err) ->
         loaded.reject err
-      loaded.promise
+      loaded.promise.then (imported_modules) ->
+        inits = Q.all _.compact _.map imported_modules, (module) ->  module.init?()
+        inits.then -> imported_modules
     else
       Q {}


test normal successful run

test normal successful run

--- test/browserify-tests.coffee
+++ test/browserify-tests.coffee
@@ -0,0 +1,38 @@
+
+# test the usual successful run
+Tinytest.add 'test processFile', (test) ->
+  # get the function registered with Plugin.registerSourceHandler
+  processFile = Plugin.fn
+
+  # create our mock CompileStep
+  compileStep = Plugin.compileStep()
+
+  # call our plugin function with our mock CompileStep
+  processFile compileStep
+
+  # test results
+  test.equal compileStep?.readBuffer?, true, 'CompileStep.read() should be called'
+  test.equal Result.passThrough.run, true, 'should create a new PassThrough'
+  test.equal Result.passThrough.buffer, compileStep.readBuffer
+
+  # test building browserify
+  test.equal Result?.browserify?, true, 'must run browserify function'
+  test.equal Result.browserify?.array.length, 1, 'browserify gets [readable], length should be 1'
+  test.equal Result.browserify?.array[0]?.isTestPassThrough, true, 'should receive our test PassThrough'
+  options = debug:true, basedir:'/full/path/to/app/packages/.npm/package'
+  test.equal Result.browserify?.options, options
+
+  # test bundling with browserify and reading its result
+  test.equal Result.browserify?.bundle, true, 'must run browserify.bundle()'
+  test.equal Result.browserify?.encoding, 'utf8'
+  test.equal Result.browserify?.on?.event, 'data', 'must call bundle.on \'data\''
+  test.equal Result.browserify?.once?.event, 'end', 'must call bundle.on\'end\''
+  test.equal Result?.bundleString, 'test data'
+
+  # test what's given to CompileStep.addJavaScript
+  test.equal compileStep.js.path, 'file.browserify.js'
+  test.equal compileStep.js.sourcePath, 'file.browserify.js'
+  test.equal compileStep.js.data, 'test data'
+
+
+# TODO: test having an error in the browserify.bundle() call


set articles to live

set articles to live

--- api/apps/articles/model/distribute.coffee
+++ api/apps/articles/model/distribute.coffee
@@ -60,6 +60,7 @@
         .send
           access_token: INSTANT_ARTICLE_ACCESS_TOKEN
           development_mode: NODE_ENV isnt 'production'
+          published: NODE_ENV is 'production'
           html_source: html
         .end (err, response) =>
           if err


Fix spec nesting

Fix spec nesting

--- spec/Helpers.coffee
+++ spec/Helpers.coffee
@@ -69,162 +69,164 @@
       s.disconnect()

   describe 'GroupComponent', ->
-    c = new component.Component
-    c.inPorts.add 'x',
-      required: true
-      datatype: 'int'
-    c.inPorts.add 'y',
-      required: true
-      datatype: 'int'
-    c.inPorts.add 'z',
-      required: true
-      datatype: 'int'
-    c.outPorts.add 'point'
-    x = new socket.createSocket()
-    y = new socket.createSocket()
-    z = new socket.createSocket()
-    p = new socket.createSocket()
-    c.inPorts.x.attach x
-    c.inPorts.y.attach y
-    c.inPorts.z.attach z
-    c.outPorts.point.attach p
-    it 'should pass data and groups to the callback', (done) ->
-      src =
-        111: {x: 1, y: 2, z: 3}
-        222: {x: 4, y: 5, z: 6}
-        333: {x: 7, y: 8, z: 9}
-      helpers.GroupComponent c, (data, groups, out) ->
-        chai.expect(data).to.deep.equal src[groups[0]]
-        out.send data
-        # done() if groups[0] is 333
-      , ['x', 'y', 'z'], 'point', {group: true}
-
-      groups = []
-      count = 0
-      p.on 'begingroup', (grp) ->
-        groups.push grp
-      p.on 'endgroup', ->
-        groups.pop()
-      p.on 'data', (data) ->
-        count++
-      p.on 'disconnect', ->
-        done() if count is 3 and groups.length is 0
-
-      for key, grp of src
-        x.beginGroup key
-        y.beginGroup key
-        z.beginGroup key
-        x.send grp.x
-        y.send grp.y
-        z.send grp.z
+    describe 'when grouping by packet groups', ->
+      c = new component.Component
+      c.inPorts.add 'x',
+        required: true
+        datatype: 'int'
+      c.inPorts.add 'y',
+        required: true
+        datatype: 'int'
+      c.inPorts.add 'z',
+        required: true
+        datatype: 'int'
+      c.outPorts.add 'point'
+      x = new socket.createSocket()
+      y = new socket.createSocket()
+      z = new socket.createSocket()
+      p = new socket.createSocket()
+      c.inPorts.x.attach x
+      c.inPorts.y.attach y
+      c.inPorts.z.attach z
+      c.outPorts.point.attach p
+
+      it 'should pass data and groups to the callback', (done) ->
+        src =
+          111: {x: 1, y: 2, z: 3}
+          222: {x: 4, y: 5, z: 6}
+          333: {x: 7, y: 8, z: 9}
+        helpers.GroupComponent c, (data, groups, out) ->
+          chai.expect(data).to.deep.equal src[groups[0]]
+          out.send data
+          # done() if groups[0] is 333
+        , ['x', 'y', 'z'], 'point', {group: true}
+
+        groups = []
+        count = 0
+        p.on 'begingroup', (grp) ->
+          groups.push grp
+        p.on 'endgroup', ->
+          groups.pop()
+        p.on 'data', (data) ->
+          count++
+        p.on 'disconnect', ->
+          done() if count is 3 and groups.length is 0
+
+        for key, grp of src
+          x.beginGroup key
+          y.beginGroup key
+          z.beginGroup key
+          x.send grp.x
+          y.send grp.y
+          z.send grp.z
+          x.endGroup()
+          y.endGroup()
+          z.endGroup()
+          x.disconnect()
+          y.disconnect()
+          z.disconnect()
+
+      it 'should work without a group provided', (done) ->
+        p.removeAllListeners()
+        helpers.GroupComponent c, (data, groups, out) ->
+          chai.expect(groups.length).to.equal 0
+          out.send {x: data.x, y: data.y, z: data.z}
+        , ['x', 'y', 'z'], 'point'
+
+        p.once 'data', (data) ->
+          chai.expect(data).to.deep.equal {x: 123, y: 456, z: 789}
+          done()
+
+        x.send 123
+        x.disconnect()
+        y.send 456
+        y.disconnect()
+        z.send 789
+        z.disconnect()
+
+      it 'should process inputs for different groups independently with group: true', (done) ->
+        src =
+          1: {x: 1, y: 2, z: 3}
+          2: {x: 4, y: 5, z: 6}
+          3: {x: 7, y: 8, z: 9}
+        inOrder = [
+          [ 1, 'x' ]
+          [ 3, 'z' ]
+          [ 2, 'y' ]
+          [ 2, 'x' ]
+          [ 1, 'z' ]
+          [ 2, 'z' ]
+          [ 3, 'x' ]
+          [ 1, 'y' ]
+          [ 3, 'y' ]
+        ]
+        outOrder = [ 2, 1, 3 ]
+
+        helpers.GroupComponent c, (data, groups, out) ->
+          out.send {x: data.x, y: data.y, z: data.z}
+        , ['x', 'y', 'z'], 'point', {group: true}
+
+        groups = []
+
+        p.on 'begingroup', (grp) ->
+          groups.push grp
+        p.on 'endgroup', (grp) ->
+          groups.pop()
+        p.on 'data', (data) ->
+          chai.expect(groups.length).to.equal 1
+          chai.expect(groups[0]).to.equal outOrder[0]
+          chai.expect(data).to.deep.equal src[outOrder[0]]
+          outOrder.shift()
+          done() unless outOrder.length
+
+        for tuple in inOrder
+          input = null
+          switch tuple[1]
+            when 'x'
+              input = x
+            when 'y'
+              input = y
+            when 'z'
+              input = z
+          input.beginGroup tuple[0]
+          input.send src[tuple[0]][tuple[1]]
+          input.endGroup()
+          input.disconnect()
+
+      it 'should support asynchronous handlers', (done) ->
+        point =
+          x: 123
+          y: 456
+          z: 789
+
+        helpers.GroupComponent c, (data, groups, out, callback) ->
+          setTimeout ->
+            out.send {x: data.x, y: data.y, z: data.z}
+            callback()
+          , 100
+        , ['x', 'y', 'z'], 'point', {async: true, group: true}
+
+        p.removeAllListeners()
+        counter = 0
+        p.on 'begingroup', (grp) ->
+          counter++
+        p.on 'endgroup', ->
+          counter--
+        p.once 'data', (data) ->
+          chai.expect(data).to.deep.equal point
+        p.once 'disconnect', ->
+          chai.expect(counter).to.equal 0
+          done()
+
+        x.beginGroup 'async'
+        y.beginGroup 'async'
+        z.beginGroup 'async'
+        x.send point.x
+        y.send point.y
+        z.send point.z
         x.endGroup()
         y.endGroup()
         z.endGroup()
-        x.disconnect()
-        y.disconnect()
-        z.disconnect()
-
-    it 'should work without a group provided', (done) ->
-      p.removeAllListeners()
-      helpers.GroupComponent c, (data, groups, out) ->
-        chai.expect(groups.length).to.equal 0
-        out.send {x: data.x, y: data.y, z: data.z}
-      , ['x', 'y', 'z'], 'point'
-
-      p.once 'data', (data) ->
-        chai.expect(data).to.deep.equal {x: 123, y: 456, z: 789}
-        done()
-
-      x.send 123
-      x.disconnect()
-      y.send 456
-      y.disconnect()
-      z.send 789
-      z.disconnect()
-
-    it 'should process inputs for different groups independently with group: true', (done) ->
-      src =
-        1: {x: 1, y: 2, z: 3}
-        2: {x: 4, y: 5, z: 6}
-        3: {x: 7, y: 8, z: 9}
-      inOrder = [
-        [ 1, 'x' ]
-        [ 3, 'z' ]
-        [ 2, 'y' ]
-        [ 2, 'x' ]
-        [ 1, 'z' ]
-        [ 2, 'z' ]
-        [ 3, 'x' ]
-        [ 1, 'y' ]
-        [ 3, 'y' ]
-      ]
-      outOrder = [ 2, 1, 3 ]
-
-      helpers.GroupComponent c, (data, groups, out) ->
-        out.send {x: data.x, y: data.y, z: data.z}
-      , ['x', 'y', 'z'], 'point', {group: true}
-
-      groups = []
-
-      p.on 'begingroup', (grp) ->
-        groups.push grp
-      p.on 'endgroup', (grp) ->
-        groups.pop()
-      p.on 'data', (data) ->
-        chai.expect(groups.length).to.equal 1
-        chai.expect(groups[0]).to.equal outOrder[0]
-        chai.expect(data).to.deep.equal src[outOrder[0]]
-        outOrder.shift()
-        done() unless outOrder.length
-
-      for tuple in inOrder
-        input = null
-        switch tuple[1]
-          when 'x'
-            input = x
-          when 'y'
-            input = y
-          when 'z'
-            input = z
-        input.beginGroup tuple[0]
-        input.send src[tuple[0]][tuple[1]]
-        input.endGroup()
-        input.disconnect()
-
-    it 'should support asynchronous handlers', (done) ->
-      point =
-        x: 123
-        y: 456
-        z: 789
-
-      helpers.GroupComponent c, (data, groups, out, callback) ->
-        setTimeout ->
-          out.send {x: data.x, y: data.y, z: data.z}
-          callback()
-        , 100
-      , ['x', 'y', 'z'], 'point', {async: true, group: true}
-
-      p.removeAllListeners()
-      counter = 0
-      p.on 'begingroup', (grp) ->
-        counter++
-      p.on 'endgroup', ->
-        counter--
-      p.once 'data', (data) ->
-        chai.expect(data).to.deep.equal point
-      p.once 'disconnect', ->
-        chai.expect(counter).to.equal 0
-        done()
-
-      x.beginGroup 'async'
-      y.beginGroup 'async'
-      z.beginGroup 'async'
-      x.send point.x
-      y.send point.y
-      z.send point.z
-      x.endGroup()
-      y.endGroup()
-      z.endGroup()

     describe 'when grouping by field', ->
       c = new component.Component


Tests: More tests for infinte recursion in subtree

--- spec/Subtree.coffee
+++ spec/Subtree.coffee
@@ -41,7 +41,48 @@
       .finally (c, res) ->
         chai.expect(c.get('non-existant2')).to.equal null
         chai.expect(res).to.be.an 'array'
-        chai.expect(res).to.eql [
-          15
-        ]
+        chai.expect(res).to.eql [ 15 ]
         done()
+
+
+  describe 'attribute lookup in continue tree', ->
+    it 'should return null', (done) ->
+      direct = (orig, data) ->
+        subtree = orig.continue 'directcalc'
+        subtree.deliver data
+        t = subtree.then 'tripled', (c, d) ->
+          c.set 'existant2', 'foo'
+          chai.expect(c.get('non-existant1')).to.equal null
+          d * 3
+
+      t = Root()
+      t.deliver 5
+      .all [direct]
+      .finally (c, res) ->
+        chai.expect(c.get('existant2')).to.equal 'foo'
+        chai.expect(res).to.be.an 'array'
+        chai.expect(res).to.eql [ 15 ]
+        done()
+
+  describe 'attribute lookup in parent continue tree', ->
+    it 'should return null', (done) ->
+      direct = (orig, data) ->
+        subtree = orig.continue 'directcalc'
+        subtree.deliver data
+        t = subtree.then 'tripled', (c, d) ->
+          chai.expect(c.get('non-existant1')).to.equal null
+          chai.expect(c.get('existant2')).to.equal 'foo'
+          console.log d*3
+          d * 3
+
+      t = Root()
+      t.deliver 5
+      .then "bar", (n, v) ->
+        n.set 'existant2', 'foo'
+        v
+      .all "foo", [direct]
+      .finally (c, res) ->
+        chai.expect(c.get('existant2')).to.equal 'foo'
+        chai.expect(res).to.be.an 'array'
+        chai.expect(res).to.eql [ 15 ]
+        done()


Fudge hasEntryContent boundary for zoomed out layouts that report (incorrect?) scrollTop. Fixes #83.

--- app/assets/javascripts/keyboard.js.coffee
+++ app/assets/javascripts/keyboard.js.coffee
@@ -408,7 +408,7 @@
     @selectItem()

   hasEntryContent: ->
-    @entryScrollHeight() - $('.entry-content').prop('scrollTop') > 0
+    @entryScrollHeight() - $('.entry-content').prop('scrollTop') > 2

   scrollContent: (interval, direction) ->
     if 'down' == direction


OX-603 fix help menu

OX-603 fix help menu

--- app/assets/javascripts/oxalis/view.coffee
+++ app/assets/javascripts/oxalis/view.coffee
@@ -107,7 +107,7 @@

P,N	Previous/Next comment	Del	Delete node/Split trees

C	Create new tree	Shift + Alt + Leftclick	Merge two trees

T	Toggle theme	M	Toggle mode

-
1	Toggle skeleton visibility	2	Toggle inactive tree visibility

+
8	Toggle skeleton visibility	9	Toggle inactive tree visibility

Shift + Mousewheel	Change node size
'

     skeletonKeys =
@@ -132,7 +132,11 @@

'''

-    html += generalKeys + if @mode == constants.MODE_PLANE_TRACING then viewportKeys + skeletonKeys else arbitraryKeys
+    html += generalKeys
+    if @mode == constants.MODE_PLANE_TRACING
+      html += viewportKeys + skeletonKeys
+    else if @mode == constants.MODE_ARBITRARY
+      html += arbitraryKeys

     html += '''


dropdown selector superposition corrected

--- app/assets/javascripts/selector.js.coffee
+++ app/assets/javascripts/selector.js.coffee
@@ -403,6 +403,11 @@
   $(document).on 'selector:change', '[data-filter-unroll]', (e) ->
     filterableUnroll.filter $(this)

+  $(document).on 'selector:menu-opened', '.selector', (e) ->
+    $currentDropDown = $(this)
+    $('.selector').not($currentDropDown).each ->
+      $(this).find('.items-menu').hide()
+
   filterableUnroll =
     filter: ($filteringUnroll) ->
       filterId = $filteringUnroll.selector('value')
@@ -450,6 +455,5 @@
           retrievedIds.push $(this).selector('value')

       { retrieved_ids: retrievedIds, selected_value: selectedValue }
-
   return
 ) ekylibre, jQuery


Update comment.

Update comment.

--- app/client/js/react/pages/editsong.coffee
+++ app/client/js/react/pages/editsong.coffee
@@ -21,8 +21,7 @@
   constructor: (actions, routes, yellowFade, usersStore, element) ->
     {div, form, input, textarea, p, nav, ol, li, br, a, span, button} = element

-    # Why not React state? Because it's not preserved over component life cycle.
-    # TODO: Add state =
+    # TODO: Move to store.
     editMode = false
     lyricsHistoryChanged = false
     lyricsHistoryShown = false


Make the ArrayPort tests silent

Make the ArrayPort tests silent

--- test/ArrayPort.coffee
+++ test/ArrayPort.coffee
@@ -2,24 +2,26 @@

 getBaseGraph = ->
     graph = new noflo.Graph "ArrayPort"
-    graph.addNode "Display", "Output"
-    graph.addInitial "Foo", "Display", "in"
-    graph.addInitial "Bar", "Display", "in"
+    graph.addNode "Merge", "Merge"
+    graph.addNode "Drop", "Drop"
+    graph.addInitial "Foo", "Merge", "in"
+    graph.addInitial "Bar", "Merge", "in"
+    graph.addEdge "Merge", "out", "Drop", "in"
     graph

 exports["test ArrayPort type"] = (test) ->
     graph = getBaseGraph()
     network = noflo.createNetwork graph, ->
-      port = network.processes["Display"].component.inPorts["in"]
+      port = network.processes["Merge"].component.inPorts["in"]
       test.equal port instanceof noflo.ArrayPort, true
       test.done()

 exports["test connecting to ArrayPorts"] = (test) ->
     graph = getBaseGraph()
     network = noflo.createNetwork graph, ->
-      test.equal network.connections.length, 2
+      test.equal network.connections.length, 3

-      port = network.processes["Display"].component.inPorts["in"]
+      port = network.processes["Merge"].component.inPorts["in"]
       test.equal port.sockets.length, 2

       test.done()
@@ -28,7 +30,7 @@
     graph = getBaseGraph()
     network = noflo.createNetwork graph, ->

-      port = network.processes["Display"].component.inPorts["in"]
+      port = network.processes["Merge"].component.inPorts["in"]

       first = port.sockets[0]
       port.detach first


Age in seconds

Age in seconds

--- test/expirable.coffee
+++ test/expirable.coffee
@@ -7,7 +7,7 @@
   obj.include Expirable

   4.times ->
-    obj.update()
+    obj.trigger(1)

   equals obj.I.active, true, "object is active until duration is exceeded"


More serialization tests.

More serialization tests.

--- test/json_test.coffee
+++ test/json_test.coffee
@@ -17,6 +17,27 @@

     expect(JSON.stringify(obj)).be.like '{"name":"John"}'

+  it 'ISO string dates', ->
+    obj = Ento()
+      .attr('date', Date)
+      .build(date: new Date("2010-01-01Z"))
+
+    expect(JSON.stringify(obj)).be.like '{"date":"2010-01-01T00:00:00.000Z"}'
+
+  it 'numbers', ->
+    obj = Ento()
+      .attr('number', Number)
+      .build(number: 0o100)
+
+    expect(JSON.stringify(obj)).be.like '{"number":64}'
+
+  it 'boolean', ->
+    obj = Ento()
+      .attr('bool', Boolean)
+      .build(bool: "1")
+
+    expect(JSON.stringify(obj)).be.like '{"bool":true}'
+
   it 'json: false', ->
     obj = Ento()
       .attr('name')


Fixed a few filter bugs

Fixed a few filter bugs

--- app/controllers/interactive/graphs.coffee
+++ app/controllers/interactive/graphs.coffee
@@ -73,7 +73,10 @@

   setGalaxyType: (e) =>
     button = $(e.currentTarget)
-    @options.galaxyType = button.data('type')
+    if button.hasClass 'pressed'
+      @options.galaxyType = null
+    else
+      @options.galaxyType = button.data('type')
     @setPressed button

   setDataSource: (e) =>
@@ -100,9 +103,10 @@
     @graph.height = 310
     @graph.channel = 'graph'

+    filter = {}
     if @options.galaxyType
-      filter = new Function "item", "return item['type'] === #{@options.galaxyType}"
-      @graph.addFilter filter
+      filter.func = new Function "item", "return item['type'] === '#{@options.galaxyType}'"
+      @graph.filters.push filter

     @graph.getDataSource("SkyServerSubject", @options.sampleSize)

@@ -112,7 +116,7 @@
   # Helper functions
   setPressed: (button) =>
     button.siblings().removeClass 'pressed'
-    button.addClass 'pressed'
+    button.toggleClass 'pressed'

 module.exports = Graphs


[test fix] pass a properly formed octal to fs.mkdir

--- test/repo.test.coffee
+++ test/repo.test.coffee
@@ -205,7 +205,7 @@
     repo    = null
     git_dir = __dirname + "/fixtures/junk_create_tag"
     before (done) ->
-      fs.mkdir git_dir, 0755, (err) ->
+      fs.mkdir git_dir, 0o755, (err) ->
         return done err if err
         git.init git_dir, (err) ->
           return done err if err


Fix PlayLevelView race condition typo

Fix PlayLevelView race condition typo

--- app/views/play/level/PlayLevelView.coffee
+++ app/views/play/level/PlayLevelView.coffee
@@ -143,7 +143,7 @@

   onLevelLoaded: (e) ->
     @god = new God({@gameUIState}) unless e.level.isType('web-dev')
-    @setUpGod() if @waitingToSetUpGod
+    @setupGod() if @waitingToSetUpGod

   trackLevelLoadEnd: ->
     return if @isEditorPreview


Modified addCSSAnimation

Modified addCSSAnimation

--- src/DivSugar.coffee
+++ src/DivSugar.coffee
@@ -54,8 +54,28 @@
       console.log "DivSugar: use '#{@_animationName}'"

       @_animationDuration = "-#{@_prefix}-animation-duration"
-      @_animationDuration = 'animation-duratino' if not div.style[@_animationDuration]?
+      @_animationDuration = 'animation-duration' if not div.style[@_animationDuration]?
       console.log "DivSugar: use '#{@_animationDuration}'"
+
+      @_animationTimingFunction = "-#{@_prefix}-animation-timing-function"
+      @_animationTimingFunction = 'animation-timing-function' if not div.style[@_animationTimingFunction]?
+      console.log "DivSugar: use '#{@_animationTimingFunction}'"
+
+      @_animationDelay = "-#{@_prefix}-animation-delay"
+      @_animationDelay = 'animation-delay' if not div.style[@_animationDelay]?
+      console.log "DivSugar: use '#{@_animationDelay}'"
+
+      @_animationIterationCount = "-#{@_prefix}-animation-iteration-count"
+      @_animationIterationCount = 'animation-iteration-count' if not div.style[@_animationIterationCount]?
+      console.log "DivSugar: use '#{@_animationIterationCount}'"
+
+      @_animationDirection = "-#{@_prefix}-animation-direction"
+      @_animationDirection = 'animation-direction' if not div.style[@_animationDirection]?
+      console.log "DivSugar: use '#{@_animationDirection}'"
+
+      @_animationFillMode = "-#{@_prefix}-animation-fill-mode"
+      @_animationFillMode = 'animation-fill-mode' if not div.style[@_animationFillMode]?
+      console.log "DivSugar: use '#{@_animationFillMode}'"

       requestAnimationFrame = @_prefix + 'RequestAnimationFrame'
       requestAnimationFrame = 'requestAnimationFrame' if not window[requestAnimationFrame]?
@@ -112,16 +132,13 @@

     for keyframe of animation
       style.innerHTML += "#{keyframe}{"
-
       transform = null
-      width = height = 1
+
       for property, value of animation[keyframe]

         switch property
           when 'size'
-            width = value[0]
-            height = value[1]
-            style.innerHTML += "width:#{width.toFixed(nod)}px;height:#{height.toFixed(nod)}px;"
+            style.innerHTML += "width:#{value[0].toFixed(nod)}px;height:#{value[1].toFixed(nod)}px;"

           when 'visible'
             style.innerHTML += "visibility:#{if value then 'visible' else 'hidden'};"
@@ -144,19 +161,18 @@
               style.innerHTML += "background-image:url(#{value});"

           when 'imageClip'
-            w = width / (value[2] - value[0])
-            h = height / (value[3] - value[1])
-            x = -value[0] * w
-            y = -value[1] * h
-            style.innerHTML += "background-position:#{x.toFixed(nod)}px #{y.toFixed(nod)}px;"
-            style.innerHTML += "background-size:#{w.toFixed(nod)}px #{h.toFixed(nod)}px;"
+            w = 1 / (value[2] - value[0]) * 100
+            h = 1 / (value[3] - value[1]) * 100
+            x = value[0] * w
+            y = value[1] * h
+            nod = DivSugar.NUM_OF_DIGITS
+
+            style.innerHTML += "background-position:#{x.toFixed(nod)}% #{y.toFixed(nod)}%;"
+            style.innerHTML += "background-size:#{w.toFixed(nod)}% #{h.toFixed(nod)}%;"

           when 'transform'
             transform ?= new DivSugar.Matrix()
-            if value instanceof DivSugar.Matrix
-              transform.set value
-            else
-              transform.set value[0], value[1], value[2], value[3], value[4], value[5], value[6], value[7], value[8], value[9], value[10], value[11]
+            transform.set value

           when 'translate'
             transform ?= new DivSugar.Matrix()


save artworks

save artworks

--- apps/home/components/top_rail/view.coffee
+++ apps/home/components/top_rail/view.coffee
@@ -35,10 +35,10 @@
       mabView = new MyActiveBids(user: @user)
       promises.push mabView.fetch()
     Q.all(promises).then ([c, data]) =>
-      @syncArtworks()
       @$el.html template
         artworks: @collection
         activeBids: activeBids = mabView?.bidderPositions.length
+      @syncArtworks()
       if activeBids
         mabView.$el = @$('.home-top-rail-right-mab')
         mabView.render().poll()


Extract MockFunction.verify_expectations()

--- src/TinyMock.coffee
+++ src/TinyMock.coffee
@@ -10,6 +10,7 @@
     mock.setup(args)
     try
       mock.run()
+      mock.verify_expectations()
     finally
       mock.cleanup()

@@ -30,6 +31,8 @@

   run: ->
     @_test_function.apply(null, @_mock_objects)
+
+  verify_expectations: ->
     errors = @_mock_methods.find_errors()
     fail( errors.join("\n") + "\n" ) unless errors.length == 0


Auto-merged master into dashboard on deployment.

--- assets/scripts/app/helpers/helpers.coffee
+++ assets/scripts/app/helpers/helpers.coffee
@@ -123,7 +123,7 @@
 _emojize = (text) ->
   emojis = text.match(/:\S+?:/g)
   if emojis isnt null
-    emojis.uniq().each (emoji, ix) ->
+    emojis.uniq().forEach (emoji, ix) ->
       strippedEmoji = emoji.substring(1, emoji.length - 1)
       unless EmojiDictionary.indexOf(strippedEmoji) is -1
         image = '<img class=\'emoji\' title=\'' + emoji + '\' alt=\'' + emoji + '\' src=\'' + '/images/emoji/' + strippedEmoji + '.png\'/>'