small fix for copy, paste in text field

small fix for copy, paste in text field

--- src/base.coffee
+++ src/base.coffee
@@ -269,6 +269,10 @@

   manageCopyAndPaste: (e) ->
     # http://stackoverflow.com/questions/17527870/how-does-trello-access-the-users-clipboard
+    #if user is using text field
+    el = document.activeElement
+    return if (el? and (el.tagName.toLowerCase() == 'input' and el.type == 'text') or el.tagName.toLowerCase() == 'textarea')
+
     target = @getLastSelectedTreema() ? @  # You can get the parent treema by somehow giving it focus but without selecting it; hacky
     if e.which is 86 and $(e.target).hasClass 'treema-clipboard'
       # Ctrl+V -- we might want the paste data


spotted mistake in algorithm.

spotted mistake in algorithm.

--- src/arguejs.coffee
+++ src/arguejs.coffee
@@ -52,6 +52,21 @@
             return false unless @isDefeated(other, args)
         @isConflictFree(args)

+    # args: subset of @argids
+    # returns true if args is admissible and there are no more arguments that can be added that would maintain it's admissiblity
+    isPreferred: (args) ->
+        # TODO
+
+    # args: subset of @argids
+    # returns true if
+    isSemiStable: (args) ->
+        # TODO
+
+    # args: subset of @argids
+    # returns true if
+    isIdeal: (args) ->
+        # TODO
+
     # returns set of accepted argument ids under grounded semantics
     grounded: ->
         label_in = []
@@ -61,13 +76,20 @@
             union = label_in.concat label_out
             others = complement(union, @argids)
             # extendin
+            added = []
             for arg in others
                 # add arg to label_in if all it's defeaters are out (or it has no defeaters)
                 tobeadded = true
                 for defeater in @defeatermap[arg]
-                    tobeadded=false if defeater not in label_out
-                label_in.push arg if tobeadded
+                    if defeater not in label_out
+                        tobeadded=false
+                        break
+                if tobeadded
+                    added.push arg
+                    label_in.push arg
+                    result=true
             # extendout
+            others = complement(added, others) if added isnt []
             for arg in others
                 if @isDefeated(arg, label_in)
                     label_out.push arg


KDTimeAgoView: Updating view when new data set.

--- client/Framework/components/time/timeagoview.coffee
+++ client/Framework/components/time/timeagoview.coffee
@@ -6,11 +6,15 @@

   constructor: (options = {}, data) ->

-    options.tagName = 'time'
+    options.tagName = "time"

     super options, data

     KDTimeAgoView.on "OneMinutePassed", => @updatePartial $.timeago @getData()

+  setData: ->
+    super
+    @updatePartial $.timeago @getData()  if @parent
+
   viewAppended: ->
     @setPartial $.timeago @getData()


Sidebar: minor code styling

Sidebar: minor code styling

--- client/Main/navigation/navigationmachineitem.coffee
+++ client/Main/navigation/navigationmachineitem.coffee
@@ -2,9 +2,9 @@

   {Running, Stopped} = Machine.State

-  stateClasses = ""
-  for state in Object.keys Machine.State
-    stateClasses += "#{state.toLowerCase()} "
+  stateClasses  = ''
+  stateClasses += "#{state.toLowerCase()} " for state in Object.keys Machine.State
+

   JView.mixin @prototype

@@ -25,7 +25,7 @@

     @machine = @getData()
     @progress = new KDProgressBarView
-      cssClass : "hidden"
+      cssClass : 'hidden'
       # initial  : Math.floor Math.random() * 100

     { computeController } = KD.singletons


Templates: Softlayer: add user_data example, update defaults

