fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -105,7 +105,7 @@
         r = labelRadius
         if x <= 0
             r = -r
-        if x < 0 and y < 0 and x > -radiusi
+        if x < 0 and y < 0 and x > -radiusi and x/y < 1
             x = x * mx
         return "translate(" + (x/h * labelRadius) + "," + (y/h * labelRadius) + ")"
         #return "translate(" + r + "," + y + ")"#(y/h * labelRadius) + ")"


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -191,13 +191,15 @@
         arcs.append("svg:path").attr("fill", (d, i) -> color i)
           .attr("fill-opacity", 0.4).attr("d", arc)

-  renderX5: (data) ->
+  render: (data) ->
         #console.log("update pie", data);

         if !data
           data = @get("value")
         if !data
           return
+
+        piedata = [data]

         $(@node).children(".title").text($(@node).attr("data-title"))
         $(@node).children(".more-info").text($(@node).attr("data-moreinfo"))
@@ -279,7 +281,7 @@

         path = pathGroup.selectAll("path.pie")
             #.data(data)
-            .data([data])
+            .data(piedata)

         path.enter().append("path")
             .attr("class", "pie")
@@ -307,7 +309,7 @@
             .remove()

         labels = labelGroup.selectAll("text")
-            .data([data].sort((p1,p2) -> return p1.startAngle - p2.startAngle))
+            .data(piedata.sort((p1,p2) -> return p1.startAngle - p2.startAngle))
         labels.enter()
             .append("text")
             .attr("text-anchor", "middle")
@@ -410,7 +412,7 @@

         pointers = pointerGroup.selectAll("path.pointer")
-            .data([data])
+            .data(piedata)
         pointers.enter()
             .append("path")
             .attr("class", "pointer")
@@ -426,7 +428,7 @@
                 return "M" + (d.r-2) + "," + d.b + "L" + (d.l+2) + "," + d.b + " " + d.cx + "," + d.cy

         )
-  render: (data) ->
+  renderX5: (data) ->
         #console.log("update pie", data);

         if !data


fix some style and issues with https://github.com/sagemathinc/smc/pull/856 (student adding)

--- src/smc-webapp/course/students_panel.cjsx
+++ src/smc-webapp/course/students_panel.cjsx
@@ -163,7 +163,11 @@
         nb_selected = @state.selected_entries?.length ? 0
         _ = require('underscore')
         es = @state.existing_students
-        existing = _.keys(es.email).length + _.keys(es.account).length > 0
+        if es?
+            existing = _.keys(es.email).length + _.keys(es.account).length > 0
+        else
+            # es not defined when user clicks the close button on the warning.
+            existing = 0
         btn_text = switch options.length
             when 0 then (if existing then "Student already added" else "No student found")
             when 1 then "Add student"
@@ -186,10 +190,14 @@
                 user = @props.user_map.get(account_id)
                 existing.push("#{user.get('first_name')} #{user.get('last_name')}")
             if existing.length > 0
-                msg = "Already existing student(s) or project collaborator(s): #{existing.join(', ')}"
+                if existing.length > 1
+                    msg = "Already added students or project collaborators: "
+                else
+                    msg = "Already added student or project collaborator: "
+                msg += existing.join(', ')
                 ed = <ErrorDisplay bsStyle='info' error=msg onClose={=>@setState(existing_students:undefined)} />
         if ed?
-            <Row><Col md=12>{ed}</Col></Row>
+            <Row style={marginTop:'1em'}><Col md=5 lgOffset=7>{ed}</Col></Row>
         else
             <Row></Row>


Fixed issue #2818 - TimeAccounting modal dialog input not focused.

--- app/assets/javascripts/app/controllers/_application_controller.coffee
+++ app/assets/javascripts/app/controllers/_application_controller.coffee
@@ -618,7 +618,17 @@

   onShown: (e) =>
     if @autoFocusOnFirstInput
-      @$('.form-group').first().find('input:not([disabled]):not([type="hidden"]):not(".btn"), select:not([disabled]), textarea:not([disabled])').first().focus()
+
+      # select generated form
+      form = @$('.form-group').first()
+
+      # if not exists, use whole @el
+      if !form.get(0)
+        form = @el
+
+      # focus first input, select or textarea
+      form.find('input:not([disabled]):not([type="hidden"]):not(".btn"), select:not([disabled]), textarea:not([disabled])').first().focus()
+
     @initalFormParams = @formParams()

   localOnClose: (e) =>


Add additional documentation on configuration for github-commit-link

--- src/scripts/github-commit-link.coffee
+++ src/scripts/github-commit-link.coffee
@@ -7,7 +7,15 @@
 #
 # Configuration:
 #   HUBOT_GITHUB_REPO
+#     The `user/repository` that you want to connect to. example: github/hubot-scripts
 #   HUBOT_GITHUB_TOKEN
+#     You can retrieve your github token via:
+#       curl -i https://api.github.com/authorizations -d '{"scopes":["repo"]}' -u "yourusername"
+#     Enter your Github password when prompted. When you get a response, look for the "token" value
+#
+#     See the following for more details:
+#       http://developer.github.com/v3/#authentication
+#       https://github.com/iangreenleaf/githubot
 #
 # Commands:
 #   Listens for <SHA> and links to the commit for your default repo on github


disable throttle -- doesn't pass test suite

--- src/smc-hub/postgres-synctable.coffee
+++ src/smc-hub/postgres-synctable.coffee
@@ -663,7 +663,7 @@

         @_select_query = "SELECT #{(quote_field(x) for x in @_select_columns)} FROM #{@_table}"

-        @_update = underscore.throttle(@_update, 500)
+        #@_update = underscore.throttle(@_update, 500)

         @_init (err) => cb(err, @)


# TODO 暂时没用到表单提交的contentType     # 如果不注释，windows系统会报错［第一次点击时，系统会报错，第二次后就没问题］     # app.use(bodyParser.urlencoded())

--- lib/emulator/debug-server.coffee
+++ lib/emulator/debug-server.coffee
@@ -51,8 +51,9 @@

     # 代理请求之后，这样不会破坏http结构
     app.use(bodyParser.json())
-    app.use(bodyParser.urlencoded())
-
+    # TODO 暂时没用到表单提交的contentType
+    # 如果不注释，windows系统会报错［第一次点击时，系统会报错，第二次后就没问题］
+    # app.use(bodyParser.urlencoded())
     router = express.Router()
     (require './cordova-emulate')(router, options.rootPath) #TODO: copy一份
     app.use(router)


Fix undefined page number

Fix undefined page number

--- views/akashic-resource-table-area.cjsx
+++ views/akashic-resource-table-area.cjsx
@@ -184,7 +184,7 @@
           </Col>
           <Col xs={3}>
             <ButtonGroup justified>
-              <DropdownButton eventKey={4} title={__ "Page %s", "#{@props.activePage}"} block>
+              <DropdownButton eventKey={4} title={__ "Page %s", "#{@state.activePage}"} block>
               {
                 if @state.dataShow.length isnt 0
                   for index in [1..Math.ceil(@state.dataShow.length/@state.showAmount)]


Show label when update is available

Show label when update is available

--- src/app/package-config-view.coffee
+++ src/app/package-config-view.coffee
@@ -13,6 +13,7 @@
       @div outlet: 'heading', class: 'panel-heading', =>
         @span outlet: 'name'
         @span outlet: 'version', class: 'label'
+        @span outlet: 'update', class: 'label label-info', 'Update Available'
         @div class: 'btn-group pull-right', =>
           @button outlet: 'defaultAction', class: 'btn btn-small btn-primary'
           @button outlet: 'dropdownButton', class: 'btn btn-small btn-primary dropdown-toggle', 'data-toggle': 'dropdown', =>
@@ -123,6 +124,7 @@

   updatePackageState: ->
     @disabled = atom.isPackageDisabled(@pack.name)
+    @updateAvailable = false
     @bundled = false
     loadedPackage = atom.getLoadedPackage(@pack.name)
     packagePath = loadedPackage?.path ? atom.resolvePackagePath(@pack.name)
@@ -137,6 +139,11 @@
         try
           version = Package.loadMetadata(@pack.name).version
       @updateAvailable = semver.gt(@pack.version, version)
+
+    if @updateAvailable
+      @update.show()
+    else
+      @update.hide()

   updateEnabledState: ->
     enableLink = @enableToggle.find('a')


Fix minor typo

Fix minor typo

--- library/membrane/securing.coffee
+++ library/membrane/securing.coffee
@@ -91,7 +91,7 @@
         key = (x) -> "securing:rduplex:token:dynamic:#{x}"
         gen = (u) -> md5().update("#{@uuid}-#{u}").digest("hex")
         internal = (e) -> "internal Redis error: #{e}"
-        noUuid = "the context has not UUID attached"
+        noUuid = "the context has no UUID attached"
         assert uuid = binder.uuid.request, noUuid
         @redis.get key(uuid), (error, value) =>
             assert not _.isObject(error), internal error


Fix the typo

Fix the typo

--- library/visualize/deposit.coffee
+++ library/visualize/deposit.coffee
@@ -69,7 +69,7 @@
     # require you to supply a tag, please do this as it is necessary.
     # If the record is not found at the storage, exception is thrown.
     get: (tag = uuid.v1(), destroy) ->
-        noSubject = "Cannot found entry at #{tag}"
+        noSubject = "Cannot locate entry at #{tag}"
         noTagging = "The supplied tagging is not valid"
         throw new Error noTagging unless _.isString tag
         throw new Error noSubject unless tag of @storage


chr2dpanelframe: remove 'horizontal' (not used)

--- src/chr2dpanelframe.coffee
+++ src/chr2dpanelframe.coffee
@@ -21,7 +21,6 @@
     boxwidth = chartOpts?.boxwidth ? 2           # width of outer box in pixels
     chrGap = chartOpts?.chrGap ? 6 # gap between chromosomes in pixels
     oneAtTop = chartOpts?.oneAtTop ? false # if true, put chromosome 1 at the top rather than bottom
-    horizontal = chartOpts.horizontal ? false # if true, chromosomes on vertical axis (xlab, ylab, etc stay the same)
     # chartOpts end
     xscale = null
     yscale = null
@@ -97,12 +96,8 @@
         # rotate y-axis title?
         rotate_ylab = rotate_ylab ? (ylab.length > 1)

-        xaxis = g.append("g").attr("class", () ->
-                                return "y axis" if horizontal
-                                "x axis")
-        yaxis = g.append("g").attr("class", () ->
-                                return "x axis" if horizontal
-                                "y axis")
+        xaxis = g.append("g").attr("class", "x axis")
+        yaxis = g.append("g").attr("class", "y axis")

         xaxis.append("text").attr("class", "title")
              .text(() -> xlab)


started adding chord support, added some doc

--- marktab.coffee
+++ marktab.coffee
@@ -19,6 +19,7 @@
 		pullOffPattern = /p/
 		slideUpPattern = /\//
 		slideDownPattern = /\\/
+		chordPattern = /\(([0-9]:[0-9])+\)/
 		lines = input.split("\n")
 		for line in lines
 			parts = line.split(" ")
@@ -26,30 +27,38 @@
 			for part, i in parts
 				jsonPart = {}
 				if part.match(notePattern)
+					# note
 					jsonPart = this.parseNotes(part, i)
 					lastString = parseInt(_.keys(jsonPart), 10)
 				else if part.match(restPattern)
 					# rest
 				else if part.match(hammerPattern)
+					# hammer-on
 					if !lastString
 						throw "invalid hammer-on"
 					jsonPart[lastString] = []
 					jsonPart[lastString][i] = "h"
 				else if part.match(pullOffPattern)
+					# pull-off
 					if !lastString
 						throw "invalid pull-off"
 					jsonPart[lastString] = []
 					jsonPart[lastString][i] = "p"
 				else if part.match(slideUpPattern)
+					# slide-up
 					if !lastString
 						throw "invalid slide-up"
 					jsonPart[lastString] = []
 					jsonPart[lastString][i] = "/"
 				else if part.match(slideDownPattern)
+					# slide-down
 					if !lastString
 						throw "invalid slide-down"
 					jsonPart[lastString] = []
 					jsonPart[lastString][i] = "\\"
+				else if part.match(chordPattern)
+					# chord
+					jsonPart = this.parseChord(part, i)
 				else
 					throw "unknown pattern: " + part
 				this.mergeMaps(json, jsonPart)
@@ -57,6 +66,7 @@
 			this.parseJson(json)
 		json

+	# merges two tabMaps together, with the source overwriting the destination
 	mergeMaps: (dest, source) ->
 		for stringNum, stringNotes of source
 			for fret, i in stringNotes
@@ -66,6 +76,7 @@
 				if fret?
 					dest[stringNum][i] = fret

+	# returns a new tabMap
 	cloneTabMap: (tabMap) ->
 		result = {}
 		for stringNum, stringNotes of tabMap
@@ -86,7 +97,7 @@
 		result

 	# parses marktab chords into json
-	parseChord: (chord) ->
+	parseChord: (chord, i = 0) ->
 		""

 	# parses marktab riffs into json


Use fs-plus

Use fs-plus

--- src/browser/squirrel-update.coffee
+++ src/browser/squirrel-update.coffee
@@ -1,6 +1,6 @@
 app = require 'app'
 ChildProcess = require 'child_process'
-fs = require 'fs'
+fs = require 'fs-plus'
 path = require 'path'

 rootAtomFolder = path.resolve(process.execPath, '..', '..')


fix(squirrel): fix win32 update to enable restart and launch from path

--- src/browser/squirrel-update.coffee
+++ src/browser/squirrel-update.coffee
@@ -54,11 +54,23 @@
 spawnUpdate = (args, callback) ->
   spawn(updateDotExe, args, callback)

+isAscii = (text) ->
+  index = 0
+  while index < text.length
+    return false if text.charCodeAt(index) > 127
+    index++
+  true
+
 # Get the user's PATH environment variable registry value.
 getPath = (callback) ->
   spawnReg ['query', environmentKeyPath, '/v', 'Path'], (error, stdout) ->
     if error?
       if error.code is 1
+        # FIXME Don't overwrite path when reading value is disabled
+        # https://github.com/atom/atom/issues/5092
+        if stdout.indexOf('ERROR: Registry editing has been disabled by your administrator.') isnt -1
+          return callback(error)
+
         # The query failed so the Path does not exist yet in the registry
         return callback(null, '')
       else
@@ -74,9 +86,74 @@
     segments = lines[lines.length - 1]?.split('    ')
     if segments[1] is 'Path' and segments.length >= 3
       pathEnv = segments?[3..].join('    ')
-      callback(null, pathEnv)
+      if isAscii(pathEnv)
+        callback(null, pathEnv)
+      else
+        # FIXME Don't corrupt non-ASCII PATH values
+        # https://github.com/atom/atom/issues/5063
+        callback(new Error('PATH contains non-ASCII values'))
     else
       callback(new Error('Registry query for PATH failed'))
+
+# Add atom and apm to the PATH
+#
+# This is done by adding .cmd shims to the root bin folder in the N1
+# install directory that point to the newly installed versions inside
+# the versioned app directories.
+addCommandsToPath = (callback) ->
+  installCommands = (callback) ->
+    nylasCommandPath = path.join(binFolder, 'N1.cmd')
+    relativeN1Path = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'N1.cmd'))
+    nylasCommand = "@echo off\r\n\"%~dp0\\#{relativeN1Path}\" %*"
+
+    nylasShCommandPath = path.join(binFolder, 'N1')
+    relativeN1ShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'N1.sh'))
+    nylasShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeN1ShPath.replace(/\\/g, '/')}\" \"$@\""
+
+    apmCommandPath = path.join(binFolder, 'apm.cmd')
+    relativeApmPath = path.relative(binFolder, path.join(process.resourcesPath, 'app', 'apm', 'bin', 'apm.cmd'))
+    apmCommand = "@echo off\r\n\"%~dp0\\#{relativeApmPath}\" %*"
+
+    apmShCommandPath = path.join(binFolder, 'apm')
+    relativeApmShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'apm.sh'))
+    apmShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeApmShPath.replace(/\\/g, '/')}\" \"$@\""
+
+    fs.writeFile nylasCommandPath, nylasCommand, ->
+      fs.writeFile nylasShCommandPath, nylasShCommand, ->
+        fs.writeFile apmCommandPath, apmCommand, ->
+          fs.writeFile apmShCommandPath, apmShCommand, ->
+            callback()
+
+  addBinToPath = (pathSegments, callback) ->
+    pathSegments.push(binFolder)
+    newPathEnv = pathSegments.join(';')
+    spawnSetx(['Path', newPathEnv], callback)
+
+  installCommands (error) ->
+    return callback(error) if error?
+
+    getPath (error, pathEnv) ->
+      return callback(error) if error?
+
+      pathSegments = pathEnv.split(/;+/).filter (pathSegment) -> pathSegment
+      if pathSegments.indexOf(binFolder) is -1
+        addBinToPath(pathSegments, callback)
+      else
+        callback()
+
+# Remove N1 and apm from the PATH
+removeCommandsFromPath = (callback) ->
+  getPath (error, pathEnv) ->
+    return callback(error) if error?
+
+    pathSegments = pathEnv.split(/;+/).filter (pathSegment) ->
+      pathSegment and pathSegment isnt binFolder
+    newPathEnv = pathSegments.join(';')
+
+    if pathEnv isnt newPathEnv
+      spawnSetx(['Path', newPathEnv], callback)
+    else
+      callback()

 # Create a desktop and start menu shortcut by using the command line API
 # provided by Squirrel's Update.exe
