doc fixes

doc fixes

--- lib/assets/Text.coffee
+++ lib/assets/Text.coffee
@@ -6,41 +6,33 @@
 try
   iconv = require 'iconv'

-###
-new Text(options)
-=================
-
-Create a new Text asset instance.
-
-Adds text encoding and decoding support to Asset. Serves as a
-superclass for `Html`, `Xml`, `Css`, `JavaScript`, `CoffeeScript`,
-`Json`, and `CacheManifest`.
-
-In addition to the options already supported by the Asset base
-class, these options are supported:
-
-- `text` (String) The decoded source of the asset. Can be used
-instead of `rawSrc` and `rawSrcProxy`.  - `encoding`
-(String) Used to decode and reencode the `rawSrc`. Can
-be any encoding supported by the `iconv` module. Can
-be changed later using the `encoding`
-getter/setter. Defaults to "utf-8" (see the docs for
-`defaultEncoding` below). If the asset is loaded via
-http, the encoding will be read from the
-`Content-Type`, and likewise for `data:` urls.
-
-Example:
-
-var textAsset = new Text({
-// "æøå" in iso-8859-1:
-rawSrc: new Buffer([0xe6, 0xf8, 0xe5]),
-encoding: "iso-8859-1"
-});
-textAsset.text; // "æøå" (decoded JavaScript string)
-textAsset.encoding = 'utf-8';
-textAsset.rawSrc; // <Buffer c3 a6 c3 b8 c3 a5>
+###*
+ * Adds text encoding and decoding support to Asset. Serves as a superclass
+   for `Html`, `Xml`, `Css`, `JavaScript`, `CoffeeScript`, `Json`, and
+   `CacheManifest`.
 ###
 class Text extends Asset
+  ###*
+   * In addition to the options already supported by the Asset base class,
+     these options are supported:
+   * @param  {String} config.text The decoded source of the asset. Can be used
+     instead of `rawSrc` and `rawSrcProxy`.
+   * @param  {String} config.encoding Used to decode and reencode the
+     `rawSrc`. Can be any encoding supported by the `iconv` module. Can be
+     changed later using the `encoding` getter/setter. Defaults to "utf-8"
+     (see the docs for `defaultEncoding` below). If the asset is loaded via
+     http, the encoding will be read from the `Content-Type`, and likewise for
+     `data:` urls.
+   * @example
+   * var textAsset = new Text({
+   * // "æøå" in iso-8859-1:
+   * rawSrc: new Buffer([0xe6, 0xf8, 0xe5]),
+   * encoding: "iso-8859-1"
+   * });
+   * textAsset.text; // "æøå" (decoded JavaScript string)
+   * textAsset.encoding = 'utf-8';
+   * textAsset.rawSrc; // <Buffer c3 a6 c3 b8 c3 a5>
+  ###
   constructor: (config) ->
     if 'text' of config
       @_text = config.text
@@ -50,28 +42,21 @@
       delete config.encoding
     super config

+  ###*
+   * Property that's true for all Text instances. Avoids reliance on the
+     `instanceof` operator.
   ###
-  text.isText
-  ===========
+  isText: true

-  Property that's true for all Text instances. Avoids reliance on
-  the `instanceof` operator.
-  ###
-
-  ###
-  text.defaultEncoding
-  ====================
-
-  The default encoding for the Text (sub)class. Used for decoding
-  the raw source when the encoding cannot be determined by other
-  means, such as a `Content-Type` header (when the asset was
-  fetched via http), or another indicator specific to the given
-  asset type (`@charset` for Css, `<meta
-  http-equiv="Content-Type" ...>` for Html).
-
-  Factory setting is "utf-8", but you can override it by setting
-  `Text.prototype.defaultEncoding` to another value supported by
-  the `iconv` module.
+  ###*
+   * The default encoding for the Text (sub)class. Used for decoding the raw
+     source when the encoding cannot be determined by other means, such as a
+     `Content-Type` header (when the asset was fetched via http), or another
+     indicator specific to the given asset type (`@charset` for Css, `<meta
+     http-equiv="Content-Type" ...>` for Html).
+   * Factory setting is "utf-8", but you can override it by setting
+     `Text.prototype.defaultEncoding` to another value supported by the
+     `iconv` module.
   ###
   defaultEncoding: 'utf-8'

@@ -88,14 +73,10 @@

   contentType: 'text/plain'

-  ###
-  Text.encoding (getter/setter)
-  =============================
-
-  Get or set the encoding (charset) used for re-encoding the raw
-  source of the asset. To affect the initial decoding of the
-  `rawSrc` option, provide the `encoding` option to the
-  constructor.
+  ###*
+  * Get or set the encoding (charset) used for re-encoding the raw source of
+    the asset. To affect the initial decoding of the `rawSrc` option, provide
+    the `encoding` option to the constructor.
   ###
   @getter 'encoding', ->
     @_encoding = @defaultEncoding  unless @_encoding
@@ -144,37 +125,31 @@
     assetGraph.addAsset this  if assetGraph
     @markDirty()

-  ###
-  text.text (getter/setter)
-  =========================
+  ###*
+   * Get or set the decoded text contents of the of the asset as a JavaScript
+     string. Unlike browsers AssetGraph doesn't try to sniff the charset of
+     your text-based assets. It will fall back to assuming utf-8 if it's
+     unable to determine the encoding/charset from HTTP headers, `<meta http-
+     equiv='Content-Type'>` tags (Html), `@charset` (Css), so if for some
+     reason you're not using utf-8 for all your text-based assets, make sure
+     to provide those hints. Other asset types provide no standard way to
+     specify the charset within the file itself, so presently there's no way
+     to load eg. JavaScript from disc if it's not utf-8 or ASCII, except by
+     overriding `Text.prototype.defaultEncoding` globally.
+   * If the internal state has been changed since the asset was initialized,
+     it will automatically be reserialized when the `text` property is
+     retrieved, for example:
+   * var htmlAsset = new Html({
+   *   rawSrc: new Buffer("hello");
+   * });
+   * htmlAsset.text; // "hello"
+   * htmlAsset.parseTree.body.innerHTML = "bye";
+   * htmlAsset.markDirty();
+   * htmlAsset.text; // "bye"

-  Get or set the decoded text contents of the of the asset as a
-  JavaScript string. Unlike browsers AssetGraph doesn't try to
-  sniff the charset of your text-based assets. It will fall back
-  to assuming utf-8 if it's unable to determine the
-  encoding/charset from HTTP headers, `<meta
-  http-equiv='Content-Type'>` tags (Html), `@charset` (Css), so
-  if for some reason you're not using utf-8 for all your
-  text-based assets, make sure to provide those hints. Other
-  asset types provide no standard way to specify the charset
-  within the file itself, so presently there's no way to load
-  eg. JavaScript from disc if it's not utf-8 or ASCII, except by
-  overriding `Text.prototype.defaultEncoding` globally.
-
-  If the internal state has been changed since the asset was
-  initialized, it will automatically be reserialized when the
-  `text` property is retrieved, for example:
-
-  var htmlAsset = new Html({
-  rawSrc: new Buffer("hello");
-  });
-  htmlAsset.text; // "hello"
-  htmlAsset.parseTree.body.innerHTML = "bye";
-  htmlAsset.markDirty();
-  htmlAsset.text; // "bye"
-
-  Setting this property after the outgoing relations have been
-  accessed currently leads to undefined behavior.
+   * Setting this property after the outgoing relations have been accessed
+     currently leads to undefined behavior.
+   * @return {[type]}
   ###
   @getter 'text', ->
     @_text = @_getTextFromRawSrc() unless '_text' of this


fixed video deletion

fixed video deletion

--- smartfields/static/coffee/smartfields.coffee
+++ smartfields/static/coffee/smartfields.coffee
@@ -81,6 +81,7 @@
             @$delete_btn.hide()
             @$current_btn.parent().hide()
         @$delete_btn.click =>
