Sort events by timestamp at findAll

Sort events by timestamp at findAll

--- lib/event_store/mongodb.coffee
+++ lib/event_store/mongodb.coffee
@@ -71,10 +71,22 @@
     callback null, uid

   findAllEvents: (callback) ->
-    @_find {}, callback
-
-  findAllEventsByAggregateUid: (aggregateUid, callback) ->
-    @_find aggregateUid: aggregateUid, callback
+    p = new Profiler "MongoDbEventStore#_find(db request)", @logger
+    p.start()
+    @eventCollection.find({}).sort("timestamp":1).toArray (err, items) =>
+      p.end()
+
+      if err?
+        callback err
+      else if not items?
+        callback null, []
+      else
+        @_instantiateEventsFromRows items, callback
+
+  findAllEventsByAggregateUid: (aggregateUid, order, callback) ->
+    [order, callback] = [null, order] unless callback?
+
+    @_find aggregateUid: aggregateUid, order, callback

   countAllEventsByAggregateUid: (aggregateUid, callback) ->
     @_count aggregateUid: aggregateUid, callback
@@ -174,10 +186,12 @@
       else
         @_instantiateEventsFromRows items, callback

-  _find: (params, callback) ->
+  _find: (params, order, callback) ->
+    [order, callback] = [null, order] unless callback?
+
     p = new Profiler "MongoDbEventStore#_find(db request)", @logger
     p.start()
-    @eventCollection.find(params).sort("version":1).toArray (err, items) =>
+    @eventCollection.find(params).sort("version":(order or 1)).toArray (err, items) =>
       p.end()

       if err?


Don't offer autocomplete for double-backslash

--- services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
@@ -128,6 +128,11 @@
 			if lineUpToCursor.match(/.*%.*/)
 				return
 			lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\"
+			lastTwoCharsAreBackslash = lineUpToCursor.slice(-2) == "\\\\"
+			# Don't offer autocomplete on double-backslash
+			if lastTwoCharsAreBackslash
+				@editor?.completer?.detach?()
+				return
 			commandFragment = getLastCommandFragment(lineUpToCursor)
 			commandName = getCommandNameFromFragment(commandFragment)
 			if commandName in ['begin', 'end']


Fix #267

Fix #267

--- files.coffee
+++ files.coffee
@@ -630,14 +630,18 @@
       @_preCollection = new Mongo.Collection '__pre_' + @collectionName
       @_preCollection._ensureIndex {'createdAt': 1}, {expireAfterSeconds: @continueUploadTTL, background: true}
       _preCollectionCursor = @_preCollection.find {}
-      _preCollectionCursor.observeChanges removed: (_id) ->
+      _preCollectionCursor.observe removed: (doc) ->
         # Free memory after upload is done
         # Or if upload is unfinished
-        console.info "[FilesCollection] [_preCollectionCursor.observeChanges] [removed]: #{_id}" if self.debug
-        if self._currentUploads?[_id]
-          self._currentUploads[_id].end()
-          self._currentUploads[_id].abort()
-          delete self._currentUploads[_id]
+        console.info "[FilesCollection] [_preCollectionCursor.observe] [removed]: #{doc._id}" if self.debug
+        if self._currentUploads?[doc._id]
+          self._currentUploads[doc._id].end()
+          self._currentUploads[doc._id].abort()
+          delete self._currentUploads[doc._id]
+
+        unless doc.isFinished
+          console.info "[FilesCollection] [_preCollectionCursor.observe] [removeUnfinishedUpload]: #{doc.file.path}" if self.debug
+          fs.unlink doc.file.path, NOOP
         return

       @_createStream = (_id, path, opts) ->
@@ -1046,16 +1050,18 @@
         cb and cb error
         console.error '[FilesCollection] [Upload] [_finishUpload] Error:', error if self.debug
       else
-        self._preCollection.remove {_id: opts.fileId}, (error) ->
-          if error
-            cb and cb error
-            console.error '[FilesCollection] [Upload] [_finishUpload] Error:', error if self.debug
-          else
-            result._id = _id
-            console.info "[FilesCollection] [Upload] [finish(ed)Upload] -> #{result.path}" if self.debug
-            self.onAfterUpload and self.onAfterUpload.call self, result
-            self.emit 'afterUpload', result
-            cb and cb null, result
+        self._preCollection.update {_id: opts.fileId}, {$set: {isFinished: true}}, () ->
+          self._preCollection.remove {_id: opts.fileId}, (error) ->
+            if error
+              cb and cb error
+              console.error '[FilesCollection] [Upload] [_finishUpload] Error:', error if self.debug
+            else
+              result._id = _id
+              console.info "[FilesCollection] [Upload] [finish(ed)Upload] -> #{result.path}" if self.debug
+              self.onAfterUpload and self.onAfterUpload.call self, result
+              self.emit 'afterUpload', result
+              cb and cb null, result
+            return
           return
       return
     return


Call native method on WebContents instead of sendActionToFirstResponder

Fixes #6587

--- src/window-event-handler.coffee
+++ src/window-event-handler.coffee
@@ -114,23 +114,23 @@

     @handleNativeKeybindings()

-  # Wire commands that should be handled by the native menu
-  # for elements with the `.native-key-bindings` class.
+  # Wire commands that should be handled by Chromium for elements with the
+  # `.native-key-bindings` class.
   handleNativeKeybindings: ->
     menu = null
     bindCommandToAction = (command, action) =>
       @subscribe $(document), command, (event) ->
         if event.target.webkitMatchesSelector('.native-key-bindings')
-          menu ?= require('remote').require('menu')
-          menu.sendActionToFirstResponder(action)
+          webContents = require('remote').getCurrentWindow().webContents
+          webContents[action]()
         true

-    bindCommandToAction('core:copy', 'copy:')
-    bindCommandToAction('core:paste', 'paste:')
-    bindCommandToAction('core:undo', 'undo:')
-    bindCommandToAction('core:redo', 'redo:')
-    bindCommandToAction('core:select-all', 'selectAll:')
-    bindCommandToAction('core:cut', 'cut:')
+    bindCommandToAction('core:copy', 'copy')
+    bindCommandToAction('core:paste', 'paste')
+    bindCommandToAction('core:undo', 'undo')
+    bindCommandToAction('core:redo', 'redo')
+    bindCommandToAction('core:select-all', 'selectAll')
+    bindCommandToAction('core:cut', 'cut')

   onKeydown: (event) ->
     atom.keymaps.handleKeyboardEvent(event)


use a separate parameter for doc and project flush

--- app/coffee/HistoryManager.coffee
+++ app/coffee/HistoryManager.coffee
@@ -39,13 +39,15 @@
 			else if res.statusCode < 200 and res.statusCode >= 300
 				logger.error { project_id }, "project history api returned a failure status code: #{res.statusCode}"

-	FLUSH_EVERY_N_OPS: 100
+	FLUSH_DOC_EVERY_N_OPS: 100
+	FLUSH_PROJECT_EVERY_N_OPS: 500
+
 	recordAndFlushHistoryOps: (project_id, doc_id, ops = [], doc_ops_length, project_ops_length, callback = (error) ->) ->
 		if ops.length == 0
 			return callback()

 		if Settings.apis?.project_history?.enabled
-			if HistoryManager._shouldFlushHistoryOps(project_ops_length, ops, HistoryManager.FLUSH_EVERY_N_OPS)
+			if HistoryManager._shouldFlushHistoryOps(project_ops_length, ops, HistoryManager.FLUSH_PROJECT_EVERY_N_OPS)
 				# Do this in the background since it uses HTTP and so may be too
 				# slow to wait for when processing a doc update.
 				logger.log { project_ops_length, project_id }, "flushing project history api"
@@ -53,7 +55,7 @@

 		HistoryRedisManager.recordDocHasHistoryOps project_id, doc_id, ops, (error) ->
 			return callback(error) if error?
-			if HistoryManager._shouldFlushHistoryOps(doc_ops_length, ops, HistoryManager.FLUSH_EVERY_N_OPS)
+			if HistoryManager._shouldFlushHistoryOps(doc_ops_length, ops, HistoryManager.FLUSH_DOC_EVERY_N_OPS)
 				# Do this in the background since it uses HTTP and so may be too
 				# slow to wait for when processing a doc update.
 				logger.log { doc_ops_length, doc_id, project_id }, "flushing track changes api"


s Implement simple DSL for scripting

s Implement simple DSL for scripting

--- app/components/Scriptable.coffee
+++ app/components/Scriptable.coffee
@@ -42,9 +42,7 @@
         if script[0] instanceof Array
           debugger
           promises = ( @addScripts s for s in script )
-          window.promises = promises
           promise = Q.all(promises)
-          window.promise = promise
           promise.then =>
             debugger
             console.debug arguments


testing auth

testing auth

--- app/controllers/fb/FbDemo.coffee
+++ app/controllers/fb/FbDemo.coffee
@@ -16,14 +16,16 @@

 muninn = require('muninn')

+
 class FbDemo extends muninn.core.Controller

-  fb = require('fb')
+  graph = require('fbgraph')

   @routes =
     '/demo':            'indexAction'
-    '/login/callback':  'loginCallback'
+    '/login/callback':  'authAction'
     '/logout':          'logoutAction'
+    '/loggedin':        'loginAction'

   #
@@ -32,48 +34,48 @@
   # @return [None]
   #
   indexAction: ->
-    accessToken = @req.session.access_token
-    if not accessToken
-      @res.render 'demo/index',
-        title: 'Express',
-        loginUrl: fb.getLoginUrl(scope: 'user_about_me')
-    else
-      @res.render 'demo/menu'
+    console.log "===================="
+    console.log @req.query.code
+    console.log "===================="
+
+    @res.render 'demo/index',
+      title: 'Express',
+      loginUrl: muninn.config.fb.redirectUri
+

   #
-  # loginCallback
+  # authAction
   #
   # @return [None]
   #
-  loginCallback: ->
+  authAction: ->

-    if @req.query.error
-      return @res.send 'login-error ' + @req.query.error_description
-    else if not @req.query.code
-      return @res.redirect '/demo'
+    if not @req.query.code

-    fb.authenticate @req.query.code, ($err, result) =>
+      authUrl = graph.getOauthUrl
+        client_id:     muninn.config.fb.appId
+        redirect_uri:  muninn.config.fb.redirectUri
+        scope:         'user_about_me'

-      if $err then return muninn.logMessage('error', String($err)) if muninn.showError($err)
+      if not @req.query.error # checks whether a user denied the app facebook login/permissions
+        console.log 'redirect '+authUrl
+        @res.redirect authUrl
+      else # req.query.error == 'access_denied'
+        console.log 'access denied'
+        @res.send 'access denied'
+      return

-      @req.session.access_token    = result.access_token
-      @req.session.expires         = result.expires or 0
-
-      if @req.query.state
-        parameters                = JSON.parse(@req.query.state)
-        parameters.access_token   = @req.session.access_token
-
-        fb.api '/me/' + config.fb.appNamespace +':eat', 'post', parameters , (result) =>
-#            console.log(result)
-          if not result || result.error
-            return @res.send(500, result or 'error')
+    graph.authorize
+      client_id:      muninn.config.fb.appId
+      redirect_uri:   muninn.config.fb.redirectUri
+      client_secret:  muninn.config.fb.appSecret
+      code:           @req.query.code
+    , (err, f) =>
+        @res.redirect '/loggedin'

-        return @res.redirect('/demo')
-      else
-        return @res.redirect('/demo')
-
-
+  loginAction: ->
+    @res.render 'demo/menu'

   #
   # logoutAction


Document resin/config

Document resin/config

--- lib/resin/config/config.coffee
+++ lib/resin/config/config.coffee
@@ -1,8 +1,23 @@
 fs = require('fs')
 errors = require('../errors/errors')

+# Read JSON configuration file
+#
+# @private
+#
 # User config loading should be sync, as we need to
 # extend this module with the result before exporting
+#
+# @param {String} configFile configuration file path
+# @return {Object} Parsed configuration file
+#
+# @throw {InvalidConfigFile} Will throw an error if file doesn't exist
+# @throw {InvalidConfigFile} Will throw an error if file is not JSON
+#
+# @example Read config file
+#		contents = resin.config.loadUserConfig('/Users/me/resin-custom.json')
+#		console.log(contents.remoteUrl)
+#
 exports.loadUserConfig = (configFile) ->
 	return if not fs.existsSync(configFile)


Dispatch the "script:run" command on the workspace view

--- lib/script-options-view.coffee
+++ lib/script-options-view.coffee
@@ -80,3 +80,7 @@
   run: ->
     @saveOptions()
     @toggleScriptOptions('hide')
+    atom.commands.dispatch @workspaceView(), 'script:run'
+
+  workspaceView: ->
+    atom.views.getView(atom.workspace)


Special case a certain word

Special case a certain word