@@ -123,15 +200,18 @@
   switch squirrelCommand
     when '--squirrel-install'
       createShortcuts ->
-        app.quit()
+        addCommandsToPath ->
+          app.quit()
       true
     when '--squirrel-updated'
       updateShortcuts ->
-        app.quit()
+        addCommandsToPath ->
+          app.quit()
       true
     when '--squirrel-uninstall'
       removeShortcuts ->
-        app.quit()
+        removeCommandsFromPath ->
+          app.quit()
       true
     when '--squirrel-obsolete'
       app.quit()


Attempt at fixing #36

Attempt at fixing #36

--- lib/completion-backend.coffee
+++ lib/completion-backend.coffee
@@ -47,13 +47,15 @@
       Promise.resolve []

   getBufferInfo: ({buffer}) =>
+    unless buffer?
+      throw new Error("Null buffer in getBufferInfo!")
     if @bufferMap.has buffer
-      bufferInfo: @bufferMap.get buffer
-    else
+      bi = @bufferMap.get buffer
+    unless bi?.buffer?
       @bufferMap.set buffer, bi = new BufferInfo(buffer)
-      bi.onDidDestroy =>
-        @bufferMap.delete buffer
-      bufferInfo: bi
+      # bi.onDidDestroy =>
+      #   @bufferMap.delete buffer
+    bufferInfo: bi

   getModuleMap: ({bufferInfo, rootDir}) =>
     unless bufferInfo? or rootDir?


removed two tap event

removed two tap event

--- src/quo.gestures.coffee
+++ src/quo.gestures.coffee
@@ -121,8 +121,6 @@
                     drag_direction = _swipeDirection(FIRST_TOUCH[0].x, CURRENT_TOUCH[0].x, FIRST_TOUCH[0].y, CURRENT_TOUCH[0].y)
                     _trigger "drag" + drag_direction
                     anyevent = true
-            if not anyevent
-                _trigger "twoTap"
             _cleanGesture()
         EVENT = undefined


add post method

add post method

--- src/coffee/services/network.coffee
+++ src/coffee/services/network.coffee
@@ -7,10 +7,26 @@
 ##

 angular.module('CoolFormServices').
-  factory('definitionService', ($q) ->
-    return (url) ->
-      deferred = $q.defer()
-      $.getJSON(url, (data) ->
-        deferred.resolve(data))
-      return deferred.promise
+  factory('networkService', ($q) ->
+    return ->
+
+      getJSON = (url) ->
+        deferred = $q.defer()
+        $.getJSON(url, (data) ->
+          deferred.resolve(data))
+        return deferred.promise
+
+      postForm = (url, data, onSuccess, onError) ->
+        cfg =
+          type: "POST"
+          url: url
+          data: data
+          success: onSuccess
+          error: onError
+        $.ajax(cfg)
+
+      net =
+        getJSON: getJSON
+
+      return net
   )


Tweak

Tweak

--- src/core/view-model.coffee
+++ src/core/view-model.coffee
@@ -134,11 +134,7 @@
       write: (new_model) => kb.ignore =>
         return if (kb.utils.wrappedObject(@) is new_model) or kb.wasReleased(@) or not event_watcher

-        if new_model and kb.Backbone and kb.Backbone.ModelRef and new_model instanceof kb.Backbone.ModelRef
-          @__kb.store.reuse(@, new_model.model())
-        else
-          @__kb.store.reuse(@, new_model)
-
+        @__kb.store.reuse(@, (if new_model and kb.Backbone and kb.Backbone.ModelRef and new_model instanceof kb.Backbone.ModelRef then new_model.model() else new_model))
         event_watcher.emitter(new_model); _model(event_watcher.ee)
         not event_watcher.ee or @createObservables(event_watcher.ee)
     }


Fix problem removing file via side bar

Fix problem removing file via side bar

--- packages/rocketchat-ui-flextab/flex-tab/tabs/uploadedFilesList.coffee
+++ packages/rocketchat-ui-flextab/flex-tab/tabs/uploadedFilesList.coffee
@@ -54,7 +54,7 @@
 				showConfirmButton: false

 				msg = ChatMessage.findOne { file: { _id: self._id } }
-				fileCollection.remove self._id, () ->
+				RocketChat.models.Uploads.remove self._id, () ->
 					chatMessages[Session.get('openedRoom')].deleteMsg(msg);

 	'scroll .content': _.throttle (e, t) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_selector.coffee
+++ src/engine_selector.coffee
@@ -22,7 +22,7 @@
   ###
   closest: (selector, position) ->
     self.find(selector).sort (a, b) ->
-
+      a.po

   ###*
   Get a selection of GameObjects that match the specified selector criteria. The selector language


Test for view destruction without assuming pane destruction

For forward-compatibility with empty panes

--- spec/archive-editor-view-spec.coffee
+++ spec/archive-editor-view-spec.coffee
@@ -93,7 +93,7 @@
       runs ->
         expect(atom.workspaceView.getActivePane().getItems().length).toBe 1
         atom.workspaceView.getActivePaneItem().file.emit('removed')
-        expect(atom.workspaceView.getActivePane()).toBeFalsy()
+        expect(atom.workspaceView.getActivePaneItem()).toBeUndefined()

   describe "when the file is modified", ->
     it "refreshes the view", ->


now block wise duplicate work again

now block wise duplicate work again

--- lib/move-selected-text.coffee
+++ lib/move-selected-text.coffee
@@ -44,6 +44,7 @@
     fn(selection)
     disposable?.dispose()

+# Up/Down
 class MoveSelectedTextUp extends MoveSelectedText
   direction: 'up'

@@ -168,11 +169,9 @@
 class MoveSelectedTextDown extends MoveSelectedTextUp
   direction: 'down'

+# Left/Right
 class MoveSelectedTextLeft extends MoveSelectedTextUp
   direction: 'left'
-
-  canMoveSelection: (selection) ->
-    selection.getBufferRange().start.column > 0

   moveLinewise: (selection) ->
     switch @direction
@@ -180,7 +179,9 @@
       when 'right' then selection.indentSelectedRows()

   moveCharacterwise: (selection) ->
-    return unless @canMoveSelection(selection)
+    if @direction is 'left'
+      return unless selection.getBufferRange().start.column > 0
+
     if @direction is 'right'
       endPoint = selection.getBufferRange().end
       insertTextAtPoint(@editor, endPoint, " ") if pointIsAtEndOfLine(@editor, endPoint)
@@ -190,8 +191,6 @@

 class MoveSelectedTextRight extends MoveSelectedTextLeft
   direction: 'right'
-  canMoveSelection: (selection) ->
-    true

 # Duplicate
 # -------------------------
@@ -232,6 +231,7 @@
         else
           @duplicateCharacterwise(selection, count)

+# Up/Down
 class DuplicateSelectedTextUp extends DuplicateSelectedText
   direction: 'up'
   withBlockwise: (fn) ->
@@ -243,25 +243,22 @@
       @vimState.activate('visual', 'characterwise')

   execute: ->