--- client/app/lib/providers/templates/softlayer.coffee
+++ client/app/lib/providers/templates/softlayer.coffee
@@ -14,17 +14,17 @@

   "resource": {
     "softlayer_virtual_guest": {
-      "softlayer-vg": {
-        "name": "softlayer-vg",
+      "softlayer-instance": {
+        "name": "softlayer-instance",
         "domain": "koding.com",
         "region": "dal09",
-        "ssh_keys": ["123456"],
         "image": "UBUNTU_14_64",
         "cpu": 1,
         "ram": 1024,
         "local_disk": true,
         "public_network_speed": 10,
-        "hourly_billing": true
+        "hourly_billing": true,
+        "user_data": "\\necho \\\"hello world!\\\" >> /helloworld.txt\\n"
       }
     }
   }
@@ -44,8 +44,8 @@

 resource:
   softlayer_virtual_guest:
-    softlayer_vg:
-      name: softlayer-vg
+    softlayer-instance:
+      name: softlayer-instance
       domain: koding.com

       # Extra keypairs to be added to the new instance.
@@ -56,7 +56,7 @@
       region: dal09

       # Default image is Ubuntu 14.04 LTS x64
-      # image: UBUNTU_14_64
+      image: UBUNTU_14_64

       cpu: 1
       ram: 1024
@@ -65,4 +65,15 @@

       hourly_billing: true

+      # on user_data section we will write bash and configure our VM
+      user_data: |-
+        # let's create a file on your root folder:
+        echo "hello world!" >> /helloworld.txt
+        # please note: all commands under user_data will be run as root.
+        # now add your credentials and save this stack.
+        # once vm finishes building, you can see this file by typing
+        # ls /
+        #
+        # for more information please click the link below "Stack Script Docs"
+
   '''


working gif bot

working gif bot

--- scripts/gif.coffee
+++ scripts/gif.coffee
@@ -9,13 +9,12 @@
 module.exports = (robot) ->
   robot.hear /(gif|gifbot)( me){1} (.*)/i, (msg) ->
     query = msg.match[3]
-    query = query.split(' ').join('+')
     url = 'http://api.giphy.com/v1/gifs/random'
     robot.http(url)
       .query({
         api_key: 'dc6zaTOxFJmzC'
         limit: 1
-        tag: search
+        tag: query.split(' ').join('+')
         })
       .get() (err, res, body) ->
         if res.statusCode is 200
@@ -23,8 +22,7 @@
           gif = gif.data.image_original_url

           unless gif?
-            msg.send 'No gifs found for \'#{search}\''
+            msg.send "No gifs found for \"#{query}\""
             return

-          msg.send gif
-
+          msg.reply gif


Tests: Added postActivity and postComment helpers.

--- tests/src/helpers/helpers.coffee
+++ tests/src/helpers/helpers.coffee
@@ -46,22 +46,8 @@
       .waitForElementVisible  '[testpath=main-header]', 10000 # Assertion

-  postActivity: (browser) ->
+  doRegister: (browser, user) ->

-    post = faker.Lorem.paragraph().replace(/(?:\r\n|\r|\n)/g, '')
-
-    browser
-      .waitForElementVisible  '[testpath=ActivityInputView]', 10000
-      .click                  '[testpath="ActivityTabHandle-/Activity/Public/Recent"]'
-      .click                  '[testpath=ActivityInputView]'
-      .setValue               '[testpath=ActivityInputView]', post
-      .click                  '[testpath=post-activity-button]'
-      .pause                  3000
-
-    browser.assert.containsText('[testpath=ActivityListItemView]:first-child', post)
-
-
-  doRegister: (browser, user) ->
     user    = utils.getUser(yes) unless user
     url     = @getUrl()

@@ -80,5 +66,38 @@
     @doLogin browser, user

+  postActivity: (browser) ->
+
+    @beginTest(browser)
+
+    browser.click '[testpath=public-feed-link]'
+
+    post = faker.Lorem.paragraph().replace(/(?:\r\n|\r|\n)/g, '')
+
+    browser
+      .waitForElementVisible  '[testpath=ActivityInputView]', 10000
+      .click                  '[testpath="ActivityTabHandle-/Activity/Public/Recent"]'
+      .click                  '[testpath=ActivityInputView]'
+      .setValue               '[testpath=ActivityInputView]', post
+      .click                  '[testpath=post-activity-button]'
+      .pause                  3000
+
+    browser.assert.containsText '[testpath=ActivityListItemView]:first-child', post # Assertion
+
+
+  postComment: (browser) ->
+
+    @postActivity(browser)
+
+    comment = faker.Lorem.paragraph().replace(/(?:\r\n|\r|\n)/g, '')
+
+    browser
+      .click        '[testpath=ActivityListItemView]:first-child [testpath=CommentInputView]'
+      .setValue     '[testpath=ActivityListItemView]:first-child [testpath=CommentInputView]', comment + '\n'
+      .pause        3000
+
+    browser.assert.containsText  '[testpath=ActivityListItemView]:first-child .comment-body-container', comment # Assertion
+
+
   getUrl: ->
     return 'http://lvh.me:8090'


Optimize animation loop for colors/number value converters

--- framer/Animation.coffee
+++ framer/Animation.coffee
@@ -181,28 +181,38 @@
 		@emit("start")
 		Framer.Loop.on("update", @_update)

+		# Figure out what kind of values we have so we don't have to do it in
+		# the actual update loop. This saves a lot of frame budget.
+
+		@_valueUpdaters = {}
+
+		for k, v of @_stateB
+			if Color.isColorObject(v) or Color.isColorObject(@_stateA[k])
+				@_valueUpdaters[k] = @_updateColorValue
+			else
+				@_valueUpdaters[k] = @_updateNumberValue
+
 	_update: (delta) =>
 		if @_animator.finished()
-			@_updateValue(1)
+			@_updateValues(1)
 			@stop(emit=false)
 			@emit("end")
 			@emit("stop")
 		else
-			@_updateValue(@_animator.next(delta))
-
-	_updateValue: (value) =>
-
-		for k, v of @_stateB
-
-			if Color.isColorObject(v) or Color.isColorObject(@_stateA[k])
-				@_target[k] = Color.mix(@_stateA[k], @_stateB[k], value, false, @options.colorModel)
-			else
-				@_target[k] = Utils.mapRange(value, 0, 1, @_stateA[k], @_stateB[k])
-
-		return
+			@_updateValues(@_animator.next(delta))
+
+	_updateValues: (value) =>
+		@_valueUpdaters[k](k, value) for k, v of @_stateB
+		return null
+
+	_updateNumberValue: (key, value) =>
+		@_target[key] = Utils.mapRange(value, 0, 1, @_stateA[key], @_stateB[key])
+
+	_updateColorValue: (key, value) =>
+		@_target[key] = Color.mix(@_stateA[key], @_stateB[key], value, false, @options.colorModel)

 	_currentState: ->
-		_.pick @options.layer, _.keys(@options.properties)
+		return _.pick(@options.layer, _.keys(@options.properties))

 	_animatorClass: ->


types can be different objects, same type

maybe we should tie types to the relevant java/lang/Class object?

--- src/runtime.coffee
+++ src/runtime.coffee
@@ -195,7 +195,7 @@
   # :type1 and :type2 should both be instances of types.Type.
   is_castable: (type1, type2) ->
     if (type1 instanceof types.PrimitiveType) or (type2 instanceof types.PrimitiveType)
-      return type1 == type2
+      return type1.name == type2.name  # since types are created on the fly, we can have different Type objects for the same type
     if type1 instanceof types.ArrayType
       if type2 instanceof types.ArrayType
         return @is_castable(type1.component_type, type2.component_type)


deleting trailing spaces

deleting trailing spaces

--- src/runtime.coffee
+++ src/runtime.coffee
@@ -274,11 +274,11 @@
     # Note that we don't throw a ReturnException here, so callers need to
     # yield the JVM execution themselves.
     return
-
+
   init_park_state: (thread) ->
     thread.$park_count ?= 0
     thread.$park_timeout ?= Infinity
-
+
   park: (thread, timeout) ->
     @init_park_state thread
     thread.$park_count++
@@ -286,16 +286,16 @@
     debug "TE(park): parking #{thread_name @, thread} (count: #{thread.$park_count}, timeout: #{thread.$park_timeout})"
     # Only choose a new thread if this one will become blocked
     @choose_next_thread null, ((nt) => @yield(nt)) if @parked thread
-
+
   unpark: (thread) ->
     @init_park_state thread
     debug "TE(unpark): unparking #{thread_name @, thread}"
     thread.$park_count--
     thread.$park_timeout = Infinity
-
+
     # Yield to the unparked thread if it should be unblocked
     @yield(thread) unless @parked thread
-
+
   parked: (thread) -> thread.$park_count > 0

   curr_frame: -> @meta_stack().curr_frame()


fixing heapify bug

fixing heapify bug

--- src/runtime.coffee
+++ src/runtime.coffee
@@ -23,17 +23,16 @@
     cdata.string_redirect = {}
     cp = cdata.constant_pool
     str_refs = []
-    cp.each (i,c) -> str_refs.push c.value if c.type is 'String'
+    cp.each (i,c) -> str_refs.push c.value if c.type in ['String','class']
     for sr in str_refs
       cdata.string_redirect[sr] = @init_string(cp.get(sr).value)
       console.log "heapifying #{sr} -> #{cdata.string_redirect[sr]} : '#{cp.get(sr).value}'"

   string_redirect: (oref,cls) ->
     cdata = @class_lookup(cls)
-    if cdata.string_redirect[oref]
-      console.log "redirecting #{oref} -> #{cdata.string_redirect[oref]}"
-      oref = cdata.string_redirect[oref]
-    return oref
+    throw new Error "can't redirect const string at #{oref}" unless cdata.string_redirect[oref]
+    console.log "redirecting #{oref} -> #{cdata.string_redirect[oref]}"
+    return cdata.string_redirect[oref]

   curr_frame: () -> _.last(@meta_stack)


fixed fastaexporter

fixed fastaexporter

--- js/msa/menu/defaultmenu.coffee
+++ js/msa/menu/defaultmenu.coffee
@@ -54,7 +54,7 @@
           saveAs blob, "all.fasta"

       menuExport.addNode "Export selection", =>
-        require ["saveAs"], (saveAs, FastaExporter) =>
+        require ["saveAs"], (saveAs) =>
           selection = @msa.selmanager.getInvolvedSeqs()
           unless selection?
             selection = @msa.seqs


Moved things around for @markijbema

Moved things around for @markijbema

--- core/app/backbone/views/react_search_results.coffee
+++ core/app/backbone/views/react_search_results.coffee
@@ -3,23 +3,6 @@
   changeOptions: 'add remove reset sync request'

   _results_per_page: 20  # WARNING: coupling with search_controller.rb
-
-  render: ->
-    _div [],
-      _h1 [],
-        'Search results for '
-        _strong [],
-          "\u201C" # left double quote
-          @model().query()
-          "\u201D" # right double quote
-      ReactLoadingIndicator model: @model()
-      if @model().length == 0 && !@model().loading()
-        _div [],
-          "Sorry, your search didn't return any results"
-      @_results()
-      if @model().length > @_results_per_page
-        _div ['search-results-capped-message'],
-          "(only showing first #{@_results_per_page} results)"

   _results: ->
     @model().map (model) =>
@@ -39,3 +22,20 @@
         else
           console.info "Unknown class of search result: ", model.get("the_class")
           _span()
+
+  render: ->
+    _div [],
+      _h1 [],
+        'Search results for '
+        _strong [],
+          "\u201C" # left double quote
+          @model().query()
+          "\u201D" # right double quote
+      if @model().length == 0 && !@model().loading()
+        _div [],
+          "Sorry, your search didn't return any results."
+      @_results()
+      ReactLoadingIndicator model: @model()
+      if @model().length > @_results_per_page
+        _div ['search-results-capped-message'],
+          "(only showing first #{@_results_per_page} results)"


Client: fix coffee gotcha

Client: fix coffee gotcha

--- workers/social/lib/social/models/socialapi/channel.coffee
+++ workers/social/lib/social/models/socialapi/channel.coffee
@@ -218,7 +218,7 @@
   # fetchActivities - fetch activities of a channel
   @fetchActivities = secure (client, options, callback)->
     {connection:{delegate}} = client
-    options.showExempt = delegate.checkFlag "super-admin" or delegate.isExempt
+    options.showExempt = delegate.checkFlag("super-admin") or delegate.isExempt
     options.channelId = options.id
     # just to create social channels
     ensureGroupChannel client, (err, socialApiChannelId)->


activity: set initial channel id of "addReply" request

--- workers/social/lib/social/models/socialapi/message.coffee
+++ workers/social/lib/social/models/socialapi/message.coffee
@@ -58,7 +58,9 @@
     success: (client, data, callback)->
       if not data.messageId or not data.body
         return callback message: "Request is not valid for adding a reply"
-      SocialMessage.doRequest 'addReply', client, data, callback
+      SocialMessage.ensureGroupChannel client, (err, socialApiChannelId)->
+        data.initialChannelId = socialApiChannelId
+        SocialMessage.doRequest 'addReply', client, data, callback

   # todo add permission here
   @edit = secure (client, data, callback)->


JVerificationToken: update verification code email [#86576308]

--- workers/social/lib/social/models/verificationtoken.coffee
+++ workers/social/lib/social/models/verificationtoken.coffee
@@ -116,18 +116,21 @@
     email.save callback

-  getTextBody:(firstName, plainPin)->
+  getTextBody: (firstName, plainPin)->
+
     """
     Hi #{firstName},

-    You can use following code to complete your request:
+    Here’s your koding.com verification code:

       #{plainPin}

+    Have a nice day!
     --
     Koding Team

     """
+

   @invalidatePin = (options, callback)->
     {email, action, username} = options


ヘルプメッセージの追加

ヘルプメッセージの追加

--- scripts/help-message.coffee
+++ scripts/help-message.coffee
@@ -7,6 +7,7 @@
 module.exports = (robot) ->
   robot.respond /(-help|-h|ヘルプ|詳細)/i, (msg) ->
     text = """
+    *チョコボットとDMで会話する際には"chocobot"この単語を消してください*
      バスの混み具合を確認する
      　`chocobot (bus or バス)`
      天気を聞く


kiteapi publish app manifest addition added

--- kites/sharedHosting/api.coffee
+++ kites/sharedHosting/api.coffee
@@ -161,22 +161,28 @@
     mkdirp versionedPath, (err)->
       if err then cb err
       else
-        fs.readFile userAppPath, (err, appScript)->
+        fs.readFile "#{userAppPath}/index.js", (err, appScript)->
           if err then cb err
           else
-            fs.writeFile "#{versionedPath}/index.js", appScript, 'utf-8', (err)->
+            fs.readFile "#{userAppPath}/.manifest", (err, manifest)->
               if err then cb err
               else
-                fs.stat latestPath, (err, statObj)->
-                  if err
-                    exec "ln -s #{versionedPath} #{latestPath}", cb
+                fs.writeFile "#{versionedPath}/index.js", appScript, 'utf-8', (err)->
+                  if err then cb err
                   else
-                    if statObj.isSymbolicLink() or statObj.isFile()
-                      exec "rm #{latestPath} && ln -s #{versionedPath} #{latestPath}", cb
-                    else if statObj.isDirectory() and appName.length?
-                      exec "rm -rf #{latestPath} && ln -s #{versionedPath} #{latestPath}", cb
-                    else
-                      cb new KodingError "Something went wrong"
+                    fs.writeFile "#{versionedPath}/.manifest", manifest, 'utf-8', (err)->
+                      if err then cb err
+                      else
+                        fs.stat latestPath, (err, statObj)->
+                          if err
+                            exec "ln -s #{versionedPath} #{latestPath}", cb
+                          else
+                            if statObj.isSymbolicLink() or statObj.isFile()
+                              exec "rm #{latestPath} && ln -s #{versionedPath} #{latestPath}", cb
+                            else if statObj.isDirectory() and appName.length?
+                              exec "rm -rf #{latestPath} && ln -s #{versionedPath} #{latestPath}", cb
+                            else
+                              cb new KodingError "Something went wrong"

   installApp: (options, callback)->


Fix indent

Fix indent

--- src/ProjectUtils.coffee
+++ src/ProjectUtils.coffee
@@ -237,7 +237,7 @@
   getCurrentDate = -> moment().toDate()
   updateProjectModifiedDate = Meteor.bindEnvironment (projectId, userId) ->
     Projects.update projectId, $set: {dateModified: getCurrentDate(), userModified: userId}
-    updateProjectModifiedDate = _.throttle(updateProjectModifiedDate, 5000)
+  updateProjectModifiedDate = _.throttle(updateProjectModifiedDate, 5000)

   Projects.before.insert (userId, doc) ->
     unless doc.dateModified


Use setProperties

Use setProperties

--- app/assets/javascripts/controllers/journal_thumbnail_controller.js.coffee
+++ app/assets/javascripts/controllers/journal_thumbnail_controller.js.coffee
@@ -23,8 +23,9 @@
     @get('model').save()
                  .then(@stopEditing.bind(@))
                  .catch ({errors: {name, description}}) ->
-                   self.set 'nameErrors', name?[0]
-                   self.set 'descriptionErrors', description?[0]
+                   self.setProperties
+                     nameErrors: name?[0]
+                     descriptionErrors: description?[0]

   actions:
     editJournalDetails: -> @set 'isEditing', true


Refactor timbit automated testing

Refactor timbit automated testing

--- src/timbits.coffee
+++ src/timbits.coffee
@@ -1,4 +1,4 @@
-express = require 'express'
+express = require 'express'
 pantry = require 'pantry'
 fs = require 'fs'
 path = require 'path'
@@ -11,25 +11,27 @@
 config = { appName: "Timbits", engine: "coffee", port: 5678, home: process.cwd() }
 server = {}

+log = new Log()
+
 # creates, configures, and returns a standard express server instance
 @serve = (options) ->
 	config[key] = value for key, value of options
 	@server = express.createServer()

-	@log = new Log(if @server.settings.env is 'development' then Log.DEBUG else Log.INFO)
-
+	log = new Log(if @server.settings.env is 'development' then Log.DEBUG else Log.INFO)
+
 	# support coffekup
 	@server.register '.coffee', ck
-
-	# configure server (still needs some thought)
+
+	# configure server (still needs some thought)
 	@server.set 'views', "#{config.home}/views"
 	@server.set 'view engine', config.engine
 	@server.set 'view options', {layout: false}
-
+
 	@server.configure =>
 		@server.use connectESI.setupESI()
 		@server.use express.static("#{config.home}/public")
-		@server.use express.static("#{config.home}/views")
+		@server.use express.static("#{config.home}/views")
 		@server.use express.bodyParser()
 		@server.use express.cookieParser()

@@ -38,18 +40,18 @@

 	@server.configure 'production', =>
 		@server.use express.errorHandler()
-
+
 	# route help page
 	@server.get '/', (req, res) ->
 		res.redirect '/timbits/help'
-
+
 	@server.get '/timbits/help', (req, res) =>
 		res.send ck.render(help, context: @box)

 	@server.get '/timbits/test', (req, res) =>
 		res.send ck.render(test, context: null)
-
-	#automagically load timbits found in the ./timbits folder
+
+	#automagically load timbits found in the ./timbits folder
 	path = "#{config.home}/timbits"
 	fs.readdir path, (err, files) =>
 		throw err if err
@@ -59,55 +61,55 @@

 	# starts the server
 	@server.listen process.env.PORT || process.env.C9_PORT || config.port
-	@log.info "Timbits server listening on port #{@server.address().port} in #{@server.settings.env} mode"
+	log.info "Timbits server listening on port #{@server.address().port} in #{@server.settings.env} mode"
 	server.address = @server.address().address
 	server.port = @server.address().port
-	return @server
-
-# the box that holds each of the individual timbits created
+	return @server
+
+# the box that holds each of the individual timbits created
 @box = {}

 # use the 'add' method to place a timbit in the box
 @add = (name, timbit) ->
 	#place the timbit in the box
-	@log.notice "Placing #{name} in the box"
+	log.notice "Placing #{name} in the box"
 	timbit.name = name
 	@box[name] = timbit
-
+
 	# configure help
 	@server.get ("/#{name}/help"), (req, res) ->
 		res.send ck.render(timbit.help, context: timbit)
-
+
 	@server.get ("/#{name}/test"), (req, res) ->
 		timbit.test server, timbit, (results) ->
-			results.timbit = timbit.name
+			#res.render "#{context.name}/#{context.view}", context: context
 			res.send ck.render(test, context: results)

 	# configure the route
 	@server.get ("/#{name}/:view?"), (req, res) ->
-
+
 		try
 			# initialize current request context
 			context = {}
 			context[k] = v for k,v of req.query
-
+
 			context.name = name
 			context.view = req.params.view ?= 'default'
-
+
 			# validate the request
 			for key, attr of timbit.params
 				context[key] ?= attr.default
 				value = context[key]
-
+
 				if attr.required and not value
-					throw "#{key} is a required parameter"
-
+					throw "#{key} is a required parameter"
+
 				if value and attr.strict and attr.values.indexOf(value) is -1
-					throw "#{value} is not a valid value for #{key}.  Must be one of [#{attr.values.join()}]"
-
+					throw "#{value} is not a valid value for #{key}.  Must be one of [#{attr.values.join()}]"
+
 				if value instanceof Array and not attr.multiple
 					throw "#{key} must be a single value"
-
+
 			# with context created, it's time to consume this timbit
 			timbit.eat req, res, context
 		catch ex
@@ -136,12 +138,12 @@
 		box-shadow: 0 0px 20px #666;
 		min-height: 500px;
 	}
-
+
 	a {color: #4E5989; text-decoration: underline;}
 	a:hover {text-decoration: none;}

 	#wrapper {width: 750px; margin: 0 auto;}
-
+
 	h1 {border-bottom: 1px solid #999; width: 100%; font: 30px Tahoma, Geneva, sans-serif}

 	p, h3 {margin: 0 0 5px 0;}
@@ -163,33 +165,70 @@
 			ul ->
 				for k, v of @
 					li -> a href: "/#{k}/help", -> k + ' »'
-
+
 test = ->
 	h1 ->
-		"Timbits - #{@timbit}"
-
-	if @views
+		"Test Timbit '#{@timbit}'"
+
+	h3 'Testing Summary'
+
+	h4 'Views'
+	ul ->
+		for view in @views
+			li -> view
+
+	if @required?.length > 0
+		h4 'Required Parameters'
 		ul ->
-			for view in @views
-				li -> view
-	if @failed
-		p -> "Errors in testing #{@failed}"
-
-
-	if @response
-		@response
-
-	for k, v of @
-		p -> "#{k} [#{v}]"
-
-	if @.failures
-		ul ->
-			for failure in @failures
-				li -> "#{failure.timbit} : #{failure.error}"
+			for required in @required
+				li -> required
+
+	if @optional?.length > 0
+		h4 'Optional Parameters'
+		ul ->
+			for optional in @optional
+				li -> optional
+
+	passed = 0
+	failed = 0
+	for test in @tests
+		if test.status == 200 then passed++ else failed++
+
+	if passed > 0
+		h3 "Passed (#{passed})"
+		table ->
+			thead ->
+				tr ->
+					td 'Type'
+					td 'URL'
+			tbody ->
+				for test in @tests
+					if test.status is 200
+						tr ->
+							td test.type
+							td test.uri
+
+	if failed > 0
+		h3 "Failed (#{failed})"
+		table ->
+			thead ->
+				tr ->
+					td 'Type'
+					td 'URL'
+					td 'HTTP Status'
+					td 'Error Message'
+			tbody ->
+				for test in @tests
+					if test.status isnt 200
+						tr ->
+							td test.type
+							td test.uri
+							td test.status
+							td test.error

 # definition of a timbit
 class @Timbit
-
+
 	# default render implementation
 	render: (req, res, context) ->
 		if context.remote is 'true'
@@ -200,26 +239,26 @@
 							return $('##{context.timbit_id}').html(CoffeeKup.render(data, context));
 						});
 					});
-			"""
+			"""
 			res.setHeader "Content-Type", "text/plain"
 			res.write output
 			res.end()
 		else
 			res.render "#{context.name}/#{context.view}", context: context
-
-	# helper method to retrieve data via REST
+
+	# helper method to retrieve data via REST
 	fetch: (req, res, context, key, options, callback = @render) ->

 		pantry.fetch options, (error, results) =>
 			context[key] = results
-
+
 			# we're done, will now execute rendor method unless otherwise specified
 			callback(req, res, context)
-
-	# this is the method executed after a matching route.  overwritten on most implementations
+
+	# this is the method executed after a matching route.  overwritten on most implementations
 	eat: (req, res, context) ->
 		@render req, res, context
-
+
 	help: ->
 		style '''body {
 			background: #d2d5dc; /* Old browsers */
@@ -242,12 +281,12 @@
 			box-shadow: 0 0px 20px #666;
 			min-height: 500px;
 		}
-
+
 		a {color: #4E5989; text-decoration: underline;}
 		a:hover {text-decoration: none;}

 		#wrapper {width: 750px; margin: 0 auto;}
-
+
 		h1 {border-bottom: 1px solid #999; width: 100%; font: 30px Tahoma, Geneva, sans-serif}
 		h2 {font: 20px Tahoma, Geneva, sans-serif}

@@ -274,16 +313,16 @@
 				p @about or 'Developer was too lazy to describe this widget'

 				h2 'Examples:'
-
+
 				if @examples
 					ul ->
 						for example in @examples
 							li -> a href: example.href, -> example.caption
 				else
 					p 'Developer was too lazy to define any examples.'
-
+
 				h2 'Parameters'
-
+
 				if @params
 					table ->
 						tbody ->
@@ -306,108 +345,94 @@
 									if attr.values then td ->
 										if attr.strict then text('One of:') else text('Examples:')
 										ul -> li value for value in attr.values
-
+
 				else
 					p 'None defined'

 				div id:'return', -> a href: '/timbits/help', -> '« Help Index'
-
+
 	test: (server, context, callback) ->
 		results = {
-			timbit: ''
+			timbit: context.name
 			views: []
 			required: []
 			optional: []
-			failures: []
 			queries: []
-			failed: false
+			tests: []
 		}
-		async.series
-			testViews: (callback) ->
-				# Retrieve list of views form the views directory for this timbit
-				fs.readdir "#{config.home}/views/#{context.name}", (err,list) ->
-					if err || list is undefined
-						failure = {
-							timbit: 		"#{context.name}"
-							view:			""
-							statusCode:		""
-							querystring:	""
-							error:			"Unable to retrieve a list of views"
-						}
-						results.failures.push failure
-						results.views.push 'default' # We will attempt the default view anyway and hope the timbit knows what it is doing.
-						callback null
-					else
-						pending = list.length
-						for file in list then do (file) ->
-							if file.match(/\.coffee/)?
-								results.views.push (require('path').basename(file, '.coffee'))
-								callback null unless --pending
-			testParams: (callback) ->
-				# Process parameters, seperate into required vs. optional
-				for k, v of context.params
-					param = "#{k}=#{v.values[0]}"
-					if v.required
-						results.required.push param
-					else
-						results.optional.push param
-				callback null
-			testQueries: (callback) ->
-				# Determine whether we have optional and required parameters and build query strings.
-				if results.required?.length
-					if results.optional?.length
-						results.queries.push "#{results.required.join('&')}&#{results.optional.join('&')}"
-					results.queries.push results.required.join('&')
+
+		testViews = (callback) ->
+			# Retrieve list of views form the views directory for this timbit
+			fs.readdir "#{config.home}/views/#{context.name}", (err,list) ->
+				if err || list is undefined
+					results.tests.push { type: 'retrieve list', uri: '', status: '', error: "Unable to retrieve a list of views" }
+					results.views.push 'default' # We will attempt the default view anyway and hope the timbit knows what it is doing.
 				else
-					if results.optional?.length
-						results.queries.push "#{results.optional.join('&')}"
-				callback null
-			testRunQueries: (callback) ->
-				# Execute the tests based on provided querystrings, views and examples.
-				console.log "\n\n\tTesting Timbit - #{context.name}\n\t-------------------------------------------------\n"
-				# If there are no paramters to test we just test the views
-				if not results.queries?.length
-					pending = results.views.length
+					for file in list then do (file) ->
+						if file.match(/\.coffee/)?
+							results.views.push (require('path').basename(file, '.coffee'))
+				callback()
+
+		testParams = ->
+			# Process parameters, seperate into required vs. optional
+			for k, v of context.params
+				param = "#{k}=#{v.values[0]}"
+				if v.required
+					results.required.push param
+				else
+					results.optional.push param
+
+		testQueries = ->
+			# Determine whether we have optional and required parameters and build query strings.
+			results.queries.push "#{results.required.concat(results.optional).join('&')}"
+
+		testRequest = (type, uri, callback) ->
+			# Execute request for uri and store result
+			request {uri: "#{uri}" }, (error, response, body) ->
+				results.tests.push { type: type, uri: uri, status: response.statusCode, error: (if error? then error else (if body? then body else '')) }
+				callback()
+
+		testRunQueries = (callback) ->
+			if results.queries?.length is 0
+				# Test each view if no query parameters present
+				pending = results.views.length
+				for view in results.views then do (view) ->
+					testRequest "views", "http://#{server.address}:#{server.port}/#{context.name}/#{view}", ->
+						callback() unless --pending
+			else
+				# Test each query for each view
+				pending = results.queries.length * results.views.length
+				for query in results.queries then do (query) ->
 					for view in results.views then do (view) ->
-						console.log "\tTesting view: #{view} with \n\t\t uri: http://#{server.address}:#{server.port}/#{context.name}/#{view}"
-						request { uri: "http://#{server.address}:#{server.port}/#{context.name}/#{view}"}, (error, response, body) ->
-							if error
-								# Would like to see if we can reduce this by a function, however too much context and other object are in use
-								# it seems to work best to leave this bit in even thoguh I repeat it in the 'else' and for examples
-								failure = {	timbit: "#{context.name}", view: "#{view}", statusCode:	"#{response.statusCode}", querystring: "", error: "#{error}" }
-								console.log "\n** \t\t Errors: #{error || body}"
-								results.failures.push failure
-								results.failed = true
-							callback null unless --pending
+						testRequest "queries", "http://#{server.address}:#{server.port}/#{context.name}/#{view}?#{query}", ->
+							callback() unless --pending
+
+		testRunExamples = (callback) ->
+			# Test the example links provided.
+			if context.examples?
+				pending = context.examples.length
+				for example in context.examples then do (example) ->
+					testRequest "example", "http://#{server.address}:#{server.port}#{example.href}", ->
+						callback() unless --pending
+			else
+			 	callback()
+
+		runTests = ->
+			# Execute the tests based on provided querystrings, views and examples.
+			testViews ->
+				testParams()
+				testQueries()
+				testRunQueries ->
+					testRunExamples ->
+						displayTests()
+
+		displayTests = ->
+			log.notice "Testing Timbit - #{context.name}"
+			for test in results.tests
+				if test.status >= 400 or error?
+					log.error "Test: #{test.type} URI: #{test.uri} Status: #{test.status} Error: #{test.error}"
 				else
-					# Test each query for each view
-					pending = results.queries.length
-					for query in results.queries then do (query) ->
-						for view in results.views then do (view) ->
-							console.log "\tTesting view: #{view} with query #{query} \n\t\t uri: http://#{server.address}:#{server.port}/#{context.name}/#{view}?#{query}"
-							request { uri: "http://#{server.address}:#{server.port}/#{context.name}/#{view}?#{query}"}, (error, response, body) ->
-								if response?.statusCode >= 400 or error
-									failure = {	timbit: "#{context.name}", view: "#{view}", statusCode:	"#{response.statusCode}", querystring: "#{query}", error: "#{error || body}" }
-									console.log "\n** \t\t Errors: #{error || body}"
-									results.failures.push failure
-									results.failed = true
-								callback null unless --pending
-			testRunExamples: (callback) ->
-				# Test the example links provided.
-				if context.examples?
-					pending = context.examples.length
-					for example in context.examples then do (example) ->
-						console.log "\tTesting example: #{example.caption} with \n\t\t uri: http://#{server.address}:#{server.port}#{example.href}"
-						request { uri: "http://#{server.address}:#{server.port}#{example.href}"}, (error, response, body) ->
-							if response?.statusCode >= 400 or error
-								failure = {	timbit: "#{context.name}", view: "", statusCode:	"#{response.statusCode}", querystring: "", error: "Using example #{example.href} - #{error || body}" }
-								console.log "\n** \t\t Errors: #{error || body}"
-								results.failures.push failure
-								results.failed = true
-							callback null unless --pending
-				else
-				 	callback null
-		, (error) ->
-			console.log "Errors: #{error}" if error
+					log.info "Test: #{test.type} URI: #{test.uri} Status: #{test.status}"
 			callback results
-
+
+		runTests()


Fix missing key prop for redirect route handler

--- src/catalog.coffee
+++ src/catalog.coffee
@@ -15,9 +15,9 @@
     styles: config.styles or []

   router = Routes {location: 'hash'},
-    Route _.extend({handler: App}, config),
-      config.pages.map((page) ->
+    Route _.extend({key: 'root', handler: App}, config),
+      config.pages.map (page) ->
         Route _.extend(key: page.name, defaultPageOptions, page)
-      ).concat(Redirect({key: 'not-found', from: '*', to: '/'}))
+    Redirect(from: '*', to: '/')

   React.renderComponent(router, document.body)


Add atom.tooltips global

Add atom.tooltips global

--- src/atom.coffee
+++ src/atom.coffee
@@ -125,6 +125,9 @@
   # Public: A {KeymapManager} instance
   keymaps: null

+  # Public: A {TooltipManager} instance
+  tooltips: null
+
   # Public: A {Project} instance
   project: null

@@ -201,6 +204,7 @@
     KeymapManager = require './keymap-extensions'
     ViewRegistry = require './view-registry'
     CommandRegistry = require './command-registry'
+    TooltipManager = require './tooltip-manager'
     PackageManager = require './package-manager'
     Clipboard = require './clipboard'
     Syntax = require './syntax'
@@ -226,6 +230,7 @@
     @config = new Config({configDirPath, resourcePath})
     @keymaps = new KeymapManager({configDirPath, resourcePath})
     @keymap = @keymaps # Deprecated
+    @tooltips = new TooltipManager
     @commands = new CommandRegistry
     @views = new ViewRegistry
     @packages = new PackageManager({devMode, configDirPath, resourcePath, safeMode})


special-cased x*y where x∊R and y∊Z, fixed #85

If x is real and y is (a real) integer, then x to the power of y must
surely be real, so we can skip the more general complex-number algorithm.

--- src/complex.coffee
+++ src/complex.coffee
@@ -75,8 +75,9 @@
     d = y.re * y.re + y.im * y.im
     simplify (x.re * y.re + x.im * y.im) / d, (y.re * x.im - y.im * x.re) / d

+  # ¯1 ¯2 ¯3 ¯4*2 ←→ 1 4 9 16
   @pow = pow = (x, y) ->
-    if typeof x is typeof y is 'number' and x >= 0
+    if typeof x is typeof y is 'number' and (x >= 0 or isInt(y))
       Math.pow x, y
     else
       exp multiply(y, log x)


stream context when building images to avoid unnecessary tarball creation/deletion step

--- src/decking.coffee
+++ src/decking.coffee
@@ -336,31 +336,27 @@
     options =
       t: image

-    tarball = "/tmp/decking-#{uuid.v4()}.tar"
-    log "Creating tarball to upload context..."
+    log "Uploading compressed context..."

     # @TODO allow user to specifiy --exclude params to avoid unnecessarily huge tarballs
-    child_process.exec "tar -cf #{tarball} ./", ->
-
-      fs.unlink "./Dockerfile", (err) -> log "[WARN] Could not remove Dockerfile" if err
-
-      log "Uploading tarball..."
-      docker.buildImage tarball, options, (err, res) ->
-        return done err if err
-
-        if res.headers["content-type"] is "application/json"
-          res
-            .pipe(JSONStream.parse "stream")
-            .pipe(process.stdout)
-        else
-          # we don't need an if/else but let's keep it for clarity; it'd be too easy to
-          # skim-read the code and misinterpret the first pipe otherwise
-          res
-            .pipe(process.stdout)
-
-        res.on "end", ->
-          log "Cleaning up..."
-          fs.unlink tarball, done
+    tar = child_process.spawn "tar", ["-c", "-", "./"]
+
+    docker.buildImage tar.stdout, options, (err, res) ->
+      return done err if err
+
+      if res.headers["content-type"] is "application/json"
+        res
+          .pipe(JSONStream.parse "stream")
+          .pipe(process.stdout)
+      else
+        # we don't need an if/else but let's keep it for clarity; it'd be too easy to
+        # skim-read the code and misinterpret the first pipe otherwise
+        res
+          .pipe(process.stdout)
+
+      res.on "end", ->
+        log "Cleaning up..."
+        fs.unlink "./Dockerfile", (err) -> log "[WARN] Could not remove Dockerfile" if err

   execute: (done) ->


handle split names and tokens effectively

--- src/epichal.coffee
+++ src/epichal.coffee
@@ -54,8 +54,10 @@
   robot.hear /.+/, (msg) ->
     return unless msg.message.text

-    maybeName = msg.message.text.split(/\b/)[0]
+
+    splitTokens = maybeName = msg.message.text.split(/\b/)
+    maybeName = splitTokens[0]
     if maybeName.toUpperCase() == robot.name.toUpperCase()
       robot.logger.debug 'Responding from EpicHAL'
-      eh.reply split.slice(2).join(''), ( err, res ) ->
+      eh.reply splitTokens.slice(2).join(''), ( err, res ) ->
         msg.send res


Concatenate JS files in main property of bower component in order.

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -300,8 +300,9 @@
     config._normalized.bowerComponents = bowerComponents
     filesMap = config._normalized.bowerFilesMap = {}
     bowerComponents.forEach (component) ->
-      component.files.forEach (file) ->
-        filesMap[file] = component.sortingLevel
+      filesLength = component.files.length
+      component.files.forEach (file, index) ->
+        filesMap[file] = component.sortingLevel + (filesLength * 0.001 - index * 0.001)

     deepFreeze config
     callback null, config


Clean up guards when no fibers are waiting.

--- synchronize.coffee
+++ synchronize.coffee
@@ -8,3 +8,4 @@
     return body()
   finally
     guards[uniqueId].exit topLevel
+    delete guards[uniqueId] if uniqueId of guards and not guards[uniqueId].isInUse()


Cleaup and done

Cleaup and done

--- src/js/main.coffee
+++ src/js/main.coffee
@@ -4,10 +4,10 @@

 # Uncomment this for having referenced
 # inline svg icons working in ie
-#
+#
 # svg4everybody = require 'svg4everybody'
-#
-#remember to initialize on doc ready svg4everybody()
+#
+# remember to initialize on doc ready svg4everybody()

 console.log 'main.js loaded!'


Better comments in `tael.coffee`

Better comments in `tael.coffee`

--- src/js/tael.coffee
+++ src/js/tael.coffee
@@ -16,9 +16,15 @@
                 type: 'leaf'
         ) - 1
     switch parent.type
+        # Modify the parent tile and append one or two new tile(s) to the
+        # `tiles` array.
         when 'container'
+            # Create only one tile; 'container' is the top-level tile.
             parent.child = do pushTile
         when 'leaf'
+            # Create two tiles; converts the 'leaf' tile into a 'branch' tile
+            # before inserting the two new 'leaf' tiles into the
+            # newly-classified 'branch' tile.
             parent =
                 type: 'branch'
                 children:
@@ -30,7 +36,7 @@
         when 'branch'
             error.throw "Branch tiles cannot spawn new children post-creation."

-newTile(tiles[0])
+newTile tiles[0]

 module.exports = ->
     ($ document).ready ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -6,6 +6,7 @@
 player = GameObject
   health: 100
   maxHealth: 100
+

 player.position()


Precompute Method.param_bytes.

Precompute Method.param_bytes.

--- src/methods.coffee
+++ src/methods.coffee
@@ -49,7 +49,10 @@
   parse_descriptor: (@raw_descriptor) ->
     [__,param_str,return_str] = /\(([^)]*)\)(.*)/.exec(@raw_descriptor)
     param_carr = param_str.split ''
+    type_size = (t) -> (if t.toString() in ['D','J'] then 2 else 1)
     @param_types = (field while (field = carr2type param_carr))
+    @param_bytes = util.sum(type_size(p) for p in @param_types)
+    @param_bytes++ unless @access_flags.static
     @num_args = @param_types.length
     @num_args++ unless @access_flags.static # nonstatic methods get 'this'
     @return_type = str2type return_str
@@ -67,16 +70,9 @@
       signature: rs.init_string @raw_descriptor
     }

