add more redis tests

add more redis tests

--- test/redis.coffee
+++ test/redis.coffee
@@ -17,6 +17,50 @@
         this.config.redis.quit (err) ->
             throw err if err?
             done()
+
+    'searching an index for existing search terms': (test) ->
+        config = this.config
+
+        index =
+            0: ['it', 'is', 'what', 'it', 'is']
+            1: ['what', 'is', 'it']
+            2: ['it', 'is', 'a', 'banana']
+
+        invertedIndex = searchlight.index.invert index
+
+        searchlight.redis.set config, invertedIndex, (err) ->
+            throw err if err?
+
+            searchlight.redis.search config, ['it', 'is'], (err, results) ->
+                throw err if err?
+                test.deepEqual results, ['0', '1', '2']
+
+                searchlight.redis.search config, ['what'], (err, results) ->
+                    throw err if err?
+                    test.deepEqual results, ['0', '1']
+
+                    searchlight.redis.search config, ['a', 'banana'], (err, results) ->
+                        throw err if err?
+                        test.deepEqual results, ['2']
+                        test.done()
+
+    'searching an index for not existing search terms': (test) ->
+        config = this.config
+
+        index =
+            0: ['it', 'is', 'what', 'it', 'is']
+            1: ['what', 'is', 'it']
+            2: ['it', 'is', 'a', 'banana']
+
+        invertedIndex = searchlight.index.invert index
+
+        searchlight.redis.set config, invertedIndex, (err) ->
+            throw err if err?
+
+            searchlight.redis.search config, ['what', 'banana'], (err, results) ->
+                throw err if err?
+                test.deepEqual results, []
+                test.done()

     'the stored index is identical to the last set index': (test) ->
         config = this.config
@@ -155,3 +199,87 @@
                     throw err if err?
                     test.deepEqual invertedIndex2, storedIndex
                     test.done()
+
+    'removing no documents leaves the index unchanged': (test) ->
+        config = this.config
+
+        index =
+            0: ['it', 'is', 'what', 'it', 'is']
+            1: ['what', 'is', 'it']
+            2: ['it', 'is', 'a', 'banana']
+
+        invertedIndex = searchlight.index.invert index
+
+        searchlight.redis.set config, invertedIndex, (err) ->
+            throw err if err?
+
+            searchlight.redis.remove config, [], (err) ->
+                throw err if err?
+
+                searchlight.redis.get config, (err, storedIndex) ->
+                    throw err if err?
+                    test.deepEqual invertedIndex, storedIndex
+                    test.done()
+
+    'removing not existing documents leaves the index unchanged': (test) ->
+        config = this.config
+
+        index =
+            0: ['it', 'is', 'what', 'it', 'is']
+            1: ['what', 'is', 'it']
+            2: ['it', 'is', 'a', 'banana']
+
+        invertedIndex = searchlight.index.invert index
+
+        searchlight.redis.set config, invertedIndex, (err) ->
+            throw err if err?
+
+            searchlight.redis.remove config, [3, 4, 5], (err) ->
+                throw err if err?
+
+                searchlight.redis.get config, (err, storedIndex) ->
+                    throw err if err?
+                    test.deepEqual invertedIndex, storedIndex
+                    test.done()
+
+    'setting an empty index empties the index': (test) ->
+        config = this.config
+
+        index =
+            0: ['it', 'is', 'what', 'it', 'is']
+            1: ['what', 'is', 'it']
+            2: ['it', 'is', 'a', 'banana']
+
+        invertedIndex = searchlight.index.invert index
+
+        searchlight.redis.set config, invertedIndex, (err) ->
+            throw err if err?
+
+            searchlight.redis.set config, {}, (err) ->
+                throw err if err?
+
+                searchlight.redis.get config, (err, storedIndex) ->
+                    throw err if err?
+                    test.deepEqual storedIndex, {}
+                    test.done()
+
+    'adding an empty index leaves the index unchanged': (test) ->
+        config = this.config
+
+        index =
+            0: ['it', 'is', 'what', 'it', 'is']
+            1: ['what', 'is', 'it']
+            2: ['it', 'is', 'a', 'banana']
+
+        invertedIndex = searchlight.index.invert index
+
+        searchlight.redis.set config, invertedIndex, (err) ->
+            throw err if err?
+
+            searchlight.redis.add config, {}, (err) ->
+                throw err if err?
+
+                searchlight.redis.get config, (err, storedIndex) ->
+                    throw err if err?
+                    test.deepEqual invertedIndex, storedIndex
+                    test.done()


Switching again to Ctrl+Enter

Switching again to Ctrl+Enter

--- content_scripts/vomnibar.coffee
+++ content_scripts/vomnibar.coffee
@@ -97,12 +97,10 @@
         (event.keyCode == keyCodes.tab && !event.shiftKey) ||
         (event.ctrlKey && (key == "j" || key == "n")))
       return "down"
+    else if (event.ctrlKey && event.keyCode == keyCodes.enter)
+      return "complete"
     else if (event.keyCode == keyCodes.enter)
       return "enter"
-    else if (key == "right")
-      return "complete"
-    else if (key == "left")
-      return "current"

   onKeydown: (event) ->
     action = @actionFromKeyEvent(event)
@@ -142,8 +140,6 @@
         @input.value = window.location
       else
         @input.value = @completions[@selection].url
-    else if (action == "current")
-      @input.value = window.location

     # It seems like we have to manually suppress the event here and still return true.
     event.stopPropagation()


remove duplicates

remove duplicates

--- test/model.coffee
+++ test/model.coffee
@@ -152,12 +152,8 @@
     inst.firstName.should.equal 'Larry'

     inst.on 'change', (data) ->
-      data.firstName.should.equal 'Ellen'
-      inst.firstName.should.equal 'Ellen'
       should.not.exist inst.age
-      should.not.exist data.age
       done()

-    inst.firstName = 'Ellen'
     inst.age = '23'
     inst.save()


decreased test time by 25% with Monitor

decreased test time by 25% with Monitor

--- test/setup.coffee
+++ test/setup.coffee
@@ -4,21 +4,31 @@

 child = null

-console.info 'sup'
+
+Monitor = (cb) ->
+  called = 0
+  register: -> ->
+    called++
+    if called == 2 then do cb
+

 before 'Make browserify bundle', (done) ->
+
+  monitor = new Monitor(done)
+
+  console.log 'Building Application Bundle...\n'
   @timeout 5000
   router = require './app/routers'
   builder = Ice.build router
   dest = fs.createWriteStream "#{__dirname}/app/bundle.js"
-  dest.on 'finish', done
+  dest.on 'finish', monitor.register()
   builder.pipe dest

-before 'Spin up test server', (done) ->
+  console.log 'Starting Test Server...\n'
   child = spawn 'node', ['test/app'], {stdio: ['ipc']}
   child.stderr.pipe(process.stderr)
   child.on 'message', (m) ->
-    if m is 'listening' then done()
+    if m is 'listening' then do monitor.register()

 after 'Remove bundle', (done) ->
   fs.unlink("#{__dirname}/app/bundle.js", done)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/state.coffee
+++ test/state.coffee
@@ -11,6 +11,8 @@
 test "State should transition to a different state after current state finishes", ->
   animation = GameObject
     initialState: 'run'
+
+  animation.include(StateMachine)

   animation.update()
   animation.update()


removing extraneous async button

removing extraneous async button

--- src/components/async-button.cjsx
+++ src/components/async-button.cjsx
@@ -1,74 +0,0 @@
-React = require 'react'
-BS = require 'react-bootstrap'
-_ = require 'underscore'
-RefreshButton = require './refresh-button'
-
-module.exports = React.createClass
-  displayName: 'AsyncButton'
-
-  getInitialState: ->
-    isTimedout: false
-
-  componentDidUpdate: ->
-    {isWaiting, timeout} = @props
-    {isTimedout} = @state
-
-    if isWaiting and not isTimedout
-      _.delay =>
-        @checkForTimeout()
-      , timeout
-
-  checkForTimeout: ->
-    {isWaiting} = @props
-    @setState(isTimedout: true) if isWaiting
-
-  propTypes:
-    isWaiting: React.PropTypes.bool.isRequired
-    isDone: React.PropTypes.bool
-    isFailed: React.PropTypes.bool
-    waitingText: React.PropTypes.node # TODO: This should be a Component or array
-    failedState: React.PropTypes.func
-    failedProps: React.PropTypes.object
-    doneText: React.PropTypes.node
-    timeout: React.PropTypes.number
-
-  getDefaultProps: ->
-    isDone: false
-    isFailed: false
-    waitingText: 'Loading…'
-    failedState: RefreshButton
-    failedProps:
-      beforeText: 'There was a problem.  '
-    doneText: ''
-    timeout: 30000
-
-  render: ->
-    {className, disabled} = @props
-    {isWaiting, isDone, isFailed} = @props
-    {children, waitingText, failedState, failedProps, doneText} = @props
-    {isTimedout} = @state
-
-    buttonTypeClass = 'async-button'
-
-    if isFailed or isTimedout
-      stateClass = 'is-failed'
-      return <failedState {...failedProps}/>
-    else if isWaiting
-      stateClass = 'is-waiting'
-      text = waitingText
-      disabled = true
-      spinner = <i className='fa fa-spinner fa-spin'/>
-    else if isDone
-      stateClass = 'is-done'
-      text = doneText
-    else
-      stateClass = null
-      text = children
-
-    <BS.Button {...@props}
-      className={[buttonTypeClass, stateClass, className]}
-      disabled={disabled}
-      >
-        {spinner}
-        {text}
-    </BS.Button>


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/state.coffee
+++ test/state.coffee
@@ -23,6 +23,8 @@
 test "State should repeat default state", ->
   animation = GameObject
     defaultState: 'stand'
+
+  animation.include(StateMachine)

   equals animation.currentState(), animation.defaultState()


Updated code style in BorderDropHint

Updated code style in BorderDropHint

--- src/drag_and_drop_handler.coffee
+++ src/drag_and_drop_handler.coffee
@@ -480,10 +480,10 @@
         @$hint = $('')
         $div.append(@$hint)

-        @$hint.css({
-            width: width,
+        @$hint.css(
+            width: width
             height: $div.height() - 4
-        })
+        )

     remove: ->
         @$hint.remove()


Added forced supersampling for nonretina

--- source/Main.coffee
+++ source/Main.coffee
@@ -122,7 +122,7 @@
 root.kosmosResize = ->
 	if not enableRetina then console.log("Note: Device pixel scaling (retina) is disabled.")
 	devicePixelRatio = if enableRetina then window.devicePixelRatio || 1 else 1
-	#devicePixelRatio = 2 # forced supersampling on nonretina
+	if devicePixelRatio < 2 then devicePixelRatio = 2 # forced supersampling on nonretina
 	canvas.width  = canvas.clientWidth * devicePixelRatio
 	canvas.height = canvas.clientHeight * devicePixelRatio
 	console.log("Main framebuffer resolution #{canvas.width} x #{canvas.height}"


Install plist-converter Atom package

Install plist-converter Atom package

This allows Atom to read binary plist files.

--- atom/packages.cson
+++ atom/packages.cson
@@ -42,6 +42,7 @@
   "linter-ui-default"
   "monokai"
   "package-sync"
+  "plist-converter"
   "resize-indent"
   "sort-lines"
   "ssh-config"


Uninstall swap-selection package for atom

--- atom/packages.cson
+++ atom/packages.cson
@@ -46,7 +46,6 @@
   "sort-lines"
   "ssh-config"
   "svg-preview"
-  "swap-selection"
   "tabs-to-spaces"
   "teletype"
 ]


more TYPO3 snippets

more TYPO3 snippets

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -75,3 +75,15 @@
   'TYPO3 Repository':
     'prefix': 't3rep'
     'body': '\\\\TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Repository'
+
+  'TYPO3 ConfigurationManagerInterface':
+    'prefix': 't3confmgrint'
+    'body': '\\\\TYPO3\\\\CMS\\\\Extbase\\\\Configuration\\\\ConfigurationManagerInterface'
+
+  'TYPO3 QueryResult':
+   'prefix': 't3qres'
+   'body': '\\\\TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Generic\\\\QueryResult'
+
+  'TYPO3 TypoScriptService':
+   'prefix': 't3ts'
+   'body': '\\\\TYPO3\\\\CMS\\\\Extbase\\\\Service\\\\TypoScriptService'


Tests:IDE:Workspace: deleteWorkspace test fixed

--- tests/src/helpers/helpers.coffee
+++ tests/src/helpers/helpers.coffee
@@ -298,6 +298,7 @@
         browser
           .waitForElementPresent   'a[href="/IDE/' + vmName + '/' + workspaceName + '"]', 40000 # Assertion
           .assert.urlContains      workspaceName # Assertion
+          .waitForElementVisible   '.vm-info', 20000
           .assert.containsText     '.vm-info', '~/Workspaces/' + workspaceName # Assertion

     return workspaceName


Add ensure default fields in all variants. Resolve references for update product as well.

--- src/coffee/product-import.coffee
+++ src/coffee/product-import.coffee
@@ -40,6 +40,7 @@
       # Fetch products from product projections end point by list of skus.
       @client.productProjections
       .where(predicate)
+      .staged(true)
       .fetch()
       .then (results) =>
         debug 'Fetched products: %j', results