-    if @isLinewise() or (not @isLinewise() and (@direction in ['right', 'left']))
-      super
-    else
-      @editor.transact =>
-        return if (count = @getCount()) is 0
-        @withBlockwise =>
-          if @direction is 'down'
-            bottom = _.last(@editor.getSelectionsOrderedByBufferPosition())
-            range = bottom.getBufferRange()
-            if range.end.row is @editor.getLastBufferRow()
-              # Since inserting new line modify selected range
-              # We have to revert selction range to preserved one.
-              ensureBufferEndWithNewLine(@editor)
-              bottom.setBufferRange(range)
-
-          bss = @vimState.getBlockwiseSelectionsOrderedByBufferPosition()
-          bss.reverse() if @direction is 'down'
-          for bs in bss
-            @duplicateCharacterwise(bs, count)
+    @editor.transact =>
+      return if (count = @getCount()) is 0
+      @withBlockwise =>
+        if @direction is 'down'
+          bottom = _.last(@editor.getSelectionsOrderedByBufferPosition())
+          range = bottom.getBufferRange()
+          if range.end.row is @editor.getLastBufferRow()
+            # Since inserting new line modify selected range
+            # We have to revert selction range to preserved one.
+            ensureBufferEndWithNewLine(@editor)
+            bottom.setBufferRange(range)
+
+        bss = @vimState.getBlockwiseSelectionsOrderedByBufferPosition()
+        bss.reverse() if @direction is 'down'
+        for bs in bss
+          @duplicateCharacterwise(bs, count)

   duplicateLinewise: (selection, count) ->
     getText = ->
@@ -316,10 +313,10 @@
     insertBlankLine(height * count)

     ranges = []
-    @countTimes (num) =>
+    @countTimes @getCount(), ({count}) =>
       for selection, i in blockwiseSelection.selections
         text = selection.getText()
-        range = getRangeToInsert(selection, num)
+        range = getRangeToInsert(selection, count)
         insertSpacesToPoint(@editor, range.start)
         ranges.push @editor.setTextInBufferRange(range, text)
     select(ranges)
@@ -327,36 +324,43 @@
 class DuplicateSelectedTextDown extends DuplicateSelectedTextUp
   direction: 'down'

-class DuplicateSelectedTextLeft extends DuplicateSelectedTextUp
+# Left/Right
+class DuplicateSelectedTextLeft extends DuplicateSelectedText
   direction: 'left'

+  # No behavior-diff by 'isOverwriteMode'
   duplicateLinewise: (selection, count) ->
     getText = ->
       swrap(selection).lineTextForBufferRows()
         .map (text) -> text.repeat(count+1)
         .join("\n") + "\n"

+    getRange = ->
+      selection.getBufferRange()
+
     @withLinewise selection, ->
-      text = getText()
-      range = selection.getBufferRange()
-      setTextInRangeAndSelect(range, text, selection)
+      setTextInRangeAndSelect(getRange(), getText(), selection)

   duplicateCharacterwise: (selection, count) ->
-    getRangeToInsert = (text) =>
+    getText = ->
+      selection.getText().repeat(count)
+
+    getRange = =>
+      width = getText().length
       {start, end} = selection.getBufferRange()
-      if @isOverwriteMode()
-        width = text.length
-        switch @direction
-          when 'right' then [end, end.translate([0, +width])]
-          when 'left' then [start.translate([0, -width]), start]
-      else
-        switch @direction
-          when 'right' then [end, end]
-          when 'left' then [start, start]
-
-    text = selection.getText().repeat(count)
-    range = getRangeToInsert(text)
-    setTextInRangeAndSelect(range, text, selection)
+      switch @direction
+        when 'right'
+          range = new Range(end, end)
+          if @isOverwriteMode()
+            range.end = range.end.translate([0, +width])
+          range
+        when 'left'
+          range = new Range(start, start)
+          if @isOverwriteMode()
+            range.start = range.start.translate([0, -width])
+          range
+
+    setTextInRangeAndSelect(getRange(), getText(), selection)

 class DuplicateSelectedTextRight extends DuplicateSelectedTextLeft
   direction: 'right'


Re-enable tree-sitter parsing

Re-enable tree-sitter parsing

--- atom/config.cson
+++ atom/config.cson
@@ -86,7 +86,6 @@
     ]
     titleBar: "custom"
     uriHandlerRegistration: "always"
-    useTreeSitterParsers: false
   docblockr:
     align_tags: "shallow"
     extend_double_slash: false


Atom config changes

Atom config changes

--- atom/config.cson
+++ atom/config.cson
@@ -27,5 +27,4 @@
     showInfoMessages: true
     statusBar: "Show error of the selected line"
     showHighlighting: false
-  "tree-view":
-    hideVcsIgnoredFiles: true
+  "tree-view": {}


Fix atom mode reset and enable outline pane toggle

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -22,6 +22,9 @@
     # file operations
     'ctrl-o': 'fuzzy-finder:toggle-file-finder'

+    # toggle option bar
+    'cmd-o': 'outline-view:toggle'
+
 # vim-mode-plus
 # ----------------------------------------
 # Ref : https://github.com/t9md/dotfiles/blob/master/atom/keymap.cson
@@ -35,6 +38,9 @@
     'ctrl-w ctrl-k': 'unset!'
     'ctrl-w ctrl-l': 'unset!'

+    # switch back to normal mode; clears highlighted search
+    'ctrl-space': 'vim-mode-plus:reset-normal-mode'
+
     # file operations
     'ctrl-s': 'custom:vim-save'

@@ -44,9 +50,6 @@

 # normal mode
 'atom-text-editor.vim-mode-plus.normal-mode':
-    # switch back to normal mode; clears highlighted search
-    'ctrl-space': 'vim-mode-plus:reset-normal-mode'
-
     # undo/redo
     'ctrl-u': 'vim-mode-plus:redo'

@@ -77,7 +80,7 @@

 # not insert mode (normal and visual)
 'atom-text-editor.vim-mode-plus:not(.insert-mode)':
- # disable noob navigation and enable quick pane resizing
+    # disable noob navigation and enable quick pane resizing
     'up': 'pane:increase-size'
     'down': 'pane:decrease-size'
     'left': 'core:nop'


output type attribute to script tags

output type attribute to script tags

--- monocat.coffee
+++ monocat.coffee
@@ -59,7 +59,7 @@
         if tag is 'script'
           parsed = uglify.parser.parse data.toString()
           compressed = uglify.uglify.ast_squeeze parsed
-          $el.after '<script>' + uglify.uglify.gen_code(compressed) + '</script>'
+          $el.after '<script type="text/javascript">' + uglify.uglify.gen_code(compressed) + '</script>'
         else if tag is 'style'
           $el.after '<style>' + cleanCss.process(data.toString()) + '</style>'


fixed some more formatting

fixed some more formatting

--- src/google-calendar.coffee
+++ src/google-calendar.coffee
@@ -91,13 +91,13 @@
       callback: (err, data)->
         return console.log(err) if err
         items = data.items.map((item)->
-          start = moment(item.start.dateTime).format('H:MM')
-          end = moment(item.end.dateTime).format('H:MM')
+          start = moment(item.start.dateTime).format('M/D h:mm')
+          end = moment(item.end.dateTime).format('M/D h:mm')
           "[#{start}-#{end}] '#{item.summary}' (#{item.location})"
         ).join("\n")
         console.log items
         message = if items.length > 0
-                    "In the next 24 Hrs: #{items}"
+                    "In the next #{daysAhead} day(s): \n#{items}"
                   else
                     "Sorry, no scheduled events in next 24 hrs."
         msg.reply message


update workspace after eval all

update workspace after eval all

--- lib/runtime/evaluation.coffee
+++ lib/runtime/evaluation.coffee
@@ -48,6 +48,7 @@
               code: editor.getText()
             }).then (result) ->
         notifications.show "Evaluation Finished"
+        require('../runtime').workspace.update()

   gotoSymbol: ->
     @withCurrentContext ({editor, mod}) =>


Default argv to '-h' (for now).

Default argv to '-h' (for now).

--- lib/schematype/get-opt.coffee
+++ lib/schematype/get-opt.coffee
@@ -30,6 +30,7 @@
     @add_help()
     # @add_version()

+    argv = ['-h'] unless argv.length
     args = @parser.parseArgs(argv)
     args.inputs = args['<input-file>']
     delete args['<input-file>']


Avatar area popupmessage click logic changed:

 - First try to select message
 - If it fails try to fetchmessages
 - And try again to select message
 - If fails again just show the message without selecting it

--- client/landing/app/Applications/Inbox.kdapplication/AppView.coffee
+++ client/landing/app/Applications/Inbox.kdapplication/AppView.coffee
@@ -91,22 +91,24 @@
         messagesSplit.didResizeBefore = yes
         @newMessageBar.enableMessageActionButtons()

-        selectMessage = =>
-          return if not item
+        messageIsSelectable = =>
           {items} = inboxMessagesList
+          return no if items.length is 0
+          {_id} = item.getData()
           wasMessageInList = no
           items.forEach (message) =>
-            if message.getData()?.getId() is item.getData().getId()
+            if message.getData()?.getId() is _id
               message.click()
               wasMessageInList = yes
-          if not wasMessageInList
-            @propagateEvent KDEventType : "MessageIsSelected", {item, event}
+          wasMessageInList

-        if inboxMessagesList.items.length > 0
-          selectMessage()
+        if item
+          if not messageIsSelectable()
+            inboxMessageListController.loadMessages =>
+              if not messageIsSelectable()
+                @propagateEvent KDEventType : "MessageIsSelected", {item, event}
         else
-          inboxMessageListController.loadMessages =>
-            selectMessage()
+          inboxMessageListController.loadMessages()

     return tab


teams: password strength fix for re-typing the same password

--- client/landing/site.landing/coffee/team/teamusernametabform.coffee
+++ client/landing/site.landing/coffee/team/teamusernametabform.coffee
@@ -47,7 +47,10 @@
         strength = ['bad', 'weak', 'moderate', 'good', 'excellent']

         return  if pass is oldPass
-        return  ps.unsetClass strength.join ' '  if pass is ''
+        if pass is ''
+          ps.unsetClass strength.join ' '
+          oldPass = null
+          return

         KD.utils.checkPasswordStrength pass, (err, report) ->
           oldPass = pass


fix(middleware): Allow middleware to resolve early

--- src/middleware.coffee
+++ src/middleware.coffee
@@ -29,6 +29,12 @@
     new Promise (resolve, reject) =>

       done ?= ->
+
+      # Allow each middleware to resolve the promise early if it calls done()
+      pieceDone = ->
+        done()
+        resolve context
+
       # Execute a single piece of middleware and update the completion callback
       # (each piece of middleware can wrap the 'done' callback with additional
       # logic).
@@ -53,7 +59,7 @@
       # Execute each piece of middleware, collecting the latest 'done' callback
       # at each step.
       process.nextTick =>
-        async.reduce(@stack, done, executeSingleMiddleware, allDone)
+        async.reduce(@stack, pieceDone, executeSingleMiddleware, allDone)

   # Public: Registers new middleware
   #


* forget to require inherits

* forget to require inherits

--- src/subkey-cache.coffee
+++ src/subkey-cache.coffee
@@ -1,5 +1,6 @@
 SecondaryCache        = require("secondary-cache")
 minimatch             = require('minimatch')
+inherits              = require("abstract-object/lib/util/inherits")

 module.exports = class SubkeyCache
   inherits SubkeyCache, SecondaryCache


Support instance overlap pragmas

Support instance overlap pragmas

--- lib/suggestion-builder.coffee
+++ lib/suggestion-builder.coffee
@@ -7,11 +7,19 @@
   sourceScope: ['source.haskell']
   moduleScope: ['meta.import.haskell', 'support.other.module.haskell']
   preprocessorScope: ['meta.preprocessor.haskell']
+  instancePreprocessorScope: ['meta.declaration.instance.haskell', 'meta.preprocessor.haskell']
   exportsScope: ['meta.import.haskell', 'meta.declaration.exports.haskell']

   pragmaWords: [
     'LANGUAGE', 'OPTIONS_GHC', 'INCLUDE', 'WARNING', 'DEPRECATED', 'INLINE',
     'NOINLINE', 'ANN', 'LINE', 'RULES', 'SPECIALIZE', 'UNPACK', 'SOURCE'
+  ]
+
+  instancePragmaWords: [
+    'INCOHERENT'
+    'OVERLAPPABLE'
+    'OVERLAPPING'
+    'OVERLAPS'
   ]

   constructor: (@options, @backend) ->
@@ -65,8 +73,8 @@
     @processSuggestions @backend.getCompletionsForModule, (s, prefix) =>
       @buildSimpleSuggestion 'import', s, prefix

-  preprocessorSuggestions: =>
-    kwrx = new RegExp "\\b(#{@pragmaWords.join('|')})\\b"
+  preprocessorSuggestions: (pragmaList) =>
+    kwrx = new RegExp "\\b(#{pragmaList.join('|')})\\b"
     kw = @lineSearch kwrx
     label = ''
     rx = undefined
@@ -80,7 +88,7 @@
         f = @backend.getCompletionsForLanguagePragmas
       when not kw
         label = 'Pragma'
-        f = (b, p) => Promise.resolve(filter @pragmaWords, p)
+        f = (b, p) -> Promise.resolve(filter pragmaList, p)
       else
         return []