--- lib/spell-check-handler.coffee
+++ lib/spell-check-handler.coffee
@@ -8,6 +8,7 @@
   for line in text.split('\n')
     while matches = wordRegex.exec(line)
       word = matches[1]
+      continue if word in ['GitHub', 'github']
       continue unless SpellChecker.isMisspelled(word)

       startColumn = matches.index + matches[0].length - word.length


Clean up keymaps

Clean up keymaps

--- keymaps/git-experiment.cson
+++ keymaps/git-experiment.cson
@@ -1,25 +1,18 @@
-# Keybindings require three things to be fully defined: A selector that is
-# matched against the focused element, the keystroke and the command to
-# execute.
-#
-# Below is a basic keybinding which registers on all platforms by applying to
-# the root workspace element.
+'atom-workspace':
+  'cmd-shift-c': 'git:view-and-commit-changes'

-# For more detailed documentation see
-# https://atom.io/docs/latest/advanced/keymaps
-'atom-workspace':
-  'cmd-shift-h': 'git:view-history'
-  'cmd-shift-c': 'git:view-and-commit-changes'
-  'cmd-ctrl-shift-f': 'git:log-focused'
-  # 'cmd-ctrl-b': 'git:checkout-branch'
-  # 'cmd-ctrl-shift-b': 'git:create-branch'
+'.git-diff-container':
+  '/': 'git:toggle-selection-mode'
+  'a': 'git:select-active-lines'
+  'shift-up': 'git:expand-selection-up'
+  'shift-down': 'git:expand-selection-down'
+  'escape': 'git:clear-selections'
+  'tab': 'git:focus-commit-message'
+  'o': 'git:open-file-to-line'

 # 'git-changes-view .data atom-text-editor':
 #   'cmd-enter': 'core:confirm'
 #   'escape': 'core:focus-changes-list'
-
-'atom-text-editor':
-  'cmd-ctrl-shift-d': 'git:open-file-diff'

 '.git-file-status-list:focus':
   'escape': 'git:close-commit-panel'
@@ -33,15 +26,6 @@
   'cmd-enter': 'git:commit'
   'ctrl-enter': 'git:commit'

-'git-diff-view':
-  '/': 'git:toggle-selection-mode'
-  'a': 'git:select-active-lines'
-  'shift-up': 'git:expand-selection-up'
-  'shift-down': 'git:expand-selection-down'
-  'escape': 'git:clear-selections'
-  'tab': 'git:focus-commit-message'
-  'o': 'git:open-file-to-line'
-
 'git-commit-list-view':
   'right': 'git:focus-commit-details'


turned on ace autocompletion

turned on ace autocompletion

--- src/angles-editor-view.coffee
+++ src/angles-editor-view.coffee
@@ -197,12 +197,21 @@
       @$editor.getSession().on 'change', (e) => @dispatcher.trigger 'editor:change', e
       @$editor.getSession().setMode "ace/mode/xml"

+      # Load ace modules #
+
+      # ext_language_tools for autocompletion
+      ace.config.loadModule 'ace/ext/language_tools', () ->
+        _this.$editor.setOptions
+          enableBasicAutocompletion: true,
+          enableSnippets: true
+
       @$editor.commands.addCommand
         name: 'contextHelp'
         bindKey:
-          win: 'Ctrl-Space'
-          mac: 'Ctrl-Space'
+          win: 'Ctrl-.'
+          mac: 'Ctrl-.'
         exec: (editor) =>
+            console.log editor
             cursor = editor.getCursorPosition()
             line = editor.session.getDocument().getLine(cursor.row)
             sub = line.substring(0, cursor.column)


minor

minor

--- client/app/Applications/Groups.kdapplication/Views/GroupsListItemView.coffee
+++ client/app/Applications/Groups.kdapplication/Views/GroupsListItemView.coffee
@@ -101,7 +101,7 @@

     # FIXME: SY
     # instantiateListItems doesnt fire by default
-    if group.slug is "koding"
+    unless group.slug is "koding"
       group.fetchMembers (err, members)=>
         if err then warn err
         else if members


group read me: button display logic changed, get rid of markdown encoding raw html default string

--- client/landing/app/Applications/Groups.kdapplication/Views/readmeview.coffee
+++ client/landing/app/Applications/Groups.kdapplication/Views/readmeview.coffee
@@ -80,16 +80,19 @@
     group.fetchReadme (err, readme)=>
       if not err and readme?
         partial = readme.content or getDefaultGroupReadme group.title
-        group.canEditGroup (err, allowed)=>
-          @readmeEditButton.show() if allowed
+        # group.canEditGroup (err, allowed)=>
+        #   @readmeEditButton.show() if allowed
       else
-        partial =
+        partialHTML =
           if err?.message then err.message
           else @getDefaultGroupReadme group.title

+      group.canEditGroup (err, allowed)=>
+        @readmeEditButton.show() if allowed
+
       #SA: is this line being used anywhere, this overrides the logic above
       #@readme = readme?.content or partial
-      @readmeView.updatePartial @utils.applyMarkdown partial
+      @readmeView.updatePartial partialHTML or @utils.applyMarkdown partial
       @highlightCode()
       JView::viewAppended.call @
       @emit "readmeReady"


de-parens

de-parens

--- plugins/project/project.coffee
+++ plugins/project/project.coffee
@@ -23,14 +23,14 @@
       $('#project .active').removeClass 'active'
       el = $(event.currentTarget)
       path = decodeURIComponent el.attr 'path'
-      if File.isDirectory(path)
+      if File.isDirectory path
         if el.hasClass 'open'
           el.removeClass 'open'
           el.children("ul").remove()
         else
           el.addClass 'open'
-          list = @createList(path)
-          el.append("
#{list}
")
+          list = @createList path
+          el.append "
#{list}
"
       else
         el.addClass 'active'
         activeWindow.open path
@@ -50,7 +50,7 @@
     @dir = dir = File.workingDirectory()
     $('#project .cwd').text _.last dir.split '/'
     $('#project li').remove()
-    $('#project .files').append (@createList dir)
+    $('#project .files').append @createList dir

   createList: (dir) ->
     files = File.list dir


KodingRouter: Ignore queries in route while extracting app name from it

--- client/Main/kodingrouter.coffee
+++ client/Main/kodingrouter.coffee
@@ -49,7 +49,7 @@
     appManager = KD.getSingleton 'appManager'
     frags      = route.split '/'
     name       = frags[1] or ''
-    name       = if name is 'Develop' and frags.length > 2 then frags[2] else name
+    name       = (name.split '?')[0]

     log 'handlingRoute', route, 'for the', name, 'app'
     if appManager.isAppAvailable name


Document cssAnimate

Document cssAnimate

--- lib/assets/javascripts/up/util.js.coffee
+++ lib/assets/javascripts/up/util.js.coffee
@@ -204,12 +204,32 @@
     else
       memo

-  cssAnimate = ($element, lastFrame, opts) ->
+  ###*
+  Animates the given element's CSS properties using CSS transitions.
+
+  @method up.util.cssAnimate
+  @param {Element|jQuery|String} elementOrSelector
+    The element to animate.
+  @param {Object} lastFrame
+    The CSS properties that should be transitioned to.
+  @param {Number} [opts.duration=300]
+    The duration of the animation, in milliseconds.
+  @param {Number} [opts.delay=0]
+    The delay before the animation starts, in milliseconds.
+  @param {String} [opts.easing='ease']
+    The timing function that controls the animation's acceleration.
+    See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function)
+    for a list of pre-defined timing functions.
+  @return
+    A promise for the animation's end.
+  ###
+  cssAnimate = (elementOrSelector, lastFrame, opts) ->
     opts = options(opts,
       duration: 300,
       delay: 0,
       easing: 'ease'
     )
+    $element = $(elementOrSelector)
     deferred = $.Deferred()
     transition =
       'transition-property': Object.keys(lastFrame).join(', ')


simplifying this down a bit

simplifying this down a bit

--- app/js/app.coffee
+++ app/js/app.coffee
@@ -7,11 +7,9 @@

 App.HelloController = Ember.ObjectController.extend()

-App.HelloRoute = Ember.Route.extend
-  setupController: (controller) ->
-    model = App.Hello.find(1)
-    controller.set('content', model)
+App.IndexRoute = Ember.Route.extend
+  redirect: ->
+    this.transitionTo('hellos/1')

 App.Router.map ->
-  @route("hello", path: '/')
-
+  @resource('hello', { path: '/hellos/:hello_id' });


Fix unit tests

Fix unit tests

--- services/web/test/UnitTests/coffee/Templates/TemplatesControllerTests.coffee
+++ services/web/test/UnitTests/coffee/Templates/TemplatesControllerTests.coffee
@@ -12,7 +12,10 @@

 	beforeEach ->
 		@request = sinon.stub()