@@ -87,9 +88,7 @@
     posts = _.map productsToProcess, (prodToProcess) =>
       existingProduct = @_isExistingEntry(prodToProcess, existingProducts)
       if existingProduct?
-        if not prodToProcess.slug
-          debug 'slug missing in product to process, assigning same as existing product: %s', existingProduct.slug
-          prodToProcess.slug = existingProduct.slug # to prevent removing slug from existing product.
+        @_prepareUpdateProduct(prodToProcess,existingProduct).then (updatedProduct) => prodToProcess = updatedProduct
         synced = @sync.buildActions(prodToProcess, existingProduct)
         if synced.shouldUpdate()
           @client.products.byId(synced.getUpdateId()).update(synced.getUpdatePayload())
@@ -100,6 +99,44 @@

     debug 'About to send %s requests', _.size(posts)
     Promise.all(posts)
+
+  _ensureVariantDefaults: (variant) ->
+    variantDefaults =
+      attributes: []
+      prices: []
+      images: []
+
+    _.defaults(variant, variantDefaults)
+
+  _ensureDefaults: (product) =>
+    debug 'ensuring default fields in variants.'
+    if product.masterVariant
+      product.masterVariant = @_ensureVariantDefaults(product.masterVariant)
+    if product.variants
+      product.variants = _.map product.variants, (variant) => @_ensureVariantDefaults(variant)
+    return product
+
+  _prepareUpdateProduct: (productToProcess, existingProduct) ->
+    if not productToProcess.slug
+      debug 'slug missing in product to process, assigning same as existing product: %s', existingProduct.slug
+      productToProcess.slug = existingProduct.slug # to prevent removing slug from existing product.
+
+    productToProcess = @_ensureDefaults(productToProcess)
+
+    Promise.all [
+      @_resolveProductCategories(productToProcess.categories)
+      @_resolveReference(@client.taxCategories, 'taxCategory', productToProcess.taxCategory, "name=\"#{productToProcess.taxCategory?.id}\"")
+    ]
+    .spread (prodCatsIds, taxCatId) =>
+      if taxCatId
+        productToProcess.taxCategory =
+          id: taxCatId
+          typeId: 'tax-category'
+      if prodCatsIds
+        productToProcess.categories = _.map prodCatsIds, (catId) ->
+          id: catId
+          typeId: 'category'
+      Promise.resolve productToProcess

   _prepareNewProduct: (product) ->
     Promise.all [


use locking for initial DB setup

use locking for initial DB setup

--- src/mailButler.appsscript.coffee
+++ src/mailButler.appsscript.coffee
@@ -96,9 +96,19 @@
       Logger.log "Installing trigger"
       ScriptApp.newTrigger("process").timeBased().everyMinutes(MailButler.FREQUENCY_MINUTES).create()

-    Logger.log 'Setting version'
-    @DB.setCurrentVersion @getEmail(), @VERSION
-    @DB.setLastUsed @getEmail()
+
+
+    # Get a lock for the current user
+    lock = LockService.getPrivateLock()
+    if lock.tryLock 10000
+      try
+        Logger.log 'Setting version'
+        @DB.setCurrentVersion @getEmail(), @VERSION
+        Logger.log 'Setting last used date'
+        @DB.setLastUsed @getEmail()
+      finally
+        # Always release lock
+        lock.releaseLock()
     return

   @getEmail: ->


BugFix - Issue#60

BugFix - Issue#60

--- src/model.coffee
+++ src/model.coffee
@@ -809,7 +809,7 @@

       super

-    _cloneEmpty: -> new Indent @prefix
+    _cloneEmpty: -> new Indent @prefix, @classes
     _serialize_header: -> "<indent prefix=\"#{
       @prefix
     }\" classes=\"#{


writeOptions to datafluxcontroller added

--- src/controller.datafluxes.coffee
+++ src/controller.datafluxes.coffee
@@ -36,3 +36,14 @@
         @log.error "The subfolder #{df.srcFolder} is already a dataflux!"
         return false
     return true
+
+  writeOptions: ->
+    datafluxesConf = []
+    for df in @objects
+      dfOptions =
+        srcFolder: df.srcFolder
+        dataFluxFolder: df.dataFluxFolder
+        options: df.options
+      datafluxesConf.push dfOptions
+    @conf.datafluxes = datafluxesConf
+    @conf.write "datafluxes"


1.2.0 test version

1.2.0 test version

--- src/app/components/directive.coffee
+++ src/app/components/directive.coffee
@@ -20,6 +20,7 @@
         recalcHeight : () ->
           unless parentObject.length then throw new Error("no parent found using #{ngFillHeightOption.parentSelector}")
           unless currObject.length   then throw new Error("no parent found using #{ngFillHeightOption.selector}")
+          return  # test version

           recurrFunc = (increment) ->
             currObject.height(currObject.height() + increment)


using proper stack space for local variables

--- src/nodes.coffee
+++ src/nodes.coffee
@@ -80,8 +80,8 @@
       @root = @parent.root
       @extraAsm = @root.extraAsm
       @nextLabel = @root.nextLabel
-    @localVariableCount = 1 # return address is at index 0
-    @nextLocalOffset = -> -@localVariableCount++
+    @localVariableCount = 0
+    @nextLocalOffset = -> -++@localVariableCount
   find: (name) ->
     @names[name] or @parent?.find name
   createVariable: (name, type, localOffset) ->


Fix some bugs

Fix some bugs

--- src/6.timer.coffee
+++ src/6.timer.coffee
@@ -18,8 +18,10 @@
         @_internalTimer = setInterval(=>
             Date.Timer._onCheck.fire()
         , precision)
+        undefined
     @disable: ->
         clearInterval(@_internalTimer)
+        undefined
     @getPrecision: ->
         Date.Timer._precision
     @_onCheck: eventField()
@@ -51,7 +53,9 @@
         @_running = false
         @
     getRunning: -> @_running
-    resetCounter: -> @_counter = 0
+    resetCounter: ->
+        @_counter = 0
+        @
     getCounter: -> @_counter
 class Date.IntervalTimer extends Date.Timer
     constructor: (options) ->
@@ -99,7 +103,7 @@
                 [arguments[1], arguments[0]]
         clonedOptions = if options? then Object.clone(options) else {}
         clonedOptions.interval ?= 100
-        super(options)
+        super(clonedOptions)
         @_fun = fun
         @onChange = eventField()
         @onUpdate = eventField()


mgiration....

mgiration....

--- salvus/storage.coffee
+++ salvus/storage.coffee
@@ -4223,6 +4223,8 @@
         limit : 1           # no more than this many simultaneous tasks
         start : 0
         end   : 10
+        reverse : false
+        dryrun : false
         cb    : undefined
     dbg = (m) -> winston.debug("prepare_bup(): #{m}")
     dbg()
@@ -4232,6 +4234,8 @@
     servers_by_dc = {}
     servers_hosting_project = {}
     projects_on_server = {}
+
+    save_time = misc.to_iso(new Date(1396569559669))

     async.series([
         (cb) ->
@@ -4248,6 +4252,7 @@
                         cb(err)
                     else
                         for x in servers
+                            x.host = cassandra.inet_to_str(x.host)
                             servers_by_id[x.server_id] = x
                             v = servers_by_dc[x.dc]
                             if not v?
@@ -4265,10 +4270,11 @@
                     else
                         for project_id in data.toString().split('\n')
                             projects_on_server[server_id].push(project_id)
-                            w = servers_hosting_project[project_id]
-                            if not w?
-                                w = servers_hosting_project[project_id] = []
-                            w.push(server_id)
+                            if not servers_hosting_project[project_id]?
+                                servers_hosting_project[project_id] = []
+                            if server_id.length != 36
+                                weird.error
+                            servers_hosting_project[project_id].push(server_id)
                         cb()
             v = ['0663ed3c-e943-4d46-8c5e-b5e7bd5a61cc', '0985aa3e-c5e9-400e-8faa-32e7d5399dab', '2d7f86ce-14a3-41cc-955c-af5211f4a85e', '3056288c-a78d-4f64-af21-633214e845ad', '306ad75d-ffe0-43a4-911d-60b8cd133bc8', '44468f71-5e2d-4685-8d60-95c9d703bea0', '4e4a8d4e-4efa-4435-8380-54795ef6eb8f', '630910c8-d0ef-421f-894e-6f58a954f215', '767693df-fb0d-41a0-bb49-a614d7fbf20d', '795a90e2-92e0-4028-afb0-0c3316c48192', '801019d9-008a-45d4-a7ce-b72f6e99a74d', '806edbba-a66b-4710-9c65-47dd70503fc9', '8f5247e5-d449-4356-9ca7-1d971c79c7df', '94d4ebc1-d5fc-4790-affe-ab4738ca0384', '9e43d924-684d-479b-b601-994e17b7fd86', 'a7cc2a28-5e70-44d9-bbc7-1c5afea1fc9e', 'b9cd6c52-059d-44e1-ace0-be0a26568713', 'bc74ea05-4878-4c5c-90e2-facb70cfe338', 'c2ba4efc-8b4d-4447-8b0b-6a512e1cac97', 'd0bfc232-beeb-4062-9ad5-439c794594f3', 'd47df269-f3a3-47ed-854b-17d6d31fa4fd', 'dad2a46d-2a57-401a-bfe2-ac4fc7d50ec1', 'e06fb88a-1683-41d6-97d8-92e1f3fb5196', 'e676bb5a-c46c-4b72-8d87-0ef62e4a5c88', 'e682408b-c165-4635-abef-d0c5809fee26', 'eec826ad-f395-4a1d-bfb1-20f5a19d4bb0', 'f71dab5b-f40c-48db-a3d2-eefe6ec55f01']
             async.map(v, f, cb)
@@ -4280,8 +4286,9 @@
             hashrings = {}
             for dc, servers of servers_by_dc
                 v = {}
-                for server_id in servers
-                    v[server_id] = {vnodes:128}
+                for server in servers
+                    v[server.server_id] = {vnodes:128}
+                console.log('making hashring from', v)
                 hashrings[dc] = new HashRing(v)

             data_centers = misc.keys(servers_by_dc)
@@ -4298,14 +4305,19 @@

                 target = []
                 servers.sort()
-                for sever_id in servers
+
+                for server_id in servers
                     dc = servers_by_id[server_id].dc
                     if not have[dc]
                         target.push(server_id)
                         have[dc] = true
                 for dc, val of have
                     if not val
-                        target.push(hashrings[dc].range(project_id, 1))
+                        server_id = hashrings[dc].range(project_id, 1, true)[0]
+                        #dbg("#{project_id}: adding new server_id = #{server_id}")
+                        if server_id.length != 36
+                            hash.ring.broken
+                        target.push(server_id)

                 if target.length != data_centers.length
                     cb("bug -- found target #{misc.to_json(target)} which has wrong length -- #{project_id}")
@@ -4321,6 +4333,8 @@
                     cb("bug -- we don't have any data--  #{project_id}")
                 for server_id in target
                     if server_id not in have_data
+                        if server_id.length != 36
+                             total.bug
                         work.push({project_id:project_id, action:'copy_and_restore', src:source, dest:server_id})

                 # 2. delete data from where we don't want it
@@ -4333,40 +4347,89 @@
                     if server_id in have_data
                         work.push({project_id:project_id, action:'restore', server_id:server_id})

-                if work.length >= opts.end
+                if not opts.reverse and opts.end != -1 and work.length >= opts.end
                     break
+
+            # sort the work by project id
+            work.sort (a,b) ->
+                if a.project_id < b.project_id
+                    return -1
+                else if a.project_id > b.project_id
+                    return 1
+                else
+                    return 0
+
             cb()

         (cb) ->
-            dbg("do tasks #{opts.start} - #{opts.end} on the work schedule doing #{opts.limit} in parallel")
-
-            exec = (server_id, cmd, cb) ->
-                host = servers_by_id[server_id].host
+            if opts.end == -1
+                opts.end = work.length
+            dbg("do tasks #{opts.start} - #{opts.end-1} on the work schedule doing #{opts.limit} in parallel")
+            #dbg("work to do: #{misc.to_json(work.slice(opts.start, opts.end))}")
+
+            exec_on = (server_id, cmd, cb) ->
+                host = servers_by_id[server_id]?['host']
+                if not host?
+                    dbg("invalid server #{server_id}")
+                    cb("target server #{server_id} not found")
+                    return
+                dbg("on #{host}: #{cmd}")
                 misc_node.execute_code
                     command     : "ssh"
                     args        : ["-o StrictHostKeyChecking=no", "root@#{host}", cmd]
-                    timeout     : 30*60  # 30 minutes
-                    err_on_exit : true
-                    cb          : cb
+                    timeout     : 60*60
+                    err_on_exit : false
+                    verbose     : false
+                    cb          : (err, output) ->
+                        if err
+                            dbg("#{host}: #{cmd} -- FAIL #{err}")
+                            cb(err)
+                        else
+                            v = output.stderr.split('\n')
+                            for x in v
+                               x = x.trim()
+                               if x.length > 1 and x.indexOf('chattr:')==-1 and x.indexOf('WARNING') == -1 and x.indexOf('ECDSA') == -1
+                                   dbg("{host}: #{cmd} -- ERROR due to '#{x}'")
+                                   cb(output.stderr); return
+                            cb()

             task_delete = (project_id, server_id, cb) ->
                 if project_id.length != 36
                     cb("invalid uuid -- #{project_id}")
                 else
-                    exec(server_id, "rm -rf /bup/bups/#{project_id}", cb)
+                    exec_on(server_id, "rm -rf /bup/bups/#{project_id}", cb)

             task_copy = (project_id, src, dest, cb) ->
+                host = servers_by_id[dest]?.host
+                if not host?
+                    cb("target server #{dest} not found")
                 if project_id.length != 36
                     cb("invalid uuid -- #{project_id}")
                 else
-                    exec(src, "rsync -axvH /bup/bups/#{project_id}/ #{dest}:/bup/bups/#{project_id}/", cb)
+                    exec_on(src, "rsync -axH /bup/bups/#{project_id}/ #{host}:/bup/bups/#{project_id}/", cb)

             task_restore = (project_id, server_id, cb) ->
                 uid = project_uid(project_id)
-                exec(server_id, "bup restore --outdir=/bup/projects/#{project_id} master/latest && chown -R #{uid}:#{uid} /bup/projects/#{project_id}", cb)
-
-
+                if project_id.length != 36
+                    cb("invalid uuid -- #{project_id}")
+                else
+                    exec_on server_id, "rm -rf /bup/projects/#{project_id} && BUP_DIR=/bup/bups/#{project_id} bup restore --outdir=/bup/projects/#{project_id} master/latest/ ; chown -R #{uid}:#{uid} /bup/projects/#{project_id}", (err) ->
+                       if err
+                           cb(err)
+                       else
+                           q = "UPDATE projects set bup_last_save[?]=? where project_id=?"
+                           database.cql(q, [server_id, save_time, project_id], cb)
+
+            i = opts.start
+            goal = opts.end - 1
+
+
             do_task = (task, cb) ->
+                dbg("******** #{i}/#{goal} ******* ")
+                i += 1
+                dbg("doing task #{misc.to_json(task)}")
+                if opts.dryrun
+                    cb(); return
                 switch task.action
                     when 'delete'
                         task_delete(task.project_id, task.server_id, cb)
@@ -4375,12 +4438,19 @@
                             if err
                                 cb(err)
                             else
-                                task_restore(task.project_id, task.server_id, cb)
+                                task_restore(task.project_id, task.dest, cb)
                     when 'restore'
                         task_restore(task.project_id, task.server_id, cb)
                     else
                         cb("unknown action #{task.action}")

+            if opts.reverse
+                work.reverse()
+            if opts.end != -1
+                work = work.slice(opts.start, opts.end)
+            else
+                work = work.slice(opts.start)
+            async.mapLimit(work, opts.limit, do_task, cb)

     ], (err) -> opts.cb?(err))