-  param_bytes: () ->
-    type_size = (t) -> (if t.toString() in ['D','J'] then 2 else 1)
-    n_bytes = util.sum(type_size(p) for p in @param_types)
-    n_bytes++ unless @access_flags.static
-    n_bytes
-
   take_params: (caller_stack) ->
     params = []
-    n_bytes = @param_bytes()
-    caller_stack.splice(caller_stack.length-n_bytes,n_bytes)
+    caller_stack.splice(caller_stack.length-@param_bytes, @param_bytes)

   # used by run and run_manually to print arrays for debugging. we need this to
   # distinguish [null] from [].
@@ -172,8 +168,8 @@
       caller_stack = runtime_state.curr_frame().stack
       if virtual
         # dirty hack to bounce up the inheritance tree, to make sure we call the method on the most specific type
-        oref = caller_stack[caller_stack.length-@param_bytes()]
-        error "undef'd oref: (#{caller_stack})[-#{@param_bytes()}] (#{sig})" unless oref
+        oref = caller_stack[caller_stack.length-@param_bytes]
+        error "undef'd oref: (#{caller_stack})[-#{@param_bytes}] (#{sig})" unless oref
         obj = runtime_state.get_obj(oref)
         m_spec = {class: obj.type.toClassString(), sig: @name + @raw_descriptor}
         m = runtime_state.method_lookup(m_spec)


