replace confusing 'Such theme' with 'Such Indicator'

--- client/test/src/views/indicator_selector_view.coffee
+++ client/test/src/views/indicator_selector_view.coffee
@@ -86,7 +86,7 @@
     _id: Factory.findNextFreeId('Section')
   )

-  indicators = [{_id: 1, name: "Such Theme"}]
+  indicators = [{_id: 1, name: "Such Indicator"}]

   @stub(Backbone.Collections.IndicatorCollection::, 'fetch', ->
     Helpers.promisify(=>


Comment out LinkedIn authenticator as we still use Twitter only

--- nodejs/palsu.coffee
+++ nodejs/palsu.coffee
@@ -43,11 +43,11 @@
     if not event.id then return

     events = event.get "rdfcal:hasTask"
-
+
     if not events
         console.log "Issue getting task collection for event " + event.id
         return callback()
-
+
     events.predicate = 'rdfcal:taskOf'
     events.object = event.id
     events.comparator = (item) ->
@@ -96,13 +96,13 @@
     # oAuth with twitter
     unless cfg.twitter.key
         console.error "Error: No Twitter ConsumerKey, check your configuration.json"
-    unless cfg.linkedin.key
-        console.error "Error: No LinkedIn ConsumerKey, check your configuration.json"
+#    unless cfg.linkedin.key
+#        console.error "Error: No LinkedIn ConsumerKey, check your configuration.json"

     server.use connect.session
         secret: 'vie palsu app'
         store: session_store
-
+
     server.set 'view options', { layout: false }

     server.use auth [auth.Twitter
@@ -139,7 +139,7 @@
     if !provider then provider = null
     console.log 'provider: ' + provider

-    if request.isAuthenticated() then return response.redirect '/meetings'
+    if request.isAuthenticated() then return response.redirect '/meetings'

     request.authenticate [provider], (error, authenticated) ->
         # move to switch...
@@ -193,17 +193,17 @@

         # Find RDFa entities and load them
         VIE.RDFaEntities.getInstances jQ "*"
-
+
         # meeting list
         # Get the Calendar object
         calendar = VIE.EntityManager.getBySubject 'urn:uuid:e1191010-5bb1-11e0-80e3-0800200c9a66'
-
+
         if !calendar
             VIE.cleanup()
             # todo return error message
             console.error "Error: loading calendar for task list"
             return response.send window.document.innerHTML
-
+
         # Query for events that have the calendar as component
         events = calendar.get 'rdfcal:has_component'
         events.predicate = 'rdfcal:component'
@@ -213,7 +213,7 @@
         return events.fetch
             success: (eventCollection) ->
                 fetched = 0
-
+
                 eventCollection.each (event) ->
                     console.log 'loop eventCollection', eventCollection.length
                     fetchTasksForEvent event, ->
@@ -222,11 +222,11 @@
                             # Send stuff
                             VIE.cleanup()
                             return response.send window.document.innerHTML
-
+
             error: (collection, error) ->
                 VIE.cleanup()
                 return response.send window.document.innerHTML
-
+
         return response.send window.document.innerHTML

 # Serve the list of meetings for /
@@ -287,11 +287,11 @@
             VIE.cleanup()
             console.log 'send content'
             return response.send window.document.innerHTML
-
+
         sendContent2 = (collection, error) ->
             #VIE.cleanup()
             return true
-
+
         # Query for posts for this event
         # @todo callbacks as array or something like that...
         getPosts = (event, callback, callback2) ->
@@ -412,7 +412,7 @@

     client.on 'disconnect', ->
         if not client.userInstance then return
-
+
         # Mark user as offline and notify other users
         client.userInstance.set
             'iks:online': 0


Updated docpad for bal-util modules change

--- src/lib/docpad.coffee
+++ src/lib/docpad.coffee
@@ -359,11 +359,11 @@

 		# Node Path
 		# The location of our node executable
-		nodePath: if /node$/.test(process.execPath) then process.execPath else 'node'
+		nodePath: null

 		# Git Path
 		# The location of our git executable
-		gitPath: if /^win/.test(process.platform) then 'git.cmd' else 'git'
+		gitPath: null

 		# Template Data
 		# What data would you like to expose to your templates
@@ -384,6 +384,7 @@
 		# Events
 		# A hash of event handlers
 		events: null  # {}
+

 	# =================================
 	# Initialization Functions
@@ -665,6 +666,18 @@
 		# Chain
 		@

+	# Init Git Repo
+	# next(err,results)
+	initGitRepo: (opts) ->
+		# Prepare
+		opts.gitPath ?= @config.gitPath
+
+		# Forward
+		balUtil.initGitRepo(opts)
+
+		# Chain
+		@
+
 	# Init Node Modules
 	# next(err,results)
 	initNodeModules: (opts={}) ->
@@ -1285,7 +1298,6 @@

 		# Configure
 		repoConfig =
-			gitPath: docpad.config.gitPath
 			path: destinationPath
 			url: skeletonModel.get('repo')
 			branch: skeletonModel.get('branch')
@@ -1313,7 +1325,7 @@
 			return docpad.error(err)  if err

 			# Initalize the git repository
-			balUtil.initGitRepo(repoConfig)
+			docpad.initGitRepo(repoConfig)

 		# Chain
 		@
@@ -1609,7 +1621,7 @@
 		websiteConfig = {}

 		# Async
-		tasks = new balUtil.Group (err) =>
+		preTasks = new balUtil.Group (err) =>
 			return next(err)  if err

 			# Merge Configuration (not deep!)
@@ -1663,16 +1675,20 @@
 			# Async
 			postTasks = new balUtil.Group (err) =>
 				return next(err)
-			postTasks.total = 2

 			# Load collections
-			@loadCollections(postTasks.completer())
+			postTasks.push (complete) ->
+				docpad.loadCollections(complete)

 			# Initialize
-			@loadPlugins(postTasks.completer())
+			postTasks.push (complete) ->
+				docpad.loadPlugins(complete)
+
+			# Fire post tasks
+			postTasks.async()

 		# Load DocPad Configuration
-		tasks.push (complete) =>
+		preTasks.push (complete) =>
 			@loadConfigPath @packagePath, (err,data) ->
 				return complete(err)  if err
 				data or= {}
@@ -1685,7 +1701,7 @@
 				complete()

 		# Load Website Package Configuration
-		tasks.push (complete) =>
+		preTasks.push (complete) =>
 			rootPath = instanceConfig.rootPath or @config.rootPath
 			websitePackagePath = pathUtil.resolve(rootPath, instanceConfig.packagePath or @config.packagePath)
 			@loadConfigPath websitePackagePath, (err,data) ->
@@ -1699,7 +1715,7 @@
 				complete()

 		# Load Website Configuration
-		tasks.push (complete) =>
+		preTasks.push (complete) =>
 			rootPath = instanceConfig.rootPath or websitePackageConfig.rootPath or @config.rootPath
 			configPaths = instanceConfig.configPaths or websitePackageConfig.configPaths or @config.configPaths
 			for configPath, index in configPaths
@@ -1714,8 +1730,24 @@
 				# Compelte the loading
 				complete()

+		# Get the git path
+		unless @config.gitPath?
+			preTasks.push (complete) ->
+				balUtil.getGitPath (err,gitPath) ->
+					return docpad.error(err)  if err
+					docpad.config.gitPath = gitPath
+					complete()
+
+		# Get the node path
+		unless @config.nodePath?
+			preTasks.push (complete) ->
+				balUtil.getNodePath (err,nodePath) ->
+					return docpad.error(err)  if err
+					docpad.config.nodePath = nodePath
+					complete()
+
 		# Run the load tasks synchronously
-		tasks.sync()
+		preTasks.sync()

 		# Chain
 		@


remove gridline overshoots as they do interact with layout

--- src/mkts-table.coffee
+++ src/mkts-table.coffee
@@ -518,15 +518,15 @@
   yield texr 'ð22', "\\begin{scope}[on background layer]"
   #.........................................................................................................
   ### TAINT use fixed size like 1mm ###
-  top       = ( @_top_from_rownr     me, 1             ) - 3
-  bottom    = ( @_bottom_from_rownr  me, me.grid.height ) + 3
+  top       = ( @_top_from_rownr     me, 1              )
+  bottom    = ( @_bottom_from_rownr  me, me.grid.height )
   for colnr in [ 1 .. me.grid.width + 1 ]
     x = @_left_from_colnr me, colnr
     yield texr 'ð23', "\\draw[sDebugCellgrid] (#{x},#{top}) -- (#{x},#{bottom});"
   #.........................................................................................................
   ### TAINT use fixed size like 1mm ###
-  left      = ( @_left_from_colnr    me, 1             ) - 3
-  right     = ( @_right_from_colnr   me, me.grid.width  ) + 3
+  left      = ( @_left_from_colnr    me, 1              )
+  right     = ( @_right_from_colnr   me, me.grid.width  )
   for rownr in [ 1 .. me.grid.height + 1 ]
     y = @_top_from_rownr me, rownr
     yield texr 'ð24', "\\draw[sDebugCellgrid] (#{left},#{y}) -- (#{right},#{y});"


ApplicationManager: Call appIsShown correctly in show, also pass the appParams

--- client/Main/ApplicationManager.coffee
+++ client/Main/ApplicationManager.coffee
@@ -236,6 +236,8 @@
     return unless appView

     @emit 'AppIsBeingShown', appInstance, appView, appOptions
+    appInstance.appIsShown? appParams
+
     @setLastActiveIndex appInstance
     @utils.defer -> callback? appInstance


KodingKontrol: reauthenticate only the stored clientId is really changed

This was causing to connect/disconnect and reconnect again to kontrol when page
loaded. Instead we are now checking if its really necessary to reauthenticate by
comparing the initial and new clientId.

--- client/Main/kite/kodingkontrol.coffee
+++ client/Main/kite/kodingkontrol.coffee
@@ -9,15 +9,17 @@
     @kites   = {}
     @regions = {}

-    @reauthenticate()

   getAuthOptions: ->

+    @_lastUsedKey = Cookies.get 'clientId'
+
     autoConnect           : no
+    autoReconnect         : no
     url                   : @_kontrolUrl ? KD.config.newkontrol.url
     auth                  :
       type                : 'sessionID'
-      key                 : Cookies.get 'clientId'
+      key                 : @_lastUsedKey
     transportClass        : SockJS
     transportOptions      :
       heartbeatTimeout    : 30 * 1000 # 30 seconds
@@ -25,9 +27,13 @@
       protocols_whitelist : ['xhr-polling'] # , 'xhr-streaming']

-  reauthenticate: ->
-    # disconnect the old kontrol kite
-    @kite?.disconnect()
+  reauthenticate: (initial)->
+
+    if @_lastUsedKey?
+      if (Cookies.get 'clientId') isnt @_lastUsedKey
+        # disconnect the old kontrol kite
+        @kite?.disconnect()
+
     # reauthenticate with the current session token
     @authenticate @getAuthOptions()


KodingKontrol: delete existing kite instances on error, reconnect them back.

--- client/Main/kite/kodingkontrol.coffee
+++ client/Main/kite/kodingkontrol.coffee
@@ -109,6 +109,8 @@

   createKite: (options, query)->

+    {computeController} = KD.singletons
+
     {kite} = options
     kiteName = kite.name

@@ -132,7 +134,12 @@
           KiteCache.unset query

-
+          delete @kites[kiteName]['singleton']
+
+          if machine = computeController.findMachineFromQueryString queryString
+            delete @kites[kiteName][machine.uid]
+
+          (@getKite { name: kiteName, queryString, waitingCalls })?.connect()

     return kite


Fix comments. Related to #162

Fix comments. Related to #162

--- client/components/EditablePage.coffee
+++ client/components/EditablePage.coffee
@@ -8,19 +8,29 @@
 import callApi from '../lib/callApi'

 class SimplePage extends React.Component
+    updatePreElements: () =>
+        preElements = @statementEl.querySelectorAll("pre")
+        preElements.forEach (preElement) ->
+            copyBtn = document.createElement "button"
+            copyBtn.className = "btn btn-secondary"
+            copyBtnSmall = document.createElement "small"
+            copyBtnSmall.appendChild document.createTextNode "скопировать"
+            copyBtn.appendChild copyBtnSmall
+            copyBtn.onclick = () =>
+                navigator.clipboard.writeText preElement.textContent
+                copyBtnSmall.textContent = "скопировано"
+            preElement.after(copyBtn)
+
     componentDidMount: () =>
-        preses = @dangerEl.querySelectorAll("pre")
-        preses.forEach (pre) ->
-            copy = document.createElement "button"
-            copy.className = "btn btn-secondary"
-            copy.appendChild document.createTextNode "скопировать"
-            copy.onclick = () =>
-                navigator.clipboard.writeText pre.textContent
-            pre.after(copy)
+        @updatePreElements()
+
+    componentDidUpdate: (prevProps) =>
+        if @props.material.content != prevProps.material.content
+            @updatePreElements()

     render: () ->

-
 @dangerEl = el}>
+
 @statementEl = el}>


rtm: improve error handling

rtm: improve error handling

--- client/ide/lib/realtimemanager.coffee
+++ client/ide/lib/realtimemanager.coffee
@@ -137,8 +137,17 @@
       @emit 'FileInitialized', model

     errorCallback = (error) =>
-      callback error
-      @emit 'FileLoadFailed', error  unless preventEvent
+      {ErrorType} = gapi.drive.realtime
+      eventName = \
+        switch error.type
+          when ErrorType.NOT_FOUND              then 'ErrorRealtimeFileMissing'
+          when ErrorType.SERVER_ERROR           then 'ErrorRealtimeServer'
+          when ErrorType.FORBIDDEN              then 'ErrorRealtimeUserForbidden'
+          when ErrorType.CLIENT_ERROR           then 'ErrorGoogleDriveApiClient'
+          when ErrorType.TOKEN_REFRESH_REQUIRED then 'ErrorRealtimeTokenExpired'
+          else 'ErrorHappened'
+
+      @emit eventName, error

     gapi.drive.realtime.load fileId, onLoadedCallback, initializerFn, errorCallback


Sort comparators

Sort comparators

--- client/lanes/lib/utilFunctions.coffee
+++ client/lanes/lib/utilFunctions.coffee
@@ -30,6 +30,11 @@
         Lanes.Vendor.ReactContext.current = previousContext
         ret

+    makeComparatorUsing: (method) ->
+        (a, b) -> Lanes.u.comparator(a[method], b[method])
+
+    comparator: (a, b) ->
+        if a < b then 1 else if a > b then -1 else 0

     objectForPath: (path) ->
         parts = path.replace(/\.js$/, '').split('/')


Handle inter-collection model copies

Handle inter-collection model copies

When a model is `copyFrom` another member of the same collection it
creates apparently duplicate models unless the other is removed

--- client/lanes/models/Collection.coffee
+++ client/lanes/models/Collection.coffee
@@ -130,6 +130,18 @@

     isLoaded: -> @_isLoaded

+    _onModelEvent: (event, model, copy, prevAttributes) ->
+        if event is 'copyfrom'
+             # are we copying from a model that is also in the same collection
+            if this.get(copy) and prevAttributes[@model::idAttribute] != model[@model::idAttribute]
+                # if we are then the copy will create two appearently identical models
+                # work around that storing the "to-be-overwritten" attrs
+                # and removeing the copy
+                this.add(prevAttributes)
+                this.remove(copy)
+        else
+            super
+
     # true if any models have unsaved data
     isDirty: ->
         !!this.findWhere(isDirty: true)


Ensure there is always an `eventBus`; even if it goes unused

--- components/connect/client/view.coffee
+++ components/connect/client/view.coffee
@@ -1,4 +1,4 @@
-{ invoke, compact } = require 'underscore'
+{ invoke, compact, extend } = require 'underscore'
 Backbone = require 'backbone'
 ConnectSearchView = require '../components/search/view.coffee'
 ConnectHeaderView = require '../components/header/view.coffee'
@@ -12,6 +12,7 @@

   initialize: ({ @search, @state, @eventBus }) ->
     @state ?= new Backbone.Model active: false, query: ''
+    @eventBus ?= extend {}, Backbone.Events

     options =
       model: @model


KDLoaderView: Added showLoader config. It will show the loader when loader appended to parent view.

--- components/loader/KDLoaderView.coffee
+++ components/loader/KDLoaderView.coffee
@@ -6,11 +6,12 @@
     o.loaderOptions or= {}
     o.size          or= {}
     options =
-      tagName       : o.tagName      or "span"
-      bind          : o.bind         or "mouseenter mouseleave"
+      tagName       : o.tagName                 or "span"
+      bind          : o.bind                    or "mouseenter mouseleave"
+      showLoader    : o.showLoader              or no
       size          :
-        width       : o.size.width  or 12
-        height      : o.size.height or 12
+        width       : o.size.width              or 12
+        height      : o.size.height             or 12
       loaderOptions :
         color       : o.loaderOptions.color     or "#000000"   # hex color
         shape       : o.loaderOptions.shape     or "spiral"    # "spiral", "oval", "square", "rect", "roundRect"
@@ -27,9 +28,11 @@
   viewAppended:->

     @canvas = new CanvasLoader @getElement(), id : "cl_#{@id}"
-    {loaderOptions} = @getOptions()
+    {loaderOptions, showLoader} = @getOptions()
     for option,value of loaderOptions
       @canvas["set#{option.capitalize()}"] value
+
+    @show()  if showLoader

   show:->


:bug: Fix a typo

:bug: Fix a typo

--- lib/atom-hack.coffee
+++ lib/atom-hack.coffee
@@ -3,9 +3,9 @@
   Hack: null
   activate: ->
     if typeof atom.packages.getLoadedPackage("linter-plus") is 'undefined'
-      return atom.notificatons.addError "[Hack] linter-plus package not found or deactivated but is required to provide support for Hack", {dismissable: true}
+      return atom.notifications.addError "[Hack] linter-plus package not found or deactivated but is required to provide support for Hack", {dismissable: true}
     else if typeof atom.packages.getLoadedPackage("language-hack") is 'undefined'
-      return atom.notificatons.addError "[Hack] language-hack package not found or deactivated but is required to provide support for Hack", {dismissable: true}
+      return atom.notifications.addError "[Hack] language-hack package not found or deactivated but is required to provide support for Hack", {dismissable: true}
     @Hack = new (require './hack')
   provideHack: ->
     @Hack


only report error stacks if that prop exists on the error obj

--- lib/cli/watch.coffee
+++ lib/cli/watch.coffee
@@ -51,8 +51,10 @@
 ###

 on_error = (cli, server, active, err) ->
-  cli.emit('err', Error(err).stack)
-  if active then server.show_error(Error(err).stack)
+  err = Error(err)
+  if 'stack' in err then err = err.stack
+  cli.emit('err', err)
+  if active then server.show_error(err)

 ###*
  * When a change has been detected, notifies the cli and browser that a compile


core/view: remove unused deprecated fn

core/view: remove unused deprecated fn

--- lib/core/view.coffee
+++ lib/core/view.coffee
@@ -10,8 +10,6 @@
 # #

   {defineProperty} = Object
-
-  deprecated = (methodName)-> KD.warn "#{methodName} is deprecated from KDView if you need it override in your subclass"

   eventNames =
     ///


PortFinder: Make linter happy

PortFinder: Make linter happy

--- lib/find-port.coffee
+++ lib/find-port.coffee
@@ -5,7 +5,7 @@
     find: (onFound) ->
         srv = net.createServer()

-        srv.listen 0, () ->
+        srv.listen 0, ->
             port = srv.address().port
             srv.close (err) ->
                 if err?
@@ -17,7 +17,7 @@
         @findManyHelper numPorts, [], onFound

     findManyHelper: (numPorts, foundPorts, onFound) ->
-        if numPorts == 0
+        if numPorts is 0
             onFound(foundPorts)
         else
             @find (port) =>


Move getSelection and empty & outside selection warnings to wrapper

--- lib/list-edit.coffee
+++ lib/list-edit.coffee
@@ -26,26 +26,15 @@

   selectCmd: ->
     console.log 'Executing command list-edit-select'
-    @withSelectedList (editor, textBuffer, bufferText, selectionIxRange, listElements) ->
-      if listElements.length == 0
-        atom.notifications.addWarning 'List selection in empty list.'
-      else
-        [selStart,selEnd] = TextManipulation.getSelectionForRange listElements, selectionIxRange
-        # TODO: handle empty selection? We currently cannot create this
+    @withSelectedList (editor, textBuffer, bufferText, selectionIxRange, listElements, [selStart,selEnd]) ->
+      # TODO: handle empty selection? We currently cannot create this
+      console.log 'List elements'
+      console.log e.show() for e in listElements
+      console.log 'Selection: ' + selStart + ' <-> ' + selEnd
+      console.log (TextManipulation.getRangeForIxRange textBuffer, [listElements[selStart].eltStart, listElements[selEnd-1].eltEnd])

-        if selEnd > listElements.length
-          atom.notifications.addWarning 'List selection end outside list.'
-          # won't happen for start, since we use this to select the list in the first place
-          # TODO: maybe make this less strict, as selection in sublists is now asymmetric:
-          #       in "[1,[a,b],2]": "[a," selects entire sublist element, but ",b]" fails with warning.
-        else
-          console.log 'List elements'
-          console.log e.show() for e in listElements
-          console.log 'Selection: ' + selStart + ' <-> ' + selEnd
-          console.log (TextManipulation.getRangeForIxRange textBuffer, [listElements[selStart].eltStart, listElements[selEnd-1].eltEnd])
-
-          editor.setSelectedBufferRange(TextManipulation.getRangeForIxRange textBuffer,
-                                        [listElements[selStart].eltStart, listElements[selEnd-1].eltEnd])
+      editor.setSelectedBufferRange(TextManipulation.getRangeForIxRange textBuffer,
+                                    [listElements[selStart].eltStart, listElements[selEnd-1].eltEnd])

   cutCmd: ->
     console.log 'Executing command list-edit-cut'
@@ -67,77 +56,48 @@
     #    if cut == newElts.length # newElts.length > 1, so newElts[n-1] exists, not first and no need to fix pre
     #      newElts[n-1].post = cutElts[m].post

-    @withSelectedList (editor, textBuffer, bufferText, selectionIxRange, listElements) ->
-      if listElements.length == 0
-        atom.notifications.addWarning 'List cut in empty list.'
-        # TODO: clear the clipboard here?
+    @withSelectedList (editor, textBuffer, bufferText, selectionIxRange, listElements, [cutStart, cutEnd]) ->
+      # TODO: refactor copy so we can call it here
+      console.log 'List elements'
+      console.log e.show() for e in listElements
+      console.log 'Selection: ' + cutStart + ' <-> ' + cutEnd
+
+      # copy selected elements to clipboard
+      selectionText = bufferText.slice listElements[cutStart].eltStart, listElements[cutEnd-1].eltEnd
+      atom.clipboard.write selectionText, @mkListEditMeta()
+
+      # elts[0 .. cutStart .. cutEnd .. n-1]
+      newLength = listElements.length - (cutEnd - cutStart) # +1 since cut range is inclusive
+      console.log newLength
+
+      if newLength == 0
+        cutIxRange = [listElements[cutStart].start, listElements[cutEnd-1].end]
+        newWhitespace = ''
       else
-        # TODO: move to wrapper
-        [selStart,selEnd] = TextManipulation.getSelectionForRange listElements, selectionIxRange
-
-        # TODO: probably want to put this in withSelectedList
-        if selEnd > listElements.length
-          atom.notifications.addWarning 'List selection end outside list.'
-          # won't happen for start, since we use this to select the list in the first place
-          # TODO: maybe make this less strict, as selection in sublists is now asymmetric:
-          #       in "[1,[a,b],2]": "[a," selects entire sublist element, but ",b]" fails with warning.
-        else
-          # TODO: refactor copy so we can call it here
-          cutStart = selStart
-          cutEnd = selEnd
-          console.log 'List elements'
-          console.log e.show() for e in listElements
-          console.log 'Selection: ' + cutStart + ' <-> ' + cutEnd
-
-          # copy selected elements to clipboard
-          selectionText = bufferText.slice listElements[cutStart].eltStart, listElements[cutEnd-1].eltEnd
-          atom.clipboard.write selectionText, @mkListEditMeta()
-
-          # elts[0 .. cutStart .. cutEnd .. n-1]
-          newLength = listElements.length - (cutEnd - cutStart) # +1 since cut range is inclusive
-          console.log newLength
-
-          if newLength == 0
-            cutIxRange = [listElements[cutStart].start, listElements[cutEnd-1].end]
+        if cutStart == 0  # newLength > 0, so not the last one and no need to fix post (and elts[cutEnd] exists)
+          cutIxRange = [ listElements[0].start, listElements[cutEnd].eltStart ]
+          newWhitespace = listElements[0].leadingWhitespace  # newElts[0].pre = cutElts[0].pre
+        else # listElements[cutStart-1] exists
+          if cutEnd < listElements.length
+            cutIxRange = [ listElements[cutStart-1].end, listElements[cutEnd-1].end] # remove from preceding separator until post whitespace of last cut elt
             newWhitespace = ''
           else
-            if cutStart == 0  # newLength > 0, so not the last one and no need to fix post (and elts[cutEnd] exists)
-              cutIxRange = [ listElements[0].start, listElements[cutEnd].eltStart ]
-              newWhitespace = listElements[0].leadingWhitespace  # newElts[0].pre = cutElts[0].pre
-            else # listElements[cutStart-1] exists
-              if cutEnd < listElements.length
-                cutIxRange = [ listElements[cutStart-1].end, listElements[cutEnd-1].end] # remove from preceding separator until post whitespace of last cut elt
-                newWhitespace = ''
-              else
-                cutIxRange = [ listElements[cutStart-1].eltEnd, listElements[cutEnd-1].end]
-                newWhitespace = listElements[cutEnd-1].trailingWhitespace   #newlistElements[n-1].post = cutElts[m].post
-          console.log 'cut index range:' + cutIxRange
-          console.log 'inserted: "' + newWhitespace + '"'
-          cutRange = TextManipulation.getRangeForIxRange textBuffer, cutIxRange
-          console.log cutRange.start
-          # editor.setSelectedBufferRange cutRange # for debugging: select the range that will be cut
-          textBuffer.setTextInRange cutRange, newWhitespace
+            cutIxRange = [ listElements[cutStart-1].eltEnd, listElements[cutEnd-1].end]
+            newWhitespace = listElements[cutEnd-1].trailingWhitespace   #newlistElements[n-1].post = cutElts[m].post
+      console.log 'cut index range:' + cutIxRange
+      console.log 'inserted: "' + newWhitespace + '"'
+      cutRange = TextManipulation.getRangeForIxRange textBuffer, cutIxRange
+      console.log cutRange.start
+      # editor.setSelectedBufferRange cutRange # for debugging: select the range that will be cut
+      textBuffer.setTextInRange cutRange, newWhitespace

   copyCmd: ->
     console.log 'Executing command list-edit-copy'
-    @withSelectedList (editor, textBuffer, bufferText, selectionIxRange, listElements) ->
-      if listElements.length == 0
-        atom.notifications.addWarning 'List copy in empty list.'
-        # TODO: clear the clipboard here?
-      else
-        [selStart,selEnd] = TextManipulation.getSelectionForRange listElements, selectionIxRange
-
-        # TODO: probably want to put this in withSelectedList
-        if selEnd > listElements.length
-          atom.notifications.addWarning 'List selection end outside list.'
-          # won't happen for start, since we use this to select the list in the first place
-          # TODO: maybe make this less strict, as selection in sublists is now asymmetric:
-          #       in "[1,[a,b],2]": "[a," selects entire sublist element, but ",b]" fails with warning.
-        else
-          selectionText = bufferText.slice listElements[selStart].eltStart, listElements[selEnd-1].eltEnd
-          # Clip includes separators, which seems logical when we use it for a non-list paste
-          #console.log "Copied: '#{selectionText}'"
-          atom.clipboard.write selectionText, @mkListEditMeta()
+    @withSelectedList (editor, textBuffer, bufferText, selectionIxRange, listElements, [selStart,selEnd]) ->
+      selectionText = bufferText.slice listElements[selStart].eltStart, listElements[selEnd-1].eltEnd
+      # Clip includes separators, which seems logical when we use it for a non-list paste
+      #console.log "Copied: '#{selectionText}'"
+      atom.clipboard.write selectionText, @mkListEditMeta()

   pasteCmd: ->
     console.log 'Executing command list-edit-paste'
@@ -180,7 +140,17 @@
       if not listElements?
         atom.notifications.addWarning 'List selection outside list.'
       else
-        (f.bind this) editor, textBuffer, bufferText, selectionIxRange, listElements
+        if listElements.length == 0
+          atom.notifications.addWarning 'List selection in empty list.'
+        else
+          listSelection = TextManipulation.getSelectionForRange listElements, selectionIxRange
+          if listSelection.end > listElements.length
+            atom.notifications.addWarning 'List selection end outside list.'
+            # won't happen for start, since we use this to select the list in the first place
+            # TODO: maybe make this less strict, as selection in sublists is now asymmetric:
+            #       in "[1,[a,b],2]": "[a," selects entire sublist element, but ",b]" fails with warning.
+          else
+            (f.bind this) editor, textBuffer, bufferText, selectionIxRange, listElements, listSelection

   # TODO: store separator, so we can handle switching elements in list of two
   # Maybe also store first pre (trailing opening bracket), last post (leading closing bracket), one middle post pre (leading, trailing separator),


Keep initial and final whitespace in clipMeta

--- lib/list-edit.coffee
+++ lib/list-edit.coffee
@@ -42,7 +42,12 @@

   cutCmd: ->
     console.log 'Executing command list-edit-cut'
-    @withSelectedList (editor, textBuffer, bufferText, selectionIxRange, {openBracket: openBracket, separator: separator, elts: listElements}, [cutStart, cutEnd]) ->
+    @withSelectedList (editor, textBuffer, bufferText, selectionIxRange, elementList, [cutStart, cutEnd]) ->
+      { startIx: listStartIx, endIx: listEndIx
+      , openBracket: openBracket, separator: separator
+      , initialWhitespace: initialWhitespace, finalWhitespace: finalWhitespace
+      , elts: listElements
+      } = elementList
       if listElements.length == 0
         atom.notifications.addWarning 'List cut in empty list.'
       else
@@ -57,7 +62,7 @@

           # copy selected elements to clipboard
           selectionText = bufferText.slice listElements[cutStart].eltStart, listElements[cutEnd-1].eltEnd
-          atom.clipboard.write selectionText, @mkListEditMeta(openBracket, separator)
+          atom.clipboard.write selectionText, @mkListEditMeta(openBracket, separator, initialWhitespace, finalWhitespace)

           if cutStart == 0
             if cutEnd == listElements.length
@@ -87,7 +92,12 @@

   copyCmd: ->
     console.log 'Executing command list-edit-copy'
-    @withSelectedList (editor, textBuffer, bufferText, selectionIxRange, {openBracket: openBracket, separator: separator, elts: listElements}, [selStart,selEnd]) ->
+    @withSelectedList (editor, textBuffer, bufferText, selectionIxRange, elementList, [selStart,selEnd]) ->
+      { startIx: listStartIx, endIx: listEndIx
+      , openBracket: openBracket, separator: separator
+      , initialWhitespace: initialWhitespace, finalWhitespace: finalWhitespace
+      , elts: listElements
+      } = elementList
       if listElements.length == 0
         atom.notifications.addWarning 'List copy in empty list.'
       else
@@ -99,7 +109,7 @@
           # Clip includes separators, which seems logical when we use it for a non-list paste
           #console.log "Copied: '#{selectionText}'"
           console.log 'Separator: ' + JSON.stringify separator
-          atom.clipboard.write selectionText, @mkListEditMeta(openBracket, separator)
+          atom.clipboard.write selectionText, @mkListEditMeta(openBracket, separator, initialWhitespace, finalWhitespace)
           editor.setSelectedBufferRange (TextManipulation.getRangeForIxRange textBuffer,
                                            [listElements[selStart].eltStart, listElements[selEnd-1].eltEnd])

@@ -145,7 +155,7 @@
           if listElements.length == 0
             console.log 'Paste in empty list'
             pasteIxRange = [listStartIx, listEndIx]
-            pasteText = ' ' + clip.text + ' ' # TODO: try to take pre and post from clipboard list?
+            pasteText = clipMeta.initialWhitespace + clip.text + clipMeta.finalWhitespace
           else
             console.log 'Paste on non-empty list'
             if selStart < listElements.length
@@ -181,8 +191,12 @@
           # Can also use => in the definition of the callbacks that require @, but this is safer
           (callback.bind this) editor, textBuffer, bufferText, selectionIxRange, elementList, listSelection

-  mkListEditMeta: (openBracket, separator) ->
-    {id: 'list-edit-clip-meta', openBracket: openBracket, separator: separator}
+  # TODO: Probably easier to just pass the entire elementList
+  mkListEditMeta: (openBracket, separator, initialWhitespace, finalWhitespace) ->
+    { id: 'list-edit-clip-meta', openBracket: openBracket
+    , initialWhitespace: initialWhitespace, finalWhitespace: finalWhitespace
+    , separator: separator
+    }

   getListEditMeta: (clip) ->
     clip.metadata if clip?.metadata?.id == 'list-edit-clip-meta'


Manually close editors for files deleted from tree view

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -568,7 +568,11 @@
         "Move to Trash": =>
           failedDeletions = []
           for selectedPath in selectedPaths
-            if not shell.moveItemToTrash(selectedPath)
+            if shell.moveItemToTrash(selectedPath)
+              for editor in atom.workspace.getTextEditors()
+                if editor?.getPath() is selectedPath
+                  editor.destroy()
+            else
               failedDeletions.push "#{selectedPath}"
             if repo = repoForPath(selectedPath)
               repo.getPathStatus(selectedPath)


Add automatically-generated updates

Add automatically-generated updates

--- etc/atom/config.cson
+++ etc/atom/config.cson
@@ -12,6 +12,7 @@
     'showIndentGuide': true
     'softWrapAtPreferredLineLength': true
     'fontSize': 14
+    'invisibles': {}
   'core':
     'themes': [
       'atom-dark-ui'
@@ -24,4 +25,3 @@
     'autocompleteBrackets': false
   'find-and-replace':
     'focusEditorAfterSearch': true
-  'docblockr': {}


setting lower limit on zooming for base plotting class

--- imports/charts/Zoom.coffee
+++ imports/charts/Zoom.coffee
@@ -95,6 +95,8 @@

     y1 = @plot.axes.yScale.invert(pos[1]);
     y2 = @plot.axes.yScale.invert(@bandPos[1])
+    if y1 < 0 then y1 = 0
+    if y2 < 0 then y2 = 0
     if y1 < y2
       @zoomArea.y1 = y1
       @zoomArea.y2 = y2


Fix the count() test

Fix the count() test

--- tests/batman/enumerable_test.coffee
+++ tests/batman/enumerable_test.coffee
@@ -3,7 +3,10 @@
   constructor: (args...) ->
     @a = []
     @a.push arg for arg in args
-  add: (e) -> @a.push e
+    @length = @a.length
+  add: (e) ->
+    @a.push e
+    @length = @a.length
   forEach: (f) ->
     @a.forEach(f)

@@ -146,7 +149,7 @@
   @enumerable = getEnumerable(@array)
   equal @enumerable.count(f), 2

-test "count should return the total length if passes no arguments", ->
+test "count should return the total length if passed no arguments", ->
   @array = [false, false, false]
   @enumerable = getEnumerable(@array)
   equal @enumerable.count(), 3


never mind -- the previous was right; I'm too tired right now.

--- salvus/storage_server.coffee
+++ salvus/storage_server.coffee
@@ -26,7 +26,7 @@

 STATE_CONSISTENCY = cql.types.consistencies.two

-REGISTRATION_INTERVAL_S = 15       # register with the database every 20 seconds
+REGISTRATION_INTERVAL_S = 15       # register with the database every this many seconds
 REGISTRATION_TTL_S      = 60       # ttl for registration record

 #TIMEOUT = 30*60   # default timeout on all locking zvol_storage operations (most use ZFS).
@@ -906,7 +906,7 @@
                         cb(err)
                     else
                         # randomize amongst servers with the same health and queue length
-                        r = ([x.health, -x.zfs_queue_len, Math.random(), x] for x in results when x.port? and x.host? and x.up_since?)
+                        r = ([x.health, x.zfs_queue_len, Math.random(), x] for x in results when x.port? and x.host? and x.up_since?)
                         r.sort()
                         if r.length == 0
                             cb("no available hosts")


testing: added few more tests regarding misc

--- salvus/test/misc-test.coffee
+++ salvus/test/misc-test.coffee
@@ -7,6 +7,58 @@
         expect(startswith("foobar",'foo')).toBe(true)
     it 'checks that "foobar" does not start with bar', ->
         expect(startswith("foobar",'bar')).toBe(false)
+    it 'checks that "bar" starts in any of the given strings (a list)', ->
+        expect(startswith("barbatz", ["aa", "ab", "ba", "bb"])).toBe(true)
+    it 'checks that "catz" does not start with any of the given strings (a list)', ->
+        expect(startswith("catz", ["aa", "ab", "ba", "bb"])).toBe(false)
+
+describe 'random_choice and random_choice_from_obj', ->
+    rc = misc.random_choice
+    rcfo = misc.random_choice_from_obj
+    it 'checks that a randomly chosen element is in the given list', ->
+        l = ["a", 5, 9]
+        el = rc(l)
+        expect(l).toContain(el)
+    it 'checks that random choice works with only one element', ->
+        expect(rc([123])).toBe(123)
+    it 'checks that random choice with no elements is also fine', ->
+        expect(rc([])).toBe(undefined)
+    it 'checks that a randomly chosen key/value pair from an object exists', ->
+        o = {abc : [1, 2, 3], cdf : {a: 1, b:2}}
+        expect([["abc", [1, 2, 3]], ["cdf" , {a: 1, b:2}]]).toContain(rcfo(o))
+
+describe 'the Python flavoured randint function', ->
+    randint = misc.randint
+    it 'checks probabilistically that randint is inclusive', ->
+        lb = -4; ub = 7
+        xmin = xmax = 0
+        for i in [1..1000]
+            x = randint(lb, ub)
+            expect(lb <= x <= ub).toBe(true)
+            xmin = Math.min(xmin, x)
+            xmax = Math.max(xmax, x)
+        expect(xmin).toBe(lb)
+        expect(xmax).toBe(ub)
+    it 'checks randint behaves well for tight intervals', ->
+        expect(randint(91, 91)).toBe(91)
+    # note: in python, this fails
+    it 'checks how randint behaves with flipped intervals bounds', ->
+        lb = 4; ub = -7
+        for i in [1..100]
+            x = randint(lb, ub)
+            expect(lb >= x >= ub).toBe(true)
+
+describe 'the Python flavoured split function', ->
+    split = misc.split
+    it 'checks splits on whitespace', ->
+        s = "this is a   sentence"
+        expect(split(s)).toEqual(["this", "is", "a", "sentence"])
+    it "checks split's behaviour on linebreaks and special characters", ->
+        s2 = """we'll have
+               a lot (of)
+               fun with sp|äci|al cħæ¶ä¢ŧ€rß"""
+        expect(split(s2)).toEqual(["we'll", "have", "a", "lot", "(of)",
+                                   "fun", "with", "sp|äci|al", "cħæ¶ä¢ŧ€rß"])

 describe 'merge', ->
     merge = misc.merge
@@ -20,6 +72,12 @@
         b = {x:3}
         merge(a, b)
         expect(a).toEqual({x:3, y:2})
+    it 'variable number of arguments are supported', ->
+        a = {x:1}; b = {y:2}; c = {z:3}; d = {u:4}; w ={v:5, x:0}
+        r = merge(a, b, c, d, w)
+        res = {x:0, y:2, z:3, u:4, v:5}
+        expect(r).toEqual(res)
+        expect(a).toEqual(res)

 describe 'cmp', ->
     cmp = misc.cmp


Solve undefined for App.Common when is no App defined

--- lib/assets/javascript/init.js.coffee
+++ lib/assets/javascript/init.js.coffee
@@ -32,7 +32,7 @@

   execFilter: (filterName) ->
     this.appName()
-    if App.Common and typeof App.Common[filterName] == 'function'
+    if App and App.Common and typeof App.Common[filterName] == 'function'
       App.Common[filterName]()

 jQuery ->


Cleaned tests.

Cleaned tests.

--- tests.coffee
+++ tests.coffee
@@ -1,7 +1,7 @@
-allCollections = []
-
 for idGeneration in ['STRING', 'MONGO']
   do (idGeneration) ->
+    allCollections = []
+
     if idGeneration is 'STRING'
       generateId = ->
         Random.id()
@@ -10,12 +10,13 @@
         new Meteor.Collection.ObjectID()

     Users = new Mongo.Collection "Users_meteor_reactivepublish_tests_#{idGeneration}", {idGeneration}
+    Posts = new Mongo.Collection "Posts_meteor_reactivepublish_tests_#{idGeneration}", {idGeneration}
+    Addresses = new Mongo.Collection "Addresses_meteor_reactivepublish_tests_#{idGeneration}", {idGeneration}
+    Fields = new Mongo.Collection "Fields_meteor_reactivepublish_tests_#{idGeneration}", {idGeneration}
+
     allCollections.push Users
-    Posts = new Mongo.Collection "Posts_meteor_reactivepublish_tests_#{idGeneration}", {idGeneration}
     allCollections.push Posts
-    Addresses = new Mongo.Collection "Addresses_meteor_reactivepublish_tests_#{idGeneration}", {idGeneration}
     allCollections.push Addresses
-    Fields = new Mongo.Collection "Fields_meteor_reactivepublish_tests_#{idGeneration}", {idGeneration}
     allCollections.push Fields

     if Meteor.isServer
@@ -902,31 +903,26 @@
           @assertEqual LocalCollection.find({}).count(), 15
       ]

-      multiplexerCountBefore = 0
-      multiplexerCountAfter = 0
-      @unblockedPub: (publishName) -> [
+      testClientUnblockedPublish: [
         @runOnServer ->
-          multiplexerCountBefore = 0
-          for collection in allCollections
-            if collection
-              multiplexerCountBefore += Object.keys(collection._driver.mongo._observeMultiplexers).length
+          @multiplexerCountBefore = 0
+          for collection in allCollections when collection
+            @multiplexerCountBefore += Object.keys(collection._driver.mongo._observeMultiplexers).length
+      ,
         ->
           @userId = generateId()
-          handle = @subscribe "#{publishName}_#{idGeneration}", @userId
-          subscriptionId = handle.subscriptionId
+          handle = @subscribe "unblocked-users-posts_#{idGeneration}", @userId
           handle?.stop()

           Meteor.setTimeout @expect(), 1000 # ms
       ,
         @runOnServer ->
           multiplexerCountAfter = 0
-          for collection in allCollections
-            if collection
-              multiplexerCountAfter += Object.keys(collection._driver.mongo._observeMultiplexers).length
-          @assertEqual multiplexerCountBefore, multiplexerCountAfter
+          for collection in allCollections when collection
+            multiplexerCountAfter += Object.keys(collection._driver.mongo._observeMultiplexers).length
+
+          @assertEqual @multiplexerCountBefore, multiplexerCountAfter
       ]
-
-      testClientUnblockedBasicAutorun: @unblockedPub 'unblocked-users-posts'

     # Register the test case.
     ClassyTestCase.addTest new ReactivePublishTestCase()


Troubleshoot: troubleshoot.sendFeedback is called within TroubleshootModal callback. Modal is shown with successfull attempt

--- client/Main/troubleshoot/troubleshootmodal.coffee
+++ client/Main/troubleshoot/troubleshootmodal.coffee
@@ -13,7 +13,9 @@
             Troubleshoot      :
               callback        : =>
                 {feedback} = @modalTabs.forms.Troubleshoot.customData
-                KD.logToExternal "troubleshoot feedback", {failure:troubleshoot.getFailureFeedback(), feedback}
+                troubleshoot.sendFeedback feedback, (err) =>
+                  return warn "an error occured while sending feedback"  if err
+                  @showFeedbackSentModal()
                 @destroy()
               buttons         :
                 sendFeedback  :
@@ -87,3 +89,13 @@
       item.off "recoveryStarted"
       item.off "recoveryCompleted"
     super
+
+  showFeedbackSentModal: ->
+    modal = new KDModalView
+      title        : "Thank you for your support"
+      overlay      : yes
+      content      : "We have received your feedback."
+      buttons      :
+        Close      :
+          style    : "modal-cancel"
+          callback : -> modal.destroy()


ComputeController: Remove stopCollaboration method

--- client/app/lib/providers/computecontroller.coffee
+++ client/app/lib/providers/computecontroller.coffee
@@ -494,8 +494,6 @@

     destroy = (machine)=>

-      @stopCollaborationSession machine.uid
-
       baseKite = machine.getBaseKite( createIfNotExists = no )
       if machine?.provider is 'managed' and baseKite.klientDisable?
       then baseKite.klientDisable().finally -> baseKite.disconnect()
@@ -554,8 +552,6 @@
     return  if methodNotSupportedBy machine, 'reinit'

     startReinit = =>
-
-      @stopCollaborationSession machine.uid

       machine.getBaseKite( createIfNotExists = no ).disconnect()

@@ -1102,15 +1098,6 @@
       return callback null, template

-  ###*
-   * Automatically kill active collaboration sessions if any
-  ###
-  stopCollaborationSession: (machineUId) ->
-
-    ideApp = envDataProvider.getIDEFromUId machineUId
-    ideApp?.stopCollaborationSession()
-
-
   showBuildLogs: (machine) ->

     # Not supported for Koding Group


Added .settings-icon to span selector

Added .settings-icon to span selector

--- client/test/lib/helpers/environmenthelpers.coffee
+++ client/test/lib/helpers/environmenthelpers.coffee
@@ -416,8 +416,8 @@
     browser
       .waitForElementVisible  sidebarSelector, 20000
       .moveToElement          sidebarSelector, 10, 10
-      .waitForElementVisible  "#{sidebarSelector} span", 20000
-      .click                  "#{sidebarSelector} span"
+      .waitForElementVisible  "#{sidebarSelector} span.settings-icon", 20000
+      .click                  "#{sidebarSelector} span.settings-icon"
       .element 'css selector', "#{alwaysOnSelector}.off", (result) ->
         if result.status is 0
           browser
@@ -461,8 +461,8 @@
     browser
       .waitForElementVisible  sidebarSelector, 20000
       .moveToElement          sidebarSelector, 10, 10
-      .waitForElementVisible  "#{sidebarSelector} span", 20000
-      .click                  "#{sidebarSelector} span"
+      .waitForElementVisible  "#{sidebarSelector} span.settings-icon", 20000
+      .click                  "#{sidebarSelector} span.settings-icon"
       .pause                  1000
       .element 'css selector', "#{alwaysOnSelector}.off", (result) ->
         if result.status is 0


more tests - but the example is broken since we"re trying to do nested groupBy"s

--- test/aggregate.coffee
+++ test/aggregate.coffee
@@ -1,9 +1,54 @@
 expect = require("chai").expect
-aggregateFunctions = require '../src/aggregate'
+Aggregate = require '../src/aggregate'

 describe 'groupBy()', ->
-  it 'should return an array with', ->
-    it 'a "data" property', ->
+  describe 'when passed an object and a field on which to groupBy', ->

-      returnedObject = aggregateFunctions.groupBy [{a:1}]
-      expect(returnedObject).to.have.property 'data'+    returnedObject = null #trick to make variable available to 'beforeEach' and the 'it's
+
+    beforeEach ->
+      returnedObject = Aggregate.groupBy [{a:1}], 'a'
+
+    it 'should return an array', ->
+
+      expect(returnedObject).to.be.instanceof Array
+
+      describe 'whose elements contain', ->
+
+        it 'a "data" property', ->
+          expect(returnedObject[0]).to.have.property 'data'
+
+        it 'a "key" property', ->
+          expect(returnedObject[0]).to.have.property 'key'
+
+  describe 'when passed objects', ->
+      manyObjects = null
+
+      beforeEach ->
+        manyObjects = [
+          name : 'brett'
+          city : 'nyc'
+          eyeColor :'blue'
+          score : 0
+        ,
+          name : 'jill'
+          city : 'nyc'
+          eyeColor :'blue'
+          score : 10
+        ,
+          name : 'jak'
+          city : 'nyc'
+          eyeColor :'green'
+          score : 20
+        ,
+          name : 'lea'
+          city : 'philly'
+          eyeColor :'green'
+          score : 30
+        ]
+
+      describe 'with an average score of 10 in nyc', ->
+        it 'should return an average score of 10', ->
+          averages = Aggregate.groupBy(manyObjects, 'city').average('score')
+          nycGrouping = (averages.filter (averageObject) -> averageObject.key == 'nyc')[0]
+          expect(nycGrouping.average.score).to.equal(10)


update config file for changes s3 settings

--- services/track-changes/config/settings.defaults.coffee
+++ services/track-changes/config/settings.defaults.coffee
@@ -23,13 +23,13 @@
 			port: 6379
 			pass: ""

-	filestore:
-		backend: "s3"
-		stores:
-			user_files: ""
+	trackchanges:
 		s3:
 			key: ""
 			secret: ""
+		stores:
+			doc_history: ""
+

 	path:
 		dumpFolder:   Path.join(TMP_DIR, "dumpFolder")


remove confirmFolder from project handler, not used.

--- services/web/app/coffee/handlers/ProjectHandler.coffee
+++ services/web/app/coffee/handlers/ProjectHandler.coffee
@@ -18,17 +18,6 @@
 tagsHandler = require('../Features/Tags/TagsHandler')

 module.exports = class ProjectHandler
-
-	confirmFolder = (project_id, folder_id, callback)->
-		logger.log folder: folder_id, project_id: project_id, "confirming existence of folder"
-		if folder_id+'' == 'undefined'
-			Project.findById project_id, (err, project)->
-				callback(project.rootFolder[0]._id)
-		else if folder_id != null
-			callback folder_id
-		else
-			Project.findById project_id, (err, project)->
-				callback(project.rootFolder[0]._id)

 	renameEntity: (project_id, entity_id, entityType, newName, callback)->
 		logger.log(entity_id: entity_id, project_id: project_id, ('renaming '+entityType))


this is gonna get tired

this is gonna get tired

--- data/.atom/config.cson
+++ data/.atom/config.cson
@@ -1,16 +1,8 @@
-'editor':
-  'fontSize': 11
-  'showIndentGuide': true
-  'softWrap': true
-  'softWrapAtPreferredLineLength': true
-'core':
-  'themes': [
-    'atom-dark-ui'
-    'atom-dark-syntax'
-  ]
 'exception-reporting':
-  'userId': '80863757-0744-9a5a-e24b-0e7a14699772'
+  'userId': '5dab82a5-60b9-b5d8-93b0-6cbbc9af46f5'
 'welcome':
   'showOnStartup': false
 'metrics':
   'userId': 'f45f6f01061a25c864d112b6d1a2f7ac2ce4591d'
+'editor':
+  'fontSize': 13


codemirror sync: sufficient refactoring that finally... we are ready to actually do something

--- salvus/page/editor.coffee
+++ salvus/page/editor.coffee
@@ -589,11 +589,19 @@
         # there were edits as an optimization
         return diffsync.dmp.patch_make(@string(), v1.string())

-    patch: (d) =>
-        return new CodeMirrorDiffSyncDoc(string: diffsync.dmp.patch_apply(d, @.string())[0])
+    patch: (p) =>
+        return new CodeMirrorDiffSyncDoc(string: diffsync.dmp.patch_apply(p, @string())[0])

     checksum: () =>
         return @string().length
+
+    patch_in_place: (p) =>
+        if @opts.string
+            console.log("patching string in place")  # should never need to happen
+            @opts.string = diffsync.dmp.patch_apply(p, @string())[0]
+        else
+            console.log("patching codemirror in place")
+            @opts.cm.setValue(diffsync.dmp.patch_apply(p, @string())[0])

 codemirror_diffsync_client = (cm_session, content) ->
@@ -606,16 +614,19 @@
         return v0.patch(d)
     checksum  = (s) ->
         return s.checksum()
+    patch_in_place = (p, v0) ->
+        v0.patch_in_place(p)

     #patch_in_place = (d, s) -> s.doc = dmp.patch_apply(d, s.doc)[0]  # modifies s.doc inside object

     cm_session.codemirror.setValue(content)
     return new diffsync.CustomDiffSync
-        doc      : new CodeMirrorDiffSyncDoc(cm:cm_session.codemirror)
-        copy     : copy
-        diff     : diff
-        patch    : patch
-        checksum : checksum
+        doc            : new CodeMirrorDiffSyncDoc(cm:cm_session.codemirror)
+        copy           : copy
+        diff           : diff
+        patch          : patch
+        checksum       : checksum
+        patch_in_place : patch_in_place

 # The CodeMirrorDiffSyncHub class represents a global hub viewed as a
 # remote server for this client.


Fix another sketch masking bug

Fix another sketch masking bug

--- framer/Importer.coffee
+++ framer/Importer.coffee
@@ -80,7 +80,7 @@
 		# Possible fix for images that have a mask in Sketch
 		# So if a layer without children has a mask, Sketch imports the full image
 		# That is whay we then take the image frame over the mask frame again
-		if superLayer?.clip and info.children.length is 0
+		if info.children.length is 0 and true in _.pluck(superLayer?.superLayers(), "clip")
 			layerInfo.frame = info.image.frame
 			layerInfo.clip = false


Fix startup message

Fix startup message

--- app._coffee
+++ app._coffee
@@ -99,6 +99,6 @@

 if module is require.main
     app.listen config.PORT
-    console.log "ZoomHub running at #{config.BASE_URL}:#{config.PORT}/"
+    console.log "ZoomHub running at #{config.BASE_URL}"
 else
     module.exports = app


Sidebar: ignore clicks on current item

Sidebar: ignore clicks on current item

--- app/assets/javascripts/lib/views/sidebar_view.coffee
+++ app/assets/javascripts/lib/views/sidebar_view.coffee
@@ -27,6 +27,8 @@
     $(document).on 'click', "a[data-nav=true]", (e) ->
       e.preventDefault()
       target = $(e.target)
+      # Don't do anything if it's already active
+      return if target.parents("li").hasClass("active")
       $("#title").busyBox
         spinner: "Loading"
       $("ul.accordion").fadeOut(100)


:bug: Fix duplicated event binding

:bug: Fix duplicated event binding

--- app/assets/javascripts/modules/table-row-link.coffee
+++ app/assets/javascripts/modules/table-row-link.coffee
@@ -1,6 +1,6 @@
 class @TableRowLink
   constructor: ->
     # Make rows (means: cells with class js-row-link) clickable
-    $(document).on 'click', 'tr[data-href] > td.js-row-link', ->
+    $(document).off('click').on 'click', 'tr[data-href] > td.js-row-link', ->
       Turbolinks.visit $(this).parent().attr('data-href')
       return


move the checks below

move the checks below

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -325,15 +325,6 @@
   variableCounter = 1
   compileWheresInModule typifyDefinitions astize tokenize "(#{source})"

-isList = (node) ->
-  Array.isArray(node) and (node[0].token is '[')
-
-isTuple = (node) ->
-  Array.isArray(node) and (node[0].token is '{')
-
-isMap = (node) ->
-  isTuple(node) and (elems = inside node; elems.length > 0) and elems[0].label is 'label'
-
 compileImpl = (node) ->
   switch node.type
     when 'comment' then 'null'
@@ -364,6 +355,15 @@
         node.token
       else
         validIdentifier node.token
+
+isList = (node) ->
+  Array.isArray(node) and (node[0].token is '[')
+
+isTuple = (node) ->
+  Array.isArray(node) and (node[0].token is '{')
+
+isMap = (node) ->
+  isTuple(node) and (elems = inside node; elems.length > 0) and elems[0].label is 'label'

 compileMap = (elems) ->
   [constr, args...] = elems


Properly updating and collecting logs

Properly updating and collecting logs

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -195,6 +195,7 @@
     @_isOperator = []
     @variableIndex = 0
     @typeVariabeIndex = 0
+    @logId = 0
     @nameIndex = 1
     @substitution = emptySubstitution()
     @statement = []
@@ -2646,7 +2647,7 @@
   compiled = termsCompile ctx, args
   call.tea = value.tea
   view = map (__ (__ toMultilineJsString, collapse), toHtml), args
-  assignCompile ctx, call, (jsCall "debugLog", join view, compiled)
+  assignCompile ctx, call, (jsCall "debugLog", concat [ctx.logId++, view, compiled])

 ms['=='] = ms_eq = (ctx, call) ->
     [a, b] = _arguments call


fix an error message when moving a file

fix an error message when moving a file

--- salvus/page/project.coffee
+++ salvus/page/project.coffee
@@ -2270,7 +2270,7 @@
                     path       : '.'
                     cb         : (err, output) =>
                         if err
-                            alert_message(type:"error", message:"Error moving #{new_src} to #{new_dest} -- #{output.stderr}")
+                            alert_message(type:"error", message:"Error moving #{new_src} to #{new_dest} -- #{err}")
                         else
                             alert_message(type:"success", message:"Successfully moved #{new_src} to #{new_dest}")
                             if path == @current_pathname()


Reorder imports, for now relying on standard ordering of object keys

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -4056,7 +4056,7 @@
     errors: checkTypes ctx

 contextWithDependencies = (moduleName) ->
-  toInject = concatSets (collectRequiresFor moduleName), (newSetWith moduleName)
+  toInject = concatSets (newSetWith moduleName), (collectRequiresFor moduleName)
   ctx: injectedContext toInject
   modules: setToArray toInject

@@ -4096,7 +4096,7 @@
       ctx._macros[name] = macro
   topScope = ctx._scope()
   for name, definition of values definitions
-    replaceOrAddToMap topScope, name, definition
+    addToMap topScope, name, definition
   topScope.typeNames = concatMaps topScope.typeNames, typeNames
   topScope.classes = concatMaps topScope.classes, classes
   ctx


sync: *major* improvements!  Make it so sync iterates until stabilizing; this cleans up code, and also fixes the "weirdness/merge on reconnect" issues properly

--- salvus/page/syncdoc.coffee
+++ salvus/page/syncdoc.coffee
@@ -273,9 +273,11 @@
     # "sync(cb)": keep trying to synchronize until success; then do cb()
     # _sync(cb) -- try once to sync; on any error cb(err).
     _sync: (cb) =>
-        #console.log("_sync")
         @_presync?()
-        snapshot = @live()
+        before = @live()
+        if before.string?
+            before = before.string()
+        #console.log("_sync, live='#{before}'")
         @dsync_client.push_edits (err) =>
             if err
                 if typeof(err)=='string' and err.indexOf('retry') != -1
@@ -288,12 +290,29 @@
                     #console.log("connect: due to sync error: #{err}")
                     @connect () =>
                         cb?(err)
+                #console.log("_sync: error -- #{err}")
             else
-                s = snapshot
+                # Emit event that indicates document changed as
+                # a result of sync; it's critical to do this even
+                # if we're not done syncing, since it's used, e.g.,
+                # by Sage worksheets to render special codes.
+                @emit('sync')
+
+                s = @live()
                 if s.copy?
                     s = s.copy()
                 @_last_sync = s    # What was the last successful sync with upstream.
-                @emit('sync')
+
+                after = @live()
+                if after.string?
+                    after = after.string()
+                if before != after
+                    #console.log("change during sync so doing again")
+                    cb?("file changed during sync")
+                    return
+
+                # success!
+                #console.log("_sync: success")
                 cb?()

     # save(cb): write out file to disk retrying until success = worked *and* what was saved to disk eq.
@@ -512,6 +531,7 @@
                 @sync()
                 @init_cursorActivity_event()
                 @codemirror.on 'change', (instance, changeObj) =>
+                    #console.log("change event when live='#{@live().string()}'")
                     if changeObj.origin?
                         if changeObj.origin == 'undo'
                             @on_undo(instance, changeObj)
@@ -519,6 +539,7 @@
                             @on_redo(instance, changeObj)
                         if changeObj.origin != 'setValue'
                             @ui_synced(false)
+                            #console.log("change event causing call to sync")
                             @sync()
             # Done initializing and have got content.
             cb?()
@@ -611,7 +632,7 @@
                     # applying missed patches to the new upstream version that we just got from the hub.
                     #console.log("now applying missed patches to the new upstream version that we just got from the hub: ", patch)
                     @_apply_patch_to_live(patch)
-                    @emit 'sync'
+                    @emit('sync')

                 if @opts.revision_tracking
                     @call
@@ -2115,19 +2136,7 @@
                     break

             @set_cell_flag(marker, FLAGS.execute)
-            # sync (up to a certain number of times) until either computation happens or is acknowledged.
-            # Just successfully calling sync could return and mean that a sync started
-            # before this computation started had completed.
-            wait = 50
-            f = () =>
-                fs = @get_cell_flagstring(marker)
-                if not fs? or FLAGS.execute in fs
-                    @sync () =>
-                        wait = wait*1.4
-                        if wait < 15000
-                            setTimeout(f, wait)
-            @sync () =>
-                setTimeout(f, wait)
+            @sync()

     # purely client-side markdown rendering for a markdown, javascript, html, etc. block -- an optimization
     execute_cell_client_side: (opts) =>


Use precise equality on sequences

Use precise equality on sequences

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -1006,7 +1006,7 @@
         else
           elemType

-    cond = (jsBinary (if hasSplat then '>=' else '=='),
+    cond = (jsBinary (if hasSplat then '>=' else '==='),
       (jsCall "seq_size", [sequence]), requiredElems)
     combinePatterns join [(precs: [(cond_ cond)])], compiledArgs
   else


fix test

fix test

--- apps/artwork_2/components/banner/test/helpers.coffee
+++ apps/artwork_2/components/banner/test/helpers.coffee
@@ -12,8 +12,8 @@

     it 'renders the correct label if the auction is not yet open', ->
       countdownLabel moment().add(1, 'day').format()
-        .should.equal 'Bidding opens in'
+        .should.equal 'Auction opens in'

     it 'renders the correct label if the auction is open', ->
       countdownLabel moment().subtract(1, 'day').format()
-        .should.equal 'Bidding closes in'
+        .should.equal 'Auction closes in'


comment out log

comment out log

--- app/assets/javascripts/angular/controllers/fileupload_ctrl.js.coffee
+++ app/assets/javascripts/angular/controllers/fileupload_ctrl.js.coffee
@@ -28,7 +28,7 @@
         "instrument_id": $scope.instrument_id,
         "question_id": $scope.question_id
       }, (result) ->
-        console.log result
+        #console.log result
     )

   $scope.deleteImage = (image) ->


fix(pref): check for mounting on async launch service

--- internal_packages/preferences/lib/tabs/workspace-section.cjsx
+++ internal_packages/preferences/lib/tabs/workspace-section.cjsx
@@ -9,7 +9,13 @@
     @_services = new LaunchServices()
     if @_services.available()
       @_services.isRegisteredForURLScheme 'mailto', (registered) =>
-        @setState(defaultClient: registered)
+        @setState(defaultClient: registered) if @_mounted
+
+  componentDidMount: ->
+    @_mounted = true
+
+  componentWillUnmount: ->
+    @_mounted = false

   render: =>
     return false unless process.platform is 'darwin'
@@ -36,11 +42,15 @@
     @_service = new SystemStartService()

   componentDidMount: ->
+    @_mounted = true
     @_service.checkAvailability().then (available) =>
-      @setState {available}
-      return if not available
+      @setState {available} if @_mounted
+      return if not available or not @_mounted
       @_service.doesLaunchOnSystemStart().then (launchOnStart) =>
-        @setState({launchOnStart})
+        @setState({launchOnStart}) if @_mounted
+
+  componentWillUnmount: ->
+    @_mounted = false

   render: =>
     return false if not @state.available


Optional sentence descriptions for cake tasks.

--- cake.coffee
+++ cake.coffee
@@ -22,9 +22,10 @@
 # Mixin the top-level Cake functions for Cakefiles to use directly.
 helpers.extend global, {

-  # Define a Cake task with a short name, a sentence description,
+  # Define a Cake task with a short name, an optional sentence description,
   # and the function to run as the action itself.
   task: (name, description, action) ->
+    [action, description]: [description, action] unless action?
     tasks[name]: {name: name, description: description, action: action}

   # Define an option that the Cakefile accepts. The parsed options hash,
@@ -59,7 +60,9 @@
   for name, task of tasks
     spaces: 20 - name.length
     spaces: if spaces > 0 then (' ' for i in [0..spaces]).join('') else ''
-    puts "cake $name$spaces # ${task.description}"
+    print "cake $name$spaces"
+    print " # ${task.description}" if task.description?
+    puts ''
   puts oparse.help() if switches.length

 # Print an error and exit when attempting to all an undefined task.


Do not assume that the project is tab based, instead let the app decide how to reload view.  This is a debugging feature anyway

--- lib/generators/nas_extjs/templates/public/app/lib/Util.coffee
+++ lib/generators/nas_extjs/templates/public/app/lib/Util.coffee
@@ -70,8 +70,7 @@

     reloadView:(name)->
         return if ! window.Application || Util.is_reloading
-        console.clear()
-        return Application.getController('Tabs').replaceView( "App.view.#{name}" )
+        Application.reloadView( name ) if Application.reloadView

     changeListenerUpCaseField : (f,nv,ov,opts)->
         f.setValue( nv.toUpperCase() ) if nv
@@ -100,8 +99,7 @@
                         for indx in old_evs
                             ary.splice(indx,1)
             Application.controllers.remove( old )
-        controller = Application.getController( controller_name );
-        controller.init(this);
+        Application.getController( controller_name );

 assoc = (association)->


payments: we can't fetch payment methods this way

stop using ill-conceived #fetchBilingInfo(type, callback)

--- client/app/Applications/Dashboard.kdapplication/AppController.coffee
+++ client/app/Applications/Dashboard.kdapplication/AppController.coffee
@@ -15,7 +15,8 @@

     options.view = new DashboardAppView
       testPath   : "groups-dashboard"
-    data or= KD.getSingleton("groupsController").getCurrentGroup()
+
+    data or= (KD.getSingleton "groupsController").getCurrentGroup()

     super options, data

@@ -103,13 +104,18 @@

   paymentViewAdded: (pane, view) ->
     paymentController = KD.getSingleton 'paymentController'
-    paymentController.fetchBillingInfo 'group', (err, billingInfo) =>
-      view.setBillingInfo billingInfo
+
+    group = @getData()
+
+    group.fetchPaymentMethod (err, paymentMethod) ->
+      console.log { err, paymentMethod }
+    # paymentController.fetchBillingInfo 'group', (err, billingInfo) =>
+      view.setBillingInfo paymentMethod

     @paymentView = view

     { loader }  = view.settingsForm.inputs.billing
-
+
     view.on 'BillingEditRequested', =>
       loader.show()
       @showBillingInfoModal ->
@@ -134,9 +140,9 @@
   createBillingInfoModal: ->

     paymentController = KD.getSingleton "paymentController"
-
+
     billingInfoModal = paymentController.createBillingInfoModal 'group', {}
-
+
     paymentController.fetchBillingInfo 'group', (err, groupBillingInfo) ->

       if groupBillingInfo
@@ -162,7 +168,7 @@
               billingInfoModal.setBillingInfo personalBillingInfo.billing
               useExistingModal.destroy()

-
+
     return billingInfoModal

   # vocabularyViewAdded:(pane, view)->


Refactor and add debug to the char width scale computation

--- lib/minimap-find-results-view.coffee
+++ lib/minimap-find-results-view.coffee
@@ -1,4 +1,4 @@
-{EditorView} = require 'atom'
+{$, EditorView} = require 'atom'

 # HACK The exports is a function here because we are not sure that the
 # `find-and-replace` and `minimap` packages will be available when this
@@ -17,12 +17,27 @@
       minimap = @getMinimap()
       minimap.miniOverlayer.append(this)

-      # As there's a slightly different char width between the minimap font
-      # and the editor font we'll retrieve both widths and compute the ratio
-      # to properly scale the find results.
-      minimapWidth = minimap.miniEditorView.find('.line').first().children().width()
-      editorWidth = @getEditor().find('.line').first().children().width()
-      @css '-webkit-transform', "scale3d(#{minimapWidth / editorWidth},1,1)"
+      @adjustResults()
+
+    # As there's a slightly different char width between the minimap font
+    # and the editor font we'll retrieve both widths and compute the ratio
+    # to properly scale the find results.
+    adjustResults: ->
+      return if @adjusted
+
+      minimapFirstLine = minimap.miniEditorView.find('.line').first()
+      editorFirstLine = @getEditor().find('.line').first()
+
+      console.log minimapFirstLine.html()
+      console.log editorFirstLine.html()
+
+      minimapWidth = minimapFirstLine.children().width()
+      editorWidth = editorFirstLine.children().width()
+      ratio = minimapWidth / editorWidth
+
+      @css '-webkit-transform', "scale3d(#{ratio},1,1)"
+
+      @adjusted = true

     getMinimap: ->
       minimapInstance.minimapForEditorView(@getEditor())


CoffeeScript style

CoffeeScript style

--- auth.coffee
+++ auth.coffee
@@ -37,8 +37,7 @@
     familyName:  profile.name.familyName
     email:       emails[0]

-  models.User.findOrCreate
-      where: user
+  models.User.findOrCreate where: user
     .then ([{dataValues: user}]) ->
       done null, user
     .error (err) ->


Edge-on question now goes to mergers/tidal debris

--- app/lib/decals_tree.coffee
+++ app/lib/decals_tree.coffee
@@ -42,7 +42,7 @@
     @answer '4', icon: 'spiral_4', examples: 5
     @answer 'More than 4', icon: 'spiral_4-plus', examples: 6

-  @question 'Bulge', 'Does the galaxy have a bulge at its centre?', leadsTo: 'Is there anything odd?', ->
+  @question 'Bulge', 'Does the galaxy have a bulge at its centre?', leadsTo: 'Is the galaxy currently merging or is there any sign of tidal debris?', ->
     @help "Concentrate on the centre of the galaxy - if it has a smooth, uninterrupted, needle or lens-shape then click "no bulge." Otherwise your options are "rounded" bulge or "boxy" (but boxy bulges are rare). Click to see larger images."
     @answer 'Rounded', icon: 'edge_round', examples: 4
     @answer 'Boxy', icon: 'edge_boxy', examples: 5


Fix PBR shaders in Edge.

Fix PBR shaders in Edge.

--- engine/material_shaders/blender_cycles_pbr.coffee
+++ engine/material_shaders/blender_cycles_pbr.coffee
@@ -243,17 +243,21 @@
                 tex_list[#{@unfreflect_index}].value.bound_unit);"
             locations.push loc

+        planar_probe_code = []
         if (loc = gl.getUniformLocation program, 'unfplanarvec')?
-            probe_code.push """
+            planar_probe_code.push """
                 v=ob.probe_planar.normal;
                 gl.uniform3f(locations[#{locations.length}], v.x, v.y, v.z);"""
             locations.push loc

         if (loc = gl.getUniformLocation program, 'unfplanarreflectmat')?
-            probe_code.push """
+            planar_probe_code.push """
                 gl.uniformMatrix4fv(locations[#{locations.length}], false,
                 ob.probe_planar.planarreflectmat.toJSON());"""
             locations.push loc
+
+        if planar_probe_code.length != 0
+            probe_code.push "if(ob.probe_planar){"+planar_probe_code.join('\n')+'}'

         ## TODO: update/store these two in probe even when not auto rendering
         if (loc = gl.getUniformLocation program, 'unfprobepos')?


Fix arena date wording

Fix arena date wording

--- app/locale/en.coffee
+++ app/locale/en.coffee
@@ -3204,7 +3204,7 @@
     blitz: "Blitz"
     clash: "Clash"
     season1_announcement_1: "Time to put your coding skills to the test in our season 1 final arena. The Infinite Inferno Cup is live and offers a new challenge and a new leaderboard to climb."
-    season1_announcement_2: "Need more practice? Stick with the Blazing Battle Arena to refine your skills. You have until April 30th to play both arenas. Note: arena balance adjustments may occur until April 23."
+    season1_announcement_2: "Need more practice? Stick with the Blazing Battle Arena to refine your skills. You have until April 30th to play both arenas. Note: arena balance adjustments may occur until April 23rd."
     season1_announcement_3: "Great prizes available for top performers in the Infinite Inferno Cup:"
     season1_prize_1: "$1,000 Scholarship"
     season1_prize_2: "RESPAWN Gaming Chair"