-		@request.returns pipe:->
+		@request.returns {
+			pipe:->
+			on:->
+		}
 		@fs = {
 			unlink : sinon.stub()
 			createWriteStream : sinon.stub().returns(on:(_, cb)->cb())


Make sure current user is checked on load

--- app/api/auth.coffee
+++ app/api/auth.coffee
@@ -20,7 +20,7 @@
   ///#{NAME_ATTR}\s*#{CONTENT_ATTR}|#{CONTENT_ATTR}\s*#{NAME_ATTR}///

 module.exports = new Model
-  currentUser: Promise.resolve null
+  currentUser: null
   bearerToken: ''

   _getAuthToken: ->


client: export component lab

client: export component lab

--- client/builder/lib/index.coffee
+++ client/builder/lib/index.coffee
@@ -206,6 +206,8 @@
       return acc
     , {}

+    rewriteMap['component-lab'] = "./component-lab/"
+
     modules = manifests.map (manifest) ->
       name = manifest.name
       if name is 'ide'


Add sorttable quick utility

Add sorttable quick utility

--- SingularityUI/app/utils.coffee
+++ SingularityUI/app/utils.coffee
@@ -21,4 +21,7 @@
             model = collection.get(id) ? model
         return model

+    setupSortableTables: ->
+        $('table.sortTable').each -> sorttable.makeSortable @
+
 module.exports = new Utils


add arenaPassport

add arenaPassport

--- lib/setup.coffee
+++ lib/setup.coffee
@@ -61,6 +61,9 @@
     domain: COOKIE_DOMAIN
     key: SESSION_COOKIE_KEY
     maxage: SESSION_COOKIE_MAX_AGE
+  app.use arenaPassport _.extend config,
+    CurrentUser: CurrentUser
+    SECURE_ARTSY_URL: API_URL

   # Mount apps
   app.use require "../apps/root"


Swap out direct analytics call.

Swap out direct analytics call.

[#51845303]

--- client/helpers/spendflow.coffee
+++ client/helpers/spendflow.coffee
@@ -86,4 +86,4 @@
     identity.username = currentUser.username if currentUser.username
     identity.created = moment(currentUser.createdAt).toDate() if currentUser.createdAt

-    analytics.identify Meteor.userId(), identity
+    SpendflowStats.identify Meteor.userId(), identity


Collaboration: Emit CollaborationStarted event to status bar.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -992,7 +992,9 @@
     @getView().addSubView @chat = new IDE.ChatView { @rtm }, channel
     @chat.show()

-    @on 'RTMIsReady', => @chat.settingsPane.createParticipantsList()
+    @on 'RTMIsReady', =>
+      @chat.settingsPane.createParticipantsList()
+      @statusBar.emit 'CollaborationStarted'

   createChatPane: ->


Collaboration: Added getCollaboration method.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -1524,6 +1524,17 @@

     @chat.emit 'ParticipantLeft', username
     @statusBar.emit 'ParticipantLeft', username
+
+
+  getCollaborationData: (callback = noop) =>
+
+    collaborationData =
+      watchMap        : @myWatchMap.values()
+      sessionHost     : @collaborationHost or KD.nick()
+
+    callback collaborationData
+
+
   kickParticipant: (account) ->

     options      =


AppController: Make local snapshot resurrect logic a separate method.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -306,9 +306,7 @@
         else
           snapshot = @localStorageController.getValue @getWorkspaceSnapshotName()

-          if snapshot
-            for key, value of snapshot
-              @createPaneFromChange value, yes
+          if snapshot then @resurrectLocalSnapshot snapshot
           else
             @ideViews.first.createEditor()
             @ideViews.last.createTerminal { machine }
@@ -838,6 +836,11 @@
       @createNewTerminal { machine }
       @setActiveTabView @ideViews.first.tabView
       @fakeViewsDestroyed = yes
+
+  resurrectLocalSnapshot: (snapshot) ->
+
+    for key, value of snapshot
+      @createPaneFromChange value, yes  if value

   toggleFullscreenIDEView: ->


Fixed comment error

Fixed comment error

--- models/game.coffee
+++ models/game.coffee
@@ -7,7 +7,7 @@

   addWord: (player, word) ->

-    //Reject word if already guessed by player
+    #Reject word if already guessed by player
     if word in player.words
       return


Fix contentFrame bug

Fix contentFrame bug

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -274,6 +274,7 @@
 				@frame = Utils.convertPoint(frame, null, @superLayer)

 	contentFrame: ->
+		return {x:0, y:0, width:0, height:0} unless @subLayers.length
 		Utils.frameMerge(_.pluck(@subLayers, "frame"))

 	centerFrame: ->


check directory exists and bail out on error

--- services/clsi/app/coffee/CompileManager.coffee
+++ services/clsi/app/coffee/CompileManager.coffee
@@ -202,16 +202,25 @@
 			callback null, CompileManager._parseSynctexFromPdfOutput(stdout, base_dir)

 	_checkFileExists: (path, callback = (error) ->) ->
-		fs.stat path, (err, stats) ->
-			return callback(err) if err?
-			return callback(new Error("not a file")) if not stats.isFile()
-			callback()
+		synctexDir = Path.dirname(path)
+		synctexFile = Path.join(synctexDir, "output.synctex.gz")
+		fs.stat synctexDir, (error, stats) ->
+			if error?.code is 'ENOENT'
+				return callback(new Error("called synctex with no output directory"))
+			return callback(error) if error?
+			fs.stat synctexFile, (error, stats) ->
+				if error?.code is 'ENOENT'
+					return callback(new Error("called synctex with no output file"))
+				return callback(error) if error?
+				return callback(new Error("not a file")) if not stats?.isFile()
+				callback()

 	_runSynctex: (args, callback = (error, stdout) ->) ->
 		bin_path = Path.resolve(__dirname + "/../../bin/synctex")
 		seconds = 1000
 		outputFilePath = args[1]
-		CompileManager._checkFileExists outputFilePath, (err) ->
+		CompileManager._checkFileExists outputFilePath, (error) ->
+			return callback(error) if error?
 			if Settings.clsi?.synctexCommandWrapper?
 				[bin_path, args] = Settings.clsi?.synctexCommandWrapper bin_path, args
 			child_process.execFile bin_path, args, timeout: 10 * seconds, (error, stdout, stderr) ->


changed style of async jobs to map to use underscore which is like we use in the rest of the stack. Also removed a few => binds which are not needed

--- services/docstore/app/coffee/DocManager.coffee
+++ services/docstore/app/coffee/DocManager.coffee
@@ -70,18 +70,17 @@
 			else if !docs?
 				return callback new Errors.NotFoundError("No docs for project #{project_id}")

-			jobs = for doc in docs
-				do (doc) =>
-					(cb) =>
-						logger.log project_id: project_id, doc_id: doc._id, "sending doc to s3"
-						options = buildS3Options(doc.lines, project_id+"/"+doc._id)
-						request.put options, (err, res)->
-							if err? || res.statusCode != 200
-								logger.err err:err, res:res, "something went wrong archiving doc in aws"
-								cb(err)
-							MongoManager.markDocAsArchived doc._id, (error) ->
-								return cb(error) if error?
-								cb()
+			jobs = _.map docs, (doc) ->
+				(cb)->
+					logger.log project_id: project_id, doc_id: doc._id, "sending doc to s3"
+					options = buildS3Options(doc.lines, project_id+"/"+doc._id)
+					request.put options, (err, res)->
+						if err? || res.statusCode != 200
+							logger.err err:err, res:res, "something went wrong archiving doc in aws"
+							cb(err)
+						MongoManager.markDocAsArchived doc._id, (error) ->
+							return cb(error) if error?
+							cb()

 			async.series jobs, callback

@@ -92,21 +91,20 @@
 			else if !docs?
 				return callback new Errors.NotFoundError("No docs for project #{project_id}")

-			jobs = for doc in docs
-				do (doc) =>
+			jobs = _.map docs, (doc) ->
+				(cb)->
 					if !doc.inS3?
-						(cb) => cb()
+						return cb()
 					else
-						(cb) =>
-							logger.log project_id: project_id, doc_id: doc._id, "getting doc from s3"
-							options = buildS3Options(true, project_id+"/"+doc._id)
-							request.get options, (err, res, lines)->
-								if err? || res.statusCode != 200
-									logger.err err:err, res:res, "something went wrong unarchiving doc from aws"
-									cb(err)
-								MongoManager.upsertIntoDocCollection project_id, doc._id.toString(), lines, (error) ->
-									return cb(error) if error?
-									cb()
+						logger.log project_id: project_id, doc_id: doc._id, "getting doc from s3"
+						options = buildS3Options(true, project_id+"/"+doc._id)
+						request.get options, (err, res, lines)->
+							if err? || res.statusCode != 200
+								logger.err err:err, res:res, "something went wrong unarchiving doc from aws"
+								cb(err)
+							MongoManager.upsertIntoDocCollection project_id, doc._id.toString(), lines, (error) ->
+								return cb(error) if error?
+								cb()

 			async.series jobs, callback


Work around XTK bug in selectOverlay.

Work around XTK bug in selectOverlay.

--- app/javascripts/image.coffee
+++ app/javascripts/image.coffee
@@ -13,7 +13,7 @@
     # * xtkVolume - the XTK volume object
     # * load() - read the image file
     # * open(element) - render the image
-    # * hasLabelMap() - whether there is an associated label map file
+    # * isOverlayVisible() - whether there is an associated label map file
     # * selectOverlay() - show the overlay
     # * deselectOverlay() - hide the overlay
     #
@@ -41,14 +41,14 @@
         #   a color table
         Object.defineProperty this, 'overlays',
           get: ->
-            @volume.session.overlays
+            @volume.container.session.overlays

       # Transfers the file content to the data properties.
       # The image state loading flag is set to true while the
       # files are being read.
       #
-      # @returns a promise which resolves when the file reads
-      #   are completed
+      # @returns a promise which resolves to this image object
+      #   when the file is loaded
       load: ->
         # Set the loading flag.
         @state = Image.STATES.LOADING
@@ -64,7 +64,15 @@
           @xtkVolume.filedata = data
           # Set the state to loaded.
           @state = Image.STATES.LOADED
-
+          # Return the loaded image.
+          this
+
+      isLoaded: ->
+        @state == Image.STATES.LOADED
+
+      isLoading: ->
+        @state == Image.STATES.LOADING
+
       # Renders the image in the given parent element.
       #
       # @param element the Angular jQueryLite element
@@ -89,12 +97,12 @@
         # Render the image.
         renderer.render()

-      # Returns whether the volume has a label map overlay.
-      # This function is required by the image-controls.jade work-around for
-      # a XTK bug.
-      hasLabelMap: ->
-        @xtkVolume._labelmap and @xtkVolume._labelmap._children and
-        @xtkVolume._labelmap._children.length
+      # Returns whether the XTK volume has a visible label map. This
+      # function is required to work around the XTK bug described in
+      # image-controls.jade.
+      isOverlayVisible: ->
+        @xtkVolume._labelmap? and @xtkVolume._labelmap._children? and
+        @xtkVolume._labelmap._children.length and @xtkVolume._labelmap.visible

       # Deselects an existing overlay as follows:
       # * If the image volume has a label map, then the label map visible
@@ -104,7 +112,7 @@
       # @param image the selected PK modeling parameter name, or 'none' to
       #  remove an existing overlay
       deselectOverlay: ->
-         @xtkVolume.labelmap.visible = false if @xtkVolume.labelmap?
+        @xtkVolume.labelmap.visible = false if @isOverlayVisible()

       # Changes the overlay label map and color lookup table as follows:
       # * Fetch the overlay files.
@@ -114,23 +122,78 @@
       # @param labelMap the selected label map {filename, colorTable}
       #   object
       selectOverlay: (labelMap) ->
+        # Note: XTK labelmaps are treacherous territory; proceed with caution.
+        # Specifically, XTK places the renderer in the browser event loop, so
+        # that XTK renders and rerenders continuously and asynchronously.
+        # Hopefully, the XTK render function checks for a state change and
+        # only schedules a browser paint if necessary. Whether this state
+        # change check is performed is unknown.
+        #
+        # Furthermore, XTK implicitly expects that its volume is complete and
+        # consistent when it is rendered. For a labelmap, XTK checks the XTK
+        # volume._labelmap. If it exists and the labelmap visible flag is set,
+        # then XTK expects the labelmap data to be fully loaded and renders
+        # the labelmap.
+        #
+        # However, simply checking the existing XTK volume.labelmap
+        # property automatically creates the XTK volume._labelmap object.
+        # Furthermore, the newly created empty XTK volume._labelmap.visible
+        # flag is set to true by default. Thus, a naive implementation that
+        # checks for an existing labelmap and then asynchronously loads the
+        # labelmap file results in the following XTK render error:
+        #
+        #   Uncaught TypeError: Cannot read property '_id' of null
+        #
+        # The work-around is as follows:
+        # * If checking for an existing labelmap, bypass accessing the
+        #   XTK volume.labelmap property and check the XTK volume._labelmap
+        #   variable instead.
+        # * If an overlay is selected which differs from the existing overlay,
+        #   then set the XTK volume.labelmap.visible property to false before
+        #   anything else. Although referencing the XTK volume.labelmap property
+        #   creates the XTK volume._labelmap object with incomplete and
+        #   inconsistent content, the visible flag set to false prevents XTK
+        #   from attempting to render the incomplete labelmap.
+        # * Set the XTK volume.labelmap content as appropriate, e.g. the file
+        #   and colortable.file.
+        # * Load the label map and color table asynchronously.
+        # * When both are loaded, then finally set the XTK volume.labelmap.visible
+        #   property to true. Setting visible to true triggers a repaint with the
+        #   overlay.
+
+        # The XTK volume labelmap file, or null if there is no labelmap.
+        xtkLabelMapFile = @xtkVolume._labelmap.file if @xtkVolume._labelmap?
+        # If the label map was not changed from the last value, then we only
+        # need to set the visible flag.
+        if xtkLabelMapFile is labelMap.filename
+          @xtkVolume.labelmap.visible = true
+          return
+
+        # The label map must have a color table.
+        if not labelMap.colorTable?
+          throw new ValueError("The label map is missing a color table:" +
+                               " #{ labelMap.filename } ")
+
+        # Set the XTK volume labelmap visible flag to false.
+        # See the function Note comment above.
+        @xtkVolume.labelmap.visible = false
         # Set the volume label map file name property.
         @xtkVolume.labelmap.file = labelMap.filename
         # Set the volume color table file name property.
-        @xtkVolume.labelmap.colortable.file = labelMap.colorTable.filename
+        @xtkVolume.labelmap.colortable.file = labelMap.colorTable
+

         # Retrieve the overlay layer map and color table.
-        loadLabelMap = File.readBinary(labelMap.filename).then (data) =>
+        loadLabelMap = File.readBinary(labelMap.filename).then (data) ->
           # Set the volume label map data property.
-          @xtkVolume.labelmap.filedata  = data
-        loadColorTable = File.readBinary(labelMap.colorTable.filename)
-          .then (data) =>
-            # Set the volume color table data property.
-            @xtkVolume.labelmap.colortable.filedata = data
+          @xtkVolume.labelmap.filedata = data
+        loadColorTable = File.readBinary(labelMap.colorTable).then (data) ->
+          # Set the volume color table data property.
+          @xtkVolume.labelmap.colortable.filedata = data

         # Join the two promises into a single promise.
         loaded = $q.all(loadLabelMap, loadColorTable)
-        loaded.then =>
+        loaded.then ->
           # Turn on the label map. This triggers a redisplay of the volume
           # with the new overlay.
           @xtkVolume.labelmap.visible = true


Use boolean instead string

Use boolean instead string

--- client/landing/app/Applications/Account.kdapplication/account/accSettingsPersEmailNotifications.coffee
+++ client/landing/app/Applications/Account.kdapplication/account/accSettingsPersEmailNotifications.coffee
@@ -32,8 +32,9 @@
       field.formView.addSubView field.switch = new KDMultipleChoice
         cssClass      : 'dark'
         labels        : labels
-        defaultValue  : field.current
+        defaultValue  : if field.current is on then 'on' else 'off'
         callback      : (state)->
+          state = if state is 'on' then on else off
           prefs = {}

           prefs[@getData()] = state
@@ -60,7 +61,7 @@

     toggleFieldStates = (state)->
       for flag, field of fields when flag isnt 'global'
-        if state is 'off'
+        if state is false
           field.formView.hide()
         else
           field.formView.show()


webapp/top banner typo in settings

webapp/top banner typo in settings

--- src/smc-webapp/r_account.cjsx
+++ src/smc-webapp/r_account.cjsx
@@ -1010,7 +1010,7 @@
                 ref      = 'show_global_info'
                 onChange = {(e)=>@on_change('show_global_info', e.target.checked)}
             >
-                Show global information: if enabled, a dismissable banner is visible on top
+                Show global information: if enabled, a dismissible banner is visible on top
             </Checkbox>
             <LabeledRow label='Default file sort'>
                 <SelectorInput


require plain colorpicker file now that .cjsx is gone

--- src/smc-webapp/r_account.cjsx
+++ src/smc-webapp/r_account.cjsx
@@ -27,7 +27,7 @@

 {SiteName} = require('./customize')

-{ColorPicker} = require('./colorpicker.tsx')
+{ColorPicker} = require('./colorpicker')
 {Avatar} = require('./other-users')

 md5 = require('md5')


syncdoc: skip undefined codemirrors in close

--- src/smc-webapp/syncdoc.coffee
+++ src/smc-webapp/syncdoc.coffee
@@ -542,6 +542,7 @@
         @_syncstring?.close()
         # TODO -- this doesn't work...
         for cm in [@codemirror, @codemirror1]
+            continue if not cm?
             cm.setOption("mode", "text/x-csrc")
             cmElem = cm.getWrapperElement()
             cmElem.parentNode.removeChild(cmElem)


Fix events when minified

Fix events when minified

`constroctor.name` does not work when the JS gets minified.

--- app/js/views/timeline.coffee
+++ app/js/views/timeline.coffee
@@ -29,7 +29,11 @@
       @collection.select unread, scroll: true

   viewFor: (model) ->
-    view = App.Views["Timeline" + model.constructor.name] || App.Views.Comment
+    view = if model instanceof App.Models.Event
+      App.Views["TimelineEvent"]
+    else
+      App.Views.Comment
+
     new view(model: model)

   selectNext: ->


Update levelBus to fix a unimporant bug

Update levelBus to fix a unimporant bug

set chagnedSessionProperties.code to false doesn't work

--- app/lib/LevelBus.coco.coffee
+++ app/lib/LevelBus.coco.coffee
@@ -264,7 +264,7 @@
     if @changedSessionProperties.code
       @updateSessionConcepts()
       spellMap = @changedSessionProperties.code
-      @changedSessionProperties.code = false
+      delete @changedSessionProperties.code
     Backbone.Mediator.publish 'level:session-will-save', session: @session
     patch = {}
     patch[prop] = @session.get(prop) for prop of @changedSessionProperties


Update copy test to expect SHA

Update copy test to expect SHA

--- spec/github-file-spec.coffee
+++ spec/github-file-spec.coffee
@@ -271,12 +271,12 @@
       describe "when text is selected", ->
         it "copies the URL to the clipboard with the selection range in the hash", ->
           githubFile.copyUrl([[0, 0], [1, 1]])
-          expect(atom.clipboard.read()).toBe 'https://github.com/some-user/some-repo/blob/master/some-dir/some-file.md#L1-L2'
+          expect(atom.clipboard.read()).toBe 'https://github.com/some-user/some-repo/blob/80b7897ceb6bd7531708509b50afeab36a4b73fd/some-dir/some-file.md#L1-L2'

       describe "when no text is selected", ->
         it "copies the URL to the clipboard with the cursor location in the hash", ->
           githubFile.copyUrl([[2, 1], [2, 1]])
-          expect(atom.clipboard.read()).toBe 'https://github.com/some-user/some-repo/blob/master/some-dir/some-file.md#L3'
+          expect(atom.clipboard.read()).toBe 'https://github.com/some-user/some-repo/blob/80b7897ceb6bd7531708509b50afeab36a4b73fd/some-dir/some-file.md#L3'

     describe "openRepository", ->
       describe "when the file is openable on GitHub.com", ->


:white_check_mark: Add proper tests for cursor page moves methods

--- spec/cursor-spec.coffee
+++ spec/cursor-spec.coffee
@@ -13,7 +13,7 @@
     atom.config.set 'table-edit.minimuColumnWidth', 10
     atom.config.set 'table-edit.rowHeight', 20
     atom.config.set 'table-edit.minimumRowHeight', 10
-    atom.config.set 'table-edit.pageMovesAmount', 3
+    atom.config.set 'table-edit.pageMovesAmount', 20

     table = new Table

@@ -125,14 +125,69 @@

       expect(cursor.position).toEqual([1,2])

-  xdescribe '::pageUp', ->
-    it 'moves the cursor by the amount of page moves', ->
+  describe 'page moves', ->
+    beforeEach ->
+      table = new Table

-  xdescribe '::pageDown', ->
-    it 'moves the cursor by the amount of page moves', ->
+      for n in [0..60]
+        table.addColumn "column_#{n}"

-  xdescribe '::pageLeft', ->
-    it 'moves the cursor by the amount of page moves', ->
+      for r in [0..60]
+        row = []
+        row.push("cell_#{r}_#{c}") for c in [0..60]

-  xdescribe '::pageRight', ->
-    it 'moves the cursor by the amount of page moves', ->
+        table.addRow(row)
+
+      tableEditor = new TableEditor({table})
+      {displayTable} = tableEditor
+
+      cursor = new Cursor({tableEditor, position: new Point(30,30)})
+      selection = new Selection({cursor, tableEditor})
+
+    describe '::pageUp', ->
+      it 'moves the cursor by the amount of page moves', ->
+        cursor.pageUp()
+
+        expect(cursor.position).toEqual([10,30])
+
+      it 'stops at the first line when going beyond top', ->
+        cursor.pageUp()
+        cursor.pageUp()
+
+        expect(cursor.position).toEqual([0,30])
+
+    describe '::pageDown', ->
+      it 'moves the cursor by the amount of page moves', ->
+        cursor.pageDown()
+
+        expect(cursor.position).toEqual([50,30])
+
+      it 'stops at the last line when going beyond bottom', ->
+        cursor.pageDown()
+        cursor.pageDown()
+
+        expect(cursor.position).toEqual([60,30])
+
+    describe '::pageLeft', ->
+      it 'moves the cursor by the amount of page moves', ->
+        cursor.pageLeft()
+
+        expect(cursor.position).toEqual([30,10])
+
+      it 'stops at the first column when going beyond left', ->
+        cursor.pageLeft()
+        cursor.pageLeft()
+
+        expect(cursor.position).toEqual([30,0])
+
+    describe '::pageRight', ->
+      it 'moves the cursor by the amount of page moves', ->
+        cursor.pageRight()
+
+        expect(cursor.position).toEqual([30,50])
+
+      it 'stops at the last column when going beyond right', ->
+        cursor.pageRight()
+        cursor.pageRight()
+
+        expect(cursor.position).toEqual([30,60])


Add spec for converting CSON back to object

--- spec/stdlib/cson-spec.coffee
+++ spec/stdlib/cson-spec.coffee
@@ -65,3 +65,18 @@

       it "returns formatted CSON", ->
         expect(CSON.stringify(a: {b: 'c'})).toBe "'a':\n  'b': 'c'"
+
+  describe "when converting back to an object", ->
+    it "produces the original object", ->
+      object =
+        showInvisibles: true
+        fontSize: 20
+        core:
+          themes: ['a', 'b']
+        stripTrailingWhitespace:
+          singleTrailingNewline: true
+
+      cson = CSON.stringify(object)
+      {CoffeeScript} = require 'coffee-script'
+      evaledObject = CoffeeScript.eval(cson, bare: true)
+      expect(evaledObject).toEqual object


:shirt:

:shirt:

--- spec/text-buffer-spec.coffee
+++ spec/text-buffer-spec.coffee
@@ -1191,7 +1191,7 @@

       state.mustExist = true
       TextBuffer.deserialize(state).then(
-        () -> expect('serialization succeeded with mustExist: true').toBeUndefined(),
+        -> expect('serialization succeeded with mustExist: true').toBeUndefined(),
         (err) -> expect(err.code).toBe('ENOENT')
       ).then(done, done)


Revert "Wait a tick before checking for path subscriptions?"

This reverts commit d9cf7a73c6cfd94707184e90cdc2bd1ec3ce133b.

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -112,7 +112,6 @@

   document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent

-  waits(0)
   ensureNoPathSubscriptions()
   waits(0) # yield to ui thread to make screen update more frequently


Small optimization

Small optimization

--- src/tools/abbrev.proto.coffee
+++ src/tools/abbrev.proto.coffee
@@ -19,20 +19,20 @@
   if word of @z
     return @z[word] or word   # Exact match
   for k, v of @z when word == k.substring 0, word.length
-    x = v or k
-    return if res and res != x
-    res = x
+    v ||= k
+    return if res and res != v
+    res = v
   res

 # List all abbreviations
 exports._ = ->
   list = {}
   for k, v of @z
-    answer = v or k
+    v || = k
     i = 0
     while ++i <= k.length
-      if answer == @$ test = k.substring 0, i
-        list[test] = answer
+      if v == @$ test = k.substring 0, i
+        list[test] = v
         break
   result = {}
   for k in (k for k of list).sort()


:mouse2: working on mouse projection

:mouse2: working on mouse projection

--- src/Termap.coffee
+++ src/Termap.coffee
@@ -15,7 +15,6 @@
 config = require './config'

 module.exports = class Termap
-
   width: null
   height: null
   canvas: null
@@ -113,8 +112,9 @@
     @_draw()

   _onMouseMove: (event) ->
-    # only continue if x/y are valid
-    return unless event.x <= config.output.columns and event.y <= config.output.rows
+    projected =
+      x: event.x * 2
+      y: event.y * 4

     # start dragging
     if event.button is "left"
@@ -130,7 +130,7 @@
         @mouseDragging = x: event.x, y: event.y

     # update internal mouse tracker
-    @mousePosition = x: event.x, y: event.y
+    @mousePosition = projected
     @notify @_getFooter()

   _onKey: (key) ->
@@ -181,7 +181,7 @@

     "center: #{utils.digits @center.lat, 3}, #{utils.digits @center.lon, 3}   "+
     "zoom: #{utils.digits @zoom, 2}   "+
-    "mouse: #{@mousePosition.x*2-@width/2} #{@mousePosition.y}   "
+    "mouse: #{@mousePosition.x-@width/2} #{@mousePosition.y-@height/2}   "

   notify: (text) ->
     @_write "\r\x1B[K"+text unless config.headless


alteration of embed for needing more info about short titles

--- static/embed/oab_embed.coffee
+++ static/embed/oab_embed.coffee
@@ -174,6 +174,7 @@
     this.css ?= _oab.css
     this._loading = false # tracks when loads are occurring
     this.submit_after_metadata = false # used by instantill to track if metadata has been provided by user
+    this.needmore = false # used by instantill to track that more metadata is required (e.g. if title is too short)
     this.file = false # used by syp to store the file for sending to backend

     this.demo ?= window.location.href.indexOf('/demo') isnt -1 and (window.location.href.indexOf('openaccessbutton.') isnt -1 or window.location.href.indexOf('shareyourpaper.') isnt -1 or window.location.href.indexOf('instantill.') isnt -1)
@@ -747,7 +748,7 @@
 _oab.prototype.find = (e) ->
   try
     try e.preventDefault()
-    if JSON.stringify(this.f) isnt '{}'
+    if JSON.stringify(this.f) isnt '{}' or this.needmore
       for k in ['title','journal','year','doi']
         if v = _L.get '#_oab_' + k
           if this.data[k] isnt v
@@ -772,21 +773,25 @@
     if val = _L.get('#_oab_input')
       val = val.trim().replace(/\.$/,'')
       if val.length
-        if val.indexOf('doi.org/') isnt -1
-          this.data.url = val
-          this.data.doi = '10.' + val.split('/10.')[1].split(' ')[0]
-        else if val.indexOf('10.') isnt -1
-          this.data.doi = val
-        else if val.indexOf('http') is 0 or val.indexOf('www.') isnt -1
-          this.data.url = val
-        else if val.toLowerCase().replace('pmc','').replace('pmid','').replace(':','').replace(/[0-9]/g,'').length is 0
-          this.data.id = val
+        if val.indexOf(' ') is -1
+          if val.indexOf('doi.org/') isnt -1
+            this.data.url = val
+            this.data.doi = '10.' + val.split('/10.')[1].split(' ')[0]
+          else if val.indexOf('10.') isnt -1
+            this.data.doi = val
+          else if val.indexOf('http') is 0 or val.indexOf('www.') isnt -1
+            this.data.url = val
+          else if val.toLowerCase().replace('pmc','').replace('pmid','').replace(':','').replace(/[0-9]/g,'').length is 0
+            this.data.id = val
+          else
+            this.data.title = val # unlikely, just a one-word title, but just in case
         else
-          this.data.title = val
+          this.data.title = val # could also be a citation but backend will try to parse that out
     else if this.data.doi or this.data.title or this.data.url or this.data.id
       _L.set '#_oab_input', this.data.doi ? this.data.title ? this.data.url ? this.data.id

-    if this.plugin is 'instantill' and not this.data.doi and not this.f?.metadata?.journal and this.data.title and this.data.title.length < 30 and this.data.title.split(' ').length < 3
+    if this.plugin is 'instantill' and not this.data.doi and not this.needmore and not this.f?.metadata?.journal and this.data.title and this.data.title.length < 30 and this.data.title.split(' ').length < 3
+      this.needmore = true
       this.metadata() # need more metadata for short titles
     else if not this.data.doi and (this.plugin is 'shareyourpaper' or (not this.data.url and not this.data.pmid and not this.data.pmcid and not this.data.title and not this.data.id))
       if this.plugin is 'shareyourpaper'
@@ -1094,6 +1099,8 @@
   # keep separate from this.config so that additional calls to configure take account of false if they do exist though
   this.config.pilot = Date.now() if this.config.pilot is true
   this.config.live = Date.now() if this.config.live is true
+  for cw of this.config
+    delete this.config[cw] if this.config[cw] is ''
   for k of wc = JSON.parse JSON.stringify this.config
     delete wc[k] if not wc[k]? or wc[k] is false or ((typeof wc[k] is 'string' or Array.isArray(wc[k])) and wc[k].length is 0)
   try


Fix the help documentation

Fix the help documentation

--- scripts/mfl.coffee
+++ scripts/mfl.coffee
@@ -9,7 +9,7 @@
 #   HUBOT_MFL_LEAGUE_WEEK - the week, ordinal number
 #
 # Commands:
-#   hubot: fantasyscores - print out the fantasy scores from myfantasyleague.com
+#   hubot fantasyscores - print out the fantasy scores from myfantasyleague.com
 #
 #
 # Authors:


allow an apikey to be set if specified

allow an apikey to be set if specified

--- code/serv.coffee
+++ code/serv.coffee
@@ -199,7 +199,7 @@
         shortname: req.params.profile
         displayname: req.body.displayname
         email: [req.body.email]
-        apikey: fresh_apikey()
+        apikey: req.body.newApikey or fresh_apikey()
       ).save (err) ->
         console.log err
         User.findOne {shortname: req.params.profile}, (err, user) ->


more updates to the test

more updates to the test

--- apps/show/test/client.coffee
+++ apps/show/test/client.coffee
@@ -20,6 +20,8 @@
       @PartnerShowView = benv.requireWithJadeify resolve(__dirname, '../client/index.coffee'), ['artworkColumns']
       @PartnerShowView.__set__ 'CarouselView', benv.requireWithJadeify resolve(__dirname, '../../../components/carousel/view.coffee'), ['carouselTemplate']
       @PartnerShowView.__set__ 'PartnerShowButtons', @PartnerShowButtons = sinon.stub()
+      carouselView = @PartnerShowView.__get__ 'CarouselView'
+      carouselView.setStops = sinon.stub()
       done()

   after ->


Removed reference to rewrite_function.

Removed reference to rewrite_function.

--- src/client.coffee
+++ src/client.coffee
@@ -76,7 +76,7 @@

     $(-> app.run '#/') if app?

-@build = (version, coffeescript_helpers, rewrite_function) ->
+@build = (version, coffeescript_helpers) ->
   String(skeleton)
     .replace('version = null;', "version = '#{version}';")
     .replace('coffeescript_helpers = null;', "var coffeescript_helpers = '#{coffeescript_helpers}';")


fixes ensime/ensime-atom#226

fixes ensime/ensime-atom#226

--- src/client.coffee
+++ src/client.coffee
@@ -17,6 +17,7 @@
     ensimeMessageCounter = 1

     publicApi = -> {
+      httpPort,
       post,
       destroy,
       getCompletions,


Upgrade node to v0.10.9 since atom-shell branch has been merged.

--- src/config.coffee
+++ src/config.coffee
@@ -8,13 +8,13 @@
     process.env.ATOM_RESOURCE_PATH ? '/Applications/Atom.app/Contents/Frameworks/Atom.framework/Resources'

   getNodeUrl: ->
-    process.env.ATOM_NODE_URL ? 'https://gh-contractor-zcbenz.s3.amazonaws.com/cefode2/dist'
+    process.env.ATOM_NODE_URL ? 'https://gh-contractor-zcbenz.s3.amazonaws.com/atom-shell/dist'

   getAtomPackagesUrl: ->
     process.env.ATOM_PACKAGES_URL ? 'http://atom.iriscouch.com/registry/_design/apm/_view/atom_packages'

   getNodeVersion: ->
-    process.env.ATOM_NODE_VERSION ? '0.10.3'
+    process.env.ATOM_NODE_VERSION ? '0.10.9'

   getUserConfigPath: ->
     path.resolve(__dirname, '..', '.apmrc')


Some irc services (like anope) doesn't have the dot in "Password accepted" notice

--- src/adapters/irc.coffee
+++ src/adapters/irc.coffee
@@ -60,7 +60,7 @@
       bot.addListener 'notice', (from, to, text) ->
         if from is 'NickServ' and text.indexOf('registered') isnt -1
           bot.say 'NickServ', "identify #{options.nickpass}"
-        else if options.nickpass and from is 'NickServ' and text.indexOf('Password accepted.') isnt -1
+        else if options.nickpass and from is 'NickServ' and text.indexOf('Password accepted') isnt -1
           for room in options.rooms
             @join room


Redirect route to dashboard or login

Redirect route to dashboard or login

--- apps/organizations/routes.coffee
+++ apps/organizations/routes.coffee
@@ -20,9 +20,10 @@

 routes = (app) ->
   app.get '/', (req, res, next) ->
-    res.render "index",
-      title: "Badges!"
-
+    if req.session.org_id
+      res.redirect '/dashboard'
+    else
+      res.redirect '/login'

   app.get '/dashboard', authenticateOrg, (req, res, next) ->
     loadBadgeAndUserCounts req, (badgesCount, usersCount) ->


Made test passing

Made test passing

--- test/clog.coffee
+++ test/clog.coffee
@@ -22,5 +22,5 @@

       scores = clog.report([cases, ifs])

-      assert.equal(scores[cases], 29)
-      assert.equal(scores[ifs], 22)+      assert.equal(scores[cases].complexity, 29)
+      assert.equal(scores[ifs].complexity, 22)


Remove underscore fn.

Remove underscore fn.


Remove auth logic from redirect in Travis.Route

Auth check in redirect duplicates beforeModel logic

--- assets/scripts/app/routes.coffee
+++ assets/scripts/app/routes.coffee
@@ -22,19 +22,6 @@

   signedIn: ->
     @controllerFor('currentUser').get('content')
-
-  redirect: ->
-    @auth.autoSignIn() unless @signedIn()
-
-    if @get('needsAuth')
-      @authorize(@router.location.getURL())
-    else
-      @_super.apply this, arguments
-
-  authorize: (path) ->
-    if !@signedIn()
-      @auth.storeAfterSignInPath(path)
-      @transitionTo('auth')

 Travis.ApplicationRoute = Travis.Route.extend
   init: ->


mekko: rotated labels

mekko: rotated labels

--- app/assets/javascripts/d3/mekko.coffee
+++ app/assets/javascripts/d3/mekko.coffee
@@ -109,12 +109,13 @@
         .attr("data-rel", (d) -> d.key())

       # vertical sector label
-      @sectors.append("svg:text")
+      @sectors
+        .append("svg:text")
+        .attr('class', 'sector_label')
         .text((d) -> d.get 'key')
-        .attr("class", "sector_label")
-        .attr("x", -195)
-        .attr("transform", "rotate(270)")
-        .attr("text-anchor", "end")
+        .attr("transform", "rotate(90)")
+        .attr("text-anchor", "start")
+        .attr('x', 200)

       @draw_legend
         svg: @svg
@@ -153,7 +154,7 @@
       @svg.selectAll("text.sector_label")
         .data(@sector_list.models, (d) -> d.get 'key' )
         .transition().duration(500)
-        .attr("dy", (d) => @x(d.total_value() / 2) + 5 )
+        .attr("dy", (d) => -@x(d.total_value() / 2))
         .text((d) -> "#{d.get 'key'} #{parseInt d.total_value()} PJ")

       # we need to track the offset for every sector


Parenteses opicionais removidos

Parenteses opicionais removidos

--- app/assets/javascripts/notes.js.coffee
+++ app/assets/javascripts/notes.js.coffee
@@ -3,5 +3,5 @@
 # You can use CoffeeScript in this file: http://coffeescript.org/

 @change_color = (color) ->
-  $('#note-card').css('background-color', color)
-  $('input[name="note[color]"]').val(color)
+  $('#note-card').css 'background-color', color
+  $('input[name="note[color]"]').val color


Selecting a subject code now submits the subject index search form

--- app/assets/javascripts/subjects.coffee
+++ app/assets/javascripts/subjects.coffee
@@ -75,6 +75,7 @@
           replace: (value) -> return "$1adverse-events:#{value}"
         },
         {
+          name: 'search'
           match: /(^|\s)([\w\-]+)$/
           search: (term, callback) ->
             # callback(cache[term], true)
@@ -86,6 +87,9 @@
           cache: true
         }
       ], { appendTo: 'body' }
