shortcuts: handle changes

shortcuts: handle changes

--- client/app/lib/shortcuts/index.coffee
+++ client/app/lib/shortcuts/index.coffee
@@ -8,24 +8,25 @@
 cloneArray     = require 'app/util/cloneArray'

 STORAGE_NAME    = 'shortcuts'
-STORAGE_VERSION = '1'
-THROTTLE_WAIT   = 300
+STORAGE_VERSION = '4'
+THROTTLE_WAIT   = 500

 module.exports =

+# Manages keyboard shortcuts.
+#
+# Wraps over a _shortcuts_ instance by default (if not passed explicitly
+# within opts.shortcuts), and makes sure we are listening and dispatching
+# correct keyboard events depending on an application's config.
+#
+# This also exposes convenience proxy methods to the underlying _keyconfig_
+# instance, and persists the state of a keyconfig#Collection to the app storage.
+#
 class ShortcutsController extends events.EventEmitter

   klass = this

   constructor: (opts={}) ->
-    # manages keyboard shortcuts.
-    #
-    # wraps over a _shortcuts_ instance by default (if not passed explicitly
-    # within opts.shortcuts), and makes sure we are listening and dispatching
-    # correct keyboard events depending on an application's config.
-    #
-    # this also exposes convenience proxy methods to get or update the underlying
-    # _keyconfig_ instance, and persist the state of key-bindings to the server.

     super()

@@ -34,13 +35,23 @@
     if opts.shortcuts instanceof Shortcuts
       @shortcuts = opts.shortcuts
     else
-      raw = _.keys(defaults).reduce (acc, key) ->
-        acc[key] = defaults[key].data
-        return acc
-      , {}
-      @shortcuts = new Shortcuts raw
-
-
+      @shortcuts = new Shortcuts _.keys(defaults).reduce (acc, key) ->
+          acc[key] = defaults[key].data
+          return acc
+        , {}
+
+    @_buffer = @shortcuts.config.reduce (acc, collection) ->
+      acc[collection.name] = []
+      return acc
+    , {}
+
+
+  # Party starts here.
+  #
+  # This adds the necessary event listeners, and fetches changes.
+  # Make sure to call this after _appStorageController_ & _appManager_
+  # singletons are set.
+  #
   addEventListeners: ->

     { appManager, appStorageController } = kd.singletons
@@ -50,33 +61,84 @@
       version : STORAGE_VERSION
       fetch   : no

-    # make sure we are not accessing storage until its ready.
-    # this also updates kb bindings which are initially listening
-    # for default combos.
     @_store.once 'ready', _.bind @_handleStoreReady, this

-    # add valid kb event listeners and invalidate obsolete upon
-    # front application is set and ready
+    # Add valid keyboard event listeners and invalidate obsolete upon
+    # front application is set and ready.
     appManager.on 'FrontAppChange', _.bind @_handleFrontAppChange, this

     @_store.fetchStorage null, yes

-  _save: _.throttle (collection, model) ->
+  # Persists changes to app storage.
+  #
+  # This is a throttled method and will only be called per every
+  # _THROTTLE_WAIT_ milliseconds.
+  #
+  _save: _.throttle ->

     throw 'could not persist changes'  unless @_store.isReady
-    # also means: do not call this method explicitly, use #update
-
-    throw 'not implemented :trollface:'
+
+    for key, models of @_buffer
+
+      set = []
+
+      while (model = @_buffer[key].pop())?
+        value =
+        _
+          .chain(model.toJSON())
+          # since every platform has independent storages, we are only
+          # saving this platform's bindings
+          .extend binding: klass._getPlatformBinding model
+          .pick 'binding', 'options', 'name' # and nothing more
+          .tap (obj) -> # to validate
+            if _.isObject obj.options
+              # the only whitelisted options prop is _enabled_
+              obj.options = _.pick obj.options, 'enabled'
+              delete obj.options  \
+                if (_.isEmpty obj.options) or (not _.isBoolean obj.options.enabled)
+            if (not _.isArray obj.binding) or _.isEmpty obj.binding
+              delete obj.binding
+          .value()
+
+        set.push value  unless _.isEmpty value
+
+      unless _.isEmpty set
+        @_store.setValue key, set,
+          _
+            .chain (collectionName, modelNames) ->
+              _.each modelNames, (modelName) =>
+                collection = @shortcuts.get collectionName
+                @emit 'change', collection, collection.find name: modelName
+            .partial key, _.pluck set, 'name'
+            .bind @
+            .value()

   , THROTTLE_WAIT,
     leading  : no
     trailing : yes

+  # Buffers up changed models.
+  #
+  # This is necessary for not to exhaust given resources, since
+  # we are listening changes in _keyconfig#Model_ level.
+  #
+  # That is to say, each _keyconfig#Model_ change dispatches this.
+  #
+  _handleShortcutsChange: (collection, model) ->
+
+    queue = @_buffer[collection.name]
+    idx   = _.findWhere queue, name: model.name
+
+    queue[((~idx and idx) or queue.length)] = model
+
+    @_save()
+
+
+  # Restores overrides when storage is ready, and starts listening for changes.
+  #
   _handleStoreReady: ->
-
-    osIndex = if globals.os is 'mac' then 1 else 0

     @shortcuts.config.each (collection) =>
       collectionName = collection.name
@@ -89,46 +151,19 @@
           model = @shortcuts.get collectionName, obj.name
           return  unless model

-          silent = if model.options?.custom then yes else no
-
-          # -------------------------------
-          # about custom shortcuts (eg ace)
-          # -------------------------------
-          #
-          # i don't really want to embed such logic into _shortcuts_; it should be
-          # totally 'shortcut type' agnostic and api provides the necessity to deal
-          # with such cases indeed.
-          #
-          # passing 'silent' as the 4th argument to shortcuts#update makes sure its
-          # internal keyconfig#change listener won't get dispatched anyhow, thus
-          # rendering the model in question unbound. plus, there is the _options_
-          # object you can use to denote such weirdos.
-          #
-          # just make sure you don't put any 'custom shortcut' set names
-          # into a bant.json file, since these sets are automatically thought to
-          # be _shortcuts_ compatible.
-
-          binding = null
-
-          if _.isArray obj.binding
-            binding = cloneArray model.binding
-            binding[osIndex] = [].concat(obj.binding).filter _.isString
-
           @emit 'change', collection,
-            shortcuts.update collectionName, obj.name,
-              binding: binding
-              options: options
-            , silent
-
-    # persist to app storage whenever a shortcut is changed.
-    # this should handled with some care to avoid exhausting
-    # resources, since _shortcuts_ emits all changes.
-    @shortcuts.on 'change', _.bind @_save, this
-
-
+            @shortcuts.update collectionName, model.name,
+              binding: klass._insertPlatformBinding model, obj.binding
+              options: obj.options
+            , klass._isCustomShortcut model
+
+    @shortcuts.on 'change', _.bind @_handleShortcutsChange, this
+
+
+  # Invalidates previous app's keyboard events and adds new
+  # listeners for the current one.
+  #
   _handleFrontAppChange: (app, prevApp) ->
-    # invalidates previous app's keyboard events and adds new
-    # listeners for the current one

     appId     = app.canonicalName
     prevAppId = prevApp?.canonicalName
@@ -144,55 +179,135 @@
         @shortcuts.on "key:#{key}", app.bound 'handleShortcut'

+  # Renders a _kd#Modal_ immediately.
+  #
   showModal: ->
-    # renders a kd.Modal immediately

     new ShortcutsModal {}, @shortcuts.config

+  # Proxy to _shortcuts#get_.
+  #
+  # See: http://github.com/koding/shortcuts#api
+  #
   get: ->
-    # proxy to shortcuts#get
-    # see: http://github.com/koding/shortcuts#api

     @shortcuts.get.apply @shortcuts, Array::slice.call arguments

-
-  update: ->
-    # proxy to shortcuts#update
-    # see: http://github.com/koding/shortcuts#api
+  # Updates a _keyconfig#Model_.
+  #
+  # This api is pretty-much same with _shortcuts#update_.
+  #
+  # One important difference is _silent_ argument is handled internally,
+  # and not exposed. That's because passing _silent_ to _shortcuts#update_
+  # as true makes sure _shortcuts_ doesn't add any keyboard event listeners,
+  # yet it still updates the underlying _keyconfig_ instance.
+  #
+  # This is how we deal with shortcuts that should be handled manually.
+  #
+  update: (collectionName, modelName, value) ->
+
+    throw 'value must be an object'  unless _.isObject value
+
+    overrides = _.pick value, 'binding', 'options'
+
+    unless Object.keys(overrides).length
+      throw 'value should contain \'binding\' and/or \'options\' props'

     unless @_store.isReady
       console.warn 'changes won\'t be persisted, since storage is not ready yet'

-    @shortcuts.update @shortcuts, Array::slice.call arguments
-
-
-  getJSON: (name, filterFn) ->
-    # convenience method that returns a collection's json repr.
-    # this method omits all binding entries that is not compatible
-    # with the current os/keymap type.
+    model = @shortcuts.get collectionName, modelName
+    throw "#{modelName} not found"  unless model
+
+    if overrides.binding
+      overrides.binding = klass._insertPlatformBinding model, overrides.binding
+
+    silent = klass._isCustomShortcut model
+
+    res = @shortcuts.update collectionName, modelName, overrides, silent
+
+    if silent then @_handleShortcutsChange @shortcuts.get(collectionName), res
+
+    return res
+
+
+  # Convenience method that returns a _keyconfig#Collection's_ json representation
+  # for the current platform. Optionally takes a filter predicate.
+  #
+  getJSON: (name, predicate) ->

     set = @shortcuts.get(name)?.chain()

     if set
-      if filterFn then set = set.filter filterFn
+      if predicate then set = set.filter predicate
+
       set.map (model) ->
-        _.extend model.toJSON(), binding: klass.getPlatformBindings model
+        _.extend model.toJSON(), binding: klass._getPlatformBinding model
       .value()

-  @bindingsGetterMethodName: _.memoize ->
-    # returns either "getMacKeys" or "getWinKeys"
-    # see: http://github.com/koding/keyconfig
+  # Returns the app storage key for the current platform.
+  #
+  # This will either be *shortcuts-mac* or *shortcuts-win*.
+  #
+  @getPlatformStorageKey: ->
+
+    "#{STORAGE_NAME}-#{globals.keymapType}"
+
+
+  # Returns _binding_ getter method name of a _keyconfig#Model_ for the current platform.
+  #
+  # This is memoized, so subsequent calls will return the cached result;
+  # it still must be defined at static level since _globals_ must be ready.
+  #
+  # This will either be *getMacKeys* or *getWinKeys*.
+  #
+  # See: http://github.com/koding/keyconfig#api
+  #
+  @_bindingGetterMethodName: _.memoize ->

     kmt = globals.keymapType
     return "get#{kmt.charAt(0).toUpperCase()}#{kmt.slice 1, 3}Keys"

-  @getPlatformBindings: (model) ->
-    # given a keyconfig model, returns its bindings for this platform
-
-    return model[klass.bindingsGetterMethodName()]()
-
-
-  @getPlatformStorageKey: -> "#{STORAGE_NAME}-#{globals.keymapType}"
+  # Given a _keyconfig#Model_, returns its binding for the current platform.
+  #
+  @_getPlatformBinding: (model) ->
+
+    return model[klass._bindingGetterMethodName()]()
+
+
+  # Returns _binding_ array index of a _keyconfig#Model_ for the current platform.
+  #
+  # This is memoized, so subsequent calls will return the cached result;
+  # it still must be defined at static level since _globals_ must be ready.
+  #
+  # This will either be 1 for mac or 0 for windows/linux.
+  #
+  # See: http://github.com/koding/keyconfig#spec
+  #
+  @_bindingPlatformIndex: _.memoize ->
+
+    if globals.keymapType is 'mac' then 1 else 0
+
+
+  # Given a keyconfig#Model and a 1d array of bindings, validates and inserts
+  # the array at the correct position for the current platform, and returns
+  # a deep clone of the model's binding.
+  #
+  @_insertPlatformBinding: (model, platformBinding) ->
+
+    binding = cloneArray model.binding
+
+    if _.isArray platformBinding
+      binding[klass._bindingPlatformIndex()] = platformBinding.filter _.isString
+
+    return binding
+
+
+  # Returns *true* if given _keyconfig#Model_ is a custom shortcut.
+  #
+  @_isCustomShortcut: (model) ->
+
+    if model.options?.custom then yes else no


Improved: code

Improved: code