some more natives

some more natives

--- src/natives.coffee
+++ src/natives.coffee
@@ -199,7 +199,15 @@
         o 'findBootstrapClass(L!/!/String;)L!/!/Class;', (rs, _this, name) ->
             type = c2t util.int_classname rs.jvm2js_str name
             rs.dyn_class_lookup type, true
+        o 'getCaller(I)L!/!/Class;', (rs, i) ->
+            type = rs.meta_stack[rs.meta_stack.length-1-i].method.class_type
+            rs.class_lookup(type, true)
+
       ],
+      Compiler: [
+        o 'disable()V', (rs, _this) -> #NOP
+        o 'enable()V', (rs, _this) -> #NOP
+      ]
       Float: [
         o 'floatToRawIntBits(F)I', (rs, f_val) ->
             f_view = new Float32Array [f_val]
@@ -429,6 +437,11 @@
         o 'close0()V', (rs, _this) -> _this.fields.$file = null
       ]
       UnixFileSystem: [
+        o 'checkAccess(Ljava/io/File;I)Z', (rs, _this, file, access) ->
+            stats = stat_file rs.jvm2js_str file.fields.path
+            return false unless stats?
+            mode = stats.mode & 511
+            true  # TODO: actually use the mode, checking if we're the owner or in group
         o 'getBooleanAttributes0(Ljava/io/File;)I', (rs, _this, file) ->
             stats = stat_file rs.jvm2js_str file.fields.path
             return 0 unless stats?


Pipeline argument regex as hereregex.

Pipeline argument regex as hereregex.

--- src/pattern.coffee
+++ src/pattern.coffee
@@ -1,9 +1,17 @@
 # This module is separated for isolation during testing. It is meant to be
 # exposed as part of the public API.

-# Regular expression to match a JavaScript scalar taken in part from
-# [json2.js](http://www.JSON.org/json2.js).
-scalar = /('(?:[^\\']|\\.)+'|"(?:[^\\"]|\\.)+"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)(\s*,\s*|\s*\))?(.*)/
+# Regular expression to match a pipeline argument, expressed as a JavaScript
+# scalar, taken in part from [json2.js](http://www.JSON.org/json2.js).
+argument = ///
+  ( '(?:[^\\']|\\.)+'|"(?:[^\\"]|\\.)+"   # string
+  | true | false                          # boolean
+  | null                                  # null
+  | -?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?     # number
+  )
+  (\s*,\s*|\s*\))?                      # remaining arguments
+  (.*)                                  # remaining pattern
+///

 ##### packing(pattern)
 # Parse bit packing.
@@ -128,7 +136,7 @@
       hasArgument     = not pipe[2]

       while hasArgument
-        arg         = scalar.exec(rest)
+        arg         = argument.exec(rest)
         value       = eval(arg[1])
         hasArgument = arg[2].indexOf(")") is -1
         rest        = arg[3]


Fixed extension dot inconsistency between executables and sources, now both without dot

--- src/program.coffee
+++ src/program.coffee
@@ -18,7 +18,7 @@
         # 'name.ext1', but we need name to be 'name' and ext to be '.ext1.ext2'
         firstDotIndex = name.indexOf '.'
         if firstDotIndex isnt -1
-            extension = name[firstDotIndex...] + extension
+            extension = name[firstDotIndex+1...] + extension
             name = name[0...firstDotIndex]

         # TODO: Maybe exit and print error if isn't executable??


Use long option in example

Use long option in example

