Fixed recovery from Worker failure

Fixed recovery from Worker failure

--- app/helpers/workers.coffee
+++ app/helpers/workers.coffee
@@ -18,7 +18,12 @@
     ###
     @startForever = (name, work, fail_callback) ->
       name = name.toLowerCase()
-      log.info "Worker [#{name}] started"
+
+      if workers_running.find name
+        log.error "Worker {#{name}} already started!"
+        return fail_callback?()
+      else
+        log.info "Worker [#{name}] started"

       async.forever (next) ->
         workers_running.add name
@@ -33,19 +38,22 @@
           else
             setTimeout next, helpers.config.get 'worker timespan'
       , (err) ->
-        worker_fails[name] = 0 if not worker_fails[name]?
-        worker_fails[name]++
+        if worker_fails[name]?
+          worker_fails[name]++
+        else
+          worker_fails[name] = 1

         # Only re-run the Worker if it's still within configured threshold
-        if worker_fails < app.get 'max worker fails'
-          log.warn "Worker [#{name}] failed for the #{worker_fails.ordinalize()} time, restarting...", err
+        if worker_fails[name] < cfg.get 'max worker fails'
+          log.warn "Worker [#{name}] failed for the #{worker_fails[name].ordinalize()} time, restarting...", err
+          console.trace()
           @startForever name, work, fail_callback
         else
           delete worker_fails[name]

-          if app.enabled 'die on max worker fails'
-            log.error "Worker [#{name}] failed for the #{worker_fails.ordinalize()} time, killing BlackSam"
+          if cfg.enabled 'die on max worker fails'
+            log.error "Worker {#{name}} failed for the #{worker_fails[name].ordinalize()} time, killing BlackSam"
             process.exit 1
           else
-            log.error "Worker [#{name}] failed for the #{worker_fails.ordinalize()} time, won't be restarted"
+            log.error "Worker {#{name}} failed for the #{worker_fails[name].ordinalize()} time, won't be restarted"
             fail_callback? err


feat(bower): add support for bower packages dependencies

--- helpers/bower.coffee
+++ helpers/bower.coffee
@@ -11,11 +11,12 @@
 # bowerHelper.get.src.pkgs appOrRb
 # =======================================
 module.exports = (config) ->
-	fs     = require 'fs'
-	path   = require 'path'
-	mkdirp = require 'mkdirp'
-	log    = require "#{config.req.helpers}/log"
-	isType = require "#{config.req.helpers}/isType"
+	fs       = require 'fs'
+	path     = require 'path'
+	mkdirp   = require 'mkdirp'
+	log      = require "#{config.req.helpers}/log"
+	isType   = require "#{config.req.helpers}/isType"
+	pathHelp = require "#{config.req.helpers}/path"

 	# helpers
 	# =======
@@ -66,13 +67,46 @@
 					_files.push file: _file, path: _path
 			_files

+		getPkgDeps: (deps) ->
+			return null if not deps
+			return null if not isType.object deps
+			return null if not Object.keys(deps).length
+			deps
+
 		getPkg: (pkg, loc='rb') ->
 			devFileInfo  = @getDevFileInfo  pkg.name, pkg.main, loc
 			prodFileInfo = @getProdFileInfo pkg.name, devFileInfo, loc
+			pkgDeps      = @getPkgDeps pkg.dependencies
+			# log.json pkgDeps
 			name:    pkg.name
 			version: pkg.version
 			dev:     devFileInfo
 			prod:    prodFileInfo
+			deps:    pkgDeps
+
+		getSubPkgs: (pkgs) ->
+			return null if not pkgs.length
+			# log.json pkgs
+			rbPkgs  = if me.has.bower 'rb'  then me.get.pkgs.from.appOrRb 'rb'  else {}
+			appPkgs = if me.has.bower 'app' then me.get.pkgs.from.appOrRb 'app' else {}
+			allPkgs = Object.keys(rbPkgs).concat Object.keys appPkgs
+			return null if not allPkgs.length
+			subPkgs = {}
+			for own i, pkg of pkgs
+				continue if not pkg.deps
+				for own name, version of pkg.deps
+					continue if allPkgs.indexOf(name) isnt -1
+					subPkgs[name] = version
+			return null if not Object.keys(subPkgs).length
+			# log.json subPkgs
+			subPkgs
+
+		getInstalledPkgs: (pkgsObj, loc) ->
+			pkgs = []
+			for own pkg, version of pkgsObj
+				_pkg = me.get.installed.pkg pkg, loc
+				pkgs.push _pkg if _pkg
+			pkgs

 		writeVersionFile: (file, version) ->
 			fs.writeFileSync file, version
@@ -126,7 +160,9 @@
 			pkgs:
 				from:
 					appOrRb: (loc='rb') ->
-						me.get.json.from.appOrRb(loc).dependencies
+						mainDeps = me.get.json.from.appOrRb(loc).dependencies
+						# log.json mainDeps
+						mainDeps
 				to:
 					install: (loc='rb', force=false) ->
 						return if not me.has.bower loc
@@ -154,9 +190,14 @@
 				pkgs: (loc='rb') ->
 					pkgs    = []
 					pkgsObj = me.get.pkgs.from.appOrRb loc
-					for own pkg, version of pkgsObj
-						_pkg = @pkg pkg, loc
-						pkgs.push _pkg if _pkg
+					pkgs    = helper.getInstalledPkgs pkgsObj, loc if pkgsObj
+					# log.json pkgs
+					if pkgs.length
+						subPkgs    = []
+						subPkgsObj = helper.getSubPkgs pkgs
+						subPkgs    = helper.getInstalledPkgs subPkgsObj, loc if subPkgsObj
+						pkgs       = pkgs.concat subPkgs if subPkgs.length
+					# log.json pkgs
 					pkgs

 			missing:
@@ -181,9 +222,9 @@
 				pkgs     = me.get.installed.pkgs loc
 				# log.json pkgs
 				pkgs.forEach (pkg) ->
-					# absPaths.push pkg[env].path
-					name = if pkg[env].length > 1 then pkg.name else ''
 					pkg[env].forEach (file) ->
+						hasPath = pathHelp.format(file.file).indexOf('/') isnt -1
+						name    = if hasPath then pkg.name else ''
 						relPaths[file.path] = path.join name, file.file
 						absPaths.push file.path
 				paths:


silly: No tools user on custard instances

--- server/code/model/tool.coffee
+++ server/code/model/tool.coffee
@@ -45,7 +45,7 @@
         cmd = "mkdir #{@directory} && git init && git fetch #{@gitUrl}; cd #{@directory}"
       else
         cmd = "cd #{@directory}; git fetch #{@gitUrl} && git checkout FETCH_HEAD"
-      cmd += "; chown -R tools:tools ." if process.env.NODE_ENV?
+      cmd += "; chown -R www-data:www-data ." if process.env.NODE_ENV?
       child_process.exec cmd, =>
         async.each Box.listServers(), @rsync, callback

@@ -60,7 +60,7 @@
     fs.exists @directory, (exists) =>
       if not exists
         cmd = "mkdir #{@directory} && git init && git fetch #{@gitUrl}; cd #{@directory}"
-        cmd += "; chown -R tools:tools ."
+        cmd += "; chown -R www-data:www-data ."
         child_process.exec cmd, =>
           async.each Box.listServers(), @rsync, callback
       else


init method is optional

init method is optional

During konnector initialization, the init method is now optional.

--- server/init/konnectors.coffee
+++ server/init/konnectors.coffee
@@ -78,13 +78,18 @@
 # Then save konnector data to database.
 initializeKonnector = (konnector, callback) ->

-    konnector.init (err) ->
-        if err
-            log.error err
+    log.debug "creating #{konnector.slug}"
+    if konnector.init?
+        konnector.init (err) ->
+            if err
+                log.error err
+                callback err
+            else
+                Konnector.create konnector, (err) ->
+                    log.error err if err
+                    callback err
+    else
+        Konnector.create konnector, (err) ->
+            log.error err if err
             callback err
-        else
-            log.debug "creating #{konnector.slug}"
-            Konnector.create konnector, (err) ->
-                log.error err if err
-                callback err


Always try to call fs.realpathSync

Always try to call fs.realpathSync

Catch the error and use the unreal path when it fails

--- lib/file.coffee
+++ lib/file.coffee
@@ -28,12 +28,11 @@
   constructor: ->
     super
     repo = atom.project.getRepo()
-    filePath = @file.getPath()

-    @path = if fs.existsSync(filePath)
-      fs.realpathSync(filePath)
-    else
-      filePath
+    try
+      @path = fs.realpathSync(@file.getPath())
+    catch error
+      @path = @file.getPath()

     if repo?
       @subscribeToRepo(repo)


fix incorrect icon name import

fix incorrect icon name import

--- src/smc-webapp/app_shared.cjsx
+++ src/smc-webapp/app_shared.cjsx
@@ -390,7 +390,7 @@
     displayName : 'AppLogo'

     render: ->
-        {APP_ICON} = require('./misc_page')
+        {APP_ICON} = require('./art')
         styles =
             display         : 'inline-block'
             backgroundImage : "url('#{APP_ICON}')"


Do nothing if project not open

Do nothing if project not open

--- src/smc-webapp/init_app.coffee
+++ src/smc-webapp/init_app.coffee
@@ -59,6 +59,9 @@
         active_top_tab = page_store.get('active_top_tab')

         index = open_projects.indexOf(project_id)
+        if index == -1
+            return
+
         size = open_projects.size
         if project_id == active_top_tab
             if index == -1 or size <= 1
@@ -69,14 +72,12 @@
                 next_active_tab = open_projects.get(index + 1)
             @set_active_tab(next_active_tab)

-        ## This is somehow (?) broken by "computed values" (or something else) --
         # The point of these "ghost tabs" is to make it so you can quickly close several
-        # open tabs, like in Chrome.   I'm disabling this for now until this gets
-        # sorted out. See https://github.com/sagemathinc/smc/issues/1271
-        #if index == size - 1
-        #    @clear_ghost_tabs()
-        #else
-        #    @add_a_ghost_tab()
+        # open tabs, like in Chrome.
+        if index == size - 1
+            @clear_ghost_tabs()
+        else
+            @add_a_ghost_tab()

         # TODO: The functionality below should perhaps here and not in the projects actions (?).
         redux.getActions('projects').set_project_closed(project_id)


Cleanup before executing specs

Cleanup before executing specs

--- src/spec/elasticio.spec.coffee
+++ src/spec/elasticio.spec.coffee
@@ -1,17 +1,33 @@
 _ = require 'underscore'
+Q = require 'q'
+SphereClient = require 'sphere-node-client'
+{ElasticIo} = require 'sphere-node-utils'
+Logger = require '../lib/logger'
+Config = require '../config'
 elasticio = require '../lib/elasticio'
-Config = require '../config'
-Logger = require '../lib/logger'
-StockImport = require '../lib/stockimport'
-{ElasticIo} = require 'sphere-node-utils'

 describe 'elasticio integration', ->

-  beforeEach ->
-    @logger = new Logger
+  beforeEach (done) ->
+    logger = new Logger
       streams: [
         { level: 'info', stream: process.stdout }
       ]
+    @client = new SphereClient
+      config: Config.config
+      logConfig:
+        logger: logger
+
+    logger.info 'Deleting old inventory entries...'
+    @client.inventoryEntries.perPage(0).fetch()
+    .then (result) =>
+      Q.all _.map result.body.results, (e) =>
+        @client.inventoryEntries.byId(e.id).delete(e.version)
+    .then (results) ->
+      logger.info "#{_.size results} deleted."
+      done()
+    .fail (err) -> done(_.prettify err)
+  , 10000 # 10sec

   it 'should work with no attachments nor body', (done) ->
     cfg =
@@ -124,8 +140,8 @@

       elasticio.process msg, cfg, (error, message) ->
         expect(error).toBe null
-        expect(message['Inventory entry created.']).toBe 0
-        expect(message['Inventory entry updated.']).toBe 1
+        expect(message['Inventory entry created.']).toBe 1
+        expect(message['Inventory entry updated.']).toBe 0
         expect(message['Inventory update was not necessary.']).toBe 0
         msg.body.QUANTITY = '3'
         elasticio.process msg, cfg, (error, message) ->
@@ -156,8 +172,8 @@

       elasticio.process msg, cfg, (error, message) ->
         expect(error).toBe null
-        expect(message['Inventory entry created.']).toBe 0
-        expect(message['Inventory entry updated.']).toBe 1
+        expect(message['Inventory entry created.']).toBe 1
+        expect(message['Inventory entry updated.']).toBe 0
         expect(message['Inventory update was not necessary.']).toBe 0
         msg.body.QUANTITY = '3'
         elasticio.process msg, cfg, (error, message) ->
@@ -174,13 +190,7 @@
         sphereClientSecret: Config.config.client_secret
         sphereProjectKey: Config.config.project_key