@@ -88,14 +96,16 @@
       @buildSimpleSuggestion 'keyword', s, prefix, label

   getSuggestions: =>
-    if @isIn(@typeScope)
+    if @isIn(@instancePreprocessorScope)
+      @preprocessorSuggestions(@instancePragmaWords)
+    else if @isIn(@typeScope)
       @symbolSuggestions @backend.getCompletionsForType
     else if @isIn(@moduleScope)
       @moduleSuggestions()
     else if @isIn(@exportsScope)
       @symbolSuggestions @backend.getCompletionsForSymbolInModule
     else if @isIn(@preprocessorScope)
-      @preprocessorSuggestions()
+      @preprocessorSuggestions(@pragmaWords)
     #should be last as least sepcialized
     else if @isIn(@sourceScope)
       if @getPrefix().startsWith '_'


mangle the variable names so that they use less memory

--- src/tasks/minify.coffee
+++ src/tasks/minify.coffee
@@ -2,7 +2,8 @@

 module.exports = ->
   uglify
-    mangle: false,
+    mangle:
+      excludes: []
     compress:
       sequences     : true,
       properties    : true,


replace some occurrences of 'fleld_hints' by 'selector'

--- src/mkts-table.coffee
+++ src/mkts-table.coffee
@@ -193,7 +193,7 @@
   unless value in [ 'top', 'bottom', 'center', 'spread', ]
     throw new Error "(MKTS/TABLE µ1876) expected one of 'top', 'bottom', 'center', 'spread' for mkts-table/fieldalignvertical, got #{rpr value}"
   #.........................................................................................................
-  for [ fail, field_designation, ] from @_walk_fails_and_field_designations_from_hints me, fieldhints
+  for [ fail, field_designation, ] from @_walk_fails_and_field_designations_from_selector me, fieldhints
     ### TAINT ad-hoc fail message production, use method ###
     if fail? then _record me, "#{fail} (#{jr {field_designation}})"
     else          me.valigns[ field_designation ] = value
@@ -209,7 +209,7 @@
   unless value in [ 'left', 'right', 'center', 'justified', ]
     throw new Error "(MKTS/TABLE µ1876) expected one of 'left', 'right', 'center', 'justified' for mkts-table/fieldalignhorizontal, got #{rpr value}"
   #.........................................................................................................
-  for [ fail, field_designation, ] from @_walk_fails_and_field_designations_from_hints me, fieldhints
+  for [ fail, field_designation, ] from @_walk_fails_and_field_designations_from_selector me, fieldhints
     ### TAINT ad-hoc fail message production, use method ###
     if fail? then _record me, "#{fail} (#{jr {field_designation}})"
     else          me.haligns[ field_designation ] = value
@@ -291,7 +291,7 @@
   edges       = [ 'top', 'left', 'bottom', 'right', ] if '*' in edges
   fieldnames  = []
   #.........................................................................................................
-  for [ fail, field_designation, ] from @_walk_fails_and_field_designations_from_hints me, fieldhints
+  for [ fail, field_designation, ] from @_walk_fails_and_field_designations_from_selector me, fieldhints
     ### TAINT ad-hoc fail message production, use method ###
     if fail? then _record me, "#{fail} (#{jr {field_designation}})"
     else          fieldnames.push field_designation
@@ -319,7 +319,7 @@
   edges       = [ 'top', 'left', 'bottom', 'right', ] if '*' in edges
   fieldnames  = []
   #.........................................................................................................
-  for [ fail, fieldname, ] from @_walk_fails_and_field_designations_from_hints me, selector
+  for [ fail, fieldname, ] from @_walk_fails_and_field_designations_from_selector me, selector
     ### TAINT ad-hoc fail message production, use method ###
     if fail? then _record me, "#{fail} (#{jr {fieldname}})"
     else          fieldnames.push fieldname
@@ -682,12 +682,12 @@
 #===========================================================================================================
 # ITERATORS
 #-----------------------------------------------------------------------------------------------------------
-@_walk_fails_and_field_designations_from_hints = ( me, fieldhints ) ->
+@_walk_fails_and_field_designations_from_selector = ( me, selector ) ->
   ### TAINT this will have to be changed to allow for named fields ###
   count                   = 0
   seen_field_designations = new Set()
   #.........................................................................................................
-  for cell from IG.GRID.walk_cells_from_selector me.grid, fieldhints
+  for cell from IG.GRID.walk_cells_from_selector me.grid, selector
     continue unless ( field_designations = me.cellfields[ cell.cellkey ] )?
     for field_designation in field_designations
       continue if seen_field_designations.has field_designation
@@ -696,7 +696,7 @@
       yield [ null, field_designation, ]
   #.........................................................................................................
   if count is 0
-    yield [ ( _fail me, 'µ5131', "field hint #{rpr fieldhints} do not match any field" ), null ]
+    yield [ ( _fail me, 'µ5131', "selector #{rpr selector} does not match any field" ), null ]
   #.........................................................................................................
   yield return

@@ -731,7 +731,7 @@
   designations, and later occurrences of a given field will replace earlier appearances. ###
   R = {}
   for [ fieldhints, stuff..., ] in fieldhints_and_stuff
-    for [ fail, field_designation, ] from @_walk_fails_and_field_designations_from_hints me, fieldhints
+    for [ fail, field_designation, ] from @_walk_fails_and_field_designations_from_selector me, fieldhints
       if fail? then _record me, fail
       else          R[ field_designation ]  = stuff
   yield [ field_designation, stuff..., ] for field_designation, stuff of R


fixing sanitize

fixing sanitize


Renamed action type ajax to http

Renamed action type ajax to http

--- lib/blogsiple.coffee
+++ lib/blogsiple.coffee
@@ -68,9 +68,9 @@
   blog_resource.map 'get', 'workflow', (req, res) ->
     results = []
     if req.post.published.toString() == 'true'
-      results.push {name: 'unpublish', label: 'Unpublish', action: {type: 'ajax', ajax: {type: 'PUT'}, url: "/#{req.post.id}/unpublish"}, type: 'button'}
+      results.push {name: 'unpublish', label: 'Unpublish', action: {type: 'http', http: {type: 'PUT'}, url: "/#{req.post.id}/unpublish"}, type: 'button'}
     else
-      results.push {name: 'publish', label: 'Publish', action: {type: 'ajax', ajax: {type: 'PUT'}, url: "/#{req.post.id}/publish"}, type: 'button'}
+      results.push {name: 'publish', label: 'Publish', action: {type: 'http', http: {type: 'PUT'}, url: "/#{req.post.id}/publish"}, type: 'button'}
     results.push {name: 'destroy', label: 'Destroy', action: {type: 'backbone_destroy'}, type: 'button'}

     res.json results


Follow mixin

Follow mixin

--- test/follow.coffee
+++ test/follow.coffee
@@ -30,4 +30,6 @@

   ok fastEnemy.I.velocity.equal(Point(0, -5)), 'fastEnemy should head toward player with velocity Point(0, 5)'

+
+
 module()


Object: Return no before emitting if object is destroyed.

--- src/lib/core/Object.coffee
+++ src/lib/core/Object.coffee
@@ -143,6 +143,8 @@
     @param {*=} data Data which will passed to listeners
   ###
   emit: (eventName, data) ->
+    return no if @isDestroyed()
+
     @dispatchEvent { type: eventName, data }


added method `getLanguage`

added method `getLanguage`

this is needed to complete the reactivity added to @language

--- t9n.coffee
+++ t9n.coffee
@@ -51,6 +51,10 @@
       for key, value of map
         @registerMap(language, prefix + key, true, value)

+  @getLanguage: () ->
+    @depLanguage.depend()
+    return @language
+
   @setLanguage: (language) ->
     if(!@maps[language])
       return;


Don't set an explicit width for LinesComponent

--- src/lines-component.coffee
+++ src/lines-component.coffee
@@ -64,10 +64,6 @@
         @domNode.appendChild(@placeholderTextDiv)
       @oldState.placeholderText = @newState.placeholderText

-    if @newState.width isnt @oldState.width
-      @domNode.style.width = @newState.width + 'px'
-      @oldState.width = @newState.width
-
     @cursorsComponent.updateSync(state)

   buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @grammars})


Clear expanded state when entry is removed

--- lib/directory.coffee
+++ lib/directory.coffee
@@ -119,6 +119,7 @@
     for name, entry of removedEntries
       entry.destroy()
       delete @entries[name]
+      delete @expandedEntries[name]
       @emit 'entry-removed', entry

     for [entry, index] in newEntries


Make 'following' and 'registered' observables per-user.

--- js/inner/user.coffee
+++ js/inner/user.coffee
@@ -26,6 +26,8 @@
     repo_filter: ""
     email: null
     id: null
+    following: false
+    registered: false

   constructor: (json) ->
     super json,


fix traits tracking

fix traits tracking


added test to ensure algorithm covers whole area defined by recorderjob

--- test/SpherePictureFactory.test.coffee
+++ test/SpherePictureFactory.test.coffee
@@ -75,7 +75,7 @@
       # first picture and last picture
       pictures[0].width.should.be.equal(2*Math.PI)

-    it 'should have overlapping of 50%', ->
+    it 'should have overlapping of 50% and cover area defined by recorderjob', ->
       # fullspehre recorderjob
       rj =
         tiltMin: 0
@@ -85,15 +85,61 @@
         pictureWidth: Math.PI/4
         pictureHeight: Math.PI/10
       pictures = new SpherePictureFactory(rj).pictures
-      lastPic = undefined
+      prevPic = tiltMin = tiltMax = panMin = panMax = undefined
+      # check for vertical bounds for all rows
+      checkTilt = ->
+        tiltMin.should.be.at.most(rj.tiltMin)
+        tiltMax.should.be.at.least(rj.tiltMax)
+      # check horizontal bounds for each row
+      checkPan = ->
+        panMin.should.be.at.most(rj.panMin)
+        # because we divide width of picture to estimate panMax
+        # it is possible that panMax is smalelr than recorderjob defined
+        # in this case its sufficient to test with closeTo operator
+        if panMax < rj.panMax
+          panMax.should.be.closeTo(rj.panMax, 0.00000000000001)
+        else
+          panMax.should.be.at.least(rj.panMax)
+
       for pic in pictures
         # same row
-        if lastPic?.y is pic.y
+        if prevPic and pic.y is prevPic.y
+          # ensure pic is in same row
+          pic.tilt.should.be.equal(prevPic.tilt)
           # should at least cover 50% of prevoius picture horizontally
-          pic.pan.should.be.at.most(lastPic.pan+pic.width/2)
+          pic.pan.should.be.at.most(prevPic.pan+prevPic.width/2)
         # next row
-        else if lastPic?.y < pic.y
+        else if prevPic and pic.y > prevPic.y
+          # ensure no row is leaved out
+          pic.y.should.be.equal(prevPic.y+1)
           # should at least cover 50% of prevoius picture vertically
-          pic.tilt.should.be.at.most(lastPic.tilt+pic.height/2)
+          pic.tilt.should.be.at.most(prevPic.tilt+prevPic.height/2)
+          # check pan and reset
+          checkPan()
+          panMin = panMax = undefined
+        #
+        # update horizontal bounds
+        if typeof panMin is 'number'
+          panMin = Math.min(panMin, pic.pan-pic.width/2)
+        else
+          panMin = pic.pan-pic.width/2
+        if typeof panMax is 'number'
+          panMax = Math.max(panMax, pic.pan+pic.width/2)
+        else
+          panMax = pic.pan+pic.width/2
+        #
+        # update vertical bounds
+        if typeof tiltMin is 'number'
+          tiltMin = Math.min(tiltMin, pic.tilt-pic.height/2)
+        else
+          tiltMin = pic.tilt-pic.height/2
+        if typeof tiltMax is 'number'
+          tiltMax = Math.max(tiltMax, pic.tilt+pic.height/2)
+        else
+          tiltMax = pic.tilt+pic.height/2
+        #
         # remember curr pic for next iteration
-        lastPic = pic+        prevPic = pic
+      # last row
+      checkPan()
+      checkTilt()


update short-cut

update short-cut

--- keymaps/csscomb.cson
+++ keymaps/csscomb.cson
@@ -1,2 +1,2 @@
 'atom-workspace':
-  'ctrl-s': 'csscomb:execute'
+  'ctrl-C': 'csscomb:execute'


Fix an issue where { off: 2 } erroneously becomes { off_: 2 }.

--- lib/js2coffee.coffee
+++ lib/js2coffee.coffee
@@ -141,10 +141,10 @@

     c.toString()

-  # `identifier`
-  # Any object identifier like a variable name.
-
-  'identifier': ->
+  # `property_identifier`
+  # A key in an object literal.
+
+  'property_identifier': ->
     str = @value.toString()

     # **Caveat:**