Fix need_update function

Fix need_update function

--- admin/static/coffee/app.coffee
+++ admin/static/coffee/app.coffee
@@ -18,22 +18,21 @@

 # Check if new_data is included in old_data and if the values are equals. (Note: We don't check if the objects are equals)
 need_update_objects = (new_data, old_data) ->
-    need_update = false
     for key of new_data
         if key of old_data is false
-            need_update = true
-            break
+            return true

-    if need_update is false
-        for key of new_data
-            if typeof new_data[key] is object and typeof old_data[key] is object
-                need_update = compare_object(new_data[key], old_data[key])
-                if need_update is true
-                    break
-                else if new_data[key] isnt old_data[key]
-                    need_update = true
-                    break
-    return need_update
+    for key of new_data
+        if typeof new_data[key] is object and typeof old_data[key] is object
+            need_update = compare_object(new_data[key], old_data[key])
+            if need_update is true
+                return need_update
+        else if typeof new_data[key] isnt typeof old_data[key]
+            return true
+        else if new_data[key] isnt old_data[key]
+            return true
+
+    return false


Fix for #1326 by value is uncached

Fix for #1326 by value is uncached

--- nodes.coffee
+++ nodes.coffee
@@ -640,26 +640,29 @@
   # needed to iterate over the values in the range. Used by comprehensions.
   compileNode: (o) ->
     @compileVariables o
-    return    @compileArray(o)  unless o.index
-    return    @compileSimple(o) if @fromNum and @toNum
-    idx      = del o, 'index'
-    step     = del o, 'step'
-    vars     = "#{idx} = #{@from}" + if @to isnt @toVar then ", #{@to}" else ''
-    cond     = "#{@fromVar} <= #{@toVar}"
-    compare  = "#{cond} ? #{idx} <#{@equals} #{@toVar} : #{idx} >#{@equals} #{@toVar}"
-    incr     = if step then "#{idx} += #{step.compile(o)}" else "#{cond} ? #{idx}++ : #{idx}--"
-    "#{vars}; #{compare}; #{incr}"
+    return        @compileArray(o)  unless o.index
+    return        @compileSimple(o) if @fromNum and @toNum
+    idx           = del o, 'index'
+    step          = del o, 'step'
+    byvar         = o.scope.freeVariable "by" if step
+    varPart       = "#{idx} = #{@from}" + ( if @to isnt @toVar then ", #{@to}" else '' ) + if step then ", #{byvar} = #{step.compile(o)}" else ''
+    cond          = "#{@fromVar} <= #{@toVar}"
+    condPart      = "#{cond} ? #{idx} <#{@equals} #{@toVar} : #{idx} >#{@equals} #{@toVar}"
+    stepPart      = if step then "#{idx} += #{byvar}" else "#{cond} ? #{idx}++ : #{idx}--"
+    "#{varPart}; #{condPart}; #{stepPart}"

   # Compile a simple range comprehension, with integers.
   compileSimple: (o) ->
     [from, to] = [+@fromNum, +@toNum]
     idx        = del o, 'index'
     step       = del o, 'step'
-    step       and= "#{idx} += #{step.compile(o)}"
-    if from <= to
-      "#{idx} = #{from}; #{idx} <#{@equals} #{to}; #{step or "#{idx}++"}"
-    else
-      "#{idx} = #{from}; #{idx} >#{@equals} #{to}; #{step or "#{idx}--"}"
+    byvar      = o.scope.freeVariable "by" if step
+    varPart    = "#{idx} = #{from}"
+    varPart   += ", #{byvar} = #{step.compile(o)}" if step
+    condPart   = if from <= to then "#{idx} <#{@equals} #{to}" else "#{idx} >#{@equals} #{to}"
+    stepPart   = "#{idx} += #{byvar}" if step
+    stepPart   = ( if from <= to then "#{idx}++" else "#{idx}--" ) if not step
+    "#{varPart}; #{condPart}; #{stepPart}"

   # When used as a value, expand the range into the equivalent array.
   compileArray: (o) ->
@@ -1503,6 +1506,8 @@
     scope.find(index, immediate: yes) if index
     rvar          = scope.freeVariable 'results' if @returns
     ivar          = (if @range then name else index) or scope.freeVariable 'i'
+    # the `_by` variable is created twice in `Range`s if we don't prevent it from being declared here
+    byvar         = scope.freeVariable 'by' if @step and not @range
     name          = ivar if @pattern
     varPart       = ''
     guardPart     = ''
@@ -1519,8 +1524,9 @@
         namePart = "#{name} = #{svar}[#{ivar}]"
       unless @object
         lvar        = scope.freeVariable 'len'
-        stepPart    = if @step then "#{ivar} += #{ @step.compile(o, LEVEL_OP) }" else "#{ivar}++"
-        forPart     = "#{ivar} = 0, #{lvar} = #{svar}.length; #{ivar} < #{lvar}; #{stepPart}"
+        forVarPart  = "#{ivar} = 0, #{lvar} = #{svar}.length" + if @step then ", #{byvar} = #{@step.compile(o, LEVEL_OP)}" else ''
+        stepPart    = if @step then "#{ivar} += #{byvar}" else "#{ivar}++"
+        forPart     = "#{forVarPart}; #{ivar} < #{lvar}; #{stepPart}"
     if @returns
       resultPart    = "#{@tab}#{rvar} = [];\n"
       returnResult  = "\n#{@tab}return #{rvar};"


atom: update - alias for vim-mode-plus

atom: update - alias for vim-mode-plus

--- .atom/keymap.cson
+++ .atom/keymap.cson
@@ -41,6 +41,6 @@
   'cmd-|': 'pane:split-right'
   'cmd-\\': 'pane:split-down'

-'atom-text-editor:not(.mini).vim-mode:not(.insert-mode):not(.jumpy-jump-mode)':
-  '-': 'file-explorer:toggle-current-directory'
+'atom-text-editor:not(.mini).vim-mode-plus:not(.insert-mode):not(.jumpy-jump-mode)':
+  '-': 'advanced-open-file:toggle'
   'f': 'jumpy:toggle'


Match longer request names first

Match longer request names first

This stops ".hpfa" being highlighted like an invocation of ".hpf a".

--- grammars/roff.cson
+++ grammars/roff.cson
@@ -150,12 +150,12 @@
 			# GNU extensions
 			name: "meta.function.request.$2.gnu.roff"
 			begin: "(?x) ^([.'])[ \t]*
-				(aln|als|asciify|backtrace|blm|box|boxa|brp|cflags|chop|close|composite|color
-				|cp|device|devicem|do|ecs|ecr|evc|fam|fchar|fcolor|fschar|fspecial|ftr|fzoom
-				|gcolor|hcode|hla|hlm|hpf|hpfa|hpfcode|hym|hys|itc|kern|length|linetabs|lsm
-				|mso|nop|nroff|open|opena|output|pev|pnr|psbb|pso|ptr|pvs|rchar|rfschar|rj
+				(aln|als|asciify|backtrace|blm|boxa|box|brp|cflags|chop|close|composite|color
+				|cp|devicem|device|do|ecs|ecr|evc|fam|fchar|fcolor|fschar|fspecial|ftr|fzoom
+				|gcolor|hcode|hla|hlm|hpfa|hpfcode|hpf|hym|hys|itc|kern|length|linetabs|lsm
+				|mso|nop|nroff|opena|open|output|pev|pnr|psbb|pso|ptr|pvs|rchar|rfschar|rj
 				|rnn|schar|shc|shift|sizes|special|spreadwarn|sty|substring|tkf|tm1|tmc|trf
-				|trin|trnt|troff|unformat|vpt|warn|warnscale|write|writec|writem)"
+				|trin|trnt|troff|unformat|vpt|warnscale|warn|writec|writem|write)"
 			end: "(?<!\\\\)(?=\n)|(?=\\\\\")"
 			beginCaptures:
 				1: {name: "punctuation.definition.request.roff"}