+    ).on('textComplete:select': (e, value, strategy) ->
+      if strategy.name == 'search' and value not in ['adverse-events', 'has', 'is', 'not']
+        $(this).closest('form').submit()
     )
   )


don't set matrix to default value if field is not empty on page load(e.g. on error during creation when #create renders action 'new')

--- app/assets/javascripts/tasks.js.coffee
+++ app/assets/javascripts/tasks.js.coffee
@@ -2,25 +2,29 @@
 # All this logic will automatically be available in application.js.
 # You can use CoffeeScript in this file: http://coffeescript.org/

-update_data_model_form = (dataModelClass)->
-  data_model = ($(dataModelClass).find('.data_model select').prop('value') || '').toUpperCase()
-  if data_model == 'PCM'
-    $(dataModelClass + '.data_model_specifiers .effective_count').hide()
-    $(dataModelClass + '.data_model_specifiers .pseudocount').show()
-    $(dataModelClass).find('.matrix textarea')[0].value  = $(dataModelClass).find('.data_model_examples .pcm').text()
-  else if data_model == 'PPM'
-    $(dataModelClass + '.data_model_specifiers .effective_count').show()
-    $(dataModelClass + '.data_model_specifiers .pseudocount').show()
-    $(dataModelClass).find('.matrix textarea')[0].value = $(dataModelClass).find('.data_model_examples .ppm').text()
-  else if data_model == 'PWM'
-    $(dataModelClass + '.data_model_specifiers .effective_count').hide()
-    $(dataModelClass + '.data_model_specifiers .pseudocount').hide()
-    $(dataModelClass).find('.matrix textarea')[0].value = $(dataModelClass).find('.data_model_examples .pwm').text()
+update_data_model_form = (dataModelSelector)->
+  $model = $(dataModelSelector)
+  data_model_type = ($model.find('.data_model select').prop('value') || '').toUpperCase()
+  $data_model_specifiers = $model.filter('.data_model_specifiers')
+  if data_model_type == 'PCM'
+    $data_model_specifiers.find('.effective_count').hide()
+    $data_model_specifiers.find('.pseudocount').show()
+    $model.find('.matrix textarea').val( $model.find('.data_model_examples .pcm').text() )
+  else if data_model_type == 'PPM'
+    $data_model_specifiers.find('.effective_count').show()
+    $data_model_specifiers.find('.pseudocount').show()
+    $model.find('.matrix textarea').val( $model.find('.data_model_examples .ppm').text() )
+  else if data_model_type == 'PWM'
+    $data_model_specifiers.find('.effective_count').hide()
+    $data_model_specifiers.find('.pseudocount').hide()
+    $model.find('.matrix textarea').val( $model.find('.data_model_examples .pwm').text() )