-      sxi = new StockImport
-        config: Config.config
-        logConfig:
-          logger: @logger
-        headerNames: {}
-
-      sxi.client.channels.ensure('channel-id-test', ['InventorySupply', 'OrderExport', 'OrderImport'])
+      @client.channels.ensure('channel-id-test', ['InventorySupply', 'OrderExport', 'OrderImport'])
       .then (channel) ->
         msg =
         attachments: {}
@@ -191,8 +201,8 @@

         elasticio.process msg, cfg, (error, message) ->
           expect(error).toBe null
-          expect(message['Inventory entry created.']).toBe 0
-          expect(message['Inventory entry updated.']).toBe 1
+          expect(message['Inventory entry created.']).toBe 1
+          expect(message['Inventory entry updated.']).toBe 0
           expect(message['Inventory update was not necessary.']).toBe 0
           msg.body.QUANTITY = '3'
           elasticio.process msg, cfg, (error, message) ->


Added protractor-tools project.

Added protractor-tools project.

--- home/.atom/projects.cson
+++ home/.atom/projects.cson
@@ -38,3 +38,8 @@
   'paths': [
     '/Users/Nils.Winkler/workspaces/Nils/nilswinkler.com'
   ]
+'protractor-tools':
+  'title': 'protractor-tools'
+  'paths': [
+    '/Users/nils.winkler/workspaces/html5-git/tools/html5-protractor-tools'
+  ]


Activity: inputview minor code styling

Activity: inputview minor code styling

--- client/Social/Activity/views/inputview.coffee
+++ client/Social/Activity/views/inputview.coffee
@@ -48,7 +48,7 @@
     value = @tokenInput.textContent.substring(prefix.length).toLowerCase()
     tokens = @menu.getData().filter @getTokenFilter()
     for token in tokens
-      if value is token.name.toLowerCase()
+      if value is token.title.toLowerCase()
         @addToken token, @getOptions().tokenViewClass
         @hideMenu()
         return  true
@@ -79,6 +79,7 @@
     @tokenInput.remove()

   keyDown: (event) ->
+
     super event
     # return  if event.isPropagationStopped()
     switch event.which
@@ -86,7 +87,7 @@
         KD.utils.stopDOMEvent event
         @handleEnter event
       when 27 # Escape
-        @emit "Escape"
+        @emit 'Escape'

     if /\W/.test String.fromCharCode event.which
       if @selectToken() then KD.utils.stopDOMEvent event
@@ -94,7 +95,9 @@

     return yes

+
   keyUp: (event) ->
+
     return  if @getTokens().length >= TOKEN_LIMIT
     return @matchPrefix()  unless @activeRule

@@ -104,6 +107,7 @@
       super event

   handleEnter: (event) ->
+
     return @insertNewline()  if event.shiftKey

     position = @getPosition() + 1


mp4: Fix getSampleAfterSeconds() and getSampleAtSeconds()

Take into account the empty duration.

--- mp4.coffee
+++ mp4.coffee
@@ -1495,7 +1495,12 @@
     timescale = @findParent('mdia').find('mdhd').timescale
     remainingTime = sec * timescale
     sampleNumber = 1
-    totalTime = 0
+    elstBox = @findParent('trak').child('edts')?.child('elst')
+    if elstBox?
+      totalTime = elstBox.getEmptyDuration()
+      remainingTime -= totalTime
+    else
+      totalTime = 0
     for entry in @entries
       numSamples = Math.ceil(remainingTime / entry.sampleDelta)
       if numSamples <= entry.sampleCount
@@ -1525,7 +1530,12 @@
     timescale = @findParent('mdia').find('mdhd').timescale
     remainingTime = sec * timescale
     sampleNumber = 1
-    totalTime = 0
+    elstBox = @findParent('trak').child('edts')?.child('elst')
+    if elstBox?
+      totalTime = elstBox.getEmptyDuration()
+      remainingTime -= totalTime
+    else
+      totalTime = 0
     for entry in @entries
       sampleIndexInChunk = Math.floor(remainingTime / entry.sampleDelta)
       if sampleIndexInChunk < entry.sampleCount


iframe fix

iframe fix

--- app/assets/javascripts/app/models/calendar_event.js.coffee
+++ app/assets/javascripts/app/models/calendar_event.js.coffee
@@ -12,4 +12,6 @@

   @bind 'ajaxError', (_, e) ->
     if e.status is 401
-      window.location = '/authorize'+      # Safari iframe cookie fix
+      form = $('<form />').attr('action', '/authorize')
+      form.appendTo('body').submit()


Modes; Note visual mode not yet implemented.

--- background_scripts/commands.coffee
+++ background_scripts/commands.coffee
@@ -285,7 +285,7 @@
   openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { background: true, repeatLimit: 20 }]

   enterInsertMode: ["Enter insert mode", { noRepeat: true }]
-  enterVisualMode: ["Enter visual mode", { noRepeat: true }]
+  enterVisualMode: ["Enter visual mode (not yet implemented)", { noRepeat: true }]

   focusInput: ["Focus the first text box on the page. Cycle between them using tab",
     { passCountToFunction: true }]


fix demo

fix demo

--- src/components/demo.cjsx
+++ src/components/demo.cjsx
@@ -303,7 +303,6 @@
       notices: <NoticesDemo />
       multipartExercise: <MultipartExerciseDemo/>
       exercise: <ExerciseDemo/>
-      exercisePreview: <ExercisePreviewDemo/>
       breadcrumbs: <BreadcrumbDemo/>
       html: <HTMLDemo/>


prevent "root@openhim.org" user from being deleted through the API

--- src/api/users.coffee
+++ src/api/users.coffee
@@ -126,6 +126,14 @@
 		return;
 	}

+	// Test if the user is root@openhim.org
+	if ( this.authenticated.email === 'root@openhim.org' ) {
+		logger.info('User ' +this.authenticated.email+ ' is OpenHIM root, User cannot be deleted through the API')
+		this.body = 'User ' +this.authenticated.email+ ' is OpenHIM root, User cannot be deleted through the API'
+		this.status = 'forbidden';
+		return;
+	}
+
 	var email = unescape (email);

 	try {


collaboration: controller remove machine from env data

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -17,6 +17,7 @@
 socialHelpers                 = require './collaboration/helpers/social'
 envHelpers                    = require './collaboration/helpers/environment'
 CollaborationStateMachine     = require './collaboration/collaborationstatemachine'
+environmentDataProvider       = require 'app/userenvironmentdataprovider'

 {warn} = kd

@@ -800,6 +801,7 @@
   removeMachineNode: ->

     kd.singletons.mainView.activitySidebar.removeMachineNode @mountedMachine
+    environmentDataProvider.removeCollaborationMachine @mountedMachine

   ensureMachineShare: (usernames, callback) ->


do not rerender on update

do not rerender on update

--- src/components/html.cjsx
+++ src/components/html.cjsx
@@ -35,14 +35,17 @@
       link.setAttribute('target', '_blank') unless link.getAttribute('href')?[0] is '#'

     # Clone the array because the browser will mutable it
-    nodes = root.querySelectorAll('[data-math]') or []
+    nodes = root.querySelectorAll('[data-math]:not(.math-rendered)') or []
     nodes = _.toArray(nodes)

     _.each nodes, (node) ->
       formula = node.getAttribute('data-math')
       # Divs with data-math should be rendered as a block
       isBlock = node.tagName.toLowerCase() in ['div']
+      if isBlock
+        formula = "\\displaystyle {#{formula}}"

+      node.classList.remove('math-rendered')
       node.textContent = "{{MATH}}#{formula}{{MATH}}"

     cb = ->


merging in early error for compound assignment to undeclared variables.

--- nodes.coffee
+++ nodes.coffee
@@ -1117,9 +1117,12 @@
   # operands are only evaluated once, even though we have to reference them
   # more than once.
   compileConditional: (o) ->
-    [left, rite] = @variable.cacheReference o
+    [left, right] = @variable.cacheReference o
+    # Disallow conditional assignment of undefined variables.
+    if left.base instanceof Literal and left.base.value != "this" and not o.scope.check left.base.value
+      throw new Error "the variable \"#{left.base.value}\" can't be assigned with #{@context} because it has not been defined."
     if "?" in @context then o.isExistentialEquals = true
-    new Op(@context[...-1], left, new Assign(rite, @value, '=') ).compile o
+    new Op(@context[...-1], left, new Assign(right, @value, '=') ).compile o

   # Compile the assignment from an array splice literal, using JavaScript's
   # `Array#splice` method.


range comprehensions are working smooth

range comprehensions are working smooth

--- nodes.coffee
+++ nodes.coffee
@@ -883,7 +883,7 @@
     @step:    source.step
     @object:  !!source.object
     [@name, @index]: [@index, @name] if @object
-    @children: [@body, @source, @filter]
+    @children: compact [@body, @source, @filter]
     this

   top_sensitive: ->


Improved the event

Improved the event

--- ZONE-watcher/app/assets/javascripts/sources.js.coffee
+++ ZONE-watcher/app/assets/javascripts/sources.js.coffee
@@ -7,11 +7,11 @@
         $('[class=number_items_container][data-url="'+myUri+'"]').append(data)
         $('[class*=number_items_wait][data-url="'+myUri+'"]').detach()

-  $(".accordion-body").on "shown", ->
+  $(".accordion-body").on "show", ->
     $(this).prev(".accordion-heading").find("i").removeClass "icon-chevron-sign-right"
     $(this).prev(".accordion-heading").find("i").addClass "icon-chevron-sign-down"

-  $(".accordion-body").on "hidden", ->
+  $(".accordion-body").on "hide", ->
     $(this).prev(".accordion-heading").find("i").removeClass "icon-chevron-sign-down"
     $(this).prev(".accordion-heading").find("i").addClass "icon-chevron-sign-right"


[pro-merge] Add pusher extensions for travis-pro

--- assets/scripts/app/pusher.coffee
+++ assets/scripts/app/pusher.coffee
@@ -108,3 +108,73 @@
   ignoreMessage: (message) ->
     message.indexOf('Existing subscription') == 0 or message.indexOf('No current subscription') == 0

+pusher_host = $('meta[name="travis.pusher_host"]').attr('value')
+pusher_path = $('meta[name="travis.pusher_path"]').attr('value')
+
+Pusher.SockJSTransport.isSupported = -> false if pusher_host != 'ws.pusherapp.com'
+
+if Travis.config.pro
+  Pusher.channel_auth_transport = 'bulk_ajax'
+
+  Pusher.authorizers.bulk_ajax = (socketId, _callback) ->
+    channels = Travis.pusher.pusher.channels
+    channels.callbacks ||= []
+
+    name = this.channel.name
+    names = $.keys(channels.channels)
+
+    channels.callbacks.push (auths) ->
+      _callback(false, auth: auths[name])
+
+    unless channels.fetching
+      channels.fetching = true
+      Travis.ajax.post Pusher.channel_auth_endpoint, { socket_id: socketId, channels: names }, (data) ->
+        channels.fetching = false
+        callback(data.channels) for callback in channels.callbacks
+
+
+  Pusher.getDefaultStrategy = (config) ->
+    [
+      [":def", "ws_options", {
+        hostUnencrypted: config.wsHost + ":" + config.wsPort + (pusher_path && "/#{pusher_path}" || ''),
+        hostEncrypted: config.wsHost + ":" + config.wssPort + (pusher_path && "/#{pusher_path}" || '')
+        path: config.path
+      }],
+      [":def", "sockjs_options", {
+        hostUnencrypted: config.httpHost + ":" + config.httpPort,
+        hostEncrypted: config.httpHost + ":" + config.httpsPort
+      }],
+      [":def", "timeouts", {
+        loop: true,
+        timeout: 15000,
+        timeoutLimit: 60000
+      }],
+
+      [":def", "ws_manager", [":transport_manager", {
+        lives: 2,
+        minPingDelay: 10000,
+        maxPingDelay: config.activity_timeout
+      }]],
+
+      [":def_transport", "ws", "ws", 3, ":ws_options", ":ws_manager"],
+      [":def_transport", "flash", "flash", 2, ":ws_options", ":ws_manager"],
+      [":def_transport", "sockjs", "sockjs", 1, ":sockjs_options"],
+      [":def", "ws_loop", [":sequential", ":timeouts", ":ws"]],
+      [":def", "flash_loop", [":sequential", ":timeouts", ":flash"]],
+      [":def", "sockjs_loop", [":sequential", ":timeouts", ":sockjs"]],
+
+      [":def", "strategy",
+        [":cached", 1800000,
+          [":first_connected",
+            [":if", [":is_supported", ":ws"], [
+                ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":sockjs_loop"]]
+              ], [":if", [":is_supported", ":flash"], [
+                ":best_connected_ever", ":flash_loop", [":delayed", 2000, [":sockjs_loop"]]
+              ], [
+                ":sockjs_loop"
+              ]
+            ]]
+          ]
+        ]
+      ]
+    ]


Fix conventions.assets for stuff like styles/img. Closes gh-533.

--- src/fs_utils/asset.coffee
+++ src/fs_utils/asset.coffee
@@ -12,14 +12,12 @@
 #
 # Returns String.
 getAssetDirectory = (path, convention) ->
