Require https on a next tick

Require https on a next tick

This require is heavy in node for some reason so only
require it on Windows since it is only used there and only
after a next tick so that it doesn't affect startup

--- src/browser/auto-update-manager.coffee
+++ src/browser/auto-update-manager.coffee
@@ -48,18 +48,24 @@
   # Windows doesn't have an auto-updater, so use this method to shim the events.
   checkForUpdatesShim: ->
     autoUpdater.emit 'checking-for-update'
-    request = https.get @feedUrl, (response) ->
-      if response.statusCode == 200
-        body = ""
-        response.on 'data', (chunk) -> body += chunk
-        response.on 'end', ->
-          {notes, name} = JSON.parse(body)
-          autoUpdater.emit 'update-downloaded', null, notes, name
-      else
-        autoUpdater.emit 'update-not-available'

-    request.on 'error', (error) ->
-      autoUpdater.emit 'error', null, error.message
+    # Do this in a next tick since requiring https can be slow the first time
+    # and this check shouldn't interfere with startup time.
+    process.nextTick =>
+      https = require 'https'
+
+      request = https.get @feedUrl, (response) ->
+        if response.statusCode == 200
+          body = ""
+          response.on 'data', (chunk) -> body += chunk
+          response.on 'end', ->
+            {notes, name} = JSON.parse(body)
+            autoUpdater.emit 'update-downloaded', null, notes, name
+        else
+          autoUpdater.emit 'update-not-available'
+
+      request.on 'error', (error) ->
+        autoUpdater.emit 'error', null, error.message

   emitUpdateAvailableEvent: (windows...) ->
     return unless @releaseVersion? and @releaseNotes


Add a comment

Add a comment

--- e2e-test/cross-browser-and-local-test.coffee
+++ e2e-test/cross-browser-and-local-test.coffee
@@ -19,6 +19,7 @@
   #{ os: 'Windows', os_version:'8', browser: 'IE', browser_version: '10.0' }
   #{ os: 'Windows', os_version:'7', browser: 'IE', browser_version: '9.0' }
   { os: 'Windows', os_version:'7', browser: 'IE', browser_version: '8.0' }
+  # Yosemite が良く失敗する！
   { os: 'OS X', os_version:'Yosemite', browser: 'Safari', browser_version: '8.0' }
   { os: 'OS X', os_version:'Mavericks', browser: 'Firefox', browser_version: '33.0' }
   { os: 'OS X', os_version:'Mavericks', browser: 'Chrome', browser_version: '38.0' }


troubleshooting

troubleshooting

--- scripts/trello.coffee
+++ scripts/trello.coffee
@@ -71,30 +71,31 @@
       .get() (err, res, body) ->
         if res.statusCode isnt 200
           return
-    jsdom = require 'jsdom'
-    jsdom.env(
-      html: msg.match[0]
-      scripts: [
-        'http://code.jquery.com/jquery-1.7.2.min.js'
-      ]
-      done: (errors, window) ->
-        unless errors
-          $ = window.$
-          title = $('title').text()
+        else
+          jsdom = require 'jsdom'
+          jsdom.env(
+            html: msg.match[0]
+            scripts: [
+              'http://code.jquery.com/jquery-1.7.2.min.js'
+            ]
+            done: (errors, window) ->
+              unless errors
+                $ = window.$
+                title = $('title').text()

-          if title
-            title = title
-            list = '510acfdfeca4af654e004e84'
-          else
-            title = url
-            list = '510be43bbfd03ea75700314b'
-          args =
-            name: title
-            idList: list
-            desc: url
+                if title
+                  title = title
+                  list = '510acfdfeca4af654e004e84'
+                else
+                  title = url
+                  list = '510be43bbfd03ea75700314b'
+                args =
+                  name: title
+                  idList: list
+                  desc: url

-          trelloIt url, args, msg
-    )
+                trelloIt url, args, msg
+          )

 trelloIt = (subject, args, msg) ->
   Trello = require("node-trello");


refactoring

refactoring

--- scripts/trello.coffee
+++ scripts/trello.coffee
@@ -94,6 +94,12 @@
         robot.messageRoom(config.notify_room, message)
   , check_interval_ms

+parse_max_cards = (list) ->
+  match = /\((\d+)\)$/.exec list.name
+  if match
+    max_cards = parseInt match[1], 10
+  max_cards
+
 board_info = (board, sep) ->
   sep ?= "\n -> "
   "Board: #{board.name}:#{sep}" + ("#{list.name}" for list in board.lists).join(sep)
@@ -104,13 +110,11 @@
       if err then return robot.send "Error: #{err}"
       messages = []
       for list in lists
-        match = /\((\d+)\)$/.exec list.name
-        if match
-          max_cards = parseInt match[1], 10
-          if list.cards.length > max_cards
-            messages.push format """hep, ça déborde dans \"#{list.name}\":
-                                    #{list.cards.length}/#{max_cards}
-                                    https://trello.com/board/#{board_id}"""
+        max_cards = parse_max_cards(list)
+        if list.cards.length > max_cards
+          messages.push format """hep, ça déborde dans \"#{list.name}\":
+                                  #{list.cards.length}/#{max_cards}
+                                  https://trello.com/board/#{board_id}"""
       cb messages

 module.exports = (robot) ->


storage: increase verbosity to better debug connection issues.

--- salvus/storage.coffee
+++ salvus/storage.coffee
@@ -218,7 +218,9 @@
                             dbg("zpool not running on #{opts.host} -- ready to go.")
                             cb()
                         else
-                            cb("zpool still being imported on #{opts.host} -- pid = #{o}")
+                            a = "zpool still being imported on #{opts.host} -- pid = #{o}"
+                            dbg(a)
+                            cb(a)

         (cb) ->
             dbg("mount filesystem")
@@ -385,7 +387,7 @@
                             dbg("project worked on #{host}")
                             host_used = host
                         else
-                            dbg("nonfatal error attempting to open on #{host}")
+                            dbg("nonfatal error attempting to open on #{host} -- #{err}")
                         c()

             async.mapSeries(hosts, f, cb)


Moving InfoDialog into admin.utils module

--- app/assets/javascripts/admin/utils/services/info_dialog.js.coffee
+++ app/assets/javascripts/admin/utils/services/info_dialog.js.coffee
@@ -1,4 +1,4 @@
-angular.module("admin.customers").factory 'InfoDialog', ($rootScope, $compile, $injector, $templateCache, DialogDefaults) ->
+angular.module("admin.utils").factory 'InfoDialog', ($rootScope, $compile, $injector, $templateCache, DialogDefaults) ->
   new class InfoDialog
     open: (type, message) ->
       scope = $rootScope.$new()


Rise of the Coffeeconomy

Rise of the Coffeeconomy

* Adds coffee balance tracking for every coffee you contribute and every coffee you consume
* Adds top lists to check other's balances
* Moves the brewing storage object into robot.brain to hopefully persist brews across robot restarts

--- scripts/coffee.coffee
+++ scripts/coffee.coffee
@@ -10,6 +10,10 @@
 #   hubot fresh pot - Announce that a pot is finished
 #   hubot fp - Alias for: hubot fresh pot
 #   hubot dibs - Reserve your spot in line for a current brew
+#   hubot :coffee: balance [user] - Check your coffee balance (defaults to you)
+#   hubot :coffee: top [n] - See the top coffee balances (defaults to 10)
+#   hubot :coffee: bottom [n] - See the bottom coffee balances (defaults to 10)
+

 # water = 3/16 * cups
@@ -44,6 +48,13 @@
 module.exports = (robot) ->
   statusEmoji = new StatusEmoji

+  brewing = {}
+  robot.brain.on 'loaded', =>
+    robot.brain.data.coffee ?= { brewing: {} }
+    brewing = robot.brain.data.coffee.brewing ?= {}
+
+  coffeeconomy = new Coffeeconomy robot
+
   robot.respond /coffee (\d+)$/i, (msg) ->
     cups = msg.match[1]

@@ -52,7 +63,31 @@

     msg.send "You need #{scoops} level scoops and #{water} L of water."

-  brewing = {}
+  robot.respond /(?::coffee:|☕) balance(?:\s+@?([^ ]+)\s*)?$/i, (msg) ->
+    clientName = msg.match[1] || msg.message.user.name
+    client = robot.brain.usersForRawFuzzyName(clientName)[0]
+
+    if not client
+      msg.send "Sorry, I don't know anyone named #{clientName}! #{statusEmoji.random('failure')}"
+      return
+
+    if not coffeeconomy.hasAccount client
+      msg.send "#{client.name} hasn't opened an account by `#{robot.alias}brewing` or `#{robot.alias}dibs`ing yet! #{statusEmoji.random('failure')}"
+      return
+    msg.send coffeeconomy.accountFormatter coffeeconomy.account client
+
+  robot.respond /(?::coffee:|☕) top(?:\s+(\d+))?$/i, (msg) ->
+    limit = parseInt msg.match[1]?.trim() || 10
+    count = 1
+    msg.send "These are the top :coffee: accounts:\n" +
+        (coffeeconomy.topListEntryFormatter count++, account for account in coffeeconomy.top limit).join("\n")
+
+  robot.respond /(?::coffee:|☕) bottom(?:\s+(\d+))?$/i, (msg) ->
+    limit = parseInt msg.match[1]?.trim() || 10
+    total = coffeeconomy.length()
+
+    msg.send "These are the bottom :coffee: accounts:\n" +
+        (coffeeconomy.topListEntryFormatter total--, account for account in coffeeconomy.bottom limit).join("\n")

   robot.respond /brewing\s*$/i, (msg) ->
     if isBrewing()
@@ -106,6 +141,8 @@
       dibs: [barista],
       messageMetadata: msg.envelope.message.metadata,

+    robot.brain.save()
+
     msg.send "@team Brew started by @#{brewing.barista}! #{statusEmoji.random('success')}\n" +
       "To grab a spot use: #{robot.alias}dibs\n" +
       "To end the brew use: #{robot.alias}fresh pot"
@@ -118,24 +155,130 @@

     claims = []
     for i in [1..dibsLimit]
-      claims.push if (dibber = brewing.dibs[i-1])
-        "@#{dibber}#{if dibber == brewing.barista then ' (barista)' else ''}"
+      dibber = brewing.dibs[i-1]
+
+      if dibber
+        client = robot.brain.userForName dibber
+
+        if dibber == brewing.barista
+          coffeeconomy.deposit client, dibsLimit-1
+        else
+          coffeeconomy.withdraw client, 1
+
+        account = coffeeconomy.account client
+
+        claims.push("**@#{dibber}** " +
+          "#{if dibber == brewing.barista then '_(barista)_ :chart_with_upwards_trend:' else ':chart_with_downwards_trend:'} " +
+          "#{coffeeconomy.balanceFormatter account.balance}")
       else
-        "Unclaimed!"
+        claims.push "_Unclaimed!_"

     threadMsg = threadedMsg(msg)
     threadMsg.send "#{if teamNotify then '@team: ' else ''}Fresh Pot!!! #{msg.random freshPots} " +
       ":coffee: Claims (valid until #{dateformat expiry, 'h:MM:ss tt'}):\n" +
-      (" • ##{(parseInt index)+1}: #{claim}" for index, claim of claims).join("\n")
-
-
-    if claims.length
+      ("#{(parseInt index)+1}. #{claim}" for index, claim of claims).join("\n")
+
+    if claims.length > 1
       setTimeout ->
         threadMsg.send "Claims have expired!"
       , dibsDuration*1000

     brewing = {}
+    robot.brain.save()

   freeSpots = ->
     remaining = dibsLimit - brewing.dibs.length
     "#{remaining} dib#{if remaining == 1 then '' else 's'}"