Fix autoload variables (#7)

Fix autoload variables (#7)

--- grammars/viml.cson
+++ grammars/viml.cson
@@ -166,7 +166,7 @@
 	numbers:
 		patterns: [{
 			name: "constant.numeric.hex.viml"
-			match: "(?:#|0[xX])[0-9A-Fa-f]+"
+			match: "0[xX][0-9A-Fa-f]+"
 		},{
 			name: "constant.numeric.float.exponential.viml"
 			match: "(?<!\\w)-?\\d+\\.\\d+[eE][-+]?\\d+"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/point.coffee
+++ src/point.coffee
@@ -46,6 +46,7 @@
     ###*
     @name clamp
     @methodOf Point#
+    @r
     ###
     clamp: (n) ->
       @copy().clamp$()


point

point

--- src/point.coffee
+++ src/point.coffee
@@ -409,7 +409,6 @@

     @name length
     @methodOf Point#
-
     @returns {Number} The length of the vector from the origin to this point.
     ###
     length: ->


adding more destructive methods

adding more destructive methods

--- src/point.coffee
+++ src/point.coffee
@@ -91,7 +91,13 @@
     @type Point
     ###
     scale: (scalar) ->
-      Point(this.x * scalar, this.y * scalar)
+      this.copy().scale$(scalar)
+
+    scale$: (scalar) ->
+      this.x *= scalar
+      this.y *= scalar
+
+      this

     ###*
     The norm of a vector is the unit vector pointing in the same direction. This method
@@ -103,7 +109,10 @@
     @type Point
     ###
     norm: (length=1.0) ->
-      this.scale(length/this.length())
+      this.copy().norm$(length)
+
+    norm$: (length=1.0) ->
+      this.scale$(length/this.length())

     ###*
     Floor the x and y values, returning a new point.


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/point.coffee
+++ src/point.coffee
@@ -625,6 +625,7 @@
       sumPoint.add(point)
     .scale(1/points.length)

+  ###
   Point.random = ->

   ###*


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/point.coffee
+++ src/point.coffee
@@ -47,7 +47,7 @@

     ###
     clamp: (n) ->
-      @copy().clamp()
+      @copy().clamp*()

     clamp$: (n) ->
       if @magnitude() > n


distance squared

distance squared

--- src/point.coffee
+++ src/point.coffee
@@ -510,7 +510,25 @@
   @returns {Number} The Euclidean distance between two points.
   ###
   Point.distance = (p1, p2) ->
-    Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2))
+    Math.sqrt(Point.distanceSquared(p1, p2))
+
+  ###*
+

+  pointA = Point(2, 3)
+  pointB = Point(9, 2)
+
+  Point.distanceSquared(pointA, pointB)
+  # => 50
+

+
+  @name distanceSquared
+  @fieldOf Point
+  @param {Point} p1
+  @param {Point} p2
+  @returns {Number} The square of the Euclidean distance between two points.
+  ###
+  Point.distanceSquared = (p1, p2) ->
+    Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)

   ###*
   Construct a point on the unit circle for the given angle.


Review per #62: spelling.

Review per #62: spelling.

--- api/models/UserPassword.coffee
+++ api/models/UserPassword.coffee
@@ -36,9 +36,9 @@
     email_template:
       type: 'string'

-  # Validate presence at least one of fields.
+  # Validate the presences of at least one of the fields.
   beforeValidate: (values, cb) ->
-    # The list of fields.
+    # The list of possible fields.
     fields = ['user_id', 'email', 'mobile']

     # Check if at least one field is present.


Clean temp folder

Clean temp folder

--- grunt/clean.coffee
+++ grunt/clean.coffee
@@ -1,2 +1,2 @@
 module.exports =
-  ['dist']
+  ['dist', 'tmp']


Failing test.

Failing test.

--- tests/game.coffee
+++ tests/game.coffee
@@ -618,19 +618,18 @@

 		query 'UPDATE armor_prototypes SET coverage = 0'
 		performSomeAttacks()
-		test.ok 0 not in damages, 'armor should not block anything if total coverage is 0'
+		test.ok 0 not of damages, 'armor should not block anything if total coverage is 0'

 		query 'UPDATE armor_prototypes SET coverage = 50'
 		query 'UPDATE armor SET strength = 0'
 		performSomeAttacks()
-		test.ok 0 not in damages, 'armor should not block anything if it is broken'
+		test.ok 0 not of damages, 'armor should not block anything if it is broken'

 		query 'UPDATE armor_prototypes SET coverage = 75 WHERE id = 2'
 		query 'UPDATE armor SET strength = 10000'
 		query 'UPDATE armor SET equipped = false'
 		performSomeAttacks()
-		console.log damages
-		test.ok 0 not in damages, 'armor should not block anything if it is unequipped'
+		test.ok 0 not of damages, 'armor should not block anything if it is unequipped'

 		test.done()


Api library removed.

Api library removed.

--- lib/api.coffee
+++ lib/api.coffee
@@ -1,68 +0,0 @@
-require 'coffee-script'
-
-# ---
-
-utils = require './utils.coffee'
-command = require './command.coffee'
-
-# ---
-
-###
-	* @param {string} src
-	* @param {function(?err)} callback
-###
-exports.registervm = (src, callback) ->
-	command.exec 'registervm', src, (err, code, output) ->
-		return callback err if err
-		return callback new Error "cannot register #{src}" if code > 0
-		return do callback if callback
-
-###
-	* @param {string} name
-	* @param {boolean=} remove
-	* @param {function(?err)} callback
-###
-exports.unregistervm = (name, remove=false, callback) ->
-	command.exec 'unregistervm', src, remove ? '--delete' : null, (err, code, output) ->
-		return callback err if err
-		return callback new Error "cannot unregister #{name}" if code > 0
-		return do callback if callback
-
-###
-	* @param {string} name
-	* @param {string=} groups
-	* @param {string=} ostype
-	* @param {string=} basefolder
-	* @param {uuid=} uuid
-	* @param {boolean=} register
-	* @param {function(?err)} callback
-###
-exports.createvm = (name, remove=false, groups=null, ostype=null, basefolder=null, uuid=null, register=false, callback) ->
-	optionals = []
-
-	optionals.push '--groups' and options.push groups if groups
-	optionals.push '--ostype' and options.push ostype if ostype
-	optionals.push '--basefolder' and options.push basefolder if basefolder
-	optionals.push '--uuid' and options.push uuid if uuid
-	optionals.push '--register' if register
-
-	command.exec 'createvm', '--name', name, optionals..., (err, code, output) ->
-		return callback err if err
-		return callback new Error "cannot create vm #{name}" if code > 0
-		return do callback if callback
-
-###
-	* @param {string} src
-	* @param {string} dst
-	* @param {boolean=} register
-	* @param {function(?err)} callback
-###
-exports.clonevm = (src, dst, snapshot=null, mode=null, options=null, register=false, callback) ->
-	optionals = []
-
-	optionals.push '--register' if register
-
-	command.exec 'clonevm', src, '--name', dst, optionals..., (err, code, output) ->
-		return callback err if err
-		return callback new Error "cannot clone #{src} into #{dst}" if code >0
-		return do callback if callback


wd module 0.4.0 version is using

wd module 0.4.0 version is using

--- tests/init.coffee
+++ tests/init.coffee
@@ -25,7 +25,7 @@
     false
 if useSauce
     execSync 'npm install sauce-connect-launcher'
-    execSync 'npm install wd'
+    execSync 'npm install wd@0.4.0'

 # code coverage
 if useCoverage


Combine details into separate object

Combine details into separate object

--- scripts/bungie.coffee
+++ scripts/bungie.coffee
@@ -292,12 +292,15 @@
       console.log 'parseActivityHash success:'
       console.log details

-      for attrname in details
-        activityDetails[attrname] = details[attrname]
+      combinedDetails = {}
+      for attr in activityDetails
+        combinedDetails[attr] = activityDetails[attr]
+      for attr in details
+        combinedDetails[attr] = details[attr]

       console.log "Resolving combined activity details for #{activityKey}:"
-      console.log activityDetails
-      deferred.resolve(activityDetails)
+      console.log combinedDetails
+      deferred.resolve(combinedDetails)

     ,(err) ->


tests: Move dropdown fixture creation into the `before` block

This prevents it appearing when the dropdown tests haven't actually been
run

--- src/components/dropdown/spec.coffee
+++ src/components/dropdown/spec.coffee
@@ -21,14 +21,8 @@
     button = undefined
     content = div().text('dropdown content')

-    fixture = select('body').append(div('hx-test-dropdown'))
-      .style('padding', '0')
-      .style('margin', '0')
-      .style('width', '1000px')
-      .style('height', '1000px')
-      .style('position', 'relative')
+    fixture = undefined
     clock = undefined
-

     getWindowMeasurement = (horizontal, scroll) ->
       if scroll then 0
@@ -53,6 +47,12 @@
         .text('button')

     before ->
+      fixture = select('body').append(div('hx-test-dropdown'))
+        .style('padding', '0')
+        .style('margin', '0')
+        .style('width', '1000px')
+        .style('height', '1000px')
+        .style('position', 'relative')
       window.resizeTo(windowSize, windowSize)

     beforeEach ->


need to nodeify a potential callback

need to nodeify a potential callback

--- src/kite-as-promised/kite.coffee
+++ src/kite-as-promised/kite.coffee
@@ -17,6 +17,7 @@
         return resolve result
     .nodeify callback

-  ready: ->
+  ready: (callback) ->
     new Promise (resolve) =>
       super resolve
+    .nodeify callback


promisify is letting me down

promisify is letting me down

--- src/kite-as-promised/kite.coffee
+++ src/kite-as-promised/kite.coffee
@@ -10,8 +10,13 @@
     return new Kite options  unless this instanceof Kite
     super options

-  [
-    'tell'
-    'ready'
-  ].forEach (method) ->
-    Kite::[method] = Promise.promisify BasicKite::[method]
+  tell: (method, params, callback) ->
+    new Promise (resolve, reject) =>
+      super method, params, (err, result) ->
+        return reject err  if err
+        return resolve result
+    .nodeify callback
+
+  ready: ->
+    new Promise (resolve) =>
+      super resolve


Decided to keep default helpers in Quest rather than special case default plugins

--- src/quest.coffee
+++ src/quest.coffee
@@ -116,9 +116,6 @@
   # just require it, otherwise make the path absolute by adding @questPath to
   # it.
   registerPlugins: ->
-    defaultPlugins = [
-      './plugins/helpers'
-    ]
     for plugin in @plugins
       console.log path
       unless _.startsWith(plugin, '/') or not _.startsWith(plugin, '.')


debug mode log slash command

debug mode log slash command

--- lib/bot.coffee
+++ lib/bot.coffee
@@ -94,8 +94,9 @@
     looker.client.fetchAccessToken()
 , 30 * 60 * 1000)

+debugMode = process.env.DEBUG_MODE == "true"
 controller = Botkit.slackbot(
-  debug: process.env.DEBUG_MODE == "true"
+  debug: debugMode
 )

 controller.setupWebserver process.env.PORT || 3333, (err, expressWebserver) ->
@@ -120,6 +121,8 @@
 GET_REGEX = 'get (.+)=(.+)'

 controller.on "slash_command", (bot, message) ->
+  if debugMode
+    console.log message
   processCommand(bot, message)

 controller.on "direct_mention", (bot, message) ->


エラー発生時は終了する。forever での再起動前提。

エラー発生時は終了する。forever での再起動前提。

--- src/robot.coffee
+++ src/robot.coffee
@@ -68,6 +68,7 @@
       @invokeErrorHandlers(err, msg)
     process.on 'uncaughtException', (err) =>
       @emit 'error', err
+      process.exit(1)

   # Public: Adds a Listener that attempts to match incoming messages based on
   # a Regex.


Fix BasicAuth for internal HTTP server

Fix BasicAuth for internal HTTP server

When using the `CONNECT_USER` and `CONNECT_PASSWORD` environment
variables, the following error was being raised:

    ReferenceError: path is not defined

--- src/robot.coffee
+++ src/robot.coffee
@@ -170,7 +170,7 @@

     @connect = Connect()

-    @connect.use Connect.basicAuth(user, path) if user and pass
+    @connect.use Connect.basicAuth(user, pass) if user and pass
     @connect.use Connect.bodyParser()
     @connect.use Connect.router (app) =>


Prevent keep-alive from pinging `undefined/` when HEROKU_URL is not set

--- src/robot.coffee
+++ src/robot.coffee
@@ -194,9 +194,9 @@
     @connect.listen process.env.PORT || 8080

     hostname = process.env.HEROKU_URL
-    hostname += '/' unless /\/$/.test hostname

     if hostname
+      hostname += '/' unless /\/$/.test hostname
       setInterval =>
         HttpClient.create("#{hostname}hubot/ping")
           .post() (err, res, body) =>


adding port 3011 as test env and 3021 as prod

--- src/lib/private/client.js.coffee
+++ src/lib/private/client.js.coffee
@@ -139,9 +139,9 @@
     anchor = document.createElement "a"
     anchor.href = document.URL
     if anchor.hostname == "localhost"
-      if anchor.port == 3011
+      if anchor.port == "3011"
         this.setEnvironment "test"
-      if anchor.port == 3021
+      if anchor.port == "3021"
         this.setEnvironment "production"
       else
         this.setEnvironment "development"


fixed coffee lint error

fixed coffee lint error

--- bin/fbsh.coffee
+++ bin/fbsh.coffee
@@ -171,7 +171,7 @@
 	return milestones().then printMilestones

 resolveMilestone = (id) ->
-	n = parseInt(id, 10);
+	n = parseInt(id, 10)
 	return milestones().then (list) ->
 		f = list.filter (m) -> m.id == n || m.name == id
 		return fb.milestone f[0] if f.length == 1


Pass the current element to before and after hooks

This allows for per-element configuration where there are multiple share buttons on the same page.

--- src/share.coffee
+++ src/share.coffee
@@ -124,9 +124,9 @@

   event_network: (instance, network) ->
     name = network.getAttribute("data-network")
-    @hook("before", name)
+    @hook("before", name, instance)
     @["network_#{name}"]()
-    @hook("after", name)
+    @hook("after", name, instance)

   ##############
@@ -228,11 +228,11 @@
   # HELPERS #
   ###########

-  hook: (type, network) ->
+  hook: (type, network, instance) ->
     fn = @config.networks[network][type]

     if typeof(fn) is "function"
-      opts = fn.call(@config.networks[network])
+      opts = fn.call(@config.networks[network], instance)
       unless opts is undefined
         opts = @normalize_filter_config_updates(opts)


Added domain error handler

Added domain error handler

--- src/akismet.coffee
+++ src/akismet.coffee
@@ -1,6 +1,7 @@
 http = require 'http'
 qs = require 'querystring'
 util = require 'util'
+domain = require 'domain'

 class Akismet
@@ -86,20 +87,17 @@
         'content-length': Buffer.byteLength(query)
         'user-agent': @userAgent

-    req = http.request options, (res) ->
-      res.setEncoding 'utf8'
-      res.on 'data', (body) ->
-        console.log "Query: " + query
-        console.log util.inspect options
-        console.log "Body: " + body
-        callback null, res.statusCode, res.headers, body
+    dom = domain.create()
+    dom.on 'error', (err) ->
+      callback err
+    dom.run () ->
+      req = http.request options, (res) ->
+        res.setEncoding 'utf8'
+        res.on 'data', (body) ->
+          callback null, res.statusCode, res.headers, body

-    req.on 'error', (err) ->
-      callback err
-
-
-    req.write query
-    req.end()
+      req.write query
+      req.end()

 # Creates and returns a new Akismet client.


Do not log every chat message

Do not log every chat message

Currently every chat message that any user sends to any room that hubot is
listening in is logged.

This removes that log message.

--- src/slack.coffee
+++ src/slack.coffee
@@ -128,7 +128,6 @@

       if hubotMsg and author
         # Pass to the robot