--- src/publish.coffee
+++ src/publish.coffee
@@ -23,7 +23,7 @@
     options.usage """

       Usage: apm publish [<newversion> | major | minor | patch | build]
-             apm publish -t <tagname>
+             apm publish --tag <tagname>

       Publish a new version of the package in the current working directory.


fix(relations): use a store system instead of directly referencing classes

--- src/backbone-relatable.coffee
+++ src/backbone-relatable.coffee
@@ -1,4 +1,15 @@
+Backbone.Relatable =
+  version: '0.0.1'
+  store: {}
+  reset: ->
+    Backbone.Relatable.store = {}
+
 class Backbone.RelatableModel extends Backbone.Model
+  @setup = (name = @name) ->
+    return throw new Error 'Class name is already defined' if Backbone.Relatable.store[name]?
+    Backbone.Relatable.store[name] =
+      class: @
+
   @relation = (name, options) ->
     return throw new Error 'Missing name' unless name?
     return throw new Error 'Missing options' unless options?
@@ -10,10 +21,25 @@

     switch options.type
       when 'one'
-        return throw new Error 'Class is not a backbone model' unless options.class.prototype instanceof Backbone.Model
         @prototype.relations[name] = options
       when 'many'
-        return throw new Error 'Class is not a backbone collection' unless options.class.prototype instanceof Backbone.Collection
         @prototype.relations[name] = options
       else
         throw new Error "Invalid relation type: #{type}"
+
+  constructor: (data) ->
+    super data
+
+    for name, options of @relations
+      if data?.hasOwnProperty name
+        obj = data[name]
+        if obj instanceof Backbone.RelatableModel
+          @[name] = obj
+        else
+          data = obj
+          data[options.inverse] = @
+
+          model = Backbone.Relatable.store[options.class]
+          return throw new Error "Class name #{options.class} is not defined" unless model?
+
+          @[name] = new model.class data


Default to the upstream node on save

Default to the upstream node on save

--- src/coffee/cilantro/models/context/manager.coffee
+++ src/coffee/cilantro/models/context/manager.coffee
@@ -77,12 +77,10 @@
         define: (attrs, options) ->
             @working.define(attrs, options)

+        # Save the model on behalf of a node. Defaults to the upstream node.
+        save: (node, options) ->
+            node ?= @upstream

-        # Save the model on behalf of a node.
-        # TODO this assumes only a single node has been changed. If multiple
-        # nodes are changed, only the node that triggers causes the save will
-        # have it's events triggered.
-        save: (node, options) ->
             # Trigger 'request' on node
             options = _.extend {}, options,
                 beforeSend: (xhr) =>


Add several snippets to atom

Add several snippets to atom

--- home/atom/snippets.cson
+++ home/atom/snippets.cson
@@ -2,5 +2,71 @@

 '.source.js':
   'console.log':
-    'prefix': 'log'
-    'body': 'console.log(${1:\'test\'})$2'
+    prefix: 'log'
+    body: 'console.log(${1:\'test\'})\n$2'
+
+  # Requires
+  'require fs':
+    prefix: 'path'
+    body: 'const fs = require(\'fs\')\n$1'
+  'require path':
+    prefix: 'path'
+    body: 'const path = require(\'path\')\n$1'
+  'require assert':
+    prefix: 'assert'
+    body: 'const assert = require(\'assert\')\n$1'
+
+  import:
+    prefix: 'imp'
+    body: 'import $1 from \'$2\'\n$3'
+
+  'console.error':
+    prefix: 'error'
+    body: 'console.error(${1:error})\n$2'
+
+  require:
+    prefix: 'req'
+    body: 'const $1 = require(\'$2\')\n$3'
+
+  'use strict':
+    prefix: 'strict'
+    body: "'use strict'\n"
+
+  'console.inspect':
+    prefix: 'ins'
+    body: """
+    console.log(require(\'util\').inspect(
+    \t$1,
+    \t{depth: null, colors: true}
+    ))$2
+    """
+
+  'console.dir':
+    prefix: 'dir'
+    body: 'console.dir($1, {depth: null, colors: true})\n$2'
+
+  'arrow function':
+    prefix: 'af'
+    body: '(${1:argument}) => ${2:statement}'
+
+  'arrow function with body':
+    prefix: 'afb'
+    body: """
+    (${1:argument}) => {
+    \t${0}
+    }
+    """
+
+'.source.coffee':
+  'console.inspect':
+    prefix: 'ins'
+    body: """
+    console.log require(\'util\').inspect(
+    \t$1
+    \t{depth: null, colors: true}
+    )$2
+    """
+
+  'console.dir':
+    prefix: 'dir'
+    body: 'console.dir $1, depth: null, colors: true\n$2'


Replaced a property read with a function call

--- scripts/time-tracker.coffee
+++ scripts/time-tracker.coffee
@@ -24,7 +24,7 @@
   constructor: (@name, @id) ->

   duration: ->
-    elapsed = 0 unless @starting?
+    elapsed = 0 unless @startTime?()
     elapsed ?= @endTime().getMinutes() - @startTime().getMinutes()
     "#{elapsed} minute#{if elapsed == 1 then '' else 's'}#{if @isRunning() then ' (*)' else ''}"


Adding more snippets

Adding more snippets

--- snippets/verify.cson
+++ snippets/verify.cson
@@ -190,13 +190,27 @@
     'prefix': 'verify that the page contains the text alias "text"'
     'body': 'verify that the page contains the text alias "$1"'

-    'verify70':
+  'verify72':
+    'prefix': 'verify that the page contains the regex "text"'
+    'body': 'verify that the page contains the regex "$1"'
+  'verify73':
+    'prefix': 'verify that the page contains the regex alias "text"'
+    'body': 'verify that the page contains the regex alias "$1"'
+
+    'verify74':
       'prefix': 'verify that the page does not contain the text "text"'
       'body': 'verify that the page does not contain the text "$1"'
-    'verify71':
+    'verify75':
       'prefix': 'verify that the page does not contain the text alias "text"'
       'body': 'verify that the page does not contain the text alias "$1"'

-  'verify80':
+    'verify76':
+      'prefix': 'verify that the page does not contain the regex "text"'
+      'body': 'verify that the page does not contain the regex "$1"'
+    'verify77':
+      'prefix': 'verify that the page does not contain the regex alias "text"'
+      'body': 'verify that the page does not contain the regex alias "$1"'
+
+  'verify90':
     'prefix': 'verify that there were no HTTP errors'
     'body': 'verify that there were no HTTP errors'


Use RootView.eachEditor from Autocomplete package

--- src/autocomplete.coffee
+++ src/autocomplete.coffee
@@ -5,8 +5,8 @@
 module.exports =
 class Autocomplete extends SelectList
   @activate: (rootView) ->
-    new Autocomplete(editor) for editor in rootView.getEditors()
-    rootView.on 'editor:attached', (e, editor) -> new Autocomplete(editor) unless editor.mini
+    rootView.eachEditor (editor) ->
+      new Autocomplete(editor) if editor.attached and not editor.mini

   @viewClass: -> "autocomplete #{super}"


raspberrypi3.coffee: Change the getting started guide links.

Signed-off-by: Florin Sarbu <3a837a37a2ac9072a088641ec330e0e5d5b86f4e@resin.io>

--- raspberrypi3.coffee
+++ raspberrypi3.coffee
@@ -12,9 +12,9 @@

 	instructions: commonImg.instructions
 	gettingStartedLink:
-		windows: 'http://docs.resin.io/#/pages/installing/gettingStarted.md#windows'
-		osx: 'http://docs.resin.io/#/pages/installing/gettingStarted.md#on-mac-and-linux'
-		linux: 'http://docs.resin.io/#/pages/installing/gettingStarted.md#on-mac-and-linux'
+		windows: 'https://docs.resin.io/raspberrypi3/nodejs/getting-started/#adding-your-first-device'
+		osx: 'https://docs.resin.io/raspberrypi3/nodejs/getting-started/#adding-your-first-device'
+		linux: 'https://docs.resin.io/raspberrypi3/nodejs/getting-started/#adding-your-first-device'
 	supportsBlink: true

 	options: [ networkOptions.group ]


Newline after package.json

Newline after package.json

--- build/scripts.coffee
+++ build/scripts.coffee
@@ -29,4 +29,4 @@
   # add all scripts to package.json
   pkg.bin[sname] = "./bin/#{sname}"

-fs.writeFileSync path.resolve(__dirname, '../package.json'), JSON.stringify(pkg, null, '  ')
+fs.writeFileSync path.resolve(__dirname, '../package.json'), JSON.stringify(pkg, null, '  ') + '\n'


Add support for a ATOM_DEV_RESOURCE_PATH env var

This is helpful on windows to set a default dev resource path that
isn't ~/github/atom since on Windows it is common to clone Atom to C:\atom

--- src/browser/main.coffee
+++ src/browser/main.coffee
@@ -54,7 +54,7 @@
     AtomApplication.open(args)
     console.log("App load time: #{Date.now() - global.shellStartTime}ms") unless args.test

-global.devResourcePath = path.join(app.getHomeDir(), 'github', 'atom')
+global.devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getHomeDir(), 'github', 'atom')

 setupCrashReporter = ->
   crashReporter.start(productName: 'Atom', companyName: 'GitHub')


Report specification and outer scenario events in teamcity output.

--- run-witness.coffee
+++ run-witness.coffee
@@ -39,10 +39,15 @@
 				else
 					escape error.toString()

+			passCount = 0
+			failCount = 0
+
 			Witness.messageBus.addHandlers
 				RunnerFinished: ->
+					console.log "#{passCount} passed. #{failCount} failed."
 					console.log "!exit"
 				RunnerDownloadFailed: ->
+					console.log "#{passCount} passed. #{failCount} failed."
 					console.log "!exit"

 				ScriptDownloading: (script) ->
@@ -69,11 +74,27 @@
 				SpecificationFileFailed: (file) ->
 					console.log "##teamcity[testSuiteFinished name='#{file.name}']"

+				SpecificationRunning: (spec) ->
+					console.log "##teamcity[testSuiteStarted name='#{spec.description}']"
+				SpecificationPassed: (spec) ->
+					console.log "##teamcity[testSuiteFinished name='#{spec.description}']"
+				SpecificationFailed: (spec) ->
+					console.log "##teamcity[testSuiteFinished name='#{spec.description}']"
+
+				OuterScenarioRunning: (outer) ->
+					console.log "##teamcity[testSuiteStarted name='Outer scenario #{outer.id}']"
+				OuterScenarioPassed: (outer) ->
+					console.log "##teamcity[testSuiteFinished name='Outer scenario #{outer.id}']"
+				OuterScenarioFailed: (outer) ->
+					console.log "##teamcity[testSuiteFinished name='Outer scenario #{outer.id}']"
+
 				ScenarioRunning: (scenario) ->
 					console.log "##teamcity[testStarted name='scenario-#{scenario.id}']"
 				ScenarioPassed: (scenario) ->
+					passCount++
 					console.log "##teamcity[testFinished name='scenario-#{scenario.id}']"
 				ScenarioFailed: (scenario, error) ->
+					failCount++
 					message = flattenErrorMessage error
 					console.log "##teamcity[testFailed name='scenario-#{scenario.id}' message='#{message}']"
 					console.log "##teamcity[testFinished name='scenario-#{scenario.id}']"


working on snap.coffee debugging.

working on snap.coffee debugging.

--- salvus/snap.coffee
+++ salvus/snap.coffee
@@ -13,12 +13,10 @@
 winston   = require 'winston'
 fs        = require 'fs'

-
 uuid      = require 'node-uuid'
 async     = require 'async'
 moment    = require 'moment'

-backup    = require 'backup'
 message   = require 'message'
 misc      = require 'misc'
 misc_node = require 'misc_node'
@@ -184,7 +182,10 @@
                     cb   : (err) ->
                         if not err
                             timestamp = moment(new Date(d*1000)).format('YYYY-MM-DD-HHmmss')
-                            local_snapshots[project_id].push(timestamp)
+                            if not local_snapshots[project_id]?
+                                local_snapshots[project_id] = [timestamp]
+                            else
+                                local_snapshots[project_id].push(timestamp)
                         cb(err)
             # update checksums in case of bitrot
             (cb) ->
@@ -435,7 +436,7 @@
         outFile:program.logfile
         errFile:program.logfile

-    daemon(conf, start_server)
+    #daemon(conf, start_server)

     #process.addListener "uncaughtException", (err) ->
     #    winston.error "Uncaught exception: " + err


replaceAll always replace str with ''

replaceAll always replace str with ''

Should replace matching string with parameter "replacement", not ''.

--- src/Pattern.coffee
+++ src/Pattern.coffee
@@ -134,7 +134,7 @@
         count = 0
         while @regex.test(str) and (limit is 0 or count < limit)
             @regex.lastIndex = 0
-            str = str.replace @regex, ''
+            str = str.replace @regex, replacement
             count++

         return [str, count]


more pagingation changes

more pagingation changes

--- src/coffee/views.coffee
+++ src/coffee/views.coffee
@@ -14,17 +14,9 @@
   className: 'div'

   render : () ->
-    console.log('data_source='+@mget('data_source'))
-    console.log('url='+@mget('url'))
-    console.log('total_rows='+@mget('total_rows'))
-    console.log('offset='+@mget('offset'))
-    console.log('chunksize='+@mget('chunksize'))
-    console.log('data_slice='+@mget('data_slice'))
-
     table_template = """

     """
-
     header_template = """

     """
@@ -56,19 +48,22 @@
         row.append(datacell)
         table.append(row)

+    @$el.empty()
     @render_pagination()
     @$el.append(table)
     if @mget('usedialog') and not @$el.is(":visible")
       @add_dialog()

   render_pagination : ->
-    console.log('render_pagination')
     table_hdr_template = """
-

+

+

+          Total Rows: {{total_rows}}

+