+
+
+class Coffeeconomy
+  constructor: (@robot, brain_ns='coffeeconomy') ->
+    @brain = @robot.brain
+    @storage = {}
+    @brain.on 'loaded', =>
+      @storage = @brain.data[brain_ns] ?= {}
+      @robot.logger.info "Coffeeconomy loaded. #{this.length()} accounts loaded."
+
+  accountNumber: (client) ->
+    if typeof client is 'string'
+      client = @brain.userForName client
+
+    return client.id if 'id' of client
+
+  client: (accountNumber) ->
+    @brain.userForId accountNumber
+
+  accountFormatter: (account) ->
+    "**#{@clientFormatter account.accountNumber}**: #{@balanceFormatter account.balance} (#{@accountDetailsFormatter account})"
+
+  shortDateFormatter: (date) ->
+    dateformat date, "mmm d yyyy h:MMtt"
+
+  longDateFormatter: (date) ->
+    dateformat date, "ddd mmm d yyyy 'at' h:MM:ss tt Z"
+
+  clientFormatter: (accountNumber) ->
+    "#{(@client accountNumber).name}"
+
+  balanceFormatter: (balance) ->
+    "#{balance} :coffee:#{unless balance == 1 then 's' else ''}"
+
+  accountDetailsFormatter: (account) ->
+    "_+#{account.totals.deposited}_ / _-#{account.totals.withdrawn}_, _Txs:_ #{account.transactions}, _Opened:_ #{@shortDateFormatter account.created}#{if account.updated then ", _Updated:_ #{@longDateFormatter account.updated}" else ""}"
+
+  topListEntryFormatter: (index, account) ->
+    "_#{index}._ **#{@clientFormatter account.accountNumber}** #{@balanceFormatter account.balance}"
+
+  deposit: (client, amount) ->
+    @transaction client, amount
+
+  withdraw: (client, amount) ->
+    @transaction client, (-1 * amount)
+
+  transaction: (client, amount) ->
+    account = @account client
+    account.balance += amount
+    if amount > 0
+      account.totals.deposited += amount
+    else
+      account.totals.withdrawn -= amount
+    account.transactions++
+    @saveAccount account
+    @robot.logger.info "#{amount} :coffee:s #{if amount > 0 then 'deposited to' else 'withdrawn from'} #{client.name} (#{client.id})"
+
+  balance: (client) ->
+    (@account client).balance
+
+  hasAccount: (client) ->
+    (@accountNumber client) of @storage
+
+  account: (client) ->
+    @storage[@accountNumber client] || @openAccount client
+
+  openAccount: (client) ->
+    accountNumber: @accountNumber client
+    balance: 0
+    transactions: 0
+    totals:
+      deposited: 0
+      withdrawn: 0
+    created: new Date()
+
+  saveAccount: (account) ->
+    account.updated = new Date()
+    @storage[account.accountNumber] = account
+    @brain.save()
+
+  orderedAccounts: (descending = true, limit = false) ->
+    accounts = ( { accountNumber: accountNumber, balance: account.balance } for accountNumber, account of @storage )
+    accounts.sort -> arguments[if descending then 1 else 0].balance - arguments[if descending then 0 else 1].balance
+    accounts[...(limit||accounts.length())]
+
+  top: (limit = 10) ->
+    @orderedAccounts true, limit
+
+  bottom: (limit = 10) ->
+    @orderedAccounts false, limit
+
+  length: ->
+    Object.getOwnPropertyNames(@storage).length


Add a test to the modal spec to ensure that the .mercury-modal and .mercury-modal-overlay elements get reused by all modals opened after the first one.

--- spec/javascripts/mercury/modal_spec.js.coffee
+++ spec/javascripts/mercury/modal_spec.js.coffee
@@ -131,6 +131,15 @@
       @modal.build()
       expect($('#modal_container .mercury-modal').length).toEqual(1)
       expect($('#modal_container .mercury-modal-overlay').length).toEqual(1)
+
+    it "reuses the .mercury-modal and .mercury-modal-overlay elements", ->
+      @modal2 = new Mercury.Modal('', {appendTo: fixture.el})
+      @modal.build()
+      @modal2.build()
+      expect($('.mercury-modal').length).toEqual(1)
+      expect($('.mercury-modal-overlay').length).toEqual(1)
+      expect(@modal2.element.get(0)).toEqual($('.mercury-modal').get(0))
+      expect(@modal2.overlay.get(0)).toEqual($('.mercury-modal-overlay').get(0))

   describe "observed events", ->


Add jschardet's MINIMUM_THRESHOLD

Add jschardet's MINIMUM_THRESHOLD

--- views/akashic-advanced-module.cjsx
+++ views/akashic-advanced-module.cjsx
@@ -3,7 +3,10 @@

 fs = require 'fs-extra'
 iconv = require 'iconv-lite'
+
 jschardet = require 'jschardet'
+jschardet.Constants.MINIMUM_THRESHOLD = 0.10
+
 path = require 'path-extra'

 remote = require 'remote'


fix(specs): Remove fdescribe

fix(specs): Remove fdescribe

--- spec/stores/draft-editing-session-spec.coffee
+++ spec/stores/draft-editing-session-spec.coffee
@@ -7,7 +7,7 @@
 _ = require 'underscore'

-fdescribe "DraftEditingSession Specs", ->
+describe "DraftEditingSession Specs", ->

   describe "DraftChangeSet", ->
     beforeEach ->


Check if aliases exists

Check if aliases exists

--- client/app/Applications/Environments.kdapplication/AppView.coffee
+++ client/app/Applications/Environments.kdapplication/AppView.coffee
@@ -17,7 +17,7 @@
         log "Looking for machine...", machine
         for _, domain of domainsContainer.dias
           log "Looking for domain...", domain
-          if machine.data.title in domain.data.aliases
+          if domain.data.aliases and machine.data.title in domain.data.aliases
             scene.connect \
               {dia : domain , joint : 'right', container: domainsContainer}, \
               {dia : machine, joint : 'left',  container: machinesContainer}


Use util instead of deprecated src.

Use util instead of deprecated src.

--- src/testrunner.coffee
+++ src/testrunner.coffee
@@ -1,4 +1,4 @@
-sys = require 'sys'
+util = require 'util'
 fs = require 'fs'
 path = require 'path'
 coffee = require 'coffee-script'
@@ -39,7 +39,7 @@
       helpers.logError error if error?
       # If we're testing brunch itself, we don't need to view the output.
       # TODO: move this to TerminalReporter.
-      stream = if global.jasmine then (->) else sys.print
+      stream = if global.jasmine then (->) else util.print

       jasmineEnv = window.jasmine.getEnv()
       jasmineEnv.reporter = new TerminalReporter


groups: fixed multiple invitations tabs

groups: fixed multiple invitations tabs

--- client/app/Applications/Groups.kdapplication/AppController.coffee
+++ client/app/Applications/Groups.kdapplication/AppController.coffee
@@ -609,12 +609,12 @@
     groupView.assureTab tabName, konstructor

   handleMembershipPolicyTabs:(policy, group, view)->
-    if policy.invitationsEnabled
+    if policy.invitationsEnabled and not view.tabView.getPaneByName 'Invitations'
       @showInvitationsTab group, view
     else
       @hideInvitationsTab view

-    if policy.approvalEnabled
+    if policy.approvalEnabled and not view.tabView.getPaneByName 'Approvals'
       @showApprovalTab group, view
     else
       @hideApprovalTab view


groups: image preview wip

groups: image preview wip

--- client/app/Applications/Groups.kdapplication/AppController.coffee
+++ client/app/Applications/Groups.kdapplication/AppController.coffee
@@ -133,9 +133,12 @@
             #
             partial : "

"
             callback :(formData)=>
-
               log 'image data, ready for S3 upload',formData

+              for image in formData.images
+                if image
+                  modal.$('.image-wrapper').css backgroundImage : "url(#{image.small})"
+                  modal.modalTabs.forms["General Settings"].addCustomData 'avatar', image.small
               # access data like this. formData.images may have undefined values for some reason
               # for image in formData.images
               #   if image
@@ -143,10 +146,11 @@
               #     modal.$('.image-wrapper').append("")
               #     modal.$('.image-wrapper').append("")

-              modal.destroy()
+              modal.modalTabs.forms['Avatar'].buttons['Use this image'].hideLoader()
+              # modal.destroy()

             buttons:
-              Save                :
+              "Use this image"    :
                 style             : "modal-clean-gray"
                 type              : "submit"
                 loader            :
@@ -157,6 +161,8 @@
                 loader            :
                   color           : "#ffffff"
                   diameter        : 16
+                callback          :=>
+                  modal.destroy()
             fields:
               "Drop Image here"              :
                 label             : "Avatar"
@@ -194,6 +200,9 @@
                         width   : 60
                         height  : 60
                       }
+                      # 'blur', {
+                      #   radius : 0
+                      # }
                     ]
                 }
           "General Settings":
@@ -259,12 +268,15 @@
                 ]
     , group

+    modal.modalTabs.forms["Avatar"].buttons["Use this image"].enable()
     modal.modalTabs.forms["Avatar"].inputs["Drop Image here"].on 'FileReadComplete', (stuff)->
       # modal.$('img.avatar-image').attr 'src' : stuff.file.data
       modal.$('div.image-wrapper').css backgroundImage : "url(#{stuff.file.data})"

   editPermissions:(group)->
+    log 'calling fetchPermissions'
     group.getData().fetchPermissions (err, permissionSet)->
+      log arguments
       if err
         new KDNotificationView title: err.message
       else


Fix typo

Fix typo

--- scripts/mumble.coffee
+++ scripts/mumble.coffee
@@ -50,7 +50,7 @@
 class MumbleBot
   constructor: (@robot) ->
     @name = 'DonkeyBot'
-    @timemap = {}
+    @timeMap = {}
     @options =
       key: fs.readFileSync './certs/private_key.pem'
       cert: fs.readFileSync './certs/cert.pem'


Reinstate the increment.

Reinstate the increment.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -107,7 +107,7 @@

             for username in people
                 points[username] ?= 0
-                points[username] = 0
+                points[username] += 5
                 save(robot)

             # Winners (and then recall) is equal to all users


Update Line Tool to extend ToolWithStroke.

--- src/tools/Line.coffee
+++ src/tools/Line.coffee
@@ -1,8 +1,8 @@
-{Tool} = require './base'
+{ToolWithStroke} = require './base'
 {createShape} = require '../core/shapes'

-module.exports = class Line extends Tool
+module.exports = class Line extends ToolWithStroke

   name: 'Line'
   iconName: 'line'


Remove HTML escaping to avoid minification errors


removed logging in trello script

removed logging in trello script

--- scripts/trello.coffee
+++ scripts/trello.coffee
@@ -65,7 +65,6 @@

 get_notifs = (options, onComplete, onError) ->
   connect (t) ->
-    console.info('retrieving notifications...')
     t.get "/1/members/me/notifications", options, (err, data) ->
       if data.length
         console.info('set last_notif_id=' + data[0].id)
@@ -75,16 +74,13 @@
   , onError

 init_checker = (robot) ->
-  console.info "set interval check to #{check_interval_ms}"
   checker = setInterval ->
-    console.info "checking for new notifications..."
     options =
       filter: Object.keys(filter_actions)
       read_filter: 'unread'
       limit: 10
       since: last_notif_id
     get_notifs options, (notifs) ->
-      console.info "#{notifs.length} new notifications received"
       for notif in notifs
         robot.messageRoom(config.notify_room, notif)
     , (err) ->


Documentation!

Documentation!

--- solr/src/table.coffee
+++ solr/src/table.coffee
@@ -1,4 +1,30 @@
-#
+# The table code exports three classes.
+#
+# TableHolder -- the main class, one per table implemented here.
+#
+# TableState  -- you subclass this and override methods and then supply it.
+#                  Contains callbacks whereby this code can record state
+#                  changes, eg column ordering, filtering, etc. and methods
+#                  to retrieve that info. Usually your implementation will
+#                  put these things in URLs, etc.
+#
+#                  There is a getter/setter method in the superclass for
+#                  each piece of state, which you should leave alone.
+#                  Subclasses just override set() which should pull info
+#                  from these getters and then call their persistence layer.
+#
+# TableSource -- you subclass this and override methods and then supply it.
+#                  Contains callbacks which the table code uses to retrieve
+#                  table contents. Typically this will trigger AJAX calls
+#                  or suchlike. When the data is returned, methods in this
+#                  class are called by you to populate the table.
+#
+#                  You only need implement two methods, columns, which
+#                  contains a list of columns and get to get the data.
+#                  A default implementation of columns is provided which is
+#                  usually fine and is initialised by a call to init in
+#                  your constructor.
+
 _clone_array = (a) -> $.extend(true,[],a)

 # XXX clear footer
@@ -235,6 +261,6 @@
 # XXX periodic headers

 window.TableSource = Source
-window.TableState = TableState
+window.TableState = TableState
 window.search_table = TableHolder


Fix typo

Fix typo

--- src/urlhandler.coffee
+++ src/urlhandler.coffee
@@ -6,7 +6,7 @@
         if not window?
             # prevents browserify from including this file
             return require('./urlhandlers/' + 'node.coffee').get(url, cb)
-        else xhr.supported()
+        else if xhr.supported()
             return xhr.get(url, cb)
         else
             return flash.get(url, cb)