+            post_data = {}
             post_data["#{@$browse_btn.attr('name')}-clear"] = "on"
             $.post(@options.url, post_data, (data, textStatus, jqXHR) =>
                 if data.state == 'ready'


next 1000 invites

next 1000 invites

--- server/app/bongo/models/invitation.coffee
+++ server/app/bongo/models/invitation.coffee
@@ -13,7 +13,7 @@
     indexes         :
       code          : 'unique'
     sharedMethods   :
-      static        : ['byCode']#,'__sendBetaInvites',,'__createBetaInvites']
+      static        : ['byCode']#,,'__sendBetaInvites','__createBetaInvites']
     schema          :
       code          : String
       inviteeEmail  : String
@@ -71,7 +71,7 @@
       {host, port} = server
       # host = 'localhost:3000'
       # protocol = 'http://'
-      uniq(betaTestersEmails.split '\n').slice(4000, 5000).forEach (email)=>
+      uniq(betaTestersEmails.split '\n').slice(6000, 7000).forEach (email)=>
         recipients.push =>
           @one {inviteeEmail: email}, (err, invite)=>
             if err


Change redirection after saving a resource. Shoots you to the view page. Fixes #7

--- ui/coffee/edit.coffee
+++ ui/coffee/edit.coffee
@@ -160,10 +160,10 @@
                 console.log err
               success: (data, status, xhr) ->
                 if update
-                  window.location.reload()
+                  window.location.href = "/repository/resource/#{root.app.record.id}/"
                 else
                   id = xhr.getResponseHeader('Location').match(/\/(.{32})\/$/)[1]
-                  window.location.href = "/repository/resource/#{id}/edit"
+                  window.location.href = "/repository/resource/#{id}/"
             $.ajax opts
             $(this).dialog 'close'
             $(this).remove()


get rid of the anonymous query param once it is "consumed".

--- src/smc-webapp/client_browser.coffee
+++ src/smc-webapp/client_browser.coffee
@@ -330,7 +330,20 @@
             x = await callback2(@create_account, {})
             if x?.event == 'account_creation_failed'
                 throw Error("failed")
+            if not @is_signed_in()
+                await once(@, "signed_in")
+            actions = @_redux.getActions('projects')
+            project_id = await actions.create_project({title:'Welcome to CoCalc!', start:true, description:''})
+            actions.open_project({ project_id:project_id, switch_to: true })
+            # Also change default account settings to not ask for the kernel,
+            # since that adds friction
+            @_redux.getTable("account").set({editor_settings: { ask_jupyter_kernel: false , jupyter:{kernel:'python3'}}})
+            # Open a new Jupyter notebook:
+            project_actions = @_redux.getProjectActions(project_id)
+            project_actions.open_file({path:"Welcome to CoCalc.ipynb", foreground:true})
         catch err
+            return # will fall back to sign in page.
+        finally
             # This happens if, e.g., a token is required,
             # or maybe this ip is blocked. Falling back
             # to normal sign up makes sense in this case.
@@ -338,17 +351,7 @@
             if i != -1
                 window.history.pushState("", "", window.location.href.slice(0,i))
             return
-        if not @is_signed_in()
-            await once(@, "signed_in")
-        actions = @_redux.getActions('projects')
-        project_id = await actions.create_project({title:'Welcome to CoCalc!', start:true, description:''})
-        actions.open_project({ project_id:project_id, switch_to: true })
-        # Also change default account settings to not ask for the kernel,
-        # since that adds friction
-        @_redux.getTable("account").set({editor_settings: { ask_jupyter_kernel: false , jupyter:{kernel:'python3'}}})
-        # Open a new Jupyter notebook:
-        project_actions = @_redux.getProjectActions(project_id)
-        project_actions.open_file({path:"Welcome to CoCalc.ipynb", foreground:true})
+

 connection = undefined


webapp/course: a rename of status_map to status seems to be the culprit for #1547

--- src/smc-webapp/course/actions.coffee
+++ src/smc-webapp/course/actions.coffee
@@ -1498,16 +1498,16 @@
             student = store.get_student(student)
             handout = store.get_handout(handout)
             obj = {table:'handouts', handout_id:handout.get('handout_id')}
-            status_map = @_get_one(obj)?.status_map
-            if not status_map?
+            status = @_get_one(obj)?.status
+            if not status?
                 return
-            student_status = (status_map[student.get('student_id')])
+            student_status = (status[student.get('student_id')])
             if not student_status?
                 return
             if student_status.start?
                 delete student_status.start
-                status_map[student.get('student_id')] = student_status
-                obj.status_map = status_map
+                status[student.get('student_id')] = student_status
+                obj.status = status
                 @_set(obj)

     # Copy the files for the given handout to the given student. If


Use tags as reference or get tags from endpoint descriptions

--- src/swagged-angular-resources.coffee
+++ src/swagged-angular-resources.coffee
@@ -42,25 +42,36 @@
   if params.length == 0 then false else params

 getResourceOperations = (apiDefinition) ->
-  _.reduce(apiDefinition.paths, (memo, methods, path) ->
-    _.each(methods, (operation, action) ->
-      response = operation.responses["200"]
-      if response and response.schema
-        if response.schema.type == "array"
-          modelDefinition = response.schema.items.$ref;
-          isQuery = true
-        else
-          modelDefinition = response.schema.$ref
+  if apiDefinition.tags
+    tags = _.map apiDefinition.tags, (tag) -> tag.name
+  else
+    tags = _.reduce apiDefinition.paths, (memo, methods) ->
+      _.each methods, (operation) ->
+        memo = memo.concat(operation.tags)
+      memo
+    , []
+    tags = _.uniq tags

-      # TODO: add parameter for mimeType
-      mockedResponseMimeType = "application/json"
-      mockedResponse = JSON.stringify(response.examples[mockedResponseMimeType], null, 4) if response and response.examples and response.examples[mockedResponseMimeType]
+  _.reduce apiDefinition.paths, (memo, methods, path) ->
+    _.each methods, (operation, action) ->
+      inTags = _.intersection operation.tags, tags

-      if modelDefinition
-        modelDefinition = modelDefinition.match(/.+\/(.+)$/)[1]
-        memo[modelDefinition] = memo[modelDefinition] || []
+      _.each inTags, (tag) ->
+        response = operation.responses["200"]
+        if response and response.schema
+          if response.schema.type == "array"
+            modelDefinition = response.schema.items.$ref;
+            isQuery = true
+          else
+            modelDefinition = response.schema.$ref

-        memo[modelDefinition].push({
+        # TODO: add parameter for mimeType
+        mockedResponseMimeType = "application/json"
+        mockedResponse = JSON.stringify(response.examples[mockedResponseMimeType], null, 4) if response and response.examples and response.examples[mockedResponseMimeType]
+
+        modelDefinition = modelDefinition and modelDefinition.match(/.+\/(.+)$/)[1]
+        memo[tag] = memo[tag] or []
+        memo[tag].push {
           path: path.replace(/\{(.+?)\}/g, ":$1")
           nickname: operation.operationId || modelDefinition + "_" + _.str.capitalize(action)
           action: action.toUpperCase()
@@ -81,10 +92,9 @@
           isPost: action == "post"
           isPut: action == "put"
           isPatch: action == "patch"
-        })
-    )
+        }
     memo
-  , {})
+  , {}

 getCode = (error, apiDefinition) ->
   throw error if error


Improve dependence on children

Improve dependence on children

--- client/message.coffee
+++ client/message.coffee
@@ -85,8 +85,12 @@
 childrenLookup = (message) ->
   ## Lookup children in message.children, and annotate with extra fields
   ## `parent` and `index`.
+  ## Alternate approach: single call to Messages.find
+  #childrenByID = {}
+  #Messages.find _id: $in: message.children
+  #.forEach (child) -> childrenByID[child._id] = child
   for childID, index in message.children
-    child = Messages.findOne childID
+    child = Messages.findOne childID #childrenByID[childID]
     continue unless child?
     ## Use canSee to properly fake non-superuser mode.
     continue unless canSee child
@@ -95,6 +99,11 @@
     child.parent = message._id
     child.index = index
     child
+useChildren = (message) ->
+  useTracker ->
+    childrenLookup message
+  , [message._id, message.children.join ',']
+    ## Depend on actual sequence of message children, not the array object

 Template.registerHelper 'tags', ->
   sortTags @tags
@@ -1599,9 +1608,7 @@
   creator = useTracker ->
     displayUser message.creator
   , [message.creator]
-  children = useTracker ->
-    childrenLookup message
-  , [message]
+  children = useChildren message
   inner =
     <>
       {unless root
@@ -1869,9 +1876,7 @@
   formattedBody = useTracker ->
     formatBody historified.format, historified.body
   , [historified.format, historified.body]
-  children = useTracker ->
-    childrenLookup message
-  , [message]
+  children = useChildren message
   ref = useRefTooltip()

   onFold = (e) ->


Add username to hover text for unsubscribed users. Fix #367

--- client/message.coffee
+++ client/message.coffee
@@ -159,16 +159,17 @@
         rolesPartial: true
         'profile.notifications': true
     (for user in users when user.username not of subscribed
+      title = "User '#{user.username}': " # like linkToAuthor
       unless user.emails?[0]?
-        title = "No email address"
+        title += "No email address"
       else if not user.emails[0].verified
-        title = "Unverified email address #{user.emails[0].address}"
+        title += "Unverified email address #{user.emails[0].address}"
       else if not (user.profile.notifications?.on ? defaultNotificationsOn)
-        title = "Notifications turned off"
+        title += "Notifications turned off"
       else if not autosubscribe @group, user
-        title = "Autosubscribe turned off, and not explicitly subscribed to thread"
+        title += "Autosubscribe turned off, and not explicitly subscribed to thread"
       else
-        title = "Explicitly unsubscribed from thread"
+        title += "Explicitly unsubscribed from thread"
       linkToAuthor @group, user.username, title
     ).join ', '
   orphans: ->


Show master as version from the client

Show master as version from the client

--- client/version.coffee
+++ client/version.coffee
@@ -1 +1 @@
-@version = '4.0.8'
+@version = 'master'


change syntax for gdrive v3 api

change syntax for gdrive v3 api

--- src/adaptor/googleDrive.coffee
+++ src/adaptor/googleDrive.coffee
@@ -42,7 +42,7 @@
     gapi.client.load 'drive', 'v3', ->
       retrievePageOfFiles = (request, result) ->
         request.execute((resp) ->
-          result = result.concat resp.items
+          result = result.concat resp.files
           nextPageToken = resp.nextPageToken
           if nextPageToken
             request = gapi.client.drive.files.list


attachment sorting for #355

attachment sorting for #355

--- resource/translators/reference.coffee
+++ resource/translators/reference.coffee
@@ -246,8 +246,12 @@
     f.errors = errors if errors.length != 0
     return null if attachments.length == 0

-    # sort attachments for stable tests
-    attachments.sort( ( (a, b) -> a.path.localeCompare(b.path) ) ) if Translator.testing
+    # sort attachments for stable tests, and to make non-snapshots the default for JabRef to open (#355)
+    attachments.sort((a, b) ->
+      return -1 if a.mimetype == 'text/html' && b.mimetype != 'text/html'
+      return 1  if b.mimetype == 'text/html' && a.mimetype != 'text/html'
+      return a.path.localeCompare(b.path)
+    )

     return (att.path.replace(/([\\{};])/g, "\\$1") for att in attachments).join(';') if Translator.attachmentsNoMetadata
     return ((part.replace(/([\\{}:;])/g, "\\$1") for part in [att.title, att.path, att.mimetype]).join(':') for att in attachments).join(';')


return origin and allow credentials already in options request

--- lib/eventsource.coffee
+++ lib/eventsource.coffee
@@ -15,10 +15,11 @@
     app.options '/subscribe', (req, res) ->
         res.set
             'Content-Type': 'text/event-stream',
-            'Access-Control-Allow-Origin': '*',
+            'Access-Control-Allow-Origin': req.get('Origin') || '*',
             'Access-Control-Allow-Methods': 'GET'
             'Access-Control-Max-Age': '86400',
-            'Access-Control-Allow-Headers': 'Authorization'
+            'Access-Control-Allow-Headers': 'Authorization',
+            'Access-Control-Allow-Credentials': true
         res.end()

     app.get '/subscribe', authorize('listen'), (req, res) ->


Fix enemy type, drops and skipping checkpoints

--- app/scripts/lazerscripts/lunch.coffee
+++ app/scripts/lazerscripts/lunch.coffee
@@ -8,6 +8,8 @@
   execute: ->
     @inventoryAdd 'item', 'lasers', ->
       Crafty.e('PowerUp').powerUp(contains: 'lasers', marking: 'L')
+    @inventoryAdd 'item', 'xp', ->
+      Crafty.e('PowerUp').powerUp(contains: 'xp', marking: 'X')

     Game.explosionMode = 'block'

@@ -120,8 +122,8 @@
       @parallel(
         @gainHeight -600, duration: 15000
         @nextSlide @sequence(
-          @placeSquad Game.Scripts.Swirler,
-            drop: 'lasers'
+          @placeSquad Game.Scripts.PresentationSwirler,
+            drop: 'xp'
             amount: 4
             delay: 500
           @waitForScenery 'OceanOld'
@@ -131,8 +133,8 @@
       @nextSlide(
         @sequence(
           @updateTitle 'Lazerscript environment'
-          @placeSquad Game.Scripts.Swirler,
-            drop: 'lasers'
+          @placeSquad Game.Scripts.PresentationSwirler,
+            drop: 'xp'
             amount: 4
             delay: 500
         )
@@ -194,7 +196,7 @@
         @_waitForSuperWeapon()
         task ? @wait(1000)
       )
-      => @player(1).ship().superUsed = 0
+      => @player(1).ship()?.superUsed = 0
     )

   _waitForSuperWeapon: ->


fix timing issue

fix timing issue

--- src/app/command/command.coffee
+++ src/app/command/command.coffee
@@ -17,9 +17,9 @@
     @arduino.in.on 'change', (v) ->
       if v
         console.log 'start'
+        self.arduino.out.set(0)
       else
         console.log 'stop'
-        self.arduino.out.set(0)
         self.addDone()

   progress: ->


rename opts, got indexRange

rename opts, got indexRange

--- coffee/datomic.coffee
+++ coffee/datomic.coffee
@@ -31,34 +31,35 @@
     request.post opts, (err, res, body) ->
       done err, body

-  datoms: (index, opt..., done) ->
-    opt = parse_opt opt
-    opt.index = index
+  datoms: (index, opts..., done) ->
+    opts = parse_opts opts
+    opts.index = index

-    get "#{@db_uri}-/datoms?#{qs.stringify opt}", done
+    get "#{@db_uri}-/datoms?#{qs.stringify opts}", done

-  indexRange: (attrid, opt..., done) ->
-    opt = parse_opt opt
-    opt.a = attrid
+  indexRange: (attrid, opts..., done) ->
+    opts = parse_opts opts
+    opts.a = attrid
+    opts.index ?= 'aevt'

-    get "#{@db_uri}/range?#{qs.stringify opt}", done
+    get "#{@db_uri}-/datoms?#{qs.stringify opts}", done

-  entity: (eid, opt..., done) ->
+  entity: (eid, opts..., done) ->
     if is_object eid
-      opt = eid
+      opts = eid
       eid = ''
     else
-      opt = parse_opt opt
+      opts = parse_opts opts
       eid = '/' + eid

-    get "#{@db_uri}/entity#{eid}?#{qs.stringify opt}", done
+    get "#{@db_uri}/entity#{eid}?#{qs.stringify opts}", done

-  q: (query, opt..., done) ->
-    opt = parse_opt opt
-    opt.q = query
-    opt.args = "[{:db/alias #{@db_alias}}]"
+  q: (query, opts..., done) ->
+    opts = parse_opts opts
+    opts.q = query
+    opts.args = "[{:db/alias #{@db_alias}}]"

-    get "#{@root}api/query?#{qs.stringify opt}", done
+    get "#{@root}api/query?#{qs.stringify opts}", done

   events: ->
     new EventStream("#{@root}events/#{@db_alias}")
@@ -75,6 +76,6 @@
     request opts, (err, res, body) ->
       done err, body

-  parse_opt = (opt) -> if opt.length is 1 then opt[0] else {}
+  parse_opts = (opts) -> if opts.length is 1 then opts[0] else {}

   is_object = (obj) -> obj is Object obj


comments

comments

--- coffee/script2.coffee
+++ coffee/script2.coffee
@@ -98,9 +98,9 @@
   # Invoked when user clicks on a table header of search results
   # Calls appropriate sort method
   # Calls method to update icons representing sort direction
-  # type [String] (name,brating)
-  # vm [ViewModel]
-  # event [jQueryEvent]
+  # @param type [String] (name,brating)
+  # @param vm [ViewModel]
+  # @param event [jQueryEvent]
   self.handleSort = (type,vm,event) ->
     #default to descending sort
     #unless already sorted by the same type
@@ -112,8 +112,8 @@
       when "brating" then self.sortByBRating()
     return
   # Updates sort direction icons
-  # type [String] (name,brating)
-  # event [jQueryEvent]
+  # @param type [String] (name,brating)
+  # @param event [jQueryEvent]
   self.updateTableHeaders = (type,event) ->
     $("#results-table thead tr th").removeClass "headerSortUp"
     $("#results-table thead tr th").removeClass "headerSortDown"
@@ -126,30 +126,30 @@
   # get information from boardgame object

   # Returns link to bgg rank page for given rank
-  # name [String] (type of rank/game)
-  # id [String] (id of the rank)
-  # value [String] (rank value)
+  # @param name [String] type of rank/game
+  # @param id [String] id of the rank
+  # @param value [String] rank value
   self.getRankLink = (name, id, value) ->
     return "http://boardgamegeek.com/browse/boardgame?sort=rank&rankobjecttype=subtype&rankobjectid=#{id}&rank=#{value}##{value}"  if name is "boardgame"
     return "http://boardgamegeek.com/#{name}/browse/boardgame?sort=rank&rankobjecttype=subtype&rankobjectid=#{id}&rank=#{value}##{value}"

-  # Returns all the ranks a boardgame has
-  # stats [Object]
+  # Returns all the ranks of a boardgame
+  # @param stats [Object]
   self.getRanks = (stats) ->
     stats.ratings.ranks.rank

   # Returns average rating of a boardgame
-  # stats [Object]
+  # @param stats [Object]
   self.getAverageRating = (stats) ->
     stats.ratings.average.value

   # Returns Bayes Rating of boardgame
-  # stats [Object]
+  # @param stats [Object]
   self.getBRating = (stats) ->
     stats.ratings.bayesaverage.value

   # Returns all the categories of a boardgame
-  # links [Array]
+  # @param links [Array]
   self.getCategoriesFromLinks = (links) ->
     # [Array]
     categories = []
@@ -158,7 +158,7 @@
     return categories

   # Returns the designer of a boardgame
-  # links [Array]
+  # @param links [Array]
   self.getDesignerFromLinks = (link) ->

     for link in links
@@ -166,20 +166,20 @@
     return

   # Returns the primary name of a boardgame
-  # name [Array or String]
+  # @param name [Array or String]
   self.getName = (name) ->
     return name[0].value  if Array.isArray(name)
     name.value

   # Returns shorted description of a boardgame
-  # description [String]
+  # @param description [String]
   self.getShortDescription = (description) ->
     description.slice(0, 100) + "..."

   # utilities

   # Returns the html of description string
-  # description [String]
+  # @param description [String]
   self.parseDescription = (description) ->
     paragraphs = 1
     contenthid = false
@@ -220,7 +220,7 @@
     return description

   # Searches games that match input string
-  # str [String]
+  # @param str [String]
   self.searchGames = (str) ->
     # Clear previous search results
     self.searchedGames.removeAll()
@@ -251,7 +251,7 @@
     return

   # Returns the list of ids of the boardgames that match the search string
-  # data [Object]
+  # @param data [Object]
   self.extractIdsFromSearch = (data) ->
     console.log data
     if data.query.results
@@ -276,14 +276,14 @@
     return

   # Returns the YQL query
-  # str [String]
+  # @param str [String]
   self.getYQLurl = (str) ->
     q = "select * from xml where url="
     url = "'#{str}'"
     return "http://query.yahooapis.com/v1/public/yql?q=#{encodeURIComponent(q + url)}&format=json&callback=?"

   # Loads data for a given boardgame id
-  # id [String]
+  # @param id [String]
   self.getGameDetails = (id) ->
     if Modernizr.sessionstorage
       gdata = eval("(" + sessionStorage["bg" + id] + ")")
@@ -363,7 +363,8 @@
     str = encodeURIComponent($("#search").val())
     location.hash = "search/" + str
     return
-
+
+  # @param object [Object] boardgame object
   self.goToGame = (object) ->
     location.hash = "game/" + object.id
     return


Removes uneeded ";" at end of line

Removes uneeded ";" at end of line

--- coffee/turbase.coffee
+++ coffee/turbase.coffee
@@ -15,7 +15,7 @@
   ]
 )

-db = new mongodb.Db('ntb_03', replSet, {native_parser: true});
+db = new mongodb.Db('ntb_03', replSet, {native_parser: true})
 console.log "Kobler seg til mongodb replica set ntb og database ntb_<versjon>"
 db.open(()->)


[front] [misc] Comment out hello messages ( for now )

--- lib/front/front.coffee
+++ lib/front/front.coffee
@@ -59,7 +59,7 @@

       engineIOServer.on 'connection', (socket) ->

-        socket.send 'hello ' + socket.id + '\ni am front-end ' + process.pid
+        # socket.send 'hello ' + socket.id + '\ni am front-end ' + process.pid

         socket.on 'error', (err) ->
           throw err
@@ -118,7 +118,7 @@

         logger.info 'front connected to ' + options.apiEndpoint

-        wsClient.send 'i am front-end ' + process.pid
+        # wsClient.send 'i am front-end ' + process.pid

         wsClient.on 'message', (message) ->


Better error handling

Better error handling

--- src/scripts/services/converter.coffee
+++ src/scripts/services/converter.coffee
@@ -119,113 +119,114 @@
 			return code

 		@getNetFromApt = (aptCode) ->
-			#try
-			name = @getAptBlock("name", aptCode).split("\"")[1]
-			if @isPartOfString("LTS", @getAptBlock("type", aptCode))
-				net = new TransitionSystem({name: name})
-
-				# add states
-				states = @getAptBlockRows("states", aptCode)
-				for stateLabel in states
-					if @isPartOfString("[initial]", stateLabel)
-						initial = true
-						stateLabel = stateLabel.replace("[initial]", "")
-					else
-						initial = false
-					state = new State({label: stateLabel})
-					net.addState(state)
-					net.setInitState(net.getNodeByText(stateLabel)) if initial
-
-				# add edges
-				edges = @getAptBlockRows("arcs", aptCode)
-				for edge in edges
-					source = edge.split(" ")[0]
-					label = edge.split(" ")[1]
-					target = edge.split(" ")[2]
-					edge = new TsEdge
-						source: net.getNodeByText(source)
-						right: 1
-						labelRight: label
-						target: net.getNodeByText(target)
-					net.addEdge(edge)
-
-
-			else if @isPartOfString("PN", @getAptBlock("type", aptCode))
-				net = new PetriNet({name: name})
-
-				# add places
-				places = @getAptBlockRows("places", aptCode)
-				for placeLabel in places
-					place = new Place({label: placeLabel})
-					net.addPlace(place)
-
-				# add transitions
-				transitionLabels = new Map()
-				transitions = @getAptBlockRows("transitions", aptCode)
-				for transitionRow in transitions
-					transitionId = transitionRow.split(" ")[0]
-
-					# labels are saved and applied later
-					if @isPartOfString("label=", transitionRow)
-						transitionLabels.set(transitionId, transitionRow.split("label=\"")[1].split("\"")[0])
-					transition = new Transition({label: transitionId})
-					net.addTransition(transition)
-
-				# add edges
-				flows = @getAptBlockRows("flows", aptCode)
-				for flow in flows
-					transition = net.getNodeByText(flow.split(": {")[0])
-					preset = flow.split(": {")[1].split("}")[0].split(", ")
-					postset = flow.split("-> {")[1].split("}")[0].split(", ")
-
-					# only create edges if they not already exist
-					for edge in preset
-						weight = edge.split("*")[0]
-						place = net.getNodeByText(edge.split("*")[1])
-						for edge in net.edges when edge.source is place and edge.target is transition
-							existingEdge = edge
-						if existingEdge
-							existingEdge.right = weight
+			try
+				name = @getAptBlock("name", aptCode).split("\"")[1]
+				if @isPartOfString("LTS", @getAptBlock("type", aptCode))
+					net = new TransitionSystem({name: name})
+
+					# add states
+					states = @getAptBlockRows("states", aptCode)
+					for stateLabel in states
+						stateLabel = stateLabel.split(" ")[0]
+						if @isPartOfString("[initial]", stateLabel)
+							initial = true
+							stateLabel = stateLabel.replace("[initial]", "")
 						else
+							initial = false
+						state = new State({label: stateLabel})
+						net.addState(state)
+						net.setInitState(net.getNodeByText(stateLabel)) if initial
+
+					# add edges
+					edges = @getAptBlockRows("arcs", aptCode)
+					for edge in edges
+						source = edge.split(" ")[0]
+						label = edge.split(" ")[1]
+						target = edge.split(" ")[2]
+						edge = new TsEdge
+							source: net.getNodeByText(source)
+							right: 1
+							labelRight: label
+							target: net.getNodeByText(target)
+						net.addEdge(edge)
+
+
+				else if @isPartOfString("PN", @getAptBlock("type", aptCode))
+					net = new PetriNet({name: name})
+
+					# add places
+					places = @getAptBlockRows("places", aptCode)
+					for placeLabel in places
+						place = new Place({label: placeLabel})
+						net.addPlace(place)
+
+					# add transitions
+					transitionLabels = new Map()
+					transitions = @getAptBlockRows("transitions", aptCode)
+					for transitionRow in transitions
+						transitionId = transitionRow.split(" ")[0]
+
+						# labels are saved and applied later
+						if @isPartOfString("label=", transitionRow)
+							transitionLabels.set(transitionId, transitionRow.split("label=\"")[1].split("\"")[0])
+						transition = new Transition({label: transitionId})
+						net.addTransition(transition)
+
+					# add edges
+					flows = @getAptBlockRows("flows", aptCode)
+					for flow in flows
+						transition = net.getNodeByText(flow.split(": {")[0])
+						preset = flow.split(": {")[1].split("}")[0].split(", ")
+						postset = flow.split("-> {")[1].split("}")[0].split(", ")
+
+						# only create edges if they not already exist
+						for edge in preset
+							weight = edge.split("*")[0]
+							place = net.getNodeByText(edge.split("*")[1])
+							for edge in net.edges when edge.source is place and edge.target is transition
+								existingEdge = edge
+							if existingEdge
+								existingEdge.right = weight
+							else
+								for edge in net.edges when edge.source is transition and edge.target is place
+									existingEdge = edge
+							if existingEdge
+								existingEdge.left = weight
+							else
+								edge = new PnEdge({source: place, target: transition, right: weight})
+								net.addEdge(edge)
+
+						for edge in postset
+							weight = edge.split("*")[0]
+							place = net.getNodeByText(edge.split("*")[1])
 							for edge in net.edges when edge.source is transition and edge.target is place
 								existingEdge = edge
-						if existingEdge
-							existingEdge.left = weight
-						else
-							edge = new PnEdge({source: place, target: transition, right: weight})
-							net.addEdge(edge)
-
-					for edge in postset
-						weight = edge.split("*")[0]
-						place = net.getNodeByText(edge.split("*")[1])
-						for edge in net.edges when edge.source is transition and edge.target is place
-							existingEdge = edge
-						if existingEdge
-							existingEdge.right = weight
-						else
-							for edge in net.edges when edge.source is place and edge.target is transition
-								existingEdge = edge
-						if existingEdge
-							existingEdge.left = weight
-						else
-							edge = new PnEdge({source: transition, target: place, right: weight})
-							net.addEdge(edge)
-
-				# add initial tokens
-				markings = @getAptBlock("initial_marking", aptCode).split("{")[1].split("}")[0].split(", ")
-				for marking in markings
-					number = marking.split("*")[0]
-					place = net.getNodeByText(marking.split("*")[1])
-					place.token = number
-
-				# rename transitions from id's to labels
-				transitionLabels.forEach (label, transitionId) ->
-					net.getNodeByText(transitionId).label = label
-
-			console.log net
-
-			#catch error
-			#	return false
+							if existingEdge
+								existingEdge.right = weight
+							else
+								for edge in net.edges when edge.source is place and edge.target is transition
+									existingEdge = edge
+							if existingEdge
+								existingEdge.left = weight
+							else
+								edge = new PnEdge({source: transition, target: place, right: weight})
+								net.addEdge(edge)
+
+					# add initial tokens
+					markings = @getAptBlock("initial_marking", aptCode).split("{")[1].split("}")[0].split(", ")
+					for marking in markings
+						number = marking.split("*")[0]
+						place = net.getNodeByText(marking.split("*")[1])
+						place.token = number
+
+					# rename transitions from id's to labels
+					transitionLabels.forEach (label, transitionId) ->
+						net.getNodeByText(transitionId).label = label
+
+				console.log net
+
+			catch error
+				return false
 			return net

 		# checks if the searchFor string is part of the searchIn string


backend support for configurable dynamic history and future sizes

--- src/server/plugins/dynamicmode.coffee
+++ src/server/plugins/dynamicmode.coffee
@@ -13,11 +13,15 @@
   restoreState: (state) =>
     @is_on = state.status.dynamic_mode ? false
     @random_ids = state.status.random_ids ? {}
+    @history_size = state.status.dynamic_history ? 10
+    @future_size = state.status.dynamic_future ? 10

   saveState: (state) =>
     state.status.dynamic_mode = @is_on
     state.status.dynamic_mode_enabled = @is_enabled
     state.status.random_ids = @random_ids
+    state.status.dynamic_history = @history_size
+    state.status.dynamic_future = @future_size

   setConf: (conf, conf_path) =>
     @is_enabled = true
@@ -33,14 +37,14 @@

   onSocketConnection: (socket) =>
     socket.on 'DynamicMode', (data) =>
+      return unless @is_enabled
       value = JSON.parse data.toString()
-      return unless @is_enabled
       return if @is_on == value
       @log.debug "DynamicMode is being turned #{value}"
       @is_on = value
       @checkDynamicMode()
       @onStatusChanged()
-
+
   checkDynamicMode: =>
     return unless @is_enabled
     return unless @mpd.library.artists.length
@@ -69,10 +73,10 @@

     if @is_on
       commands = []
-      delete_count = Math.max(current_index - 10, 0)
+      delete_count = Math.max(current_index - @history_size, 0)
       for i in [0...delete_count]
         commands.push "deleteid #{item_list[i].id}"
-      add_count = Math.max(11 - (item_list.length - current_index), 0)
+      add_count = Math.max(@future_size + 1 - (item_list.length - current_index), 0)

       commands = commands.concat ("addid #{JSON.stringify file}" for file in @getRandomSongFiles add_count)
       @mpd.sendCommands commands, (msg) =>


Update models/repo.coffee

Update models/repo.coffee

--- lib/models/repo.coffee
+++ lib/models/repo.coffee
@@ -16,7 +16,6 @@
     @branchList    = new BranchList []
     @commitList    = new CommitList []
     @currentBranch = new CurrentBranch(@headRefsCount() > 0)
-    git.on 'reload', @reload

   # Public: Forces a reload on the repository.
   reload: ->
@@ -30,10 +29,10 @@
   #
   # Returns the active selection as {Object}.
   selection: ->
-    @active_list.selection()
+    @activeList.selection()

   leaf: ->
-    @active_list.leaf()
+    @activeList.leaf()

   # Internal: The commit message file path.
   #
@@ -48,23 +47,21 @@
     atom.project.getRepo().getReferences().heads.length

   fetch: ->
-    git.incrementTaskCounter()
-    git.remoteFetch 'origin', ->
-      git.decrementTaskCounter()
+    git.cmd 'fetch'

   checkoutBranch: ->
-    @branchList.checkout_branch
+    @branchList.checkoutBranch

   stash: ->
-    git.git 'stash'
+    git.cmd 'stash'

   stashPop: ->
-    git.git 'stash pop'
+    git.cmd 'stash pop'

   # Internal: Initiate a new commit.
   initiateCommit: ->
-    if atom.config.get('atomatigit.pre_commit_hook') isnt ''
-      atom.workspaceView.trigger(atom.config.get('atomatigit.pre_commit_hook'))
+    if atom.config.get('atomatigit.PRE_COMMIT_HOOK') isnt ''
+      atom.workspaceView.trigger(atom.config.get('atomatigit.PRE_COMMIT_HOOK'))

     fs.writeFileSync(@commitMessagePath(), '')

@@ -107,9 +104,7 @@
   initiateCreateBranch: ->
     @trigger 'need_input',
       query: 'Branch name'
-      callback: (name) ->
-        git.createBranch name, ->
-          git.git "checkout #{name}"
+      callback: (name) -> git.cmd "checkout -b #{name}"

   # Public: Initiate a user defined git command.
   initiateGitCommand: ->


Cleanup :fire: no longer necessary intermediate scanPair function

--- lib/pair-finder.coffee
+++ lib/pair-finder.coffee
@@ -2,6 +2,7 @@
 _ = require 'underscore-plus'
 {
   isEscapedCharRange
+  getEndOfLineForBufferRow
   scanBufferRow
 } = require './utils'

@@ -50,25 +51,25 @@
   filterEvent: ->
     true

-  scanPair: (which, direction, from, fn) ->
+  getScanSpecFor: (direction, from) ->
     switch direction
       when 'forward'
-        scanRange = new Range(from, @editor.buffer.getEndPosition())
-        scanFunctionName = 'scanInBufferRange'
+        if @allowNextLine
+          scanRange = new Range(from, @editor.buffer.getEndPosition())
+        else
+          scanRange = new Range(from, getEndOfLineForBufferRow(@editor, from.row))
+        functionName = 'scanInBufferRange'
       when 'backward'
-        scanRange = new Range([0, 0], from)
-        scanFunctionName = 'backwardsScanInBufferRange'
-
-    @editor[scanFunctionName] @getPattern(), scanRange, (event) =>
-      if not @allowNextLine and (from.row isnt event.range.start.row)
-        event.stop()
-        return
-
-      return if isEscapedCharRange(@editor, event.range)
-      if @filterEvent(event)
-        fn(event)
-
-  findPair: (which, direction, from, fn) ->
+        if @allowNextLine
+          scanRange = new Range([0, 0], from)
+        else
+          scanRange = new Range([from.row, 0], from)
+        functionName = 'backwardsScanInBufferRange'
+    {scanRange, functionName, pattern: @getPattern()}
+
+  findPair: (which, direction, from) ->
+    {scanRange, functionName, pattern} = @getScanSpecFor(direction, from)
+
     stack = []
     range = null

@@ -76,7 +77,10 @@
     # it is forwarding pair, so stoppable is not @allowForwarding
     findingNonForwardingClosingQuote = (this instanceof QuoteFinder) and which is 'close' and not @allowForwarding

-    @scanPair which, direction, from, (event) =>
+    @editor[functionName] pattern, scanRange, (event) =>
+      return if isEscapedCharRange(@editor, event.range)
+      return unless @filterEvent(event)
+
       eventState = @getEventState(event)

       if eventState.state isnt which
@@ -85,7 +89,13 @@
           return
         stack.push(eventState)
       else
-        if fn(stack, eventState)
+        foundEvent = {eventState, from}
+        isValid =
+          switch eventState.state
+            when 'open' then @onOpenFound(stack, foundEvent)
+            when 'close' then @onCloseFound(stack, foundEvent)
+
+        if isValid
           range = event.range
           event.stop()

@@ -94,26 +104,28 @@
   spliceStack: (stack, eventState) ->
     stack.pop()

+  onCloseFound: (stack, {eventState, from}) ->
+    openState = @spliceStack(stack, eventState)
+    unless openState?
+      return true
+
+    if stack.length is 0
+      {start} = openState.range
+      start.isEqual(from) or (@allowForwarding and start.row is from.row)
+
+  onOpenFound: (stack, {eventState, from}) ->
+    @spliceStack(stack, eventState)
+    stack.length is 0
+
   findCloseForward: (from) ->
-    @findPair 'close', 'forward', from, (stack, closeState) =>
-      openState = @spliceStack(stack, closeState)
-      unless openState?
-        return true
-
-      if stack.length is 0
-        {start} = openState.range
-        start.isEqual(from) or (@allowForwarding and start.row is from.row)
+    @findPair('close', 'forward', from)

   findOpenBackward: (from) ->
-    @findPair 'open', 'backward', from, (stack, openState) =>
-      @spliceStack(stack, openState)
-      stack.length is 0
+    @findPair('open', 'backward', from)

   find: (from) ->
-    closeRange = @findCloseForward(from)
-    if closeRange?
-      @closeRange = closeRange
-      openRange = @findOpenBackward(closeRange.end)
+    closeRange = @closeRange = @findCloseForward(from)
+    openRange = @findOpenBackward(closeRange.end) if closeRange?

     if closeRange? and openRange?
       {


Don't parse line and column if path to open exists

This previously prevented file containing a colon from being open
from the command line.

Closes #2376

--- src/browser/atom-application.coffee
+++ src/browser/atom-application.coffee
@@ -306,13 +306,7 @@
   #   :safeMode - Boolean to control the opened window's safe mode.
   #   :windowDimensions - Object with height and width keys.
   openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions}={}) ->
-    if pathToOpen
-      [basename, initialLine, initialColumn] = path.basename(pathToOpen).split(':')
-      pathToOpen = path.join(path.dirname(pathToOpen), basename) if initialLine
-
-      # Convert line numbers to a base of 0
-      initialLine -= 1 if initialLine
-      initialColumn -= 1 if initialColumn
+    {pathToOpen, initialLine, initialColumn} = @locationForPathToOpen(pathToOpen)

     unless devMode
       existingWindow = @windowForPath(pathToOpen) unless pidToKillWhenClosed or newWindow
@@ -414,6 +408,20 @@

     isSpec = true
     new AtomWindow({bootstrapScript, @resourcePath, isSpec})
+
+  locationForPathToOpen: (pathToOpen) ->
+    return {pathToOpen} unless pathToOpen
+    return {pathToOpen} if fs.existsSync(pathToOpen)
+
+    [fileToOpen, initialLine, initialColumn] = path.basename(pathToOpen).split(':')
+    return {pathToOpen} unless initialLine
+    return {pathToOpen} unless parseInt(initialLine) > 0
+
+    # Convert line numbers to a base of 0
+    initialLine -= 1 if initialLine
+    initialColumn -= 1 if initialColumn
+    pathToOpen = path.join(path.dirname(pathToOpen), fileToOpen)
+    {pathToOpen, initialLine, initialColumn}

   # Opens a native dialog to prompt the user for a path.
   #


Remove debugger

Remove debugger

--- src/coffee/portal-buy-button.coffee
+++ src/coffee/portal-buy-button.coffee
@@ -117,7 +117,6 @@
 	update: =>
 		url = if @sku or @options.multipleProductIds then @getURL() else "javascript:alert('#{@options.errorMessage}');"
 		@element.attr('href', url)
-		debugger
 		@element.show()

 		if @options.hideUnavailable and @skuData and @skuData.available is false


Add JsDoc

Add JsDoc

--- src/coffee/services/s3client.coffee
+++ src/coffee/services/s3client.coffee
@@ -6,7 +6,21 @@
 ProgressBar = require 'progress'
 fs = Promise.promisifyAll require('fs')

+DEFAULT_IMG_EXTENSION = 'jpg'
+
+###*
+ * An S3Client wrapper that holds some useful methods to talk to S3
+###
 class S3Client
+
+  ###*
+   * Creates a new S3Client instance
+   * @constructor
+   * @param  {Object} opts A JSON object containing required information
+   *
+   * (internally initializes a `knoxClient`)
+   * {@link https://github.com/LearnBoost/knox}
+  ###
   constructor: (opts = {}) ->
     {key, secret, bucket} = opts
     throw new Error 'Missing AWS \'key\'' unless key
@@ -20,53 +34,111 @@

     @knoxClient = Promise.promisifyAll @knoxClient

+  ###*
+   * Lists all files in the given bucket
+   * @param  {Object} args The arguments to pass
+   * @return {Promise} A promise, fulfilled with the response or rejected with an error
+  ###
   list: (args) -> @knoxClient.listAsync args

+  ###*
+   * Returns a specific file from the given bucket
+   * @param  {String} source The path to the remote file (bucket)
+   * @return {Promise} A promise, fulfilled with the response or rejected with an error
+  ###
   getFile: (source) -> @knoxClient.getFileAsync source

+  ###*
+   * Uploads a given file to the given bucket
+   * @param  {String} source The path to the local file to upload
+   * @param  {String} filename The path to the remote destination file (bucket)
+   * @param  {Object} header A JSON object containing some Headers to send
+   * @return {Promise} A promise, fulfilled with the response or rejected with an error
+  ###
   putFile: (source, filename, header) -> @knoxClient.putFileAsync source, filename, header

+  ###*
+   * Copies a file directly in the bucket
+   * @param  {String} source The path to the remote source file (bucket)
+   * @param  {String} filename The path to the remote destination file (bucket)
+   * @return {Promise} A promise, fulfilled with the response or rejected with an error
+  ###
   copyFile: (source, destination) -> @knoxClient.copyFileAsync source, destination

+  ###*
+   * Moves a given file to the given bucket
+   * @param  {String} source The path to the remote source file (bucket)
+   * @param  {String} filename The path to the remote destination file (bucket)
+   * @return {Promise} A promise, fulfilled with the response or rejected with an error
+  ###
+  moveFile: (source, destination) ->
+    @copyFile(source, destination).then => @deleteFile source
+
+  ###*
+   * Deletes a file from the given bucket
+   * @param  {String} file The path to the remote file to be deleted (bucket)
+   * @return {Promise} A promise, fulfilled with the response or rejected with an error
+  ###
   deleteFile: (file) -> @knoxClient.deleteFileAsync file

-  moveFile: (source, destination) ->
-    @copyFile source, destination
-    .then => @deleteFile source
+  ###*
+   * @private
+   *
+   * Builds a
+   * @param  {String} prefix A prefix for the image key
+   * @param  {String} suffix A suffix for the image key
+   * @param  {String} [extension] An optional file extension (default 'jpg')
+   * @return {String} The built image key
+  ###
+  _imageKey: (prefix, suffix, extension) -> "#{prefix}#{suffix}#{extension or DEFAULT_IMG_EXTENSION}"

-  _imageKey: (prefix, suffix, extension) -> "#{prefix}#{suffix}#{extension or 'jpg'}"
-
-  resizeAndUploadImage: (image, prefix, formats) ->
+  ###*
+   * @private
+   *
+   * Resizes and uploads a given image to the bucket
+   * @param  {String} image The path the the image
+   * @param  {String} prefix A prefix for the image key
+   * @param  {Array} formats A list of formats for image resizing
+   * @return {Promise} A promise, fulfilled with the upload response or rejected with an error
+  ###
+  _resizeAndUploadImage: (image, prefix, formats) ->

     extension = path.extname image
     basename = path.basename image, extension
     basename_full = path.basename image

+    # TODO: use `tmp` module
     tmp_original = "/tmp/#{basename_full}"

     Promise.map formats, (format) =>
       tmp_resized = @_imageKey "/tmp/#{basename}", format.suffix, extension

       easyimage.resize
-        src: tmp_original,
-        dst: tmp_resized,
-        width: format.width,
+        src: tmp_original
+        dst: tmp_resized
+        width: format.width
         height: format.height
       .then (image) =>
         header = 'x-amz-acl': 'public-read'
         aws_content_key = @_imageKey "#{prefix}#{basename}", format.suffix, extension
         @putFile tmp_resized, aws_content_key, header
-      .catch (error) ->
+      .catch (error) -> # TODO: don't just swallow the error
     , {concurrency: 2}

+  ###*
+   * Resizes and uploads a list of images to the bucket
+   * Internally calls {@link _resizeAndUploadImage}
+   * @param  {Array} images A list of images to be processed
+   * @param  {Object} description A config JSON object describing the images conversion
+   * @return {Promise} A promise, fulfilled with a successful response or rejected with an error
+  ###
   resizeAndUploadImages: (images, description) ->

-    bar = new ProgressBar "Processing prefix '#{description.prefix}':\t[:bar] :percent, :current of :total images done (time: elapsed :elapseds, eta :etas)", {
-      complete: '=',
-      incomplete: ' ',
-      width: 20,
+    bar = new ProgressBar "Processing prefix '#{description.prefix}':\t[:bar] :percent, :current of :total images done (time: elapsed :elapseds, eta :etas)",
+      complete: '='
+      incomplete: ' '
+      width: 20
       total: images.length
-    }

     Promise.map images, (image) =>
       @getFile(image.Key)
@@ -78,15 +150,13 @@
           response.pipe stream
           response.on 'end', resolve
           response.on 'error', reject
-      .then =>
-        @resizeAndUploadImage image.Key, description.prefix, description.formats
+      .then => @resizeAndUploadImage image.Key, description.prefix, description.formats
       .then (result) =>
         name = path.basename(image.Key)
         source = "#{description.prefix_unprocessed}#{name}"
         target = "#{description.prefix_processed}#{name}"
         @moveFile source, target
-      .then ->
-        Promise.resolve bar.tick()
+      .then -> Promise.resolve bar.tick()
     , {concurrency: 1}

 module.exports = S3Client


add subject for email provider

add subject for email provider

--- src/client/sharer/email.coffee
+++ src/client/sharer/email.coffee
@@ -8,6 +8,7 @@
     @opts   = opts
     @params = opts.params || {}

+    @params.subject ||= document.title
     @params.body ||= @params.url

     return @sharePopup()
@@ -16,3 +17,4 @@
     @_redirect("mailto:", @opts, @params)

 module.exports = EmailShare
+


Insere com conteúdo com text ao inves de html

--- src/coffee/vtex-message.coffee
+++ src/coffee/vtex-message.coffee
@@ -86,10 +86,10 @@
 		$(@domElement).hide()
 		$(@domElement).data('vtex-message', @)
 		if @content.title and @content.title isnt ''
-			$(@classes.TITLE, @domElement).html(@content.title)
+			$(@classes.TITLE, @domElement).text(@content.title)
 		else
 			$(@classes.TITLE, @domElement).hide()
-		$(@classes.DETAIL, @domElement).html(@content.detail)
+		$(@classes.DETAIL, @domElement).text(@content.detail)

 		if @usingModal
 			$(@domElement).on 'hidden', => @visible = false


add time indexes to eval_inputs and eval_outputs

--- src/smc-util/syncstring_schema.coffee
+++ src/smc-util/syncstring_schema.coffee
@@ -324,6 +324,7 @@
     primary_key: ['string_id', 'time', 'user_id']
     durability : 'soft' # loss of eval requests not serious
     unique_writes: true
+    pg_indexes : ['time']
     fields:
         string_id :
             pg_type : 'CHAR(40)'
@@ -366,6 +367,7 @@
 schema.eval_outputs =
     primary_key: ['string_id', 'time', 'number']
     durability : 'soft' # loss of eval output not serious (in long term only used for analytics)
+    pg_indexes : ['time']
     fields:
         string_id :
             pg_type : 'CHAR(40)'


Fix couldn't be closed on electron 1.2.1

--- views/services.coffee
+++ views/services.coffee
@@ -55,11 +55,13 @@
 confirmExit = false
 exitPoi = ->
   confirmExit = true
+  remote.require('./lib/window').rememberMain()
+  remote.require('./lib/window').closeWindows()
+  window.onbeforeunload = (e) ->
   window.close()
 window.onbeforeunload = (e) ->
   if confirmExit || !config.get('poi.confirm.quit', false)
-    remote.require('./lib/window').rememberMain()
-    e.returnValue = true
+    exitPoi()
   else
     toggleModal __('Exit'), __('Confirm?'), [
       name: __ 'Confirm'


Remove a bunch of css for settings

Remove a bunch of css for settings

--- lib/theme-panel.coffee
+++ lib/theme-panel.coffee
@@ -17,15 +17,16 @@
 module.exports =
 class ThemeConfigPanel extends View
   @content: ->
-    @div id: 'themes-config', =>
-      @legend "Themes"
+    @div class: 'section', id: 'themes-config', =>
+      @h1 class: 'section-heading', "Themes"
       @div id: 'theme-picker', =>
         @div class: 'panel', =>
           @div class: 'panel-heading', "Enabled Themes"
-          @ol id: 'enabled-themes', class: 'list-group list-group-flush', outlet: 'enabledThemes'
+          @ol id: 'enabled-themes', class: 'list-group', outlet: 'enabledThemes'
+
         @div class: 'panel', =>
           @div class: 'panel-heading', "Available Themes"
-          @ol id: 'available-themes', class: 'list-group list-group-flush', outlet: 'availableThemes'
+          @ol id: 'available-themes', class: 'list-group', outlet: 'availableThemes'

   constructor: ->
     super
@@ -44,11 +45,12 @@
     @on "click", "#enabled-themes .disable-theme", (e) =>
       $(e.target).closest('li').remove()
       @enabledThemesUpdated()
+      false

   buildThemeLi: (name, {draggable} = {}) ->
     li = $$ ->
-      @li class: 'list-group-item', name: name, =>
-        @div class: 'disable-theme pull-right'
+      @li class: 'list-item', name: name, =>
+        @a href: '#', class: 'icon icon-x disable-theme pull-right'
         @text name
     if draggable
       li.draggable


extra helper method

extra helper method

--- lib/assets/javascripts/underscore_patch.js.coffee
+++ lib/assets/javascripts/underscore_patch.js.coffee
@@ -21,3 +21,15 @@
   if ! str
     return []
   return str.split(regex)
+
+# Check whether the given element is in the current viewport
+#
+# via: http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling#488073
+_.isScrolledIntoView = (el) ->
+  docViewTop = $(window).scrollTop()
+  docViewBottom = docViewTop + $(window).height()
+
+  elTop = $(el).offset().top
+  elBottom = elTop + $(el).height()
+
+  return ((elBottom <= docViewBottom) && (elTop >= docViewTop))


Fix https://github.com/poooi/poi/issues/1031

--- views/tabarea.cjsx
+++ views/tabarea.cjsx
@@ -256,7 +256,7 @@
     else

-
+
             <NavItem key='mainView' eventKey='mainView'>
               {mainview.displayName}
             </NavItem>
@@ -281,8 +281,8 @@
           </TabContentsUnion>

-
-            <NavDropdown id='plugin-dropdown' pullRight
+
+            <NavDropdown id='plugin-dropdown' pullRight onSelect={@handleSelectDropdown}
               title={activePlugin?.displayName || defaultPluginTitle}>
             {pluginDropdownContents}
             </NavDropdown>


NEW: Add additional API helpdesk endpoints

--- src/app/api/models/helpdesk-ticket.coffee
+++ src/app/api/models/helpdesk-ticket.coffee
@@ -6,18 +6,90 @@
   HelpdeskTicket = resourcePlus "/helpdesk/tickets/", { id: "@id" }

   #
-  # Gets all unresolved tickets, optionally for the specified user.
-  # If user is not specified, then all unresolved tickets will be
-  # returned from all users.
+  # Submits a new helpdesk ticket. Provide a project ID, description and task
+  # def id. Project ID is required, whereas the others can be `null`. Callback
+  # is a function whose first parameter is error and second parameter is success
+  # data.
   #
-  HelpdeskTicket.getUnresolvedTickets = (user, callback) ->
-    HelpdeskTicket.get({ user_id: user }, )
+  HelpdeskTicket.submitTicket = (projectId, description, taskDefinitionId, callback) ->
+    dataToPost =
+      project_id: projectId
+      description: description
+      task_definition_id: taskDefinitionId
+    onSuccess = (response) -> callback(null, response)
+    onFailure = (response) -> callback(response)
+    HelpdeskTicket.create(dataToPost)
+                  .$promise
+                  .then(onSuccess, onFailure)
+
+  #
+  # Gets all tickets by a specific state, optionally for the specified user id.
+  # If user is not specified, then all tickets will be returned.
+  #
+  HelpdeskTicket.getTickets = (state, userId, callback) ->
+    onSuccess = (response) -> callback(null, response)
+    onFailure = (response) -> callback(response)
+    HelpdeskTicket.get({ user_id: user, filter: state })
+                  .$promise
+                  .then(onSuccess, onFailure)
+
+  #
+  # Gets all unresolved tickets (optionally by user)
+  #
+  HelpdeskTicket.getUnresolvedTickets = (userId, callback) ->
+    HelpdeskTicket.getTickets('unresolved', userId, callback)
+
+  #
+  # Gets all resolved tickets (optionally by user)
+  #
+  HelpdeskTicket.getResolvedTickets = (userId, callback) ->
+    HelpdeskTicket.getTickets('resolved', userId, callback)
+
+  #
+  # Gets all closed tickets (optionally by user)
+  #
+  HelpdeskTicket.getClosedTickets = (userId, callback) ->
+    HelpdeskTicket.getTickets('closed', userId, callback)
+
+  #
+  # Closes a specified ticket with the given ID
+  #
+  HelpdeskTicket.closeTicket = (ticketId, callback) ->
+    onSuccess = (response) -> callback(null, response)
+    onFailure = (response) -> callback(response)
+    HelpdeskTicket.delete({ id: ticketId })
+                  .$promise
+                  .then(onSuccess, onFailure)
+
+  #
+  # Prototype close ticket method
+  #
+  HelpdeskTicket.prototype.close = (callback) ->
+    HelpdeskTicket.closeTicket(this.id, callback)
+
+  #
+  # Resolves a specified ticket with the given ID
+  #
+  HelpdeskTicket.resolveTicket = (ticketId, callback) ->
+    onSuccess = (response) -> callback(null, response)
+    onFailure = (response) -> callback(response)
+    HelpdeskTicket.delete({ id: ticketId, resolve: true })
+                  .$promise
+                  .then(onSuccess, onFailure)
+
+  #
+  # Prototype resolve ticket method
+  #
+  HelpdeskTicket.prototype.resolve = (callback) ->
+    HelpdeskTicket.resolveTicket(this.id, callback)

   #
   # Returns the duration since the ticket was open
   #
   HelpdeskTicket.prototype.lengthOfTimeOpen = ->
-    this.created_at
+    now = moment()
+    createdTime = moment(this.created_at)
+    moment.duration(createdTime.diff(now)).humanize()

   HelpdeskTicket
 )


fix delete msg with oembed problem -  closes #1429

view renders oembed in msg - even if msg type is 'rm'

--- packages/rocketchat-lib/server/models/Messages.coffee
+++ packages/rocketchat-lib/server/models/Messages.coffee
@@ -140,6 +140,7 @@
 			$set:
 				msg: ''
 				t: 'rm'
+				urls: []
 				editedAt: new Date()
 				editedBy:
 					_id: Meteor.userId()


make epub container not dirty on load

make epub container not dirty on load

--- scripts/gh-book/epub-container.coffee
+++ scripts/gh-book/epub-container.coffee
@@ -26,7 +26,7 @@
     initialize: () ->
       @children = new Backbone.Collection()

-      @children.on 'change', (collection, options) =>
+      @children.on 'add', (collection, options) =>
         @_markDirty(options, true)

       @children.on 'reset', (collection, options) =>
@@ -78,8 +78,7 @@
     # Called by `loadableMixin.reload` when the repo settings change
     reset: () -> @children.reset()

-    addChild: (book) ->
-      @children.add(book)
+    addChild: (book) -> @children.add(book)

   EpubContainer = EpubContainer.extend loadableMixin
   # All content in the Workspace


reset default tab

reset default tab

--- src/coffee/controllers/player-ctrl.coffee
+++ src/coffee/controllers/player-ctrl.coffee
@@ -19,7 +19,7 @@
       values: []
     $scope._player = Player
     $scope.xpPercent = 0
-    $scope.selectedIndex = 3
+    $scope.selectedIndex = 0
     $scope.statisticsKeys = {}
     $scope._ = $window._


Allow to use custom formatter for handlebars helper formatDate

--- src/coffee/utils/handlebarsHelpers.coffee
+++ src/coffee/utils/handlebarsHelpers.coffee
@@ -14,8 +14,9 @@
       return b

   # FORMAT DATE
-  hb.registerHelper "formatDate", (value) ->
-    moment.utc(value).format("LLLL")
+  hb.registerHelper "formatDate", (value, format) ->
+    format = "LLLL" unless _.isString(format)
+    moment.utc(value).format(format)

   # VALUEKEY return value for given object key
   hb.registerHelper 'valueKey', (object, key, placeholder) ->


feat: Respect $GOBIN, $GOPATH, $(go env GOPATH)

--- src/hooks-worker-client.coffee
+++ src/hooks-worker-client.coffee
@@ -139,21 +139,28 @@
       # If the GOBIN environment variable is set, commands are installed to the
       # directory it names instead of DIR/bin. GOBIN must be an absolute path."
       # Use the gobin if provided, otherwise fall back to gopath
-      gobin = process.env.GOBIN
-      if !gobin
-        gobin = "#{process.env.GOPATH}/bin"
-
-      @handlerCommand = "#{gobin}/goodman"
-      @handlerCommandArgs = []
-      unless which.which @handlerCommand
-        msg = '''\
-          Go hooks handler command not found in $GOBIN or $GOPATH/bin
-          Install go hooks handler by running:
-          $ go get github.com/snikch/goodman/cmd/goodman
-        '''
-        return callback(new Error(msg))
-      else
-        callback()
+      {exec} = require 'child_process'
+      getGobin = (callback) ->
+        gobin = process.env.GOBIN
+        if !gobin
+          if process.env.GOPATH
+            gobin = "#{process.env.GOPATH}/bin"
+          else
+            exec "go env GOPATH", (error, stdout, stderr) -> callback "#{stdout.trim()}/bin"
+            return
+        callback gobin
+      getGobin (gobin) =>
+        @handlerCommand = "#{gobin}/goodman"
+        @handlerCommandArgs = []
+        unless which.which @handlerCommand
+          msg = '''\
+            Go hooks handler command not found in $GOBIN or $GOPATH/bin
+            Install go hooks handler by running:
+            $ go get github.com/snikch/goodman/cmd/goodman
+          '''
+          return callback(new Error(msg))
+        else
+          callback()
     else
       parsedArgs = spawnArgs(@language)
       @handlerCommand = parsedArgs.shift()


Include ember-i18n

Include ember-i18n

--- demo/app/app.coffee
+++ demo/app/app.coffee
@@ -1,7 +1,9 @@
 # Ember Menglifang Dependencies
 require 'bower_components/jquery/jquery'
+require 'bower_components/cldr/plurals'
 require 'bower_components/handlebars/handlebars'
 require 'bower_components/ember/ember'
+require 'bower_components/ember-i18n/lib/i18n'
 require 'bower_components/ember-data/ember-data'
 require 'bower_components/ember-simple-auth/ember-simple-auth'
 require 'bower_components/bootstrap/dist/js/bootstrap'
@@ -13,10 +15,6 @@

 require 'dist/menglifang-app'

-Menglifang.App.TRANSLATIONS =
-  admin: '管理员'
-  user: '普通用户'
-
 App = window.App = Menglifang.App.create
   host: 'http://localhost:8000'
   namespace: 'v1'


Add a test to check that users are summarised properly even when a null user occurs earlier in the update list.

--- services/track-changes/test/unit/coffee/UpdatesManager/UpdatesManagerTests.coffee
+++ services/track-changes/test/unit/coffee/UpdatesManager/UpdatesManagerTests.coffee
@@ -655,15 +655,8 @@
 					end_ts:   @now + 30
 			}]