-        self.log "Received #{hubotMsg} from #{author.name}"
         self.receive new TextMessage(author, hubotMsg)

       # Just send back an empty reply, since our actual reply,


Reload page on language change

Reload page on language change

--- app/scripts/controllers.coffee
+++ app/scripts/controllers.coffee
@@ -9,9 +9,10 @@
     Waterpoint.query (waterpoints) ->
       $scope.waterpoints = waterpoints._items

-  .controller 'LocaleCtrl', ($scope, $cookies, gettextCatalog) ->
+  .controller 'LocaleCtrl', ($scope, $cookies, gettextCatalog, $route) ->
     $scope.update = () ->
       $cookies.locale = gettextCatalog.currentLanguage
+      $route.reload()

   .controller 'MapCtrl', ($scope, Map) ->
     $scope.map = Map


Fix error message handling for non-internal errors for 'e.log()'

--- src/app/App.coffee
+++ src/app/App.coffee
@@ -7,7 +7,7 @@
       @appConfig = AppEngine.Helpers.getApplicationConfig()
     catch e
       console.error "Error: Loading application config"
-      e.log()
+      e.log() if _.isFunction(e.log)
       return

     #configure the application
@@ -32,11 +32,11 @@
         catch e
           if(console.isError)
             console.error "Error: Engine start failure"
-            e.log()
+            e.log() if _.isFunction(e.log)
     catch e
         if(console.isError)
           console.error "Error: Engine initialise failure"
-          e.log()
+          e.log() if _.isFunction(e.log)

   setupComponentRegistry: ->
     scopes = []


Add logging

Add logging

--- src/mailButler.appsscript.coffee
+++ src/mailButler.appsscript.coffee
@@ -57,6 +57,7 @@
     if ScriptApp.getScriptTriggers().length is 0
       Logger.log "Installing trigger"
       ScriptApp.newTrigger("process").timeBased().everyMinutes(MailButler.FREQUENCY_MINUTES).create()
+      Logger.log 'Setting version'
       @DB.setCurrentVersion @getEmail(), @VERSION
     @DB.setLastUsed @getEmail()
     return


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/state.coffee
+++ src/state.coffee
@@ -12,3 +12,6 @@
       return self
     else
       return I.currentState
+
+  transition: (toState) ->
+    currentState = toState


Fix bug in event handlers calling

Fix bug in event handlers calling

--- src/state.coffee
+++ src/state.coffee
@@ -12,7 +12,7 @@
     # relay the event upward
     @fire(e)
     # call added callbacks
-    @eventHandlers[e.type].forEach (cb) -> cb()
+    @eventHandlers[e.type]?.forEach (cb) -> cb()
     return
   addObject: (obj) ->
     if obj not instanceof Game.Object


Try referencing prior data.

Try referencing prior data.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -112,22 +112,22 @@
             # their own array so we can reference them here and then
             # call on X amount of them when needed.

-            leaders =
-                superking119:
-                    points: 8
-                masonest:
-                    points: 20
-                knexem:
-                    points: 10
-                harry:
-                    points: 12
-                john:
-                    points: 40
-                peter:
-                    points: 2
+            # leaders =
+            #     superking119:
+            #         points: 8
+            #     masonest:
+            #         points: 20
+            #     knexem:
+            #         points: 10
+            #     harry:
+            #         points: 12
+            #     john:
+            #         points: 40
+            #     peter:
+            #         points: 2

             # Create array of users and points
-            score = ["#{user} has #{attrs.points}" for user, attrs of leaders]
+            score = ["#{username} has #{points[username]}" for username in people]

             # Slice the top X results from array once sorted.
             # topscore = score.slice(0, 3) would be top 3 for example.


next try at fixing #37

next try at fixing #37

--- lib/ink.coffee
+++ lib/ink.coffee
@@ -25,10 +25,6 @@
     Result.deactivate()
     Console.deactivate()

-    # Not sure why this gets set, but it prevents pane serialisation
-    pkg = atom.packages.getActivePackage('ink')
-    localStorage.clear(pkg.getCanDeferMainModuleRequireStorageKey())
-
   provide: ->
     highlight: (ed, start, end) =>
       block.highlight ed, start, end


fix coffeelinter to coffeelint

fix coffeelinter to coffeelint

--- app/templates/Gruntfile.coffee
+++ app/templates/Gruntfile.coffee
@@ -136,7 +136,7 @@
     grunt.registerTask "link_templatecompiler", tasks

   do ->