@@ -152,9 +152,15 @@
     # quoted if need be.*

     if str.match(/^([_\$a-z][a-z0-9_]*)$/i) or str.match(/^[0-9]+/i)
-      unreserve str
+      str
     else
       strEscape str
+
+  # `identifier`
+  # Any object identifier like a variable name.
+
+  'identifier': ->
+    unreserve @value.toString()

   'number': ->
     "#{@value}"
@@ -488,7 +494,7 @@
   # left is a `identifier`, right can be anything.

   'property_init': ->
-    "#{build @left()}: #{build @right()}"
+    "#{re 'property_identifier', @left()}: #{build @right()}"

   # `object_init`
   # An object initializer.


:lipstick: newline at eof

:lipstick: newline at eof

--- src/extensions/snippets/index.coffee
+++ src/extensions/snippets/index.coffee
@@ -1 +1 @@
-module.exports = require 'extensions/snippets/snippets.coffee'+module.exports = require 'extensions/snippets/snippets.coffee'


Use notifications in the "layers" controller

--- testApp/app/controllers/layers.coffee
+++ testApp/app/controllers/layers.coffee
@@ -26,7 +26,7 @@

   @testPop: ->
     popped = ->
-      alert "popped"
+      notification "popped"

     steroids.layers.pop()

@@ -137,7 +137,7 @@
   @testInvalidLayerEvents: ->
     try
       steroids.layers.on 'invalidEventName', (event) ->
-        alert "should never be invoked"
+        notification "should never be invoked"
     catch error
       alert "steroids.layers.on callend with an invalid event name! error: #{error}"

@@ -159,7 +159,7 @@

     @willChangeHandlers.push eventHandler

-    alert "event listener added"
+    notification "event listener added"

   @testdidchangeEvent: ->
     eventHandler = steroids.layers.on 'didchange', (event) ->
@@ -175,7 +175,7 @@

     @didChangeHandlers.push eventHandler

-    alert "event listener added"
+    notification "event listener added"

   @testRemoveAllEventHandlers: ->
     @didChangeHandlers.forEach (handlerId) -> steroids.layers.off 'didchange', handlerId
@@ -185,15 +185,15 @@
     @willChangeHandlers = []
     @didChangeHandlers = []

-    alert "event handlers removed"
+    notification "event handlers removed"

   @testRemovedidchangeEvents: ->
     steroids.layers.off 'didchange'

-    alert "didchange events handlers removed"
+    notification "didchange events handlers removed"

   @testRemovewillchangeEvents: ->
     steroids.layers.off 'willchange'

-    alert "willchange events handlers removed"
+    notification "willchange events handlers removed"


Refactor and DRY up operators

Refactor and DRY up operators

--- lib/operators.coffee
+++ lib/operators.coffee
@@ -4,21 +4,53 @@
   constructor: (@message) ->
     @name = "Operator Error"

-#
-# It deletes everything selected by the following motion.
-#
-class Delete
+class Operator
+  vimState: null
   motion: null
   complete: null
-  vimState: null
+  selectOptions: null
+
+  # selectOptions - The options object to pass through to the motion when
+  #                 selecting.
+  constructor: (@editor, @vimState, {@selectOptions}={}) ->
+    @complete = false
+
+  # Public: Determines when the command can be executed.
+  #
+  # Returns true if ready to execute and false otherwise.
+  isComplete: -> @complete
+
+  # Public: Marks this as ready to execute and saves the motion.
+  #
+  # motion - The motion used to select what to operate on.
+  #
+  # Returns nothing.
+  compose: (motion) ->
+    if not motion.select
+      throw new OperatorError("Must compose with a motion")
+
+    @motion = motion
+    @complete = true
+
+
+  # Protected: Wraps the function within an single undo step.
+  #
+  # fn - The function to wrap.
+  #
+  # Returns nothing.
+  undoTransaction: (fn) ->
+    @editor.getBuffer().transact(fn)
+#
+# It deletes everything selected by the following motion.
+#
+class Delete extends Operator
   allowEOL: null
-  selectOptions: null
-
-  constructor: (@editor, @vimState, {@motion, @allowEOL, @selectOptions}={}) ->
+
+  # allowEOL - Determines whether the cursor should be allowed to rest on the
+  #            end of line character or not.
+  constructor: (@editor, @vimState, {@allowEOL, @selectOptions}={}) ->
     @complete = false

-  isComplete: -> @complete
-
   # Public: Deletes the text selected by the given motion.
   #
   # count - The number of times to execute.
@@ -26,9 +58,8 @@
   # Returns nothing.
   execute: (count=1) ->
     cursor = @editor.getCursor()
-    buffer = @editor.getBuffer()
-
-    buffer.transact =>
+
+    @undoTransaction =>
       _.times count, =>
         if _.last(@motion.select(1, @selectOptions))
           @editor.getSelection().delete()
@@ -38,69 +69,27 @@
       if @motion.isLinewise?()
         @editor.setCursorScreenPosition([cursor.getScreenRow(), 0])

-  # Public: Marks this as complete and saves the motion.
-  #
-  # motion - The motion used to select what to delete.
-  #
-  # Returns nothing.
-  compose: (motion) ->
-    if not motion.select
-      throw new OperatorError("Delete must compose with a motion")
-
-    @motion = motion
-    @complete = true
-
 #
 # It changes everything selected by the following motion.
 #
-class Change
-  motion: null
-  complete: null
-  vimState: null
-
-  constructor: (@editor, @vimState) ->
-    @complete = false
-
-  isComplete: -> @complete
-
+class Change extends Operator
   # Public: Changes the text selected by the given motion.
   #
   # count - The number of times to execute.
   #
   # Returns nothing.
   execute: (count=1) ->
-    operator = new Delete(@editor, @vimState,
-      motion: @motion, allowEOL: true, selectOptions: {excludeWhitespace: true})
+    operator = new Delete(@editor, @vimState, allowEOL: true, selectOptions: {excludeWhitespace: true})
+    operator.compose(@motion)
     operator.execute(count)

     @vimState.activateInsertMode()

-  # Public: Marks this as complete and saves the motion.
-  #
-  # motion - The motion used to select what to change.
-  #
-  # Returns nothing.
-  compose: (motion) ->
-    if not motion.select
-      throw new OperatorError("Change must compose with a motion")
-
-    @motion = motion
-    @complete = true
-
 #
 # It copies everything selected by the following motion.
 #
-class Yank
-  motion: null
-  complete: null
-  register: null
-  vimState: null
-
-  constructor: (@editor, @vimState) ->
-    @complete = false
-    @register ?= '"'
-
-  isComplete: -> @complete
+class Yank extends Operator
+  register: '"'

   # Public: Copies the text selected by the given motion.
   #
@@ -110,8 +99,8 @@
   execute: (count=1) ->
     text = ""
     type = if @motion.isLinewise then 'linewise' else 'character'
-
     originalPosition = @editor.getCursorScreenPosition()
+
     _.times count, =>
       if _.last(@motion.select())
         text += @editor.getSelection().getText()
@@ -123,107 +112,57 @@
     else
       @editor.clearSelections()

-  # Public: Marks this as complete and saves the motion.
-  #
-  # motion - The motion used to select what to copy.
-  #
-  # Returns nothing.
-  compose: (motion) ->
-    if not motion.select
-      throw new OperatorError("Yank must compose with a motion")
-
-    @motion = motion
-    @complete = true
-
 #
 # It indents everything selected by the following motion.
 #
-class Indent
-  motion: null
-  complete: null
-
-  constructor: (@editor) ->
-    @complete = false
-
-  isComplete: -> @complete
-
+class Indent extends Operator
   # Public: Indents the text selected by the given motion.
   #
   # count - The number of times to execute.
   #
   # Returns nothing.
   execute: (count=1) ->
+    @indent(count)
+
+  # Protected: Indents or outdents the text selected by the given motion.
+  #
+  # count  - The number of times to execute.
+  # direction - Either 'indent' or 'outdent'
+  #
+  # Returns nothing.
+  indent: (count, direction='indent') ->
     row = @editor.getCursorScreenRow()

     @motion.select(count)
-    @editor.indentSelectedRows()
+    if direction == 'indent'
+      @editor.indentSelectedRows()
+    else if direction == 'outdent'
+      @editor.outdentSelectedRows()

     @editor.setCursorScreenPosition([row, 0])
     @editor.moveCursorToFirstCharacterOfLine()

-  # Public: Marks this as complete and saves the motion.
-  #
-  # motion - The motion used to select what to indent.
-  #
-  # Returns nothing.
-  compose: (motion) ->
-    if not motion.select
-      throw new OperatorError("Indent must compose with a motion")
-
-    @motion = motion
-    @complete = true
-
 #
 # It outdents everything selected by the following motion.
 #
-class Outdent
-  motion: null
-  complete: null
-
-  constructor: (@editor) ->
-    @complete = false
-
-  isComplete: -> @complete
-
-  # Public: Outdents the text selected by the given motion.
-  #
-  # count - The number of times to execute.
-  #
-  # Returns nothing.
-  execute: (count=1) ->
-    row = @editor.getCursorScreenRow()
-
-    @motion.select(count)
-    @editor.outdentSelectedRows()
-
-    @editor.setCursorScreenPosition([row, 0])
-    @editor.moveCursorToFirstCharacterOfLine()
-
-  # Public: Marks this as complete and saves the motion.
-  #
-  # motion - The motion used to select what to outdent.
-  #
-  # Returns nothing.
-  compose: (motion) ->
-    if not motion.select
-      throw new OperatorError("Outdent must compose with a motion")
-
-    @motion = motion
-    @complete = true
+class Outdent extends Indent
+  # Public: Indents the text selected by the given motion.
+  #
+  # count - The number of times to execute.
+  #
+  # Returns nothing.
+  execute: (count=1) ->
+    @indent(count, 'outdent')

 #
 # It pastes everything contained within the specifed register
 #
-class Put
-  motion: null
-  direction: null
-  register: null
-
-  constructor: (@editor, @vimState, {@location}={}) ->
-    @direction ?= 'after'
-    @register ?= '"'
-
-  isComplete: -> true
+class Put extends Operator
+  direction: 'after'
+  register: '"'
+  location: null
+
+  constructor: (@editor, @vimState, {@location, @selectOptions}={}) -> @complete = true

   # Public: Pastes the text in the given register.
   #
@@ -233,9 +172,8 @@
   execute: (count=1) ->
     {text, type} = @vimState.getRegister(@register) || {}
     return unless text
-    buffer = @editor.getBuffer()
-
-    buffer.transact =>
+
+    @undoTransaction =>
       _.times count, =>
         if type == 'linewise' and @location == 'after'
           @editor.moveCursorDown()
@@ -249,19 +187,11 @@
           @editor.moveCursorUp()
           @editor.moveCursorToFirstCharacterOfLine()

-  # Public: Not implemented.
-  #
-  # Returns nothing.
-  compose: (register) ->
-    throw new OperatorError("Not Implemented")
-
 #
 # It combines the current line with the following line.
 #
-class Join
-  constructor: (@editor) ->
-
-  isComplete: -> true
+class Join extends Operator
+  constructor: (@editor, @vimState, {@selectOptions}={}) -> @complete = true

   # Public: Combines the current with the following lines
   #
@@ -269,16 +199,8 @@
   #
   # Returns nothing.
   execute: (count=1) ->
-    buffer = @editor.getBuffer()
-
-    buffer.transact =>
+    @undoTransaction =>
       _.times count, =>
         @editor.joinLine()

-  # Public: Not implemented.
-  #
-  # Returns nothing.
-  compose: (register) ->
-    throw new OperatorError("Not Implemented")
-
 module.exports = { OperatorError, Delete, Yank, Put, Join, Indent, Outdent, Change }


:lipstick:

:lipstick:

--- lib/operators.coffee
+++ lib/operators.coffee
@@ -272,17 +272,17 @@
     {text, type} = @vimState.register.get(@getRegisterName()) ? {}
     return unless text

-    textToInsert = _.times(@getCount(1), -> text).join('')
+    text = _.multiplyString(text, @getCount(1))

     selection = @editor.getSelectedBufferRange()
     if selection.isEmpty()
       # Clean up some corner cases on the last line of the file
       if type is 'linewise'
-        textToInsert = textToInsert.replace(/\n$/, '')
+        text = text.replace(/\n$/, '')
         if @location is 'after' and @onLastRow()
-          textToInsert = "\n#{textToInsert}"
+          text = "\n#{text}"
         else
-          textToInsert = "#{textToInsert}\n"
+          text = "#{text}\n"

       if @location is 'after'
         if type is 'linewise'