--- client/app/models/scheduleitem.coffee
+++ client/app/models/scheduleitem.coffee
@@ -220,8 +220,7 @@
     confirmSendEmails: (callback) ->
         attendees = @get('attendees') or []
         guestsToInform = attendees.filter (guest) ->
-            return guest.status is 'INVITATION-NOT-SENT' or \
-                   (guest.status is 'ACCEPTED' and dateChanged)
+            return guest.status in ['INVITATION-NOT-SENT', 'ACCEPTED']
         .map (guest) -> guest.email

         if guestsToInform.length is 0


Trimming superfluous code

Trimming superfluous code

--- lib/bash.coffee
+++ lib/bash.coffee
@@ -1,6 +1,6 @@
+module.exports = {

-Bash = () ->
-  _opt_to_args = (options) ->
+  options_to_args: (options) ->
     args = for key, val of options
       prepend = if key.length == 1 then "-" else "--"
       assign = if key.length == 1 then " " else "="
@@ -10,10 +10,6 @@
         "#{prepend}#{key}#{assign}#{val}"
       else
         ""
-    return args.join " "
+    args.join " "

-  return {
-    options_to_args: _opt_to_args
-  }
-
-module.exports = Bash()+}


:art: Move Gutter event methods up

:art: Move Gutter event methods up

Need for generated docs

--- src/gutter.coffee
+++ src/gutter.coffee
@@ -25,6 +25,23 @@
       @emitter.emit 'did-destroy'
       @emitter.dispose()

+  # Calls your `callback` when the {Gutter}'s' visibility changes.
+  #
+  # * `callback` {Function}
+  #  * `gutter` The {Gutter} whose visibility changed.
+  #
+  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
+  onDidChangeVisible: (callback) ->
+    @emitter.on 'did-change-visible', callback
+
+  # Calls your `callback` when the {Gutter} is destroyed
+  #
+  # * `callback` {Function}
+  #
+  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
+  onDidDestroy: (callback) ->
+    @emitter.on 'did-destroy', callback
+
   hide: ->
     if @visible
       @visible = false
@@ -47,20 +64,3 @@
   # Returns a {Decoration} object.
   decorateMarker: (marker, options) ->
     @gutterContainer.addGutterDecoration(this, marker, options)
-
-  # Calls your `callback` when the {Gutter}'s' visibility changes.
-  #
-  # * `callback` {Function}
-  #  * `gutter` The {Gutter} whose visibility changed.
-  #
-  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
-  onDidChangeVisible: (callback) ->
-    @emitter.on 'did-change-visible', callback
-
-  # Calls your `callback` when the {Gutter} is destroyed
-  #
-  # * `callback` {Function}
-  #
-  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
-  onDidDestroy: (callback) ->
-    @emitter.on 'did-destroy', callback


fix a typo

fix a typo

--- client/landing/app/MainApp/avatararea/avatarareagroupswitcherpopup.coffee
+++ client/landing/app/MainApp/avatararea/avatarareagroupswitcherpopup.coffee
@@ -74,7 +74,7 @@

     @switchLink = new CustomLinkView
       title       : title
-      href        : "/#{slug}"
+      href        : "/#{slug}/Activity"
       target      : slug
       icon        :
         cssClass  : 'new-page'
@@ -87,11 +87,11 @@
     JView::viewAppended.call this

     {group:{slug}, roles} = @getData()
-
+
     dashboardHref = "/#{slug}/Dashboard"

     if 'admin' in roles
-      @addSubView new KDCustomHTMLView
+      @addSubView new CustomLinkView
         title     : 'Admin dashboard'
         href      : dashboardHref
         click     : (event)->
@@ -103,7 +103,8 @@
     """
     {{> @avatar}}

-      {{> @switchLink}}#{roles.join ', '}
+      {{> @switchLink}}
+      #{roles.join ', '}

     """


Fixed a problem, where clicking a public transit stop always fired a map click event, by changing leaflet circle overlay to marker

--- src/map-view.coffee
+++ src/map-view.coffee
@@ -338,9 +338,12 @@
         drawPublicTransitStops: (stops, route) ->
             for stop in stops
                 latLng = L.latLng(stop.lat, stop.lon)
-                marker = L.circle latLng,
+
+                marker = L.marker latLng,
                     clickable: true
-                marker.setRadius 3
+                    icon: new L.DivIcon
+                        iconSize: L.point [10, 10]
+
                 do (stop) ->
                     marker.on 'click', (e) ->
                         publicTransitStop = new PublicTransitStopView {stop, route}


Minor UPI update

Minor UPI update

--- lib/upi-consumer.coffee
+++ lib/upi-consumer.coffee
@@ -51,10 +51,10 @@
   upi: null
   process: null

-  constructor: (service, @process) ->
+  constructor: (register, @process) ->
     @disposables = new CompositeDisposable

-    @upi = service.register
+    @upi = register
       name: 'haskell-ghc-mod'
       menu: @mainMenu
       messageTypes: @messageTypes


Set base_matches to the correct base_analysis.sequence in repeat_match()

--- src/matching.coffee
+++ src/matching.coffee
@@ -331,7 +331,7 @@
         base_token
         @omnimatch base_token
       )
-      base_matches = base_analysis.match_sequence
+      base_matches = base_analysis.sequence
       base_guesses = base_analysis.guesses
       matches.push
         pattern: 'repeat'


исправления в регулярные выражения

исправления в регулярные выражения


remove old code

remove old code

--- lib/init.coffee
+++ lib/init.coffee
@@ -245,11 +245,6 @@
 doc.getElementById('lupa-refresh').addEventListener('click', refresh)

 update1 = ->
-    # TODO this is copy pasted§
-    if dashboard
-        plugin.filterFiles((v) -> v).toArray().subscribe( (files)->
-            #dashboard.setFiles(files, addLabelDecoration) # TODO remove addLabelDecoration from here. This is hack
-        )

     identitity = (v) ->
         v


#515, fix don't throw error on single pane, and update flexScale only once per pane or paneAxis

--- lib/main.coffee
+++ lib/main.coffee
@@ -117,12 +117,13 @@
       classList.remove('hide-tab-bar')

   equalizePanes: ->
-    for pane in atom.workspace.getPanes()
-      pane.setFlexScale 1
-      # I couldn't figure out a way to get all the PaneAxis in the workspace, this might result in some
-      # redundant sets
-      pane.parent.setFlexScale 1
+    setFlexScale = (base, newFlexScale) ->
+      base.setFlexScale(newFlexScale)
+      for child in base.children ? []
+        setFlexScale(child, newFlexScale)

+    root = atom.workspace.getActivePane().getContainer().getRoot()
+    setFlexScale(root, 1)

   registerVimStateCommands: ->
     # all commands here is executed with context where 'this' binded to 'vimState'


Slight changes to grammarToKernel description

--- lib/main.coffee
+++ lib/main.coffee
@@ -28,12 +28,12 @@
             type: 'string'
             default: '{}'
         grammarToKernel:
-            description: 'JSON mappings between specific kernel and
-                          language/grammar. This value updated automatically
-                          by switch kernel command. So if you switch from
-                          python2 to python3, this kernel would be used next
-                          time. It means, you probabbly shouldn\'t
-                          change this by hand'
+            description: 'JSON mappings between specific kernel and a
+                          language/grammar. This value is updated automatically
+                          by the "switch kernel" command. If you switch from
+                          python2 to python3, python3 will be used the next time
+                          you open a Python file. You probabbly shouldn\'t
+                          change this by hand.'
             type: 'string'
             default: '{}'


reset cwd on breakin

reset cwd on breakin

--- lib/main.coffee
+++ lib/main.coffee
@@ -24,6 +24,7 @@
           return socket.broken = true

         if msg is 'breakin'
+          socket.cwd = process.cwd()
           return socket.broken = false

         if socket.broken


Listitem: cleanup

Listitem: cleanup

--- components/list/KDListItemView.coffee
+++ components/list/KDListItemView.coffee
@@ -29,20 +29,20 @@

   dim:->

-    @getDomElement().addClass "dimmed"
+    @setClass "dimmed"

   undim:->

-    @getDomElement().removeClass "dimmed"
+    @unsetClass "dimmed"

   highlight:->

+    @undim()
     @setClass "selected"
-    @unsetClass "dimmed"

   removeHighlight:->

+    @undim()
     @unsetClass "selected"
-    @unsetClass "dimmed"

   getItemDataId:-> @getData().getId?() or @getData().id or @getData()._id


Adds referring domain o tracking data

Adds referring domain o tracking data

--- src/remote/services/hull.coffee
+++ src/remote/services/hull.coffee
@@ -20,6 +20,7 @@
       url: app.config.data.request.url.href
       path: app.config.data.request.url.path
       referrer: app.config.data.request.referrer?.href
+      referring_domain: app.config.data.request.referrer?.host
       browser_id: getBrowserId()
       session_id: getSessionId()
     identify = (me) ->


Batch API

Batch API

--- src/remote/services/hull.coffee
+++ src/remote/services/hull.coffee
@@ -6,11 +6,95 @@
   (app)->

     config = app.config
-
     identified = false
-
     accessToken     = app.config.access_token
     originalUserId  = app.config.data?.me?.id
+
+    # Stores calls to flushCall, and processes them at once.
+    batchable = (opts) ->
+      delay                                = opts.delay
+      timeout                              = null
+      batchMinSize                         = opts.minSize
+      batchMaxSize                         = opts.maxSize
+      requests                             = []
+      request_headers                      = { 'Hull-App-Id': config.appId }
+      request_headers['Hull-Access-Token'] = accessToken if accessToken
+
+      flush = ()->
+        timeout = null
+        flushedRequests = _.first(requests,batchMaxSize)
+        requests = _.rest(requests,batchMaxSize)
+        return if flushedRequests.length == 0
+
+        if flushedRequests.length<=batchMinSize
+
+          $.ajax(flushedRequests[0].request).then (response, status, request)->
+
+            headers = _.reduce RESPONSE_HEADER, (memo, name) ->
+              value = request.getResponseHeader(name)
+              memo[name] = value if value?
+              memo
+            , {}
+
+            flushedRequests[0].deferred.resolve({response:response, headers:headers})
+          , (error)->
+            flushedRequests[0].deferred.reject(error)
+        else
+          formatted_request =
+            ops: _.map(flushedRequests, (r)->
+              req = r.request
+              return {
+                url: req.url
+                method: req.type
+                params: req.data
+                headers: req.headers
+              })
+            sequential:true
+
+          req =
+            url:  '/api/v1/batch'
+            type: 'post'
+            data: JSON.stringify(formatted_request)
+            contentType: 'application/json'
+            headers: request_headers
+            dataType: 'json'
+
+          # Apply stored params to flush call
+          $.ajax.call(this, req).then (responses)->
+            _.each responses.results, (response, i)->
+              response.response = JSON.parse(response.body)
+
+              # response.headers = _.reduce RESPONSE_HEADER, (memo, name) ->
+              #   value = response.headers[name]
+              #   memo[name] = value if value?
+              #   memo
+              # , {}
+
+              flushedRequests[i].deferred.resolve(response)
+          , (error)->
+            _.map _.pluck(flushedRequests, 'deferred'), (d)-> d.reject(error)
+
+        true
+
+      (request) ->
+        context = this;
+        deferred = $.Deferred()
+
+        # Store one more requests into the request queue
+        request.headers = request_headers
+        requests.push({ request: request, deferred: deferred })
+        # When we call the method, clear the queue
+
+        flush() while requests.length > batchMaxSize
+        timeout ?= setTimeout(flush, delay || 30)
+
+        deferred.promise()
+
+    batchableRequest = batchable
+      delay:30
+      minSize: 1
+      maxSize: 10
+

     identify = (me) ->
       return unless me
@@ -42,35 +126,25 @@
       else
         req_data = req.params

-      request_headers = { 'Hull-App-Id': config.appId }
-      if accessToken
-        request_headers['Hull-Access-Token'] = accessToken
-
-      request = $.ajax
+      request = batchableRequest({
         url: url
         type: req.method
         data: req_data
         contentType: 'application/json'
         dataType: 'json'
-        headers: request_headers
+      })

       request.done (response)->
-        identify(_.clone(response)) if url == '/api/v1/me'
+        identify(_.clone(response.response)) if url == '/api/v1/me'

-        headers = _.reduce RESPONSE_HEADER, (memo, name) ->
-          value = request.getResponseHeader(name)
-          memo[name] = value if value?
-          memo
-        , {}
-
-        if accessToken && originalUserId && originalUserId != headers['Hull-User-Id']
+        if accessToken && originalUserId && originalUserId != response.headers['Hull-User-Id']
           # Reset token if the user has changed...
           accessToken = false