http server: fix path to serve

http server: fix path to serve

--- lib/commands/server.coffee
+++ lib/commands/server.coffee
@@ -16,7 +16,7 @@
     kill -0 $pid && exit 1
     rm -f conf/server.pid
   fi
-  #{bin} -p 5680 -d -i conf/public >/dev/null 2>&1 &
+  #{bin} conf/public -p 5680 -d -i >/dev/null 2>&1 &
   echo $! > conf/server.pid
   """, (err) ->
     if err


Initialize navigation shortcuts on project's files page

--- app/assets/javascripts/dispatcher.js.coffee
+++ app/assets/javascripts/dispatcher.js.coffee
@@ -90,6 +90,7 @@
       when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new'
         new GroupAvatar()
       when 'projects:tree:show'
+        shortcut_handler = new ShortcutsNavigation()
         new TreeView()
       when 'projects:find_file:show'
         shortcut_handler = true


spec: asar: fs.open

spec: asar: fs.open

--- spec/asar-spec.coffee
+++ spec/asar-spec.coffee
@@ -214,3 +214,18 @@
         stats = fs.readdir p, (err, stats) ->
           assert.equal err.code, 'ENOENT'
           done()
+
+    describe 'fs.openSync', ->
+      it 'opens a normal/linked/under-linked-directory file', ->
+        for file in ['file1', 'link1', path.join('link2', 'file1')]
+          p = path.join fixtures, 'asar', 'a.asar', file
+          fd = fs.openSync p, 'r'
+          buffer = new Buffer(6)
+          fs.readSync fd, buffer, 0, 6, 0
+          assert.equal String(buffer), 'file1\n'
+          fs.closeSync fd
+
+      it 'throws ENOENT error when can not find file', ->
+        p = path.join fixtures, 'asar', 'a.asar', 'not-exist'
+        throws = -> fs.openSync p
+        assert.throws throws, /ENOENT/


Try to fix flaky spec

Try to fix flaky spec

--- spec/atom-spec.coffee
+++ spec/atom-spec.coffee
@@ -510,22 +510,20 @@
           # enabling of theme
           pack = atom.packages.enablePackage(packageName)

-          waitsFor (done) ->
-            atom.themes.once 'reloaded', done
-
-          runs ->
-            expect(atom.packages.getActivePackages()).toContain pack
+          waitsFor ->
+            pack in atom.packages.getActivePackages()
+
+          runs ->
             expect(atom.config.get('core.themes')).toContain packageName
             expect(atom.config.get('core.disabledPackages')).not.toContain packageName

             # disabling of theme
             pack = atom.packages.disablePackage(packageName)

-          waitsFor (done) ->
-            atom.themes.once 'reloaded', done
-
-          runs ->
-            expect(atom.packages.getActivePackages()).not.toContain(pack)
+          waitsFor ->
+            not (pack in atom.packages.getActivePackages())
+
+          runs ->
             expect(atom.config.get('core.themes')).not.toContain packageName
             expect(atom.config.get('core.themes')).not.toContain packageName
             expect(atom.config.get('core.disabledPackages')).not.toContain packageName


Only backbone on www links.

Only backbone on www links.

--- app/assets/javascripts/views/root.js.coffee
+++ app/assets/javascripts/views/root.js.coffee
@@ -27,7 +27,7 @@
     $(@el).bind 'clouds:initialize', => @renderSidebar()

     $('a').live 'click', (e) ->
-      if (@hostname == document.location.hostname) && (@pathname.match(/^\/(auth|logout|login|register|password|oauth\/.*/ig) == null)
+      if (@hostname.match(document.location.hostname) != null) && (@pathname.match(/^\/(auth|logout|login|register|password|oauth\/.*/ig) == null) && location.hostname.split('.').shift() == "www"
         Backbone.history.navigate(@pathname,true)
         e.preventDefault()
         false


Update locations for cson-parser 1.0.9

Update locations for cson-parser 1.0.9

--- spec/cson-spec.coffee
+++ spec/cson-spec.coffee
@@ -277,9 +277,7 @@
       expect(parseError.path).toBe errorPath
       expect(parseError.filename).toBe errorPath
       expect(parseError.location.first_line).toBe 0
-      expect(parseError.location.first_column).toBe 4
-      expect(parseError.location.last_line).toBe 0
-      expect(parseError.location.last_column).toBe 4
+      expect(parseError.location.first_column).toBe 3

     it "throws errors for invalid .json files", ->
       errorPath = path.join(__dirname, 'fixtures', 'syntax-error.json')
@@ -350,9 +348,7 @@
         expect(parseError.path).toBe errorPath
         expect(parseError.filename).toBe errorPath
         expect(parseError.location.first_line).toBe 0
-        expect(parseError.location.first_column).toBe 4
-        expect(parseError.location.last_line).toBe 0
-        expect(parseError.location.last_column).toBe 4
+        expect(parseError.location.first_column).toBe 3

       readFile(errorPath, callback)


:art:

:art:

--- spec/link-spec.coffee
+++ spec/link-spec.coffee
@@ -25,9 +25,8 @@
       runs ->
         expect(fs.existsSync(path.join(atomHome, 'packages', path.basename(packageToLink)))).toBeTruthy()
         expect(fs.realpathSync(path.join(atomHome, 'packages', path.basename(packageToLink)))).toBe fs.realpathSync(packageToLink)
+
         callback.reset()
-
-      runs ->
         apm.run(['unlink'], callback)

       waitsFor 'waiting for unlink to complete', ->


Change tests to rely on state rather than on the implementation

--- spec/tabs-spec.coffee
+++ spec/tabs-spec.coffee
@@ -265,17 +265,20 @@
       # immediately afterward unless propagation of the mousedown event is
       # stopped. But stopping propagation of the mousedown event prevents the
       # dragstart event from occurring.
-      waitsForPromise -> new Promise setImmediate
+      expect(pane.getActiveItem()).not.toBe(pane.getItems()[0])
+      waitsFor ->
+        pane.getActiveItem() is pane.getItems()[0]
+
       runs ->
-        expect(pane.getActiveItem()).toBe pane.getItems()[0]
         expect(event.preventDefault).not.toHaveBeenCalled() # allows dragging
-
         event = triggerMouseEvent('mousedown', tabBar.tabAtIndex(2).element, which: 1)
-      waitsForPromise -> new Promise setImmediate
+        expect(pane.getActiveItem()).not.toBe(pane.getItems()[2])
+
+      waitsFor ->
+        pane.getActiveItem() is pane.getItems()[2]
+
       runs ->
-        expect(pane.getActiveItem()).toBe pane.getItems()[2]
         expect(event.preventDefault).not.toHaveBeenCalled() # allows dragging
-
         expect(pane.activate.callCount).toBe 2

     it "closes the tab when middle clicked", ->


adds find method to card view

adds find method to card view

--- src/containers/card_view.coffee
+++ src/containers/card_view.coffee
@@ -45,8 +45,7 @@
     nextIndex = if @activeCard < @components.length - 1 then @activeCard + 1 else 0
     @activate( nextIndex )

-  hide_components: ()->
-    _( @components ).pluck('container')
+  findCard: (name)-> @findComponentByName(name,true)

   activate: (index)->
     return if index is @activeCard


Updating Theoricus class

Updating Theoricus class

--- www/src/theoricus/theoricus.coffee
+++ www/src/theoricus/theoricus.coffee
@@ -48,9 +48,10 @@
   ###*
     Theoricus constructor, must to be invoked by the application with a `super`
     call.
-    @method constructor
-    @param @Settings {Object} App Settings
-    @param @Routes {Object} App Routes
+    @class Theoricus
+    @constructor
+    @param Settings {Object} App Settings
+    @param Routes {Object} App Routes
   ###
   constructor:( @Settings, @Routes )->
     @config  = new Config @, @Settings


Remove dead code

Remove dead code

--- src/scripts/modules/media/tabs/tabs.coffee
+++ src/scripts/modules/media/tabs/tabs.coffee
@@ -1,6 +1,5 @@
 define (require) ->
   $ = require('jquery')
-  content = require('cs!models/content')
   BaseView = require('cs!helpers/backbone/views/base')
   ContentsView = require('cs!./contents/contents')
   MetadataView = require('cs!./metadata/metadata')


:memo: Add documentation to MinimapElement

--- lib/minimap-element.coffee
+++ lib/minimap-element.coffee
@@ -6,7 +6,18 @@

 MinimapQuickSettingsElement = null

-# Public:
+# Public: The {MinimapElement} is the view meant to render a {Minimap} instance
+# in the DOM.
+#
+# You can retrieve the {MinimapElement} associated to a {Minimap} as
+# demonstrated below:
+#
+# ```coffee
+# minimapElement = atom.views.getView(minimap)
+# ```
+#
+# Note that most interactions with the Minimap package is done through the
+# {Minimap} model so you should never have to access {MinimapElement} instances.
 class MinimapElement extends HTMLElement
   DOMStylesReader.includeInto(this)
   CanvasDrawer.includeInto(this)
@@ -24,6 +35,7 @@
   #    ##     ## ##     ## ##     ## ##   ##  ##    ##
   #    ##     ##  #######   #######  ##    ##  ######

+  # Internal: DOM callback invoked when a new {MinimapElement} is created.
   createdCallback: ->
     @subscriptions = new CompositeDisposable
     @initializeContent()
@@ -65,15 +77,17 @@
       'minimap.useHardwareAcceleration': (@useHardwareAcceleration) =>
         @requestUpdate() if @attached

+  # Internal: DOM callback invoked when a new {MinimapElement} is attached
+  # to the DOM.
   attachedCallback: ->
     @subscriptions.add atom.views.pollDocument => @pollDOM()
     @measureHeightAndWidth()
     @attached = true

+  # Internal: DOM callback invoked when a new {MinimapElement} is detached
+  # from the DOM.
   detachedCallback: ->
     @attached = false
-
-  attributeChangedCallback: (attrName, oldValue, newValue) ->

   #       ###    ######## ########    ###     ######  ##     ##
   #      ## ##      ##       ##      ## ##   ##    ## ##     ##
@@ -83,31 +97,45 @@
   #    ##     ##    ##       ##    ##     ## ##    ## ##     ##
   #    ##     ##    ##       ##    ##     ##  ######  ##     ##

+  # Returns `true` if the {MinimapElement} is currently visible on screen.
+  #
+  # Returns a {Boolean}.
   isVisible: -> @offsetWidth > 0 or @offsetHeight > 0

+  # Attaches the {MinimapElement} to the DOM.
+  #
+  # The position at which the element is attached is defined by the
+  # `displayMinimapOnLeft` setting.
   attach: ->
     return if @attached
     @swapMinimapPosition()
     @attached = true

+  # Attaches the {MinimapElement} to the left of the target {TextEditorElement}.
   attachToLeft: ->
     root = @getTextEditorElementRoot()
     root.insertBefore(this, root.children[0])

+  # Attaches the {MinimapElement} to the right of the target
+  # {TextEditorElement}.
   attachToRight: ->
     @getTextEditorElementRoot().appendChild(this)

+  # Swaps the {MinimapElement} position based on the value of the
+  # `displayMinimapOnLeft` setting.
   swapMinimapPosition: ->
     if @displayMinimapOnLeft
       @attachToLeft()
     else
       @attachToRight()

+  # Detaches the {MinimapElement} from the DOM.
   detach: ->
     return unless @attached
     return unless @parentNode?
     @parentNode.removeChild(this)

+  # Destroys this {MinimapElement}.
   destroy: ->
     @subscriptions.dispose()
     @detach()
@@ -120,6 +148,8 @@
   #    ##    ## ##     ## ##   ###    ##    ##       ##   ###    ##
   #     ######   #######  ##    ##    ##    ######## ##    ##    ##

+  # Internal: Creates the content of the {MinimapElement} and attaches the
+  # mouse control event listeners.
   initializeContent: ->
     @initializeCanvas()

@@ -148,15 +178,21 @@
       @canvas.removeEventListener 'mousedown', canvasMousedown
       @visibleArea.removeEventListener 'mousedown', visibleAreaMousedown

+  # Initializes the scroll indicator div when the `minimapScrollIndicator`
+  # settings is enabled.
   initializeScrollIndicator: ->
     @scrollIndicator = document.createElement('div')
     @scrollIndicator.classList.add 'minimap-scroll-indicator'
     @controls.appendChild(@scrollIndicator)

+  # Disposes the scroll indicator div when the `minimapScrollIndicator`
+  # settings is disabled.
   disposeScrollIndicator: ->
     @controls.removeChild(@scrollIndicator)
     @scrollIndicator = undefined

+  # Initializes the quick settings openener div when the
+  # `displayPluginsControls` setting is enabled.
   initializeOpenQuickSettings: ->
     return if @openQuickSettings?

@@ -187,22 +223,39 @@
           else
             @quickSettingsElement.style.left = (left - @quickSettingsElement.clientWidth) + 'px'

+  # Disposes the quick settings openener div when the
+  # `displayPluginsControls` setting is disabled.
   disposeOpenQuickSettings: ->
     return unless @openQuickSettings?
     @controls.removeChild(@openQuickSettings)
     @openQuickSettingSubscription.dispose()
     @openQuickSettings = undefined

+  # Returns the target {TextEditor} of the {Minimap}.
+  #
+  # Returns a {TextEditor}.
   getTextEditor: -> @minimap.getTextEditor()

+  # Returns the {TextEditorElement} for the {Minimap}'s {TextEditor}.
+  #
+  # Returns a {TextEditorElement}.
   getTextEditorElement: ->
     @editorElement ?= atom.views.getView(@getTextEditor())

+  # Internal: Returns the root of the {TextEditorElement} content.
+  # This method is mostly used to ensure compatibility with the `shadowDom`
+  # setting.
+  #
+  # Returns an {HTMLElement}.
   getTextEditorElementRoot: ->
     editorElement = @getTextEditorElement()

     editorElement.shadowRoot ? editorElement

+  # Internal: Returns the root where to inject the dummy node used to read
+  # DOM styles.
+  #
+  # Returns an {HTMLElement}.
   getDummyDOMRoot: (shadowRoot) ->
     if shadowRoot
       @getTextEditorElementRoot()
@@ -217,8 +270,14 @@
   #    ##     ## ##     ## ##     ## ##       ##
   #    ##     ##  #######  ########  ######## ########

+  # Returns the {Minimap} for which this {MinimapElement} was created.
+  #
+  # Returns a {Minimap}.
   getModel: -> @minimap

+  # Defines the {Minimap} model for this {MinimapElement} instance.
+  #
+  # minimap - The {Minimap} model for this instance.
   setModel: (@minimap) ->
     @subscriptions.add @minimap.onDidChangeScrollTop => @requestUpdate()
     @subscriptions.add @minimap.onDidChangeScrollLeft => @requestUpdate()
@@ -239,6 +298,7 @@
   #    ##     ## ##        ##     ## ##     ##    ##    ##
   #     #######  ##        ########  ##     ##    ##    ########

+  # Internal: Requests an update to be performed on the next frame.
   requestUpdate: ->
     return if @frameRequested

@@ -247,11 +307,14 @@
       @update()
       @frameRequested = false

+  # Internal: Requests an update to be performed on the next frame that will
+  # completely redraw the minimap.
   requestForcedUpdate: ->
     @offscreenFirstRow = null
     @offscreenLastRow = null
     @requestUpdate()

+  # Internal: Performs the actual {MinimapElement} update.
   update: ->
     return unless @attached and @isVisible() and not @minimap.isDestroyed()

@@ -293,16 +356,24 @@

     @updateCanvas()

+  # Defines whether to render the code highlights or not.
+  #
+  # displayCodeHighlights - A {Boolean}.
   setDisplayCodeHighlights: (@displayCodeHighlights) ->
     @requestForcedUpdate() if @attached

+  # Internal: Polling callback used to detect visibility and size changes.
   pollDOM: ->
     if @isVisible()
       @requestForcedUpdate() unless !@wasVisible

       @measureHeightAndWidth(false)

-
+  # Internal: A method that checks for visibility changes in the
+  # {MinimapElement}. The method returns `true` when the visibility changed
+  # from visible to hidden or from hidden to visible.
+  #
+  # Returns a {Boolean}.
   checkForVisibilityChange: ->
     if @isVisible()
       if @wasVisible
@@ -316,6 +387,11 @@
       else
         @wasVisible = false

+  # Internal: A method used to measure the size of the {MinimapElement} and
+  # update internal components based on the new size.
+  #
+  # forceUpdate - A {Boolean} that forces the update even when no changes were
+  #               detected.
   measureHeightAndWidth: (forceUpdate=true) ->
     wasResized = @width isnt @clientWidth or @height isnt @clientHeight
     visibilityChanged = @checkForVisibilityChange()
@@ -355,10 +431,18 @@
   #    ##         ## ##   ##       ##   ###    ##    ##    ##
   #    ########    ###    ######## ##    ##    ##     ######

+  # Internal: Helper method to register config observers.
+  #
+  # config - An {Object} mapping the config name to observe with the listener
+  #          {Function} to call when the setting was changed.
   observeConfig: (configs={}) ->
     for config, callback of configs
       @subscriptions.add atom.config.observe config, callback

+  # Internal: Callback triggered when the mouse is pressed on the
+  # {MinimapElement} canvas.
+  #
+  # event - The {Event} object.
   mousePressedOverCanvas: ({which, pageY, target}) ->
     return if which isnt 1

@@ -378,6 +462,10 @@
     else
       textEditor.setScrollTop(scrollTop)

+  # Internal: A method that relays the `mousewheel` events received by
+  # the {MinimapElement} to the {TextEditorElement}.
+  #
+  # e - The {Event} object.
   relayMousewheelEvent: (e) =>
     editorElement = atom.views.getView(@minimap.textEditor)

@@ -391,6 +479,10 @@
   #    ##     ## ##   ##   ##     ##
   #    ########   ####  ## ########

+  # Internal: A method triggered when the mouse is pressed over the visible
+  # area that starts the dragging gesture.
+  #
+  # event - The {Event} object.
   startDrag: ({which, pageY}) ->
     return if which isnt 1
     {top} = @visibleArea.getBoundingClientRect()
@@ -412,6 +504,14 @@
       document.body.removeEventListener('mouseup', mouseupHandler)
       document.body.removeEventListener('mouseout', mouseupHandler)

+  # Internal: The method called during the drag gesture.
+  #
+  # e - The {Event} object.
+  # initial - An {Object} with the data from the original data from the drag
+  #           start event. The object holds the following properties:
+  #           dragOffset - The mouse offset {Number} within the visible area.
+  #           offsetTop - The {MinimapElement} offset at the moment of the
+  #                       drag start.
   drag: (e, initial) ->
     return if e.which isnt 1
     y = e.pageY - initial.offsetTop - initial.dragOffset
@@ -420,6 +520,14 @@

     @minimap.textEditor.setScrollTop(ratio * @minimap.getTextEditorMaxScrollTop())

+  # Internal: The method that ends the drag gesture.
+  #
+  # e - The {Event} object.
+  # initial - An {Object} with the data from the original data from the drag
+  #           start event. The object holds the following properties:
+  #           dragOffset - The mouse offset {Number} within the visible area.
+  #           offsetTop - The {MinimapElement} offset at the moment of the
+  #                       drag start.
   endDrag: (e, initial) ->
     @dragSubscription.dispose()

@@ -431,6 +539,11 @@
   #    ##    ## ##    ## ##    ##
   #     ######   ######   ######

+  # Internal: Applies the passed-in styles properties to the specified element
+  #
+  # element - The {HTMLElement} onto which applies the styles.
+  # styles - An {Object} where the keys are the properties name and the values
+  #          are the CSS values for theses properties.
   applyStyles: (element, styles) ->
     cssText = ''

@@ -439,20 +552,47 @@

     element.style.cssText = cssText

+  # Returns a {String} with a CSS translation tranform value.
+  #
+  # x - The translation {Number} on the x axis.
+  # y - The translation {Number} on the y axis.
+  #
+  # Returns a {String}.
   makeTranslate: (x=0,y=0) ->
     if @useHardwareAcceleration
       "translate3d(#{x}px, #{y}px, 0)"
     else
       "translate(#{x}px, #{y}px)"

+  # Returns a {String} with a CSS scale tranform value.
+  #
+  # x - The scaling {Number} on the x axis.
+  # y - The scaling {Number} on the y axis.
+  #
+  # Returns a {String}.
   makeScale: (x=0,y=x) ->
     if @useHardwareAcceleration
       "scale3d(#{x}, #{y}, 1)"
     else
       "scale(#{x}, #{y})"

+  # Internal: A method that return the current time as a {Date}.
+  #
+  # That method exist so that we can mock it in tests.
+  #
+  # Returns a {Date}.
   getTime: -> new Date()

+  # Internal: A method that mimic the jQuery `animate` method and used to
+  # animate the scroll when clicking on the {MinimapElement} canvas.
+  #
+  #  properties - An {Object} with the following properties:
+  #               from - The starting {Number} value.
+  #               to - The ending {Number} value.
+  #               duration - The duration {Number} of the animation.
+  #               step - A {Function} to call on each step of the animation.
+  #                      The method will receive a {Number} between `0` and `1`
+  #                      as argument.
   animate: ({from, to, duration, step}) ->
     start = @getTime()

@@ -484,6 +624,10 @@

 module.exports = MinimapElement = document.registerElement 'atom-text-editor-minimap', prototype: MinimapElement.prototype

+# Public: The method that registers the {MinimapElement} factory in the
+# `atom.views` registry with the passed-in model.
+#
+# model - The model class to registers the factory with.
 MinimapElement.registerViewProvider = ->
   atom.views.addViewProvider require('./minimap'), (model) ->
     element = new MinimapElement


Use correct variable

Use correct variable

--- lib/pdf-editor-view.coffee
+++ lib/pdf-editor-view.coffee
@@ -151,10 +151,10 @@
         synctexPath = atom.config.get('pdf-view.syncTeXPath')
         clickspec = page + ":" + x + ":" + y + ":"
         if synctexPath
-          clickspec += @filename
+          clickspec += @filePath
           execFile synctexPath, ["edit", "-o", clickspec], callback
         else
-          clickspec += if @filename.includes(' ') then '"' + @filePath + '"' else @filePath
+          clickspec += if @filePath.includes(' ') then '"' + @filePath + '"' else @filePath
           cmd = "synctex edit -o " + clickspec
           exec cmd, callback


Document the required import for UI variables.

--- lib/styleguide-view.coffee
+++ lib/styleguide-view.coffee
@@ -14,7 +14,7 @@
         @p 'This exercises all UI components and acts as a styleguide.'

       @exampleSection 'variables', 'Variables', ->
-        @p => @raw '''Use these UI variables in your package's stylesheets. They are set by UI themes and therefore your package will match the overall look.'''
+        @p => @raw '''Use these UI variables in your package's stylesheets. They are set by UI themes and therefore your package will match the overall look. Make sure to @import 'ui-variables' in your stylesheets to use these variables.'''

         @div class: 'variables-group', =>
           @h2 'Text colors'


fixed endpoint

fixed endpoint

--- src/api/statsd.coffee
+++ src/api/statsd.coffee
@@ -81,7 +81,7 @@
     for (i = 0; i < status_array.length; i++) {
       path = render_url + ".statuses." + status_array[i] + ".count,'1week'))&from=-1weeks&format=json";
       results[status_array[i]] = yield exports.fetchData(path);
-      statuses[status_array[i]] =  'data' in results[status_array[i]] ? results[status_array[i]].data[0][0] + results[status_array[i]].data[1][0] : 0,
+      statuses[status_array[i]] =  'data' in results[status_array[i]] ? results[status_array[i]].data[0][0] + results[status_array[i]].data[1][0] : 0
     }

     data.push({


:lipstick:

:lipstick:

--- src/app/config.coffee
+++ src/app/config.coffee
@@ -33,8 +33,6 @@
   load: ->
     @loadUserConfig()
     @requireUserInitScript()
-
-

   loadUserConfig: ->
     if fs.exists(@configFilePath)


Add Collection#indexOf

Add Collection#indexOf

--- src/collection.coffee
+++ src/collection.coffee
@@ -34,7 +34,14 @@
   # @param [Object] item the item to check for in the collection
   # @return [Boolean] whether or not the item is in the collection
   include: (item) ->
-    _.indexOf(@__collection, item) >= 0
+    @indexOf(item) >= 0
+
+  # Get the index of the specified item in the collection
+  #
+  # @param [Object] item the item to get the index for in the collection
+  # @return [Integer] the index of the item in the collection, or -1 if it is not in the collection
+  indexOf: (item) ->
+    _.indexOf(@__collection, item)

   # Gets the item at the index of the collection
   #


Fix #125

Fix #125

--- lib/completion-backend/buffer-info.coffee
+++ lib/completion-backend/buffer-info.coffee
@@ -32,9 +32,17 @@
         return new Disposable ->
       @buffer.onDidSave callback

+    parse: ->
+      try
+        parseHsModuleImportsSync(@buffer.getText())
+      catch error
+        console.error error
+        name: undefined
+        imports: []
+
     getImports: =>
       return [] unless @buffer?
-      modules = parseHsModuleImportsSync(@buffer.getText()).imports.map (imp) ->
+      modules = @parse().imports.map (imp) ->
         getName = (thing) ->
           switch
             when thing.Ident?
@@ -75,4 +83,4 @@

     getModuleName: =>
       return unless @buffer?
-      parseHsModuleImportsSync(@buffer.getText()).name
+      @parse().name


fix https://github.com/sagemathinc/cocalc/issues/2548 -- in jupyter, clicking in code cell shifts view away from cursor click

--- src/smc-webapp/jupyter/codemirror-editor.cjsx
+++ src/smc-webapp/jupyter/codemirror-editor.cjsx
@@ -296,9 +296,6 @@
                 tab_key     : @tab_key
             @props.actions.register_input_editor(@props.id, editor)

-        if @props.is_focused
-            @cm.focus()
-
         if @props.click_coords?
             # editor clicked on, so restore cursor to that position
             @cm.setCursor(@cm.coordsChar(@props.click_coords, 'window'))
@@ -313,7 +310,10 @@
         # Note that this also avoids a significant disturbing flicker delay
         # even for non-raw cells.  This obviously probably slows down initial
         # load or switch to of the page, unfortunately.  Such is life.
-        setTimeout((=>@cm?.refresh()),1)
+        # CRITICAL: Also do the focus only after the refresh, or when
+        # switching from static to non-static, whole page gets badly
+        # repositioned (see https://github.com/sagemathinc/cocalc/issues/2548).
+        setTimeout((=>@cm?.refresh(); if @props.is_focused then @cm?.focus()),1)

     componentDidMount: ->
         @init_codemirror(@props.options, @props.value)


even more

even more

--- src/smc-project/jupyter/jupyter.coffee
+++ src/smc-project/jupyter/jupyter.coffee
@@ -230,8 +230,8 @@
             clearInterval(@_usage_interval)
         locals =
             last_usage : {cpu:0, memory:0}
-            thresh     : 0.2  # report any change of at least thresh percent (we always report cpu dropping to 0)
-            interval   : 8000  # frequently (~8s) should be OK, since it just reads /proc filesystem
+            thresh     : 0.25  # report any change of at least thresh percent (we always report cpu dropping to 0)
+            interval   : 10000  # frequently (~8s) should be OK, since it just reads /proc filesystem
         @emit('usage', locals.last_usage)
         dbg = @dbg("usage_monitor")
         update_usage = =>


webapp/proj upgrade: different approach, and deal with "negative" remainder

--- src/smc-webapp/r_upgrade_adjustor.cjsx
+++ src/smc-webapp/r_upgrade_adjustor.cjsx
@@ -65,31 +65,48 @@
         # This function is quite confusing and tricky.
         # It combines the remaining upgrades of the user with the already applied ones by the same user.
         # Then it limits the applyable upgrades by what's still possible to apply until the maximum is reached.
+        # My mental model:
+        #
+        #   0                                total          maximum
+        #   |<-------------------------------->|                |
+        #   |<----->|<------------------------>|<-------------->|
+        #   | admin |  all upgrades by users   | proj remainder |
+        #   | +     |<------------>|<--------->|<--------->|    |
+        #   | free  |  other users | this user | remaining |    |
+        #   |       |              |           | this user |    |
+        #   |       |              |<--------------------->|    |
+        #   |       |              |  limit for this user  | <= | max
+        #
+        #   admin/free: could be 0
+        #   all upgrades by users is total_project_quotas
+        #   remainder: >=0, usually, but if there are already too many upgrades it is negative!
+        #   this user: upgrades_you_applied_to_this_project. this is >= 0!
+        #   limit for this user: is capped by the user's overall quotas AND the quota maximum

         # NOTE : all units are ^ly 'internal' instead of display, e.g. seconds instead of hours
         quota_params = @props.quota_params
         # how much upgrade you have used between all projects
-        used_upgrades = @props.upgrades_you_applied_to_all_projects
+        user_upgrades = @props.upgrades_you_applied_to_all_projects
         # how much upgrade you currently use on this one project
-        current = @props.upgrades_you_applied_to_this_project
+        user_current = @props.upgrades_you_applied_to_this_project
+        # all currently applied upgrades to this project
+        total_upgrades = @props.total_project_quotas
         # how much unused upgrade you have remaining
-        remaining = misc.map_diff(@props.upgrades_you_can_use, used_upgrades)
-        # maximums you can use, including the upgrades already on this project
-        limits = misc.map_sum(current, remaining)
-        # additionally, the limits are capped by the maximum per project
-        maximum = require('smc-util/schema').PROJECT_UPGRADES.max_per_project
-        limits = misc.map_limit(limits, maximum)
-        # all currently applied upgrades to this project
-        project_upgrades = @props.total_project_quotas
-        remainder_upgrades = misc.map_diff(maximum, project_upgrades)
-        limits = misc.map_limit(limits, remainder_upgrades)
-        # if there are already more upgrades than the limit, limits shouldn't be negative!
-        limits = misc.map_max(limits, 0)
+        user_remaining = misc.map_diff(@props.upgrades_you_can_use, user_upgrades)
+        # the overall limits are capped by the maximum per project
+        proj_maximum = require('smc-util/schema').PROJECT_UPGRADES.max_per_project
+        # and they're also limited by what everyone has already applied
+        proj_remainder = misc.map_diff(proj_maximum, total_upgrades)
+        # note: if quota already exeeds, proj_remainder might have negative values -- don't cap at 0
+        # the overall limit for the user is capped by what's left for the project
+        limits = misc.map_limit(user_remaining, proj_remainder)
+        # and finally, we add up what a user can add (with the maybe negative remainder) and cap at 0
+        user_limits = misc.map_max(misc.map_sum(limits, user_current), 0)
         return
-            limits    : limits
-            remaining : remaining
-            current   : current
-            totals    : project_upgrades
+            limits    : user_limits
+            remaining : user_remaining
+            current   : user_current
+            totals    : total_upgrades

     clear_upgrades: ->
         @set_upgrades('min')
@@ -144,8 +161,7 @@
             show_remaining = remaining + current - val
             show_remaining = Math.max(show_remaining, 0)

-            # compensate val by current set upgrade
-            if not @is_upgrade_input_valid(Math.max(val - current, 0), limit)
+            if not @is_upgrade_input_valid(Math.max(val, 0), limit)
                 label =
Uncheck this: you do not have enough upgrades or exceed the limit

             else
                 label = if val == 0 then 'Enable' else 'Enabled'
@@ -189,8 +205,7 @@

             val = @state["upgrade_#{name}"]
             if misc.parse_number_input(val)?
-            # compensate val by current set upgrade
-                if not @is_upgrade_input_valid(Math.max(val - current, 0), limit)
+                if not @is_upgrade_input_valid(Math.max(val, 0), limit)
                     bs_style = 'error'
                     label =
Value too high: not enough upgrades or exceeding limit

                 else
@@ -206,7 +221,7 @@

             unit = misc.plural(show_remaining, display_unit)
             if limit < remaining
-                remaining_note = You have {remaining_all} unallocated {unit}
(you may allocate up to {limit + current} {unit} here)
+                remaining_note = You have {remaining_all} unallocated {unit}
(you may allocate up to {limit} {unit} here)

             else
                 remaining_note = You have {remaining_all} unallocated {unit}


Hide dropdown on toggle

Hide dropdown on toggle

--- src/controller.coffee
+++ src/controller.coffee
@@ -3132,6 +3132,8 @@

   Editor::performMeltAnimation = (fadeTime = 500, translateTime = 1000, cb = ->) ->
     if @currentlyUsingBlocks and not @currentlyAnimating
+      @hideDropdown()
+
       @fireEvent 'statechange', [false]

       @setAceValue @getValue()
@@ -3883,8 +3885,10 @@
         @highlightCanvas.opacity = 1

       @resizeBlockMode(); @redrawMain()
-
+
     else
+      @hideDropdown()
+
       paletteVisibleInNewState = @paletteEnabled and @showPaletteInTextMode

       oldScrollTop = @aceEditor.session.getScrollTop()


Fix removing attachments

Fix removing attachments

--- src/trix/controllers/editor_controller.coffee
+++ src/trix/controllers/editor_controller.coffee
@@ -268,6 +268,7 @@
   removeAttachment: (attachment) ->
     @editor.recordUndoEntry("Delete Attachment")
     @composition.removeAttachment(attachment)
+    @render()

   recordFormattingUndoEntry: ->
     locationRange = @selectionManager.getLocationRange()


more failing test

more failing test

--- spec/BaconSpec.coffee
+++ spec/BaconSpec.coffee
@@ -3346,8 +3346,12 @@
 describe "Exceptions", ->
   it "are thrown through the stack", ->
     b = new Bacon.Bus()
-    b.flatMap(-> throw "testing testing").onValue(->)
+    b.take(1).flatMap(-> throw "testing testing").onValue(->)
     expect(-> b.push()).to.throw("testing testing")
+    values = []
+    b.take(1).onValue((x) -> values.push(x))
+    b.push()
+    expect(values).to.deep.equal([1])

 endlessly = (values...) ->
   index = 0


Specs for the error handler.

Specs for the error handler.

--- spec/atom-spec.coffee
+++ spec/atom-spec.coffee
@@ -52,3 +52,67 @@
     it 'loads the default core config', ->
       expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe true
       expect(atom.config.get('editor.showInvisibles')).toBe false
+
+  describe "window onerror handler", ->
+    beforeEach ->
+      spyOn atom, 'openDevTools'
+      spyOn atom, 'executeJavaScriptInDevTools'
+
+    it "will open the dev tools when an error is triggered", ->
+      try
+        a + 1
+      catch e
+        window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
+
+      expect(atom.openDevTools).toHaveBeenCalled()
+      expect(atom.executeJavaScriptInDevTools).toHaveBeenCalled()
+
+    describe "::onWillThrowError", ->
+      willThrowSpy = null
+      beforeEach ->
+        willThrowSpy = jasmine.createSpy()
+
+      it "is called when there is an error", ->
+        error = null
+        atom.onWillThrowError(willThrowSpy)
+        try
+          a + 1
+        catch e
+          error = e
+          window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
+
+        delete willThrowSpy.mostRecentCall.args[0].preventDefault
+        expect(willThrowSpy).toHaveBeenCalledWith
+          message: error.toString()
+          url: 'abc'
+          line: 2
+          column: 3
+          originalError: error
+
+      it "will not show the devtools when preventDefault() is called", ->
+        willThrowSpy.andCallFake (errorObject) -> errorObject.preventDefault()
+        atom.onWillThrowError(willThrowSpy)
+
+        try
+          a + 1
+        catch e
+          window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
+
+        expect(willThrowSpy).toHaveBeenCalled()
+        expect(atom.openDevTools).not.toHaveBeenCalled()
+        expect(atom.executeJavaScriptInDevTools).not.toHaveBeenCalled()
+
+    describe "::onDidThrowError", ->
+      didThrowSpy = null
+      beforeEach ->
+        didThrowSpy = jasmine.createSpy()
+
+      it "is called when there is an error", ->
+        error = null
+        atom.onDidThrowError(didThrowSpy)
+        try
+          a + 1
+        catch e
+          error = e
+          window.onerror.call(window, e.toString(), 'abc', 2, e)
+        expect(didThrowSpy).toHaveBeenCalledWith(error.toString())


test(dependencies): add test of succeeding dependencies

--- specs/units/dependencies.spec.coffee
+++ specs/units/dependencies.spec.coffee
@@ -1,3 +1,18 @@
+
+virtualEnv('example depending on succeeding examples')
+.shouldSucceedWith /3 success, 3 assertions, 0 failures, 0 skipped/, ->
+  describe 'dependency 1', id: 'success1', ->
+    it -> true.should be true
+
+  describe 'dependency 2', id: 'success2', ->
+    it -> true.should be true
+
+  describe 'dependent', ->
+    dependsOn 'success1'
+    dependsOn 'success2'
+
+    it -> true.should be true
+

 virtualEnv('example depending on a failing example')
 .shouldFailWith /1 failure, 1 skipped/, ->


Add rootView to tab spec

Add rootView to tab spec

--- spec/tabs-spec.coffee
+++ spec/tabs-spec.coffee
@@ -32,6 +32,7 @@
     serialize: -> { deserializer: 'TestView', @title, @longTitle }

   beforeEach ->
+    window.rootView = new RootView
     registerDeserializer(TestView)
     item1 = new TestView('Item 1')
     item2 = new TestView('Item 2')


collision with regular bounds

collision with regular bounds

--- test/collision.coffee
+++ test/collision.coffee
@@ -66,4 +66,5 @@

   equals p2.y, 0
   equals p2.x, 20
+
 module()


added test for arguments munging

added test for arguments munging

--- test/contracts.coffee
+++ test/contracts.coffee
@@ -457,3 +457,8 @@
       console.log b

   blames (-> a null)
+
+test "dont touch the arguments object", ->
+  a :: (Any) -> Any
+  a = (b) -> arguments.length
+  eq (a 'b', 'c', 'd'), 3, "the arguments object should be untouched by a contract"


Update palettes.coffee

Update palettes.coffee


Fixed CounterLog.increment() to log once it reaches or exceeds the next buffer boundary.

--- src/CounterLog.coffee
+++ src/CounterLog.coffee
@@ -13,11 +13,12 @@
     @log()

   increment: (amount = 1) ->
+    nextBoundary = @options.bufferSize - (@value % @options.bufferSize) + @value
     @value += amount
     if @value >= @total
       @value == @total
       @log()
-    else if @value % @options.bufferSize == 0
+    else if @value >= nextBoundary
       @log()

   log: -> Logger.info("#{@options.label}: #{@value}/#{@total}")


updates Handlebars' activity helper

updates Handlebars' activity helper

--- src/client/handlebars-helpers.coffee
+++ src/client/handlebars-helpers.coffee
@@ -51,10 +51,10 @@
     ###*
      * Auto-links URLs in text.
      * Uses [twitter-text.js](https://github.com/twitter/twitter-text-js) behind the scenes.
-     *
+     *
      *     snippet="You have to try http://hull.io/try"
      *
-     *
+     *
      *
{{autoLink snippet}}

      *     =>
You have to try http://hull.io/try

      *
@@ -395,7 +395,7 @@
         fallback = map.fallback
         return '' unless fallback?

-        type = (entry.object?.uid if type is 'entity') || fallback.object[type] || entry.object.description
+        type = (entry.object?.uid if type is 'entity') || fallback.object[type] || entry.object.name || entry.object.description

         (fallback.verb[verb]||verb) + ' ' + type


Removing peripheral extensions from Corelib

--- test/quad_tree.coffee
+++ test/quad_tree.coffee
@@ -1,58 +0,0 @@
-module "QuadTree"
-
-test "should have bounds", ->
-  quadTree = QuadTree()
-
-  equals quadTree.I.bounds.x, 0
-  equals quadTree.I.bounds.y, 0
-  equals quadTree.I.bounds.width, 480
-  equals quadTree.I.bounds.height, 320
-
-  equals quadTree.root().I.bounds.width, 480, "root should have correct width"
-  equals quadTree.root().I.bounds.height, 320, "root should have correct height"
-
-test "should insert and retrieve point", ->
-  quadTree = QuadTree()
-
-  quadTree.insert
-    x: 50
-    y: 100
-
-  items = quadTree.retrieve
-    x: 50
-    y: 100
-
-  ok items.length
-
-test "should subdivide if too many points are inserted", ->
-  quadTree = QuadTree()
-
-  (quadTree.I.maxChildren + 1).times ->
-    quadTree.insert
-      x: 2
-      y: 5
-
-  ok quadTree.root().I.nodes.length
-
-test "should retrieve correct number of nodes", ->
-  quadTree = QuadTree(
-    maxChildren: 4
-  )
-
-  (quadTree.I.maxChildren + 1).times (n) ->
-    quadTree.insert
-      x: 1 + n * 50
-      y: 50
-
-  node1 = quadTree.retrieve
-    x: 0
-    y: 0
-
-  node2 = quadTree.retrieve
-    x: 150
-    y: 0
-
-  equals node1.length, 3
-  equals node2.length, 2
-
-module()


Expanding subrender tests

Expanding subrender tests

--- test/subrender.coffee
+++ test/subrender.coffee
@@ -1,59 +1,119 @@
 describe "subrender", ->
-  template = makeTemplate """
-    %div
+  describe "with root node", ->
+    template = makeTemplate """
+      %div
+        = @generateItem
+    """
+
+    it "should render elements in-line", ->
+      model =
+        generateItem: ->
+          document.createElement("li")
+
+      behave template(model), ->
+        assert Q("li")
+
+    it "should render lists of nodes", ->
+      model =
+        generateItem: ->
+          [
+            document.createElement("li")
+            document.createElement("li")
+            document.createElement("p")
+          ]
+
+      behave template(model), ->
+        assert all("li").length, 2
+        assert all("p").length, 1
+
+    it "should work with a node with children", ->
+      model =
+        generateItem: ->
+          div = document.createElement "div"
+
+          div.innerHTML = "
Yo