@@ -301,7 +301,7 @@
         @editor.moveToBeginningOfLine()
         originalPosition = @editor.getCursorScreenPosition()

-    @editor.insertText(textToInsert)
+    @editor.insertText(text)

     if originalPosition?
       @editor.setCursorScreenPosition(originalPosition)


Fix while loops getting stuck

Fix while loops getting stuck

--- src/lib/script_modules/core.coffee
+++ src/lib/script_modules/core.coffee
@@ -80,7 +80,9 @@

       whileResolved = no
       condition(sequence)
+        .then -> whileResolved = yes
         .catch (e) ->
+          whileResolved = yes
           throw e unless e.message is 'sequence mismatch'
         .finally -> whileResolved = yes
       WhenJS.iterate(


Correctly update scroll position after deserializing

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -34,10 +34,7 @@

     @disposables = new CompositeDisposable
     @emitter = new Emitter
-    @focusAfterAttach = false
     @roots = []
-    @scrollLeftAfterAttach = -1
-    @scrollTopAfterAttach = -1
     @selectedPath = null
     @ignoredPatterns = []
     @useSyncFS = false
@@ -59,8 +56,16 @@
     @selectEntry(@roots[0])

     @selectEntryForPath(state.selectedPath) if state.selectedPath
-    @scrollTopAfterAttach = state.scrollTop if state.scrollTop
-    @scrollLeftAfterAttach = state.scrollLeft if state.scrollLeft
+
+    if state.scrollTop? or state.scrollLeft?
+      observer = new IntersectionObserver(=>
+        if @isVisible()
+          @element.scrollTop = state.scrollTop
+          @element.scrollLeft = state.scrollLeft
+          observer.disconnect()
+      )
+      observer.observe(@element)
+
     @element.style.width = "#{state.width}px" if state.width > 0

     @disposables.add @onEntryMoved ({initialPath, newPath}) ->
@@ -775,7 +780,7 @@
   onStylesheetsChanged: =>
     # If visible, force a redraw so the scrollbars are styled correctly based on
     # the theme
-    return if @element.offsetWidth is 0 and @element.offsetHeight is 0
+    return unless @isVisible()
     @element.style.display = 'none'
     @element.offsetWidth
     @element.style.display = ''
@@ -943,3 +948,6 @@
         # Drop event from OS
         for file in e.dataTransfer.files
           @moveEntry(file.path, newDirectoryPath)
+
+  isVisible: ->
+    @element.offsetWidth isnt 0 or @element.offsetHeight isnt 0


Use workspace to open files

Use workspace to open files

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -346,7 +346,7 @@
     if selectedEntry instanceof DirectoryView
       selectedEntry.toggleExpansion()
     else if selectedEntry instanceof FileView
-      atom.workspaceView.open(selectedEntry.getPath(), {changeFocus})
+      atom.workspace.open(selectedEntry.getPath(), {changeFocus})

   moveSelectedEntry: ->
     if @hasFocus()


:lipstick: Remove extra newline

:lipstick: Remove extra newline

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -282,7 +282,6 @@
     dialog.on 'file-created', (event, createdPath) ->
       atom.workspaceView.open(createdPath)
       false
-
     dialog.attach()

   selectedEntry: ->


Adding Logging when finish the function

Adding Logging when finish the function

--- src/observers/logger.observer.coffee
+++ src/observers/logger.observer.coffee
@@ -10,3 +10,6 @@

     observable.on "unsuccessful", ({ context: { log }, err }) ->
       log "Process unsuccessful", errorToJson(err)
+
+    observable.on "finished", ({ context: { log } }) ->
+      log "Process finished"


state; and check bad omission argument

state; and check bad omission argument

--- src/protomixin.spec.coffee
+++ src/protomixin.spec.coffee
@@ -138,27 +138,31 @@

   describe 'protomixing options', ->

+    beforeEach ->
+      @mixin = MIXINS.default_protomixin()
+
     it 'should omit mixin keys', ->
-        mixin = MIXINS.default_protomixin()
-
         class Example
-          @mixinto_proto mixin, omit: ['bar']
+        Example.mixinto_proto @mixin, omit: ['bar']

         e = new Example

         expect(e.bar).toBeUndefined()
         expect(e.baz).toBeDefined()

+    it 'should throw an error when omitting a non-existing mixin key', ->
+      expect(=>
+        class Example
+        Example.mixinto_proto @mixin, omit: ['non_mixin_key']
+      ).toThrow new MixinUtils.ArgumentError "Some omit keys aren't in mixin object: non_mixin_key"
+
     it 'should not mangle the class hierarchy when omitting keys', ->
-      mixin = MIXINS.default_protomixin()
-
       class Super
-
         bar: ->
           'bar'

       class Example extends Super
-        @mixinto_proto mixin, omit: ['bar']
+      Example.mixinto_proto @mixin, omit: ['bar']

       e = new Example

@@ -166,12 +170,10 @@
       expect(e.bar()).toBe('bar')

     it 'should not omit all mixin keys', ->
-        mixin = MIXINS.default_protomixin()
-
-        expect(->
-          class Example
-            @mixinto_proto mixin, omit: ['bar', 'baz']
-        ).toThrow new MixinUtils.ArgumentError "Found nothing to mix in!"
+      expect(=>
+        class Example
+        Example.mixinto_proto @mixin, omit: ['bar', 'baz']
+      ).toThrow new MixinUtils.ArgumentError "Found nothing to mix in!"

     it 'should support pre-mixin hooks', ->
       throw new Error 'Write me!'


Move button group under metrics note

Move button group under metrics note

--- lib/about-view.coffee
+++ lib/about-view.coffee
@@ -74,12 +74,7 @@
               @input type: 'checkbox', checked: true
               @span 'Automatically download updates'

-        @div class: 'about-actions group-start', =>
-          @div class: 'btn-group', =>
-            @button class: 'btn view-license', outlet: 'viewLicense', 'License'
-            @button class: 'btn terms-of-use', outlet: 'viewTerms', 'Terms of Use'
-
-        @p class: 'about-metrics group-item', =>
+        @p class: 'about-metrics group-start', =>
           @raw '''
               Note: To help us improve Atom, we anonymously
               track usage metrics, such as launch time, screen size, and current
@@ -87,6 +82,11 @@
               atom/metrics
               package for details and instructions to disable it.
             '''
+
+        @div class: 'about-actions group-item', =>
+          @div class: 'btn-group', =>
+            @button class: 'btn view-license', outlet: 'viewLicense', 'License'
+            @button class: 'btn terms-of-use', outlet: 'viewTerms', 'Terms of Use'

         @div class: 'about-love group-start', outlet: 'love', =>
           @span class: 'icon icon-code'


Added clipboard sync to kill-ring class

Added clipboard sync to kill-ring class

--- lib/kill-ring.coffee
+++ lib/kill-ring.coffee
@@ -20,6 +20,8 @@

   push: (texts) ->
     @buffer.push texts
+    atom.clipboard.write texts.join '\n'
+    console.log atom.clipboard.read()
     if @buffer.length > @limit then @buffer.shift()
     console.log _.last @buffer
     @sealed = false
@@ -27,11 +29,15 @@
   update: (texts,forward) ->
     concat = (t) -> if forward then t[0] + (t[1] or '') else (t[1] or '') + t[0]
     new_texts = ((concat t) for t in _.zip (_.last @buffer),texts)
-    @buffer.pop
+    @buffer.pop()
     @push new_texts

   top: ->
-    _.last @buffer
+    _.last @list()

   list: ->
+    last = _.last @buffer
+    last ?= [""]
+    if atom.clipboard.md5(last.join '\n') != atom.clipboard.signatureForMetadata
+      @push [atom.clipboard.read()]
     @buffer


Math.floor() percentage

Math.floor() percentage

--- lib/actions/device.coffee
+++ lib/actions/device.coffee
@@ -184,7 +184,7 @@
 				drive.writeImage params.device, params.image,
 					progress: not options.quiet
 					onProgress: (status) ->
-						console.log("#{bar.format(status.percentage / 100)} #{status.percentage}% eta #{status.eta}s")
+						console.log("#{bar.format(status.percentage / 100)} #{Math.floor(status.percentage)}% eta #{status.eta}s")
 				, callback

 		], (error) ->


Fixing multiblueprint data structure

Fixing multiblueprint data structure

--- src/reporters/apiary-reporter.coffee
+++ src/reporters/apiary-reporter.coffee
@@ -50,8 +50,8 @@

       # transform blueprints data to array
       blueprints = []
-      for blueprint of blueprintsData
-        blueprints.push blueprint
+      for blueprintPath, blueprintData of blueprintsData
+        blueprints.push blueprintData

       data =
         blueprints: blueprints


Case insensitive header lookup

Case insensitive header lookup

--- lib/performer.coffee
+++ lib/performer.coffee
@@ -15,6 +15,12 @@
         else
           handler
   handlers
+
+getHeaderName = (headers, searchName) ->
+  searchName = searchName.toLowerCase()
+  for name, value of headers
+    if name.toLowerCase() == searchName
+      return name

 exports.create = (options) ->
   # Short circuit if no plugins were provided
@@ -46,7 +52,7 @@
         headers = reasonPhrase
       headers or= res.headers

-      contentType = headers?['Content-Type']?.split(';')[0]
+      contentType = headers?[getHeaderName headers, 'content-type']?.split(';')[0]

       if handlers[contentType]
         # Divert the content stream for further processing
@@ -76,7 +82,7 @@
             res.write = write
             res.end = end

-            headers['Content-Length'] = content.length
+            headers[getHeaderName headers, 'content-length'] = content.length

             # Send everything on the way
             res.writeHead statusCode, headers


unclamp price value

unclamp price value

--- resources/assets/coffee/_classes/store-support-osu.coffee
+++ resources/assets/coffee/_classes/store-support-osu.coffee
@@ -64,14 +64,15 @@
       value: @MIN_VALUE * @RESOLUTION,
       min: @MIN_VALUE * @RESOLUTION,
       max: @MAX_VALUE * @RESOLUTION,
-      step: 32,
+      step: 1,
+      animate: 'fast',
       slide: (event, ui) =>
         @onSliderValueChanged event, ui
       change: (event, ui) =>
         @onSliderValueChanged event, ui
     }
     .slider('pips', {
-      step: 1,
+      step: 32,
       rest: "label",
       labels: ['1', '2', '4', '6', '8', '10', '12', '', '16', '', '20', '', '24']
     })
@@ -109,13 +110,13 @@
     cost = Math.floor(position / @RESOLUTION)
     values = switch
       when cost < @MIN_VALUE then { price: @MIN_VALUE, duration: 0 }
-      when cost < 8 then { price: 4, duration: 1 }
-      when cost < 12 then { price: 8, duration: 2 }
-      when cost < 16 then { price: 12, duration: 4 }
-      when cost < 20 then { price: 16, duration: 6 }
-      when cost < 22 then { price: 20, duration: 8 }
-      when cost < 24 then { price: 22, duration: 9 }
-      when cost < 26 then { price: 24, duration: 10 }
+      when cost < 8 then { price: cost, duration: 1 }
+      when cost < 12 then { price: cost, duration: 2 }
+      when cost < 16 then { price: cost, duration: 4 }
+      when cost < 20 then { price: cost, duration: 6 }
+      when cost < 22 then { price: cost, duration: 8 }
+      when cost < 24 then { price: cost, duration: 9 }
+      when cost < 26 then { price: cost, duration: 10 }
       else
         months = 0
         while ((months + 1) / 12 * 26 <= cost)


Support for compiler "undefined" errors

Support for compiler "undefined" errors

--- lib/spark-ide.coffee
+++ lib/spark-ide.coffee
@@ -1,6 +1,7 @@
 fs = null
 settings = null
 utilities = null
+path = null

 module.exports =
   SettingsHelper: null
@@ -177,6 +178,8 @@
         @selectPortView.show()

   parseErrors: (raw) ->
+    path ?= require 'path'
+
     lines = raw.split "\n"
     errors = []
     for line in lines
@@ -189,6 +192,17 @@
           type: result[4],
           message: result[5]
         }
+      else
+        result = line.match /^([^:]+):(\d+):\s(.*)$/
+        if result
+          # This is probably "undefined" error
+          errors.push {
+            file: path.basename(result[1]),
+            row: result[2],
+            col: 0,
+            type: 'error',
+            message: result[3]
+          }
     errors

   compileCloud: ->
@@ -236,6 +250,7 @@
           @downloadBinaryPromise = null
       else
         # Handle errors
+        console.error e
         @SettingsHelper.set 'compile-status', {errors: @parseErrors(e.errors[0])}
         atom.workspaceView.trigger 'spark-ide:update-compile-status'
         atom.workspaceView.trigger 'spark-ide:show-compile-errors'