-  splitted = path.split(common.sep)
+  split = path.split('/')
   # Creates thing like this
   # 'app/', 'app/assets/', 'app/assets/thing/', 'app/assets/thing/thing2.html'
-  splitted
+  split
     .map (part, index) ->
-      previous = if index is 0 then '' else splitted[index - 1] + common.sep
-      current = part + common.sep
-      previous + current
+      split.slice(0, index).concat([part, '']).join(common.sep)
     .filter(convention)[0]

 # A static file that shall be copied to public directory.


KDTokenizedInputView: do not display token autocomplete menu if token input doesn't exist

--- components/inputs/KDTokenizedInputView.coffee
+++ components/inputs/KDTokenizedInputView.coffee
@@ -97,7 +97,7 @@
     @menu?.destroy()
     @blur()

-    return  unless data.length
+    return  unless @tokenInput and data.length

     pos       = @tokenInput.getBoundingClientRect()
     options.x = pos.left


Webserver: Fix webserver route for accessing logged out pages and account profile page

--- servers/lib/server/index.coffee
+++ servers/lib/server/index.coffee
@@ -511,7 +511,7 @@
 # Handles all internal pages
 # /USER || /SECTION || /GROUP[/SECTION] || /APP
 #
-app.all '/:name/:section?/:slug?*', (req, res, next)->
+app.all '/:name/:section?/:slug?', (req, res, next)->
   {JName, JGroup} = koding.models

   {params} = req


Nodejs Web Server: add ssotoken/create endpoint

--- servers/lib/server/index.coffee
+++ servers/lib/server/index.coffee
@@ -109,6 +109,7 @@
 app.post '/recaptcha'                            , require './handlers/recaptcha'
 app.get  '/-/presence/:service'                  , (req, res) -> res.status(200).end()
 app.get  '/-/api/user/:username/flags/:flag'     , require './handlers/flaguser'
+app.post '/-/api/ssotoken/create'                , require './handlers/createssotoken'
 app.get  '/-/image/cache'                        , require './image_cache'
 app.get  '/-/oauth/odesk/callback'               , require './odesk_callback'
 app.get  '/-/oauth/github/callback'              , require './github_callback'


change constructor return value handling

--- obj.coffee
+++ obj.coffee
@@ -9,7 +9,9 @@
     else
       # Create function which forwards to given constructor function (if given).
       F = if not constructor? then -> else
-        (args) -> (constructor.apply @, args) || @ # if constructor returns truthy value: use it instead of object
+        (args) ->
+          ret = constructor.apply @, args
+          if ret != undefined then ret else @ # if constructor returns not undefined value: use it instead of object
       # Set functions prototype field.
       F.prototype = prototype
       # Create function which creates a new object with the given prototype and initializes with the given constructor.


Uncomment some commented code on app.coffee related to /login redirects.

--- app/coffee/app.coffee
+++ app/coffee/app.coffee
@@ -69,7 +69,7 @@
     $routeProvider.when('/project/:pid/admin', {
         controller: "ProjectAdminController", templateUrl: "partials/project-admin.html"})

-    #$routeProvider.otherwise({redirectTo: '/login'})
+    $routeProvider.otherwise({redirectTo: '/login'})

     defaultHeaders =
         "Content-Type": "application/json",
@@ -88,7 +88,7 @@
                 return $q.reject(response)

     $provide.factory("authHttpIntercept", ["$q", "$location", authHttpIntercept])