-        callback({ response: response, headers: headers, provider: 'hull' }) if _.isFunction(callback)
+        response.provider = 'hull'
+        callback(response) if _.isFunction(callback)

-        trackAction(request, response)
-
+        trackAction(response)

       request.fail(errback)

@@ -82,8 +156,8 @@
       params.hull_app_name  = config?.data?.app?.name
       require('analytics').track(event, params)

-    trackAction = (request, response)->
-      return unless track = request.getResponseHeader('Hull-Track')
+    trackAction = (response)->
+      return unless track = response.headers['Hull-Track']
       try
         [eventName, trackParams] = JSON.parse(atob(track))
         doTrack(eventName, trackParams)


Add to window when in the browser, listen for blur

--- src/phalange.coffee
+++ src/phalange.coffee
@@ -9,7 +9,7 @@
   events:
     "click": "append"
     "submit": "submit"
-    "blur": "submit"
+    "blur input": "submit"

   submit: ->
     @text = @_input().val()
@@ -45,4 +45,7 @@
     """
     $(template)

-module?.exports = Phalange
+if module?
+  module.exports = Phalange
+else
+  window.Phalange = Phalange


Enforce a collection name length of at least 1

--- app/collections/create-form.cjsx
+++ app/collections/create-form.cjsx
@@ -14,6 +14,7 @@

   getInitialState: ->
     error: null
+    collectionNameLength: 0

   onSubmit: (e) ->
     e.preventDefault()
@@ -39,6 +40,9 @@
         @refs.private.value = true
         @props.onSubmit collection

+  handleNameInputChange: ->
+    @setState collectionNameLength: React.findDOMNode(@refs.name).value.length
+
   render: ->

       <form onSubmit={@onSubmit} className="collections-create-form">
@@ -49,14 +53,17 @@
             else 'There was a problem creating your collection.'

{errorMessage}
}
-        <input className="collection-name-input" ref="name" placeholder="Collection Name" />
+        <input className="collection-name-input" ref="name" onChange={@handleNameInputChange} placeholder="Collection Name" />

             <input ref="private" type="checkbox" defaultChecked={true}/>
             <Translate content="collectionCreateForm.private" />

-            <button type="submit"><Translate content="collectionCreateForm.submit" /></button>
+            {if @state.collectionNameLength is 0
+              <button type="submit" disabled><Translate content="collectionCreateForm.submit" /></button>
+            else
+              <button type="submit"><Translate content="collectionCreateForm.submit" /></button>}

       </form>


FrameViewer should pass frame up on load

--- app/components/frame-viewer.cjsx
+++ app/components/frame-viewer.cjsx
@@ -144,4 +144,4 @@
         width: width ? 0
         height: height ? 0

-    @props.onLoad? arguments...
+    @props.onLoad? e, @props.frame


Refactor coffee source to match the last PR changes

--- src/parker.coffee
+++ src/parker.coffee
@@ -81,24 +81,28 @@
                         ]

                     if aResults.length
-                        grunt.log.writeln()
-                        grunt.log.writeln chalk.underline sFilePath
-                        grunt.log.writeln()
+                        unless oOptions.file
+                            grunt.log.writeln()
+                            grunt.log.writeln chalk.underline sFilePath
+                            grunt.log.writeln()
                         for aResult in aResults
                             sValue = switch grunt.util.kindOf aResult[ 1 ]
                                 when "array"
-                                    grunt.log.writeln chalk.cyan( "#{ aResult[ 0 ] }:" )
-                                    grunt.log.writeln "\t#{ sResult }" for sResult in aResult[ 1 ]
+                                    unless oOptions.file
+                                        grunt.log.writeln chalk.cyan( "#{ aResult[ 0 ] }:" )
+                                        grunt.log.writeln "\t#{ sResult }" for sResult in aResult[ 1 ]
                                     aFileResults.push "- **#{ aResult[ 0 ] }:**"
                                     for sResult in aResult[ 1 ]
                                         if sResult.substring( 0, 1 ) is "#"
                                             sResult = "`#{ sResult }`"
                                         aFileResults.push "\t- #{ sResult }"
                                 when "number"
-                                    grunt.log.writeln chalk.cyan( "#{ aResult[ 0 ] }:" ), chalk.yellow aResult[ 1 ]
+                                    unless oOptions.file
+                                        grunt.log.writeln chalk.cyan( "#{ aResult[ 0 ] }:" ), chalk.yellow aResult[ 1 ]
                                     aFileResults.push "- **#{ aResult[ 0 ] }:** #{ aResult[ 1 ] }"
                                 else
-                                    grunt.log.writeln chalk.cyan( "#{ aResult[ 0 ] }:" ), aResult[ 1 ]
+                                    unless oOptions.file
+                                        grunt.log.writeln chalk.cyan( "#{ aResult[ 0 ] }:" ), aResult[ 1 ]
                                     aFileResults.push "- **#{ aResult[ 0 ] }:** #{ aResult[ 1 ] }"

                 if oOptions.file and aFileResults.length


Increase Player drag to reduce 'slide'.

Increase Player drag to reduce 'slide'.

--- src/player.coffee
+++ src/player.coffee
@@ -84,7 +84,7 @@

     _initPhysics: ->
       @physics.collideWorldBounds = on
-      @physics.drag.x = 200
+      @physics.drag.x = 800
       h = @sprite.height
       w = @sprite.width
       @_yOffset = h / 4


fix bug where focus on the link tooltip textbox is stolen

--- src/renderer.coffee
+++ src/renderer.coffee
@@ -119,8 +119,8 @@
     this.runWhenLoaded( =>
       doc = this.getDocument()
       doc.body.appendChild(@root)
-      ScribeDOM.addEventListener(doc.body, 'click', =>
-        @root.focus()
+      ScribeDOM.addEventListener(doc.body, 'click', (event) =>
+        @root.focus() if event.target?.tagName == 'BODY'
       )
       ScribeDOM.addEventListener(@container, 'click', =>
         @root.focus()


change default_mode buttonbar text into working code

--- src/smc-webapp/buttonbar.coffee
+++ src/smc-webapp/buttonbar.coffee
@@ -1073,7 +1073,7 @@
         mode_cython:
             insert : "%cython\n"
         mode_default_mode:
-            insert : "%default_mode mode_name\n"
+            insert : "%default_mode r # change r to any mode\n"
         mode_exercise:
             insert : "%exercise\n"
         mode_gap:


Remove projects tabs

Remove projects tabs

--- src/smc-webapp/desktop_app.cjsx
+++ src/smc-webapp/desktop_app.cjsx
@@ -298,7 +298,7 @@
             {<GlobalInformationMessage /> if @props.show_global_info}
             {<Navbar className="smc-top-bar" style={style_top_bar}>
                 {@render_project_nav_button() if @props.is_logged_in}
-                {<ProjectsNav dropdown={false} /> and not @props.is_anonymous}
+                {<ProjectsNav dropdown={false} /> if not @props.is_anonymous}
                 {@render_right_nav()}
             </Navbar> if not @props.fullscreen}
             {<div className="smc-sticky-position-hack" style={minHeight:positionHackHeight}> </div> if not @props.fullscreen}


Update FERENGI2 to 40 percent

Update FERENGI2 to 40 percent

--- app/models/subject.coffee
+++ app/models/subject.coffee
@@ -95,9 +95,9 @@
   # For iPhone apps there will also be a delay until the updated App is approved.
   @randomSurveyId: ->
     n = Math.random()
-    if n <= (1/3)
+    if n <= (0.3)
       @::surveys.gama09.id
-    else if n < (2/3)
+    else if n < (0.6)
       @::surveys.sdss_lost_set.id
     else
       @::surveys.ferengi_2.id


Add some test to understand xml2js parser.

--- src/spec/xmlhelpers.spec.coffee
+++ src/spec/xmlhelpers.spec.coffee
@@ -31,7 +31,9 @@

 describe "#xmlVal", ->
   it "works", ->
-    xml = "<root><row><id>foo</id></row></root>"
+    xml = "<root><row>foo123</row></root>"
     xmlHelpers.xmlTransform xml, (err, result) ->
-      expect(xmlHelpers.xmlVal(result.root.row[0], 'id')).toBe 'foo'
-      expect(xmlHelpers.xmlVal(result.root.row[0], 'bar', 'default')).toBe 'default'
+      console.log "R %j", result
+      expect(xmlHelpers.xmlVal(result.root.row[0], 'code')).toBe 'foo'
+      expect(result.root.row[0]['_']).toBe '123'
+      expect(xmlHelpers.xmlVal(result.root.row[0], 'foo', 'defaultValue')).toBe 'defaultValue'


Remove debugging statements.

Remove debugging statements.

--- src/resolver.coffee
+++ src/resolver.coffee
@@ -20,7 +20,6 @@

   # Build list of valid node_modules paths.
   modulePaths = do (root) ->
-    console.log root
     last = ''
     paths = []
     for path in root.split sep
@@ -31,8 +30,6 @@
     if process.env.NODE_PATH
       paths = paths.concat process.env.NODE_PATH.split ':'
     paths
-
-  console.log modulePaths

   # Resolve directory/npm module to index/main file.
   resolveDirectory = (path, cb) ->


Implement a more flexible custom binder/routine interface such that all special binding logic can be self-contained within their own binder.

Binders/routines can now be defined as an object where additional
properties can be defined (setup and teardown callbacks that get called
on bind() and unbind(), traversal blocking (for iteration-type
bindings), whether the binding published or not (so that view.publish()
will catch your custom binding), etc.).

--- src/rivets.coffee
+++ src/rivets.coffee
@@ -15,19 +15,21 @@
   # element, the type of binding, the model object and the keypath at which
   # to listen for changes.
   constructor: (@el, @type, @model, @keypath, @options = {}) ->
-    @routine = switch @options.special
-      when 'event'     then eventBinding @type
-      when 'class'     then classBinding @type
-      when 'iteration' then iterationBinding @type
-      else Rivets.routines[@type] || attributeBinding @type
+    unless @binder = Rivets.routines[type]
+      for identifier, value of Rivets.routines
+        if identifier isnt '*' and identifier.indexOf('*') isnt -1
+          regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
+          if regexp.test type
+            @binder = value
+            @args = new RegExp("^#{identifier.replace('*', '(.+)')}$").exec type
+            @args.shift()
+
+    @binder or= Rivets.routines['*']
+
+    if @binder instanceof Function
+      @binder = {routine: @binder}

     @formatters = @options.formatters || []
-
-  # Returns true|false depending on whether or not the binding should also
-  # observe the DOM element for changes in order to propagate those changes
-  # back to the model object.
-  isBidirectional: =>
-    @type in ['value', 'checked', 'unchecked']

   # Applies all the current formatters to the supplied value and returns the
   # formatted value.
@@ -45,17 +47,12 @@
   # Sets the value for the binding. This Basically just runs the binding routine
   # with the suplied value formatted.
   set: (value) =>
-    value = if value instanceof Function and @options.special isnt 'event'
+    value = if value instanceof Function and !@binder.function
       @formattedValue value.call @model
     else
       @formattedValue value

-    if @options.special is 'event'
-      @currentListener = @routine @el, @model, value, @currentListener
-    else if @options.special is 'iteration'
-      @routine @el, value, @
-    else
-      @routine @el, value
+    @binder.routine?.call @, @el, value

   # Syncs up the view binding with the model.
   sync: =>
@@ -75,11 +72,9 @@
     if @options.bypass
       @sync()
     else
+      @binder.bind?.call @, @el
       Rivets.config.adapter.subscribe @model, @keypath, @sync
       @sync() if Rivets.config.preloadData
-
-      if @isBidirectional()
-        bindEvent @el, 'change', @publish

     if @options.dependencies?.length
       for dependency in @options.dependencies
@@ -93,14 +88,11 @@

         Rivets.config.adapter.subscribe model, keypath, @sync

-
   # Unsubscribes from the model and the element.
   unbind: =>
     unless @options.bypass
+      @binder.unbind?.call @, @el
       Rivets.config.adapter.unsubscribe @model, @keypath, @sync
-
-      if @isBidirectional()
-        unbindEvent @el 'change', @publish

     if @options.dependencies?.length
       for keypath in @options.dependencies
@@ -119,28 +111,30 @@
     prefix = Rivets.config.prefix
     if prefix then new RegExp("^data-#{prefix}-") else /^data-/

-  # Builds the Rivets.Binding instances for the view.
   build: =>
     @bindings = []
     skipNodes = []
-    iterator = null
     bindingRegExp = @bindingRegExp()
-    eventRegExp = /^on-/
-    classRegExp = /^class-/
-    iterationRegExp = /^each-/

     parseNode = (node) =>
       unless node in skipNodes
         for attribute in node.attributes
           if bindingRegExp.test attribute.name
             type = attribute.name.replace bindingRegExp, ''
-
-            if iterationRegExp.test type
-              unless @models[type.replace iterationRegExp, '']
-                skipNodes.push n for n in node.getElementsByTagName '*'
-                iterator = [attribute]
-
-        for attribute in iterator or node.attributes
+            unless binder = Rivets.routines[type]
+              for identifier, value of Rivets.routines
+                if identifier isnt '*' and identifier.indexOf('*') isnt -1
+                  regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
+                  if regexp.test type
+                    binder = value
+
+            binder or= Rivets.routines['*']
+
+            if binder.block
+              skipNodes.push n for n in node.getElementsByTagName '*'
+              attributes = [attribute]
+
+        for attribute in attributes or node.attributes
           if bindingRegExp.test attribute.name
             options = {}

@@ -162,31 +156,19 @@
               if dependencies = context.shift()
                 options.dependencies = dependencies.split /\s+/

-              if eventRegExp.test type
-                type = type.replace eventRegExp, ''
-                options.special = 'event'
-
-              if classRegExp.test type
-                type = type.replace classRegExp, ''
-                options.special = 'class'
-
-              if iterationRegExp.test type
-                type = type.replace iterationRegExp, ''
-                options.special = 'iteration'
-
               binding = new Rivets.Binding node, type, model, keypath, options
               binding.view = @

               @bindings.push binding

-          if iterator
-            node.removeAttribute(a.name) for a in iterator
-            iterator = null
+        attributes = null if attributes
+
       return

     for el in @els
       parseNode el
       parseNode node for node in el.getElementsByTagName '*'
+
     return

   # Returns an array of bindings where the supplied function evaluates to true.
@@ -207,7 +189,7 @@

   # Publishes the input values from the view back to the model (reverse sync).
   publish: =>
-    binding.publish() for binding in @select (b) -> b.isBidirectional()
+    binding.publish() for binding in @select (b) -> b.binder.publishes

 # Cross-browser event binding.
 bindEvent = (el, event, handler, context) ->
@@ -248,84 +230,110 @@
     when 'select-multiple' then o.value for o in el when o.selected
     else el.value

-# Returns an event binding routine for the specified event.
-eventBinding = (event) -> (el, context, bind, unbind) ->
-  unbindEvent el, event, unbind if unbind
-  bindEvent el, event, bind, context
-
-# Returns a class binding routine for the specified class name.
-classBinding = (name) -> (el, value) ->
-  elClass = " #{el.className} "
-  hasClass = elClass.indexOf(" #{name} ") != -1
-
-  if !value is hasClass
-    el.className = if value
-      "#{el.className} #{name}"
-    else
-      elClass.replace(" #{name} ", ' ').trim()
-
-# Returns an iteration binding routine for the specified collection.
-iterationBinding = (name) -> (el, collection, binding) ->
-  if binding.iterated?
-    for iteration in binding.iterated
-      iteration.view.unbind()
-      iteration.el.parentNode.removeChild iteration.el
-  else
-    binding.marker = document.createComment " rivets: each-#{name} "
-    el.parentNode.insertBefore binding.marker, el
-    el.parentNode.removeChild el
-
-  binding.iterated = []
-
-  for item in collection
-    data = {}
-    data[n] = m for n, m of binding.view.models
-    data[name] = item
-    itemEl = el.cloneNode true
-    previous = binding.iterated[binding.iterated.length - 1] or binding.marker
-    binding.marker.parentNode.insertBefore itemEl, previous.nextSibling ? null
-
-    binding.iterated.push
-      el: itemEl
-      view: rivets.bind itemEl, data
-
-# Returns an attribute binding routine for the specified attribute. This is what
-# is used when there are no matching routines for an identifier.
-attributeBinding = (attr) -> (el, value) ->
-  if value then el.setAttribute attr, value else el.removeAttribute attr
-
 # Core binding routines.
 Rivets.routines =
   enabled: (el, value) ->
     el.disabled = !value
+
   disabled: (el, value) ->
     el.disabled = !!value
-  checked: (el, value) ->
-    if el.type is 'radio'
-      el.checked = el.value is value
-    else
-      el.checked = !!value
-  unchecked: (el, value) ->
-    if el.type is 'radio'
-      el.checked = el.value isnt value
-    else
-      el.checked = !value
+
+  checked:
+    publishes: true
+    bind: (el) ->
+      bindEvent el, 'change', @publish
+    unbind: (el) ->
+      unbindEvent el, 'change', @publish
+    routine: (el, value) ->
+      if el.type is 'radio'
+        el.checked = el.value is value
+      else
+        el.checked = !!value
+
+  unchecked:
+    publishes: true
+    bind: (el) ->
+      bindEvent el, 'change', @publish
+    unbind: (el) ->
+      unbindEvent el, 'change', @publish
+    routine: (el, value) ->
+      if el.type is 'radio'
+        el.checked = el.value isnt value
+      else
+        el.checked = !value
+
   show: (el, value) ->
     el.style.display = if value then '' else 'none'
+
   hide: (el, value) ->
     el.style.display = if value then 'none' else ''
-  html:  (el, value) ->
+
+  html: (el, value) ->
     el.innerHTML = if value? then value else ''
-  value: (el, value) ->
-    if el.type is 'select-multiple'
-      o.selected = o.value in value for o in el if value?
-    else
-      el.value = if value? then value else ''
+
+  value:
+    publishes: true
+    bind: (el) ->
+      bindEvent el, 'change', @publish
+    unbind: (el) ->
+      unbindEvent el, 'change', @publish
+    routine: (el, value) ->
+      if el.type is 'select-multiple'
+        o.selected = o.value in value for o in el if value?
+      else
+        el.value = if value? then value else ''
+
   text: (el, value) ->
     if el.innerText?
       el.innerText = if value? then value else ''
     else
       el.textContent = if value? then value else ''
+
+  "on-*":
+    function: true
+    routine: (el, value) ->
+      unbindEvent el, @args[0], @currentListener if @currentListener
+      @currentListener = bindEvent el, @args[0], value, @model
+
+  "each-*":
+    block: true
+    bind: (el, collection) ->
+      el.removeAttribute ['data', rivets.config.prefix, @type].join('-').replace '--', '-'
+    routine: (el, collection) ->
+      if @iterated?
+        for view in @iterated
+          view.unbind()
+          e.parentNode.removeChild e for e in view.els
+      else
+        @marker = document.createComment " rivets: #{@type} "
+        el.parentNode.insertBefore @marker, el
+        el.parentNode.removeChild el
+
+      @iterated = []
+
+      for item in collection
+        data = {}
+        data[n] = m for n, m of @view.models
+        data[@args[0]] = item
+        itemEl = el.cloneNode true
+        previous = @iterated[@iterated.length - 1] or @marker
+        @marker.parentNode.insertBefore itemEl, previous.nextSibling ? null
+        @iterated.push rivets.bind itemEl, data
+
+  "class-*": (el, value) ->
+    elClass = " #{el.className} "
+
+    if !value is (elClass.indexOf(" #{@args[0]} ") isnt -1)
+      el.className = if value
+        "#{el.className} #{@args[0]}"
+      else
+        elClass.replace(" #{@args[0]} ", ' ').trim()
+
+  "*": (el, value) ->
+    if value
+      el.setAttribute @type, value
+    else
+      el.removeAttribute @type

 # Default configuration.
 Rivets.config =


Make sure that we pass along to view's local config to the iterated views, forcing config.preloadData to true.

--- src/rivets.coffee
+++ src/rivets.coffee
@@ -404,8 +404,16 @@
           else
             @marker

+          options =
+            binders: @view.options.binders
+            formatters: @view.options.formatters
+            config: {}
+
+          options.config[k] = v for k, v of @view.options.config
+          options.config.preloadData = true
+
           template = el.cloneNode true
-          @iterated.push rivets.bind template, data
+          @iterated.push rivets.bind template, data, options
           @marker.parentNode.insertBefore template, previous.nextSibling
         else if @iterated[index].models[modelName] isnt model
           @iterated[index].update data


Fix manager startup issue

Fix manager startup issue

--- lib/test.coffee
+++ lib/test.coffee
@@ -426,10 +426,12 @@
 run_setup = ->
   manager = new Manager { verbose, flush: true }
   add_workers()
+  manager.on 'ready', ->
+    fiber.run()
   manager.run ->
     debug 'run from run_setup'
     fiber.run()
-  wait 100
+  yield()
   try
     setup() for setup in the_test.setups
   catch err


changed Utils to _Utils

changed Utils to _Utils

--- src/router.coffee
+++ src/router.coffee
@@ -32,7 +32,7 @@
               replyMsg = JSON.stringify(res)
               _Utils.log("srndp-login-success:"+replyMsg)
               parent.postMessage("srndp-login-success:"+replyMsg,window.__SRNDP__ORIGIN_)
-            ).fail( (err) ->
+            ).fail( () ->
               _Utils.log("srndp-login-failed")
               parent.postMessage("srndp-login-failed",window.__SRNDP__ORIGIN_)
             )
@@ -48,5 +48,5 @@
     session = -1
   replyMsg = if session is -1 then "logged_out" else "logged_in"
   parent.postMessage("srndp-chk-session:"+replyMsg,window.__SRNDP__ORIGIN_)
-  Utils.log("srndp-chk-session:"+replyMsg)
+  _Utils.log("srndp-chk-session:"+replyMsg)


Fix extra newline in errors

Fix extra newline in errors


check for minimum short amount and displays a message to user

--- app/js/controllers/market.coffee
+++ app/js/controllers/market.coffee
@@ -189,6 +189,9 @@
         #    form.short_price.$error.message = "market.tip.short_price_should_below_max_range"
         #    return
         short.cost = short.quantity * short.price
+        if short.cost < 100
+            form.$error.message = "Short amount cannot be less than 100 " + $scope.market.asset_base_symbol
+            return
         if short.cost > $scope.account.base_balance
             form.short_quantity.$error.message = 'market.tip.insufficient_balances'
             return


some bugs

some bugs

--- app/js/controllers/packet.coffee
+++ app/js/controllers/packet.coffee
@@ -35,7 +35,7 @@
   $scope.$watch ->
     Wallet.current_account
   , ->
-    $scope.frm_data.to_account_name = Wallet.current_account.name
+    $scope.frm_data.to_account_name = Wallet.current_account?.name

   $scope.hide = ->
@@ -52,7 +52,7 @@
       $scope.form.password.$valid = false
       $scope.form.password.$error = {}

-      error   = err.data.error
+      error   = err.data?.error || err.response?.data?.error
       code    = error.code

       if code


sendMessage should no-op if server not running.

--- lib/analysis_server.coffee
+++ lib/analysis_server.coffee
@@ -40,10 +40,10 @@
     @process?.close()

   sendMessage: (obj) =>
+    return unless @isRunning
     obj.id ||= "dart-tools-#{(@id++)}"
     msg = JSON.stringify(obj)
-    if @isRunning
-      @process?.stdin?.write(msg + "\n")
+    @process.stdin.write(msg + "\n")

     deferred = Q.defer()
     promiseMap[obj.id] = deferred


Modify error message when adding a collaborator.  Closes #1331

--- app/pages/lab/collaborators.cjsx
+++ app/pages/lab/collaborators.cjsx
@@ -119,7 +119,10 @@
         @props.onAdd? arguments...

       .catch (error) =>
-        @setState error: error
+        if error.message.match /not allowed to create this role/i
+          error.message = 'Your account status on this project is still being setup. Please try again later.'
+
+        @setState {error}

       .then =>
         @setState creating: false


Return a LocationRange

Return a LocationRange

--- src/trix/models/document.coffee
+++ src/trix/models/document.coffee
@@ -155,7 +155,7 @@
     {text, range} = @getTextAndRangeOfAttachment(attachment) ? {}
     if text
       index = @findIndexForText(text)
-      [{index, position: range[0]}, {index, position: range[1]}]
+      new Trix.LocationRange {index, position: range[0]}, {index, position: range[1]}

   getAttachmentById: (id) ->
     for {text} in @blockList.toArray()


Leave empty blocks when removing all of their non break block characters

--- src/trix/models/document.coffee
+++ src/trix/models/document.coffee
@@ -113,11 +113,7 @@
     block = leftBlock.copyWithText(text)
     blocks = @blockList.toArray()
     affectedBlockCount = rightIndex + 1 - leftIndex
-
-    if block.isEmpty()
-      blocks.splice(leftIndex, affectedBlockCount)
-    else
-      blocks.splice(leftIndex, affectedBlockCount, block)
+    blocks.splice(leftIndex, affectedBlockCount, block)

     @blockList = new Trix.SplittableList blocks


:shirt:

:shirt:

--- lib/util.coffee
+++ lib/util.coffee
@@ -5,7 +5,6 @@
 untildify = require 'untildify'
 async = require 'async'
 notificationManager = require './notification-manager'
-{saveState} = require './provider/session'
 atomProjectUtil = require 'atom-project-util'

 saveCurrentState = () ->


Don't want extra copies...

Don't want extra copies...

--- assets/javascripts/views/interactives.coffee
+++ assets/javascripts/views/interactives.coffee
@@ -18,6 +18,7 @@
         #committees: committees
         #offices: offices
   render: () ->
+    this.$el.empty()
     this.$el.append(view.attach()) for name, view of this.views
   detach: () ->
     this.$el.empty()


[FIX] Bug on remote stream when buffer is full, => reset sample_count

--- src/streamer.coffee
+++ src/streamer.coffee
@@ -56,6 +56,7 @@
       if code == 0 and path.indexOf('http') > -1
         console.log '[FFMPEG] Stream terminated'
         me.prototype.startBuffering(path, f)
+        me.prototype.initSample()
     f(0, "Bufferin started")


removed useless variable

removed useless variable

--- core/app/backbone/models/topic.coffee
+++ core/app/backbone/models/topic.coffee
@@ -1,6 +1,6 @@
 class window.Topic extends Backbone.Model
   newChannelForUser: (user) ->
-    ch = new Channel
+    new Channel
       title: this.get 'title'
       slug_title: this.get 'slug_title'
       username: user.get 'username'


fixed wrong time order

fixed wrong time order

--- src/test/xml.coffee
+++ src/test/xml.coffee
@@ -339,11 +339,12 @@
         setTimeout ( ->
             grass.tag('cat').end()
             dog.end()
-        ), 4
+        ), 3
         setTimeout ( ->
+            xml.end()
             grass.end()
-            xml.end()
-        ),3
+        ),4
+

     pipe: (æ) ->
         xml = streamify new Builder


Removed the comment about it being broken


staging config changed for ssl dev-app.koding.com

--- config/stage.coffee
+++ config/stage.coffee
@@ -41,7 +41,7 @@
         auth    : 'https://dev.koding.com/auth'
         vhost   : '/'
       apiUri    : 'https://dev-api.koding.com'
-      appsUri   : 'http://dev-app.koding.com'
+      appsUri   : 'https://dev-app.koding.com'
       env       : 'stage'
   mq            :
     host        : 'localhost'


terser prep() syntax

terser prep() syntax

--- oridomi.coffee
+++ oridomi.coffee
@@ -352,37 +352,38 @@
     if @_touchStarted
       fn.apply @, arguments
     else
-      args = arguments
-      options = {}
-      angle = anchor = null
+      [a0, a1, a2] = arguments
+      opt          = {}
+      angle        = anchor = null
+
       switch fn.length
         when 1
-          options.callback = args[0]
+          opt.callback = a0
         when 2
-          if typeof args[0] is 'function'
-            options.callback = args[0]
+          if typeof a0 is 'function'
+            opt.callback = a0
           else
-            anchor = args[0]
-            options.callback = args[1]
+            anchor       = a0
+            opt.callback = a1
         when 3
-          angle = args[0]
-          if args.length is 2
-            if typeof args[1] is 'object'
-              options = args[1]
-            else if typeof args[1] is 'function'
-              options.callback = args[1]
+          angle = a0
+          if arguments.length is 2
+            if typeof a1 is 'object'
+              opt = a1
+            else if typeof a1 is 'function'
+              opt.callback = a1
             else
-              anchor = args[1]
-          else if args.length is 3
-            anchor = args[1]
-            if typeof args[2] is 'object'
-              options = args[2]
-            else if typeof args[2] is 'function'
-              options.callback = args[2]
+              anchor = a1
+          else if arguments.length is 3
+            anchor = a1
+            if typeof a2 is 'object'
+              opt = a2
+            else if typeof a2 is 'function'
+              opt.callback = a2

       angle  or= @lastOp.angle or 0
       anchor or= @lastOp.anchor
-      @_queue.push [fn, @_normalizeAngle(angle), @_getLonghandAnchor(anchor), options]
+      @_queue.push [fn, @_normalizeAngle(angle), @_getLonghandAnchor(anchor), opt]
       @_step()
       @


exit early lacking jquery

exit early lacking jquery

--- oridomi.coffee
+++ oridomi.coffee
@@ -1121,46 +1121,46 @@

 # Only create bridge if jQuery (or an imitation supporting `data()`) exists.
-if $
-  # Attach an `oriDomi` method to `$`'s prototype.
-  $::oriDomi = (options) ->
-    # Return selection if oriDomi is unsupported by the browser.
-    return @ unless isSupported
-
-    # If `options` is a string, assume it's a method call.
-    if typeof options is 'string'
-      methodName = options
-      # Check if method exists and warn if it doesn't.
-      unless typeof OriDomi::[methodName] is 'function'
-        console?.warn "oriDomi: No such method '#{ methodName }'"
+return unless $
+# Attach an `oriDomi` method to `$`'s prototype.
+$::oriDomi = (options) ->
+  # Return selection if oriDomi is unsupported by the browser.
+  return @ unless isSupported
+
+  # If `options` is a string, assume it's a method call.
+  if typeof options is 'string'
+    methodName = options
+    # Check if method exists and warn if it doesn't.
+    unless typeof OriDomi::[methodName] is 'function'
+      console?.warn "oriDomi: No such method '#{ methodName }'"
+      return @
+
+    # Convert arguments to a proper array and remove the first element.
+    args = Array::slice.call arguments
+    args.shift()
+    # Loop through the jQuery selection.
+    for el in @
+      # Warn if oriDomi hasn't been initialized on this element.
+      unless instance = $.data el, baseName
+        console?.warn "oriDomi: Can't call #{ methodName }, oriDomi hasn't been initialized on this element"
         return @

-      # Convert arguments to a proper array and remove the first element.
-      args = Array::slice.call arguments
-      args.shift()
-      # Loop through the jQuery selection.
-      for el in @
-        # Warn if oriDomi hasn't been initialized on this element.
-        unless instance = $.data el, baseName
-          console?.warn "oriDomi: Can't call #{ methodName }, oriDomi hasn't been initialized on this element"
-          return @
-
-        # Call the requested method with arguments.
-        instance[methodName] args
-
-      # Return selection.
-      @
-
-    # If not calling a method, initialize oriDomi on the selection.
-    else
-      for el in @
-        # If the element in the selection already has an instance of oriDomi
-        # attached to it, return the instance.
-        if instance = $.data el, baseName
-          return instance
-        else
-          # Create an instance of oriDomi and attach it to the element.
-          $.data el, baseName, new OriDomi el, options
-
-      # Return the selection.
-      @
+      # Call the requested method with arguments.
+      instance[methodName] args
+
+    # Return selection.
+    @
+
+  # If not calling a method, initialize oriDomi on the selection.
+  else
+    for el in @
+      # If the element in the selection already has an instance of oriDomi
+      # attached to it, return the instance.
+      if instance = $.data el, baseName
+        return instance
+      else
+        # Create an instance of oriDomi and attach it to the element.
+        $.data el, baseName, new OriDomi el, options
+
+    # Return the selection.
+    @


no need for encodeURI here

no need for encodeURI here

--- lib/installed-package-view.coffee
+++ lib/installed-package-view.coffee
@@ -118,7 +118,7 @@
     if atom.packages.isPackageActive('markdown-preview')
       atom.workspace.open("#{encodeURI("markdown-preview://#{path}")}")
     else
-      atom.workspaceView.open(encodeURI(path))
+      atom.workspaceView.open(path)

   updateFileButtons: ->
     @changelogPath = null


debug

debug

--- lib/project/create-project.coffee
+++ lib/project/create-project.coffee
@@ -147,7 +147,7 @@
     urlList = []
     for name, url of options.projectDetail.moduleUrlMap
       urlList.push({name: name, url: url})
-
+
     copyDetail = _.omit options.projectDetail, 'moduleUrlMap'
     Util.createDir filePath, (err)=>
       if err
@@ -173,9 +173,10 @@
                           if err
                             console.error err
                           else
-                            alert '同步项目成功'
+                            # alert '同步项目成功'
                             atom.project.addPath filePath
-                            Util.rumAtomCommand 'tree-view:toggle' if ChameleonBox.$('.tree-view-resizer').length is 0
+                            atom.workspace.open pathM.join(filePath, 'appConfig.json')
+                            Util.rumAtomCommand 'tree-view:toggle' if $('.tree-view-resizer').length is 0
                             @modalPanel.item.children(".loading-mask").remove()
                             @closeView()
                   Util.createFile pathM.join(filePath, "modules", "#{item.name}.zip"), data, abc
@@ -185,9 +186,9 @@
                 if err
                   console.error error
                 else
-                  alert '同步项目成功'
+                  # alert '同步项目成功'
                   atom.project.addPath filePath
-                  Util.rumAtomCommand 'tree-view:toggle' if ChameleonBox.$('.tree-view-resizer').length is 0
                   atom.workspace.open pathM.join(filePath, 'appConfig.json')
+                  Util.rumAtomCommand 'tree-view:toggle' if $('.tree-view-resizer').length is 0
                   @modalPanel.item.children(".loading-mask").remove()
                   @closeView()


fix focus restoration on cancel

fix focus restoration on cancel

--- lib/provider/provider-base.coffee
+++ lib/provider/provider-base.coffee
@@ -105,6 +105,7 @@
     pane = paneForItem(@editor)
     if @editor.isAlive() and pane.isAlive() and @needRestoreEditorState
       @restoreEditorState()
+      pane.activate()
       pane.activateItem(@editor)

     {@editor, @editorSubscriptions} = {}


unecessary

unecessary

--- apps/articles/client/show.coffee
+++ apps/articles/client/show.coffee
@@ -5,7 +5,6 @@
 Artwork = require '../../../models/artwork.coffee'
 ShareView = require '../../../components/share/view.coffee'
 CarouselView = require '../../../components/carousel/view.coffee'
-imagesLoaded = require 'imagesloaded'
 carouselTemplate = -> require('../templates/carousel.jade') arguments...
 artworkItemTemplate = -> require(
   '../../../components/artwork_item/templates/artwork.jade') arguments...


Removed superfluous copy of withinTolerance function

--- app/classifier/index.cjsx
+++ app/classifier/index.cjsx
@@ -311,11 +311,6 @@
         </div>
     </div>

-  withinTolerance: (userX, userY, metaX, metaY, tolerance) ->
-    distance = Math.sqrt((userY - metaY)**2 + (userX - metaX)**2)
-    isWithinTolerance = distance < tolerance
-    return isWithinTolerance
-
   renderSummary: (classification) ->
     <div>
       Thanks!


Fix porting issues

Fix porting issues

--- app/coffee/toolbar.coffee
+++ app/coffee/toolbar.coffee
@@ -16,7 +16,7 @@
     @toolbar_id = "#{id}-barbody"
     @div id: id, =>
       @toolbar = @create_toolbar(@toolbar_id)
-
+
       toolbar_group = @create_toolbar_group()
       for def in buttons
         if def.split
@@ -48,7 +48,7 @@
         else if typeof def.label == 'string'
           button.html(def.label)

-        button.click = (e) ->
+        button.click (e) ->
           $this = $(this)
           context = def.context

@@ -68,11 +68,12 @@
           css(
             'margin-top': '0.5em',
             'margin-bottom': '0.5em'
-          ).
-          tooltip(
-            container: 'body',
-            placement: 'top auto'
           )
+          # .
+          # tooltip(
+          #   container: 'body',
+          #   placement: 'top auto'
+          # )

   @create_toolbar: (toolbar_id)->
     $$ ->


c430a81de5674a427d290706c5051e5a3eef106f fix

[#97184290]

--- review_front/tests/acceptance/projects-test.coffee
+++ review_front/tests/acceptance/projects-test.coffee
@@ -6,24 +6,24 @@
 application = null
 server = null

-module 'Acceptance: Projects',
+module "Acceptance: Projects",
   beforeEach: ->
     application = startApp()
     server = Server.create()
-    visit '/projects'
+    visit "/projects"
     return

   afterEach: ->
-    Ember.run application, 'destroy'
+    Ember.run application, "destroy"
     server.shutdown()
     return

-test 'visiting /projects', (assert) ->
-  assert.equal currentPath(), 'projects.index'
+test "visiting /projects", (assert) ->
+  assert.equal currentPath(), "projects.index"

-test 'displays table with projects', (assert) ->
-  assert.equal find('.table-bordered tr:eq(1) a:first').text(), 'firstProject'
-  assert.equal find('.table-bordered tr:eq(2) a:first').text(), 'secondProject'
+test "displays table with projects", (assert) ->
+  assert.equal find(".table-bordered tr:eq(1) a:first").text(), "firstProject"
+  assert.equal find(".table-bordered tr:eq(2) a:first").text(), "secondProject"

 test "checks correct values for project", (assert) ->
   findFirstProjectRejected = ->
@@ -41,10 +41,10 @@
   findFirstProjectStatus = ->
     find(".table-bordered tr:eq(1) td:eq(5) .glyphicon.glyphicon-ok.text-success")

-  assert.equal findFirstProjectRejected(), '2'
-  assert.equal findFirstProjectPending(), '6'
-  assert.equal findFirstProjectAccepted(), '1'
-  assert.equal findFirstProjectTotal(), '10'
+  assert.equal findFirstProjectRejected(), "2"
+  assert.equal findFirstProjectPending(), "6"
+  assert.equal findFirstProjectAccepted(), "1"
+  assert.equal findFirstProjectTotal(), "10"
   assert.equal findFirstProjectStatus().length, 1

 test "find github links", (assert) ->


rename pointerOffset to moveOffset

rename pointerOffset to moveOffset

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -590,7 +590,7 @@
     markWidth = parseFloat(@element.style.width)

     # no overlapping of marks or moving out of canvas bounds
-    leftXPos = Math.max (@toCanvasXPoint(e) - @pointerOffset),
+    leftXPos = Math.max (@toCanvasXPoint(e) - @moveOffset),
                         (@closestXBelow || -@handleWidth()) + @handleWidth(),
                         @canvasGraph.leftPadding
     leftXPos = Math.min leftXPos,
@@ -652,7 +652,7 @@
       @dragging = true
     else if e.target.className is "mark"
       @moving = true
-      @pointerOffset = (@toCanvasXPoint(e) - @canvasXMin)
+      @moveOffset = (@toCanvasXPoint(e) - @canvasXMin)

     @closestXBelow = @canvasGraph.marks.closestXBelow(@canvasXMin)
     @closestXAbove = @canvasGraph.marks.closestXAbove(@canvasXMax)


logger foireux

logger foireux

--- controllers/game_controller.coffee
+++ controllers/game_controller.coffee
@@ -134,7 +134,6 @@
             cb gameTeam[0]

     checkPoint: (user, game, point, lat, lng, useGPS, cb) ->
-        logger.info "controller: Point check gid=#{game.id} pid=#{point.id} uid=#{user.id}"
         @getGamePoint point, game, (gamePoint) =>
             @getGameUser game, user, (gameUser) =>
                 # Check if coordinates are right


Simple x reattempt mode for stats

Simple x reattempt mode for stats

--- app/modules/vm/index.coffee
+++ app/modules/vm/index.coffee
@@ -42,7 +42,6 @@
     $scope.currentPCIPage = 1
     $scope.currentGPUPage = 1

-    # Provides a fibonacci behaviour for stats refresh on failure
     $scope.refreshStatControl = refreshStatControl = {
       baseStatInterval: 5000
       timeout: null
@@ -51,39 +50,29 @@

       start: () ->
         return if this.running
+        this.stop()
         this.running = true
         this._reset()
         $scope.$on('$destroy', () => this.stop())
         return this._trig(Date.now())
       _trig: (t1) ->
         if this.running
-          t2 = Date.now()
-          timeLeft = Math.max(this.baseStatInterval * this._factor() - Math.max(t2 - t1 - (this.baseStatInterval * this._factor(true)), 0), 0)
-          return this.timeout = $timeout(
-            () => $scope.refreshStats($scope.VM.UUID),
-            timeLeft
-          )
-
-          .then () =>
-            this._reset()
-            return this._trig(t2)
-
+          return $scope.refreshStats($scope.VM.UUID)
+          .then () => this._reset()
           .catch (err) =>
-            if !this.running || $scope.VM.power_state isnt 'Running' || $scope.isVMWorking($scope.VM)
+            if !this.running || this.attempt >= 2 || $scope.VM.power_state isnt 'Running' || $scope.isVMWorking($scope.VM)
               return this.stop()
             else
-              if this.attempt >= 1
-                return this.stop()
               this.attempt++
-              this._next()
-              return this._trig(t2)
+          .finally () =>
+            if this.running
+              t2 = Date.now()
+              return this.timeout = $timeout(
+                () => this._trig(t2),
+                Math.max(this.baseStatInterval - (t2 - t1), 0)
+              )
       _reset: () ->
-        this.terms = [1,1]
         this.attempt = 0
-      _next: () ->
-        this.terms = [this.terms[1], this.terms[0] + this.terms[1]]
-      _factor: (previous) ->
-        return this.terms[if previous then 0 else 1]
       stop: () ->
         if this.timeout
           $timeout.cancel(this.timeout)


follow the convention

follow the convention

--- library/membrane/preflight.coffee
+++ library/membrane/preflight.coffee
@@ -97,6 +97,7 @@
     prelude: (symbol, context, request, next) ->
         context.inline -> `assert = chai.assert`
         context.inline -> `assert(logger = log)`
+        context.inline -> `assert($logger = log)`
         context.inline -> try logger.enableAll()
         context.inline -> _.mixin _.string.exports()
         context.inline -> $(document).ready =>


Fix up the layout of the the Alert dismiss button, make it extra small

--- tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/alert.coffee
+++ tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/alert.coffee
@@ -66,12 +66,12 @@

       if (outer.attribute "data-show-dismiss-all") is "true"
         outer.append """