add error messages when the compiler crashes/can't be found

for #42

--- lib/idris-ide-mode.coffee
+++ lib/idris-ide-mode.coffee
@@ -14,6 +14,9 @@
     @process = spawn pathToIdris, ['--ide-mode']
     @process.on 'exit', @stopped
     @process.on 'error', @stopped
+    @process.on 'close', @stopped
+    @process.on 'disconnect', @stopped
+
     @process.stdout.setEncoding('utf8').on 'data', @stdout

   send: (cmd) ->
@@ -24,8 +27,20 @@
     @process?.kill()
     @stopped()

-  stopped: =>
-    Logger.logText 'Exited'
+  stopped: (error, signal) =>
+    e =
+      if error
+        if error.code == 'ENOENT'
+          short: "Couldn't find idris executable"
+          long: "Couldn't find idris executable at \"#{error.path}\""
+        else
+          short: error.code
+          long: error.message
+      else
+        short: signal
+        long: "Process exited with signal #{signal}"
+
+    atom.notifications.addError e.short, detail: e.long
     @process = null

   running: ->


Showing errors in other files kinda works...

So I guess this fixes #5.

--- lib/linter-crystal.coffee
+++ lib/linter-crystal.coffee
@@ -31,7 +31,7 @@
     Path = require 'path'
     XRegExp = require('xregexp').XRegExp

-    regex = XRegExp('.+:(?<line>\\d+): (?<message>.+)')
+    regex = XRegExp('Error in (?<file>.+):(?<line>\\d+): (?<message>.+)')

     return new Promise (Resolve) ->
       FilePath = TextEditor.getPath()
@@ -40,21 +40,29 @@
       @cmd = atom.config.get 'linter-crystal.command'
       @cmd = "#{@cmd} --no-build" unless atom.config.get 'linter-crystal.buildOutput'
       @cmd = "#{@cmd} --no-color" unless atom.config.get 'linter-crystal.colorOutput'
-      Process = CP.exec("#{@cmd} #{TextEditor.getTitle()}",
+      Process = CP.exec("#{@cmd} #{Path.basename(FilePath)}",
         {cwd: Path.dirname(FilePath)})
       Process.stdout.on 'data', (data) -> Data.push(data.toString())
       Process.on 'close', ->
         Content = []
         for line in Data
           Content.push XRegExp.exec(line, regex)
-          console.log line if atom.inDevMode()
+          console.log line# if atom.inDevMode()
         ToReturn = []
         Content.forEach (regex) ->
           if regex
-            ToReturn.push(
-              Type: 'Error',
-              Message: regex.message,
-              File: FilePath
-              Position: [[regex.line, 0], [regex.line, TextBuffer.lineLengthForRow(regex.line)]]
-            )
+            if Path.basename(FilePath) == Path.normalize(regex.file)
+              ToReturn.push(
+                Type: 'Error',
+                Message: regex.message,
+                File: FilePath
+                Position: [[regex.line, 0], [regex.line, TextBuffer.lineLengthForRow(regex.line)]]
+              )
+            else
+              ToReturn.push(
+                Type: 'Error',
+                Message: regex.message,
+                File: Path.normalize(regex.file)
+                Position: [[regex.line, 0], [regex.line, 0]]
+              )
         Resolve(ToReturn)


Minor docs fixup.

Minor docs fixup.

--- src/state-play.coffee
+++ src/state-play.coffee
@@ -201,7 +201,7 @@
       @endingPoint = @platforms.ledges[-1...][0].createMidpoint @platforms
       @startingPoint = new Point playerW, (@world.height - playerH + playerYOffset)
       # Use for debugging ending.
-      # @startingPoint = @platforms.ledges[-2...-1][0].createMidpoint @platforms
+      #@startingPoint = @platforms.ledges[-2...-1][0].createMidpoint @platforms

     _addPlayer: ->
       origin = @startingPoint


Add quitting on Q to PlayState by doing custom pausing.

Phaser.State doesn't seem to have a way to pause selectively.

--- src/state-play.coffee
+++ src/state-play.coffee
@@ -15,7 +15,7 @@

   'use strict'

-  {Timer} = Phaser
+  {Key, Keyboard, Timer} = Phaser

   MateLastFrame = 14

@@ -43,6 +43,11 @@
       @physics.arcade.gravity.y = 500

       @cursors = @input.keyboard.createCursorKeys()
+      # Quit on Q
+      @quitKey = @input.keyboard.addKey Keyboard.Q
+      @quitKey.onDown.add @quit, @
+      @input.keyboard.removeKeyCapture Keyboard.Q
+      # Quit on click at end.
       @onHit = @input[if @game.device.touch then 'onTap' else 'onUp']
       @onHit.add @quit, @

@@ -98,6 +103,7 @@

       @game.onBlur.remove @onBlur, @
       @game.onFocus.remove @onFocus, @
+      @quitKey.onDown.remove @quit, @
       @onHit.remove @quit, @

       @gui?.destroy()
@@ -147,7 +153,8 @@
         ['Arrow keys to move', { fontSize: 16 }]
         ['Press 0, -, + for volume', { fontSize: 16 }]
         ['Press Q to quit', { fontSize: 16 }]
-      ], @game
+      ], @game,
+        pauseHandler: (paused) => @player.control = not paused

     _addMate: ->
       {x, y} = @endingPoint


Add more tyku

Add more tyku

--- views/components/ship/parts/panebody.cjsx
+++ views/components/ship/parts/panebody.cjsx
@@ -100,6 +100,8 @@
       # Alv
       if item.api_type[3] == 6 && item.api_alv > 0 && item.api_alv <= 7
         alvTyku += [0, 1, 4, 6, 11, 16, 17, 25][item.api_alv]
+      else if item.api_type[3] in [7, 8] && item.api_alv == 7
+        alvTyku += 3
       else if item.api_type[3] == 10 && item.api_type[2] == 11 && item.api_alv == 7
         alvTyku += 9
   totalTyku = basicTyku + alvTyku


rewrote setShader method

rewrote setShader method

--- oriDomi.coffee
+++ oriDomi.coffee
@@ -100,6 +100,7 @@
   hPanels: 5
   perspective: 1000
   shading: true
+  hardShading: false
   speed: .6
   oriDomiClass: 'oriDomi'
   silent: false
@@ -382,23 +383,34 @@
       [angle, anchor, options]

-
-  _setShader: (i, anchor, deg) ->
-    opacity = Math.abs(deg) / 90 * @shadingIntensity * .3
+  _setShader: (i, anchor, angle) ->
+    opacity = Math.abs(angle) / 90 * @shadingIntensity * .3
+
+    if @settings.hardShading
+      angle = Math.abs angle
+
+    switch anchor
+      when 'left', 'top'
+        if angle < 0
+          a = opacity
+          b = 0
+        else
+          a = 0
+          b = opacity
+      when 'right', 'bottom'
+        if angle < 0
+          a = 0
+          b = opacity
+        else
+          a = opacity
+          b = 0
+
     if anchor is 'left' or anchor is 'right'
-      if deg < 0
-        @shaders[anchor].right[i].style.opacity = 0
-        @shaders[anchor].left[i].style.opacity = opacity
-      else
-        @shaders[anchor].left[i].style.opacity = 0
-        @shaders[anchor].right[i].style.opacity = opacity
+      @shaders[anchor].left[i].style.opacity = a
+      @shaders[anchor].right[i].style.opacity = b
     else
-      if deg < 0
-        @shaders[anchor].bottom[i].style.opacity = 0
-        @shaders[anchor].top[i].style.opacity = opacity
-      else
-        @shaders[anchor].top[i].style.opacity = 0
-        @shaders[anchor].bottom[i].style.opacity = opacity
+      @shaders[anchor].top[i].style.opacity = a
+      @shaders[anchor].bottom[i].style.opacity = b

   _getShaderGradient: (anchor) ->


transform panels upon construction to fix gaps

--- oridomi.coffee
+++ oridomi.coffee
@@ -559,6 +559,7 @@
             for a, i in anchorSet
               @_shaders[anchor][a][panelN] = panel.children[0].children[i + 1]

+          @_transformPanel panel, 0, anchor
           @_panels[anchor][panelN] = panel
           # Panels are nested inside each other.
           @_panels[anchor][panelN - 1].appendChild panel unless panelN is 0


fix requesting of timeout tiles

fix requesting of timeout tiles

--- src/plugins/request-portals.coffee
+++ src/plugins/request-portals.coffee
@@ -252,7 +252,7 @@
                 if t.tile.error is 'TIMEOUT'

                     Tile.data[t.id].status = STATUS_TIMEOUT
-                    timeoutTiles.push id
+                    timeoutTiles.push t.id

                 else

@@ -341,4 +341,4 @@
     timeoutTiles = []
     failTiles = []

-    callback()+    callback()


:memo:

:memo:

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -858,6 +858,7 @@
   moveConflictingEntry: (initialPath, newPath, newDirectoryPath) =>
     try
       unless fs.isDirectorySync(initialPath)
+        # Files, symlinks, anything but a directory
         chosen = atom.confirm
           message: "'#{path.relative(newDirectoryPath, newPath)}' already exists"
           detailedMessage: 'Do you want to replace it?'


Fix custom controls config options to match changes in Cilantro 2.2.11

--- varify/static/scripts/coffeescript/main.coffee
+++ varify/static/scripts/coffeescript/main.coffee
@@ -49,15 +49,10 @@
     c.config.set('fields.instances.28.form.controls', ['multiSelectionList'])
     c.config.set('fields.instances.29.form.controls', ['multiSelectionList'])

-    # Set the custom control for the HGMD, Sift, and PolyPhen2 fields. We use
-    # c.ui.set instead of c.ui.controls.set because of a bug in Cilantro where
-    # the controls properties get mixed in with ui instead of controls itself.
-    #
-    # XXX: This will need to be updated when the bug is fixed in Cilantro and
-    # that version of Cilantro is added to Varify.
-    c.ui.set('Hgmd', ui.HgmdSelector)
-    c.ui.set('Sift', ui.SiftSelector)
-    c.ui.set('PolyPhen', ui.PolyPhenSelector)
+    # Set the custom control for the HGMD, Sift, and PolyPhen2 fields.
+    c.controls.set('Hgmd', ui.HgmdSelector)
+    c.controls.set('Sift', ui.SiftSelector)
+    c.controls.set('PolyPhen', ui.PolyPhenSelector)
     c.config.set('fields.instances.110.form.controls', ['Hgmd'])
     c.config.set('fields.instances.58.form.controls', ['Sift'])
     c.config.set('fields.instances.56.form.controls', ['PolyPhen'])


Fix base64 UTF-8 Decode & Encode issue

Fix base64 UTF-8 Decode & Encode issue

--- core/utils.coffee
+++ core/utils.coffee
@@ -629,6 +629,45 @@
   stopDOMEvent :(event)->
     event.preventDefault()
     event.stopPropagation()
+
+  utf8Encode:(string)->
+    string = string.replace(/\r\n/g, "\n")
+    utftext = ""
+    n = 0
+
+    while n < string.length
+      c = string.charCodeAt(n)
+      if c < 128
+        utftext += String.fromCharCode(c)
+      else if (c > 127) and (c < 2048)
+        utftext += String.fromCharCode((c >> 6) | 192)
+        utftext += String.fromCharCode((c & 63) | 128)
+      else
+        utftext += String.fromCharCode((c >> 12) | 224)
+        utftext += String.fromCharCode(((c >> 6) & 63) | 128)
+        utftext += String.fromCharCode((c & 63) | 128)
+      n++
+    utftext
+
+  utf8Decode:(utftext)->
+    string = ""
+    i = 0
+    c = c1 = c2 = 0
+    while i < utftext.length
+      c = utftext.charCodeAt(i)
+      if c < 128
+        string += String.fromCharCode(c)
+        i++
+      else if (c > 191) and (c < 224)
+        c2 = utftext.charCodeAt(i + 1)
+        string += String.fromCharCode(((c & 31) << 6) | (c2 & 63))
+        i += 2
+      else
+        c2 = utftext.charCodeAt(i + 1)
+        c3 = utftext.charCodeAt(i + 2)
+        string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))
+        i += 3
+    string

   # Return true x% of time based on argument.
   #


Fix URL for cricket feed

Fix URL for cricket feed

--- src/scripts/cricket.coffee
+++ src/scripts/cricket.coffee
@@ -5,7 +5,7 @@
 #

 module.exports = (robot) ->