-window.register_data_model_form = (dataModelClass)->
-  update_data_model_form(dataModelClass)
-  $(dataModelClass).find('.data_model select').change ->
-    update_data_model_form(dataModelClass)
+window.register_data_model_form = (dataModelSelector)->
+  $model = $(dataModelSelector)
+  if $model.find('.matrix textarea').length != 0 && $model.find('.matrix textarea').val().length == 0
+    update_data_model_form($model)
+  $model.find('.data_model select').change ->
+    update_data_model_form($model)

 $(document).ready ->
   update_background_model_form = (background_form)->


Give star tool a move cursor

Give star tool a move cursor

--- app/controllers/star_point_tool.coffee
+++ app/controllers/star_point_tool.coffee
@@ -6,6 +6,8 @@

 class PointTool extends MarkingTool
   @mark: PointMark
+
+  cursors: style.cursors

   constructor: ->
     super


Fixed bug: Element loses focus when accessed via tab.

--- app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
+++ app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
@@ -1051,7 +1051,7 @@
         $( item[0] ).on('focus', ->
           visible = $( item[0] ).is(":visible")
           if visible && !$( item[0] ).expanding('active')
-            $( item[0] ).expanding()
+            $( item[0] ).expanding().focus()
         )
       App.Delay.set( a, 80 )