-

-            <button class="btn btn-mini pull-right" data-action="dismiss-all">
+

+            <button class="btn btn-mini btn-default" data-action="dismiss-all">
               ×
               #{messages "core-dismiss-label"}
             </button>
-

+

          """

       outer.on "click", "[data-action=dismiss-all]", ->


revert to use the type dropdown as the UX is better

--- modules/data-table/main/index.coffee
+++ modules/data-table/main/index.coffee
@@ -1,7 +1,6 @@
 hx.userFacingText({
   dataTable: {
     addFilter: 'Add Filter',
-    addFilterGroup: 'Add Filter Group',
     advancedSearch: 'Advanced Search',
     and: 'and',
     anyColumn: 'Any column'
@@ -11,7 +10,6 @@
     noData: 'No Data',
     noSort: 'No Sort',
     or: 'or',
-    removeFilterGroup: 'Remove Filter Group',
     rowsPerPage: 'Rows Per Page',
     search: 'Search',
     selectedRows: '$selected of $total selected.',
@@ -91,8 +89,6 @@
       addFilterText: hx.userFacingText('dataTable', 'addFilter')
       clearFiltersText: hx.userFacingText('dataTable', 'clearFilters')
       anyColumnText: hx.userFacingText('dataTable', 'anyColumn')
-      removeFilterGroupText: hx.userFacingText('dataTable', 'removeFilterGroup')
-      addFilterGroupText: hx.userFacingText('dataTable', 'addFilterGroup')

       advancedSearchPlaceholder: hx.userFacingText('dataTable', 'search')
     }, options)
@@ -147,6 +143,31 @@
     # Render individual row
     advancedSearchRowEnter = (filterGroup, filterGroupIndex, dataTable) ->
       (filterRow, index, trueIndex) ->
+        typePickerOptions =
+          items: [
+            { text: hx.userFacingText('dataTable', 'and'), value: 'and' }
+            { text: hx.userFacingText('dataTable', 'or'), value: 'or' }
+          ]
+
+        typePickerSel = hx.picker(typePickerOptions)
+          .classed('hx-data-table-advanced-search-type', true)
+
+        typePickerSel.component()
+          .on 'change', (data) ->
+            if data.cause is 'user'
+              prevFilters = dataTable.advancedSearch()
+              [leftFilterGroups, filterGroup, rightFilterGroups] = splitArray(prevFilters, filterGroupIndex)
+
+              newFilters = if data.value.value is 'or'
+                [leftFilters, filter, rightFilters] = splitArray(filterGroup, trueIndex)
+                [leftFilterGroups..., leftFilters, [filter, rightFilters...], rightFilterGroups...]
+              else
+                [leftAllButLast..., leftLast] = leftFilterGroups
+                [leftAllButLast..., [leftLast..., filterGroup...], rightFilterGroups...]
+
+              dataTable.advancedSearch(newFilters)
+
+
         anyColumn = {
           text: resolvedOptions.anyColumnText
           value: 'any'
@@ -212,59 +233,43 @@
                 [leftFilterGroups..., newFilterGroup, rightFilterGroups...]
               else
                 [leftFilterGroups..., rightFilterGroups...]
-            dataTable.advancedSearch(newFilters)
+
+            filterToUse = newFilters.filter((group) => group.length)
+
+            dataTable.advancedSearch(if filterToUse.length then filterToUse else undefined)

         @append('div').class('hx-data-table-advanced-search-filter hx-section hx-input-group hx-input-group-full-width')
+          .add(typePickerSel)
+          .add(columnPickerSel)
           .add(termInput)
-          .add(columnPickerSel)
           .add(removeBtn)
           .node()

     advancedSearchRowUpdate = ({term, column}, element, index) ->
       filterRowSel = hx.select(element)
-        .classed('hx-data-table-advanced-search-invalid', not term)
-
-      filterRowSel.select('.hx-data-table-advanced-search-column').component()
-        .value(column or 'any')
-
-      filterRowSel.select('.hx-data-table-advanced-search-input').value(term or '')
+
+      validContext = if not term then 'negative' else undefined
+
+      hx.palette.borderContext(filterRowSel.select('.hx-data-table-advanced-search-type'), validContext)
+        .component().value(if index is 0 then 'or' else 'and')
+
+      hx.palette.borderContext(filterRowSel.select('.hx-data-table-advanced-search-column'), validContext)
+        .component().value(column or 'any')
+
+      hx.palette.borderContext(filterRowSel.select('.hx-data-table-advanced-search-input'), validContext)
+        .value(term or '')

     # Render grouped filters
     advancedSearchGroupEnter = (dataTable) ->
       (filterGroup, index, trueIndex) ->
-        addFilterToGroup = ->
-          prevFilters = dataTable.advancedSearch()
-          [leftFilterGroups, filterGroup, rightFilterGroups] = splitArray(prevFilters, trueIndex)
-          newFilter = {
-            column: 'any',
-            term: ''
-          }
-          dataTable.advancedSearch([leftFilterGroups..., [filterGroup..., newFilter], rightFilterGroups...])
-
-        removeFilterGroup = ->
-          prevFilters = dataTable.advancedSearch()
-          [leftFilterGroups, filterGroup, rightFilterGroups] = splitArray(prevFilters, trueIndex)
-          dataTable.advancedSearch([leftFilterGroups..., rightFilterGroups...])
-
         filterGroupSel = hx.detached('div').class('hx-data-table-advanced-search-filter-group')
         filterGroupView = filterGroupSel.append('div')
           .view('.hx-data-table-advanced-search-filter')
             .enter(advancedSearchRowEnter(filterGroup, trueIndex, dataTable))
             .update(advancedSearchRowUpdate)
-        filterGroupButtons = filterGroupSel.append('div')
-
-        filterGroupButtons.append(hx.button({context: 'positive'})
-          .add(hx.icon({class: 'hx-icon hx-icon-plus'}))
-          .add(hx.detached('span').text(resolvedOptions.addFilterText)))
-          .on 'click', addFilterToGroup
-
-        filterGroupButtons.append(hx.button({context: 'negative'})
-          .add(hx.icon({class: 'hx-icon hx-icon-close'}))
-          .add(hx.detached('span').text(resolvedOptions.removeFilterGroupText)))
-          .on 'click', removeFilterGroup

         hx.component.register(filterGroupSel.node(), {
           filterGroupView
@@ -278,19 +283,21 @@
       .enter(advancedSearchGroupEnter(this))
       .update(advancedSearchGroupUpdate)

-    addFilterGroup = ->
-      prevFilters = dataTable.advancedSearch() or []
-      dataTable.advancedSearch([prevFilters..., [{
+    addFilter = =>
+      currentFilters = @advancedSearch() or [[]]
+      [previousFilterGroups..., lastFilterGroup] = currentFilters
+      newLastFilterGroup = [lastFilterGroup..., {
         column: 'any',
         term: ''
-      }]])
-
-    clearFilters = => @advancedSearch([[]])
+      }]
+      @advancedSearch([previousFilterGroups..., newLastFilterGroup])
+
+    clearFilters = => @advancedSearch(undefined)

     advancedSearchButtons.append(hx.button({context: 'positive'})
       .add(hx.icon({class: 'hx-icon hx-icon-plus'}))
-      .add(hx.detached('span').text(resolvedOptions.addFilterGroupText)))
-      .on 'click', addFilterGroup
+      .add(hx.detached('span').text(resolvedOptions.addFilterText)))
+      .on 'click', addFilter

     advancedSearchButtons.append(hx.button({context: 'negative'})
       .add(hx.icon({class: 'hx-icon hx-icon-close'}))
@@ -544,7 +551,7 @@
     # XXX: how much of this could be split out so it's not re-defined every time render is called?
     feed.headers (headers) =>
       if options.showAdvancedSearch and options.advancedSearchEnabled
-        currentFilters = @advancedSearch() or [[]]
+        currentFilters = @advancedSearch() or []
         @_.advancedSearchView.apply currentFilters.filter((x) -> x.length).map (filterGroup) ->
           filterGroup.map (filterRow) ->
             hx.merge(filterRow, {
@@ -948,7 +955,6 @@
     validPart = hx.find arr, (part) -> ~rowSearchTerm.indexOf part
     hx.defined validPart

-
   options = hx.merge({
     filter: (term, row) ->
       rowSearchTerm = (v for k, v of row.cells).join(' ').toLowerCase()
@@ -956,7 +962,7 @@

     advancedSearch: (filters, row) ->
       rowSearchTerm = (v for k, v of row.cells).join(' ').toLowerCase()
-      # if term is empty this will return false
+      # If term is empty this will return false
       validFilters = hx.find filters, (groupedFilters) ->
         invalidFilter = hx.find groupedFilters, (filter) ->
           searchTerm = if filter.column is 'any' then rowSearchTerm else row.cells[filter.column].toLowerCase()


Disable generic r fenced-code-block

Disable generic r fenced-code-block

--- grammars/fixtures/fenced-code.cson
+++ grammars/fixtures/fenced-code.cson
@@ -30,7 +30,9 @@
   { pattern:'diff|patch|rej', include:'source.diff' }
   { pattern:'elixir' }
   { pattern:'elm' }
+
   # { pattern:'erb', include:'source.ruby', contentName:'text.html.erb' }
+
   { pattern:'erlang' }
   { pattern:'go|golang', include:'source.go' }
   { pattern:'haskell' }
@@ -44,14 +46,19 @@
   { pattern:'mermaid' }
   { pattern:'mson', include:'text.html.markdown.source.gfm.mson' }
   { pattern:'objc|objective-c', include:'source.objc' }
+
   # { pattern:'php' }
   # { pattern:'php', contentName:'text.html.php' }
   # { pattern:'php', include:'text.html.php' }
   # { pattern:'php', include:'text.html.basic', contentName:'embedded.text.html.php' }
   # { pattern:'php', include:'text.html.basic' }
   # { pattern:'php', include:'text.html.php', contentName:'source.js.embedded.html' }
+
   { pattern:'py|python', include:'source.python' }
-  { pattern:'r' }
+
+  # NOTE Disabled in favor of using R Markdown
+  # { pattern:'r' }
+
   { pattern:'ruby' }
   { pattern:'rust|rs', include:'source.rust' }
   { pattern:'sass' }


chat: create video chat view

chat: create video chat view

--- client/ide/lib/views/chat/idechatview.coffee
+++ client/ide/lib/views/chat/idechatview.coffee
@@ -111,6 +111,8 @@
     chatOptions     = { name, type, channelId, @isInSession }
     settingsOptions = { @rtm, @isInSession }

+    @createChatVideoView()
+
     @addPane @chatPane     = new IDEChatMessagePane  chatOptions, channel
     @addPane @settingsPane = new IDEChatSettingsPane settingsOptions, channel


Use AceView which is bundled editor of Koding for EditorPane.

--- client/ide/workspace/panes/editorpane.coffee
+++ client/ide/workspace/panes/editorpane.coffee
@@ -18,7 +18,7 @@
     unless content
       return new Error "You must pass file content to EditorPane"

-    @ace = new Ace { delegate : this }, file
+    @ace = new AceView { delegate: this }, file
     @addSubView @ace
     @ace.once "ace.ready", =>
       @getEditor().setValue content, 1


Removing .lero

Removing .lero

--- client/views/app/chatMessageDashboard.coffee
+++ client/views/app/chatMessageDashboard.coffee
@@ -22,7 +22,7 @@
 			UserManager.addUser(this.uid)
 		switch this.t
 			when 'p' then "#{this.msg}"
-			when 'r' then t('chatMessageDashboard.Room_name_changed', { room_name: this.msg, user_by: Session.get('user_' + this.by + '_name') }) + '.lero'
+			when 'r' then t('chatMessageDashboard.Room_name_changed', { room_name: this.msg, user_by: Session.get('user_' + this.by + '_name') })
 			when 'au' then t('chatMessageDashboard.User_added_by', { user_added: this.msg, user_by: Session.get('user_' + this.by + '_name') })
 			when 'ru' then t('chatMessageDashboard.User_removed_by', { user_removed: this.msg, user_by: Session.get('user_' + this.by + '_name') })
 			when 'ul' then t('chatMessageDashboard.User_left', this.msg)


Include Accept header in getUser request

Now that we rely on Yesod's automatic JSON responses to authentication
routes, we must supply a proper Accept header to get them.

--- templates/embed/Carnival.coffee
+++ templates/embed/Carnival.coffee
@@ -56,6 +56,7 @@
     request = new XMLHttpRequest
     request.withCredentials = true
     request.open('GET', '@{UserR}', false)
+    request.setRequestHeader('Accept', 'application/json')
     request.send()
     if request.status is 200
       @user = JSON.parse(request.responseText).user


Fix synchronous CORS requests in Firefox 37

- Extract Carnival.openRequest
- Put withCredentials = true within a beforeSend function[1]
- Resolves #248

1: http://stackoverflow.com/questions/16668386/cors-synchronous-requests-not-working-in-firefox.

--- templates/embed/Carnival.coffee
+++ templates/embed/Carnival.coffee
@@ -20,20 +20,14 @@
       elem.className = newClass.replace(/^\s+|\s+$/g, '')

   @get: (url, callback) ->
-    request = new XMLHttpRequest
-    request.withCredentials = true
-    request.open('GET', url, true)
-    request.setRequestHeader('Content-Type', 'application/json')
+    request = @openRequest('GET', url)
     request.onload = () ->
       if request.status >= 200 and request.status < 400
         callback(JSON.parse(request.responseText))
     request.send()

   @post: (url, data, callback) ->
-    request = new XMLHttpRequest()
-    request.withCredentials = true
-    request.open('POST', url, true)
-    request.setRequestHeader('Content-Type', 'application/json')
+    request = @openRequest('POST', url)
     request.onload = () ->
       if request.status >= 200 and request.status < 400
         callback(JSON.parse(@responseText))
@@ -53,10 +47,7 @@
       return false

   @getUser: ->
-    request = new XMLHttpRequest
-    request.withCredentials = true
-    request.open('GET', '@{UserR}', false)
-    request.setRequestHeader('Accept', 'application/json')
+    request = @openRequest('GET', '@{UserR}', false)
     request.send()
     if request.status is 200
       @user = JSON.parse(request.responseText).user
@@ -79,3 +70,13 @@
       'height='+height+',width='+width+',top='+top+',left='+left+',menubar=no'
     )
     window.addEventListener 'message', @hasLoggedIn
+
+  @openRequest: (method, url, async = true) ->
+    request = new XMLHttpRequest()
+    request.beforeSend = (xhr) ->
+      xhr.withCredentials = true
+    request.open(method, url, async)
+    request.setRequestHeader('Accept', 'application/json')
+    request.setRequestHeader('Content-Type', 'application/json')
+
+    request


first set parent and builder of tag before emitting replace

--- src/xml.coffee
+++ src/xml.coffee
@@ -250,9 +250,9 @@
         tag = @builder?.query 'tag', this, rawtag
         tag = rawtag unless tag? or @builder?
         return this if this is tag
-        @emit 'replace', this, tag
         tag.parent  ?= @parent
         tag.builder ?= @builder
+        @emit 'replace', this, tag
         if @builder is tag.builder
             @builder = null
         @parent = null


update meta_description (#5698)

update meta_description (#5698)

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -2,8 +2,8 @@

   new_home:
     title: "CodeCombat - 硅谷原创AP官方权威认证青少年编程平台"
+    meta_description: "CodeCombat中国官方教学版，美国大学理事会认证，国际等级证书，AI世青赛全球独家技术伙伴。"
     meta_keywords: "青少年编程, 少儿编程, CodeCombat, CodeCombat中国, steam教育, python, javascript, 游戏化编程, 美国AP/CSP认证, AI世青赛技术支持平台"
-    meta_description: "CodeCombat（中国官方网址：koudashijie.com）于2013年成立于美国硅谷，是美国大学理事会权威认证的青少年编程学习平台和工信部赛迪人工智能大赛全球独家技术合作平台，学习者可以在CodeCombat学习后参加AP计算机考试，拿到世界一流名校如斯坦福大学、哈佛大学、麻省理工学院等学分减免政策，是全球公认的权威青少年计算机编程学习平台之一。"
     meta_og_url: "https://koudashijie.com"
     built_for_teachers_title: "一个以教师为中心的课程体系"
     built_for_teachers_blurb: "教孩子们写代码往往会觉得无从下手。 CodeCombat 旨在帮助所有的教育工作者传授学生学习和掌握当下最流行的两种计算机语言，JavaScript 和 Python。整个体系包含六门计算机科学课程，以及通过基于项目的游戏开发和网页开发单元巩固学习，教会孩子们一步步从基本语法到高级递归前进！"


Update zh-HANT.coffee (#5616)

Update zh-HANT.coffee (#5616)

--- app/locale/zh-HANT.coffee
+++ app/locale/zh-HANT.coffee
@@ -550,7 +550,7 @@
     years: "年"

   play_level:
-#    back_to_map: "Back to Map"
+    back_to_map: "回到地圖"
     directions: "目標方向"
     edit_level: "編輯關卡"
 #    keep_learning: "Keep Learning"
@@ -1649,7 +1649,7 @@

   teacher:
     assigning_course: "派發課程中"
-#    back_to_top: "Back to Top"
+    back_to_top: "回到頂部"
 #    click_student_code: "Click on any level that the student has started or completed below to view the code they wrote."
 #    code: "__name__'s Code"
 #    complete_solution: "Complete Solution"
@@ -1975,7 +1975,7 @@
 #    level_label: "Level:"
 #    time_played_label: "Time Played:"
 #    back_to_resource_hub: "Back to Resource Hub"
-#    back_to_course_guides: "Back to Course Guides"
+    back_to_course_guides: "回到課程引導"
 #    print_guide: "Print this guide"
 #    combo: "Combo"
 #    combo_explanation: "Students pass Combo challenge levels by using at least one listed concept. Review student code by clicking the progress dot."


quick expect addition on delivery service specs

--- test/delivery_service.spec.cjsx
+++ test/delivery_service.spec.cjsx
@@ -32,6 +32,7 @@
         bindResources: ->
           @retrieveAll store
         resourcesDidLoad: ->
+          expect(component.state.loaded).toBe true
           expect(component.state.users).toExist()
           expect(component.state.users.length).toBe 2
           done()


Add direct flash support

Add direct flash support

--- lib/microbit-python.coffee
+++ lib/microbit-python.coffee
@@ -16,6 +16,7 @@

     # Register command that toggles this view
     @subscriptions.add atom.commands.add 'atom-workspace', 'microbit-python:compile': => @compile()
+    @subscriptions.add atom.commands.add 'atom-workspace', 'microbit-python:flash': => @flash()

   deactivate: ->
     @subscriptions.dispose()
@@ -33,3 +34,16 @@
     cmd.on('close', (code) ->
       nm.addInfo(result)
       )
+
+  flash: ->
+    editor = atom.workspace.getActiveTextEditor()
+    fpath = editor.getPath()
+    cmd = spawn('uflash', [fpath])
+    result = ''
+    cmd.stdout.on('data', (data) ->
+      result += data.toString()
+      )
+    nm = @notificationManager
+    cmd.on('close', (code) ->
+      nm.addInfo(result)
+      )


Robustly handle phantomjs callbacks. Fixes #140

--- lib/mocha-phantomjs.coffee
+++ lib/mocha-phantomjs.coffee
@@ -71,9 +71,9 @@
       @onLoadFailed() if status isnt 'success'
       @waitForInitMocha()
     @page.onCallback = (data) =>
-      if data.hasOwnProperty 'Mocha.process.stdout.write'
+      if data?.hasOwnProperty 'Mocha.process.stdout.write'
         @output.write data['Mocha.process.stdout.write']
-      else if data.hasOwnProperty 'mochaPhantomJS.run'
+      else if data?.hasOwnProperty 'mochaPhantomJS.run'
         @waitForRunMocha() if @injectJS()
       true


IMPORTANT now checkpoint is created BEFORE selectTarget

--- lib/operator-insert.coffee
+++ lib/operator-insert.coffee
@@ -50,10 +50,11 @@
   # - one for undo(handled by modeManager)
   # - one for preserve last inserted text
   setCheckpoint: (purpose) ->
+    @checkpoint ?= {}
     @checkpoint[purpose] = @editor.createCheckpoint()

   getCheckpoint: (purpose) ->
-    @checkpoint[purpose]
+    @checkpoint?[purpose]

   # When each mutaion's extent is not intersecting, muitiple changes are recorded
   # e.g
@@ -95,14 +96,14 @@
     limitNumber(@insertionCount, max: 100)

   execute: ->
-    if @isRequireTarget()
-      targetSelected = @selectTarget()
-      if not targetSelected and not @canContinueOnEmptySelection()
-        @vimState.activate('normal')
-        return
-
     if @isRepeated()
       @flashTarget = @trackChange = true
+
+      if @isRequireTarget()
+        targetSelected = @selectTarget()
+        if not targetSelected and not @canContinueOnEmptySelection()
+          return
+
       @editor.transact =>
         @mutateText?()
         for selection, i in @editor.getSelections()
@@ -113,8 +114,13 @@
         @vimState.clearSelections()

     else
-      @checkpoint = {}
       @setCheckpoint('undo')
+      if @isRequireTarget()
+        targetSelected = @selectTarget()
+        if not targetSelected and not @canContinueOnEmptySelection()
+          @vimState.activate('normal')
+          return
+
       @observeWillDeactivateMode()

       @mutateText?()
@@ -263,11 +269,13 @@
   target: 'MoveToNextFoldStart'

 # -------------------------
-class Change extends ActivateInsertMode # FIXME
+class Change extends ActivateInsertMode
   @extend()
   requireTarget: true
   trackChange: true
   supportInsertionCount: false
+
+

   canContinueOnEmptySelection: ->
     not @isOccurrence()


feat(properties): make name's dasherization configurable, and fix the support for '@=' vs '@=ResourceFuntion' properties.

--- coffee/classes/081_base_resource.tail.coffee
+++ coffee/classes/081_base_resource.tail.coffee
@@ -1,9 +1,11 @@
-
 angoolar.BaseResource = class BaseResource extends angoolar.Named
 	$_prefix: '' # by default, we don't want to prefix our resource names
+
+	$_dasherizeName: yes
+
 	$_makeName: ->
 		try
-			name = angoolar.camelToDashes super # dasherize the name of the resource
+			name = if @$_dasherizeName then angoolar.camelToDashes super else super
 		catch e
 			name = ''

@@ -17,7 +19,7 @@

 		apiPath

-	$_underscoreProperties: no
+	$_underscoreProperties: yes

 	$_makeApiProperty : ( property ) -> if @$_underscoreProperties then angoolar.camelToUnderscores property else property
 	$_makeJsonProperty: ( property ) -> if @$_underscoreProperties then angoolar.camelToUnderscores property else property
@@ -99,7 +101,7 @@
 				apiProperty  = @$_makeApiProperty property

 				if angular.isString propertyUsage
-					if /[@=]{1,2}/.test propertyUsage
+					if /^[@=]{1,2}$/.test propertyUsage
 						inJson = -1 isnt propertyUsage.indexOf '='
 						inApi  = -1 isnt propertyUsage.indexOf '@'


[EVENTRIC-50] Fix remote projection feature specs working with promises

--- spec/feature/remote_projection_feature.spec.coffee
+++ spec/feature/remote_projection_feature.spec.coffee
@@ -42,7 +42,7 @@
       projectionId = null
       exampleRemote = null

-      beforeEach (done) ->
+      beforeEach ->
         exampleRemote = eventric.remote 'Example'

         class ExampleProjection
@@ -57,7 +57,6 @@
         exampleRemote.initializeProjectionInstance 'ExampleProjection'
         .then (_projectionId) ->
           projectionId = _projectionId
-          done()

       it 'then the projection should update the projection as expected', ->
@@ -95,7 +94,7 @@
         exampleRemote.addProjection 'ExampleProjection', ExampleProjection

-      it 'then it should update the projection as expected', (done) ->
+      it 'then it should update the projection as expected', ->
         testExampleId = null
         exampleRemote.command 'CreateExample'
         .then (exampleId) ->
@@ -118,7 +117,6 @@
           exampleProjection = exampleRemote.getProjectionInstance projectionId
           expect(exampleProjection.updated).to.be.true
           expect(exampleProjection.eventBus.emit).to.have.been.calledWith 'changed', exampleProjection
-          done()

       it 'then we should be able to remove it', ->


remove redundant browsers, switch to dot reporter

--- tests/karma/browserstack.coffee
+++ tests/karma/browserstack.coffee
@@ -14,11 +14,7 @@
   'windows-8-firefox' : ['Windows', '8', 'firefox', '24.0']
   'windows-8-safari'  : ['Windows', '8', 'safari', '5.1']
   'windows-8-ie-10'   : ['Windows', '8', 'ie', '10.0']
-
-  'windows-7-chrome'  : ['Windows', '7', 'chrome', '29.0']
-  'windows-7-firefox' : ['Windows', '7', 'firefox', '24.0']
-  'windows-7-safari'  : ['Windows', '7', 'safari', '5.1']
-  'windows-7-ie-10'   : ['Windows', '7', 'ie', '10.0']
+
   'windows-7-ie-9'    : ['Windows', '7', 'ie', '9.0']
   'windows-7-ie-8'    : ['Windows', '7', 'ie', '8.0']

@@ -44,6 +40,7 @@
 options.basePath = 'build'
 options.customLaunchers = customLaunchers
 options.exclude = ['tests/mocha/editor.js']
+options.reporters = ['dot']

 browserGroups = _.groupBy(browserList, (browser, i) ->
   return Math.floor(i / BROWSERSTACK_PARALLEL)


Use TextEditor APIs instead of private, deprecated displayBuffer field

--- lib/bookmarks.coffee
+++ lib/bookmarks.coffee
@@ -81,17 +81,14 @@
     markers[bookmarkIndex]

   createBookmarkMarker: (range) ->
-    bookmark = @displayBuffer().markBufferRange(range, @bookmarkMarkerAttributes(invalidate: 'surround'))
+    bookmark = @editor.markBufferRange(range, @bookmarkMarkerAttributes(invalidate: 'surround'))
     @subscribe bookmark.onDidChange ({isValid}) ->
       bookmark.destroy() unless isValid
     @editor.decorateMarker(bookmark, {type: 'line-number', class: 'bookmarked'})
     bookmark

   findBookmarkMarkers: (attributes={}) ->
-    @displayBuffer().findMarkers(@bookmarkMarkerAttributes(attributes))
+    @editor.findMarkers(@bookmarkMarkerAttributes(attributes))

   bookmarkMarkerAttributes: (attributes={}) ->
     _.extend(attributes, class: 'bookmark')
-
-  displayBuffer: ->
-    @editor.displayBuffer


UP [assets javascripts] Remove vendors include in main.coffee


Pick out only the data needed... sounds familiar :thinking:

--- apps/sitemaps/routes.coffee
+++ apps/sitemaps/routes.coffee
@@ -112,7 +112,14 @@
       limit: FUSION_PAGE_SIZE
       'fields[]': switch template
         when 'artworks' then ['id']
-        when 'images' then ['id', 'images', 'title', 'artist', 'date']
+        when 'images' then [
+          'id'
+          'images.image_urls.large'
+          'images.image_urls.larger'
+          'title'
+          'artist.name'
+          'date'
+        ]
     )
     .end (err, sres) ->
       return next err if err


Equation Builder removing parentheses around -/+

--- javascripts/widgets/math_buttons.coffee
+++ javascripts/widgets/math_buttons.coffee
@@ -35,7 +35,7 @@
   negative_slash_positive: (opts)->
     @button({
       value: '-/+'
-      label: "(–/+)"
+      label: "–/+"
       class: 'math-button number-specifier negative-slash-positive'
     }, opts)


Update rabbit and mongo urls

Update rabbit and mongo urls

--- config/main.rc.coffee
+++ config/main.rc.coffee
@@ -6,11 +6,11 @@
 version = "0.9.10" #fs.readFileSync nodePath.join(__dirname, '../.revision'), 'utf-8'

 # PROD
-mongo = 'PROD-koding:34W4BXx595ib3J72k5Mh@web0.beta.system.aws.koding.com:27017/beta_koding'
+# mongo = 'PROD-koding:34W4BXx595ib3J72k5Mh@web0.beta.system.aws.koding.com:27017/beta_koding'
+mongo = 'PROD-koding:34W4BXx595ib3J72k5Mh@db-m0.prod.aws.koding.com:27017/beta_koding'

 # RabbitMQ Host
-rabbit_host = fs.readFileSync '/etc/rabbit_host', 'utf-8'
-rabbit_host = rabbit_host.trim()
+rabbit_host = 'rabbit-b.prod.aws.koding.com'

 projectRoot = nodePath.join __dirname, '..'


Reuse archive's fd in Node asar API

Reuse archive's fd in Node asar API

--- atom/common/lib/asar.coffee
+++ atom/common/lib/asar.coffee
@@ -236,21 +236,20 @@
       return fs.readFile realPath, options, callback

     if not options
-      options = encoding: null, flag: 'r'
+      options = encoding: null
     else if util.isString options
-      options = encoding: options, flag: 'r'
+      options = encoding: options
     else if not util.isObject options
       throw new TypeError('Bad arguments')

-    flag = options.flag || 'r'
     encoding = options.encoding

     buffer = new Buffer(info.size)
-    open archive.path, flag, (error, fd) ->
-      return callback error if error
-      fs.read fd, buffer, 0, info.size, info.offset, (error) ->
-        fs.close fd, ->
-          callback error, if encoding then buffer.toString encoding else buffer
+    fd = archive.getFd()
+    return notFoundError asarPath, filePath, callback unless fd >= 0
+
+    fs.read fd, buffer, 0, info.size, info.offset, (error) ->
+      callback error, if encoding then buffer.toString encoding else buffer

   openSync = fs.openSync
   readFileSync = fs.readFileSync
@@ -270,23 +269,19 @@
       return fs.readFileSync realPath, options

     if not options
-      options = encoding: null, flag: 'r'
+      options = encoding: null
     else if util.isString options
-      options = encoding: options, flag: 'r'
+      options = encoding: options
     else if not util.isObject options
       throw new TypeError('Bad arguments')

-    flag = options.flag || 'r'
     encoding = options.encoding

     buffer = new Buffer(info.size)
-    fd = openSync archive.path, flag
-    try
-      fs.readSync fd, buffer, 0, info.size, info.offset
-    catch e
-      throw e
-    finally
-      fs.closeSync fd
+    fd = archive.getFd()
+    notFoundError asarPath, filePath unless fd >= 0
+
+    fs.readSync fd, buffer, 0, info.size, info.offset
     if encoding then buffer.toString encoding else buffer

   readdir = fs.readdir


:bug: fix wrap-around bug

:bug: fix wrap-around bug

--- lib/prediction-list.coffee
+++ lib/prediction-list.coffee
@@ -42,16 +42,17 @@
       when amount > 0 then 'next'
       else 'prev'

-    index = @_index += amount
+    @_index += amount

-    unless 0 <= index < @_predictions.length
-      # Wrap the value around.
-      index %%= @_predictions.length
-      wrapped = false
-    else
-      wrapped = true
+    wrapped = switch
+      when @_predictions.length is 0 then false
+      when not (0 <= @_index < @_predictions.length)
+        # Wrap the value around.
+        @_index %%= @_predictions.length
+        false
+      else true

-    status = {index, direction, wrapped, target: @}
+    status = {index: @_index, direction, wrapped, target: @}

     @emitter.emit 'did-change-index', status
     status