-  feed_url = "http://query.yahooapis.com/v1/public/yql?q=select%20title%20from%20rss%20where%20url%3D%22http%3A%2F%2Fxstatic.cricinfo.com%2Frss%2Flivescores.xml%22&format=json&diagnostics=true&callback="
+  feed_url = "http://query.yahooapis.com/v1/public/yql?q=select%20title%20from%20rss%20where%20url%3D%22http%3A%2F%2Fstatic.cricinfo.com%2Frss%2Flivescores.xml%22&format=json&diagnostics=true&callback="
   prefix = "CRICKET SCORE: "
   robot.respond /cricket scores for (.*)/i, (msg) ->
     query = msg.match[1]?.toUpperCase()


thanks safari

thanks safari

--- resources/assets/coffee/_classes/sticky-header.coffee
+++ resources/assets/coffee/_classes/sticky-header.coffee
@@ -37,11 +37,11 @@
     for marker in @stickMarker by -1
       css = window.getComputedStyle(marker)
       top = if css.position == 'sticky' || css.position == '-webkit-sticky'
-              parseInt(css.top, 10)
+              parseInt(css.top, 10) + 1 # because Safari
             else
               0

-      if marker.getBoundingClientRect().top <= top
+      if marker.getBoundingClientRect().top < top
         $.publish 'stickyHeader', marker.getAttribute('data-sticky-header-target')
         return


ログにエラー内容を出力するように修正

ログにエラー内容を出力するように修正


added docs about jenkins auth

added docs about jenkins auth

fixes issue #1065

--- src/scripts/jenkins.coffee
+++ src/scripts/jenkins.coffee
@@ -7,6 +7,8 @@
 # Configuration:
 #   HUBOT_JENKINS_URL
 #   HUBOT_JENKINS_AUTH
+#
+#   Auth should be in the "user:password" format.
 #
 # Commands:
 #   hubot jenkins b <jobNumber> - builds the job specified by jobNumber. List jobs to get number.


make_me: link to more user-friendly web page

--- src/scripts/make_me.coffee
+++ src/scripts/make_me.coffee
@@ -42,7 +42,8 @@
                "#{robot.name} 3d unlock - unlocks the printer after you clean up\n\n" +
                "Only 1 print at a time is allowed, and you are required to tell\n" +
                "#{robot.name} after you've cleaned your print off.\n\n" +
-               "The most current log is always available at #{makeServer}/log"
+               "The web frontend is at #{makeServer}, and\n" +
+               "the most current log is always available at #{makeServer}/log\n\n" +
     msg.send response

   robot.respond /3d (snapshot|status)/i, (msg) ->
@@ -123,10 +124,10 @@
         }
       })) (err, res, body) =>
         if res.statusCode is 200
-          msg.reply "Your thing is printin'! Check logs at #{makeServer}/log"
+          msg.reply "Your thing is printin'! Check logs at #{makeServer}"
         else if res.statusCode is 409
           msg.reply "I couldn't process that pile of triangles."
         else if res.statusCode is 423
-          msg.reply "Wait your turn, someone is already printing a thing. You can check progress at #{makeServer}/log"
+          msg.reply "Wait your turn, someone is already printing a thing. You can check progress at #{makeServer}"
         else if err or res.statusCode is 500
           msg.reply "Something broke!"


Add additional pivotal options (get specific story for defined project)

--- src/scripts/pivotal.coffee
+++ src/scripts/pivotal.coffee
@@ -39,3 +39,29 @@
                   msg.send message
             return
         msg.send "No project #{project_name}"
+
+  robot.respond /(pivotal story)? (.*)/i, (msg)->
+    Parser = require("xml2js").Parser
+    token = process.env.HUBOT_PIVOTAL_TOKEN
+    project_id = process.env.HUBOT_PIVOTAL_PROJECT
+    story_id = msg.match[2]
+
+    msg.http("https://www.pivotaltracker.com/services/v3/projects/#{project_id}/stories/#{story_id}").headers("X-TrackerToken": token).get() (err, res, body) ->
+      if err
+        msg.send "Pivotal says: #{err}"
+        return
+
+      if res.statusCode == 500
+        msg.send "Whoa! Pivotal returned 500..."
+        return
+
+      (new Parser).parseString body, (err, story)->
+        if !story.id
+          msg.send "Story not found."
+          return
+
+        message = "##{story.id['#']} #{story.name}"
+        message += " (#{story.owned_by})" if story.owned_by
+        message += " is #{story.current_state}" if story.current_state && story.current_state != "unstarted"
+        msg.send message
+


Documentation for pivotal story command

Documentation for pivotal story command

--- src/scripts/pivotal.coffee
+++ src/scripts/pivotal.coffee
@@ -10,6 +10,7 @@
 #
 # Commands:
 #   show me stories for <project> - shows current stories being worked on
+#   pivotal story <story_id> - shows story title, owner and status
 #
 # Author:
 #   assaf


fixing comments so `hubot help` shows commands first

--- src/scripts/redmine.coffee
+++ src/scripts/redmine.coffee
@@ -1,16 +1,16 @@
-# Showing of redmine issuess via the REST API.
-#
 # (redmine|show) me <issue-id>     - Show the issue status
 # show (my|user's) issues          - Show your issues or another user's issues
 # assign <issue-id> to <user-first-name> ["notes"]  - Assign the issue to the user (searches login or firstname)
 #                                                     *With optional notes
 # update <issue-id> with "<note>"  - Adds a note to the issue
 # add <hours> hours to <issue-id> ["comments"]  - Adds hours to the issue with the optional comments
-
+#
 # Note: <issue-id> can be formatted in the following ways:
 #       1234, #1234, issue 1234, issue #1234
 #
 #---
+#
+# Showing of redmine issuess via the REST API.
 #
 # To get set up refer to the guide http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication
 # After that, heroku needs the following config


Fix \n so it's not paste but text

Fix \n so it's not paste but text

--- src/scripts/weather.coffee
+++ src/scripts/weather.coffee
@@ -53,7 +53,7 @@
       else
         temp = body.getElementsByTagName("current_conditions")[0].getElementsByTagName("temp_f")[0].getAttribute("data") + "ºF"

-      msg.send "Currently in #{city} it is #{conditions} and #{temp} with a humidity of #{humidity}.\n"
+      msg.send "Currently in #{city} it is #{conditions} and #{temp} with a humidity of #{humidity}."

   getDom = (xml) ->
     body = jsdom.jsdom(xml)


edison.coffee: enable generating compressed artifacts.

--- edison.coffee
+++ edison.coffee
@@ -60,6 +60,7 @@
 		version: 'yocto-daisy'
 		deployArtifact: 'resin-edison'
 		archive: true
+		compressed: true

 	options: [
 		isGroup: true


fix keep-lines

fix keep-lines


Changed "retries" property from binding to computed alias

--- packages/emberella/lib/mixins/mq_mixin.coffee
+++ packages/emberella/lib/mixins/mq_mixin.coffee
@@ -179,7 +179,7 @@
     @type Integer
     @default 0
   ###
-  retriesBinding: 'mqStateManager.retries'
+  retries: Ember.computed.alias('mqStateManager.retries').readOnly()

   ###
     A computed property that returns true when this object is in the


Fixed bug that could cause an orphaned listing to appear

--- packages/emberella/lib/views/list_view.coffee
+++ packages/emberella/lib/views/list_view.coffee
@@ -579,15 +579,15 @@
     else
       # If `emptyView` is correctly defined and the list length is 0, append
       # it to the listing
+      for i in [0...childViewsLength]
+        childView = childViews.objectAt(i)
+        @_reuseChildForContentIndex(childView, i) if childView
       return unless emptyView
       isClass = Ember.CoreView.detect emptyView
       emptyView = @createChildView(emptyView)
       set(@, 'emptyView', emptyView)
       @_createdEmptyView = emptyView if isClass
       @unshiftObject(emptyView)
-      for i in [0...childViewsLength]
-        childView = childViews.objectAt(i)
-        @_reuseChildForContentIndex(childView, i)

     null
   , 'startingIndex'
@@ -609,6 +609,7 @@
     @return null
   ###
   _reuseChildForContentIndex: (childView, contentIndex) ->
+    return unless childView?
     return if get(childView, 'isDestroyed')

     currentStartingIndex = @_startingIndex()


add a new url resource for wiki attchaments


using encodeURIComponent for docName

using encodeURIComponent for docName

--- src/server/db/mongo.coffee
+++ src/server/db/mongo.coffee
@@ -29,11 +29,14 @@

   client = new mongodb.Db(options.db, new mongodb.Server(options.hostname, options.port, options.mongoOptions))

-  opsCollectionForDoc = (docName) -> 'ops.'+docName.replace /$/g, ''  # $ is reserved in collection names.
+  opsCollectionForDoc = (docName) -> 'ops.' + encodeURIComponent(docName).replace(/\./g, '%2E').replace(/-/g, '%2D')

   # Creates a new document.
   # data = {snapshot, type:typename, [meta]}
   @create = (docName, data, callback) ->
+    # docName too long? http://www.mongodb.org/display/DOCS/Collections#Collections-Overview
+    return callback? "Document name too long: #{docName}" if opsCollectionForDoc(docName).length > 90
+
     client.collection 'docs', (err, collection) ->
       return callback? err if err


integrate removeClass methods

integrate removeClass methods

--- lib/vim-state.coffee
+++ lib/vim-state.coffee
@@ -494,4 +494,4 @@
       @removeStatusBarClass()
       $('#status-bar-vim-mode').addClass('status-bar-vim-mode-visual').html("Visual")

-  removeStatusBarClass: -> $('#status-bar-vim-mode').removeClass('status-bar-vim-mode-insert').removeClass('status-bar-vim-mode-command').removeClass('status-bar-vim-mode-visual')
+  removeStatusBarClass: -> $('#status-bar-vim-mode').removeClass('status-bar-vim-mode-insert status-bar-vim-mode-command status-bar-vim-mode-visual')


Improve alert message

Improve alert message

--- packages/rocketchat-webrtc/WebRTCClass.coffee
+++ packages/rocketchat-webrtc/WebRTCClass.coffee
@@ -364,12 +364,18 @@
 	###
 	onRemoteCall: (data) ->
 		fromUsername = Meteor.users.findOne(data.from)?.username
-		if data.video and data.audio
-			title = "Audio and video call from #{fromUsername}"
-		else if data.video
-			title = "Video call from #{fromUsername}"
-		else if data.audio
-			title = "Audio call from #{fromUsername}"
+		subscription = ChatSubscription.findOne({rid: data.room})
+
+		if subscription?.t is 'd'
+			if data.video
+				title = "Direct video call from #{fromUsername}"
+			else
+				title = "Direct audio call from #{fromUsername}"
+		else
+			if data.video
+				title = "Group video call from #{subscription.name}"
+			else
+				title = "Group audio call from #{subscription.name}"

 		swal
 			title: title


enable table state save

enable table state save

--- app/assets/javascripts/datatables.js.coffee
+++ app/assets/javascripts/datatables.js.coffee
@@ -5,7 +5,8 @@
 jQuery ->
   # Set up datatable
   $('.data-table').DataTable
-    order: [0, 'desc']
+    order: [0, 'desc'],
+    stateSave: true,
     columnDefs: [{
       orderable: false
       targets: 'no-sort'
@@ -15,7 +16,8 @@
 jQuery ->
 # Set up datatable
   $('.data-table-new-job').DataTable
-    order: [0, 'desc']
+    order: [0, 'desc'],
+    stateSave: true,
     columnDefs: [{
       orderable: false
       targets: 'no-sort'


improves mixin initialization api

improves mixin initialization api

--- source/object.coffee
+++ source/object.coffee
@@ -1,8 +1,15 @@

 class Space.Object

+  @_mixinCallbacks: null
+
   # Assign given properties to the instance
   constructor: (properties) -> @[key] = value for key, value of properties
+
+  onDependenciesReady: ->
+    if @constructor._mixinCallbacks?
+      # Let mixins initialize themselves when dependencies are ready
+      callback.call(this) for callback in @constructor._mixinCallbacks

   # Extends this class and return a child class with inherited prototype
   # and static properties.
@@ -121,6 +128,16 @@
   # properties that are plain objects to support the mixin of configs etc.
   @mixin: (mixin) ->

+    # Create a clone so that we can remove properties without affecting the global mixin
+    mixin = _.clone mixin
+
+    # Register the onDependenciesReady method of mixins as a initialization callback
+    mixinCallback = mixin.onDependenciesReady
+    if mixinCallback?
+      @_mixinCallbacks ?= []
+      @_mixinCallbacks.push mixinCallback
+      delete mixin.onDependenciesReady
+
     # Helper function to check for object literals only
     isPlainObject = (value) ->
       _.isObject(value) and !_.isArray(value) and !_.isFunction(value)