Fix Wrong Password Issue

Fix Wrong Password Issue

--- app/js/controllers/unlockwallet.coffee
+++ app/js/controllers/unlockwallet.coffee
@@ -2,6 +2,7 @@
   $scope.descriptionCollapsed = true
   $scope.wrongPass = false
   $scope.submitForm = ->
+    $scope.wrongPass = false
     promise = Wallet.wallet_unlock($scope.spending_password).then( () ->
       $scope.history_back()
     , (error) ->


Fixed unwrapping instead of removing

Fixed unwrapping instead of removing

--- app/js/jail_iframe/classes/fact.coffee
+++ app/js/jail_iframe/classes/fact.coffee
@@ -118,7 +118,6 @@

   destroy: ->
     for el in @elements
-      $el = $(el)
-      $el.remove()
+      $(el).contents().unwrap()

     @fact_interaction.destroy()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera_flash.coffee
+++ src/camera_flash.coffee
@@ -28,7 +28,7 @@

       I.flashCooldown = I.flashCooldown.approach(0, 1)

-  self.bind('flash', function(canvas) {
+  self.bind 'flash', function(canvas) {
     canvas.fill(I.flashColor)

   ###*


Add Editor::onDidChangeSelectedScreenRange

--- src/editor.coffee
+++ src/editor.coffee
@@ -336,6 +336,9 @@

   onDidRemoveSelection: (callback) ->
     @emitter.on 'did-remove-selection', callback
+
+  onDidChangeSelectionScreenRange: (callback) ->
+    @emitter.on 'did-change-selection-screen-range', callback

   observeDecorations: (callback) ->
     @displayBuffer.observeDecorations(callback)
@@ -390,6 +393,8 @@
         deprecate("Use Editor::onDidAddSelection instead")
       when 'selection-removed'
         deprecate("Use Editor::onDidRemoveSelection instead")
+      when 'selection-screen-range-changed'
+        deprecate("Use Editor::onDidChangeSelectionScreenRange instead")

       when 'decoration-added'
         deprecate("Use Editor::onDidAddDecoration instead")
@@ -2428,6 +2433,7 @@

   selectionScreenRangeChanged: (selection) ->
     @emit 'selection-screen-range-changed', selection
+    @emitter.emit 'did-change-selection-screen-range', selection

   ###


Autoscroll when moving lines up/down

Autoscroll when moving lines up/down

Closes #1829

--- src/editor.coffee
+++ src/editor.coffee
@@ -913,7 +913,7 @@
       for foldedRow in foldedRows when 0 <= foldedRow <= @getLastBufferRow()
         @foldBufferRow(foldedRow)

-      @setSelectedBufferRange(selection.translate([-insertDelta]), preserveFolds: true)
+      @setSelectedBufferRange(selection.translate([-insertDelta]), preserveFolds: true, autoscroll: true)

   # Move lines intersecting the most recent selection down by one row in screen
   # coordinates.
@@ -969,7 +969,7 @@
       for foldedRow in foldedRows when 0 <= foldedRow <= @getLastBufferRow()
         @foldBufferRow(foldedRow)

-      @setSelectedBufferRange(selection.translate([insertDelta]), preserveFolds: true)
+      @setSelectedBufferRange(selection.translate([insertDelta]), preserveFolds: true, autoscroll: true)

   # Duplicate the most recent cursor's current line.
   duplicateLines: ->


fix indent

fix indent

--- test/test.coffee
+++ test/test.coffee
@@ -76,35 +76,35 @@
   throw "TestFail"

 tests = [
- ->
-   lets_test "connection to mpd"
-   mpdEvent 1, 'sendCommand', "status", (msg) ->
-     ok /^playlist:/m.test(msg)
-     ok /^repeat:/m.test(msg)
-     ok /^random:/m.test(msg)
- ->
-   lets_test "two instances of mpd"
-   mpdEvent 1, 'sendCommand', "status", (msg) ->
-     ok /^playlist:/m.test(msg)
-     ok /^repeat:/m.test(msg)
-     ok /^random:/m.test(msg)
- ->
-   lets_test "remove event listeners"
-   count = 0
-   mpdEvent 1, 'onStatusUpdate', ->
-     count += 1
-     ok count < 2
-     mpd.removeEventListeners 'onStatusUpdate'
-     mpdEvent 1, 'onStatusUpdate', ->
-       ok true
-     mpd.updateStatus()
-   mpd.updateStatus()
+  ->
+    lets_test "connection to mpd"
+    mpdEvent 1, 'sendCommand', "status", (msg) ->
+      ok /^playlist:/m.test(msg)
+      ok /^repeat:/m.test(msg)
+      ok /^random:/m.test(msg)
+  ->
+    lets_test "two instances of mpd"
+    mpdEvent 1, 'sendCommand', "status", (msg) ->
+      ok /^playlist:/m.test(msg)
+      ok /^repeat:/m.test(msg)
+      ok /^random:/m.test(msg)
+  ->
+    lets_test "remove event listeners"
+    count = 0
+    mpdEvent 1, 'onStatusUpdate', ->
+      count += 1
+      ok count < 2
+      mpd.removeEventListeners 'onStatusUpdate'
+      mpdEvent 1, 'onStatusUpdate', ->
+        ok true
+      mpd.updateStatus()
+    mpd.updateStatus()
   ->
     lets_test "get artist list and detailed info"
     mpdEvent 1, 'onLibraryUpdate', ->
       ok mpd.library.artist_list.length > 1
       mpd.removeEventListeners 'onLibraryUpdate'
-
+
       rand_index = Math.floor(Math.random()*mpd.library.artist_list.length)
       random_artist = mpd.library.artist_list[rand_index]
       mpdEvent 1, 'onLibraryUpdate', ->
@@ -117,13 +117,13 @@
             eq track.artist, random_artist
             eq track.album, album
       mpd.updateArtistInfo random_artist.name
-
+
     mpd.updateArtistList()
- ->
-   lets_test "get current track"
-   mpdEvent 1, 'onStatusUpdate', ->
-     ok mpd.status.state is "play" or mpd.status.state is "stop" or mpd.status.state is "pause"
-   mpd.updateStatus()
+  ->
+    lets_test "get current track"
+    mpdEvent 1, 'onStatusUpdate', ->
+      ok mpd.status.state is "play" or mpd.status.state is "stop" or mpd.status.state is "pause"
+    mpd.updateStatus()
 ]

 runTest = (test, args...) ->


update

update

--- src/evemit.coffee
+++ src/evemit.coffee
@@ -18,19 +18,25 @@
             @events = {}
             obj[i] = j for i,j of Evemit.prototype
             return obj
+
         on: (eve, fn) ->
             @events[eve] = @events[eve] or []
             @events[eve].push(fn)
+
         once: (eve, fn) ->
             self = @
             @on eve, ->
                 self.off(eve)
                 fn.apply @, arguments
+
         off: (eve) -> delete @events[eve]
+
         emit: (eve, args...) ->
             if @events[eve]?
                 e.apply(@, args) for e in @events[eve]
+
         events: -> e for e of @events
+
         listeners: (eve) -> l for l in @events[eve]

     Evemit.bind = (el, eve, fn, priority) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/follow.coffee
+++ src/follow.coffee
@@ -1,7 +1,6 @@
 Follow = (I={}, self) ->
   Object.reverseMerge I,
-    velocity: Point(0, 0)
-
+    velocity: Point(0, 0)

   follow: (obj) ->
     I.velocity = obj.I.position().subtract(self.position()).norm()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/text.coffee
+++ test/text.coffee
@@ -17,6 +17,8 @@
 test "text fades out", ->
   text = Text()

-  text.
+  text.update()
+
+

 module()


Fix todo test on ubuntu

Fix todo test on ubuntu

--- test/todo.coffee
+++ test/todo.coffee
@@ -7,7 +7,8 @@
 	s = sigar()

 	it 'should return net adress', ->
-		testNetAdress = s.testNetAdress '127.0.0.1'
+		# Ubuntu return "TypeError: node_sigar_str2net_address error"
+		# testNetAdress = s.testNetAdress '127.0.0.1'
 		# console.log testNetAdress
 		# assert.eql testNetAdress, '127.0.0.1'


Smooth scroll; fix displacement and calibrate.

--- content_scripts/scroller.coffee
+++ content_scripts/scroller.coffee
@@ -57,7 +57,7 @@
   element[axisName] = before
   before != after

-# Find the element we should and can scroll.
+# Find the element which we should and can scroll (or document.body).
 findScrollableElement = (element, direction, amount, factor = 1) ->
   axisName = scrollProperties[direction].axisName
   while element != document.body and
@@ -85,10 +85,10 @@
 #   - For non-smooth scrolling, the entire scroll happens immediately.
 #   - For smooth scrolling with distinct key presses, a separate animator is initiated for each key press.
 #     Therefore, several animators may be active at the same time.  This ensures that two quick taps on `j`
-#     scroll to the same position as two slower taps (modulo rounding errors).
-#   - For smooth scrolling with keyboard repeat, the most recently-activated animator continues scrolling
-#     at least until its corresponding keyup event is received.  We never initiate a new animator on keyboard
-#     repeat.
+#     scroll to the same position as two slower taps.
+#   - For smooth scrolling with keyboard repeat (continuous scrolling), the most recently-activated animator
+#     continues scrolling at least until its corresponding keyup event is received.  We never initiate a new
+#     animator on keyboard repeat.

 # Scroll by a relative amount (a number) in some direction, possibly smoothly.
 doScrollBy = do ->
@@ -103,7 +103,7 @@

     unless keyHandler
       keyHandler = handlerStack.push
-        keydown: (event) -> lastEvent = event
+        keydown: -> lastEvent = event
         keyup: -> time += 1

     axisName = scrollProperties[direction].axisName
@@ -120,40 +120,63 @@

     mostRecentActivationTime = activationTime = ++time

+    isKeyStillDown = ->
+      time == activationTime
+
+    # Store amount's sign and make amount positive; the logic is clearer when amount is positive.
+    sign = amount / Math.abs amount
+    amount = Math.abs amount
+
     # Duration in ms. Allow a bit longer for longer scrolls.
-    duration = 100 + 25 * Math.log Math.abs amount
-
-    # Round away from 0, so that we don't leave any requested scroll amount unscrolled.
-    roundOut = if 0 <= amount then Math.ceil else Math.floor
-    delta = roundOut(amount / duration)
-
+    duration = Math.max 100, 20 * Math.log amount
+
+    totalDelta = 0
+    calibration = 1.0
+    initialTimestamp = null
+    previousTimestamp = null
     animatorId = null
-    start = null
-    scrolledAmount = 0
-
-    shouldStopScrolling = (progress) ->
-      # If activationTime == time, then this is the most recently-activated animator and we haven't yet seen
-      # its keyup event, so keep going; otherwise, check progress and duration.
-      if activationTime == time then false else duration <= progress
+
+    progressAnimation = ->
+      animatorId = requestAnimationFrame animate
+
+    cancelAnimation = ->
+      # Make one final call of performScroll to check the visibility of the activated element.
+      performScroll element, axisName, 0, true
+      cancelAnimationFrame animatorId

     animate = (timestamp) ->
-      start ?= timestamp
-
-      # FIXME(smblott) This calculation is not correctly calibrated:
-      #   - A `j` with smooth scrolling does not take you to the same place as a `j` with jump scrolling.
-      #   - A `j` followed by `k` does not take you back to exactly where you started.
-      progress = timestamp - start
-      scrollDelta = roundOut(delta * progress) - scrolledAmount
-      scrolledAmount += scrollDelta
-
-      if not performScroll(element, axisName, scrollDelta, false) or shouldStopScrolling progress
-          # One final call of performScroll to check the visibility of the activated element.
-          performScroll(element, axisName, 0, true)
-          window.cancelAnimationFrame(animatorId)
+      initialTimestamp ?= timestamp
+      previousTimestamp ?= timestamp
+
+      if timestamp == previousTimestamp
+        return progressAnimation()
+
+      # The elapsed time is typically about 16ms.
+      elapsed = timestamp - previousTimestamp
+      totalElapsed = timestamp - initialTimestamp
+      previousTimestamp = timestamp
+
+      # The constants in the duration calculation, above, are chosen to provide reasonable scroll speeds for
+      # scrolls resulting from distinct keypresses.  For continuous scrolls (where the key remains depressed),
+      # some scrolls are too slow, and others too fast.  Here, we compensate a bit.
+      if isKeyStillDown() and 50 <= totalElapsed and 0.5 <= calibration <= 1.6
+        calibration *= 1.05 if 1.05 * calibration * amount <= 150 # Speed up slow scrolls.
+        calibration *= 0.95 if 150 <= 0.95 * calibration * amount # Slow down fast scrolls.
+
+      # Calculate the initial delta, rounding up to ensure progress.  Then, adjust delta to account for the
+      # current scroll state.
+      delta = Math.ceil amount * (elapsed / duration) * calibration
+      delta = if isKeyStillDown() then delta else Math.max 0, Math.min delta, amount - totalDelta
+
+      if delta and performScroll element, axisName, sign * delta, false
+        totalDelta += delta
+        progressAnimation()
       else
-        animatorId = window.requestAnimationFrame(animate)
-
-    animatorId = window.requestAnimationFrame(animate)
+        # We're done. Make one final call of performScroll to check the visibility of the activated element.
+        performScroll element, axisName, 0, true
+        cancelAnimationFrame animatorId
+
+    progressAnimation()

 Scroller =
   init: (frontendSettings) ->


Revert "Fix scrolling issue."

Revert "Fix scrolling issue."

This reverts commit 90d2addc9b8f95f9272f8c2a77bdaf9dfebc0fa8.

Nope.  This is bad.  It fixes the issue referred to in the commit
record, but breaks scrolling out of text areas.

With 90d2addc9b8f95f9272f8c2a77bdaf9dfebc0fa8:

- Start editing in a text area, add enough text that it scrolls.
- `Escape`
- `k`, `k`, `k`, ...

We expect first the text areas to scroll, then -- when it reaches the
top -- the document to scroll.  However, with
90d2addc9b8f95f9272f8c2a77bdaf9dfebc0fa8, we get stuck in the text
area.

--- content_scripts/scroller.coffee
+++ content_scripts/scroller.coffee
@@ -75,11 +75,7 @@
   # definitely scrolling backwards, so a delta of -1 will do.  For absolute scrolls, factor is always 1.
   delta = factor * getDimension(element, direction, amount) || -1
   delta = getSign delta # 1 or -1
-  for change in [ delta, -delta ]
-    if performScroll element, direction, change
-      performScroll element, direction, -change
-      return true
-  false
+  performScroll(element, direction, delta) and performScroll(element, direction, -delta)

 # From element and its parents, find the first which we should scroll and which does scroll.
 findScrollableElement = (element, direction, amount, factor) ->


Only show hero campaign progress in ClanDetailsView

--- app/views/clans/ClanDetailsView.coffee
+++ app/views/clans/ClanDetailsView.coffee
@@ -92,7 +92,7 @@
     userConceptsMap = {}
     if @campaigns.loaded
       levelCount = 0
-      for campaign in @campaigns.models
+      for campaign in @campaigns.models when campaign.get('type') is 'hero'
         campaignID = campaign.id
         lastLevelIndex = 0
         for levelID, level of campaign.get('levels')
@@ -181,8 +181,7 @@
     @campaignLevelProgressions = []
     @conceptsProgression = []
     @arenas = []
-    for campaign in @campaigns.models
-      continue if campaign.get('slug') is 'auditions'
+    for campaign in @campaigns.models when campaign.get('type') is 'hero'
       campaignLevelProgression =
         ID: campaign.id
         slug: campaign.get('slug')


Wrong Command Name in Check

Wrong Command Name in Check

--- bin/kdc-plus.coffee
+++ bin/kdc-plus.coffee
@@ -235,7 +235,7 @@

   # See below defaults
   validArgs = [
-    'command'
+    'compile'
     'install'
     'outdated'
   ]


:lipstick: param order

:lipstick: param order

--- src/keymap.coffee
+++ src/keymap.coffee
@@ -95,7 +95,7 @@
       candidateBindings = @keyBindingsForKeystrokesAndTarget(keystrokes, target)
       if candidateBindings.length > 0
         @keystrokes = []
-        return if @dispatchCommandEvent(event, target, candidateBindings[0].command)
+        return if @dispatchCommandEvent(candidateBindings[0].command, target, event)
       target = target.parentElement

   # Public: Get the key bindings for a given command and optional target.
@@ -161,7 +161,7 @@
           and target.webkitMatchesSelector(binding.selector)
       .sort (a, b) -> a.compare(b)

-  dispatchCommandEvent: (keyboardEvent, target, command) ->
+  dispatchCommandEvent: (command, target, keyboardEvent) ->
     return true if command is 'native!'
     keyboardEvent.preventDefault()
     commandEvent = document.createEvent("CustomEvent")


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/controllable.coffee
+++ src/controllable.coffee
@@ -1,3 +1,39 @@
+###*
+The ClampBounds module adds a check to make sure
+that the including GameObject doesn't move outside
+the viewport.
+
+

+  # create a player and include ClampBounds
+  player = GameObject
+    includedModules: ["ClampBounds"]
+    width: 5
+    height: 17
+
+  # put the player outside the viewport
+  player.I.x = -400
+  player.I.y = 1000
+
+  # update the player so ClampBounds can set
+  # his position back inside the viewport.
+  player.update()
+
+  # x, y position is based on the center point so
+  # the position the player is set to is based on
+  # half their width and height
+  player.I.x
+  # => 2.5 # half the player's width
+
+  player.I.y
+  # => 311.5 # The default App.height (320) - half the player's height
+

+
+@name Cooldown
+@module
+@constructor
+@param {Object} I Instance variables
+@param {Core} self Reference to including object
+###
 Controllable = (I={}, self) ->
   Object.reverseMerge I,
     speed: 1


scope it

scope it

--- apps/home/client/hero_unit_view.coffee
+++ apps/home/client/hero_unit_view.coffee
@@ -3,10 +3,10 @@

 module.exports = class HeroUnitView extends Backbone.View

-  initialize: (options) ->
+  initialize: ->
+    @$window = $ window
     @$mainHeader = @$('#main-layout-header')
     @$heroUnits = @$('#home-hero-units')
-    @$window = $ window
     @setBodyClass()
     @setInterval()
     @$window .on 'scroll', _.throttle @setBodyClass, 100
@@ -36,7 +36,7 @@

   showHeroUnit: (index) ->
     @$('.home-hero-unit').removeClass('home-hero-unit-active')
-    $('.home-hero-unit').eq(index).addClass('home-hero-unit-active')
+    @$('.home-hero-unit').eq(index).addClass('home-hero-unit-active')
     @$("#home-hero-unit-dots li").removeClass 'hhud-active'
     @$("#home-hero-unit-dots li").eq(index).addClass('hhud-active')
     @setBodyClass()


check if config.syslog has keys

check if config.syslog has keys

--- src/logger.coffee
+++ src/logger.coffee
@@ -13,7 +13,7 @@
         logFile = fs.createWriteStream config_.file, flags: 'a'
     # syslog needs a hostname as dgram_unix
     # support has been removed from node.
-    if config_.syslog? and config_.syslog.hostname
+    if config_.syslog? and Object.keys(config_.syslog).length > 0
         ain2.set
             tag: 'buddycloud'
             facility: 'daemon'
@@ -45,7 +45,7 @@
             console.log message
         if logFile
             logFile.write "#{message}\n"
-        if config.syslog?
+        if config.syslog? and Object.keys(config.syslog).length > 0
             ain2['info'] message


Remove coffescript source

Remove coffescript source

--- src/logger.coffee
+++ src/logger.coffee
@@ -1,90 +0,0 @@
-pgkinfo = require('pkginfo')(module,'version')
-_ = require 'underscore'
-
-# default options
-exports.defaultConfig =
-  showMillis: false
-  showTimestamp: true
-  printObjFunc: require('util').inspect
-  prefix: ""
-
-# default level = debug
-logLevel = 4
-
-module.exports = class Logger
-
-  @levels =
-    error: 1
-    warning: 2
-    warn: 2
-    info: 3
-    debug: 4
-    trace: 5
-
-  @setLevel: (level, silent=false) ->
-    levelName = null
-    levelValue = null
-    for name,val of Logger.levels
-      if level == name or level == val
-        levelName = name
-        levelValue = val
-        break;
-
-    log = new this {prefix: 'basic-logger'}
-    if levelName? and levelValue?
-      log.info "Setting log level to '"+levelName+"'" if !silent
-      logLevel = levelValue
-    else
-      log.warn "Can't set log level to '"+level+"'. This level does not exist."
-
-  constructor: (config={}) ->
-    @config = _.defaults config,exports.defaultConfig
-
-  padZeros: (num,digits) ->
-    num = String num
-    zerosToAdd = digits - num.length
-    while zerosToAdd > 0
-      num = '0'+num
-      zerosToAdd = zerosToAdd - 1;
-    num
-
-  log: (msg,levelName,args...) ->
-    level = Logger.levels[levelName]
-    if level <= logLevel
-      date = new Date
-      timestamp = date.getFullYear()+"-"+@padZeros((date.getMonth()+1),2)+"-"+@padZeros(date.getDate(),2)+" "+@padZeros(date.getHours(),2)+":"+@padZeros(date.getMinutes(),2)+":"+@padZeros(date.getSeconds(),2)
-      timestamp += "."+@padZeros(date.getMilliseconds(),3) if @config.showMillis
-      msg = @config.printObjFunc msg if typeof msg == "object"
-      output = ''
-      output += '['+timestamp+']' if @config.showTimestamp
-      output += ' '+@config.prefix if @config.prefix != ""
-      output += ' ('+levelName+') '
-      output += msg
-      args.unshift output
-      return console.log.apply this, args
-    else
-      return -1
-
-  error: (msg, args...) ->
-    args.unshift(msg, 'error');
-    @log.apply this,args
-
-  info: (msg, args...) ->
-    args.unshift(msg, 'info');
-    @log.apply this,args
-
-  warn: (msg, args...) ->
-    args.unshift(msg, 'warning');
-    @log.apply this,args
-
-  warning: (msg, args...) ->
-    args.unshift(msg, 'warning');
-    @log.apply this,args
-
-  debug: (msg, args...) ->
-    args.unshift(msg, 'debug');
-    @log.apply this,args
-
-  trace: (msg, args...) ->
-    args.unshift(msg, 'trace');
-    @log.apply this,args


:racehorse: Reduce allocations in Marker.extractParams

--- src/marker.coffee
+++ src/marker.coffee
@@ -5,7 +5,7 @@
 Point = require './point'
 Range = require './range'

-OptionKeys = ['reversed', 'tailed', 'invalidate', 'persistent']
+OptionKeys = new Set(['reversed', 'tailed', 'invalidate', 'persistent'])

 # Private: Represents a buffer annotation that remains logically stationary
 # even as the buffer changes. This is used to represent cursors, folds, snippet
@@ -33,9 +33,12 @@
     outputParams = {}
     if inputParams?
       @handleDeprecatedParams(inputParams) if Grim.includeDeprecatedAPIs
-      extend(outputParams, pick(inputParams, OptionKeys))
-      properties = omit(inputParams, OptionKeys)
-      outputParams.properties = properties if size(properties) > 0
+      for key in Object.keys(inputParams)
+        if OptionKeys.has(key)
+          outputParams[key] = inputParams[key]
+        else
+          outputParams.properties ?= {}
+          outputParams.properties[key] = inputParams[key]
     outputParams

   @delegatesMethods 'containsPoint', 'containsRange', 'intersectsRow', toMethod: 'getRange'
@@ -293,8 +296,8 @@
   # Returns whether this marker matches the given parameters. The parameters
   # are the same as {MarkerManager::findMarkers}.
   matchesParams: (params) ->
-    for key, value of params
-      return false unless @matchesParam(key, value)
+    for key in Object.keys(params)
+      return false unless @matchesParam(key, params[key])
     true

   # Returns whether this marker matches the given parameter name and value.


Add selected class

Add selected class

--- src/render.coffee
+++ src/render.coffee
@@ -26,6 +26,7 @@
   klass = [label, "#{labels.length}_#{label}"]
   klass.push world.get_local_plain('class') ? ''
   klass.push world.get_local_plain(my.key.kind) ? ''
+  klass.push 'selected' if world.get 'selected'
   attrs = {
     id: "#{labels.length}_#{labels.join '_'}"
     class: klass


Use jQuery's .val(…) in the value binder's routine function if window.jQuery is present. [Closes #133]

--- src/rivets.coffee
+++ src/rivets.coffee
@@ -313,10 +313,16 @@
     unbind: (el) ->
       unbindEvent el, 'change', @currentListener
     routine: (el, value) ->
-      if el.type is 'select-multiple'
-        o.selected = o.value in value for o in el if value?
-      else if value?.toString() isnt el.value?.toString()
-        el.value = if value? then value else ''
+      if window.jQuery?
+        el = jQuery el
+
+        if value?.toString() isnt el.val()?.toString()
+          el.val if value? then value else ''
+      else
+        if el.type is 'select-multiple'
+          o.selected = o.value in value for o in el if value?
+        else if value?.toString() isnt el.value?.toString()
+          el.value = if value? then value else ''

   text: (el, value) ->
     if el.innerText?


Fix spelling mistake: reponse => response This caused errors when clicking "not solved" on an assignment that was  already not solved.

--- trix/trix_student/static/trix_student/src/app/assignments/controllers.coffee
+++ trix/trix_student/static/trix_student/src/app/assignments/controllers.coffee
@@ -98,7 +98,7 @@
           $scope.saving = false
           $scope.howsolved = null
         .catch (response) ->
-          if reponse.status == 404  # Handle 404 just like 200
+          if response.status == 404  # Handle 404 just like 200
             $scope.saving = false
             $scope.howsolved = null
           else


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/function_extensions.coffee
+++ test/function_extensions.coffee
@@ -25,7 +25,7 @@
   score = 0
   onceScore = ->
     ( ->
-      score
+      score += 100
     ).once()

 test "#returning", ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/function_extensions.coffee
+++ test/function_extensions.coffee
@@ -31,6 +31,7 @@
   onceScore()
   onceScore()

+  expect

 test "#returning", ->
   x = 0


bug fix : done called multiple times in file watcher adapter

--- test/hFilewatcherAdapter.coffee
+++ test/hFilewatcherAdapter.coffee
@@ -44,14 +44,12 @@
       @timeout 1500
       fs.unlink("./test.json")
       setTimeout(=>
+        hActor.onMessage = (hMessage) ->
+          hMessage.payload.should.be.equal("333")
+          done()
+
         fs.writeFile("./test.json",'"333"')
       , 500)
-
-      hActor.onMessage = (hMessage) ->
-        hMessage.payload.should.be.equal("333")
-        done()
-
-

     it "Should do nothing if the file is not present when it start ", (done) ->
       @timeout 2000


cleanup

cleanup

--- src/search.coffee
+++ src/search.coffee
@@ -1,43 +0,0 @@
-http = require 'http'
-
-class Search
-    constructor: (@host='localhost', @tenant='bfdocs', @port=9200) ->
-
-    index: (manifest, callback) ->
-        c = 1 + manifest.slugs.length
-        handler = (res) ->
-            if --c == 0 and callback then callback()
-
-        @request 'put', manifest.slug, '_home', {body: manifest.homeData}, handler
-        for slug, i in manifest.slugs
-            if slug
-                @request 'put', manifest.slug, slug, {body: manifest.filesData[i]}, handler
-
-    search: (manifest, query, callback) ->
-        q = query: { text: { _all: query } }
-        @request 'post', manifest.slug, '_search', q, (res) ->
-            slugs = []
-            if res and res.hits
-                slugs.push h._id for h in res.hits.hits
-            callback slugs
-
-
-    request: (method, type, path, payload, callback) ->
-        options =
-            host: @host
-            port: @port
-            method: method.toUpperCase()
-            path: @tenant + '/' + type + '/' + path
-
-        req = http.request options, (res) ->
-            res.setEncoding 'utf8'
-            res.on 'data', (data) -> callback JSON.parse data.toString()
-
-        req.on 'error', (e) ->
-            console.log 'ERROR: ' + e.message
-            callback false
-
-        req.write JSON.stringify(payload)
-        req.end()
-
-module.exports = Search


Added check for getCollection() to catch errors when calling methods on undefined collections.

--- src/server.coffee
+++ src/server.coffee
@@ -49,7 +49,8 @@

 getCollection = (name) ->
   name ?= 'Files'
-  Collections.get(name)
+  collection = Collections.get(name)
+  unless collection throw new Error('Cannot find collection with name ' + name)

 _.extend FileUtils,


Fix envelop typos

Fix envelop typos

--- src/adapters/campfire.coffee
+++ src/adapters/campfire.coffee
@@ -8,9 +8,9 @@
 class Campfire extends Adapter
   send: (envelope, strings...) ->
     if strings.length > 0
-      @bot.Room(envelop.room).speak strings.shift(), (err, data) =>
+      @bot.Room(envelope.room).speak strings.shift(), (err, data) =>
         @robot.logger.error "Campfire error: #{err}" if err?
-        @send envelop.user, strings...
+        @send envelope.user, strings...

   reply: (envelope, strings...) ->
     @send envelope, strings.map((str) -> "#{envelope.user.name}: #{str}")...


Remove extra spacing

Remove extra spacing

--- src/status.coffee
+++ src/status.coffee
@@ -117,7 +117,6 @@

   attach_event_listeners: (source) ->
     event_listeners = {
-
       "error": @error_listener,
       "ping_event": @ping_listener,
       "init_event": @initialize_listener,


Not using Utils.loadWebFont in TextLayerTests


Forward disconnect for Output component

Forward disconnect for Output component

--- src/components/Output.coffee
+++ src/components/Output.coffee
@@ -28,6 +28,9 @@
       @log data
       @outPorts.out.send data if @outPorts.out.isAttached()

+    @inPorts.in.on "disconnect", =>
+      @outPorts.out.disconnect()
+
     @inPorts.options.on "data", (data) =>
       @setOptions data


Removed duplicate entry.

Removed duplicate entry.

--- app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
+++ app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
@@ -376,10 +376,6 @@
         {
           value:    'envelope-to'
           name:     'Envelope-To'
-        },
-        {
-          value:    'delivered-to'
-          name:     'Delivered-To'
         },
         {
           value:    'return-path'


Delete adapter2.coffee

Delete adapter2.coffee

--- src/examples/adapter2.coffee
+++ src/examples/adapter2.coffee
@@ -1,79 +0,0 @@
-angular.module('application', ['ui.scroll', 'ui.scroll.jqlite', 'ngAnimate']).controller('mainController',
-	[ '$scope', '$log', '$timeout'
-		($scope, console, $timeout)->
-			datasource = {}
-
-			datasource.get = (index, count, success)->
-				$timeout(
-					->
-						result = []
-						actualIndex = index
-
-						start = Math.max(-4, actualIndex)
-						end = Math.min(actualIndex + count-1, 10)
-
-						if (start > end)
-							success result
-						else
-							for i in [start..end]
-								item = {}
-								item.id = i
-								item.content = "item #" + i
-								result.push item
-							success(result)
-					100
-				)
-
-			$scope.datasource =  datasource
-
-			# 1st list adapter implementation
-
-			$scope.firstListAdapter = remain: true
-
-			$scope.updateList1 = ->
-				$scope.firstListAdapter.applyUpdates (item, scope) ->
-					item.content += " *"
-
-			$scope.removeFromList1 = ->
-				$scope.firstListAdapter.applyUpdates (item) ->
-					if item.id % 2 == 0
-						[]
-
-			idList1 = 1000
-
-			$scope.addToList1 = ->
-				$scope.firstListAdapter.applyUpdates (item, scope) ->
-					if scope.$index == 2
-						newItem =
-							id: idList1
-							content: 'a new one #' + idList1
-						idList1++
-						[item, newItem]
-
-			# 2nd list adapter implementation
-
-			$scope.updateList2 = ->
-				$scope.second.list.adapter.update (scope) ->
-					scope.item.content += ' *'
-
-			$scope.removeFromList2 = ->
-				$scope.second.list.adapter.delete (scope) ->
-					scope.item.id % 2 != 0
-				return
-
-			idList2 = 2000
-
-			$scope.addToList2 = ->
-				$scope.second.list.adapter.insert 4,
-					id: idList2
-					content: 'a new one #' + idList2
-				idList2++
-				return
-
-	])
-
-angular.bootstrap(document, ["application"])
-
-###
-//# sourceURL=src/scripts/adapter.js
-###


bottom left chat icon panel toggle bug fixed

--- client/landing/app/Applications/Chat.kdapplication/Views/mainchatpanel.coffee
+++ client/landing/app/Applications/Chat.kdapplication/Views/mainchatpanel.coffee
@@ -22,7 +22,8 @@
       if visible
         KD.getSingleton('windowController').addLayer @
         @once 'ReceivedClickElsewhere', (event)=>
-          unless $(event.target).closest('.main-chat-handler').length > 0
+          unless $(event.target).closest('.main-chat-handler').length > 0 or\
+                 $(event.target).closest('.kdlistitemview-default.chat').length > 0
             @hidePanel()

     # FIXME Later ~ GG


Refactor tip initialization into simpler methods. :cake:

--- src/js/jquery.hlf.tip.coffee
+++ src/js/jquery.hlf.tip.coffee
@@ -188,32 +188,21 @@
       # Tip instances start off rendered and bound.
       @_render()
       @_bind()
-      # Process `$triggers` and setup content, event, and positioning aspects.
-      processTrigger = ($trigger) =>
-        return no if not $trigger.length
+      # Also setup any context bindings.
+      @_bindContext()
+      # Do this for initially provided triggers.
+      @_processTriggers()
       @_bindTriggers()
+
+    # Process `$triggers` and setup content, event, and positioning aspects.
+    _processTriggers: ($triggers) ->
+      $triggers ?= @$triggers
+      $triggers.each (i, el) =>
+        $trigger = $ el
+        return no unless $trigger.length
         $trigger.addClass @classNames.trigger
         @_saveTriggerContent $trigger
         @_updateDirectionByTrigger $trigger
-      # Do this for initially provided triggers.
-      @$triggers.each (i, el) => processTrigger $(el)
-      # If `doLiveUpdate` is inferred to be true, process triggers added in the
-      # future. Make sure to ignore mutations related to the tip.
-      @doLiveUpdate = window.MutationObserver?
-      if @doLiveUpdate
-        selector = @$triggers.selector
-        onMutations = (mutations) =>
-          for mutation in mutations
-            $target = $ mutation.target
-            continue if $target.hasClass(@classNames.content) # TODO: Limited.
-            if mutation.addedNodes.length
-              $triggers = $(mutation.addedNodes).find('[title],[alt]') # TODO: Limited.
-              $triggers.each (i, el) => processTrigger $(el)
-              @$triggers = @$triggers.add $triggers
-        @_mutationObserver = new MutationObserver onMutations
-        @_mutationObserver.observe @$context[0],
-          childList: yes
-          subtree: yes

     # `_defaultHtml` provides a basic html structure for tip content. It can be
     # customized via the `tipTemplate` external option, or by subclasses using
@@ -249,6 +238,24 @@
         if canRemoveAttr then $trigger.removeAttr attr
       if content?
         $trigger.data @attr('content'), content
+
+    # `_bindContext` uses MutationObserver. If `doLiveUpdate` is inferred to be
+    # true, process triggers added in the future. Make sure to ignore mutations
+    # related to the tip.
+    _bindContext: ->
+      return false unless window.MutationObserver?
+      selector = @$triggers.selector
+      @_mutationObserver = new MutationObserver (mutations) =>
+        for mutation in mutations
+          $target = $ mutation.target
+          continue if $target.hasClass(@classNames.content) # TODO: Limited.
+          if mutation.addedNodes.length
+            $triggers = $(mutation.addedNodes).find('[title],[alt]') # TODO: Limited.
+            @_processTriggers $triggers
+            @$triggers = @$triggers.add $triggers
+      @_mutationObserver.observe @$context[0],
+        childList: yes
+        subtree: yes

     # `_bindTriggers` links each trigger to the tip for: 1) possible appearance
     # changes during mouseenter, mouseleave (uses special events) and 2)


build-client: spawn process if program exists

--- build-client.coffee
+++ build-client.coffee
@@ -307,11 +307,12 @@
       else if changedCSS.length > 0
         cprojects = changedCSS.join " "
       if cprojects
-        spawn.apply null, ["osascript", [
-          "-e", """
-            display notification "#{cprojects}" with title "Koding projects compiled"
-          """]
-        ]
+        if fs.existsSync '/usr/bin/osascript'
+          spawn.apply null, ["osascript", [
+            "-e", """
+              display notification "#{cprojects}" with title "Koding projects compiled"
+            """]
+          ]

     if args.watch is yes
       if initial
@@ -427,7 +428,8 @@

           catch error
             log.error "CoffeeScript Error in #{file.includePath}: #{(error.stack.split "\n")[0]}"
-            spawn.apply null, ["say", ["coffeescript error"]]
+            if fs.existsSync '/usr/bin/say'
+              spawn.apply null, ["say", ["coffeescript error"]]
             file.cacheTime = sourceTime # avoid repeated error
             return
         else


better help text

better help text

--- index.coffee
+++ index.coffee
@@ -152,6 +152,12 @@
           help += " — _#{command.description}_"
         help += "\n"

+      if _.values(customCommands).length > 0
+        spaces = lookers.filter((l) -> l.customCommandSpaceId ).map((l) ->
+          "<#{l.url}/spaces/#{l.customCommandSpaceId}|this space>"
+        ).join(" or ")
+        help += "\n To add your own custom command, add a dashboard to #{spaces}."
+
       replyPrivateIfPossible(help)

     refreshCommands()