-    # $httpProvider.responseInterceptors.push('authHttpIntercept')
+    $httpProvider.responseInterceptors.push('authHttpIntercept')

     apiUrls = {
         "auth": "/api/v1/auth"


:art: Remove Temp dev return

:art: Remove Temp dev return

--- lib/main.coffee
+++ lib/main.coffee
@@ -6,8 +6,6 @@
   activate:->
     @OpenLastProject = require('./open-last-project')
     @OpenLastProject.LoadProject() unless atom.project.getPath()
-    return ;
-    @OpenLastProject.LoadFiles()
     @Subscriptions.push atom.project.onDidChangePaths @OpenLastProject.Save
     @Subscriptions.push atom.workspace.onDidAddPaneItem @OpenLastProject.Save
     @Subscriptions.push atom.workspace.onDidDestroyPaneItem @OpenLastProject.Save


Fix statusbar interfering with settings view

and styleguide pane

--- lib/main.coffee
+++ lib/main.coffee
@@ -60,7 +60,7 @@
         @editor = atom.workspace.getActiveTextEditor()

         @subscriptions.add atom.workspace.observeActivePaneItem (item) =>
-            if item
+            if item and item is atom.workspace.getActiveTextEditor()
                 @editor = item
                 @setStatusBarElement()


Update upload project code to pick project based on open text editor

--- lib/main.coffee
+++ lib/main.coffee
@@ -56,7 +56,9 @@

   uploadProject: ->
     if atom.project.getPaths().length > 0
-      cli.uploadInTerminal '-f ' + atom.project.getPaths()[0]
+      cli.uploadInTerminal '-f "' + \
+        (atom.project.relativizePath(atom.workspace.getActiveTextEditor().getPath())[0] or \
+          atom.project.getPaths()[0]) + '"'

   newProject: ->
     @newProjectPanel.toggle()


Fix typo in @showWSKernelPicker

Fix typo in @showWSKernelPicker

--- lib/main.coffee
+++ lib/main.coffee
@@ -354,7 +354,7 @@
                 grammar = @editor.getGrammar()
                 @kernelManager.destroyRunningKernelFor grammar

-                @kernelManager.setRunningKernel grammar, kernel
+                @kernelManager.setRunningKernelFor grammar, kernel
                 @onKernelChanged kernel

         @wsKernelPicker.toggle()


Don't barf if the file is empty

Don't barf if the file is empty

--- lib/main.coffee
+++ lib/main.coffee
@@ -46,7 +46,7 @@
           arr
       else ''

-tagsArray = _(processStatements(result.statements)).flatten().join('').match(/<.+?>|{{.+?}}|[^<>{}]+/g);
+tagsArray = _(processStatements(result.statements)).flatten().join('').match(/<.+?>|{{.+?}}|[^<>{}]+/g) || []
 _(tagsArray).reject((t,i,ctx) -> t == '{{else}}' && _(['{{/if}}', '{{/unless}}']).include(ctx[i+1]))

 indent = -1


Don’t check min build target if no device is selected

--- lib/main.coffee
+++ lib/main.coffee
@@ -265,7 +265,7 @@

   # "Decorator" which tests if we're using at least 0.5.3 when compiling with libraries
   minBuildTargetRequired: (callback) ->
-    if @isProject() or @isLibrary()
+    if (@isProject() or @isLibrary()) and @profileManager.hasCurrentDevice
       semver ?= require 'semver'
       defaultBuildTarget = @profileManager.currentDevice.defaultBuildTarget
       if defaultBuildTarget && semver.lt(defaultBuildTarget, '0.5.3')


Removed logs

Removed logs

--- lib/main.coffee
+++ lib/main.coffee
@@ -22,9 +22,7 @@
   activate: (state) ->
     reload = false
     if state
-      console.log "state"
       referencesFiles = atom.config.get "autocomplete-citeproc.references"
-      console.log referencesFiles
       if not Array.isArray(referencesFiles)
         # TODO remove this bloc after testing
         referencesFiles = [referencesFiles]


Add description for config setting

Add description for config setting

--- lib/main.coffee
+++ lib/main.coffee
@@ -5,6 +5,7 @@
     includeLineNumbersInUrls:
       default: true
       type: 'boolean'
+      description: 'Include the line range selected in the editor when opening or copying URLs to the clipboard. When opened in the browser, the GitHub page will automatically scroll to the selected line range.'

   activate: ->
     atom.commands.add 'atom-pane',


Only fix PATH on macOS

Only fix PATH on macOS

--- lib/main.coffee
+++ lib/main.coffee
@@ -2,6 +2,7 @@
 fs = null
 glob = null
 path = null
+os = null
 CompositeDisposable = null

 DockerManager = null
@@ -106,9 +107,11 @@
 		@core

 	setupDocker: ->
+		os ?= require 'os'
 		# Fix for "Unable to connect to Docker" error
-		childProcess = require 'child_process'
-		process.env.PATH = childProcess.execFileSync(process.env.SHELL, ['-i', '-c', 'echo $PATH']).toString().trim()
+		if os.type() == "Darwin"
+			childProcess = require 'child_process'
+			process.env.PATH = childProcess.execFileSync(process.env.SHELL, ['-i', '-c', 'echo $PATH']).toString().trim()

 		@dockerManager = null


No longer use throttle #3

No longer use throttle #3

For better understanding of problem again.

--- lib/main.coffee
+++ lib/main.coffee
@@ -63,7 +63,7 @@
                 newPoint = editor.getCursorBufferPosition()
                 console.log "move from #{point.toString()} -> #{newPoint.toString()}"
                 if @needRemember(point, newPoint, null)
-                  @saveHistory editor, point, editor.getURI(), dumpMessage: "[Cursor moved] save history"
+                  @history.add editor, point, editor.getURI(), dumpMessage: "[Cursor moved] save history"
                 @unLock()
                 console.log "hidden #{itemKind}"
               , 300
@@ -116,7 +116,7 @@

     {editor, point, URI: lastURI} = @lastEditor.getInfo()
     if not @isLocked() and (lastURI isnt item.getURI())
-      @saveHistory editor, point, lastURI, dumpMessage: "[Pane item changed] save history"
+      @history.add editor, point, lastURI, dumpMessage: "[Pane item changed] save history"

     @lastEditor.set item
     @debug "set LastEditor #{path.basename(item.getURI())}"
@@ -128,7 +128,7 @@
     return unless URI = editor.getURI()

     if @needRemember(oldBufferPosition, newBufferPosition, cursor)
-      @saveHistory editor, oldBufferPosition, URI, dumpMessage: "[Cursor moved] save history"
+      @history.add editor, oldBufferPosition, URI, dumpMessage: "[Cursor moved] save history"

   # Throttoling save to history once per 500ms.
   # When activePaneItem change and cursorMove event happened almost at once.
@@ -137,9 +137,9 @@
   # Ignoring tail call mean ignoring cursor move happen just after pane change.
   # This is mainly for saving only target position on `symbols-view:goto-declaration` and
   # ignoring relaying position(first line of file of target position.)
-  saveHistory: (editor, point, URI, options)->
-    @_saveHistory ?= _.throttle(@history.add.bind(@history), 500, trailing: false)
-    @_saveHistory editor, point, URI, options
+  # saveHistory: (editor, point, URI, options)->
+  #   @_saveHistory ?= _.throttle(@history.add.bind(@history), 500, trailing: false)
+  #   @_saveHistory editor, point, URI, options

   needRemember: (oldBufferPosition, newBufferPosition, cursor) ->
     # Line number delata exceeds or not.


:art: very minor refactoring as preparation before big change.

--- lib/main.coffee
+++ lib/main.coffee
@@ -60,8 +60,11 @@
     scope = 'atom-text-editor:not([mini])'
     for name, fn of vimStateCommands
       do (fn) =>
-        @subscribe atom.commands.add scope, "#{packageScope}:#{name}", (event) ->
+        @addCommand commandScope, name, (event) ->
           fn.bind(getState())(event)
+
+  addCommand: (scope, name, fn) ->
+    @subscribe atom.commands.add scope, "#{packageScope}:#{name}", fn

   registerViewProviders: ->
     atom.views.addViewProvider Hover, (model) ->


only one underscore

only one underscore

--- lib/middleware/proxy_to_reflection.coffee
+++ lib/middleware/proxy_to_reflection.coffee
@@ -1,5 +1,5 @@
 #
-# When Google requests __escaped_fragement__ proxy to Reflection
+# When Google requests _escaped_fragement_ proxy to Reflection
 # https://github.com/artsy/reflection
 #


code readability fix

code readability fix

--- client/app/Applications/Ace.kdapplication/AppController.coffee
+++ client/app/Applications/Ace.kdapplication/AppController.coffee
@@ -1,8 +1,9 @@
 class AceAppController extends AppController

-  compileCondition = (view)->
-    ace = view.getActiveAceView()
-    KodingAppsController.getManifestFromPath ace.getData().path
+  canCompile = (view)->
+    ace      = view.getActiveAceView()
+    manifest = KodingAppsController.getManifestFromPath ace.getData().path
+    return if manifest then yes else no

   KD.registerAppClass this,
     name          : "Ace"


rollbar: add concerns; mixpanel: minor fix

--- client/finder/filetree/controllers/findertreecontroller.coffee
+++ client/finder/filetree/controllers/findertreecontroller.coffee
@@ -140,10 +140,10 @@
       unless silence
         if err?.message?.match /permission denied/i
           message = "Permission denied!"
-          KD.logToExternal "Couldn't fetch files, permission denied"
+          KD.logToExternal "filetree: Couldn't fetch files, permission denied"
         else
           message = "Couldn't fetch files! Click to retry"
-          KD.logToExternalWithTime "Couldn't fetch files"
+          KD.logToExternalWithTime "filetree: Couldn't fetch files"
         @notify message, 'clickable', \
                 """Sorry, a problem occured while communicating with servers,
                    please try again later.""", yes


showloader on search

showloader on search

--- client/ide/lib/views/contentsearch/idecontentsearchview.coffee
+++ client/ide/lib/views/contentsearch/idecontentsearchview.coffee
@@ -1,7 +1,6 @@
 kd                          = require 'kd'
 KDButtonView                = kd.ButtonView
 KDCustomHTMLView            = kd.CustomHTMLView
-KDInputView                 = kd.InputView
 KDHitEnterInputView         = kd.HitEnterInputView
 KDMultipleChoice            = kd.MultipleChoice
 JView                       = require 'app/jview'
@@ -90,6 +89,7 @@

     return @searchButton.hideLoader()  if searchText is ''

+    @searchButton.showLoader()
     @searchText     = searchText
     @rootPath       = Encoder.XSSEncode @whereInput.getValue()
     @selections     = @choices.getValue()
@@ -141,50 +141,11 @@
     @localStorageController.setValue 'PreviousSearchTerms', terms

-  createElements: ->
-
-    @findInput = new KDHitEnterInputView
-      type         : 'text'
-      cssClass     : 'search-input-with-icon'
-      placeholder  : 'Search in all files...'
-      validate     :
-        rules      :
-          required : yes
-      keydown      : _.bind @handleKeyDown, this
-      callback     : @bound 'search'
-
-    @whereInput = new KDInputView
-      type         : 'text'
-      cssClass     : 'search-input'
-      placeholder  : @rootPath
-      defaultValue : @rootPath
-      validate     :
-        rules      :
-          required : yes
-      keydown      : _.bind @handleKeyDown, this
-
-    @searchButton = new KDButtonView
-      title        : 'Search'
-      cssClass     : 'search-button'
-      loader       :
-        color      : '#FFFFFF'
-      callback     : @bound 'search'
-
-    @closeButton = new KDCustomHTMLView
-      tagName      : 'span'
-      cssClass     : 'close-icon'
-      click        : @bound 'destroy'
-
-    @choices = new KDMultipleChoice
-      cssClass     : ''
-      labels       : ['case-sensitive', 'whole-word', 'regex']
-      multiple     : yes
-      defaultValue : 'fakeValueToDeselectFirstOne'
-
-
   escapeRegExp: (str) -> str.replace /([.*+?\^${}()|\[\]\/\\])/g, '\\$1'

+
   escapeShell: (str) -> str.replace /([\\"'`$\s\(\)<>])/g, '\\$1'
+

   grepEscapeRegExp: (str) -> str.replace /[[\]{}()*+?.,\\^$|#\s"']/g, '\\$&'

@@ -260,11 +221,54 @@

     @searchButton.hideLoader()

+  createElements: ->
+
+    @findInput = new KDHitEnterInputView
+      type         : 'text'
+      cssClass     : 'search-input-with-icon'
+      placeholder  : 'Search in all files...'
+      validate     :
+        rules      :
+          required : yes
+      keydown      : _.bind @handleKeyDown, this
+      callback     : @bound 'search'
+
+    @whereInput = new KDHitEnterInputView
+      type         : 'text'
+      cssClass     : 'search-input'
+      placeholder  : @rootPath
+      defaultValue : @rootPath
+      validate     :
+        rules      :
+          required : yes
+      keydown      : _.bind @handleKeyDown, this
+      callback     : @bound 'search'
+
+    @searchButton = new KDButtonView
+      title        : 'Search'
+      cssClass     : 'search-button'
+      loader       :
+        color      : '#FFFFFF'
+      callback     : @bound 'search'
+
+    @closeButton = new KDCustomHTMLView
+      tagName      : 'span'
+      cssClass     : 'close-icon'
+      click        : @bound 'destroy'
+
+    @choices = new KDMultipleChoice
+      cssClass     : ''
+      labels       : ['case-sensitive', 'whole-word', 'regex']
+      multiple     : yes
+      defaultValue : 'fakeValueToDeselectFirstOne'
+
+
   viewAppended: ->

     super

     @findInput.setFocus()
+

   pistachio: ->
     return '''


fix @mget/@get mixup

fix @mget/@get mixup

--- bokehjs/src/coffee/mapper/color/linear_color_mapper.coffee
+++ bokehjs/src/coffee/mapper/color/linear_color_mapper.coffee
@@ -9,15 +9,15 @@

     initialize: (attrs, options) ->
       super(attrs, options)
-      @palette       = @_build_palette(@mget('palette'))
+      @palette       = @_build_palette(@get('palette'))
       @little_endian = @_is_little_endian()

     v_map_screen: (data) ->
       buf = new ArrayBuffer(data.length * 4);
       color = new Uint32Array(buf);

-      low = @mget('low') ? _.min(data)
-      high = @mget('high') ? _.max(data)
+      low = @get('low') ? _.min(data)
+      high = @get('high') ? _.max(data)

       N = @palette.length - 1
       scale = N/(high-low)


* attributeModifiers receive object

* attributeModifiers receive object

--- app/assets/javascripts/backbone.rails_store.js.coffee
+++ app/assets/javascripts/backbone.rails_store.js.coffee
@@ -1060,7 +1060,7 @@
         return modifier[1] if @attributes[attr]
         return modifier[0] if not @attributes[attr] or @attributes[attr] == 'false'
       else if _.isFunction(@attributeModifiers[attr].getConverter)
-        return @attributeModifiers[attr].getConverter(@attributes[attr])
+        return @attributeModifiers[attr].getConverter(@attributes[attr],@)

     # If we have, deliver
     val = super
@@ -1373,7 +1373,7 @@
           return 1 if value
           return 0 if not value or value == 'false'
         else if _.isFunction(@attributeModifiers[key].getConverter)
-          attr[key] = @attributeModifiers[key].getConverter(value)
+          attr[key] = @attributeModifiers[key].getConverter(value,@)
     return attr

 ###


Map a whole store directly to a state key

--- app/data/store.coffee
+++ app/data/store.coffee
@@ -31,8 +31,14 @@
     (stateProperties) ->
       getCurrentState = ->
         state = {}
-        for storeProperty, stateProperty of stateProperties
-          state[stateProperty] = store[storeProperty]
+
+        if typeof stateProperties is 'string'
+          state[stateProperties] = store
+
+        else
+          for storeProperty, stateProperty of stateProperties
+            state[stateProperty] = store[storeProperty]
+
         state

       updateState = ->


Resize the input_field

Resize the input_field

--- app/assets/javascripts/lunar_shell/lunar_shell.coffee
+++ app/assets/javascripts/lunar_shell/lunar_shell.coffee
@@ -10,10 +10,16 @@
       $('#shell-cli .prompt').clone().appendTo('#shell-history .input:last')
       $('.input').last().append input

+set_input_field_width = ->
+  body_w = $('body').width()
+  prompt_w = $('#shell-cli label').width()
+  $('.input_field').width(body_w - prompt_w - 50)
+
 $ ->
   # Set on page load
   focus_cli()
   cli_submit()
+  set_input_field_width()

   # Focus when clicking anywhere on the page
   $(document).click ->
@@ -23,3 +29,7 @@
 $(document).ajaxComplete ->
   focus_cli()
   cli_submit()
+  set_input_field_width()
+
+$(window).on 'resize', ->
+  set_input_field_width()


sum fix: accumlator/sums works with strings also

--- app/assets/javascripts/pinkman_base/collection.coffee
+++ app/assets/javascripts/pinkman_base/collection.coffee
@@ -28,23 +28,26 @@
   # Desc: sum a certain attribute of every element
   sum: (attr,callback) ->
     if attr?
+      v = if @any() then '___initial___' else 0
       if $p.isString(attr)
-        v = 0
         @each (obj) ->
           if Pinkman.isNumber(obj[attr])
+            v = 0 if v=='___initial___'
             v = Number(obj[attr]) + v
-          else if obj[attr]?
+          else
+            v = '' if v=='___initial___'
             v = obj[attr] + v
         , =>
           callback(this, v) if typeof callback == 'function'
         return(v)
       else if $p.isFunction(attr)
-        v = 0
         @each (obj) ->
           current = attr(obj)
           if $p.isNumber(current)
+            v = 0 if v=='___initial___'
             v = Number(current) + v
           else
+            v = '' if v=='___initial___'
             v = current + v
         , =>
           callback(this, v) if typeof callback == 'function'


fix up css requesting and add DEBUG

fix up css requesting and add DEBUG

--- node.js/server.coffee
+++ node.js/server.coffee
@@ -1,6 +1,8 @@
 http = require 'http'
 fs = require 'fs'
 qs = require 'querystring'
+
+DEBUG = 0

 port = 8888

@@ -12,9 +14,13 @@
   index: {
       content_type: 'text/html', encoding: 'ascii', dir: '../server/views/static.html'
     },
-  css: {
+  css: {  #this one is complicated - there's css from jquery, the view css, and then (probably) css for plugins - so I'll make the views css a special case in code below :(
+      #content_type: 'text/css', encoding: 'ascii', dir: '../server/views/'
+      content_type: 'text/css', encoding: 'ascii', dir: '../client/'
+    },
+  stylecss: {
       content_type: 'text/css', encoding: 'ascii', dir: '../server/views/'
-    },
+    }
   js: {
       content_type: 'text/javascript', encoding: 'ascii', dir: '../client/'
     },
@@ -28,6 +34,7 @@

 process.serve_url = (req, res) ->
     file = req.url[1..]
+    console.log req.method+' '+req.url+': '+file if DEBUG

     if req.method == 'PUT'
        #nasty hack, as we're only putting pages atm, and the URI's are :(
@@ -37,11 +44,11 @@
        req.on 'data', (data) -> body +=data;
        req.on 'end', ->
          POST =  qs.parse(body, true);
-         console.log(POST);
+         console.log(POST) if DEBUG

     if file == ''
-      console.log 'getting / == welcome-visitors.html topic'
+      console.log '    getting / == welcome-visitors.html topic' if DEBUG
       file = 'welcome-visitors.html'

     #TODO: use require('url').parse(request.url, true) to parse the url
@@ -51,12 +58,15 @@
     file = file.replace(/\?.*$/i, '')     #remove ?random= - or other urlparams

     file_extension = file.match(/\.([^\.]*)$/i)
+    if file == 'style.css'
+      file_extension = ['stylecss', 'stylecss']
     if file_extension && file_extension[0]
       file_extension = file_extension[1].toLowerCase()
     else
       file_extension = 'json'
-
+
     if ! filetype[file_extension]
+      console.log '    defaulting: ' + file_extension if DEBUG
       file_extension = 'index'
       file = ''
       #TODO: modify the index.html page div id (I think I'll get client.coffee to update it)
@@ -68,7 +78,7 @@
     #and here's a pointless exception to the filename.extension :(
     filePath = filePath.replace(/\.json/, '') if file_extension == 'json'

-    console.log req.method+' '+req.url+': '+filePath
+    console.log '    serverfile: '+filePath if DEBUG

     #fulfil the request
     #read
@@ -80,7 +90,8 @@
         else
           status = 404
         data = ""
-      console.log 'status: '+ status
+      console.log '    filePath: ' + filePath if DEBUG
+      console.log '    status: '+ status if DEBUG
       #console.log ' contentType: '+ contentType
       if status is 200
           res.writeHead 200,
@@ -99,6 +110,7 @@
         res.writeHead status

       res.end()
+      console.log 'DONE: ' + filePath if DEBUG
 #done.

 if process.myserver


Fixes console inconsistency for IE

Fixes console inconsistency for IE

--- src/bootstrap.coffee
+++ src/bootstrap.coffee
@@ -12,6 +12,16 @@
 # The global code only defines some methods into window.Hull and pools the parameters
 # to replay them if the app has loaded correctly
 #
+
+unless window.console and console.log
+  (->
+    noop = ->
+
+    methods = ["assert", "clear", "count", "debug", "dir", "dirxml", "error", "exception", "group", "groupCollapsed", "groupEnd", "info", "log", "markTimeline", "profile", "profileEnd", "markTimeline", "table", "time", "timeEnd", "timeStamp", "trace", "warn"]
+    length = methods.length
+    console = window.console = {}
+    console[methods[length]] = noop  while length--
+  )()

 #


Add a template-only snippet

Add a template-only snippet

It seems that most template-only elements hook into <content>, so why
not provide the styling scaffold as well?

--- snippets/polymer-element.cson
+++ snippets/polymer-element.cson
@@ -51,3 +51,25 @@
       </script>
     </dom-module>
     """
+  "polymer element template only":
+    "prefix": "pet"
+    "body": """
+    <link rel="import" href="${1:../bower_components}/polymer/polymer.html">
+
+    <dom-module id="$2">
+      <style>
+        :host {
+          $4
+        }
+        .content-wrapper > ::content {
+          $5
+        }
+      </style>
+      <template>
+        $3
+        <div class="content-wrapper">
+          <content></content>
+        </div>
+      </template>
+    </dom-module>
+    """


Correct the url.

Correct the url.

--- test/e2e/collections.spec.coffee
+++ test/e2e/collections.spec.coffee
@@ -12,7 +12,7 @@
     # Call the Page superclass initializer with the helpShown
     # flag set to true, since the help box is displayed on
     # this landing page.
-    super('/quip?project=QIN_Test',  true)
+    super('/qiprofile/QIN_Test',  true)

   # @returns the collection {name, description, url} object
   #   array promise


FinderController: Make storing mountedVM states in appStorage synchronous, no need to be asynchronous since it has race-condition there.

--- client/Finder/filetree/controllers/findercontroller.coffee
+++ client/Finder/filetree/controllers/findercontroller.coffee
@@ -25,7 +25,7 @@
     TreeControllerClass = options.treeControllerClass or NFinderTreeController
     @treeController     = new TreeControllerClass treeOptions, []

-    @appStorage = KD.getSingleton('appStorageController').storage 'Finder', '1.1.1'
+    @appStorage = KD.getSingleton('appStorageController').storage 'Finder', '1.1.2'

     @watchers = {}

@@ -102,15 +102,14 @@
     return  if KD.isGuest()
     groupSlug  = KD.getSingleton("groupsController").getGroupSlug()
     groupSlug ?= KD.defaultSlug
-    @appStorage.fetchValue "mountedVM", (vms)=>
-      vms or= {}
-      vms[groupSlug] or= []
-      items = vms[groupSlug]
-      if state and vmName not in items
-        items.push vmName
-      else if not state and vmName in items
-        items.splice items.indexOf(vmName), 1
-      @appStorage.setValue "mountedVM", vms
+    vms = @appStorage.getValue("mountedVM") or {}
+    vms[groupSlug] or= []
+    items = vms[groupSlug]
+    if state and vmName not in items
+      items.push vmName
+    else if not state and vmName in items
+      items.splice items.indexOf(vmName), 1
+    @appStorage.setValue "mountedVM", vms

   checkVMState: (err, vm, info)->
     return warn err if err or not info


Tests: Added testpath.

Tests: Added testpath.

--- client/Main/avatararea/avatarareagroupswitcherpopup.coffee
+++ client/Main/avatararea/avatarareagroupswitcherpopup.coffee
@@ -192,7 +192,9 @@

     @avatarPopupContent.addSubView new KDCustomHTMLView
       tagName    : 'a'
-      attributes : href : '/Logout'
+      attributes :
+        href     : '/Logout'
+        testpath : 'logout-link'
       partial    : 'Logout'
       click      : (event)=>
         KD.utils.stopDOMEvent event


Trigger 'navigate' on calls to navigate

Trigger 'navigate' on calls to navigate

--- src/scripts/router.coffee
+++ src/scripts/router.coffee
@@ -20,6 +20,10 @@
       @route /^search/, 'search', () ->
         @appView.render('search')

+    navigate: () ->
+      super(arguments...)
+      @trigger('navigate')
+
     # Helper function to determine the current route's parameters
     current: () ->
       routes = []


contentsearch: handle keyup manually

contentsearch: handle keyup manually

--- client/ide/lib/views/contentsearch/idecontentsearch.coffee
+++ client/ide/lib/views/contentsearch/idecontentsearch.coffee
@@ -1,12 +1,14 @@
-kd = require 'kd'
-KDCustomHTMLView = kd.CustomHTMLView
-KDModalViewWithForms = kd.ModalViewWithForms
-KDView = kd.View
-nick = require 'app/util/nick'
-KodingSwitch = require 'app/commonviews/kodingswitch'
+kd                         = require 'kd'
+KDCustomHTMLView           = kd.CustomHTMLView
+KDModalViewWithForms       = kd.ModalViewWithForms
+KDView                     = kd.View
+nick                       = require 'app/util/nick'
+KodingSwitch               = require 'app/commonviews/kodingswitch'
 IDEContentSearchResultView = require './idecontentsearchresultview'
-Encoder = require 'htmlencode'
-editorSettings = require '../../workspace/panes/settings/editorsettings'
+Encoder                    = require 'htmlencode'
+editorSettings             = require '../../workspace/panes/settings/editorsettings'
+_                          = require 'lodash'
+keycode                    = require 'keycode'

 module.exports = class IDEContentSearch extends KDModalViewWithForms
@@ -36,15 +38,13 @@
               type          : 'text'
               label         : 'Find'
               placeholder   : 'Find'
-              keyup         :
-                esc         : @bound 'destroy'
+              keyup         : _.bind @handleKeyUp, this
             whereInput      :
               type          : 'text'
               label         : 'Where'
               placeholder   : "/home/#{nick()}"
               defaultValue  : "/home/#{nick()}"
-              keyup         :
-                esc         : @bound 'destroy'
+              keyup         : _.bind @handleKeyUp, this
             caseToggle      :
               label         : 'Case Sensitive'
               itemClass     : KodingSwitch
@@ -66,6 +66,15 @@

     super options, data

+
+  handleKeyUp: (e) ->
+
+    code = e.which or e.keyCode
+    key  = keycode code
+
+    @destroy()  if key is 'esc'
+
+
   search: ->
     @warningView.hide()


add option to rename file

add option to rename file

--- lib/atomizr.coffee
+++ lib/atomizr.coffee
@@ -1,12 +1,20 @@
 {CompositeDisposable} = require 'atom'

 # 3rd-party dependencies
+fs = require 'fs'
+path = require 'path'
 CSON = require 'cson'
 parseCson = require 'cson-parser'
 parseJson = require 'parse-json'
 {parseString} = require 'xml2js'

 module.exports = Atomizr =
+  config:
+    renameFiles:
+      title: "Rename Files"
+      description: "This will rename the file after the conversion"
+      type: "boolean"
+      default: true
   atom: atom
   workspace: atom.workspace
   grammars: atom.grammars
@@ -25,7 +33,6 @@
     "source.markdown": ".source.gfm"

   activate: (state) ->
-
     # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
     @subscriptions = new CompositeDisposable

@@ -108,6 +115,14 @@
     # Write back to editor and change scope
     editor.setText(json)
     editor.setGrammar(@grammars.grammarForScopeName('source.json.subl'))
+    @renameFile(editor, "sublime-completions")
+
+    # rename file
+    inputFile = editor.getPath()
+    parentDir = path.dirname(inputFile)
+    baseName = path.basename(inputFile, path.extname(inputFile))
+    outputFile =  path.join(parentDir, baseName + ".sublime-completions")
+    fs.rename(inputFile, outputFile)

   # Convert Sublime Text completion into Atom snippet
   sublToAtom: ->
@@ -257,6 +272,7 @@
     # Write back to editor and change scope
     editor.setText(output)
     editor.setGrammar(@grammars.grammarForScopeName('source.json'))
+    @renameFile(editor, "json")

   jsonToCson: ->
     editor = @workspace.getActiveTextEditor()
@@ -296,16 +312,24 @@
     return "#{input}$#{highest}"

   removeTrailingTabstops: (input) ->
-
     if input.match(/\$\d+$/g) is null
       # nothing to do here
       return input

     return input.replace(/\$\d+$/g, "")

+  renameFile: (editor, extension) ->
+    if @atom.config.get('atomizr.renameFiles')
+      inputFile = editor.getPath()
+      parentDir = path.dirname inputFile
+      baseName = path.basename inputFile, path.extname inputFile
+      outputFile = path.join parentDir, baseName + ".#{extension}"
+      fs.rename inputFile, outputFile
+
   makeCoffee: (editor, input) ->
     output = CSON.createCSONString(input)

     # Write back to editor and change scope
     editor.setText("# #{@meta}\n#{output}")
     editor.setGrammar(@grammars.grammarForScopeName('source.coffee'))
+    @renameFile(editor, "cson")


checkpoint for broken code debugging

checkpoint for broken code debugging

--- src/smc-webapp/profile.cjsx
+++ src/smc-webapp/profile.cjsx
@@ -180,34 +180,34 @@

                 if p.project_id == @props.project_id
                     for user in p.users
-                        console.log(JSON.stringify(p.path), JSON.stringify(user))
-                        for k in ['open', 'edit', 'chat']
-                            console.log(k, user[k])
-                            if user[k]
-                                tm = (user[k] ? 0) - 0
-                                if tm > newest
-                                    latest_key = k
-                                    newest     = tm
-                        last_seen = newest/1000
+                        console.log('user', JSON.stringify(p), user)
+                        [event, most_recent] = @_find_most_recent(user)
                         if user.account_id in users
-
-                            if last_seen > users[user.account_id]['last_seen']
-                                users[user.account_id] = {'last_seen': last_seen, 'path': p.path}
+                            if most_recent > users[user.account_id]['most_recent']
+                                users[user.account_id] = user
+                                users[user.account_id]['most_recent'] = most_recent
+                                users[user.account_id]['path'] = p.path
                         else
-                            users[user.account_id] = {'last_seen': last_seen, 'path': p.path}
-            console.log('users', JSON.stringify(users))
+                            users[user.account_id] = user
+                            users[user.account_id]['most_recent'] = most_recent
+                            users[user.account_id]['path'] = p.path
+            console.log('ALL THE USERS', JSON.stringify(users))
             for user_id, info of users
                 if user_id == @props.account_id
                     continue
                 account = @props.user_map.get(user_id)?.toJS() ? {}
-
-                seconds = info.last_seen
+                events = []
+                for event of events
+                    if event.account_id == user_id
+                        events.push(event)
+                seconds = info.most_recent
+                console.log(JSON.stringify(@props.user_map.get(user_id)?.toJS()), salvus_client.server_time()/1000, info.path, seconds,salvus_client.server_time()/1000 - seconds)
                 time_since =  salvus_client.server_time()/1000 - seconds

                 # TODO do something with the type like show a small typing picture
                 # or whatever corresponds to the action like "open" or "edit"
                 style = {opacity:Math.max(1 - time_since/seconds_for_user_to_disappear, 0)}
-                console.log(salvus_client.server_time()/1000, seconds, time_since, Math.max(1 - time_since/seconds_for_user_to_disappear, 0))
+
                 # style = {opacity:1}  # used for debugging only -- makes them not fade after a few minutes...
                 if time_since < seconds_for_user_to_disappear # or true  # debugging -- to make everybody appear
                     all_users.push <Avatar viewing_what='project' key={user_id} account={account} path={info.path} />


avoid error when passing as limit in mongo query

convert request parameter from string to number

--- services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee
+++ services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee
@@ -6,7 +6,7 @@

 	deactivateOldProjects: (req, res)->
 		logger.log "recived request to deactivate old projects"
-		numberOfProjectsToArchive = req.body.numberOfProjectsToArchive
+		numberOfProjectsToArchive = parseInt(req.body.numberOfProjectsToArchive, 10)
 		ageOfProjects = req.body.ageOfProjects
 		InactiveProjectManager.deactivateOldProjects numberOfProjectsToArchive, ageOfProjects, (err, projectsDeactivated)->
 			if err?


add support for zero always being a nice number

--- static/coffee/common/ticking.coffee
+++ static/coffee/common/ticking.coffee
@@ -103,7 +103,7 @@
   return ret_arr

-auto_ticks = (data_low, data_high, bound_low, bound_high, tick_interval, use_endpoints=false) ->
+auto_ticks = (data_low, data_high, bound_low, bound_high, tick_interval, use_endpoints=false, zero_always_nice=true) ->
     """ Finds locations for axis tick marks.

         Calculates the locations for tick marks on an axis. The *bound_low*,
@@ -133,6 +133,9 @@
             If True, the lower and upper bounds of the data are used as the
             lower and upper end points of the axis. If False, the end points
             might not fall exactly on the bounds.
+        zero_always_nice : Boolean
+            If True, ticks much closer to zero than the tick interval will be
+            coerced to have a value of zero

         Returns
         -------
@@ -204,6 +207,11 @@
         end += tick_interval
     ticks = arange( start, end + (tick_interval / 2.0), tick_interval )

+    if zero_always_nice
+        for i in [0..ticks.length-1]
+            if Math.abs(ticks[i]) < tick_interval/1000
+                ticks[i] = 0
+
     # FIXME
     # if len( ticks ) < 2
     #  ticks = array( ( ( lower - lower * 1.0e-7 ), lower ) )


Fix race condition crash

Fix race condition crash

--- src/server/session.coffee
+++ src/server/session.coffee
@@ -158,7 +158,7 @@

       # Tell the socket the doc is open at the requested version
       agent.listen docName, version, listener, (error, v) ->
-        delete docState[docName].listener if error
+        delete docState[docName].listener if error and docState
         callback error, v

     # Close the named document.


Added possibility to set background. Fixed #33

--- src/chocolate.coffee
+++ src/chocolate.coffee
@@ -88,6 +88,8 @@

     getEnv()
     translate @slider, env.shift * item.cid
+    if item.bg
+      @overlay.style.background = item.bg

     if @options.thumbnails
       if @current?
@@ -140,6 +142,7 @@
           orig: getAttribute(image, 'data-src') or getAttribute(image.parentNode, 'href')
           title: getAttribute(image, 'data-title') or getAttribute(image, 'title') or getAttribute(image, 'alt') or getAttribute(image.parentNode, 'title')
           thumb: getAttribute(image, 'src')
+          bg: getAttribute(image, 'data-background')

       addImage @, object, image


debugs

debugs

--- src/node_modules/bucket-item/bucket-item-link.coffee
+++ src/node_modules/bucket-item/bucket-item-link.coffee
@@ -1,5 +1,6 @@
 _ = require('lodash')
 Decimal = require('big.js')
+debug = require('debug')("bucket-item")

 ### @ngInject ###
 module.exports = (scope, element, attrs) ->
@@ -50,6 +51,7 @@
     startContributing:
       value: (ev) ->
         scope.model.inputStatus = "unsaved"
+        debug("unsaved")
         # TODO hack
         window.setTimeout ->
           element.find('input[type="number"]').focus()
@@ -58,21 +60,26 @@
       value: (myContribution) ->
         # if contribution is empty and new, go back to ready
         if not myContribution.amount.gt(0) and not myContribution.id
+          debug("ready")
           scope.model.inputStatus = "ready"
           return

         scope.model.inputStatus = "saving"
+        debug("saving")
         scope.saveContribution(myContribution).then ->
           if myContribution.amount.eq(0)
             scope.model.inputStatus = "ready"
+            debug("ready")
           else
             scope.model.inputStatus = "saved"
+            debug("saved")
         , (err) ->
           scope.model.inputStatus = "error"
+          debug("error", err)
           # TODO handle error

   if bucket.myContribution.amount > 0 or scope.model.readonly
-    scope.model.inputStatus = 'saved'
+    scope.model.inputStatus = "saved"
   else
     scope.model.inputStatus = "ready"


resolving long line issues

resolving long line issues

--- bokehjs/src/coffee/renderer/tile/tile_renderer.coffee
+++ bokehjs/src/coffee/renderer/tile/tile_renderer.coffee
@@ -136,15 +136,18 @@
     @map_canvas.restore()

   _prefetch_tiles: () =>
+    tile_source = @mget('tile_source')
     extent = @get_extent()
-    zoom_level = @mget('tile_source').get_level_by_extent(extent, @map_frame.get('height'), @map_frame.get('width'))
+    h = @map_frame.get('height')
+    w = @map_frame.get('width')
+    zoom_level = @mget('tile_source').get_level_by_extent(extent, h, w)
     tiles = @mget('tile_source').get_tiles_by_extent(extent, zoom_level)
     for t in [0..Math.min(10, tiles.length)] by 1
       [x, y, z, bounds] = t
       children = @mget('tile_source').children_by_tile_xyz(x, y, z)
       for c in children
         [cx, cy, cz, cbounds] = c
-        if @mget('tile_source').tile_xyz_to_key(cx, cy, cz) of @mget('tile_source').tiles
+        if tile_source.tile_xyz_to_key(cx, cy, cz) of tile_source.tiles
           continue
         else
           @_create_tile(cx, cy, cz, cbounds, true)
@@ -155,15 +158,19 @@
       @_create_tile(x, y, z, bounds)

   _update: () =>
-    min_zoom = @mget('tile_source').get('min_zoom')
-    max_zoom = @mget('tile_source').get('max_zoom')
-
-    @mget('tile_source').update()
+    tile_source = @mget('tile_source')
+
+    min_zoom = tile_source.get('min_zoom')
+    max_zoom = tile_source.get('max_zoom')
+
+    tile_source.update()
     extent = @get_extent()
     zooming_out = @extent[2] - @extent[0] < extent[2] - extent[0]
-
-    zoom_level = @mget('tile_source').get_level_by_extent(extent, @map_frame.get('height'), @map_frame.get('width'))
+    h = @map_frame.get('height')
+    w = @map_frame.get('width')
+    zoom_level = tile_source.get_level_by_extent(extent, h, w)
     snap_back = false
+
     if zoom_level < min_zoom
       extent = @extent
       zoom_level = min_zoom
@@ -181,8 +188,7 @@
       @extent = extent

     @extent = extent
-    tiles = @mget('tile_source').get_tiles_by_extent(extent, zoom_level)
-
+    tiles = tile_source.get_tiles_by_extent(extent, zoom_level)
     parents = []
     need_load = []
     cached = []
@@ -191,25 +197,23 @@
     for t in tiles
       [x, y, z, bounds] = t
       if @_is_valid_tile(x, y, z)
-        key = @mget('tile_source').tile_xyz_to_key(x, y, z)
-        tile = @mget('tile_source').tiles[key]
+        key = tile_source.tile_xyz_to_key(x, y, z)
+        tile = tile_source.tiles[key]
         if tile? and tile.loaded == true
           cached.push(key)
-
         else
-          [px, py, pz] = @mget('tile_source').get_closest_parent_by_tile_xyz(x, y, z)
-          parent_key = @mget('tile_source').tile_xyz_to_key(px, py, pz)
-          parent_tile = @mget('tile_source').tiles[parent_key]
+          [px, py, pz] = tile_source.get_closest_parent_by_tile_xyz(x, y, z)
+          parent_key = tile_source.tile_xyz_to_key(px, py, pz)
+          parent_tile = tile_source.tiles[parent_key]
           if parent_tile? and parent_tile.loaded and parent_key not in parents
             parents.push(parent_key)
-
           if zooming_out
-            children = @mget('tile_source').children_by_tile_xyz(x, y, z)
+            children = tile_source.children_by_tile_xyz(x, y, z)
             for c in children
               [cx, cy, cz, cbounds] = c
-              child_key = @mget('tile_source').tile_xyz_to_key(cx, cy, cz)
-
-              if child_key of @mget('tile_source').tiles
+              child_key = tile_source.tile_xyz_to_key(cx, cy, cz)
+
+              if child_key of tile_source.tiles
                 children.push(child_key)

         if not tile?
@@ -222,7 +226,7 @@
     # draw cached ----------
     @_render_tiles(cached)
     for t in cached
-      @mget('tile_source').tiles[t].current = true
+      tile_source.tiles[t].current = true

     # fetch missing -------
     if @render_timer?


Using the same string as before refactor

--- core/app/assets/javascripts/base/registration_forms.coffee
+++ core/app/assets/javascripts/base/registration_forms.coffee
@@ -51,7 +51,7 @@
       clearPreviousErrorsForForm($(el))

     registration_code = $("#" + formId + "_user_registration_code").val()
-    mp_track 'User: Reserved Username', code: registration_code
+    mp_track 'User: Reserved username', code: registration_code

   form.on "ajax:error", (event, response, error) ->
     clearPreviousErrorsForForm(form)


Fixed setting of attributes instead of setting entire model

--- core/app/backbone/views/conversations/messages_view.coffee
+++ core/app/backbone/views/conversations/messages_view.coffee
@@ -5,7 +5,7 @@

   initialize: ->
     @sender = @model.sender()
-    @sender.set(@options.user_collection.get(@sender.id))
+    @sender.set(@options.user_collection.get(@sender.id).attributes)
     @listenTo(@model, "scroll", @scroll); # Remove this line when upgrading Marionette, as the new version uses @modelEvents

   templateHelpers: =>


remove 3rd map item

remove 3rd map item

--- source/javascripts/all.coffee
+++ source/javascripts/all.coffee
@@ -1,25 +1,6 @@
 # all.coffee

 document.addEventListener 'DOMContentLoaded', (e) ->
-
-  d3.selectAll('.js-pdx-map').each ->
-    el = d3.select this
-    lat = el.attr 'data-lat'
-    lon = el.attr 'data-lon'
-
-    pdxmap = L.mapbox.map(this, 'caged.historical-map-two')
-      .setView([20, 65], 3)
-
-    L.mapbox.featureLayer({
-      type: 'Feature'
-      geometry: { type: 'Point', coordinates: [lat, lon] }
-      properties: {
-        'marker-size': 'medium'
-        'marker-color': '#4e4433'
-        'marker-symbol': 'rail-metro'
-      }
-    }).addTo pdxmap
-
   d3.selectAll('.js-street-view').each ->
     el = d3.select this
     lat = el.attr 'data-lat'


test for #2

test for #2

--- src/test/issues_test.coffee
+++ src/test/issues_test.coffee
@@ -19,6 +19,15 @@
 			assert myRoutes.hello.myCountry.path(), 'hi/Colombia/:department'
 			assert myRoutes.hello.myCountry.path({department: 'Santander'}), 'hi/Colombia/Santander'

+		# it '2: Support custom pre & post in path()', ->
+		# 	templateRoutes = new Trocha
+		# 		pre: 'templates'
+		# 		post: '-myH45H.html'
+		# 		routes:
+		# 			home: {}
+		# 	assert templateRoutes.home.path(ext: true), 'templates/home-myH45H.html'
+		# 	assert templateRoutes.home.path(pre: 'gato', post: 'perro', ext: true), 'gato/homeperro'
+
 		it '4: customSelector must works with routes attributes', ->
 			myRoutes = new Trocha
 				customSelector: 'TRCH'


Fix tests.

Fix tests.

--- test/spec/dispatcher_spec.coffee
+++ test/spec/dispatcher_spec.coffee
@@ -362,9 +362,11 @@

         done()

-    it 'should support redirection to a URL', (done) ->
+    it 'should support redirection to an URL', (done) ->
       dispatch = sinon.spy()
+      routed = sinon.spy()
       mediator.subscribe 'dispatcher:dispatch', dispatch
+      mediator.subscribe 'router:route', routed

       # Dispatch a route to check if previous controller info is correct after
       # redirection


Affix on transition in.

Affix on transition in.

--- src/traits/Affixable.coffee
+++ src/traits/Affixable.coffee
@@ -204,8 +204,7 @@

     affix = => @__affix()

-    @on 'show', affix
-    @on 'change:affixPosition', affix
+    @on 'show change:affixPosition transition:in', affix
     @addEventListenerTo window, 'scroll', affix
     @addEventListenerTo window, 'resize', affix


Don't override console.group/.trace

Don't override console.group/.trace

--- src/trix/core/logger.coffee
+++ src/trix/core/logger.coffee
@@ -11,8 +11,6 @@

   @log: (name, args...) ->
     @get(name).log(args...)
-
-  forwardMethods ofObject: console, onConstructor: this, toProperty: "console"

   constructor: (@name, @console, enabled) ->
     @disable() unless enabled
@@ -37,3 +35,5 @@
   groupCollapsed: ->

   trace: ->
+
+  forwardMethods ofObject: console, onConstructor: this, toProperty: "console"


Add support for ∇ and ∇∇ in lambdas

Add support for ∇ and ∇∇ in lambdas

Fixes #7.

--- grammars/apl.cson
+++ grammars/apl.cson
@@ -659,6 +659,8 @@
 			{match: "[⍺⍶]", name: "variable.lambda.arguments.left.apl"}
 			{match: "[⍵⍹]", name: "variable.lambda.arguments.right.apl"}
 			{match: "χ",    name: "variable.lambda.arguments.axis.apl"}
+			{match: "∇∇",   name: "variable.lambda.operands.self.operator.apl"}
+			{match: "∇",    name: "variable.lambda.operands.self.function.apl"}
 			{match: "λ",    name: "variable.lambda.symbol.apl"}
 		]


Turns out the order of patterns matters.

--- grammars/pug.cson
+++ grammars/pug.cson
@@ -460,6 +460,9 @@
         include: "#buffered_comments"
       }
       {
+        include: "#unbuffered_code_block"
+      }
+      {
         include: "#inline_pug"
       }
       {
@@ -467,9 +470,6 @@
       }
       {
         include: "#unbuffered_code"
-      }
-      {
-        include: "#unbuffered_code_block"
       }
       {
         include: "#mixin_definition"


Replaced deprecated live() with on()

Replaced deprecated live() with on()

--- Resources/coffee/dialog.coffee
+++ Resources/coffee/dialog.coffee
@@ -24,7 +24,7 @@
             @errorizers.push(new App.FatalErrorizer.Default)

         self = @
-        $(@element).live('click', (event) ->
+        $('body').on('click', @element, (event) ->
             dialog = basedialog.clone()
             loader.start()

@@ -104,7 +104,7 @@
         $confirmBtn = $('[data-id|="confirm-delete"] > [data-id|="confirm"]')
         $confirmBtn.attr('href', $(element).attr('href'))

-        $('[data-id|="confirm-delete"] > [data-id|="cancel"]').live('click', =>
+        $('body').on('click', '[data-id|="confirm-delete"] > [data-id|="cancel"]', (e) =>
             $(dialog).dialog('close')
             return false
         )


only try and migrate bower.json if there is one in the legacy path

--- src/steroids/Bower.coffee
+++ src/steroids/Bower.coffee
@@ -1,34 +1,40 @@
 fs = require "fs"
 path = require "path"
-

 paths = require "./paths"
 sbawn = require "./sbawn"

 class Bower
-  constructor: ->
+  configs = paths.application.configs

   update: ->
-    @checkObsoleteConfiguration ->
+    checkObsoleteConfiguration ->
       bowerRun = sbawn
         cmd: paths.bower
         args: ["update"]
         stdout: true
         stderr: true

-  checkObsoleteConfiguration: (done) ->
-    fs.exists paths.application.configs.bower,
+  checkObsoleteConfiguration = (done) ->
+    fs.exists configs.bower,
       (isConfigured) =>
         if isConfigured
           done()
         else
-          @migrateObsoleteConfiguration done
+          console.log "Bower configuration not found at #{configs.bower}"
+          migrateObsoleteConfiguration done

-  migrateObsoleteConfiguration: (done) ->
-    fs.rename paths.application.configs.legacy.bower,
-      paths.application.configs.bower,
-      done
-
+  migrateObsoleteConfiguration = (done) ->
+    fs.exists configs.legacy.bower,
+      (hasLegacyConfiguration) =>
+        if hasLegacyConfiguration
+          console.log "Moving Bower configuration from #{configs.legacy.bower} to #{configs.bower}"
+          fs.rename configs.legacy.bower,
+            configs.bower,
+            done
+        else
+          console.log "ERROR: Unable to continue without a bower.json file"
+          process.exit 1

 module.exports = Bower


Start counting transitions at 0, not 1

Start counting transitions at 0, not 1

--- src/storage/memory.coffee
+++ src/storage/memory.coffee
@@ -16,7 +16,7 @@
     for transition in transitions
       key = transition.from.join(' ')
       hops = (@transitions[key] ?= {})
-      prior = hops[transition.to] or 1
+      prior = hops[transition.to] or 0
       hops[transition.to] = prior + 1
     process.nextTick callback


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/collision.coffee
+++ src/collision.coffee
@@ -216,7 +216,7 @@
     ###
     rayRectangle: (source, direction, target) ->
       unless target.xw? and target.yw?
-        if
+        if target.
         return Collision.rayRectangle source, direction,
           xw: target.width/2
           yw: target.height/2


:new: Add a spec about returning null for non-specified keys

--- spec/custom-element-spec.coffee
+++ spec/custom-element-spec.coffee
@@ -30,6 +30,13 @@
     expect(element.Wow).toBeUndefined()
     expect(element.wow).toBe(false)

+  it 'returns null for non-specified config keys', ->
+    element = new (registerCustomElement(getCustomElement({
+      name: 'x-spec-return-null-nonspecified-keys'
+      config: {wow: {type: Boolean}}
+    })))
+    expect(element.wow).toBe(null)
+
   it 'does not fire attributeChanged on config change', ->
     executed = false
     element = new (registerCustomElement(getCustomElement({


Set soft wrap explicitly on the display buffer

This is required now that soft wrap column only takes affect
when soft wrap is enabled.

--- spec/display-buffer-spec.coffee
+++ spec/display-buffer-spec.coffee
@@ -60,6 +60,7 @@

   describe "soft wrapping", ->
     beforeEach ->
+      displayBuffer.setSoftWrap(true)
       displayBuffer.setSoftWrapColumn(50)
       changeHandler.reset()

@@ -143,7 +144,7 @@
       describe "when a newline is inserted, deleted, and re-inserted at the end of a wrapped line (regression)", ->
         it "correctly renders the original wrapped line", ->
           buffer = project.buildBuffer(null, '')
-          displayBuffer = new DisplayBuffer({buffer, tabLength, softWrapColumn: 30})
+          displayBuffer = new DisplayBuffer({buffer, tabLength, softWrapColumn: 30, softWrap: true})

           buffer.insert([0, 0], "the quick brown fox jumps over the lazy dog.")
           buffer.insert([0, Infinity], '\n')
@@ -505,6 +506,7 @@

   describe ".clipScreenPosition(screenPosition, wrapBeyondNewlines: false, wrapAtSoftNewlines: false, skipAtomicTokens: false)", ->
     beforeEach ->
+      displayBuffer.setSoftWrap(true)
       displayBuffer.setSoftWrapColumn(50)

     it "allows valid positions", ->


Activity: removed forgotten debugger

Activity: removed forgotten debugger

--- client/Social/Activity/sidebar/activitysidebar.coffee
+++ client/Social/Activity/sidebar/activitysidebar.coffee
@@ -169,8 +169,6 @@
     item = @itemsById[data.id] or
            @itemsBySlug[data.slug] or
            @itemsByName[data.name]
-
-    debugger

     return item or null


Add spec for partial matches of bindings with keyup events

--- spec/keymap-manager-spec.coffee
+++ spec/keymap-manager-spec.coffee
@@ -870,6 +870,7 @@
         "body":
           "ctrl-x 1": "command-1"
           "ctrl-x 2": "command-2"
+          "a c ^c ^a": "command-3"

       keymapManager.handleKeyboardEvent(buildKeydownEvent('x', ctrl: true, target: document.body))
       expect(handler).toHaveBeenCalled()
@@ -878,6 +879,36 @@
       expect(keystrokes).toBe 'ctrl-x'
       expect(partiallyMatchedBindings).toHaveLength 2
       expect(partiallyMatchedBindings.map ({command}) -> command).toEqual ['command-1', 'command-2']
+      expect(keyboardEventTarget).toBe document.body
+
+    fit "emits `matched-partially` when a key binding partially matches an event for key bindings that have keyup events", ->
+      handler = jasmine.createSpy('matched-partially handler')
+      keymapManager.onDidPartiallyMatchBindings handler
+      keymapManager.add "test",
+        "body":
+          "a c ^c ^a": "command-1"
+
+      keymapManager.handleKeyboardEvent(buildKeydownEvent('a', target: document.body))
+      keymapManager.handleKeyboardEvent(buildKeydownEvent('c', target: document.body))
+      expect(handler).toHaveBeenCalled()
+
+      {keystrokes, partiallyMatchedBindings, keyboardEventTarget} = handler.argsForCall[0][0]
+      expect(keystrokes).toBe 'a'
+
+      {keystrokes, partiallyMatchedBindings, keyboardEventTarget} = handler.argsForCall[1][0]
+      expect(keystrokes).toBe 'a c'
+      expect(partiallyMatchedBindings).toHaveLength 1
+      expect(partiallyMatchedBindings.map ({command}) -> command).toEqual ['command-1']
+      expect(keyboardEventTarget).toBe document.body
+
+      handler.reset()
+      keymapManager.handleKeyboardEvent(buildKeyupEvent('c', target: document.body))
+      expect(handler).toHaveBeenCalled()
+
+      {keystrokes, partiallyMatchedBindings, keyboardEventTarget} = handler.argsForCall[0][0]
+      expect(keystrokes).toBe 'a c ^c'
+      expect(partiallyMatchedBindings).toHaveLength 1
+      expect(partiallyMatchedBindings.map ({command}) -> command).toEqual ['command-1']
       expect(keyboardEventTarget).toBe document.body

     it "emits `match-failed` when no key bindings match the event", ->


tweaked test hopefully resolved the issue we test occasionally failing

--- spec/lib/controller-spec.coffee
+++ spec/lib/controller-spec.coffee
@@ -71,7 +71,9 @@
         @m = @message.clone()
         @m.data.target = 'people'
       When -> @controller.deliver 'people'
-      Then -> expect(@controller.emit).toHaveBeenCalledWith 'deliver', @m
+      Then -> expect(@controller.emit).toHaveBeenCalled()
+      And -> expect(@controller.emit.mostRecentCall.args[0]).toBe 'deliver'
+      And -> expect(@controller.emit.mostRecentCall.args[1]).toEqual @m

   describe '#actor', ->


managed: Add your own machine vs vm

managed: Add your own machine vs vm

--- client/app/lib/environment/environmentlistitem.coffee
+++ client/app/lib/environment/environmentlistitem.coffee
@@ -39,7 +39,7 @@
     if isKoding()
       addVMMenu['Create Koding VM'] = callback: @generateMenuCallback 'koding'

-    addVMMenu['Add Your own VM']  = callback: @generateMenuCallback 'managed'
+    addVMMenu['Add Your Own Machine']  = callback: @generateMenuCallback 'managed'

     @addVMButton  = new kd.ButtonView


fixed options assignment issue

fixed options assignment issue

--- src/tasks/selenium.coffee
+++ src/tasks/selenium.coffee
@@ -42,12 +42,12 @@
       analytics: @analytics
       project: @project

-    commandOptions = assign serverOpts, browserOptions,
+    options = assign serverOpts, browserOptions,
       baseURL: @project.config(browserOptions.environment).baseURL || '/'

-    serve.run(commandOptions)
+    serve.run(options)
     .then ->
-      start.run(browserOptions)
+      start.run(options)
     .then (browser) ->
       browser.buildPages()
       .then -> browser.quit()


Add toStringWithoutFragment() method to PostType

--- src/tent-client.js.coffee
+++ src/tent-client.js.coffee
@@ -35,6 +35,9 @@

     toString: =>
       "#{@base}/v#{@version}##{@fragment || ''}"
+
+    toStringWithoutFragment: =>
+      "#{@base}/v#{@version}"

   class @HTTP
     @MEDIA_TYPES = {


Unset @logPanel after destroying

Unset @logPanel after destroying

--- lib/main.coffee
+++ lib/main.coffee
@@ -68,6 +68,7 @@

   removeLogPanel: ->
     @logPanel?.destroy()
+    @logPanel = null

   toggleLogPanel: ->
     if @logPanel?


update description

update description

--- lib/main.coffee
+++ lib/main.coffee
@@ -32,7 +32,7 @@
       order: 3
     buildOnWine:
       title: "Build on Wine"
-      description: "*Experimental* – When not on Windows, `ISCC.exe` will be launched in Wine. For better error detection, you might have to tweak `WINEDEBUG`."
+      description: "*Experimental* — When not on Windows, `ISCC.exe` will be launched in Wine. For better error detection, you might have to tweak `WINEDEBUG`."
       type: "boolean"
       default: true
       order: 4


Add linter name

Add linter name

Specifies the linter name, allowing the `linter` package to display it
if the user chooses to.

--- lib/main.coffee
+++ lib/main.coffee
@@ -20,6 +20,7 @@
   provideLinter: ->
     helpers = require('atom-linter')
     provider =
+      name: 'PHP'
       grammarScopes: ['text.html.php', 'source.php']
       scope: 'file'
       lintOnFly: true


configDefaults -> config

configDefaults -> config

--- lib/main.coffee
+++ lib/main.coffee
@@ -1,8 +1,10 @@
 PackageGeneratorView = require './package-generator-view'

 module.exports =
-  configDefaults:
-    createInDevMode: false
+  config:
+    createInDevMode:
+      default: true
+      type: 'boolean'

   activate: ->
     @view = new PackageGeneratorView()


Fix \textsf support in LaTeX mode

Fix \textsf support in LaTeX mode


Use existing editor

Use existing editor

--- nsync/remote-file-opener.coffee
+++ nsync/remote-file-opener.coffee
@@ -12,6 +12,12 @@
   atom.project.addBuffer(buffer)

 buildEditor = (buffer, largeFileMode, options) ->
+  existingEditor = null
+  atom.textEditors.editors.forEach (editor) ->
+    existingEditor = editor if editor.getPath() is buffer.getPath()
+
+  return existingEditor if existingEditor?
+
   editor = atom.workspace.buildTextEditor(Object.assign({buffer, largeFileMode}, options))
   disposable = atom.textEditors.add(editor)
   editor.onDidDestroy -> disposable.dispose()
@@ -27,7 +33,7 @@
       Cancel: null
   proceed

-displayLoading = (path) ->
+notify = (path) ->
   atom.notifications.addInfo "Loading #{path}...", icon: 'cloud-download'

 module.exports =
@@ -47,7 +53,7 @@
     editor = buildEditor(buffer, @largeFileMode, @options)

     if not @isLarge or (@isLarge and largeFileConfirm())
-      displayLoading(@path) if @isLarge
+      notify(@path) if @isLarge
       learnIDE.remoteFS.open(@path)

     # TODO: this should prevent opening no matter what
@@ -55,7 +61,6 @@
     # so an empty editor just hanging out is weird)
     editor

-
   openFile: ->
     buffer = bufferForPath(@path)
     buffer.setText(@contents) if @contents?


Update RangesTracker to include undo logic

--- app/coffee/RangesTracker.coffee
+++ app/coffee/RangesTracker.coffee
@@ -1,5 +1,5 @@
-load = (EventEmitter) ->
-	class RangesTracker extends EventEmitter
+load = () ->
+	class RangesTracker
 		# The purpose of this class is to track a set of inserts and deletes to a document, like
 		# track changes in Word. We store these as a set of ShareJs style ranges:
 		#   {i: "foo", p: 42} # Insert 'foo' at offset 42
@@ -36,6 +36,7 @@
 		#     middle of a previous insert by the first user, the original insert will be split into two.
 		constructor: (@changes = [], @comments = []) ->
 			@setIdSeed(RangesTracker.generateIdSeed())
+			@resetDirtyState()

 		getIdSeed: () ->
 			return @id_seed
@@ -75,7 +76,7 @@
 			comment = @getComment(comment_id)
 			return if !comment?
 			@comments = @comments.filter (c) -> c.id != comment_id
-			@emit "comment:removed", comment
+			@_markAsDirty comment, "comment", "removed"

 		getChange: (change_id) ->
 			change = null
@@ -103,7 +104,11 @@
 				@addComment(op, metadata)
 			else
 				throw new Error("unknown op type")
-
+
+		applyOps: (ops, metadata = {}) ->
+			for op in ops
+				@applyOp(op, metadata)
+
 		addComment: (op, metadata) ->
 			# TODO: Don't allow overlapping comments?
 			@comments.push comment = {
@@ -114,18 +119,18 @@
 					t: op.t
 				metadata
 			}
-			@emit "comment:added", comment
+			@_markAsDirty comment, "comment", "added"
 			return comment

 		applyInsertToComments: (op) ->
 			for comment in @comments
 				if op.p <= comment.op.p
 					comment.op.p += op.i.length
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"
 				else if op.p < comment.op.p + comment.op.c.length
 					offset = op.p - comment.op.p
 					comment.op.c = comment.op.c[0..(offset-1)] + op.i + comment.op.c[offset...]
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"

 		applyDeleteToComments: (op) ->
 			op_start = op.p
@@ -138,7 +143,7 @@
 				if op_end <= comment_start
 					# delete is fully before comment
 					comment.op.p -= op_length
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"
 				else if op_start >= comment_end
 					# delete is fully after comment, nothing to do
 				else
@@ -161,12 +166,13 @@

 					comment.op.p = Math.min(comment_start, op_start)
 					comment.op.c = remaining_before + remaining_after
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"

 		applyInsertToChanges: (op, metadata) ->
 			op_start = op.p
 			op_length = op.i.length
 			op_end = op.p + op_length
+			undoing = !!op.u

 			already_merged = false
@@ -184,8 +190,9 @@
 						change.op.p += op_length
 						moved_changes.push change
 					else if op_start == change_start
-						# If the insert matches the start of the delete, just remove it from the delete instead
-						if change.op.d.length >= op.i.length and change.op.d.slice(0, op.i.length) == op.i
+						# If we are undoing, then we want to cancel any existing delete ranges if we can.
+						# Check if the insert matches the start of the delete, and just remove it from the delete instead if so.
+						if undoing and change.op.d.length >= op.i.length and change.op.d.slice(0, op.i.length) == op.i
 							change.op.d = change.op.d.slice(op.i.length)
 							change.op.p += op.i.length
 							if change.op.d == ""
@@ -203,15 +210,15 @@
 					# Only merge inserts if they are from the same user
 					is_same_user = metadata.user_id == change.metadata.user_id

-					# If this is an insert op at the end of an existing insert with a delete following, and it cancels out the following
-					# delete then we shouldn't append it to this insert, but instead only cancel the following delete.
+					# If we are undoing, then our changes will be removed from any delete ops just after. In that case, if there is also
+					# an insert op just before, then we shouldn't append it to this insert, but instead only cancel the following delete.
 					# E.g.
 					#                   foo|<--- about to insert 'b' here
 					#  inserted 'foo'  --^ ^-- deleted 'bar'
 					# should become just 'foo' not 'foob' (with the delete marker becoming just 'ar'), .
 					next_change = @changes[i+1]
 					is_op_adjacent_to_next_delete = next_change? and next_change.op.d? and op.p == change_end and next_change.op.p == op.p
-					will_op_cancel_next_delete = is_op_adjacent_to_next_delete and next_change.op.d.slice(0, op.i.length) == op.i
+					will_op_cancel_next_delete = undoing and is_op_adjacent_to_next_delete and next_change.op.d.slice(0, op.i.length) == op.i

 					# If there is a delete at the start of the insert, and we're inserting
 					# at the start, we SHOULDN'T merge since the delete acts as a partition.
@@ -281,8 +288,8 @@
 			for change in remove_changes
 				@_removeChange change

-			if moved_changes.length > 0
-				@emit "changes:moved", moved_changes
+			for change in moved_changes
+				@_markAsDirty change, "change", "moved"

 		applyDeleteToChanges: (op, metadata) ->
 			op_start = op.p
@@ -406,8 +413,8 @@
 					@_removeChange change
 					moved_changes = moved_changes.filter (c) -> c != change

-			if moved_changes.length > 0
-				@emit "changes:moved", moved_changes
+			for change in moved_changes
+				@_markAsDirty change, "change", "moved"

 		_addOp: (op, metadata) ->
 			change = {
@@ -427,17 +434,11 @@
 				else
 					return -1

-			if op.d?
-				@emit "delete:added", change
-			else if op.i?
-				@emit "insert:added", change
+			@_markAsDirty(change, "change", "added")

 		_removeChange: (change) ->
 			@changes = @changes.filter (c) -> c.id != change.id
-			if change.op.d?
-				@emit "delete:removed", change
-			else if change.op.i?
-				@emit "insert:removed", change
+			@_markAsDirty change, "change", "removed"

 		_applyOpModifications: (content, op_modifications) ->
 			# Put in descending position order, with deleting first if at the same offset
@@ -486,13 +487,32 @@
 					previous_change = change
 			return { moved_changes, remove_changes }

+		resetDirtyState: () ->
+			@_dirtyState = {
+				comment: {
+					moved: {}
+					removed: {}
+					added: {}
+				}
+				change: {
+					moved: {}
+					removed: {}
+					added: {}
+				}
+			}
+
+		getDirtyState: () ->
+			return @_dirtyState
+
+		_markAsDirty: (object, type, action) ->
+			@_dirtyState[type][action][object.id] = object
+
 		_clone: (object) ->
 			clone = {}
 			(clone[k] = v for k,v of object)
 			return clone

 if define?
-	define ["utils/EventEmitter"], load
+	define [], load
 else
-	EventEmitter = require("events").EventEmitter
-	module.exports = load(EventEmitter)+	module.exports = load()


Update ranges tracker.

Update ranges tracker.

--- app/coffee/RangesTracker.coffee
+++ app/coffee/RangesTracker.coffee
@@ -95,6 +95,20 @@
 					change = c
 					break
 			return change
+
+		getChanges: (change_ids) ->
+			changes_response = []
+			ids_map = {}
+
+			for change_id in change_ids
+				ids_map[change_id] = true
+
+			for change in @changes
+				if ids_map[change.id]
+					delete ids_map[change.id]
+					changes_response.push change
+
+			return changes_response

 		removeChangeId: (change_id) ->
 			change = @getChange(change_id)


Make drones bit harder to kill again

Make drones bit harder to kill again

--- src/components/entities/Drone.coffee
+++ src/components/entities/Drone.coffee
@@ -8,7 +8,7 @@
     @attr defaults(attr,
       w: 40
       h: 40
-      health: 100
+      health: 200
       defaultSpeed: 100
     )
     @origin 'center'


fix state/status typo

fix state/status typo

--- src/components/scores/late-work.cjsx
+++ src/components/scores/late-work.cjsx
@@ -25,7 +25,7 @@
     @task.completed_exercise_count - @task.completed_on_time_exercise_count

   lateDueDate: ->
-    if @state is 'accepted'
+    if @status is 'accepted'
       'due date'
     else
       <Time date={@task.last_worked_at} format='shortest' />


Add Backbone.Mediator.Publish logging (#688)

--- app/core/initialize.ozar.coffee
+++ app/core/initialize.ozar.coffee
@@ -99,11 +99,30 @@
   Backbone.Mediator.addChannelSchemas schemas for channel, schemas of channelSchemas
   # Major performance bottleneck if it is true in production
   Backbone.Mediator.setValidationEnabled(not app.isProduction())
-  if false  # Debug which events are being fired
-    originalPublish = Backbone.Mediator.publish
-    Backbone.Mediator.publish = ->
-      console.log 'Publishing event:', arguments... unless /(tick|frame-changed)/.test(arguments[0])
-      originalPublish.apply Backbone.Mediator, arguments
+
+  if window.location.hostname == 'localhost'
+    if window.sessionStorage?.getItem('COCO_DEBUG_LOGGING') == "1"
+      unwantedEventsRegex = new RegExp('tick|mouse-moved|mouse-over|mouse-out|hover-line|check-away|new-thang-added|zoom-updated|away-back')
+      unwantedStackRegex = new RegExp('eval|debounce|defer|delay|Backbone|Idle')
+      originalPublish = Backbone.Mediator.publish
+      Backbone.Mediator.publish = ->
+        unless unwantedEventsRegex.test(arguments[0])
+          try
+            splitStack = (new Error()).stack.split("\n").slice(1)
+            maxDepth = 5
+            for s, i in splitStack
+              break if i > maxDepth
+              filteredStack = s.trim().replace(/at\ |prototype|module|exports/gi, '').replace('..', '')
+              filteredStack = filteredStack.split('(webpack-internal')[0]
+              unless unwantedStackRegex.test(filteredStack)
+                console.log ">>> #{filteredStack}->", arguments...
+                break
+          catch
+            console.log ">>> ? -> ", arguments...
+
+        originalPublish.apply Backbone.Mediator, arguments
+    else
+      console.log("Not logging Backbone events. Turn on by typing this in your browser console: window.sessionStorage.setItem('COCO_DEBUG_LOGGING', 1)")

 setUpMoment = ->
   {me} = require 'core/auth'


Debug ipad on text edition

Debug ipad on text edition

--- assets/views/postit.coffee
+++ assets/views/postit.coffee
@@ -184,12 +184,13 @@

   focus: =>
     @inEdition = true
-    @p.textContent = "" if @p.textContent == App.Postit.defaultTitle
+    if @p.textContent == App.Postit.defaultTitle and !$.browser.webkit
+      @p.textContent = ""
     true

   blur: =>
     @inEdition = false
-    @model.set title: App.Postit.defaultTitle if @p.textContent == ''
+    @p.textContent = App.Postit.defaultTitle if @p.textContent == ''
     true

   editTitle: (e) =>


Add parts of bin-path to stack-sandbox

Add parts of bin-path to stack-sandbox

Should help with #186

--- lib/util.coffee
+++ lib/util.coffee
@@ -84,7 +84,7 @@
     Util.debug("Looking for stack sandbox...")
     env.PATH = joinPath(apd)
     Util.debug("Running stack with PATH ", env.PATH)
-    Util.execPromise 'stack', ['path', '--snapshot-install-root', '--local-install-root'],
+    Util.execPromise 'stack', ['path', '--snapshot-install-root', '--local-install-root', '--bin-path'],
       encoding: 'utf-8'
       stdio: 'pipe'
       cwd: rootPath
@@ -94,8 +94,11 @@
       lines = out.split(EOL)
       sir = lines.filter((l) -> l.startsWith('snapshot-install-root: '))[0].slice(23) + "#{sep}bin"
       lir = lines.filter((l) -> l.startsWith('local-install-root: '))[0].slice(20) + "#{sep}bin"
-      Util.debug("Found stack sandbox ", lir, sir)
-      return [lir, sir]
+      bp =
+         lines.filter((l) -> l.startsWith('bin-path: '))[0].slice(10).split(delimiter).filter (p) ->
+           not ((p is sir) or (p is lir) or (p in apd))
+      Util.debug("Found stack sandbox ", lir, sir, bp...)
+      return [lir, sir, bp...]
     .catch (err) ->
       Util.warn("No stack sandbox found because ", err)


Use more robust checking of error values

--- test/unit/coffee/SpellingAPIManagerTests.coffee
+++ test/unit/coffee/SpellingAPIManagerTests.coffee
@@ -46,7 +46,9 @@
 				@SpellingAPIManager.runRequest @token, {}, (@error, @result) => done()

 			it "should return an error", ->
-				expect(@error).to.deep.equal new Error("malformed JSON")
+				expect(@error).to.exist
+				expect(@error).to.be.instanceof Error
+				expect(@error.message).to.equal "malformed JSON"

 		describe "with a missing token", ->
 			beforeEach (done) ->
@@ -94,13 +96,17 @@
 			beforeEach (done) -> @SpellingAPIManager.learnWord null, word: "banana", (@error) => done()

 			it "should return an error", ->
-				expect(@error).to.deep.equal new Error("malformed JSON")
+				expect(@error).to.exist
+				expect(@error).to.be.instanceof Error
+				expect(@error.message).to.equal "no token provided"

 		describe "without a word", ->
 			beforeEach (done) -> @SpellingAPIManager.learnWord @token, {}, (@error) => done()

 			it "should return an error", ->
-				expect(@error).to.deep.equal new Error("no token provided")
+				expect(@error).to.exist
+				expect(@error).to.be.instanceof Error
+				expect(@error.message).to.equal "malformed JSON"

 		describe "with a word and a token", ->
 			beforeEach (done) ->