-    tasks = if yeomanConfig.scriptlang is "coffee" then ["coffeelinter"] else ["jshint"]
+    tasks = if yeomanConfig.scriptlang is "coffee" then ["coffeelint"] else ["jshint"]
     grunt.registerTask "link_lintscript", tasks

   grunt.registerTask "link_requirejs", [


Add \ before question mark

Add \ before question mark

--- scripts/resume.coffee
+++ scripts/resume.coffee
@@ -1,4 +1,4 @@
 module.exports = (robot) ->
   names = ['Dana', 'Freia', 'Jhishan', 'Kyle', 'Brenton', 'Terri', 'Abhi', 'Drew']
-  robot.hear /Resume queen, who is next?/i, (res) ->
+  robot.hear /Resume queen, who is next\?/i, (res) ->
     res.send res.random names


senpai kuuhaku ok

senpai kuuhaku ok

--- scripts/senpai.coffee
+++ scripts/senpai.coffee
@@ -12,6 +12,7 @@
 #   {アダ名 or nickname}--  - ヨクナイネ！
 #   hubot {アダ名 or nickname} 何点ですか?  - hubot に後輩の点数を教えてもらおう!

+
 AISATSU = [# {{{
   'よろしくな'
   'まいど'
@@ -74,7 +75,6 @@
   usersInfo[name][key]
 # }}}

-
 whoIsThis = (robot, msg, name) -># {{{
   return name if existsUser robot, msg, name
   gNicknames = getSenpaiStorage robot, msg, 'NICKNAMES'
@@ -82,7 +82,6 @@
   return gNicknames[name] if gNicknames[name]
   return null
 # }}}
-

 # {{{ Keigo
 REGEXP_KEIGO = '(です|デス|desu|ます|マス|masu|っす|ッス)か?[\?？!！]?'
@@ -122,7 +121,7 @@
     checkKeigo robot, msg
 # }}}

-  robot.respond /後輩の ([^ ]+)/, (msg) -># {{{
+  robot.respond /後輩の[ ]*([^ ]+)/, (msg) -># {{{
     name = msg.match[1]
     name = trimKeigo name
     if existsUser robot, msg, name
@@ -133,7 +132,7 @@
     msg.send "#{name} か。よろしくな"
 # }}}

-  robot.respond /([^ ]+) のアダ名何/, (msg) -># {{{
+  robot.respond /([^ ]+)[ ]*のアダ名(何|なん|なに)/, (msg) -># {{{
     realname = msg.match[1]
     unless existsUser robot, msg, realname
       msg.send "#{realname} って誰？"
@@ -156,7 +155,7 @@

 # }}}

-  robot.respond /([^ ]+) は ([^ ]+) のアダ名/, (msg) -># {{{
+  robot.respond /([^ ]+)[ ]*は[ ]*([^ ]+)[ ]*のアダ名/, (msg) -># {{{
     return if msg.message.match('じゃない')
     nickname = msg.match[1]
     realname = msg.match[2]
@@ -190,7 +189,7 @@
     checkKeigo robot, msg
 # }}}

-  robot.respond /([^ ]+) は ([^ ]+) のアダ名(じゃない)/, (msg) -># {{{
+  robot.respond /([^ ]+)[ ]*は[ ]*([^ ]+)[ ]*のアダ名(じゃない)/, (msg) -># {{{
     nickname = msg.match[1]
     realname = msg.match[2]
     unless existsUser robot, msg, realname


style(menu-clock): fix indentation

style(menu-clock): fix indentation

--- src/menu-clock/menu-clock.coffee
+++ src/menu-clock/menu-clock.coffee
@@ -4,10 +4,10 @@

 leftMenu = document.querySelector "#side_bar_inner"
 leftMenu.insertAdjacentHTML "beforeend", "
-

-

-

-
"
+

+

+

+
"

 updateClock = ->
   document.querySelector( ".vkx-time" ).innerHTML = moment().format "HH:mm:ss"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/tween.coffee
+++ src/tween.coffee
@@ -10,9 +10,7 @@
 Tween = (I={}, self) ->
   # Set some default properties
   Object.reverseMerge I,
-    color: "blue"
-    height: 32
-    width: 32
+    activeTweens: ->
     # spriteName: "block" # Use the name of a sprite in the images folder

   # Inherit from game object


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/tween.coffee
+++ src/tween.coffee
@@ -45,6 +45,7 @@

     easing = properties.easing || "linear"
     delete properties.easing
+

     for property, target of properties
       I.activeTweens[property] =


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/tween.coffee
+++ src/tween.coffee
@@ -23,6 +23,7 @@
         I[property] = f(t)

   tween: (duration, properties) ->
+
     # TODO Extract special properties
     easing = properties.easing || "linear"


fixing issue with DELETE and hangup connection.  Had to make sure I send the propper Header information to the HTTP Request, or it does not work.

--- src/xfuse.coffee
+++ src/xfuse.coffee
@@ -27,6 +27,7 @@
         @options = @options || {}

         @options.host = siteUrl
+        @options.hostname = siteUrl
         @options.auth = apiToken  + ':'
         @options.method = method
         @options.path    = url
@@ -144,6 +145,9 @@
         req.on 'error', (e) =>
             @callback e, null
             return
+        req.setHeader 'Content-Type', 'application/json'
+        req.setHeader 'Content-Length', 0
+        req.end()
         return
 exports.get = (url, params, callback) ->
     if typeof params == 'function'
@@ -163,7 +167,7 @@

     new XFuse 'PUT', url, putData, callback

-exports.delete = (url, callback) ->
+exports.del = (url, callback) ->

     new XFuse 'DELETE', url, callback


Initialize metaAttribute and pathView

Initialize metaAttribute and pathView

--- apps/user/profile/index.coffee
+++ apps/user/profile/index.coffee
@@ -1,6 +1,9 @@
 Backbone = require 'backbone'
-{ QUERY, PROFILE_CHANNELS, SORT } = require("sharify").data
+{ QUERY, PROFILE_CHANNELS, SORT, USER } = require("sharify").data
 mediator = require '../../../lib/mediator.coffee'
+User = require "../../../models/user.coffee"
+PathView = require '../../../components/path/client/path_view.coffee'
+MetaEditableAttributeView = require '../../../components/editable_attribute/client/meta_editable_attribute_view.coffee'
 ChannelGroupView = require '../../../components/channel_block_group/view.coffee'

 template = -> require('../templates/partials/_channel_groups.jade') arguments...
@@ -55,7 +58,20 @@
       view.initBlockViews()

 module.exports.init = ->
+  user = new User USER
+
   view = new ProfileView
     el: $('.profile')

-  view.setUpChannelGroupViews(PROFILE_CHANNELS)+  view.setUpChannelGroupViews(PROFILE_CHANNELS)
+
+  new PathView
+    el: $('section.path--header')
+    model: user
+
+  new MetaEditableAttributeView
+    model: user
+    el: $("#metadata--about .metadata__content")
+    _attribute: 'description'
+    _kind: 'markdown'
+    wait: true


comment out the builtin set tests

comment out the builtin set tests

--- lib/set.coffee
+++ lib/set.coffee
@@ -43,6 +43,7 @@
         @union: (sets...) ->
                 (foldl ((a,b) -> a.union b), (new Set), [sets...])

+###
 # Set tests

 s1 = new Set [1, 2, 3, 4]
@@ -61,3 +62,4 @@

 console.log "should be { 3 4 }:    #{(s5.intersect s6).toString()}"

+###


extracted protected info to environment

extracted protected info to environment

--- src/scripts/buzzfeedstats.coffee
+++ src/scripts/buzzfeedstats.coffee
@@ -18,6 +18,9 @@
   robot.respond /realtime$/i, (msg) ->
     gaCallback msg, "buzzfeed"

+  robot.hear /buzzfeed .* users$/i, (msg) ->
+    gaCallback msg, "buzzfeed"
+

 gaCallback = (msg, query) ->
   return msg.send("HUBOT_BUZZFEED_GA_URL needs to be set") if not env.HUBOT_BUZZFEED_GA_URL


github issues now advises githubot 0.4.0 in order to add a user-agent

--- src/scripts/github-issues.coffee
+++ src/scripts/github-issues.coffee
@@ -4,7 +4,7 @@
 # Dependencies:
 #   "underscore": "1.3.3"
 #   "underscore.string": "2.1.1"
-#   "githubot": "0.2.0"
+#   "githubot": "0.4.0"
 #
 # Configuration:
 #   HUBOT_GITHUB_TOKEN


Script google-images will now only add #.png to url if it doesn't end with an image extension

--- src/scripts/google-images.coffee
+++ src/scripts/google-images.coffee
@@ -39,6 +39,12 @@
       images = JSON.parse(body)
       images = images.responseData?.results
       if images?.length > 0
-        image  = msg.random images
-        cb "#{image.unescapedUrl}#.png"
+        image = msg.random images
+        cb ensureImageExtension image.unescapedUrl

+ensureImageExtension = (url) ->
+  ext = url.split('.').pop()
+  if /(png|jpe?g|gif)/i.test(ext)
+    url
+  else
+    "#{url}#.png"


move sign up to the left

move sign up to the left

--- src/smc-webapp/landing_page.cjsx
+++ src/smc-webapp/landing_page.cjsx
@@ -652,13 +652,6 @@

                 </Row>
                 <Row>
-                    <Col sm=7 className="hidden-xs" style=marginTop:'10px'>
-                        <Well style={'textAlign': 'center', 'float':'right'}>
-                            <SMC_Commercial />
-

-                            <SMC_Quote />
-                        </Well>
-                    </Col>
                     <Col sm=5>
                         <SignUp
                             sign_up_error = {@props.sign_up_error}
@@ -667,6 +660,13 @@
                             signing_up    = {@props.signing_up}
                             has_account   = {@props.has_account} />
                     </Col>
+                    <Col sm=7 className="hidden-xs" style=marginTop:'10px'>
+                        <Well style={'textAlign': 'center', 'float':'right'}>
+                            <SMC_Commercial />
+

+                            <SMC_Quote />
+                        </Well>
+                    </Col>
                 </Row>
                 <Well>
                     <Row>


fetcher: better handling non-string body

--- shared/fetcher.coffee
+++ shared/fetcher.coffee
@@ -146,7 +146,8 @@
     success: (model, body) ->
       callback(null, model)
     error: (model, body, options) ->
-      err = new Error("ERROR fetching model '#{model.constructor.name}' with options '#{JSON.stringify(options)}'. Response: " + body.slice(0, 150))
+      bodyOutput = if typeof body is 'string' then body.slice(0, 150) else JSON.stringify(body)
+      err = new Error("ERROR fetching model '#{model.constructor.name}' with options '#{JSON.stringify(options)}'. Response: " + bodyOutput)
       callback(err)

 retrieveModelsForCollectionName = (collectionName, modelIds) ->


ensure the bluetooth device is disconnected before quitting

--- src/tasks/watch-analytics.coffee
+++ src/tasks/watch-analytics.coffee
@@ -9,7 +9,7 @@
     bufferB.copy(newBuffer, bufferA.length)
     newBuffer

-  process = (data) ->
+  processData = (data) ->
       name = data.toString('ascii', 0, 9)
       format = data.toString('ascii', 10, 11)

@@ -18,8 +18,8 @@
           timeInSeconds = data.readUInt32BE(12)
           value = data.readUInt16BE(16)
           console.log("#{name}: #{timeInSeconds}, #{value}")
-        else
-          console.log "failed to parse message #{data.toString()} - expected integer messages to be length 18 but was #{data.length}"
+#        else
+#          console.log "failed to parse message #{data.toString()} - expected integer messages to be length 18 but was #{data.length}"

       else if format == 'F'
         if data.length == 20
@@ -47,7 +47,13 @@
         console.log "disconnected, retrying..."
         setTimeout(connect, 100)

-      peripheral.on "connect", -> peripheral.discoverServices ["2220"]
+      peripheral.on "connect", ->
+
+        process.on 'SIGINT', ->
+          peripheral.disconnect();
+          process.exit(0)
+
+        peripheral.discoverServices ["2220"]

       buffer = new Buffer(0)

@@ -58,7 +64,7 @@

             if stopBit != -1
               buffer = concatenate(buffer, data.slice(0, stopBit))
-              process(buffer)
+              processData(buffer)
               buffer = data.slice(stopBit + 1)
             else
               buffer = concatenate(buffer, data)


Add Mages' Might banner image

Add Mages' Might banner image


fix default values

fix default values

--- bokehjs/src/coffee/source/ajax_data_source.coffee
+++ bokehjs/src/coffee/source/ajax_data_source.coffee
@@ -1,6 +1,5 @@
 $ = require "jquery"
 _ = require "underscore"
-Backbone = require "backbone"
 {logger} = require "../common/logging"
 RemoteDataSource = require "./remote_data_source"

@@ -49,10 +48,9 @@
   defaults: =>
     return _.extend {}, super(), {
       mode: 'replace'
-      data_url : ''
-      expr : null
+      data_url : null
       content_type : 'application/json'
-      headers : {}
+      http_headers : {}
       max_size: null
       method: 'POST'
       if_modified: false


mp4: Now it is able to parse an mp4 file

--- mp4.coffee
+++ mp4.coffee
@@ -1,10 +1,19 @@
 Bits = require './bits'
+EventEmitterModule = require './event_emitter'
+Sequent = require 'sequent'
 fs = require 'fs'

 formatDate = (date) ->
   date.toISOString()

 TAG_CTOO = new Buffer([0xa9, 0x74, 0x6f, 0x6f]).toString 'utf8'
+MIN_TIME_DIFF = 0.01  # seconds
+READ_BUFFER_TIME = 3.0
+QUEUE_BUFFER_TIME = 1.5
+
+getCurrentTime = ->
+  time = process.hrtime()
+  return time[0] + time[1] / 1e9

 class MP4File
   constructor: (filename) ->
@@ -13,7 +22,8 @@

   open: (filename) ->
     startTime = process.hrtime()
-    @bits = new Bits fs.readFileSync filename  # up to 1GB
+    @fileBuf = fs.readFileSync filename  # up to 1GB
+    @bits = new Bits @fileBuf
     diffTime = process.hrtime startTime
     console.log "took #{(diffTime[0] * 1e9 + diffTime[1]) / 1000000} ms to read"

@@ -25,14 +35,241 @@
     @tree = { boxes: [] }
     while @bits.has_more_data()
       box = Box.parse @bits, null  # null == root box
-      process.stdout.write box.dump 0, 1
+      if box instanceof MovieBox
+        @moovBox = box
+      else if box instanceof MediaDataBox
+        @mdatBox = box
+#      process.stdout.write box.dump 0, 1
       @tree.boxes.push box.getTree()
-      if box instanceof MediaDataBox  # found mdat
-        console.log box
     diffTime = process.hrtime startTime
     console.log "took #{(diffTime[0] * 1e9 + diffTime[1]) / 1000000} ms to parse"
     console.log "EOF"
-    return
+
+    for child in @moovBox.children
+      if child instanceof TrackBox  # trak
+        tkhdBox = child.find 'tkhd'
+        if tkhdBox.isAudioTrack
+          @audioTrakBox = child
+        else
+          @videoTrakBox = child
+
+    return
+
+  getSPS: ->
+    avcCBox = @videoTrakBox.find 'avcC'
+    return avcCBox.sequenceParameterSets[0]
+
+  getPPS: ->
+    avcCBox = @videoTrakBox.find 'avcC'
+    return avcCBox.pictureParameterSets[0]
+
+  getAudioSpecificConfig: ->
+    esdsBox = @audioTrakBox.find 'esds'
+    return esdsBox.decoderConfigDescriptor.decoderSpecificInfo.specificInfo
+
+  play: ->
+    @currentTime = 0
+    @consumedAudioSamples = 0
+    @consumedVideoSamples = 0
+    @consumedAudioChunks = 0
+    @consumedVideoChunks = 0
+    startTime = process.hrtime()
+    @numVideoSamples = @getNumVideoSamples()
+    @numAudioSamples = @getNumAudioSamples()
+
+    seq = new Sequent
+
+    @bufferedAudioTime = 0
+    @bufferedVideoTime = 0
+    @queuedAudioTime = 0
+    @queuedVideoTime = 0
+    @currentPlayTime = 0
+    @playStartTime = getCurrentTime()
+    @bufferedAudioSamples = []
+    @bufferedVideoSamples = []
+
+    @isAudioEOF = false
+    @isVideoEOF = false
+
+    @bufferAudio =>
+      # audio samples has been buffered
+      seq.done()
+
+    @bufferVideo =>
+      # video samples has been buffered
+      seq.done()
+
+    seq.wait 2, =>
+      # ready to play
+      @startStreaming()
+
+  checkAudioBuffer: ->
+    timeDiff = @bufferedAudioTime - @currentPlayTime
+    if timeDiff < READ_BUFFER_TIME
+      # fill buffer
+      if @readNextAudioChunk()
+        @queueBufferedSamples()
+    else
+      @queueBufferedSamples()
+    return
+
+  checkVideoBuffer: ->
+    timeDiff = @bufferedVideoTime - @currentPlayTime
+    if timeDiff < READ_BUFFER_TIME
+      if @readNextVideoChunk()
+        @queueBufferedSamples()
+    else
+      @queueBufferedSamples()
+    return
+
+  startStreaming: ->
+    @queueBufferedSamples()
+
+  updateCurrentPlayTime: ->
+    @currentPlayTime = getCurrentTime() - @playStartTime
+
+  queueBufferedAudioSamples: ->
+    audioSample = @bufferedAudioSamples.shift()
+    if not audioSample?  # @bufferedAudioSamples is empty
+      return
+    timeDiff = audioSample.time - @currentPlayTime
+    if timeDiff <= MIN_TIME_DIFF
+      @emit 'audio_data', audioSample.data, audioSample.pts
+      @updateCurrentPlayTime()
+    else
+      setTimeout =>
+        @emit 'audio_data', audioSample.data, audioSample.pts
+        @updateCurrentPlayTime()
+        @checkAudioBuffer()
+      , timeDiff * 1000
+    @queuedAudioTime = audioSample.time
+    if @queuedAudioTime - @currentPlayTime < QUEUE_BUFFER_TIME
+      @queueBufferedSamples()
+
+  queueBufferedVideoSamples: ->
+    videoSample = @bufferedVideoSamples.shift()
+    if not videoSample?  # read buffer is empty
+      return
+    timeDiff = videoSample.time - @currentPlayTime
+    if timeDiff <= MIN_TIME_DIFF
+      console.log "emit1"
+      @emit 'video_data', videoSample.data, videoSample.pts
+      @updateCurrentPlayTime()
+    else
+      setTimeout =>
+        @emit 'video_data', videoSample.data, videoSample.pts
+        @updateCurrentPlayTime()
+        @checkVideoBuffer()
+      , timeDiff * 1000
+    @queuedVideoTime = videoSample.time
+    if @queuedVideoTime - @currentPlayTime < QUEUE_BUFFER_TIME
+      @queueBufferedSamples()
+
+  queueBufferedSamples: ->
+    @queueBufferedAudioSamples()
+    @queueBufferedVideoSamples()
+
+  bufferAudio: (callback) ->
+    # TODO: Use async
+    while @bufferedAudioTime < @currentPlayTime + READ_BUFFER_TIME
+      if not @readNextAudioChunk()
+        @isAudioEOF = true
+    callback?()
+
+  bufferVideo: (callback) ->
+    # TODO: Use async
+    while @bufferedVideoTime < @currentPlayTime + READ_BUFFER_TIME
+      if not @readNextVideoChunk()
+        @isVideoEOF = true
+    callback?()
+
+  getNumVideoSamples: ->
+    sttsBox = @videoTrakBox.find 'stts'
+    return sttsBox.getTotalSamples()
+
+  getNumAudioSamples: ->
+    sttsBox = @audioTrakBox.find 'stts'
+    return sttsBox.getTotalSamples()
+
+  parseH264Sample: (buf) ->
+    # The format is defined in ISO 14496-15 5.2.3
+
+    # Why do we need a length header here even we
+    # already have the same data in stsz box???
+    avcCBox = @videoTrakBox.find 'avcC'
+    lengthSize = avcCBox.lengthSizeMinusOne + 1
+    bits = new Bits buf
+    length = bits.read_bits lengthSize * 8
+    nalUnitType = bits.get_byte_at(0) & 0x1f
+    bits.skip_bytes length
+
+    if bits.get_remaining_bits() isnt 0
+      throw new Error "remaining bits is not zero: #{bits.get_remaining_bits()}"
+
+  readChunk: (chunkNumber, sampleNumber, trakBox) ->
+    sttsBox = trakBox.find 'stts'
+    stscBox = trakBox.find 'stsc'
+    numSamplesInChunk = stscBox.getNumSamplesInChunk chunkNumber
+    stcoBox = trakBox.find 'stco'
+    chunkOffset = stcoBox.getChunkOffset chunkNumber
+    stszBox = trakBox.find 'stsz'
+    sampleSizes = stszBox.getSampleSizes sampleNumber, numSamplesInChunk
+    chunkLength = stszBox.getSampleSize sampleNumber, numSamplesInChunk
+    samples = []
+    sampleOffset = 0
+    for sampleSize, i in sampleSizes
+      sampleTime = sttsBox.getDecodingTime sampleNumber + i
+      samples.push {
+        pts: sampleTime.pts
+        time: sampleTime.seconds
+        data: @fileBuf[chunkOffset+sampleOffset...chunkOffset+sampleOffset+sampleSize]
+      }
+      sampleOffset += sampleSize
+
+    return samples
+
+  readNextVideoChunk: ->
+    if @consumedVideoSamples >= @numVideoSamples
+      return false
+    nextChunkNumber = @consumedVideoChunks + 1
+    nextSampleNumber = @consumedVideoSamples + 1
+
+    samples = @readChunk nextChunkNumber, nextSampleNumber, @videoTrakBox
+
+    for sample in samples
+#      @parseH264Sample sample.data
+      # Remove length header
+      sample.data = sample.data[4..]
+
+    @consumedVideoChunks++
+    @consumedVideoSamples += samples.length
+    @bufferedVideoTime = samples[samples.length-1].time
+    @bufferedVideoSamples = @bufferedVideoSamples.concat samples
+
+    return true
+
+  parseAACSample: (buf) ->
+    # nop
+
+  readNextAudioChunk: ->
+    if @consumedAudioSamples >= @numAudioSamples
+      return false
+    nextChunkNumber = @consumedAudioChunks + 1
+    nextSampleNumber = @consumedAudioSamples + 1
+
+    samples = @readChunk nextChunkNumber, nextSampleNumber, @audioTrakBox
+
+#    for sample in samples
+#      @parseAACSample sample.data
+
+    @consumedAudioChunks++
+    @consumedAudioSamples += samples.length
+    @bufferedAudioTime = samples[samples.length-1].time
+    @bufferedAudioSamples = @bufferedAudioSamples.concat samples
+
+    return true
+
+EventEmitterModule.mixin MP4File

 class Box
   # time: seconds since midnight, Jan. 1, 1904 UTC
@@ -838,8 +1075,12 @@
       time += entry.sampleDelta * entry.sampleCount
     return time / mdhdBox.timescale

-  # Returns a decoding time for the given sample number
+  # Returns a decoding time for the given sample number.
+  # The sample number starts at 1.
   getDecodingTime: (sampleNumber) ->
+    mdhdBox = @findParent('mdia').find('mdhd')
+
+    sampleNumber--
     time = 0
     for entry in @entries
       if sampleNumber > entry.sampleCount
@@ -848,7 +1089,15 @@
       else
         time += entry.sampleDelta * sampleNumber
         break
-    return time
+    if mdhdBox.timescale isnt 90000
+      pts = Math.round(time * 90000 / mdhdBox.timescale)
+    else
+      pts = time
+    return {
+      time: time
+      seconds: time / mdhdBox.timescale
+      pts: pts
+    }

   getDetails: (detailLevel) ->
     str = "entryCount=#{@entryCount}"
@@ -918,10 +1167,19 @@

     return

+  getNumSamplesExceptLastChunk: ->
+    samples = 0
+    for entry in @entries
+      if entry.numChunks?
+        samples += entry.samplesPerChunk * entry.numChunks
+    return samples
+
   getNumSamplesInChunk: (chunk) ->
     for entry in @entries
       if not entry.numChunks?
-        return null  # the last chunk
+        # TOOD: too heavy
+        sttsBox = @findParent('stbl').find 'stts'
+        return sttsBox.getTotalSamples() - @getNumSamplesExceptLastChunk()
       if chunk < entry.firstChunk + entry.numChunks
         return entry.samplesPerChunk
     throw new Error "Chunk not found: #{chunk}"
@@ -966,12 +1224,29 @@
         @entrySizes.push bits.read_uint32()
     return

+  getSampleSizes: (sampleNumber, len=1) ->
+    sizes = []
+    if @sampleSize isnt 0
+      for i in [len...0]
+        sizes.push @sampleSize
+    else
+      for i in [len...0]
+        sizes.push @entrySizes[sampleNumber - 1]
+        sampleNumber++
+    return sizes
+
   # Returns the sample size for the given sample number
-  getSampleSize: (sampleNumber) ->
-    if @sampleSize is 0  # the samples have different sizes
-      return @entrySizes
-    else  # all samples are the same size
-      return @entrySizes[sampleNumber]  # TODO: sampleNumber - 1?
+  getSampleSize: (sampleNumber, len=1) ->
+    if @sampleSize isnt 0  # all samples are the same size
+      return @sampleSize * len
+    else  # the samples have different sizes
+      totalLength = 0
+      for i in [len...0]
+        if sampleNumber > @entrySizes.length
+          throw new Error "Sample number is out of range: #{sampleNumber} > #{@entrySizes.length}"
+        totalLength += @entrySizes[sampleNumber - 1]  # TODO: Is -1 correct?
+        sampleNumber++
+      return totalLength

   getDetails: (detailLevel) ->
     str = "sampleSize=#{@sampleSize} sampleCount=#{@sampleCount}"
@@ -1441,9 +1716,45 @@
 # (copyright sign) + 'too'
 class CTOOBox extends GenericDataBox

+
+# test
 if process.argv.length < 3
   console.log "Error: specify an mp4 filename"
   return 1

 mp4file = new MP4File process.argv[2]
+lastAudioReceivedTime = null
+lastAudioActualTime = null
+mp4file.on 'audio_data', (data, pts) ->
+  console.log "received audio_data: pts=#{pts} time=#{pts / 90000}"
+  currentTime = getCurrentTime()
+  if lastAudioReceivedTime?
+    receivedDiff = mp4file.currentPlayTime - lastAudioReceivedTime
+    actualDiff = currentTime - lastAudioActualTime
+    timeDiff = receivedDiff - actualDiff
+    if Math.abs(timeDiff) >= 0.1
+      throw new Error "ERROR: too much audio time difference: lastAudioReceivedTime=#{lastAudioReceivedTime} playTime=#{mp4file.currentPlayTime} lastAudioActualTime=#{lastAudioActualTime} currentTime=#{currentTime} received=#{receivedDiff} actual=#{actualDiff} diff=#{timeDiff}"
+  lastAudioReceivedTime = mp4file.currentPlayTime
+  lastAudioActualTime = currentTime
+
+lastVideoReceivedTime = null
+lastVideoActualTime = null
+mp4file.on 'video_data', (data, pts) ->
+  console.log "received video_data: pts=#{pts} time=#{pts / 90000}"
+  currentTime = getCurrentTime()
+  if lastVideoReceivedTime?
+    receivedDiff = mp4file.currentPlayTime - lastVideoReceivedTime
+    actualDiff = currentTime - lastVideoActualTime
+    timeDiff = receivedDiff - actualDiff
+    if Math.abs(timeDiff) >= 0.1
+      throw new Error "ERROR: too much video time difference: lastVideoReceivedTime=#{lastVideoReceivedTime} playTime=#{mp4file.currentPlayTime} lastVideoActualTime=#{lastVideoActualTime} currentTime=#{currentTime} received=#{receivedDiff} actual=#{actualDiff} diff=#{timeDiff}"
+  lastVideoReceivedTime = mp4file.currentPlayTime
+  lastVideoActualTime = currentTime
+
 mp4file.parse()
+mp4file.play()
+
+api =
+  MP4File: MP4File
+
+module.exports = api


on reorder, update player.current.index

on reorder, update player.current.index

--- src/scripts/controllers/main.coffee
+++ src/scripts/controllers/main.coffee
@@ -11,8 +11,22 @@
     Mousetrap.bind 'shift+left', ->
         $scope.$apply -> $scope.player.prev()

-    $scope.$on 'dragToReorder.reordered', ->
+    $scope.$on 'dragToReorder.reordered', (e, move) ->
         $scope.playlist.save()
+        current = $scope.player.current
+        return unless current.index?
+
+        # moved currently-playing track
+        if move.from is current.index
+            current.index = move.to
+
+        # moved from below current to above current
+        else if move.to <= current.index < move.from
+            current.index += 1
+
+        # moved from above current to below current
+        else if move.to >= current.index > move.from
+            current.index -= 1

     $scope.$on '$destroy', ->
         Mousetrap.unbind 'space'


add gc() to encrypt()

add gc() to encrypt()

--- src/encrypt.coffee
+++ src/encrypt.coffee
@@ -145,6 +145,8 @@
   encrypt: (buf) ->
     if @method?
       result = to_buffer @cipher.update(buf.toString('binary'))
+      if global.gc?
+        gc()
       if @iv_sent
         return result
       else


keep shortcuts displayed in menu in sync with prefs

--- client/menu.coffee
+++ client/menu.coffee
@@ -1,3 +1,6 @@
+prefs = require './prefs'
+{cmds} = require './cmds'
+
 # This is a generic menu for a browser or NW.js
 # There's an alternative implementation for NW.js in ../init-nw.coffee
 # For the concrete content in the menu, see ide.coffee
@@ -21,7 +24,7 @@
     name = x[''].replace /_(.)/g, (_, k) -> if acc || k == '_' then k else "#{acc = k}"
     $a = $ "#{name}"
     if acc then $a.attr 'accessKey', acc.toLowerCase()
-    if x.cmd then $a.append '' # todo: keep shortcut in menu in sync with the prefs
+    if x.cmd then $a.append ""
     if x.group
       $a.addClass "m-group-#{x.group}"
       $a.toggleClass 'm-checked', !!x.checked
@@ -86,7 +89,12 @@
       if isAccessKeyEvent e
         $x = $m.find "[accessKey=#{String.fromCharCode(e.which).toLowerCase()}]:visible"
         if $x.length then $x.mousedown(); $x.parent().find('a').eq(1).focus(); false
-
   # todo: is mapping F10 in CodeMirror really necessary?
 #   CodeMirror.keyMap.default.F10 = -> $m.children().eq(0).addClass('m-open').find('a').eq(1).focus(); false
+  updateMenuShortCuts prefs.keys()
   return
+
+prefs.keys updateMenuShortCuts = (h) ->
+  k = {}; for [code, desc, defaults] in cmds then [k[code]] = h[code] || defaults
+  $('.m-shortcut').each -> $(@).text k[$(@).data 'cmd'] || ''; return
+  return


improving random utilities

improving random utilities

--- lingsim.coffee
+++ lingsim.coffee
@@ -5,6 +5,14 @@
   # random number uniformly distributed over range [n, m]
   randomRange: (n, m) ->
     n + (m-n) * Math.random()
+
+  # random index of a list
+  randomIndex: (list) =>
+    @randomRange 0, list.length - 1
+
+  # random element of list
+  sample: (list) =>
+    list[@randomIndex(list)]

   # standardly distributed random number
   # with mean 0 and std_dev 1
@@ -36,8 +44,8 @@
     String.fromCharCode 101 + n

   # return a random letter from the first n
-  randomAlpha: (n) =>
-    @alpha @randomRange(0,n)
+  randomAlpha: (n=26) =>
+    @alpha @randomRange(0,n-1)

 Utils = new Utilities()


Adds option to allow attributes popup. Fixes #672.


overrides for label and title

overrides for label and title


support for pushing shapes and rotations

--- public/javascripts/tetris.coffee
+++ public/javascripts/tetris.coffee
@@ -10,20 +10,6 @@
   blockSize = gridSize - 2
   shapes = {}
   playerID = makeGuid()
-
-  channel.bind 'created', (data) ->
-    console.log('created got', data.id)
-    return if data.playerID is playerID
-    gs.addEntity( new CubeShape(id: data.id, x:data.x, y:data.y, rotation: data.rotation, color: data.color, fixed: true) )
-
-  channel.bind 'moved', (data) ->
-    console.log('moved got', data.id)
-    return if data.playerID is playerID
-    shape = shapes[data.id]
-    return unless shape?
-    shape.x = data.x
-    shape.y = data.y
-    shape.rotate = data.rotate

   class Level
     constructor: ->
@@ -41,7 +27,6 @@
       @color = opts.color || '#000'
       @fixed = opts.fixed || false
       shapes[@id] = this
-      @type = null
       @created() if isNewObj

     draw: (c) ->
@@ -64,7 +49,7 @@
         y: @y,
         rotation: @rotation,
         id: @id,
-        type: @type
+        type: @type,
         color: @color

     moved: ->
@@ -168,12 +153,35 @@
         3: [[-1,1],[0,1],[1,1],[2,1]]
       }[rotation]

-
+  Shape.types = {
+    'C': CubeShape,
+    'L': LShape,
+    'J': JShape,
+    'S': SShape,
+    'Z': ZShape,
+    'T': TShape,
+    'I': IShape
+  }
+
+  channel.bind 'created', (data) ->
+    console.log('created got', data.id)
+    return if data.playerID is playerID
+    shapeClass = Shape.types[data.type]
+    gs.addEntity( new shapeClass(id: data.id, x:data.x, y:data.y, rotation: data.rotation, color: data.color, fixed: true) )
+
+  channel.bind 'moved', (data) ->
+    console.log('moved got', data.id)
+    return if data.playerID is playerID
+    shape = shapes[data.id]
+    return unless shape?
+    shape.x = data.x
+    shape.y = data.y
+    shape.rotation = data.rotation
+
   level = new Level()
   gs.addEntity(level)
-  setTimeout (->
+  pusher.connection.bind 'connected', ->
     # gs.addEntity(new ZShape(x:1, y:1, color: '#f00', fixed: true))
     # gs.addEntity(new SShape(x:5, y:5, color: '#0f0', fixed: false))
-    gs.addEntity(new CubeShape(x:2, y:7, color: '#00f', fixed: false))
-    # gs.addEntity(new TShape(x:8, y:3, color: '#000', fixed: true))
-    ), 1000
+    # gs.addEntity(new CubeShape(x:2, y:7, color: '#00f', fixed: false))
+    gs.addEntity(new TShape(x:8, y:3, color: '#000', fixed: false))


Only proceed if a visitable link is clicked

--- src/turbolinks/controller.coffee
+++ src/turbolinks/controller.coffee
@@ -97,11 +97,11 @@

   clickBubbled: (event) =>
     if @clickEventIsSignificant(event)
-      link = @getVisitableLinkForNode(event.target)
-      if location = @getVisitableLocationForLink(link)
-        if @applicationAllowsFollowingLinkToLocation(link, location)
-          event.preventDefault()
-          @visit(location)
+      if link = @getVisitableLinkForNode(event.target)
+        if location = @getVisitableLocationForLink(link)
+          if @applicationAllowsFollowingLinkToLocation(link, location)
+            event.preventDefault()
+            @visit(location)

   # Application events


Missed 'orange' and 'aliceblue' colors added to the color parser

--- impl/base/utils/color.coffee
+++ impl/base/utils/color.coffee
@@ -29,6 +29,8 @@
 		blue: 0x0000ffff
 		teal: 0x008080ff
 		aqua: 0x00ffffff
+		orange: 0xffa500ff
+		aliceblue: 0xf0f8ffff
 		antiquewhite: 0xfaebd7ff
 		aquamarine: 0x7fffd4ff
 		azure: 0xf0ffffff


remove BOM characters from YAML files.

remove BOM characters from YAML files.

--- rime/config.coffee
+++ rime/config.coffee
@@ -10,8 +10,10 @@
         console.error "error loading config: #{err}"
         callback null
       else
+        # remove potential BOM character
+        yaml = data.replace /^\ufeff/, ''
         try
-          @root = jsyaml.safeLoad data, filename: filePath
+          @root = jsyaml.safeLoad yaml, filename: filePath
         catch err
           console.error "error loading config: #{err}"
           callback null


:racehorse: Don't clone rule stack unless specified

--- src/grammar.coffee
+++ src/grammar.coffee
@@ -104,9 +104,13 @@

   createPattern: (options) -> new Pattern(this, @registry, options)

-  tokenizeLine: (line, ruleStack=[@getInitialRule()], firstLine=false) ->
+  tokenizeLine: (line, ruleStack, firstLine=false) ->
+    if ruleStack?
+      ruleStack = new Array(ruleStack...) # clone ruleStack
+    else
+      ruleStack = [@getInitialRule()]
     originalRuleStack = ruleStack
-    ruleStack = new Array(ruleStack...) # clone ruleStack
+
     tokens = []
     position = 0

@@ -158,7 +162,7 @@
             break

     ruleStack.forEach (rule) -> rule.clearAnchorPosition()
-    { tokens, ruleStack }
+    {tokens, ruleStack}

   tokenizeLines: (text) ->
     lines = text.split('\n')


In spec, explicitly wait for pane to be activated

Signed-off-by: Katrina Uychaco <5a2824f7c8abfdcf266e6fcebe8d67d372d16fcb@github.com>

--- spec/tabs-spec.coffee
+++ spec/tabs-spec.coffee
@@ -880,12 +880,12 @@
           waitsForPromise ->
             atom.workspace.open('sample.txt', pending: true).then (o) -> editor2 = o

-          runs ->
+          waitsFor (done) ->
             pane.activateItem(editor2)
+            pane.onDidActivate(done)
             expect($(tabBar.tabForItem(editor2)).find('.title')).toHaveClass 'temp'
             triggerMouseEvent('mousedown', tabBar.tabForItem(editor2), which: 1)
             expect($(tabBar.tabForItem(editor2)).find('.title')).not.toHaveClass 'temp'
-            waits 0 # To prevent uncaught error "Pane has been destroyed"

       describe 'when opening views that do not have file paths', ->
         editor2 = null


Add spec for tabs:keep-preview-tab

Add spec for tabs:keep-preview-tab

--- spec/tabs-spec.coffee
+++ spec/tabs-spec.coffee
@@ -738,6 +738,18 @@
           expect(tabBar.find('.tab .temp').length).toBe 1
           expect(tabBar.find('.tab:eq(0) .title')).toHaveClass 'temp'

+    describe "when tabs:keep-preview-tab is trigger on the pane", ->
+      it "removes the 'temp' class", ->
+        editor1 = null
+        waitsForPromise ->
+          atom.project.open('sample.txt').then (o) -> editor1 = o
+
+        runs ->
+          pane.activateItem(editor1)
+          expect(tabBar.find('.tab .temp').length).toBe 1
+          atom.commands.dispatch(atom.views.getView(atom.workspace.getActivePane()), 'tabs:keep-preview-tab')
+          expect(tabBar.find('.tab .temp').length).toBe 0
+
     describe "when there is a temp tab already", ->
       it "it will replace an existing temporary tab", ->
         editor1 = null


Update specs for pending tab functionality

--- spec/tabs-spec.coffee
+++ spec/tabs-spec.coffee
@@ -45,10 +45,8 @@
       expect(workspaceElement.querySelectorAll('.pane > .tab-bar').length).toBe 0

     it "serializes preview tab state", ->
-      atom.config.set('tabs.usePreviewTabs', true)
-
       waitsForPromise ->
-        atom.workspace.open('sample.txt')
+        atom.workspace.open('sample.txt', pending: true)

       runs ->
         expect(workspaceElement.querySelectorAll('.tab.preview-tab .title').length).toBe 1
@@ -850,16 +848,15 @@
         expect(pane.getItems().length).toBe 1
         expect(tabBar).toHaveClass 'hidden'

-  describe "when usePreviewTabs is true in package settings", ->
+  describe "when tab is pending", ->
     beforeEach ->
-      atom.config.set("tabs.usePreviewTabs", true)
       pane.destroyItems()

     describe "when opening a new tab", ->
       it "adds tab with class 'temp'", ->
         editor1 = null
         waitsForPromise ->
-          atom.workspace.open('sample.txt').then (o) -> editor1 = o
+          atom.workspace.open('sample.txt', pending: true).then (o) -> editor1 = o

         runs ->
           pane.activateItem(editor1)
@@ -870,7 +867,7 @@
       it "removes the 'temp' class", ->
         editor1 = null
         waitsForPromise ->
-          atom.workspace.open('sample.txt').then (o) -> editor1 = o
+          atom.workspace.open('sample.txt', pending: true).then (o) -> editor1 = o

         runs ->
           pane.activateItem(editor1)
@@ -884,12 +881,10 @@
         editor2 = null

         waitsForPromise ->
-          atom.workspace.open('sample.txt').then (o) ->
+          atom.workspace.open('sample.txt', pending: true).then (o) ->
             editor1 = o
-            pane.activateItem(editor1)
-            atom.workspace.open('sample2.txt').then (o) ->
+            atom.workspace.open('sample2.txt', pending: true).then (o) ->
               editor2 = o
-              pane.activateItem(editor2)

         runs ->
           expect(editor1.isDestroyed()).toBe true
@@ -916,15 +911,13 @@

       beforeEach ->
         waitsForPromise ->
-          atom.workspace.open('sample.txt').then (o) ->
-            editor2 = o
-            pane.activateItem(editor2)
-
-        waitsForPromise ->
           atom.packages.activatePackage('settings-view').then ->
             atom.workspace.open('atom://config').then (o) ->
               settingsView = o
-              pane.activateItem(settingsView)
+
+        waitsForPromise ->
+          atom.workspace.open('sample.txt', pending: true).then (o) ->
+            editor2 = o

       it 'creates a permanent tab', ->
         expect(tabBar.tabForItem(settingsView)).toExist()
@@ -960,29 +953,23 @@
           expect($(tabBar.tabForItem(editor1)).find('.title')).not.toHaveClass 'temp'

     describe 'when switching from a preview tab to a permanent tab', ->
-      it "keeps the preview tab open", ->
-        atom.config.set("tabs.usePreviewTabs", false)
+      it "closes the preview tab", ->
         editor1 = null
         editor2 = null

         waitsForPromise ->
           atom.workspace.open('sample.txt').then (o) ->
             editor1 = o
-            pane.activateItem(editor1)
-
-        runs ->
-          atom.config.set("tabs.usePreviewTabs", true)
-
-        waitsForPromise ->
-          atom.workspace.open('sample2.txt').then (o) ->
+
+        waitsForPromise ->
+          atom.workspace.open('sample2.txt', pending: true).then (o) ->
             editor2 = o
-            pane.activateItem(editor2)

         runs ->
           pane.activateItem(editor1)
-          expect(pane.getItems().length).toBe 2
+          expect(pane.getItems().length).toBe 1
+          expect(pane.getItems()).toEqual [editor1]
           expect($(tabBar.tabForItem(editor1)).find('.title')).not.toHaveClass 'temp'
-          expect($(tabBar.tabForItem(editor2)).find('.title')).toHaveClass 'temp'

     describe "when splitting a preview tab", ->
       it "makes the tab permanent in the new pane", ->
@@ -1015,7 +1002,7 @@
           expect($(tabBar2.tabForItem(pane2.getActiveItem())).find('.title')).not.toHaveClass 'temp'

     describe "when a non-text file is opened", ->
-      it "opens a preview tab", ->
+      it "opens a permanent tab", ->
         imageView = null
         waitsForPromise ->
           atom.workspace.open('sample.png').then (o) ->
@@ -1024,7 +1011,7 @@

         runs ->
           expect(tabBar.tabForItem(imageView)).toExist()
-          expect($(tabBar.tabForItem(imageView)).find('.title')).toHaveClass 'temp'
+          expect($(tabBar.tabForItem(imageView)).find('.title')).not.toHaveClass 'temp'

     describe "when double clicking a file in the tree view", ->
       it "makes the tab for that file permanent", ->
@@ -1042,11 +1029,9 @@
           workspaceElement.querySelector('.tree-view')

         waitsForPromise ->
-          atom.workspace.open('sample.js').then (o) -> editor1 = o
-
-        runs ->
-          pane.activateItem(editor1)
-
+          atom.workspace.open('sample.js', pending: true).then (o) -> editor1 = o
+
+        runs ->
           expect($(tabBar.tabForItem(editor1)).find('.title')).toHaveClass 'temp'

           fileNode = workspaceElement.querySelector(".tree-view [data-path=\"#{path.join(__dirname, 'fixtures', 'sample.js')}\"]")
@@ -1054,6 +1039,7 @@
           fileNode.dispatchEvent(new MouseEvent('click', detail: 2, bubbles: true, cancelable: true))
           fileNode.dispatchEvent(new MouseEvent('dblclick', detail: 2, bubbles: true, cancelable: true))

+          waits(1) # wait for click handlers to get invoked
           expect($(tabBar.tabForItem(editor1)).find('.title')).not.toHaveClass 'temp'

   describe "integration with version control systems", ->


Fix typo, change "登陆" to "登录"

Fix typo, change "登陆" to "登录"

--- t9n/zh_cn.coffee
+++ t9n/zh_cn.coffee
@@ -29,8 +29,8 @@
   resetYourPassword: "重置您的密码"
   setPassword: "设置密码"
   sign: "登"
-  signIn: "登陆"
-  signin: "登陆"
+  signIn: "登录"
+  signin: "登录"
   signOut: "登出"
   signUp: "注册"
   signupCode: "注册码"
@@ -55,7 +55,7 @@
     pwdsDontMatch: "两次密码不一致"
     pwOneDigit: "密码中至少有一位数字"
     pwOneLetter: "密码中至少有一位字母"
-    signInRequired: "您必须登陆后才能查看"
+    signInRequired: "您必须登录后才能查看"
     signupCodeIncorrect: "注册码错误"
     signupCodeRequired: "必须有注册码"
     usernameIsEmail: "是用户名而不是电子邮件地址"
@@ -69,20 +69,20 @@
       #"A login handler should return a result or undefined"
       "Email already exists.": "该电子邮件地址已被使用。"
       "Email doesn't match the criteria.": "错误的的电子邮件地址。"
-      "Invalid login token": "登陆密匙错误"
-      "Login forbidden": "登陆被阻止"
+      "Invalid login token": "登录密匙错误"
+      "Login forbidden": "登录被阻止"
       #"Service " + options.service + " already configured"
       "Service unknown": "未知服务"
-      "Unrecognized options for login request": "登陆请求存在无法识别的选项"
+      "Unrecognized options for login request": "登录请求存在无法识别的选项"
       "User validation failed": "用户验证失败"
       "Username already exists.": "用户名已被占用。"
-      "You are not logged in.": "您还没有登陆。"
-      "You've been logged out by the server. Please log in again.": "您被服务器登出了。请重新登陆。"
-      "Your session has expired. Please log in again.": "会话过期，请重新登陆。"
+      "You are not logged in.": "您还没有登录。"
+      "You've been logged out by the server. Please log in again.": "您被服务器登出了。请重新登录。"
+      "Your session has expired. Please log in again.": "会话过期，请重新登录。"

       #---- accounts-oauth
-      "No matching login attempt found": "未发现对应登陆请求"
+      "No matching login attempt found": "未发现对应登录请求"

       #---- accounts-password-client
@@ -92,7 +92,7 @@
       #---- accounts-password
       "Incorrect password": "错误的密码"
       "Invalid email": "不合法的电子邮件地址"
-      "Must be logged in": "必须先登陆"
+      "Must be logged in": "必须先登录"
       "Need to set a username or email": "必须设置用户名或电子邮件地址"
       "old password format": "较老的密码格式"
       "Password may not be empty": "密码不应该为空"
@@ -111,4 +111,4 @@
       "Unknown error": "未知错误"

-T9n.map "zh-cn", zh_cn+T9n.map "zh-cn", zh_cn