+

     """
     btn_group = $('

')
-    console.log('offset='+@mget('offset'))
     if @mget('offset') > 0
       node = $('')
       btn_group.append(node)
@@ -84,7 +79,6 @@
       )

     maxoffset = @mget('total_rows') - @mget('chunksize')
-    console.log('maxoffset='+maxoffset)

     if @mget('offset') < maxoffset
       node = $('')
@@ -101,9 +95,11 @@
         @model.load(maxoffset)
         return false
       )
-      table_hdr = $(table_hdr_template)
-      table_hdr.append(btn_group)
-      @$el.append(table_hdr)
+
+    table_hdr = $(_.template(table_hdr_template, {'total_rows' : @mget('total_rows')}))
+    btn_group = $('

').append(btn_group)
+    @$el.append(btn_group)
+    @$el.append(table_hdr)

 class TableView extends ContinuumView


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine.coffee
+++ test/engine.coffee
@@ -181,7 +181,7 @@
   engine.collides(Rectangle(0, 0, 10, 10), null)

 test "Integration", ->
-
+  engine = Engin
 test "#setState", 3, ->
   engine = Engine()


test fix

test fix

--- test/hActor.coffee
+++ test/hActor.coffee
@@ -28,6 +28,7 @@
   hActor2 = undefined
   config = require("./_config")
   hResultStatus = require("../lib/codes").hResultStatus
+  factory = require "../lib/hfactory"
   Actor = require("../lib/actor/hactor")

   describe "#FilterMessage()", ->


extracting point

extracting point

--- test/matrix.coffee
+++ test/matrix.coffee
@@ -131,71 +131,6 @@

     matrixEqual(m1, m2)

-  module "Point"
-
-  test "#add", ->
-    p1 = Point(5, 6)
-    p2 = Point(7, 5)
-
-    result = p1.add(p2)
-
-    equals result.x, p1.x + p2.x
-    equals result.y, p1.y + p2.y
-
-  test "#add two arg", ->
-    point = Point(3, 7)
-    x = 2
-    y = 1
-
-    result = point.add(x, y)
-
-    equals result.x, point.x + x
-    equals result.y, point.y + y
-
-  test "#subtract", ->
-    p1 = Point(5, 6)
-    p2 = Point(7, 5)
-
-    result = p1.subtract(p2)
-
-    equals result.x, p1.x - p2.x
-    equals result.y, p1.y - p2.y
-
-  test "#norm", ->
-    p = Point(2, 0)
-
-    normal = p.norm()
-    equals normal.x, 1
-
-    normal = p.norm(5)
-    equals normal.x, 5
-
-  test "#scale", ->
-    p1 = Point(5, 6)
-    scalar = 2
-
-    result = p1.scale(scalar)
-
-    equals result.x, p1.x * scalar
-    equals result.y, p1.y * scalar
-
-  test "#floor", ->
-    p1 = Point(7.2, 6.9)
-
-    ok Point(7, 6).equal(p1.floor())
-
-  test "#equal", ->
-    ok Point(7, 8).equal(Point(7, 8))
-
-  test "#magnitude", ->
-    equals Point(3, 4).magnitude(), 5
-
-  test ".fromAngle", ->
-    p = Point.fromAngle(Math.TAU / 4)
-
-    equalEnough p.x, 0, TOLERANCE
-    equals p.y, 1
-
   module undefined
 )()


CLI minimal refactor

CLI minimal refactor

--- src/bintray.coffee
+++ src/bintray.coffee
@@ -70,10 +70,14 @@

     @searchFile(pkgname, repository)
       .then (response) ->
+        notFound = ->
+          response.code = 404
+          response.status = 'Not Found'
+          return response
+
         { data } = response
         if response isnt 200 or _.isEmpty data
-          response.code = 404
-          deferred.reject response
+          deferred.reject notFound()
         else
           if _.isArray data
             data = data[0]
@@ -81,10 +85,9 @@
               response.data = { url: "#{Bintray.downloadsHost}/#{data.owner}/#{data.repo}/#{data.path}" }
               deferred.resolve response
             else
-              response.code = 404
-              deferred.reject response
+              deferred.reject notFound()
           else
-            deferred.reject response
+            deferred.reject notFound()
       , (error) ->
         deferred.reject error


为DataProvider提供response事件

为DataProvider提供response事件

--- src/core/service.coffee
+++ src/core/service.coffee
@@ -100,6 +100,7 @@

 	@events:
 		beforeSend: null
+		response: null
 		complete: null
 		success: null
 		error: null


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_flash.coffee
+++ src/engine_flash.coffee
@@ -32,9 +32,9 @@

   @name flash
   @methodOf Engine#
-  @param {Color} [color="white"] The flash color
-  @param {Number} [duration=12] How long the effect lasts
-  @returns {Number} [targetAlpha=0] The alpha value to fade to. By default, this is set to 0, which fades the color to transparent.
+  @param {Color} [color] The flash color
+  @param {Number} [duration] How long the effect lasts
+  @returns {Number} [targetAlpha] The alpha value to fade to. By default, this is set to 0, which fades the color to transparent.
   ###
   flash: (color, duration, targetAlpha) ->
     I.flashColor = Color(color || 'white')


more tasking logic with separate for date and time

--- src/flux/tasking.coffee
+++ src/flux/tasking.coffee
@@ -4,18 +4,10 @@
 {makeSimpleStore, extendConfig} = require './helpers'
 TimeHelper = require '../helpers/time'

-TASKING_IDENTIFIERS = ['target_id', 'target_type']
-
-getIndexFromForTasking = (fromCollection, tasking) ->
-  _.findIndex(fromCollection, (fromItem) ->
-    _.isEqual(
-      _.pick(tasking, TASKING_IDENTIFIERS),
-      _.pick(fromItem, TASKING_IDENTIFIERS)
-    )
-  )
-
 getFromForTasking = (fromCollection, tasking) ->
-  _.findWhere(fromCollection, _.pick(tasking, TASKING_IDENTIFIERS))
+  return fromCollection if _.isEmpty(tasking)
+
+  fromCollection[toTaskingIndex(tasking)]

 transformDefaultPeriod = (period) ->
   target_id: period.id
@@ -24,16 +16,19 @@
   due_time: period.default_due_time

 transformCourseToDefaults = (course) ->
-  {periods} = course
-
-  courseDefaults =
-    open_time: course.default_open_time
-    due_time: course.default_due_time
-
-  defaults = _.map periods, transformDefaultPeriod
-  defaults.push(courseDefaults)
-
-  defaults
+  {periods, id} = course
+
+  courseDefaults = _.chain(periods)
+    .indexBy((period) ->
+      "period#{period.id}"
+    )
+    .mapObject(transformDefaultPeriod)
+    .value()
+
+  courseDefaults.open_time = course.default_open_time
+  courseDefaults.due_time = course.default_due_time
+
+  courseDefaults

 transformTasking = (tasking) ->
   transformed = _.pick(tasking, TASKING_IDENTIFIERS)
@@ -47,8 +42,10 @@
   transformed

 transformTaskings = (taskings) ->
-  _.map(taskings, transformTasking)
-
+  _.chain(taskings)
+    .indexBy(toTaskingIndex)
+    .mapObject(transformTasking)
+    .value()

 maskToTasking = (tasking) ->
   masked = _.pick(tasking, TASKING_IDENTIFIERS)
@@ -58,90 +55,185 @@

   masked

+isTaskingValid = (tasking) ->
+  TimeHelper.isDateTimeString(tasking.opens_at) and TimeHelper.isDateTimeString(tasking.due_at)
+
+toTaskingIndex = (tasking) ->
+  "#{tasking.target_type}#{tasking.target_id}"
+
 # sample defaults
-# "#{courseId}": [{
+# "#{courseId}": {
 #   "open_time": "00:01"
-#   "due_time": "22:00"
-# }, {
-#   "target_id": "#{periodId}"
-#   "target_type": "period"
-#   "open_time": "00:01"
-#   "due_time": "11:00"
-# }...]
-
-toIndexer = (courseId, tasking) ->
-  identifiers = ["course#{courseId}"]
-
-  unless _.isUndefined(tasking)
-    if _.isString(tasking)
-      # assume tasking is taskId
-      tasking =
-        target_id: tasking
-        target_type: 'period'
-
-    identifiers.push("#{tasking.target_type}#{tasking.target_id}")
-
-  identifiers.join('.')
-
+#   "due_time": "22:00",
+#   "#{target_type}#{target_id}": {
+#     "open_time": "00:01"
+#     "due_time": "11:00"
+#   },
+#   "#{target_type}#{target_id}": {
+#     "open_time": "00:01"
+#     "due_time": "11:00"
+#   }
+# }

 TaskingConfig =
   _defaults: {}
   _taskings: {}
+  _tasksToCourse: {}

   loadDefaults: (courseId, course) ->
     @_defaults[courseId] = transformCourseToDefaults(course)
-    @emit("defaultsLoaded.#{courseId}")
+    @emit("defaults.#{courseId}.loaded")
+    true

   loadTaskings: (taskId, courseId, taskings) ->
-    taskings = transformTaskings(taskings)
-    @_taskings[taskId] = {taskings, courseId}
-    @emit("taskingsLoaded.#{taskId}")
-
-  updateTime: (taskId, tasking) ->
-
-  updateDate: () ->
+    @_taskings[taskId] = transformTaskings(taskings)
+    @_tasksToCourse[taskId] = courseId
+    @emit("taskings.#{taskId}.all.loaded")
+    true
+
+  updateTime: (taskId, tasking, timeString, type) ->
+    taskingIndex = toTaskingIndex(tasking)
+    timeString = TimeHelper.getTimeOnly(timeString)
+    return false unless @_taskings[taskId][taskingIndex]? and timeString?
+
+    @_taskings[taskId][taskingIndex]["#{type}_time"] = timeString
+    true
+
+  updateDate: (taskId, tasking, dateString, type) ->
+    taskingIndex = toTaskingIndex(tasking)
+    dateString = TimeHelper.getDateOnly(dateString)
+    return false unless @_taskings[taskId][taskingIndex]? and dateString?
+
+    @_taskings[taskId][taskingIndex]["#{type}_date"] = dateString
+    true

   updateTasking: (taskId, tasking) ->
-    taskingIndex = getIndexFromForTasking(@_taskings[taskId].taskings, tasking)
-    @_taskings[taskId].taskings[taskingIndex] = transformTasking(tasking)
+    taskingIndex = toTaskingIndex(tasking)
+    return false unless @_taskings[taskId][taskingIndex]?
+
+    @_taskings[taskId][taskingIndex] = transformTasking(tasking)
+    true
+
+  updateAllDates: (taskId, timeString, type) ->
+    dateString = TimeHelper.getDateOnly(dateString)
+    return false unless @_taskings[taskId]? and dateString?
+
+    _.each @_taskings[taskId], (tasking) ->
+      tasking["#{type}_date"] = dateString
+    true
+
+  updateAllTimes: (taskId, timeString, type) ->
+    timeString = TimeHelper.getTimeOnly(timeString)
+    return false unless @_taskings[taskId]? and timeString?
+
+    _.each @_taskings[taskId], (tasking) ->
+      tasking["#{type}_time"] = timeString
+    true
+
+  updateAllTaskings: (taskId, tasking) ->
+    return false unless @_taskings[taskId]?
+
+    _.each @_taskings[taskId], (prevTasking, taskingIndex) =>
+      @_taskings[taskId][taskingIndex] = tasking
+    true
+
+  initializeTaskingsToDefaultTimes: (taskId, dates = {open_date: '', due_date: ''}) ->
+    courseId = @exports.getCourseIdForTask.call(@, taskId)
+    taskings = @exports.getTaskingDefaults.call(@, courseId)
+
+    defaultToCourse = @exports.areDefaultTaskingTimesSame.call(@, courseId)
+
+    default = @exports.getDefaultsFor.call(@, courseId) if defaultToCourse
+    @_taskings[taskId] ?= {}
+
+    _.each taskings, (tasking) =>
+      default = @exports.getDefaultsFor.call(@, courseId, tasking) unless defaultToCourse
+      taskingIndex = toTaskingIndex(tasking)
+
+      @_taskings[taskId][taskingIndex] = _.extend({}, dates, default)
+
+    true

   exports:
     getDefaults: (courseId) ->
       @_defaults[courseId]

+    getTaskingDefaults: (courseId) ->
+      defaults = @exports.getDefaults.call(@, courseId)
+      _.omit(defaults, 'open_time', 'due_time')
+
     getDefaultsFor: (courseId, tasking) ->
       defaults = @exports.getDefaults.call(@, courseId)
-      getFromForTasking(defaults, tasking)
+      default = getFromForTasking(defaults, tasking)
+      _.pick(default, 'open_time', 'due_time')
+
+    areDefaultTaskingTimesSame: (courseId) ->
+      defaults = @exports.getTaskingDefaults.call(@, courseId)
+      firstDefault = _.chain(defaults)
+        .values()
+        .first()
+        .pick('open_time', 'due_time')
+        .value()
+
+      _.every defaults, (default) ->
+        _.isEqual(firstDefault, _.pick(default, 'opens_time', 'due_time'))

     _getTaskings: (taskId) ->
-      @_taskings[taskId].taskings
+      @_taskings[taskId]

     getTaskings: (taskId) ->
       storedTaskings = @exports._getTaskings.call(@, taskId)

       taskings = _.map storedTaskings, maskToTasking

-    getTaskingFor: (taskId, tasking) ->
+    _getTaskingFor: (taskId, tasking) ->
       storedTaskings = @exports._getTaskings.call(@, taskId)
       tasking = getFromForTasking(storedTaskings, tasking)

+    getTaskingFor: (taskId, tasking) ->
+      tasking = @exports._getTaskingFor.call(@, taskId, tasking)
       maskToTasking(tasking)
+
+    getCourseIdForTask: (taskId) ->
+      @_tasksToCourse[taskId]

     areTaskingsValid: (taskId) ->
       storedTaskings = @exports._getTaskings.call(@, taskId)
-      _.every storedTaskings, _.partial(@exports.isTaskingValid.bind(@), taskId)
+      _.every storedTaskings, isTaskingValid

     isTaskingValid: (taskId, tasking) ->
       tasking = @exports.getTaskingFor.call(@, taskId, tasking)
-      TimeHelper.isDateTimeString(tasking.opens_at) and TimeHelper.isDateTimeString(tasking.due_at)
+      isTaskingValid(tasking)

     isTaskingDefaultTime: (taskId, tasking, type = 'open') ->
-      {courseId, taskings} = @_taskings[taskId]
-
-      tasking = getFromForTasking(taskings, tasking)
+      courseId = @exports.getCourseIdForTask.call(@, taskId)
+
+      tasking = @exports.getTaskingFor.call(@, taskId, tasking)
       defaults = @exports.getDefaultsFor.call(@, courseId, tasking)

       tasking["#{type}_time"] is defaults["#{type}_time"]

-
-
+    hasTasking: (taskId, tasking) ->
+    hasAllTaskings: (taskId, tasking) ->
+    hasAnyTasking: (taskId, tasking) ->
+    getCommonDateTime: (taskId) ->
+      taskings = @exports.getTaskings(taskId)
+      firstTasking = _.chain(taskings)
+        .values()
+        .first()
+        .pick('opens_at', 'due_at')
+        .value()
+
+      hasCommon = _.every taskings, (tasking) ->
+        _.isEqual(firstTasking, _.pick(tasking, 'opens_at', 'due_at'))
+
+      return firstTasking if hasCommon
+      return false
+
+    getTaskingDate: (taskId, tasking, type = 'open') ->
+      tasking = @exports._getTaskingFor.call(@, taskId, tasking)
+      tasking["#{type}_date"]
+
+    getTaskingTime: (taskId, tasking, type = 'open') ->
+      tasking = @exports._getTaskingFor.call(@, taskId, tasking)
+      tasking["#{type}_time"]


Feature: String::endsWith(suffix)

Feature: String::endsWith(suffix)

--- src/galao/string.coffee
+++ src/galao/string.coffee
@@ -3,6 +3,9 @@

 String::startsWith ?= (prefix) ->
   @indexOf(prefix) is 0
+
+String::endsWith ?= (suffix) ->
+  @indexOf(suffix) is @length - suffix.length

 # source: http://coffeescript.org/documentation/docs/helpers.html
 String::repeat ?= (n) ->


Use key.full before key.name for bindings

--- src/key-handling.coffee
+++ src/key-handling.coffee
@@ -5,7 +5,7 @@

 module.exports = exports = (game) ->
 	game.on 'key.*', (ch, key) ->
-		action = bindings[key.name ? key.full]
+		action = bindings[key.full ? key.name]

 		if action?
 			parts = action.split('.')


Comments in timezone.

Comments in timezone.

--- src/lib/timezone.coffee
+++ src/lib/timezone.coffee
@@ -163,8 +163,7 @@
     { format, locale, zone } = request
     [ offset, output ] = [ new Date(date), [] ]
     tzdata = TIMEZONES.zones[zone]
-    if zone isnt "UTC"
-      offset = wallclock(offset, tzdata)
+    offset = wallclock(offset, tzdata) if zone isnt "UTC"
     while format.length
       match = /^(.*?)%([-0_^]?)([aAcdDeFHIjklMNpPsrRSTuwXUWVmhbByYcGgCx])(.*)$/.exec(format)
       if match
@@ -219,9 +218,17 @@
       adjusted = adjustment
     new Date(adjusted)

+  # The all purpose exported function.
   exports.tz = tz = (date, splat...) ->
+    # Assert that we've been given a date.
     throw new Error "invalid arguments" if not date?
+
+    # Create a default request.
     request = { zone: "UTC", adjustments: [] }
+
+    # Arguments with a `%` are date formats. Arguments that match a timezone are
+    # timezones, while arguments that look like locales are locales. If nothing
+    # matches, we assume that it is date math.
     for argument in splat
       argument = String(argument)
       if argument.indexOf("%") != -1
@@ -233,13 +240,21 @@
         throw new Error "unknown locale" if not request.locale
       else
         request.adjustments.push argument
+
+    # America No. 1! U-S-A! U-S-A!
     request.locale or= LOCALES.en_US
+
+    # Convert the date argument to seconds since the epoch.
     if date.getTime
       date = date.getTime()
     else if typeof date is "string"
       date = parse date, request
+
+    # Apply date math if any.
     for adjustment in request.adjustments
       date = adjust adjustment, request
+
+    # Apply format if any.
     if request.format
       toString date, request
     else


Only retrieve next/previous leaf ids when needed

--- src/marker-index.coffee
+++ src/marker-index.coffee
@@ -69,7 +69,7 @@
     extentAfterChange = @extent.traversalFrom(spliceOldEnd)
     @extent = spliceNewEnd.traverse(Point.max(Point.zero(), extentAfterChange))

-    if position.isZero() and oldExtent.isZero()
+    if position.isZero() and oldRangeIsEmpty
       precedingIds?.forEach (id) =>
         unless exclusiveIds.has(id)
           @ids.add(id)
@@ -94,13 +94,16 @@
             remainderToDelete = remainderToDelete.traversalFrom(previousExtent)
             childEnd = childStart.traverse(child.extent)
         else
+          if oldRangeIsEmpty
+            previousChildIds = @children[i - 1]?.getRightmostIds() ? precedingIds
+            nextChildIds = @children[i + 1]?.getLeftmostIds() ? followingIds
           splitNodes = child.splice(
             position.traversalFrom(childStart),
             oldExtent,
             newExtent,
             exclusiveIds,
-            @children[i - 1]?.getRightmostIds() ? precedingIds,
-            @children[i + 1]?.getLeftmostIds() ? followingIds
+            previousChildIds,
+            nextChildIds
           )
           @children.splice(i, 1, splitNodes...) if splitNodes
           remainderToDelete = spliceOldEnd.traversalFrom(childEnd)


added conditions

added conditions

--- src/model/statsd.coffee
+++ src/model/statsd.coffee
@@ -38,8 +38,8 @@
     var raw = yield fetchData(path);
     data.push({
       _id : {"channelID": allowedIds[i] },
-      failed: raw.data[0][0],
-      successful: raw.data1[0][0],
+      failed: raw.data[0][0] || 0,
+      successful: raw.data1[0][0] || 0,
       processing:0,
       completed:0,
       completedWErrors:0
@@ -72,10 +72,20 @@
     var path = render_url + ".Statuses.*.count,'1week'))&from=-1weeks&format=json";
     var raw = yield fetchData(path);
     var i = 0;
+    var failed = function () {
+      if (raw.data[0][0])
+        return raw.data[0][0]
+      return 0;    }
+
+    var successful = function () {
+      if (raw.data1[0][0])
+        return raw.data1[0][0]
+      return 0;
+    }
     data.push({
       _id : {"channelID": channelId },
-      failed: raw.data[0][0],
-      successful: raw.data1[0][0],
+      failed: failed,
+      successful: successful,
       processing:0,
       completed:0,
       completedWErrors:0


Addressed issue #2 - Provide more information on last punch when punching against an incomplete

--- src/models/punch.coffee
+++ src/models/punch.coffee
@@ -110,10 +110,10 @@
     datetimes = []
     for i in [0..1]
       if row[headers[MODES[i]]]
-        newDate = moment(row[headers[MODES[i]]], 'MM/DD/YYYY hh:mm:ss a')
+        newDate = moment(row[headers[MODES[i]]], 'M/D/YYYY hh:mm:ss a')
         if not newDate or
            not newDate.isValid() or
-           newDate.format('MM/DD/YYYY') isnt row[headers.today]
+           newDate.format('M/D/YYYY') isnt row[headers.today]
           newDate = moment(row[headers.today] + ' ' +
                               row[headers[MODES[i]]], 'MM/DD/YYYY hh:mm:ss a')
         datetimes.push newDate.tz(user.timetable.timezone.name)
@@ -229,7 +229,9 @@
       if user.punches and
          user.punches.length > 0 and
          user.punches.slice(-1)[0].mode is 'in'
-        return 'You haven\'t punched out yet.'
+        last = user.punches.slice(-1)[0]
+        time = last.times[0].tz(user.timetable.timezone.name)
+        return "You haven't punched out yet. Your last in-punch was at #{time.format('h:mma')} on #{time.format('dddd, MMMM Do')}."
       else if @times
         yesterday = moment().subtract(1, 'days').startOf('day')
         for time in @times
@@ -240,7 +242,9 @@
       if user.punches and
          user.punches.length > 0 and
          user.punches.slice(-1)[0].mode is 'out'
-        return 'You cannot punch out before punching in.'
+        last = user.punches.slice(-1)[0]
+        time = last.times[0].tz(user.timetable.timezone.name)
+        return "You cannot punch out before punching in. Your last out-punch was at #{time.format('h:mma')} on #{time.format('dddd, MMMM Do')}."
     # if mode is 'unpaid' and user is non-salary
     else if @mode is 'unpaid' and not user.salary
       return 'You aren\'t eligible to punch for unpaid time.'
@@ -250,7 +254,9 @@
       if user.punches and
          user.punches.length > 0 and
          user.punches.slice(-1)[0].mode is 'in'
-        return 'You haven\'t punched out yet.'
+        last = user.punches.slice(-1)[0]
+        time = last.times[0].tz(user.timetable.timezone.name)
+        return "You haven't punched out yet. Your last in-punch was at #{time.format('h:mma')} on #{time.format('dddd, MMMM Do')}."
       if elapsed
         # if mode is 'vacation' and user doesn't have enough vacation time
         if @mode is 'vacation' and


Explain dismiss when called programmatically

--- src/notification.coffee
+++ src/notification.coffee
@@ -64,8 +64,8 @@
       and @getType() is other.getType() \
       and @getDetail() is other.getDetail()

-  # Extended: Dismisses the notification, removing it from the UI. This will call all callbacks
-  # added via `onDidDismiss`.
+  # Extended: Dismisses the notification, removing it from the UI. Calling this programmatically
+  # will call all callbacks added via `onDidDismiss`.
   dismiss: ->
     return unless @isDismissable() and not @isDismissed()
     @dismissed = true


fix this up for the new bjson channel stuff

--- src/sct.iced
+++ src/sct.iced
@@ -4,11 +4,6 @@
 constants      = require './constants'
 sc             = require('./status').codes
 mm             = require('./mod').mgr
-
-##=======================================================================
-
-encode = (s) -> s.toString 'base64'
-decode = (b) -> new Buffer b, 'base64'

 ##=======================================================================

@@ -19,9 +14,9 @@

   #-----------------------------------------

-  constructor : ({@version, @id, @generated, @lifetime, @mac, @key, @addr, cfg}) ->
+  constructor : ({@version, @id, @generated, @lifetime, @mac, @addr, cfg}) ->
     if cfg?
-      @key = cfg.key
+      @key = new Buffer cfg.key, 'base64' # keep the key b64-encoded in the config file
       @lifetime = cfg.lifetime unless @lifetime

   #-----------------------------------------
@@ -33,12 +28,12 @@

   #-----------------------------------------

-  @from_client : (s, cfg) ->
+  @from_client : (b, cfg) ->
     err = null
     ret = null
     klass = cfg.klass or SelfCertifiedToken
-    if not (b = decode(s))? or b.length is 0
-      err = "Failed to base64-decode"
+    if not Buffer.isBuffer(b) or b.length is 0
+      err = "Failed to find a non-0 buffer"
     else if not ([e,x] = (utils.katch () -> unpack b))? or e?
       err = "Failed to purepack.unpack"
     else if not Array.isArray x
@@ -96,8 +91,9 @@

   _make_mac : (enc = null) ->
     msg = @_pack true
-    key = decode @key
-    hm = crypto.createHmac 'sha512', key
+    if not @key or @key.length is 0
+      throw new Error "Failing to make, since @key is empty"
+    hm = crypto.createHmac 'sha512', @key
     hm.update msg
     hm.digest()

@@ -122,15 +118,7 @@

   #-----------------------------------------

-  generate_to_client : (cb) ->
-    await @generate defer err, ret
-    if not err? and ret?
-      ret = encode ret
-    cb err, ret
-
-  #-----------------------------------------
-
-  pack_to_client : () -> encode @_pack false
+  pack_to_client : () -> @_pack false

 ##=======================================================================


ConflictMarker specs now pass.

ConflictMarker specs now pass.

:sparkles:

--- spec/conflict-marker-spec.coffee
+++ spec/conflict-marker-spec.coffee
@@ -55,7 +55,7 @@
       expect(lines.hasClass 'conflict-theirs').toBe(true)

     it 'applies the "dirty" class to modified sides', ->
-      editor = editorView.getEditor()
+      editor = editorView.getModel()
       editor.setCursorBufferPosition [14, 0]
       editor.insertText "Make conflict 1 dirty"
       detectDirty()
@@ -69,15 +69,15 @@
       atom.on 'merge-conflicts:resolved', (e) -> event = e
       m.conflicts[2].theirs.resolve()

-      expect(event.file).toBe(editorView.getEditor().getPath())
+      expect(event.file).toBe(editorView.getModel().getPath())
       expect(event.total).toBe(3)
       expect(event.resolved).toBe(1)
       expect(event.source).toBe(m)

     it 'tracks the active conflict side', ->
-      editorView.getEditor().setCursorBufferPosition [11, 0]
+      editorView.getModel().setCursorBufferPosition [11, 0]
       expect(m.active()).toEqual([])
-      editorView.getEditor().setCursorBufferPosition [14, 5]
+      editorView.getModel().setCursorBufferPosition [14, 5]
       expect(m.active()).toEqual([m.conflicts[1].ours])

     describe 'with an active merge conflict', ->
@@ -88,28 +88,28 @@
         active = m.conflicts[1]

       it 'accepts the current side with merge-conflicts:accept-current', ->
-        editorView.trigger 'merge-conflicts:accept-current'
+        atom.commands.dispatch editorView, 'merge-conflicts:accept-current'
         expect(active.resolution).toBe(active.ours)

       it "does nothing if you have cursors in both sides", ->
         editor.addCursorAtBufferPosition [16, 2]
-        editorView.trigger 'merge-conflicts:accept-current'
+        atom.commands.dispatch editorView, 'merge-conflicts:accept-current'
         expect(active.resolution).toBeNull()

       it 'accepts "ours" on merge-conflicts:accept-ours', ->
-        editorView.trigger 'merge-conflicts:accept-ours'
+        atom.commands.dispatch editorView, 'merge-conflicts:accept-current'
         expect(active.resolution).toBe(active.ours)

       it 'accepts "theirs" on merge-conflicts:accept-theirs', ->
-        editorView.trigger 'merge-conflicts:accept-theirs'
+        atom.commands.dispatch editorView, 'merge-conflicts:accept-theirs'
         expect(active.resolution).toBe(active.theirs)

       it 'jumps to the next unresolved on merge-conflicts:next-unresolved', ->
-        editorView.trigger 'merge-conflicts:next-unresolved'
+        atom.commands.dispatch editorView, 'merge-conflicts:next-unresolved'
         expect(cursors()).toEqual([[22, 0]])

       it 'jumps to the previous unresolved on merge-conflicts:previous-unresolved', ->
-        editorView.trigger 'merge-conflicts:previous-unresolved'
+        atom.commands.dispatch editorView, 'merge-conflicts:previous-unresolved'
         expect(cursors()).toEqual([[5, 0]])

       it 'reverts a dirty hunk on merge-conflicts:revert-current', ->
@@ -117,18 +117,18 @@
         detectDirty()
         expect(active.ours.isDirty).toBe(true)

-        editorView.trigger 'merge-conflicts:revert-current'
+        atom.commands.dispatch editorView, 'merge-conflicts:revert-current'
         detectDirty()
         expect(active.ours.isDirty).toBe(false)

       it 'accepts ours-then-theirs on merge-conflicts:ours-then-theirs', ->
-        editorView.trigger 'merge-conflicts:ours-then-theirs'
+        atom.commands.dispatch editorView, 'merge-conflicts:ours-then-theirs'
         expect(active.resolution).toBe(active.ours)
         t = editor.getTextInBufferRange active.resolution.marker.getBufferRange()
         expect(t).toBe("My middle changes\nYour middle changes\n")

       it 'accepts theirs-then-ours on merge-conflicts:theirs-then-ours', ->
-        editorView.trigger 'merge-conflicts:theirs-then-ours'
+        atom.commands.dispatch editorView, 'merge-conflicts:theirs-then-ours'
         expect(active.resolution).toBe(active.theirs)
         t = editor.getTextInBufferRange active.resolution.marker.getBufferRange()
         expect(t).toBe("Your middle changes\nMy middle changes\n")
@@ -140,18 +140,18 @@

       it 'no-ops the resolution commands', ->
         for e in ['accept-current', 'accept-ours', 'accept-theirs', 'revert-current']
-          editorView.trigger "merge-conflicts:#{e}"
+          atom.commands.dispatch editorView, "merge-conflicts:#{e}"
           expect(m.active()).toEqual([])
           for c in m.conflicts
             expect(c.isResolved()).toBe(false)

       it 'jumps to the next unresolved on merge-conflicts:next-unresolved', ->
         expect(m.active()).toEqual([])
-        editorView.trigger 'merge-conflicts:next-unresolved'
+        atom.commands.dispatch editorView, 'merge-conflicts:next-unresolved'
         expect(cursors()).toEqual([[14, 0]])

       it 'jumps to the previous unresolved on merge-conflicts:next-unresolved', ->
-        editorView.trigger 'merge-conflicts:previous-unresolved'
+        atom.commands.dispatch editorView, 'merge-conflicts:previous-unresolved'
         expect(cursors()).toEqual([[5, 0]])

     describe 'when the resolution is complete', ->
@@ -159,11 +159,11 @@
       beforeEach -> c.ours.resolve() for c in m.conflicts

       it 'removes all of the CoveringViews', ->
-        expect(editorView.find('.overlayer .side').length).toBe(0)
-        expect(editorView.find('.overlayer .navigation').length).toBe(0)
+        expect($(editorView).find('.overlayer .side').length).toBe(0)
+        expect($(editorView).find('.overlayer .navigation').length).toBe(0)

       it 'removes the .conflicted class', ->
-        expect(editorView.hasClass 'conflicted').toBe(false)
+        expect($(editorView).hasClass 'conflicted').toBe(false)

       it 'appends a ResolverView to the workspace', ->
         workspaceView = atom.views.getView atom.workspace
@@ -178,7 +178,7 @@
         editorView.getFirstVisibleScreenRow = -> 0
         editorView.getLastVisibleScreenRow = -> 999

-        editor = editorView.getEditor()
+        editor = editorView.getModel()
         state =
           isRebase: true

@@ -188,13 +188,13 @@
         active = m.conflicts[0]

     it 'accepts theirs-then-ours on merge-conflicts:theirs-then-ours', ->
-      editorView.trigger 'merge-conflicts:theirs-then-ours'
+      atom.commands.dispatch editorView, 'merge-conflicts:theirs-then-ours'
       expect(active.resolution).toBe(active.theirs)
       t = editor.getTextInBufferRange active.resolution.marker.getBufferRange()
       expect(t).toBe("These are your changes\nThese are my changes\n")

     it 'accepts ours-then-theirs on merge-conflicts:ours-then-theirs', ->
-      editorView.trigger 'merge-conflicts:ours-then-theirs'
+      atom.commands.dispatch editorView, 'merge-conflicts:ours-then-theirs'
       expect(active.resolution).toBe(active.ours)
       t = editor.getTextInBufferRange active.resolution.marker.getBufferRange()
       expect(t).toBe("These are my changes\nThese are your changes\n")


:art: Cleanup Editor Registry specs

:art: Cleanup Editor Registry specs

--- spec/editor-registry-spec.coffee
+++ spec/editor-registry-spec.coffee
@@ -1,55 +1,42 @@
 describe 'editor-registry', ->
   EditorRegistry = require('../lib/editor-registry')
+  editorRegistry = null
   beforeEach ->
     waitsForPromise ->
       atom.workspace.destroyActivePaneItem()
       atom.workspace.open('test.txt')
-  describe '::constructor', ->
-    it 'expects no arguments', ->
-      editorRegistry = new EditorRegistry
-      editorRegistry.deactivate()
+    editorRegistry?.deactivate()
+    editorRegistry = new EditorRegistry
+
   describe '::create', ->
     it 'cries when invalid TextEditor was provided', ->
-      editorRegistry = new EditorRegistry
       expect ->
         editorRegistry.create()
       .toThrow("Given editor isn't really an editor")
       expect ->
         editorRegistry.create(5)
       .toThrow("Given editor isn't really an editor")
-      editorRegistry.deactivate()
     it "adds TextEditor to it's registry", ->
-      editorRegistry = new EditorRegistry
       editorRegistry.create(atom.workspace.getActiveTextEditor())
       expect(editorRegistry.editorLinters.size).toBe(1)
-      editorRegistry.deactivate()
     it 'automatically clears the TextEditor from registry when destroyed', ->
-      editorRegistry = new EditorRegistry
       editorRegistry.create(atom.workspace.getActiveTextEditor())
       atom.workspace.destroyActivePaneItem()
       expect(editorRegistry.editorLinters.size).toBe(0)
-      editorRegistry.deactivate()
+
   describe '::ofTextEditor', ->
     it 'returns undefined when invalid key is provided', ->
-      editorRegistry = new EditorRegistry
       expect(editorRegistry.ofTextEditor(null)).toBeUndefined()
       expect(editorRegistry.ofTextEditor(1)).toBeUndefined()
       expect(editorRegistry.ofTextEditor(5)).toBeUndefined()
       expect(editorRegistry.ofTextEditor("asd")).toBeUndefined()
-      editorRegistry.deactivate()
     it 'returns editorLinter when valid key is provided', ->
       activeEditor = atom.workspace.getActiveTextEditor()
-      editorRegistry = new EditorRegistry
       editorRegistry.create(activeEditor)
       expect(editorRegistry.ofTextEditor(activeEditor)).toBeDefined()
-      editorRegistry.deactivate()
   describe '::ofActiveTextEditor', ->
     it 'returns undefined if active pane is not a text editor', ->
-      editorRegistry = new EditorRegistry
       expect(editorRegistry.ofActiveTextEditor()).toBeUndefined()
-      editorRegistry.deactivate()
     it 'returns editorLinter when active pane is a text editor', ->
-      editorRegistry = new EditorRegistry
       editorRegistry.create(atom.workspace.getActiveTextEditor())
       expect(editorRegistry.ofActiveTextEditor()).toBeDefined()
-      editorRegistry.deactivate()


bug fix

bug fix

--- src/ritsbus.coffee
+++ src/ritsbus.coffee
@@ -81,6 +81,7 @@
       key = "#{to}_#{allDay[dayIndex]}_time#{busHour}"
       while robot.brain.data[key] is null and busHour <= 24
         busHour++
+        key = "#{to}_#{allDay[dayIndex]}_time#{busHour}"
       if busHour > 24
         replyMessage += "最後のバスです"
         break
@@ -92,7 +93,7 @@
         if (busHour > hour and ///#{bus}///.test(parseBus)) or (parseTime > min and ///#{bus}///.test(parseBus))
           nextBus.push(value)
           busCount++
-        if busCount > SHOW_MAX_BUS
+        if busCount >= SHOW_MAX_BUS
           break
       replyMessage += "#{busHour}時：#{nextBus.join()}\n"
       busHour++


In live-reload, if hostname is empty, set it to localhost.

--- src/client/live-reload.coffee
+++ src/client/live-reload.coffee
@@ -3,6 +3,7 @@

 window.onload = ->
   host = window.location.hostname
+  if host is '' then host = 'localhost'
   port = <?= port || 9485 ?>
   connection = new WebSocket("ws://#{host}:#{port}")
   connection.onmessage = onMessage