-		it "should roll several null user values into one", ->
+		it "should include null user values, when the null is earlier in the updates list", ->
 			result = @UpdatesManager._summarizeUpdates [{
-				doc_id: "doc-id-1"
-				meta:
-					user: @user_1
-					start_ts: @now + 20
-					end_ts:   @now + 30
-				v: 5
-			}, {
 				doc_id: "doc-id-1"
 				meta:
 					user: null
@@ -673,10 +666,10 @@
 			}, {
 				doc_id: "doc-id-1"
 				meta:
-					user: null
-					start_ts: @now + 2
-					end_ts:   @now + 4
-				v: 4
+					user: @user_1
+					start_ts: @now + 20
+					end_ts:   @now + 30
+				v: 5
 			}]
 			expect(result).to.deep.equal [{
 				docs:
@@ -684,6 +677,40 @@
 						fromV: 4
 						toV: 5
 				meta:
+					users: [null, @user_1]
+					start_ts: @now
+					end_ts:   @now + 30
+			}]
+
+		it "should roll several null user values into one", ->
+			result = @UpdatesManager._summarizeUpdates [{
+				doc_id: "doc-id-1"
+				meta:
+					user: @user_1
+					start_ts: @now + 20
+					end_ts:   @now + 30
+				v: 5
+			}, {
+				doc_id: "doc-id-1"
+				meta:
+					user: null
+					start_ts: @now
+					end_ts:   @now + 10
+				v: 4
+			}, {
+				doc_id: "doc-id-1"
+				meta:
+					user: null
+					start_ts: @now + 2
+					end_ts:   @now + 4
+				v: 4
+			}]
+			expect(result).to.deep.equal [{
+				docs:
+					"doc-id-1":
+						fromV: 4
+						toV: 5
+				meta:
 					users: [@user_1, null]
 					start_ts: @now
 					end_ts:   @now + 30


Tests: Activity: EditDelete: Deleted wrong test.

--- tests/src/activity/editdelete.coffee
+++ tests/src/activity/editdelete.coffee
@@ -95,19 +95,3 @@
       .waitForElementNotPresent '.kdoverlay', 25000
       .assert.containsText      activitySelector + ' .comment-container', comment # Assertion
       .end()
-
-
-  cancelEditDeletion: (browser) ->
-
-    comment         = helpers.postComment(browser)
-    commentSelector = activitySelector + ' .comment-container button.comment-menu'
-
-    browser
-      .waitForElementPresent    commentSelector, 25000
-      .click                    commentSelector
-      .waitForElementVisible    '.kdcontextmenu .delete-comment', 25000
-      .click                    '.kdcontextmenu .delete-comment'
-      .click                    '.kdmodal-inner .solid.light-gray.medium'
-      .waitForElementNotPresent '.kdoverlay', 25000
-      .assert.containsText      activitySelector + ' .comment-container', comment # Assertion
-      .end()


Spectator chat: Don't crash. A strong case for tests.

--- server/battle_controller.coffee
+++ server/battle_controller.coffee
@@ -48,8 +48,8 @@

   messageSpectators: (user, message) ->
     for spectator in @battle.spectators
-      continue  if spectator == user
-      spectator.send('update battle chat', battleId, user.toJSON(), message)
+      continue  if spectator.id == user.id
+      spectator.send('update battle chat', @battle.id, user.toJSON(), message)

   # Continue or begin a new turn if each player has made an action.
   transitionToNextState: ->


Fixing test broken from fix for DE18628

Fixing test broken from fix for DE18628

--- test/spec/kanban/KanbanAppSpec.coffee
+++ test/spec/kanban/KanbanAppSpec.coffee
@@ -277,6 +277,18 @@
     settings = {}
     settings.groupByField = groupByField
     settings[settingsKey] = policy
+
+    field =
+      name: groupByField
+
+    try
+      userstory_model = Rally.test.mock.data.WsapiModelFactory.getUserStoryModel()
+      userstory_model.addField field
+      defect_model = Rally.test.mock.data.WsapiModelFactory.getDefectModel()
+      defect_model.addField field
+    finally
+      @removeField(userstory_model, field)
+      @removeField(defect_model, field)

     @createApp(settings).then =>
       @assertPolicyCmpConfig('c_' + settingsKey, policy)
@@ -353,3 +365,7 @@
       expect(Ext.Object.getKeys(prefConfigSettings)[0]).toBe settingsKey
       expect(prefConfigSettings[settingsKey]).toBe policy
       expect(plugin.policyCmpConfig.policies).toBe policy
+
+    removeField: (model, field) ->
+      model.prototype.fields.remove field
+      expect(model.getField(field.name)).toBeUndefined


did SOME balancing

did SOME balancing

--- src/js/debaters/parent-judge.coffee
+++ src/js/debaters/parent-judge.coffee
@@ -12,7 +12,7 @@
     # @level (speaker score) (Lvl)
     # @type (type)
     @clash = 4 # ATTACK
-    @presentation = 6 # SP. ATTACK
+    @presentation = 3 # SP. ATTACK
     @organization = 4 # SPEED
     @civility = 6 # Totally arbitrary
     @cross = 3 # DEFENCE


Updated pause-typing docs.

Updated pause-typing docs.

--- src/directives/events/pause_typing.coffee
+++ src/directives/events/pause_typing.coffee
@@ -1,7 +1,16 @@
+###
+@chalk overview
+@name Pause Typing
+
+@description
+macPauseTyping directive allow user to specify custom behavior after user stops typing for more than (delay) milliseconds
+
+@param {Expression} mac-pause-typing       Expression to evaluate after delay
+@param {Expression} mac-pause-typing-delay Delay value to evaluate expression (default 800)
+###
+
 angular.module("Mac").
   directive("macPauseTyping", ["$parse", "$timeout", ($parse, $timeout) ->
-    # Fires when the user stops typing for more than (delay) milliseconds
-    # To change the delay, add a mac-pause-typing-delay="500" attribute to the element
     restrict: "A"
     link:     (scope, element, attributes) ->
       expression = $parse attributes["macPauseTyping"]


minor

minor

--- src/lib/blockchains/nxt.coffee
+++ src/lib/blockchains/nxt.coffee
@@ -69,7 +69,7 @@

             @log.debug gLineInfo("#{@name} resolve"), {path:path}

-            req = http.get @peer + path, (res) ->
+            req = http.get @peer + encodeURIComponent(path), (res) ->
                 data = ''
                 res.on 'data', (chunk) ->
                     data += chunk.toString()
@@ -80,8 +80,8 @@
                       cb null, result
                     catch e
                       cb e
-            req.on 'error', ->
-                cb
+            req.on 'error', (e) ->
+                cb e

         validRequest: (path) -> VALID_NXT_DOMAINS.test path


Log release.tag_name correctly

Log release.tag_name correctly

--- build/tasks/publish-build-task.coffee
+++ build/tasks/publish-build-task.coffee
@@ -190,7 +190,7 @@
       callback(null, body)

 deleteRelease = (release) ->
-  grunt.log.ok("Deleting GitHub release #{release}")
+  grunt.log.ok("Deleting GitHub release #{release.tag_name}")
   options =
     uri: release.url
     method: 'DELETE'
@@ -201,7 +201,7 @@
       logError('Deleting release failed', error, body)

 deleteExistingAssets = (release, assetNames, callback) ->
-  grunt.log.ok("Deleting #{assetNames.join(',')} from GitHub release #{release}")
+  grunt.log.ok("Deleting #{assetNames.join(',')} from GitHub release #{release.tag_name}")
   [callback, assetNames] = [assetNames, callback] if not callback?

   deleteAsset = (url, callback) ->
@@ -223,7 +223,7 @@

 uploadAssets = (release, buildDir, assets, callback) ->
   uploadToReleases = (release, assetName, assetPath, callback) ->
-    grunt.log.ok("Uploading #{assetName} to GitHub release #{release}")
+    grunt.log.ok("Uploading #{assetName} to GitHub release #{release.tag_name}")
     options =
       uri: release.upload_url.replace(/\{.*$/, "?name=#{assetName}")
       method: 'POST'


do not idle timeout projects in cocalc-personal

--- src/smc-hub/compute-server.coffee
+++ src/smc-hub/compute-server.coffee
@@ -874,6 +874,9 @@
     )

 init_mintime = (cb) ->
+    if process.env.COCALC_PERSONAL == 'yes'
+       cb()
+       return
     setInterval(kill_idle_projects, 3*60*1000)
     kill_idle_projects(cb)


Specify containerOf when value mismatch = false

--- spec/bacon.matchers.spec.coffee
+++ spec/bacon.matchers.spec.coffee
@@ -102,6 +102,11 @@
             alien: 'morninglightmountain'
           stream = Bacon.once(object).is().containerOf(object)
           assertConstantly true, stream, done
+        it 'should return false when key matches but value does not', (done) ->
+          stream = Bacon.once(
+            alien: 'morninglightmountain'
+          ).is().containerOf(alien: 't1000')
+          assertConstantly false, stream, done
         it 'should return false when the object contains a subset of the compared value', (done) ->
           stream = Bacon.once(
             alien: 'morninglightmountain'


:lipstick: Add missing space

:lipstick: Add missing space

--- spec/bookmarks-view-spec.coffee
+++ spec/bookmarks-view-spec.coffee
@@ -199,4 +199,4 @@
         bookmarks.find('.bookmark').mousedown().mouseup()

         waitsFor ->
-         editor.getCursorBufferPosition().isEqual([8, 0])
+          editor.getCursorBufferPosition().isEqual([8, 0])


If user turns on a new vehicle after app is running, zoom into that new flt

--- src/scripts/controllers/mapController.coffee
+++ src/scripts/controllers/mapController.coffee
@@ -76,7 +76,8 @@
     scope.leafletData = leafletData
     @boundsFactory = new BoundsFactory

-    @count = 0
+    @currentMissionId = -1 # The most recent flight we have zoomed in on
+
     scope.vehicleMarkers = {}
     scope.vehiclePaths = {}
     scope.bounds = {} # Angular wants to see at least an empty object (not undefined)
@@ -111,12 +112,20 @@
     listeners =
       "loc": @onLive
       "att": @onAttitude
-      "start": @onMissionUpdate
       "end": @onMissionEnd
       "mode": @updateVehicleMessage
       "arm": @updateVehicleMessage
+
+      # Contains a complete update of mission state - resent occasionally by the server
       "update": @onMissionUpdate
-      "user": @onUserMessage
+
+      # Formatted the same as an update, but indicates start of a new mission (we treat both cases identically)
+      "start": @onMissionUpdate
+
+      # This is like a mission update, but it is guaranteed to be the most recent flight for the current user
+      # It is also _guaranteed_ to be sent before any start/update msgs on the stream
+      "user": @onMissionUpdate
+
       "mystery": @updateVehicleMessage
       "text": @updateVehicleMessage

@@ -143,17 +152,7 @@
         # markers (probably a good idea) or we need to recreate the marker to get the new heading to show up.
         #@log.debug("Can't set angle to #{v.iconAngle} because angular-leaflet is dumb and separates options from markers")

-  # This is like a mission update, but it is guaranteed to be the most recent flight for the current user
-  onUserMessage: (data) =>
-    @log.debug('handling user msg')
-    @onMissionUpdate(data) # First do a regular update
-    # Now recenter on the specified flight
-    @scope.$apply () =>
-      @scope.center =
-        lat: data.payload.latitude
-        lng: data.payload.longitude
-        zoom: 9
-
+  # This is an update of mission data for a prexisting flight
   onMissionUpdate: (data) =>
     @scope.$apply () =>
       key = @vehicleKey(data.missionId)
@@ -163,11 +162,26 @@
       if lat? && lon?
         v = @updateVehicle(key, lat, lon, payload)
         @updateMarkerPopup(v, payload)
+        if v.isMine
+          @zoomToVehicle(v)

   onMissionEnd: (data) =>
     vehicleKey = @vehicleKey(data.missionId)
     delete @scope.vehicleMarkers[vehicleKey]
     delete @scope.vehiclePaths[vehicleKey]
+
+  # Zoom to vehicle, if it is different from the previous targeted vehicle
+  zoomToVehicle: (vehicle) =>
+    payload = vehicle.payload
+    lat = payload.latitude
+    lon = payload.longitude
+    if payload.id != @currentMissionId
+      @log.debug("Zooming to #{payload.id}")
+      @currentMissionId = payload.id
+      @scope.center =
+        lat: lat
+        lng: lon
+        zoom: 9

   # Returns the marker
   updateVehicle: (vehicleKey, lat, lon, newMission) =>
@@ -187,11 +201,11 @@
     # direction of arrow on icon should change depending on direction

     loginName = @authService.getUser()?.login
-    isMine = loginName == mission?.userName
-    @log.debug("#{mission?.userName} #{mission?.id} vs #{loginName} isMine=#{isMine}")
+    v.isMine = loginName == mission?.userName
+    @log.debug("#{mission?.userName} #{mission?.id} vs #{loginName} isMine=#{v.isMine}")
     v.icon =
         iconUrl:
-          if isMine
+          if v.isMine
             mission.userAvatarImage + '?d=mm'
           else if isLive
             'images/vehicle-marker-active.png'
@@ -201,7 +215,7 @@
         iconAnchor: [17.5, 17.5] # point of the icon which will correspond to marker's location
         popupAnchor: [0, -17.5] # point from which the popup should open relative to the inconAnchor

-    if isMine # Show rounded corners on avatar icons
+    if v.isMine # Show rounded corners on avatar icons
       v.icon.className = "img-rounded"

     @scope.vehicleMarkers[vehicleKey] = v


Groups: searchRoute is regressed from dashboard

--- client/Dashboard/AppController.coffee
+++ client/Dashboard/AppController.coffee
@@ -12,7 +12,6 @@
       "/:name?/Dashboard/:section" : ({params : {section,name}})->
         handler name, (app)-> app.loadSection title : decodeURIComponent section
     hiddenHandle : yes
-    searchRoute : "/Dashboard?q=:text:"

   constructor: (options = {}, data) ->


Activity: new message modal arrow to point to `new` link when the screen is small

--- client/Social/Activity/AppView.coffee
+++ client/Social/Activity/AppView.coffee
@@ -159,6 +159,7 @@
     top      = bounds.y - 310
     left     = bounds.x + bounds.w + 40
     arrowTop = 310 + (bounds.h / 2) - 10 #10 = arrow height
+    arrowTop = arrowTop + top  if top < 0

     modal = new PrivateMessageModal
       delegate     : this


Add comments to node.structured

Add comments to node.structured

--- src/public/library/node.structured.coffee
+++ src/public/library/node.structured.coffee
@@ -286,6 +286,14 @@
 		getSiblings: ( ) ->
 			return @getPeers(Peer.Role.Sibling)

+		# Set this node to be supernode or not. When we're set to
+		# become supernode, we set all connected supernodes as siblings
+		# and create a token if we don't have one yet. If we're demoted
+		# from supernode status, we remove our token, remove our siblings
+		# and advice a new parent to all children.
+		#
+		# @param superNode [Boolean] wether or not to become supernode.
+		#
 		setSuperNode: (superNode) ->
 			if @isSuperNode is superNode
 				return
@@ -297,30 +305,46 @@
 			@trigger('setSuperNode', superNode)

 			if @isSuperNode
+				# If we have a parent, remove it.
 				if @_parent?
 					peer = @_parent
 					@removeParent()
-					@addSibling(peer)
-				@addSibling(peer) for peer in @getPeers() when peer.isSuperNode
+
+				# Set all connected supernodes as siblings.
+				@addSibling(peer) for peer in @getPeers() when peer.isSuperNode
+
+				# If we don't have a token, create one.
 				unless @token?
 					@token = new Token()
 					@token.nodeId = @id
 					@token.position = @position

 			else
+				# If we have a token, remove it.
 				if @token?
 					@broadcast('token.die', @token.serialize())
 					console.log "I quit. Sending token.die", @token.id
 					@token = null
+
+				# Remove all siblings.
 				for sibling in @getSiblings()
 					@removeSibling(sibling)
+
+				# Recommend a new parent to all children, and then
+				# remove them.
 				@_recommendParent(false)
 				for child in @getChildren()
 					@removeChild(child)
+
+				# Select a new parent.
 				@_selectParent()

-
-		_onPeerDisconnect: (peer) =>
+		# Is called when a peer disconnects. Will make sure we handle this
+		# disconnect in the appropriate manner.
+		#
+		# @param peer [Peer] the peer that disconnected.
+		#
+		_onPeerDisconnect: ( peer ) =>
 			if peer is @_parent
 				@removeParent()
 			else if peer.role is Peer.Role.Sibling
@@ -328,116 +352,137 @@
 			else if peer.role is Peer.Role.Child
 				@removeChild(peer)

-
-		_onPeerSetSuperNode: (id, superNode) =>
+		# Is called when a peer switches supernode status. We will connect
+		# to all new supernodes when we're supernode.
+		#
+		# @param id [String] the string id the of the peer
+		# @param superNode [Boolean] the new supernode status of the peer
+		#
+		_onPeerSetSuperNode: ( id, superNode ) =>
 			unless peer = @getPeer(id)
 				if superNode and @isSuperNode
 					peer = @connect(id)
 					peer.once('channel.opened', =>
 						@addSibling(peer)
 					)
-			else
-				peer.isSuperNode = superNode
-
-		_enterNetwork: () =>
+			else peer.isSuperNode = superNode
+
+		# Attempts to enter the network by requesting a list of supernodes
+		# and selecting and connecting to a parent from the list.
+		#
+		_enterNetwork: ( ) =>

 			if @isSuperNode or @_parent?
 				return

-			connectParent =  (superNodes) =>
-
+			# Recursive submethod attempt connections to a list
+			# of nodes untill success.
+			connectParent =  ( superNodes ) =>
+				# If no available supernodes, we promote ourself.
 				if superNodes.length is 0
 					@setSuperNode(true)
 					return

-				i = _.random(0,superNodes.length-1)
+				# We pick a random supernode from the list.
+				i = _.random(0, superNodes.length - 1)
 				superNode = superNodes[i]
 				superNodes.splice(i, 1)

+				# If we're connected, just set as parent.
 				if peer = @getPeer(superNode.id)
-					@setParent(peer, (accepted) =>
+					@setParent(peer, ( accepted ) =>
 						unless accepted
 							connectParent(superNodes)
 					)
+
+				# Else, first connect, then set as parent.
 				else
 					peer = @connect(superNode.id)
-					peer.once('channel.opened', () =>
-						@setParent(peer, (accepted) =>
+					peer.once('channel.opened', ( ) =>
+						@setParent(peer, ( accepted ) =>
 							unless accepted
 								connectParent(superNodes)
 						)
 					)
-
-			@server.query('nodes', 'node.structured', (nodes) =>
-				superNodes = _(nodes).filter( (node) -> node.isSuperNode)
-
-				if superNodes.length is 0
-					@setSuperNode(true)
-					return
-
+			# Query the server for nodes, and pass attempt to connect to supers.
+			@server.query('nodes', 'node.structured', ( nodes ) =>
+				superNodes = _(nodes).filter( ( node ) -> node.isSuperNode)
 				connectParent(superNodes)
-
 			)

-		_selectParent: () ->
+		# Selects the supernode with the lowest latency and attempts to connect.
+		# If this fails, try the next lowest latency.
+		#
+		_selectParent: ( ) ->

 			if @isSuperNode or @_parent?
 				return

+			# Recursive submethod attempt connections to a list
+			# of nodes untill success.
 			requestParent = (superNodes) =>
-
+				# If no supernode is left, request from the server.
 				if superNodes.length is 0
 					@_enterNetwork()
 					return

+				# Connect to the lowest latency node.
 				superNode = superNodes.shift()
-
 				@setParent(superNode, (accepted) =>
 					unless accepted
 						requestParent(superNodes)
 				)

+			# Compile a list of connected supernodes and sort by latency.
 			superNodes = _(@getPeers()).filter( (peer) -> peer.isSuperNode)
 			superNodes = _(superNodes).sortBy( (peer) -> peer.latency)
 			requestParent(superNodes)

+		# Ensures that the foundation of this node remains valid. Will do  this
+		# by connecting to supernodes when required, or setting a parent when we
+		# don't have one.
+		#
 		_updateFoundationNodes: () =>
-
-
+			# Make sure we have a parent when we need one.
+			if not @isSuperNode and not @_parent?
+				@_selectParent()
+				return
+
+			# Request all nodes from the server and set all supernodes as siblings.
 			if @isSuperNode
-				@server.query('nodes', 'node.structured', (nodes) =>
-					superNodes = _(nodes).filter( (node) -> node.isSuperNode)
+				@server.query('nodes', 'node.structured', ( nodes ) =>
+					superNodes = _(nodes).filter( ( node ) -> node.isSuperNode)

 					if superNodes.length is 0
 						return

 					for superNode in superNodes
-						( (superNode) =>
+						( ( superNode ) =>

 							if peer = @getPeer(superNode.id)
 								@addSibling(peer)
 							else
 								peer = @connect(superNode.id)
-								peer.once('channel.opened', () =>
+								peer.once('channel.opened', ( ) =>
 									@addSibling(peer)
 								)
 						) (superNode)
 				)

-			else unless @_parent?
-				@_selectParent()
-
+			# Ensure we are connected to enough supernodes to aid us in finding
+			# our correct position in the network and to catch our fall we we
+			# lose our parent.
 			else
 				current = _(@getPeers()).filter( (peer) -> peer.isSuperNode).length
 				needed = @_foundationNodes - current
 				if needed <= 0
 					return

-				@_parent.query('siblings', (superNodes) =>
+				@_parent.query('siblings', ( superNodes ) =>
 					if superNodes.length is 0
 						return

-					superNodes = _(superNodes).filter( (node) => not @getPeer(node.id)?)
+					superNodes = _(superNodes).filter( ( node ) => not @getPeer(node.id)?)
 					n = Math.min(needed, superNodes.length)
 					while superNodes.length > n
 						i = _.random(0,superNodes.length-1)
@@ -446,7 +491,10 @@
 					@connect(superNode.id) for superNode in superNodes
 				)

-		_updatePosition: () =>
+		# Updates our position by requesting latencies and positions
+		# of all connected nodes, and computing our position from that.
+		#
+		_updatePosition: ( ) =>
 			i = 0
 			for peer in @getPeers()
 				( ( peer ) =>
@@ -461,7 +509,11 @@
 					)
 				) ( peer )

-		_computePosition: () ->
+		# Computes our position in the network from the positions of our neighbours
+		# and the latency to them. This implements the vivaldi network coordinates:
+		# http://en.wikipedia.org/wiki/Vivaldi_coordinates
+		#
+		_computePosition: ( ) ->
 			for peer in @getPeers()
 				direction = peer.position.subtract(@position)				# Vector to peer
 				distance = peer.position.getDistance(@position)				# Distance between node and peer
@@ -477,7 +529,12 @@
 				@token.position = @position
 				@_computeTokenTargetPosition()

-		_recommendParent: (includeSelf = true) =>
+		# Recommends a parent to our children. We do this by checking which sibling is closest to
+		# the child. We can include ourselves or not when we want to get rid of all our children.
+		#
+		# @param includeSelf [Boolean] wether or not to include ourselves in the recommendation.
+		#
+		_recommendParent: ( includeSelf = true ) =>

 			siblings = @getSiblings()
 			children = @getChildren()
@@ -488,16 +545,22 @@
 			for child in children when child.position?
 				closestSuperNode = null
 				closestDistance = Infinity
+
 				for sibling in siblings when sibling.position?
 					distance = child.position.getDistance(sibling.position)
 					if distance < closestDistance
 						closestDistance = distance
 						closestSuperNode = sibling
+
 				if closestDistance < child.position.getDistance(@position) * @_superNodeSwitchThreshold or not includeSelf
 					child.emit('peer.recommendParent', closestSuperNode.id)

-
-		_onPeerRecommendParent: (id) =>
+		# Is called when we receive a parent recommendation of our parent. Will attempt to
+		# set the other supernode as parent.
+		#
+		# @param id [String] the string identifier of the recommended parent.
+		#
+		_onPeerRecommendParent: ( id ) =>

 			if @isSuperNode
 				return
@@ -506,7 +569,7 @@
 				@setParent(peer)
 			else
 				peer = @connect(id)
-				peer.once('channel.opened', () =>
+				peer.once('channel.opened', ( ) =>
 					@setParent(peer)
 				)

@@ -529,15 +592,21 @@
 			if oldToken = _(@_tokens).find( ( t ) -> token.id is t.id)
 				@_tokens.remove(oldToken)

-
-		_distributeToken: () =>
+		# Creates a new token and passes it on to a random child.
+		#
+		_distributeToken: ( ) =>
 			token = new Token()
 			children = @getChildren()
 			randomChild = children[_.random(0,children.length-1)]
 			randomChild.emit('token.receive', token.serialize())
 			console.log  randomChild.id +  ' received a token with id ', token.id

-		_onTokenReceived: (tokenString) =>
+		# Is called when we received a token. This will start the token hop
+		# process.
+		#
+		# @param tokenString [String] a string representation of the received token.
+		#
+		_onTokenReceived: ( tokenString ) =>
 			if @token?
 				return

@@ -553,7 +622,13 @@
 				@_computeTokenTargetPosition()
 			, @_tokenInfoTimeout)

-		_onTokenInfo: (tokenString, instantiate = true) =>
+		# Is called when we received information about a token. We will store this
+		# information and return our info on own token if we have one.
+		#
+		# @param tokenString [String] a string representation of the token.
+		# @param instantiate [Boolean] wether or not to respond.
+		#
+		_onTokenInfo: ( tokenString, instantiate = true ) =>
 			token = Token.deserialize(tokenString)
 			console.log "received info about token with id ", token.id
 			@addToken(token)
@@ -561,13 +636,20 @@
 			if @token? and instantiate
 				@emitTo(token.nodeId, 'token.info', @token.serialize(), false)

-		_onTokenDied: (tokenString) =>
+		# Is called when a token is killed. We will destroy any stored information
+		# on the token.
+		#
+		# @param tokenString [String] a string representation of the token.
+		#
+		_onTokenDied: ( tokenString ) =>
 			token = Token.deserialize(tokenString)
 			console.log "Received dead token", token.id
 			@removeToken(token)

-
-		_computeTokenTargetPosition: () ->
+		# Computes the desired position of the token from the positions of other tokens,
+		# and requests candidates closer to this desired position then ourselves.
+		#
+		_computeTokenTargetPosition: ( ) ->
 			unless @token?
 				return

@@ -589,8 +671,11 @@
 			else
 				@_selectTokenOwner()

-
-
+		# Is called when we receive a candidate request for a token. We will propose our child closest
+		# to the target position of the token.
+		#
+		# @param tokenString [String] a string representation of the token.
+		#
 		_onTokenRequestCandidate: ( tokenString ) =>
 			unless @isSuperNode
 				return
@@ -609,7 +694,12 @@
 			if closestChild?
 				@emitTo(token.nodeId, 'token.candidate', closestChild.id, distance)

-
+		# Is called when we receive a candidate for our token. We will store this
+		# information in the token.
+		#
+		# @param id [String] the string identifier of the candidate
+		# @param distance [Float] the distance to our token's target position
+		#
 		_onTokenCandidate: ( id, distance ) =>
 			unless @token?
 				return
@@ -620,6 +710,9 @@

 			@token.candidates.push(candidate)

+		# Selects the best candidate from all token candidates and passes our
+		# token to this candidate.
+		#
 		_selectTokenOwner: ( ) =>
 			unless @token?
 				return


Remove online/offline status in offline controller.

--- client/app/controllers/offline.coffee
+++ client/app/controllers/offline.coffee
@@ -33,16 +33,6 @@

   constructor: ->
     super
-
-    # Online/Offline status
-    ononline = @onNavigatorOnline
-    onoffline = @onNavigatorOffline
-    if navigator.onLine
-      @onNavigatorOnline()
-    else
-      @onNavigatorOffline()
-
-    # Application Cache
     @stopButton.hide()
     applicationCache.oncached = (event) =>
       @log "cached", event
@@ -87,13 +77,6 @@
       @setStatus "update ready"
       @stopButton.hide()

-  onNavigatorOffline: (event) =>
-    @log "offline"
-    @setStatus "offline"
-
-  onNavigatorOnline: (event) =>
-    @log "online"
-
   onStopButtonClicked: (event) =>
     applicationCache.abort()


Added: the enter key triggers the new task form

--- client/app/views/todolist_view.coffee
+++ client/app/views/todolist_view.coffee
@@ -48,6 +48,9 @@
         @newTaskFormInput.keyup (event) =>
             @hasUserTyped = true
             @newTaskButtonHandler()
+
+            keyCode = event.which | event.keyCode
+            @newTaskFormButton.click() if(keyCode is 13)

         @newTaskFormInput.focus (event) =>
             if !@hasUserTyped


Force reload on sign out regardless of account setting

--- src/smc-webapp/redux_account.coffee
+++ src/smc-webapp/redux_account.coffee
@@ -146,6 +146,7 @@
                     # Force a refresh, since otherwise there could be data
                     # left in the DOM, which could lead to a vulnerability
                     # or bleed into the next login somehow.
+                    $(window).off('beforeunload', redux.getActions('page').check_unload)
                     window.location.reload(false)

     push_state: (url) =>


fix https://github.com/sagemathinc/smc/issues/1775 -- "jupyter: notebook not always defined"

--- src/smc-webapp/editor_jupyter.coffee
+++ src/smc-webapp/editor_jupyter.coffee
@@ -871,6 +871,15 @@
         async.parallel [@init_syncstring, @init_dom, @ipynb_timestamp], (err) =>
             @element.find(".smc-jupyter-startup-message").hide()
             @element.find(".smc-jupyter-notebook-buttons").show()
+
+            if not err and not @dom?.nb?
+                # I read through all code and there is "no possible way" this can
+                # happen.  Except it does and would cause a traceback later:
+                # https://github.com/sagemathinc/smc/issues/1775
+                # This is just a lame hack, rather than really understanding the issue,
+                # as this code is going to be deleted in a week or two anyways.
+                err = "failed to properly initialize notebook"
+
             if err
                 @state = 'failed'
             else


Add debugging log to trace undefined ship

--- views/env-parts/data-resolver.coffee
+++ views/env-parts/data-resolver.coffee
@@ -68,6 +68,9 @@
         store.dispatch onGameResponse details
       catch e
         console.error e.stack
+      # used for debugging, can delete
+      if store.getState().info.ships.hasOwnProperty('undefined')
+        console.log 'Empty ship emerged!', details

       switch path
         when '/kcsapi/api_port/port'


Extend scope of shortcut for closing current window

The keyboard combo added in 3e57b3e wouldn't work if there wasn't a file
opened in the editing window.

--- .config/keymap.cson
+++ .config/keymap.cson
@@ -43,10 +43,10 @@
 	'cmd-|':        'bookmarks:toggle-bookmark'
 	'cmd-{':        'bookmarks:jump-to-previous-bookmark'
 	'cmd-}':        'bookmarks:jump-to-next-bookmark'
-	'shift-alt-W':  'window:close'

 '.platform-darwin':
 	'cmd-0':        'tree-view:toggle'
+	'shift-alt-W':  'window:close'

 'atom-text-editor':
 	'cmd-l':        'grammar-selector:show'


remove extraneous logging

remove extraneous logging

--- static/coffee/common/ticking.coffee
+++ static/coffee/common/ticking.coffee
@@ -241,7 +241,6 @@
     for nticks in divisions
       [min, max, interval] = heckbert_interval(data_low, data_high, nice_2_5_10, nticks)
       candidate_intervals.push([min, max, interval])
-    console.log candidate_intervals

     diff = 10000
     ind = -1
@@ -251,7 +250,6 @@
         if newdiff < diff
           diff = newdiff
           ind = i
-    console.log ind

     return candidate_intervals[ind]


return an error if trying to pop the last update from a pack

--- services/track-changes/app/coffee/MongoManager.coffee
+++ services/track-changes/app/coffee/MongoManager.coffee
@@ -10,7 +10,10 @@
 			.limit(1)
 			.toArray (error, compressedUpdates) ->
 				return callback(error) if error?
-				return callback null, null if compressedUpdates[0]?.pack? # cannot pop from a pack
+				if compressedUpdates[0]?.pack?
+					# cannot pop from a pack, throw error
+					error = new Error("last compressed update is a pack")
+					return callback error, null
 				return callback null, compressedUpdates[0] or null

 	deleteCompressedUpdate: (id, callback = (error) ->) ->


Add indentation auto detection to Atom

Add indentation auto detection to Atom

--- .atom/packages.cson
+++ .atom/packages.cson
@@ -1,4 +1,5 @@
 packages: [
+  "auto-detect-indentation"
   "autocomplete-python"
   "fold-functions"
   "language-ini"


Add setting for proposed "Show Wrap Icon" option

Fingers crossed this gets merged into the package's upstream.

See also: atom/find-and-replace#618

--- .config/config.cson
+++ .config/config.cson
@@ -49,3 +49,5 @@
   hex: {}
   docblockr: {}
   "toggle-quotes": {}
+  "find-and-replace":
+    showSearchWrapIcon: false


Disable full-width status-bar

Disable full-width status-bar

--- .config/config.cson
+++ .config/config.cson
@@ -65,7 +65,8 @@
     compactView: true
     fileIcons: false
   "spell-check": {}
-  "status-bar": {}
+  "status-bar":
+    fullWidth: false
   "symbols-tree-view":
     scrollAnimation: false
   tabs: {}