Yolo

Broheim

"
+
+          div
+
+      behave template(model), ->
+        assert all("li").length, 2
+        assert all("p").length, 1
+        assert all("ol").length, 1
+
+    it "should work with observables", ->
+      model =
+        name: Observable "wat"
+        generateItem: ->
+          item = document.createElement("li")
+
+          item.textContent = @name()
+
+          item
+
+      behave template(model), ->
+        assert.equal all("li").length, 1
+
+        assert.equal Q("li").textContent, "wat"
+
+        model.name "yo"
+
+        assert.equal Q("li").textContent, "yo"
+
+  describe "without root node", ->
+    template = makeTemplate """
       = @generateItem
-  """
+    """

-  it "should render elements in-line", ->
-    model =
-      generateItem: ->
-        document.createElement("li")
+    it "should work with observables"
+    # TODO
+    ->
+      model =
+        name: Observable "wat"
+        generateItem: ->
+          item = document.createElement("li")

-    behave template(model), ->
-      assert Q("li")
+          item.textContent = @name()

-  it "should render lists of nodes", ->
-    model =
-      generateItem: ->
-        [
-          document.createElement("li")
-          document.createElement("li")
-          document.createElement("p")
-        ]
+          item

-    behave template(model), ->
-      assert all("li").length, 2
-      assert all("p").length, 1
+      behave template(model), ->
+        assert.equal all("li").length, 1

-  it "should work with a node with children", ->
-    model =
-      generateItem: ->
-        div = document.createElement "div"
+        assert.equal Q("li").textContent, "wat"

-        div.innerHTML = "
Yo

Yolo

Broheim

"
+        model.name "yo"

-        div
+        assert.equal Q("li").textContent, "yo"

-    behave template(model), ->
-      assert all("li").length, 2
-      assert all("p").length, 1
-      assert all("ol").length, 1
+  describe "with multiple sibling elements without a root node", ->
+    template = makeTemplate """
+      = @generateItem
+      = @otherItem
+    """

-  it "should work with observables", ->
-    model =
-      name: Observable "wat"
-      generateItem: ->
-        item = document.createElement("li")
+    it "should work with observables"
+    # TODO
+    ->
+      model =
+        name: Observable "wat"
+        otherItem: ->
+          item = document.createElement("li")

-        item.textContent = @name()
+          item.textContent = @name()

-        item
+          item
+        generateItem: ->
+          item = document.createElement("li")

-    behave template(model), ->
-      assert.equal all("li").length, 1
+          item.textContent = @name()

-      assert.equal Q("li").textContent, "wat"
+          item

-      model.name "yo"
+      behave template(model), ->
+        assert.equal all("li").length, 2

-      assert.equal Q("li").textContent, "yo"
+        assert.equal all("li")[0].textContent, "wat"
+
+        model.name "yo"
+
+        assert.equal Q("li")[0].textContent, "yo"


Test clarifications

Test clarifications

--- test/test.date.coffee
+++ test/test.date.coffee
@@ -36,11 +36,14 @@
       assert.equal '2011-06-18T02:20:45.678-08:00', date.toISO8601String(true, true, true)

     it 'throws RangeError for Invalid Dates', ->
-      fn = -> new Date('not a date').toISO8601String()
+      invalidDate = new Date('not a date')     # Doesn't throw exception
+      fn = -> invalidDate.toISO8601String()    # Should throw exception when called
       expect(fn).to.throw RangeError

   describe 'parseISO8601', ->
+
+    # Using Date.parseISO8601(..., false) to parse without browser fallback.

     it 'parses UTC ISO 8601 strings', ->
       assert.equal date.getTime(), Date.parseISO8601('2011-06-18T10:20:45.678Z', false).getTime()


Revamped collection query integration tests

--- test/integration/collection-int.coffee
+++ test/integration/collection-int.coffee
@@ -166,95 +166,151 @@
         .expect documentFields.bind undefined, ['_id', 'navn']
         .end done

-  describe '?tag', ->
-    it 'should return documents matching tag', (done) ->
-      req.get '/turer?tag=Skitur&api_key=dnt'
-        .expect 200
-        .expect (res) ->
-          assert.equal res.body.count, 2
-          assert.equal sted.tags[0], 'Skitur' for sted in res.body.documents
-          return
-        .end done
-
-    it 'should return documents not matching tag', (done) ->
-      req.get '/turer?tag=!Fottur&api_key=dnt'
-        .expect 200
-        .expect (res) ->
-          assert.equal res.body.count, 2
-          assert.notEqual tur.tags[0], 'Fottur' for tur in res.body.documents
-          return
-        .end done
-
-  describe '?gruppe', ->
-    it 'should filter based on gruppe', (done) ->
-      gruppe = '52407f3c4ec4a1381500025d'
-      req.get "/steder?gruppe=#{gruppe}&api_key=dnt"
-        .expect 200
-        .expect (res) ->
-          assert.equal res.body.count, 20
-          assert.equal res.body.total, 26
-          #assert gruppe in doc.grupper for doc in res.body.documents
-          return
-        .end done
-
-  describe '?after', ->
-    documentCount = (count, res) ->
-      assert.equal res.body.count, count
-
-    documentAfter = (after, res) ->
-      for doc in res.body.documents
-        assert doc.endret >= after, "#{doc.endret} >= #{after}"
-      return
-
-    it 'should return documents changed after UTZ datestamp', (done) ->
-      date = '2014-06-01T17:42:39.766Z'
-      req.get "/turer?after=#{date}&api_key=dnt"
-        .expect 200
-        .expect documentCount.bind(undefined, 10)
-        .expect documentAfter.bind(undefined, date)
-        .end done
-
-    it 'should return documents changed after millis from 1.1.1970', (done) ->
-      date = new Date '2014-06-01T17:42:39.766Z'
-      req.get "/turer?after=#{date.getTime()}&api_key=dnt"
-        .expect 200
-        .expect documentCount.bind(undefined, 10)
-        .expect documentAfter.bind(undefined, date.toISOString())
-        .end done
-
-  describe '?bbox', ->
-    it 'should return documents within bbox', (done) ->
-      coords = '5.3633880615234375,60.777937176256515,6.52862548828125,61.044326483979894'
-      req.get "/steder?bbox=#{coords}&api_key=dnt"
-        .expect 200
-        .expect (res) ->
-          assert.equal res.statusCode, 200
-          assert.equal res.body.count, 7
-          # @TODO assert geometry for doc in res.body.documents
-          return
-        .end done
-
-  describe '?near', ->
-    it 'should return documents near coordinate', (done) ->
-      coords = '6.22051,60.96570'
-      req.get "/steder?near=#{coords}&api_key=dnt"
-        .expect 200
-        .expect (res) ->
-          assert.equal res.statusCode, 200
-          assert.equal res.body.count, 20
-          assert.equal res.body.documents[0]._id, '52407fb375049e5615000460'
-      .end done
-
-  describe '?privat.', ->
-    it 'should return documents matching private property', (done) ->
-      req.get '/steder?privat.opprettet_av.id=3456&api_key=dnt'
-        .expect 200
-        .expect (res) ->
-          assert.equal res.statusCode, 200
-          assert.equal res.body.count, 17
-          # @TODO assert doc.privat.opprettet_av.id for doc in res.body.documents
-          return
-        done()
+  describe 'Queries', ->
+    describe 'Basic Operators', ->
+      it 'field should exist', (done) ->
+        req.get '/turer?bilder=&fields=bilder&api_key=dnt'
+          .expect 200
+          .expect (res) ->
+            for doc in res.body.documents
+              assert.notEqual typeof doc.bilder, 'undefined'
+            return
+          .end done
+
+      it 'field should not exist', (done) ->
+        req.get '/steder?bilder=!&fields=bilder&api_key=dnt'
+          .expect 200
+          .expect (res) ->
+            for doc in res.body.documents
+              assert.equal typeof res.bilder, 'undefined'
+            return
+          .end done
+
+      it 'field should equal string', (done) ->
+        req.get '/turer?tags.0=Skitur&api_key=dnt'
+          .expect 200
+          .expect 'count-total', 2
+          .expect (res) ->
+            assert.equal sted.tags[0], 'Skitur' for sted in res.body.documents
+            return
+          .end done
+
+      it 'field should equal number', (done) ->
+        req.get '/steder?privat.opprettet_av.id=3456&api_key=dnt'
+          .expect 200
+          .expect 'count-total', 17
+          .end done
+
+      it 'field should not equal string', (done) ->
+        req.get '/turer?tags.0=!Fottur&api_key=dnt'
+          .expect 200
+          .expect 'count-total', 2
+          .expect (res) ->
+            assert.notEqual tur.tags[0], 'Fottur' for tur in res.body.documents
+            return
+          .end done
+
+      it 'field should not equal number', (done) ->
+        req.get '/steder?privat.opprettet_av.id=!3456&api_key=dnt'
+          .expect 200
+          .expect 'count-total', 103
+          .end done
+
+      it 'field should start with string', (done) ->
+        req.get '/steder?navn=^b&api_key=dnt'
+          .expect 200
+          .expect 'count-total', 10
+          .end done
+
+      it 'field should end with string', (done) ->
+        req.get '/steder?navn=$0&api_key=dnt'
+          .expect 200
+          .expect 'count-total', 9
+          .end done
+
+      it 'field should contain string', (done) ->
+        req.get '/steder?navn=~033&api_key=dnt'
+          .expect 200
+          .expect 'count-total', 3
+          .end done
+
+      it 'field should be greater than', (done) ->
+        req.get '/steder?privat.opprettet_av.id=>1234&api_key=dnt'
+          .expect 200
+          .expect 'count-total', 37
+          .end done
+
+      it 'field should be less than', (done) ->
+        req.get '/steder?privat.opprettet_av.id=<3456&api_key=dnt'
+          .expect 200
+          .expect 'count-total', 19
+          .end done
+
+    describe 'Custom Operators', ->
+      describe '?after', ->
+        documentCount = (count, res) ->
+          assert.equal res.body.count, count
+
+        documentAfter = (after, res) ->
+          for doc in res.body.documents
+            assert doc.endret >= after, "#{doc.endret} >= #{after}"
+          return
+
+        it 'should return documents changed after UTZ datestamp', (done) ->
+          date = '2014-06-01T17:42:39.766Z'
+          req.get "/turer?after=#{date}&api_key=dnt"
+            .expect 200
+            .expect documentCount.bind(undefined, 10)
+            .expect documentAfter.bind(undefined, date)
+            .end done
+
+        it 'should return documents changed after millis from 1.1.1970', (done) ->
+          date = new Date '2014-06-01T17:42:39.766Z'
+          req.get "/turer?after=#{date.getTime()}&api_key=dnt"
+            .expect 200
+            .expect documentCount.bind(undefined, 10)
+            .expect documentAfter.bind(undefined, date.toISOString())
+            .end done
+
+      describe '?bbox', ->
+        it 'should return documents within bbox', (done) ->
+          coords = '5.3633880615234375,60.777937176256515,6.52862548828125,61.044326483979894'
+          req.get "/steder?bbox=#{coords}&api_key=dnt"
+            .expect 200
+            .expect 'count-total', 7
+            .expect (res) ->
+              # @TODO assert geometry for doc in res.body.documents
+              return
+            .end done
+
+      describe '?near', ->
+        it 'should return documents near coordinate', (done) ->
+          coords = '6.22051,60.96570'
+          req.get "/steder?near=#{coords}&api_key=dnt"
+            .expect 200
+            .expect (res) ->
+              assert.equal res.body.documents[0]._id, '52407fb375049e5615000460'
+          .end done
+
+    describe 'Leggacy Fields', ->
+      it '?tag should query on tags.0', (done) ->
+        req.get '/turer?tag=Fottur&api_key=dnt'
+          .expect 200
+          .expect 'count-total', 25
+          .expect (res) ->
+            assert.equal sted.tags[0], 'Fottur' for sted in res.body.documents
+            return
+          .end done
+
+      it '?gruppe should query on grupper', (done) ->
+        gruppe = '52407f3c4ec4a1381500025d'
+        req.get "/steder?gruppe=#{gruppe}&fields=grupper&api_key=dnt"
+          .expect 200
+          .expect 'count-total', 26
+          .expect (res) ->
+            assert gruppe in doc.grupper for doc in res.body.documents
+            return
+          .end done

 describe 'POST', ->
   url = '/turer?api_key=dnt'


Fix testing pseudo content across browsers

--- test/src/system/attachment_test.coffee
+++ test/src/system/attachment_test.coffee
@@ -41,7 +41,7 @@
         # Caption prompt is displayed when editing attachment
         captionElement = findElement("figcaption")
         assert.ok captionElement.clientHeight > 0
-        assert.equal getCaptionContent(captionElement), "\"#{Trix.config.lang.captionPrompt}\""
+        assert.equal getCaptionContent(captionElement), Trix.config.lang.captionPrompt
         done()

 getFigure = ->
@@ -51,6 +51,19 @@
   getEditorElement().querySelector(selector)

 getCaptionContent = (element) ->
-  element.textContent or
-    getComputedStyle(element, "::before").content or
-    getComputedStyle(element, "::after").content
+  element.textContent or getPseudoContent(element)
+
+
+getPseudoContent = (element) ->
+  before = getComputedStyle(element, "::before").content
+  after = getComputedStyle(element, "::after").content
+
+  content =
+    if before and before isnt "none"
+      before
+    else if after and after isnt "none"
+      after
+    else
+      ""
+
+  content.replace(/^['"]/, "").replace(/['"]$/, "")


Attachment figure is replaced after click

--- test/src/system/attachment_test.coffee
+++ test/src/system/attachment_test.coffee
@@ -9,9 +9,8 @@
         expectDocument "a#{Trix.OBJECT_REPLACEMENT_CHARACTER}b\n"

 editorTest "resizing an image", (expectDocument) ->
-  figure = document.activeElement.querySelector("figure.attachment.image")
-  clickElement figure, ->
-    ok handle = figure.querySelector(".resize-handle")
+  clickElement getFigure(), ->
+    ok handle = getFigure().querySelector(".resize-handle")

     mouseDownOnElementAndMove handle, 5, ->
       locationRangeOfAttachment = Trix.LocationRange.forLocationWithLength({index: 0, offset: 2}, 1)
@@ -21,8 +20,10 @@
       expectDocument "ab#{Trix.OBJECT_REPLACEMENT_CHARACTER}\n"

 editorTest "removing an image", (expectDocument) ->
-  figure = document.activeElement.querySelector("figure.attachment.image")
-  clickElement figure, ->
-    closeButton = figure.querySelector(".remove")
+  clickElement getFigure(), ->
+    closeButton = getFigure().querySelector(".remove")
     clickElement closeButton, ->
       expectDocument "ab\n"
+
+getFigure = ->
+  document.activeElement.querySelector("figure.attachment.image")


job creation

job creation

--- src/kue-worker.coffee
+++ src/kue-worker.coffee
@@ -1,12 +1,14 @@
 _          = require 'lodash'
-async       = require 'async'
+async      = require 'async'
+debug      = require('debug')('interval-service')
 cronParser = require 'cron-parser'
-debug      = require('debug')('interval-service')

 class KueWorker
   constructor: (dependencies={})->
+    @INTERVAL_TTL       = process.env.INTERVAL_TTL ? 10000
+    @INTERVAL_JOBS      = process.env.INTERVAL_JOBS ? 1000
+    @INTERVAL_ATTEMPTS  = process.env.INTERVAL_ATTEMPTS ? 999
     @INTERVAL_PROMOTION = process.env.INTERVAL_PROMOTION ? 100
-    @INTERVAL_JOBS      = process.env.INTERVAL_JOBS ? 1000

     @kue = dependencies.kue ? require 'kue'
     IORedis = dependencies.IORedis ? require 'ioredis'
@@ -33,14 +35,19 @@
         debug 'creating a new job!'
         @meshbluMessage.message [job.data.targetId], timestamp: Date.now()

-        try
-          intervalTime = @calculateNextCronInterval cronString, jobStartTime if cronString?
-        catch error
-          console.error error
-          done()
+        if cronString?
+          try
+            intervalTime = @calculateNextCronInterval cronString, jobStartTime
+            @redis.set "interval/time/#{job.data.targetId}", intervalTime
+          catch error
+            console.error error
+            done()

-        @setCronInterval intervalTime, cronString
-        done()
+        @createJob(job, intervalTime).
+          save (err) =>
+            debug 'created a job', job.id
+            redis.sadd "interval/job/#{job.data.targetId}", job.id
+            done(err)

   getTargetJobs: (job, callback=->) =>
     @redis.srem "interval/job/#{job.data.targetId}", job.id
@@ -73,15 +80,18 @@
     parser = cronParser.parseExpression cronString, currentDate: currentDate

     while timeDiff <= 0
-      nextTime = parser.next()
-      nextTime.setMilliseconds 0
-      timeDiff = nextTime - currentDate
-      debug 'this is the next time', timeDiff, nextTime.getTime()
+      nextDate = parser.next()
+      nextDate.setMilliseconds 0
+      timeDiff = nextDate - currentDate
+      debug 'this is the next time', timeDiff, nextDate.getTime()

     return timeDiff

-  setCronInterval: (intervalTime, cronString) =>
-    return unless intervalTime? and cronString?
-    @redis.set "interval/time/#{job.data.targetId}", intervalTime
+  createJob: (job, intervalTime)=>
+    return queue.create('interval', job.data).
+      delay(intervalTime).
+      removeOnComplete(true).
+      attempts(@INTERVAL_ATTEMPTS).
+      ttl(@INTERVAL_TTL)

 module.exports = KueWorker


Change names of gulp bump-tasks.

Change names of gulp bump-tasks.

--- build/gulpfile.coffee
+++ build/gulpfile.coffee
@@ -215,9 +215,9 @@

 # Generate tasks for bumping project versions and tagging.
 _.each {
-  patch: 'Bump, commit, and tag the package patch version.'
-  minor: 'Bump, commit, and tag the package minor version.'
-  major: 'Bump, commit, and tag the package major version.'
+  'bump:patch': 'Bump, commit, and tag the package patch version.'
+  'bump:minor': 'Bump, commit, and tag the package minor version.'
+  'bump:major': 'Bump, commit, and tag the package major version.'
 }, (description, importance) ->
   gulp.task importance, description, ->
     gulp.src ['./package.json']


Social: style cleanup

Social: style cleanup

--- client/Social/Activity/AppController.coffee
+++ client/Social/Activity/AppController.coffee
@@ -84,7 +84,8 @@
     return router.getCurrentPath().search(routeToLookUp) > 0

-  fetch: ({channelId, from, limit, skip, mostLiked}, callback = noop) ->
+  fetch: (options, callback = noop) ->
+    {channelId, from, limit, skip, mostLiked} = options

     prefetchedKey = if mostLiked then "popularPosts" else "navigated"
     id = channelId


Update inverter operator in favor of coffee-compliant syntax

--- client/app/components/message_header.coffee
+++ client/app/components/message_header.coffee
@@ -144,7 +144,7 @@
                         row @props.message.get('subject'), 'headers subject'

     toggleDetails: ->
-        @setState showDetails: !@state.showDetails
+        @setState showDetails: not @state.showDetails

     toggleAttachments: ->
-        @setState showAttachements: !@state.showAttachements
+        @setState showAttachements: not @state.showAttachements


shortcuts: resize modal on viewAppended and on tabChange

--- client/app/lib/shortcuts/views/modal.coffee
+++ client/app/lib/shortcuts/views/modal.coffee
@@ -33,6 +33,11 @@

   viewAppended: ->

+    @modalTabs.on 'PaneDidShow', => kd.utils.defer @bound '_windowDidResize'
+
+    @_windowDidResize()
+
+
   prepareTabData: (keyconfig) ->

     forms = {}


isMyNotification: return early instead of defaulting parameter

--- client/app/lib/util/isMyNotification.coffee
+++ client/app/lib/util/isMyNotification.coffee
@@ -1,6 +1,8 @@
 whoami = require './whoami'

-module.exports = isMyNotification = (notification = {}) ->
+module.exports = isMyNotification = (notification) ->
+
+  return no  unless notification

   { accountId } = notification.channelMessage


Removed suggestions Session variable

Removed suggestions Session variable

--- client/coffeescript/templates/tagbox.coffee
+++ client/coffeescript/templates/tagbox.coffee
@@ -14,11 +14,11 @@
     return {suggested:false,tags:visible}

   suggested_tags: ->
-    suggestions = Session.get("suggestions_#{ @_id }")
     filtered = []
-    for tag in suggestions
-      filtered.push(tag) if tag? &&
-        tag.search(Session.get("filter_text_#{ @_id }")) != -1
+    if @suggestions?
+      for tag in @suggestions
+        filtered.push(tag) if tag? &&
+          tag.search(Session.get("filter_text_#{ @_id }")) != -1
     return {suggested:true,tags:filtered}

   tagging_post: -> Session.equals("tagging_#{ @_id }", true)
@@ -39,7 +39,7 @@
           when 74 #J/Check
             Template.tagbox.add_tag(@_id, @my_tags, tag_text, tag_box)
           when 75 #K/Kill
-            Session.get("suggestions_#{ @_id }").remove(tag_text)
+            @suggestions?.remove(tag_text)
             Session.get("context_#{@_id}")?.invalidate()

         event.stopImmediatePropagation()
@@ -64,7 +64,7 @@
                 suggested_tag, event.target)
           when 39 #Right-arrow: REMOVE SUGGESTED TAG
             if event.ctrlKey
-              Session.get("suggestions_#{ @_id }").remove(suggested_tag)
+              @suggestions?.remove(suggested_tag)
               Session.get("context_#{ @_id }")?.invalidate()
           else    #Update filter with new text
             Session.set("filter_text_#{ @_id }", entered_text)
@@ -84,9 +84,6 @@
       if not event.isImmediatePropagationStopped()

         Session.set("tagging_#{ @_id }", true)  #Display tagbox
-
-        Session.set("suggestions_#{ @_id }",
-          if @suggestions? then @suggestions else ['tag'])

         event.stopImmediatePropagation()

@@ -110,7 +107,7 @@
     if tag_text != "" && not (tag_text in my_tags)
       my_tags.push(tag_text)
       Posts.update(id, {$set: {'my_tags': my_tags}})
-      Session.get("suggestions_#{ id }").remove(tag_text)
+      @suggestions?.remove(tag_text)
     #clear textbox and update suggestion filter
     text_box.value = ''
     Session.set("filter_text_#{ id }", '')


Reorganize require statements in IDEView

--- client/ide/lib/views/tabview/ideview.coffee
+++ client/ide/lib/views/tabview/ideview.coffee
@@ -1,21 +1,21 @@
+$                     = require 'jquery'
 kd                    = require 'kd'
+nick                  = require 'app/util/nick'
+FSFile                = require 'app/util/fs/fsfile'
+KDView                = kd.View
+FSHelper              = require 'app/util/fs/fshelper'
+IDEHelpers            = require '../../idehelpers'
+IDEEditorPane         = require '../../workspace/panes/ideeditorpane'
 KDContextMenu         = kd.ContextMenu
+KDTabPaneView         = kd.TabPaneView
+IDEPreviewPane        = require '../../workspace/panes/idepreviewpane'
+IDEDrawingPane        = require '../../workspace/panes/idedrawingpane'
+IDETerminalPane       = require '../../workspace/panes/ideterminalpane'
 KDCustomHTMLView      = kd.CustomHTMLView
 KDSplitViewPanel      = kd.SplitViewPanel
-KDTabPaneView         = kd.TabPaneView
-KDView                = kd.View
-nick                  = require 'app/util/nick'
-FSHelper              = require 'app/util/fs/fshelper'
-FSFile                = require 'app/util/fs/fsfile'
-showErrorNotification = require 'app/util/showErrorNotification'
-IDEDrawingPane        = require '../../workspace/panes/idedrawingpane'
-IDEEditorPane         = require '../../workspace/panes/ideeditorpane'
-IDEPreviewPane        = require '../../workspace/panes/idepreviewpane'
-IDETerminalPane       = require '../../workspace/panes/ideterminalpane'
 IDEWorkspaceTabView   = require '../../workspace/ideworkspacetabview'
 IDEApplicationTabView = require './ideapplicationtabview.coffee'
-IDEHelpers            = require '../../idehelpers'
-$                     = require 'jquery'
+showErrorNotification = require 'app/util/showErrorNotification'

 module.exports = class IDEView extends IDEWorkspaceTabView


IDE: Always fetch file permissions before opening a file.

--- client/ide/lib/views/tabview/ideview.coffee
+++ client/ide/lib/views/tabview/ideview.coffee
@@ -245,7 +245,7 @@
     # we use emitChange to detect this case for now
     notifyIfNoPermissions = emitChange

-    if file.isDummyFile() or not notifyIfNoPermissions
+    if file.isDummyFile()
       return @createEditorAfterFileCheck file, content, callback, emitChange, no

     file.fetchPermissions (err, result) =>
@@ -253,7 +253,7 @@
       return showErrorNotification err  if err

       { readable, writable } = result
-      if not readable
+      if notifyIfNoPermissions and not readable
         IDEHelpers.showFileAccessDeniedError()
         return callback()

@@ -290,9 +290,12 @@
       ace.editor.scrollToRow 0
       editorPane.goToLine 1

+      notifyIfNoPermissions = emitChange
+
       if isReadOnly
         editorPane.makeReadOnly()
-        IDEHelpers.showFileReadOnlyNotification()
+        editorPane.isFileReadonly = yes
+        IDEHelpers.showFileReadOnlyNotification()  if notifyIfNoPermissions

       callback editorPane


Rename class Expects to ExpectsMethod.

Rename class Expects to ExpectsMethod.

--- src/TinyMockJS.coffee
+++ src/TinyMockJS.coffee
@@ -14,12 +14,12 @@
   #
   publish.mock = (test_function) ->
     try
-      Expects.install_expects_method()
+      ExpectsMethod.install_expects_method()
       convenience_mocks = _build_convenience_mock_objects()
       _run_test_function(test_function, convenience_mocks)
       AllExpectations.verify_all_expectations()
     finally
-      Expects.uninstall_expects_method()
+      ExpectsMethod.uninstall_expects_method()
       AllExpectations.uninstall_all_mocked_methods()
       AllExpectations.unregister_all_expectations()

@@ -33,11 +33,11 @@

 #
-# Expects
+# ExpectsMethod
 #
 # Note: this is better thought of as a mixin; comment further
 #
-class Expects
+class ExpectsMethod

   @install_expects_method: ->
     Object.prototype.expects = expects


Showing active chips

Showing active chips

--- client/src/app/game/board/boardNodes.coffee
+++ client/src/app/game/board/boardNodes.coffee
@@ -9,23 +9,20 @@
   SwitchRenamedEvent.listen (event) ->
     renameChips(currentGame.version.circuits[circuits.currentCircuitMeta.type].board, 'switch', event.oldName, event.newName)

-  WillChangeLocalVersionEvent.listen -> updateChipToPathMap()
+  WillChangeLocalVersionEvent.listen -> updateChipPaths()

   # TODO: change on circuit change as well

   chipHashKeyToPath = {}

   # Create map of chips to their paths
-  updateChipToPathMap = ->
+  updateChipPaths = ->
     recursiveUpdate = (path, chip) ->
-      if not chip.$$hashKey then return
-
-      chipHashKeyToPath[chip.$$hashKey] = path
+      chip.path = path
       for childIndex, childChip of chip.children
         childPath = RW.appendToArray(path, childIndex)
         recursiveUpdate(childPath, childChip)

-    chipHashKeyToPath = {}
     recursiveUpdate([], chips.getCurrentBoard())

   renameChips = (chip, chipType, oldName, newName) ->
@@ -59,4 +56,4 @@
   open: open
   close: close

-  getChipPath: (node) => chipHashKeyToPath[node.$$hashKey]
+  getChipPath: (node) => node.path


Use youtube video html

Use youtube video html

--- src/react/attachments/youtube.coffee
+++ src/react/attachments/youtube.coffee
@@ -2,13 +2,13 @@

 class YouTubeAttachment extends AttachmentElement
   render: ->
-    imgStyle =
-      "maxWidth": "#{@state.width- 50}px"
-

-
{ @props.attachment.service_name }

-
{ @props.attachment.author_name }

-
{@props.attachment.title}

-
+

+

+
{ @props.attachment.service_name }

+
{ @props.attachment.author_name }

+
{@props.attachment.title}

+

+

 module.exports = YouTubeAttachment


simplify _getLastChangeIndex implementation

--- src/modules/undo-manager.coffee
+++ src/modules/undo-manager.coffee
@@ -68,25 +68,15 @@
     this._change('undo', 'redo')

   _getLastChangeIndex: (delta) ->
-    lastChangeIndex = index = offset = 0
-    _.each(delta.ops, (op) ->
-      # Insert
-      if Tandem.InsertOp.isInsert(op)
-        offset += op.getLength()
-        lastChangeIndex = index + offset
-      else if Tandem.RetainOp.isRetain(op)
-        # Delete
-        if op.start > index
-          lastChangeIndex = index + offset
-          offset -= (op.start - index)
-        # Format
-        if _.keys(op.attributes).length > 0
-          lastChangeIndex = op.end + offset
-        index = op.end
+    lastIndex = 0
+    delta.apply((index, text) ->
+      lastIndex = Math.max(index + text.length, lastIndex)
+    , (index, length) ->
+      lastIndex = Math.max(index, lastIndex)
+    , (index, length) ->
+      lastIndex = Math.max(index + length, lastIndex)
     )
-    if delta.endLength < delta.startLength + offset
-      lastChangeIndex = delta.endLength
-    return lastChangeIndex
+    return lastIndex

   _change: (source, dest) ->
     if @stack[source].length > 0


webapp/public md files: style similar to ipynb

--- src/smc-webapp/public/editor_md.cjsx
+++ src/smc-webapp/public/editor_md.cjsx
@@ -45,11 +45,19 @@
         else if not @props.content?
             <Loading />
         else
-            <div className="webapp-editor-static-html-content">
+            md_style =
+                margin          : '20px'
+                padding         : '15px'
+                boxShadow       : 'rgba(87, 87, 87, 0.2) 0px 0px 12px 1px'
+                backgroundColor : 'white'
+                display         : 'block'   # because wrapped HTML in Markdown is a span by default
+            <div
+                className = "webapp-editor-static-html-content"
+                style     = {backgroundColor: 'rgb(238, 238, 238)'}>
                 <Markdown
                     project_id  = {@props.project_id}
                     file_path   = {@props.file_path}
-                    style       = {marginLeft: "2em", marginRight: "2em"}
+                    style       = {md_style}
                     value       = {@props.content} />
             </div>


Send failedData on error emit

Send failedData on error emit

This way components can mark the error as handled so the error modal
won't display

--- src/api/loader.coffee
+++ src/api/loader.coffee
@@ -63,7 +63,7 @@

           apiEventChannel.emit(failedEvent, failedData)
         setting.onFail?(response) or defaultFail(response)
-        apiEventChannel.emit('error', response: response, apiSetting: apiSetting)
+        apiEventChannel.emit('error', {response, apiSetting, failedData})
       )
   , delay


logCursorScope works properly

logCursorScope works properly

--- src/app/editor.coffee
+++ src/app/editor.coffee
@@ -152,7 +152,7 @@
         'show-next-buffer': @loadNextEditSession
         'show-previous-buffer': @loadPreviousEditSession
         'toggle-line-comments': @toggleLineCommentsInSelection
-        'log-cursor-scope': @logCurrsorScope
+        'log-cursor-scope': @logCursorScope

     for name, method of editorBindings
       do (name, method) =>
@@ -249,8 +249,8 @@
   bufferRowsForScreenRows: (startRow, endRow) -> @activeEditSession.bufferRowsForScreenRows(startRow, endRow)
   stateForScreenRow: (row) -> @activeEditSession.stateForScreenRow(row)

-  logCurrsorScope: ->
-    console.log @activeEditSession.tokenizedBuffer.scopesForPosition(@getCursorBufferPosition)
+  logCursorScope: ->
+    console.log @activeEditSession.tokenizedBuffer.scopesForPosition(@getCursorBufferPosition())

   pageDown: ->
     newScrollTop = @scrollTop() + @scrollView[0].clientHeight