Mapping to default http code descriptions

--- src/processors/request.processor.coffee
+++ src/processors/request.processor.coffee
@@ -2,6 +2,7 @@
 Promise = require "bluebird";
 request = require "request-promise"
 { StatusCodeError, RequestError } = require "request-promise/errors"
+httpStatus = require("http").STATUS_CODES
 MESSAGE_PROPERTIES = ["reason", "error", "message"]

 _retrieveMessage = (message) ->
@@ -20,7 +21,7 @@
     .catch __isSilentError, (err) -> _.omit err, "response"
     .catch StatusCodeError, ({ statusCode, error }) ->
       throw
-        message: _(_safeParse error).pick(MESSAGE_PROPERTIES).values().map(_retrieveMessage).compact().head()
+        message: _(_safeParse error).pick(MESSAGE_PROPERTIES).values().concat(httpStatus[statusCode]).map(_retrieveMessage).compact().head()
         detail: response: { statusCode, body: error }
     .catch RequestError, ({ cause }) ->
       throw


orange buttons win

orange buttons win

--- js/ab_test_definitions.coffee
+++ js/ab_test_definitions.coffee
@@ -8,7 +8,6 @@
 exports.ab_test_definitions =
   options:
     github_warning_modal: [true, false]
-    home_red_buttons: [false]
     home_extra_ctas: [true, false]
     home_cta_plan_price_trial: [true, false] # around the home button CTA, include text about plans starting from $19, and 14 day trial
     number_one_instead_of_happy_customers_callout: [true, false]


Add messageStorage to inspector

Add messageStorage to inspector

--- src/public/scripts/app.inspector.coffee
+++ src/public/scripts/app.inspector.coffee
@@ -53,6 +53,7 @@
 		serverAddress: ':8080/'

 		nodes: []
+		messageStorage: []

 		# Constructs a new inspector app.
 		#
@@ -60,16 +61,6 @@
 			viewAngle = 45
 			nearClip = 0.1
 			farClip = 10000
-
-			@_zoom = 1
-
-			@_deltaX = 0
-			@_deltaY = 0
-			@_lastMouseX = 0
-			@_lastMouseY = 0
-			@_mouseDown = false
-
-			@_cameraAngle = new Three.Quaternion()

 			# Connect to the server.
 			@server = new Server(@, @serverAddress)


append config.title to title when backbone route event happens

--- lib/carte/client/views/content.cjsx
+++ lib/carte/client/views/content.cjsx
@@ -3,6 +3,7 @@
 List = require('./list')
 CardCollection = require('../models/cards')
 CardModel = require('../models/card')
+config = require('../../shared/config.json')

 module.exports = React.createClass
   displayName: 'Content'
@@ -33,7 +34,7 @@
             title.push(k + ': ' + v)
         title = title.join(', ')
         title = 'search: ' + cards.query.title + ' (' + title + ')' if cards.query.title
-        title += ' - carte'
+        title += ' - ' + config.title
         document.title = title
         <List key='list' cards={cards} showNav=true />
       when "show"
@@ -59,7 +60,7 @@
             cards.fetching = false
           error: (card, response)=>
             console.log response
-        document.title = card.get('title') + ' - carte'
+        document.title = card.get('title') + ' - ' + config.title
         <List key='show' cards={cards} showNav=false />
       else
         console.log 'else'


Send SIGINT instead of SIGTERM to kill execution processes

--- lib/execution-manager.coffee
+++ lib/execution-manager.coffee
@@ -75,19 +75,6 @@
         when 'darwin' then @killProcessOnDarwinAndLinux(@process.pid)
         when 'linux'  then @killProcessOnDarwinAndLinux(@process.pid)
         when 'win32'  then @killProcessOnWin32(@process.pid)
-        # else
-        #   # FIXME on OS X and Linux this only kills the sh root process but not the
-        #   # child processes (primarily the run process); therefore the close event
-        #   # will not be emitted until the run process is killed manually
-        #   # @process.kill()
-        #   message =
-        #     'Unfortunately, this functionality is not yet supported.\nAt the moment
-        #     execution can only be stopped by killing the process manually.\n \nThis
-        #     will be fixed in a future release.'
-        #   notificationUtils.addWarning message,
-        #     head: 'Sorry! This is not yet supported!'
-        #     important: true
-        #   # -----------------------------------------------------------------------

   ## Killing processes ---------------------------------------------------------

@@ -101,23 +88,17 @@
       # execSync returns an error if the process has no childs, so we ignore
       # this error here and continue with an empty childPids array
       childPids = []
-
     # recursively kill child processes
     for childPid in childPids
       @killProcessOnDarwinAndLinux(childPid)
-
     # kill parent process
-    try process.kill(parentPid)
+    try process.kill(parentPid,'SIGINT')

   # NOTE stolen from Atom's BufferedNodeProcess API (Oops!)
   killProcessOnWin32: (parentPid) ->
     try
       wmicProcess = child_process.spawn 'wmic', [
-        'process'
-        'where'
-        "(ParentProcessId=#{parentPid})"
-        'get'
-        'processid'
+        'process','where',"(ParentProcessId=#{parentPid})",'get','processid'
       ]
       out = ''
       wmicProcess.stdout.on 'data', (data) ->
@@ -126,13 +107,13 @@
         # get child process pids
         childPids = out.split(/\s+/)
                       .filter (pid) -> /^\d+$/.test(pid)
-                      .map (pid) -> parseInt(pid)
+                      .map    (pid) -> parseInt(pid)
                       .filter (pid) -> pid isnt parentPid and 0 < pid < Infinity
         # recursively kill child processes
         for childPid in childPids
           @killProcessOnWin32(childPid)
         # kill parent process
-        try process.kill(parentPid)
+        try process.kill(parentPid,'SIGINT')
     catch error
       # TODO show proper error notification here
       console.log("Spawn error")


Enable snapshot

Enable snapshot

--- src/fs-plus.coffee
+++ src/fs-plus.coffee
@@ -15,6 +15,8 @@
 #
 # [fs]: http://nodejs.org/api/fs.html
 fsPlus =
+  __esModule: false
+
   getHomeDirectory: ->
     if process.platform is 'win32'
       process.env.USERPROFILE
@@ -471,7 +473,12 @@
   # Public: Like {.resolve} but uses node's modules paths as the load paths to
   # search.
   resolveOnLoadPath: (args...) ->
-    loadPaths = Module.globalPaths.concat(module.paths)
+    modulePaths = module.paths ? [
+      path.join(process.resourcesPath, 'app', 'node_modules'),
+      path.join(process.resourcesPath, 'app.asar', 'node_modules'),
+      path.join(process.resourcesPath, 'node_modules')
+    ]
+    loadPaths = Module.globalPaths.concat(modulePaths)
     fsPlus.resolve(loadPaths..., args...)

   # Public: Finds the first file in the given path which matches the extension
@@ -541,18 +548,23 @@
   # Returns `true` if case sensitive, `false` otherwise.
   isCaseSensitive: -> not fsPlus.isCaseInsensitive()

-{statSyncNoException, lstatSyncNoException} = fs
-statSyncNoException ?= (args...) ->
-  try
-    fs.statSync(args...)
-  catch error
-    false
-
-lstatSyncNoException ?= (args...) ->
-  try
-    fs.lstatSync(args...)
-  catch error
-    false
+statSyncNoException = (args...) ->
+  if fs.statSyncNoException
+    fs.statSyncNoException(args...)
+  else
+    try
+      fs.statSync(args...)
+    catch error
+      false
+
+lstatSyncNoException = (args...) ->
+  if fs.lstatSyncNoException
+    fs.lstatSyncNoException(args...)
+  else
+    try
+      fs.lstatSync(args...)
+    catch error
+      false

 BINARY_EXTENSIONS =
   '.ds_store': true
@@ -603,8 +615,6 @@
   '.rmd':      true
   '.ron':      true

-
-
 isPathValid = (pathToCheck) ->
   pathToCheck? and typeof pathToCheck is 'string' and pathToCheck.length > 0

@@ -639,4 +649,7 @@
     oldStat.dev is newStat.dev and
     oldStat.ino is newStat.ino

-module.exports = _.extend({}, fs, fsPlus)
+module.exports = new Proxy({}, {
+  get: (target, key) ->
+    fsPlus[key] ? fs[key]
+})


'Updating for version 0.12.4dev4'

'Updating for version 0.12.4dev4'

--- bokehjs/src/coffee/version.coffee
+++ bokehjs/src/coffee/version.coffee
@@ -1 +1 @@
-export version = '0.12.4dev3'
+export version = '0.12.4dev4'


Added :frog:

Added :frog:

--- scripts/idobata-talk.coffee
+++ scripts/idobata-talk.coffee
@@ -50,7 +50,8 @@
   robot.hear /かえる|帰る/i, (msg) ->
     msg.send "はぁはぁはぁ…。き、奇遇ね、わ、私も今ちょうど帰るとこなんだけど、ぜぇぜぇ。"

-
+  robot.hear /:frog/i, (msg) ->
+    msg.send "ぴょんぴょん"
   # respond
   robot.respond /嫌い？$/i, (msg) ->
     words = [
@@ -97,4 +98,3 @@
       "私も眠くなってきたわ・・・。 (つ∀-)ｵﾔｽﾐｰ"
       "まだ仕事残ってるんじゃないの？ しょうがないから終わるまで付き合ってあげるわ… 居眠りなんてさせないんだから///"
     ]
-


almost there with half-assignments

almost there with half-assignments

--- lexer.coffee
+++ lexer.coffee
@@ -249,27 +249,32 @@
   literal_token: ->
     match: @chunk.match(OPERATOR)
     value: match and match[1]
+    space: match and match[2]
     @tag_parameters() if value and value.match(CODE)
     value ||= @chunk.substr(0, 1)
-    not_spaced: not @prev() or not @prev().spaced
+    prev_spaced: @prev() and @prev().spaced
     tag: value
     if value.match(ASSIGNMENT)
       tag: 'ASSIGN'
       @assignment_error() if include JS_FORBIDDEN, @value
     else if value is ';'
       tag: 'TERMINATOR'
-    else if value is '[' and @tag() is '?' and not_spaced
+    else if value is '[' and @tag() is '?' and not prev_spaced
       tag: 'SOAKED_INDEX_START'
       @soaked_index: true
       @tokens.pop()
     else if value is ']' and @soaked_index
       tag: 'SOAKED_INDEX_END'
       @soaked_index: false
-    else if include(CALLABLE, @tag()) and not_spaced
+    else if include(CALLABLE, @tag()) and not prev_spaced
       tag: 'CALL_START'  if value is '('
       tag: 'INDEX_START' if value is '['
-    @token tag, value
     @i += value.length
+    if space and prev_spaced and @prev()[0] is 'ASSIGN' and include HALF_ASSIGNMENTS, tag
+      last: @tokens.pop()
+      @tokens.push ["$tag=", "$tag=", last[2]]
+    else
+      @token tag, value
     true

   # Token Manipulators
@@ -462,7 +467,7 @@
 NUMBER        : /^(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i
 HEREDOC       : /^("{6}|'{6}|"{3}\n?([\s\S]*?)\n?([ \t]*)"{3}|'{3}\n?([\s\S]*?)\n?([ \t]*)'{3})/
 INTERPOLATION : /^\$([a-zA-Z_@]\w*(\.\w+)*)/
-OPERATOR      : /^([+\*&|\/\-%=<>:!?]+)/
+OPERATOR      : /^([+\*&|\/\-%=<>:!?]+)([ \t]*)/
 WHITESPACE    : /^([ \t]+)/
 COMMENT       : /^(((\n?[ \t]*)?#[^\n]*)+)/
 CODE          : /^((-|=)>)/
@@ -509,6 +514,9 @@
 # avoid an ambiguity in the grammar.
 LINE_BREAK: ['INDENT', 'OUTDENT', 'TERMINATOR']

+# Half-assignments...
+HALF_ASSIGNMENTS: ['-', '+', '/', '*', '%', '||', '&&', '?']
+
 # Conversions from CoffeeScript operators into JavaScript ones.
 CONVERSIONS: {
   'and':  '&&'


diffsync -- create an example illustrating a mutable class.

--- salvus/diffsync.coffee
+++ salvus/diffsync.coffee
@@ -25,7 +25,7 @@
 # coffee  -o node_modules -c diffsync.coffee && echo "require('diffsync').test1()" | coffee

 SIMULATE_LOSS = false
-SIMULATE_LOSS = true
+#SIMULATE_LOSS = true

 async = require('async')
 diff_match_patch = require('googlediff')  # TODO: this greatly increases the size of browserify output (unless we compress it) -- watch out.
@@ -184,6 +184,7 @@
                         @shadow = result
                         @shadow_version  += 1
                         @_apply_edits_to_live(edits.edits, cb)
+
             else
                 if edits.shadow_version < @shadow_version
                     # Packet duplication or loss: ignore -- it will sort itself out later.
@@ -203,8 +204,9 @@

 class CustomDiffSync extends DiffSync
     constructor: (opts) ->
-        #IMPORTANT: None of the custom functions below take callbacks
+        #IMPORTANT: (1) None of the custom functions below take callbacks
         # as the last argument!
+        # (2) You really, really want to define patch_in_place.  You must if doc0 = doc1 doesn't work.
         opts = defaults opts,
             id       : undefined   # anything you want; or leave empty to have a uuid randomly assigned
             doc      : required    # the starting live document
@@ -214,20 +216,25 @@
             checksum : required    # checksum(doc0) -> something simple
             patch_in_place : undefined  # patch(d, doc0) modified doc0 in place to become doc1.

+        @opts = opts
         @init(id : opts.id,  doc : opts.doc)

-        @_copy           = opts.copy
-        @_compute_edits  = opts.diff
         @_patch          = opts.patch
         @_patch_in_place = opts.patch_in_place
         @_checksum       = opts.checksum

+    _copy: (doc) =>
+        return @opts.copy(doc)
+
+    _compute_edits: (version0, version1) =>
+        return @opts.diff(version0, version1)
+
     _apply_edits: (edits, doc, cb) =>
-        cb(false, @_patch(edits, doc))
+        cb(false, @opts.patch(edits, doc))

     _apply_edits_to_live: (edits, cb) =>
-        if @_patch_in_place?
-            @_patch_in_place(edits, @live)
+        if @opts.patch_in_place?
+            @opts.patch_in_place(edits, @live)
             cb()
         else
             @_apply_edits  edits, @live, (err, result) =>
@@ -237,17 +244,29 @@
                     @live = result
                     cb()

-
-test0 = (client, server) ->
-    client.live = "sage"
-    server.live = "my\nsage"
+    _checksum: (doc) =>
+        return @opts.checksum(doc)
+
+test0 = (client, server, DocClass, Doc_equal, Doc_str) ->
+    if DocClass?
+        client.live = new DocClass("sage")
+        server.live = new DocClass("my\nsage")
+    else
+        client.live = "sage"
+        server.live = "my\nsage"
+
+    if not Doc_equal?
+        Doc_equal = (s,t) -> s==t
+    if not Doc_str?
+        Doc_print = (s) -> s
+
     status = () ->
         #console.log("------------------------")
         #console.log(misc.to_json(client.status()))
         #console.log(misc.to_json(server.status()))
         console.log("------------------------")
-        console.log("'#{client.live}'")
-        console.log("'#{server.live}'")
+        console.log("'#{Doc_str(client.live)}'")
+        console.log("'#{Doc_str(server.live)}'")

     pusher = undefined
     go = () ->
@@ -271,10 +290,14 @@
                     throw err

     go()
-    client.live += "more stuffklajsdf lasdjf lasdj flasdjf lasjdfljas dfaklsdjflkasjd flajsdflkjasdklfj"
-    server.live = 'bar' + server.live + "lkajsdfllkjasdfl jasdlfj alsdkfjas'dfjlkasdjflasjdfkljasdf"
+    if DocClass?
+        client.live = new DocClass("bar more stuffklajsdf lasdjf lasdj flasdjf lasjdfljas dfaklsdjflkasjd flajsdflkjasdklfj" + misc.uuid())
+        server.live = new DocClass("bar lkajsdfllkjasdfl jasdlfj alsdkfjasdfjlkasdjflasjdfkljasdf" + misc.uuid())
+    else
+        client.live += "more stuffklajsdf lasdjf lasdj flasdjf lasjdfljas dfaklsdjflkasjd flajsdflkjasdklfj" + misc.uuid()
+        server.live = 'bar' + server.live + "lkajsdfllkjasdfl jasdlfj alsdkfjas'dfjlkasdjflasjdfkljasdf" + misc.uuid()
     status()
-    while client.live != server.live
+    while not Doc_equal(client.live, server.live)
         status()
         go()
     status()
@@ -304,6 +327,7 @@
         console.log(misc.to_json(server.status()))

 exports.test4 = (n=1) ->
+    # Just use the standard dmp functions on strings again.
     copy      = (s) -> s
     diff      = (v0,v1)  -> dmp.patch_make(v0,v1)
     patch     = (d, doc) -> dmp.patch_apply(d, doc)[0]
@@ -331,5 +355,57 @@
             console.log(misc.to_json(server.status()))
         test0(client, server)

+exports.test6 = (n=1) ->
+    #
+    # Make the documents a mutable version of strings (defined via a class) instead.
+    #
+
+    class Doc
+        constructor: (@doc) ->
+            if @doc.doc?
+                console.log("tried to make Doc(Doc)")
+                traceback()
+            if @doc == '[object Object]'
+                console.log("tried to make Doc from obvious mistake")
+                traceback()
+            if not @doc?
+                console.log("tried to make Doc with undefined doc")
+                traceback() # cause stack trace
+            if not (typeof @doc == 'string')
+                console.log("tried to make Doc from non-string '#{misc.to_json(@doc)}'")
+                traceback()
+
+    copy      = (s) ->
+        console.log("copying: ", s.doc)
+        return new Doc(s.doc)
+    diff      = (v0,v1)  -> console.log(v0, v0.doc, v1, v1.doc); return dmp.patch_make(v0.doc, v1.doc)
+    patch     = (d, doc) -> new Doc(dmp.patch_apply(d, doc.doc)[0])
+    checksum  = (s) -> s.doc.length + 3    # +3 for luck
+    patch_in_place = (d, s) -> s.doc = dmp.patch_apply(d, s.doc)[0]  # modifies s.doc inside object
+
+    DS = (id, doc) -> return new CustomDiffSync
+        id       : id
+        doc      : doc
+        copy     : copy
+        diff     : diff
+        patch    : patch
+        patch_in_place : patch_in_place
+        checksum : checksum
+
+    for i in [0...n]
+        client = DS("client", new Doc("cat"))
+        server = DS("server", new Doc("cat"))
+
+        client.connect(server)
+        server.connect(client)
+
+        client.live = new Doc("cats")
+        server.live = new Doc("my\ncat")
+        client.sync () =>
+            console.log(misc.to_json(client.status()))
+            console.log(misc.to_json(server.status()))
+        test0(client, server, Doc, ((s,t) -> s.doc ==t.doc), ((s) -> s.doc))
+
+

 exports.DiffSync = DiffSync


Commented server game file

Commented server game file

--- server/src/Game.coffee
+++ server/src/Game.coffee
@@ -50,12 +50,11 @@

-  constructor: (@gameNumber, @name, gameSize, numRounds, throwErrors = true) ->
+  constructor: (@gameNumber, @name, gameSize, numRounds) ->
     # Initalise all variables so that they're object (not prototype) specfific

     @playerManager = new PlayerManager()

-    @throwErrors = throwErrors
     @goalTree = undefined
     @goalArray = []
     @goalValue = undefined
@@ -115,14 +114,6 @@
     @goalTree? #returns false if goalTree undefined, true otherwise

-  throwError: (errorNumber, jsonParams, errorMessage) ->
-    error = new Error(errorMessage)
-    error.number = errorNumber
-    if(jsonParams?) then error.params = jsonParams
-    error.emsg = errorMessage
-    throw error
-
-
   ###*
    * Spawns the global array. Uses a random distribution to work out the dice for the game.
   ###
@@ -278,19 +269,31 @@
   getNumPlayers: () -> @playerManager.numPlayers()

+  ###*
+   * [goalStart description]
+   * @param  {[type]} turnEndCallback [description]
+   * @return {[type]}                 [description]
+  ###
   goalStart: (turnEndCallback) ->
     @started = true
     @state.turnNumber = 0
     @resetTurnTimer(Settings.goalSeconds, turnEndCallback)

-
+  ###*
+   * For allocation turns, start with the player after goal setter.
+   * @param  {Function} turnEndCallback What to do if time ends on his turn.
+  ###
   start: (turnEndCallback) ->
     @state.currentPlayer = (@playerManager.goalSetter+1)%@getNumPlayers()
     @state.turnNumber = 1
     @resetTurnTimer(Settings.turnSeconds, turnEndCallback)

-
+  ###*
+   * For allocation turns, move to the next player.
+   * @param  {Function} turnEndCallback What to do if time ends on his turn.
+  ###
   nextTurn: (turnEndCallback) ->
+    # Only move onto the next player if the goal has been set
     if @started
       @state.currentPlayer = (@state.currentPlayer+1)%@getNumPlayers()
       @state.turnNumber += 1
@@ -321,13 +324,16 @@
     else
       clearInterval(@turnTimer)

+  # For debug purposes - pause the timer for the current turn
   pauseTurnTimer: () ->
+    # Make idempotent - only pause if game isn't already paused
     if !@pausedTime?
       @pausedTime = Date.now()
       clearInterval(@turnTimer)
     else
       console.log "Game already paused."

+  # For debug purposes - resume a paused timer
   resumeTurnTimer: () ->
     @state.turnStartTime = Date.now()
     resumeDuration = @state.turnEndTime -  @pausedTime
@@ -338,7 +344,11 @@

-
+  ###*
+   * This is called whenever the timer runs on on a turn. This looks at the current point
+   * of the game - does any code that was no in previous turn's turnEndCallback
+   * @param  {[type]} turnEndCallback (Optional) A new turnEndCallback for next turn
+  ###
   handleTimeOut: (turnEndCallback) ->
     if @started
       if @challengeMode
@@ -390,7 +400,7 @@
         return true
     return true

-
+  # Used on each allocation turn to check player if valid and check allocation move is valid.
   checkValidAllocationMove: (index, clientId) ->
     if @challengeMode # prevents moves from taking place during challenge mode
       ErrorManager.throw(ErrorManager.codes.moveDuringChallenge, {}, "Can't move during challenge mode")
@@ -449,6 +459,11 @@
     @state.impossiblePlayers.push(challengerId)
     @resetTurnTimer(Settings.challengeDecisionTurnSeconds, turnEndCallback)

+  ###*
+   * A player has submitted that he thinks a challenge is possible.
+   * @param  {Integer} clientId        The now.js socketid given to player
+   * @param  {Function} turnEndCallback What to do if the timer ends on the solutions turn.
+  ###
   submitPossible: (clientId, turnEndCallback) ->
     @checkChallengeDecision()
     agreedId = @playerManager.getPlayerIdBySocket(clientId) #The player id of the player who agreed
@@ -457,17 +472,27 @@
       # Give 40 seconds for the solutions turn
       @resetTurnTimer(Settings.submitTurnSeconds, turnEndCallback)

+  ###*
+   * A player has submitted that he thinks a challenge is not possible.
+   * @param  {Integer} clientId        The now.js socketid given to player
+   * @param  {Function} turnEndCallback What to do if the timer ends on the solutions turn.
+  ###
   submitImpossible: (clientId, turnEndCallback) ->
     @checkChallengeDecision()
     disagreedId = @playerManager.getPlayerIdBySocket(clientId) #The player id of the player who disagreed
     @state.impossiblePlayers.push(disagreedId)
+    # Once we have accepted his decision, check whether all are made.
+    # If so, and everyone thinks it's impossible then it's the end of round. Add up scores.
     if(@allDecisionsMade())
-      # Give 40 seconds for the solutions turn
       @resetTurnTimer(Settings.submitTurnSeconds, turnEndCallback)
       if(@state.possiblePlayers.length == 0)
         @scoreAdder()

-
+  ###*
+   * Check whether a possible/impossible decision make sense.
+   * Prevent people making duplicate decisions or decisions at the wrong time.
+   * @param  {Integer} clientId The now.js socketid given to player
+  ###
   checkChallengeDecision:(clientId) ->
     if !@challengeMode
       ErrorManager.throw(ErrorManager.codes.submitNotChallengeMode, {}, "Can't submit opinion, not currently challenge mode")
@@ -500,19 +525,24 @@
       numGlobalDice = @globalDice.length
       for i in [0 ... dice.length]
         for j in [i+1 ... dice.length]
+          # If he uses globalDice indices that don't make sense, give error.
+          # If he uses globalDice indices twice, then he's used two identical dice twice. He's cheated. Give error.
           if (dice[i] < -2  || dice[i] >= numGlobalDice) then ErrorManager.throw(ErrorManager.codes.outOfBoundsDice, {}, "Solution has out of bounds array index")
           if (dice[i] == dice[j] && i!=j  && dice[i] >= 0) then ErrorManager.throw(ErrorManager.codes.duplicateDice, {}, "Solution uses duplicate dice")
-
+        # -1 and -2 are pseudo-indicies reserved for left/right brackets. handle this case.
+        # otherwise add the corresponding globalDice into our temporary solutions array we are building
         if dice[i] == -1
           diceValues.push(DICEFACESYMBOLS.bracketL)
         else if dice[i] == -2
           diceValues.push(DICEFACESYMBOLS.bracketR)
         else
           diceValues.push(@globalDice[dice[i]])
+        # Now check that he has used the allocations correctly.
+        # He can't use any forbidden dice, must use all the required, and only one unallocated.
         if(dice[i] in @state.forbidden) then ErrorManager.throw(ErrorManager.codes.usesForbidden, {}, "Solution uses dice from forbidden")
         if(dice[i] in @state.required) then numRequiredInAns++
         if(dice[i] in @state.unallocated) then numUnallocatedInAns++
-
+      # Checks for using all the required and only one unallocated.
       if(numRequiredInAns < @state.required.length) then ErrorManager.throw(ErrorManager.codes.doesntUseAllRequired, {}, "Solution doesn't use all of the required dice.")
       if(numUnallocatedInAns > 1 && @challengeModeNow) then ErrorManager.throw(ErrorManager.codes.doesntUseOneUnallocated, {}, "Solution doesnt use one grey dice")

@@ -528,6 +558,7 @@
     else
       playerid = @playerManager.getPlayerIdBySocket(socketId)
       ErrorManager.throw(ErrorManager.codes.possibleSubmitSolution, {}, "Client not in 'possible' list")
+    # If everyone has submitted their solutions, it's the end of round. Add up the scores for the round.
     if(@allSolutionsSent()) then @scoreAdder()

   # returns true if all players that believe a solution is possible have sent their solution
@@ -539,14 +570,20 @@

   # Return a copy of the submitted solutions
   getSubmittedSolutions: () -> @playerManager.submittedSolutions[..]
+  # Has at least one person make a solution that solves the goal?
   getAnswerExists: () -> @answerExists
+  # Return an array that gives the points for actually making the challenge.
   getRoundChallengePoints: () -> @playerManager.challengePoints[..]
+  # Return an array that gives the points for whether they decided with challenge.
   getRoundDecisionPoints: () -> @playerManager.decisionPoints[..]
+  # Return an array that gives the points for whether solutions submitted are correct
   getRoundSolutionPoints: () -> @playerManager.solutionPoints[..]

-
+  ###*
+   * Once every one has submitted their solutions. It's the end of the round. Add up points.
+  ###
   scoreAdder: ->
     # See if we definitely know it's solvable. See if somebody got the goal.
     numPlayers = @getNumPlayers()
@@ -581,10 +618,12 @@
       if(@playerManager.solutionPoints[i]?) then @state.playerScores[i] += @playerManager.solutionPoints[i]
       if(@playerManager.challengePoints[i]?) then @state.playerScores[i] += @playerManager.challengePoints[i]

+  # A player has said that he is ready for the next round.
   readyForNextRound: (clientid) ->
     index = @playerManager.getPlayerIdBySocket(clientid)
     if(index not in @state.readyForNextRound) then @state.readyForNextRound.push(index)

+  # Returns true iff everyone has said they are ready for the next round.
   allNextRoundReady: () -> @state.readyForNextRound.length == @playerManager.players.length

   # sets up the next round in the game, resetting certain values.
@@ -614,12 +653,14 @@
     # check if it's the last round and update the current round
     if @state.currentRound > @state.numRounds then @endGame() else @state.currentRound++

-
+  # Set that the game has ended.
   endGame: ->
     @endOfGame = true

+  # Has the game ended?
   isGameOver: -> @endOfGame or @state.currentRound > @state.numRounds

+  # Restart a new round of the game.
   restartGame: ->
     @nextRound()
     @constructor(@gameNumber, @name, @gameSize, @state.numRounds, @endOfGameCallback)


KDEventEmitter: code style

KDEventEmitter: code style

I never needed edge again; it's more readable this way

--- client/Framework/core/KDEventEmitter.coffee
+++ client/Framework/core/KDEventEmitter.coffee
@@ -139,11 +139,9 @@

     listeners = []

-    edge      = edges[i]
-
     straight  = node[listenerKey]  if i is edges.length
     wild      = node[wildcardKey]
-    nextNode  = node[edge]
+    nextNode  = node[edges[i]]

     if straight?
       listeners = listeners.concat straight


remove unnecessary chaining

remove unnecessary chaining

--- client/Main/VirtualizationController.coffee
+++ client/Main/VirtualizationController.coffee
@@ -278,7 +278,6 @@
       .then (kite) =>
         @osKites[hostname] = kite

-      .then (kite) ->
         kite.connect().then -> resolve kite

   handleFetchedVms: (vms, callback) ->


Gastro Acid is passable

Gastro Acid is passable

--- server/bw/attachment.coffee
+++ server/bw/attachment.coffee
@@ -1284,6 +1284,7 @@
 # Suppresses the opponent's ability until they switch out
 class @Attachment.AbilitySuppress extends @VolatileAttachment
   name: "AbilitySuppressAttachment"
+  passable: true

   @preattach: (options, attributes) ->
     {pokemon} = attributes


IDEView: Fix missing indent.

IDEView: Fix missing indent.

--- client/ide/lib/views/tabview/ideview.coffee
+++ client/ide/lib/views/tabview/ideview.coffee
@@ -352,7 +352,7 @@
               machine :
                 uid   : file.machine.uid

-      @emitChange tailerPane, change
+        @emitChange tailerPane, change

       return tailerPane


stack-editor: minor text changes

stack-editor: minor text changes

--- client/stack-editor/lib/routehandler.coffee
+++ client/stack-editor/lib/routehandler.coffee
@@ -6,7 +6,7 @@
 module.exports = -> lazyrouter.bind 'stackeditor', (type, info, state, path, ctx) ->

   unless canCreateStacks()
-    new kd.NotificationView { title: 'You are not allowed to create stacks!' }
+    new kd.NotificationView { title: 'You are not allowed to create/edit stacks!' }
     return kd.singletons.router.back()

   kd.singletons.appManager.open 'Stackeditor', (app) ->


enhanced tokenizer

enhanced tokenizer

--- server/lib/tokenizer.coffee
+++ server/lib/tokenizer.coffee
@@ -34,12 +34,12 @@
 ## Temporal helpers
 t.addRule /^ce$/, "currentTemporal"
 t.addRule /^cette$/, "currentTemporal"
+t.addRule /^derniere$/, "lastTemporal"

 ## Years
-t.addRule /^\d{4}$/,"year"
+t.addRule /^\d{4}$/,"givenYear"
 t.addRule /^en(\d{4})$/,"givenYear"
-t.addRule /^cette(\s)annee$/,"currentYear"
-t.addRule /^l'annee(\s)derniere$/,"lastYear"
+t.addRule /^annee$/,"year"
 ## Months
 t.addRule /^janvier$/,"month"
 t.addRule /^fevrier$/,"month"
@@ -80,5 +80,7 @@
     #t.write "qui ai-je appele en 2013"
     #t.write "Quand Pierre m'a appele en juin"
     #t.write "Quand ai-je appele Pierre en juin"
-    t.write "a qui ai-je ecrit cette semaine"
+    #t.write "a qui ai-je ecrit cette semaine"
+    #t.write "a qui ai-je ecrit l'annee derniere ?"
+    t.write "qui m'a ecrit cette annee"
     t.end()


DataField: CUI.DataField.defaults.undo_and_changed_support variable.

With this its easier to support the old behaviour App wide.

--- src/base/DataField/DataField.coffee
+++ src/base/DataField/DataField.coffee
@@ -8,6 +8,9 @@
 class CUI.DataField extends CUI.DOM

 	@changed_marker_css_class: "cui-data-field-changed-marker"
+
+	@defaults:
+		undo_and_changed_support: false

 	constructor: (@opts={}) ->
 		super(@opts)
@@ -82,13 +85,13 @@
 			undo_and_changed_support:
 				check: Boolean
 			undo_support:
-				default: false
+				default: CUI.DataField.defaults.undo_and_changed_support
 				check: Boolean
 			mark_changed:
-				default: false
+				default: CUI.DataField.defaults.undo_and_changed_support
 				check: Boolean
 			check_changed:
-				default: false
+				default: CUI.DataField.defaults.undo_and_changed_support
 				check: Boolean
 			onDataChanged:
 				check: Function


Add gray as Bucket color option

Add gray as Bucket color option

--- server/models/bucket.coffee
+++ server/models/bucket.coffee
@@ -82,7 +82,7 @@
     required: yes
   color:
     type: String
-    enum: ['teal', 'purple', 'red', 'yellow', 'blue', 'orange', 'green']
+    enum: ['teal', 'purple', 'red', 'yellow', 'blue', 'orange', 'green', 'gray']
     default: 'teal'
     required: yes
   fields: [fieldSchema]


Only use IE <select> width hack in IE. Using browser detection

--- src/view/bindings/select_binding.coffee
+++ src/view/bindings/select_binding.coffee
@@ -81,6 +81,7 @@
     @selectedBindings.forEach (binding) -> binding._fireNodeChange()

   fixSelectElementWidth: ->
+    return if window.navigator.userAgent.toLowerCase().indexOf('msie') is -1 # I. Hate. Everything.
     clearTimeout(@_fixWidthTimeout) if @_fixWidthTimeout

     @_fixWidthTimeout = setTimeout =>
@@ -89,8 +90,12 @@
     , 100

   _fixSelectElementWidth: ->
+    # There is a nasty bug in IE where select elements never reflow themselves (like ever),
+    # until there is mouse interaction with them. This is a fix for select elements which
+    # have their options set after they are rendered. They won't ever show their width without it.
     style = @get('node')?.style
     return if not style

+    previousWidth = @get('node').currentStyle.width
     style.width = '100%'
-    style.width = ''
+    style.width = previousWidth ? ''


Fixed filename input not closing when user clicks in document [#127301439]

CODAP eats the click events in the main workspace which cause the blur event not to be triggered for the filename rename input element.  This adds a global window listener to simulate the blur event.

--- src/code/views/menu-bar-view.coffee
+++ src/code/views/menu-bar-view.coffee
@@ -8,6 +8,10 @@
   displayName: 'MenuBar'

   componentWillMount: ->
+    # need to use direct DOM events because the event needs to be captured
+    if window.addEventListener
+      window.addEventListener 'click', @checkBlur, true
+
     @props.client._ui.listen (event) =>
       switch event.type
         when 'editInitialFilename'
@@ -15,6 +19,10 @@
             editingFilename: true
             editingInitialFilename: true
           setTimeout (=> @focusFilename()), 10
+
+  componentWillUnmount: ->
+    if window.removeEventListener
+      window.removeEventListener 'click', @checkBlur, true

   getFilename: (props) ->
     if props.filename?.length > 0 then props.filename else (tr "~MENUBAR.UNTITLED_DOCUMENT")
@@ -87,13 +95,17 @@
   help: ->
     window.open @props.options.help, '_blank'

+  # CODAP eats the click events in the main workspace which causes the blur event not to fire so we need to check for a non-bubbling global click event when editing
+  checkBlur: (e) ->
+    @filenameBlurred() if @state.editingFilename and e.target isnt @filename()
+
   render: ->
     (div {className: 'menu-bar'},
       (div {className: 'menu-bar-left'},
         (Dropdown {items: @props.items})
         if @state.editingFilename
           (div {className:'menu-bar-content-filename'},
-            (input {ref: 'filename', value: @state.editableFilename, onChange: @filenameChanged, onBlur: @filenameBlurred, onKeyDown: @watchForEnter})
+            (input {ref: 'filename', value: @state.editableFilename, onChange: @filenameChanged, onKeyDown: @watchForEnter})
           )
         else
           (div {className:'menu-bar-content-filename', onClick: @filenameClicked}, @state.filename)


Add JSDoc comments to all methods + Minor refactorings

--- lib/text-manipulation.coffee
+++ lib/text-manipulation.coffee
@@ -1,3 +1,9 @@
+###*
+ * Classes and methods for analyzing and manipulating list elements.
+ *
+ * @module TextManipulation
+###
+
 {Range} = require 'atom'
 _ = require 'underscore-plus'

@@ -10,6 +16,11 @@
   strippedElement: ''    # element string without whitespace
   trailingWhitespace: ''

+  ###*
+   * @param  {string}   bufferText - Buffer text that contains the list element
+   * @param  {number[]} range      - Index range in bufferText: [start, end]
+   * @constructs ListElement
+  ###
   constructor: (bufferText, range) ->
     [@start, @end] = range
     element = bufferText.slice @start, @end
@@ -22,7 +33,9 @@
     @eltEnd = @end - @trailingWhitespace.length
     @strippedElement = bufferText.slice @eltStart, @eltEnd

-
+  ###*
+   * @return {string}
+  ###
   show: ->
     'ListElement: [' + @start + ' - ' + @end + '> ' +
       'stripped: [' + @eltStart + ' - ' + @eltEnd + '> : ' +
@@ -38,7 +51,14 @@
   separator: null           # for lists with more than 1 element: separator & whitespace: {leadingWhitespace: string, sepChar: string, trailingWhitespace: string}
   elts: []

-  constructor:  (bufferText, {listRange: [listStartIx,listEndIx], nonNestedRanges: nonNestedIxRanges}) ->
+  ###*
+   * @param  {string}     bufferText        - Buffer text that contains the element list.
+   * @param  {number[]}   listRange         - Range ([startIx, endIx]) for the contents of the list, not including the brackets.
+   * @param  {number[][]} nonNestedIxRanges - List of ranges for those parts of the list that are not nested sublists.
+   * @constructs ElementList
+  ###
+  constructor:  (bufferText, listRange, nonNestedIxRanges) ->
+    [listStartIx,listEndIx] = listRange
     @startIx = listStartIx
     @endIx  = listEndIx
     @openBracket = bufferText[listStartIx-1]
@@ -51,9 +71,9 @@
     listEltsStartIx = listStartIx + @initialWhitespace.length
     listEltsEndIx   = listEndIx   - @finalWhitespace.length
     elementRanges = TextManipulation.getElementRangesFromNonNested bufferText, listEltsStartIx, listEltsEndIx, nonNestedIxRanges
-    # TextManipulation.showIxRanges bufferText, elementRanges
-
-    # Computing the layout for all elements is a bit overkill, but can be optimized later, if necessary.
+    # TextManipulation.logIxRanges bufferText, elementRanges
+
+    # OPTIMIZE: Computing the layout for all elements is a bit overkill, but can be optimized later, if necessary.
     @elts =
         _.map elementRanges, (r) ->
           new ListElement(bufferText, r)
@@ -67,10 +87,19 @@
         sepTrailingWhitespace = @elts[1].leadingWhitespace
         {leadingWhitespace: sepLeadingWhitespace, sepChar: sepChar, trailingWhitespace: sepTrailingWhitespace}

+###*
+ * Factory method for creating ElementList object.
+ *
+ * @memberof ElementList
+ * @param  {string}     bufferText   - Buffer text that contains the element list
+ * @param  {number[][]} ignoreRanges - List of ranges that should be ignored while recoginizing list elements.
+ * @param  {number[]}   ixRange      - Range corresponding to current text selection.
+ * @return {ElementList} An ElementList corresponding to the smallest list that contains ixRange, or null if ixRange is not inside a valid list.
+###
 ElementList.getElementList = (bufferText, ignoreRanges, ixRange) ->
     containingList = TextManipulation.getListContainingRange bufferText, ignoreRanges, ixRange
     if containingList?
-      new ElementList(bufferText, containingList)
+      new ElementList(bufferText, containingList.listRange, containingList.nonNestedRanges)
     else
       null

@@ -78,20 +107,33 @@
 TextManipulation = module.exports =
   ElementList: ElementList

-  # Get inner range and non-nested subranges for nearest enclosing list that contains
-  # both range start and range end (which may be at different depth levels).
+  ###*
+   * Get inner range and non-nested subranges for nearest enclosing list that contains
+   * both range start and range end (which may be at different depth levels).
+   * @param  {string}     bufferText   - Buffer text that contains the element list
+   * @param  {number[][]} ignoreRanges - List of ranges that should be ignored while recoginizing list elements.
+   * @param  {number[]}   range        - Range ([startIx, endIx]) corresponding to current text selection.
+   * @return {{ listRange: number[], nonNestedRanges:number[][]}}
+  ###
   getListContainingRange: (bufferText, ignoreRanges, range) ->
     leftIx = rightIx = range[0]
+    # Starting at the range start, repeatedly take enclosing lists, until the range end is also included or we arrive at the document bounds
     loop
-      # Repeatedly take enclosing lists, until the range is included or we arrive at the document bounds
       list = @getEnclosingList bufferText, ignoreRanges, leftIx, rightIx
       break unless list? and list.listRange[0] > 0 and list.listRange[1] < range[1] # no need to check for end of file, because of range[1] check
       leftIx = list.listRange[0] - 1
       rightIx = list.listRange[1] + 1
     list

-  # Get inner range and non-nested subranges for nearest enclosing list that holds [start, end>
-  # PRECONDITION: [start, end> is either empty or a well-formed list
+  ###*
+   * Get inner range and non-nested subranges for nearest enclosing list that holds [start, end>.
+   * PRECONDITION: [start, end> is either empty or a well-formed list.
+   * @param  {string}     bufferText
+   * @param  {number[][]} ignoreRanges
+   * @param  {number}     start        - [description]
+   * @param  {number}     end          - [description]
+   * @return {{listRange: number[], nonNestedRanges:number[][]}}
+  ###
   getEnclosingList: (bufferText, ignoreRanges, start, end) ->
     rangesToOpen = @findMatchingOpeningBracket bufferText, ignoreRanges, start, false
     rangesToClose = @findMatchingClosingBracket bufferText, ignoreRanges, end, false
@@ -104,6 +146,16 @@
     else
       null

+  ###*
+   * Find nearest following non-nested closing bracket and non-nested ranges.
+   * @param  {string}     bufferText
+   * @param  {number[][]} ignoreRanges
+   * @param  {number}     startIx          - Index in bufferText to start search.
+   * @param  {boolean}    isNested         - If true, this is a nested call, and no ranges are computed.
+   * @param  {string}     [openingBracket] - Optional opening bracket, causing null to be returned if a closing bracket is found that does not match.
+   * @return {{bracketIx: number, ranges: number[][]}} Index for first non-nested closing bracket, and list of ranges for those parts of the list that
+   *                                                   are not nested sublists. (Or null if startIx is not in a  well-formed list)
+  ###
   findMatchingClosingBracket: (bufferText, ignoreRanges, startIx, isNested, openingBracket) ->
     # console.log "findMatchingClosingBracket: " + startIx + (if openingBracket? then openingBracket else "any opening bracket")
     ranges = []
@@ -132,11 +184,28 @@
         @pushRange ranges, rangeStart, beforeSkipIx, isNested
         rangeStart = ix

-    return null # list not well formed, or no list)
-
+    return null # list not well formed, or no list
+
+  ###*
+   * If not isNested and the range [rangeStart,rangeEnd] is not empty, add it at the end of ranges.
+   * @param  {number[][]}  ranges
+   * @param  {number}      rangeStart
+   * @param  {number}      rangeEnd
+   * @param  {boolean}     isNested
+  ###
   pushRange: (ranges, rangeStart, rangeEnd, isNested) ->
     ranges.push [rangeStart,rangeEnd] if not isNested && rangeStart != rangeEnd

+  ###*
+   * Find nearest preceding non-nested closing bracket and non-nested ranges.
+   * @param  {string}     bufferText
+   * @param  {number[][]} ignoreRanges
+   * @param  {number}     startIx          - Index in bufferText to start search.
+   * @param  {boolean}    isNested         - If true, this is a nested call, and no ranges are computed.
+   * @param  {string}     [closingBracket] - Optional closing bracket, causing null to be returned if an opening bracket is found that does not match.
+   * @return {{bracketIx: number, ranges: number[][]}} Index for first non-nested openinging bracket, and list of ranges for those parts of the list that
+   *                                           are not nested sublists. (Or null if startIx is not in a  well-formed list)
+  ###
   findMatchingOpeningBracket: (bufferText, ignoreRanges, startIx, isNested, closingBracket) ->
     # console.log "findMatchingclosingBracket: " + startIx + ' ' + (if closingBracket? then closingBracket else "any closing bracket")
     ranges = []
@@ -167,10 +236,22 @@

     return null # list not well formed, or no list

+  ###*
+   * If not isNested and the range [rangeStart,rangeEnd] is not empty, add it at the start of ranges.
+   * @param  {number[][]} ranges
+   * @param  {number}     rangeStart
+   * @param  {number}     rangeEnd
+   * @param  {boolean}    isNested
+  ###
   unshiftRange: (ranges, rangeStart, rangeEnd, isNested) ->
     ranges.unshift [rangeStart,rangeEnd] if not isNested && rangeStart != rangeEnd

-  # Use binary search to return the element of ignoreRanges that contains targetIx, or null
+  ###*
+   * Use binary search to return the element of ignoreRanges that contains targetIx, or null
+   * @param  {number[][]} ignoreRanges
+   * @param  {number}     targetIx
+   * @return {number[]} Ignored range that contains targetIx (or null).
+  ###
   findRangeForIndex: (ignoreRanges, targetIx) ->
     startIx = 0
     endIx = ignoreRanges.length - 1
@@ -184,20 +265,41 @@
         return ignoreRanges[ix]
     null

-  # PRECONDITION: ranges do not connect (i.e. there is at least one element in between)
+  ###*
+   * If ix is inside an ignored range, return an index to the character following that ignored range, otherwise return ix.
+   * PRECONDITION: ranges do not connect (i.e. there is at least one character in between)
+   * @param  {number[][]} ignoreRanges
+   * @param  {number}     ix
+   * @return {number}
+  ###
   forwardSkipIgnored: (ignoreRanges, ix) ->
     (@findRangeForIndex ignoreRanges, ix)?[1] ? ix

-  # PRECONDITION: ranges do not connect (i.e. there is at least one element in between)
   # NOTE: for backwardSkipIgnored, ix denotes a position after the character, so it skips
   #       if the preceding character is in a skip range: e.g. backwardSkipIgnored([[1,2]],2) == 1
+  ###*
+   * If the character preceding ix is inside an ignored range, return an index to the start of the ignored range, otherwise return ix.
+   * e.g. backwardSkipIgnored([[1,3]],3) == 1
+   * PRECONDITION: ranges do not connect (i.e. there is at least one character in between)
+   * @param  {number[][]} ignoreRanges
+   * @param  {number}     ix
+   * @return {number}
+  ###
   backwardSkipIgnored: (ignoreRanges, ix) ->
     (@findRangeForIndex ignoreRanges, ix-1)?[0] ? ix

-  # Convert list of ranges that cover the entire list except its sublists, to ranges for its elements.
-  # The first separator encountered is expected to be the separator for the entire list.
-  # NOTE: nonNestedRanges may start before startIx and end after endIx, but only element ranges in the
-  #       range [startIx, endIx> will be returned.
+
+  ###*
+   * Convert list of ranges that cover the entire list except its sublists, to ranges for its elements.
+   * The first separator encountered is expected to be the separator for the entire list.
+   * NOTE: nonNestedRanges may start before startIx and end after endIx, but only element ranges in the
+   *       range [startIx, endIx> will be returned.
+   * @param  {string}     bufferText
+   * @param  {number}     startIx         - Start index of list contents (== index of opening bracket + 1)
+   * @param  {number}     endIx           - End index of list contents (== index of closing bracket, as ranges don't include end)
+   * @param  {number[][]} nonNestedRanges - List of ranges for those parts of the list that are not nested sublists.
+   * @return {number[][]} List-element index ranges
+  ###
   getElementRangesFromNonNested: (bufferText, startIx, endIx, nonNestedRanges) ->
     # Because lists with empty elements are not uncommon, we return empty element ranges for empty or whitespace-only
     # ranges between separators. If the entire list is empty, however, we return no element ranges.
@@ -226,15 +328,22 @@
     # console.log elementRanges
     elementRanges

-  # PRECONDITION: rangeStart <= rangeEnd
-  # NOTE: selection does not include end, so selection [1,2] of [a,b,c,d] = [b]
-  # TODO: Allow empty selection when in whitespace
-  #        Maybe need booleans for distinguishing "one><, two"  "one, ><two" "one>, <two"
-  #       these are empty for paste, but we may interpret them as ">one<, two" "one, >two" and ">one, two<" for select, cut, and copy.
-  #       Not only for empty selections: ">one,< two" may be more intuitive as ">one, two<"
-  #       maybe startIsBeforeSep and endIsBeforeSep? and add an expandListSelection function?
-  #       Not immediately necessary, it may even be possible that letting these selections include the extra element is confusing.
-  getSelectionForRange: (listElements, [rangeStart,rangeEnd]) ->
+  ###*
+   * Convert a buffer-text character index range to a list-element index range.
+   * PRECONDITION: rangeStart <= rangeEnd
+   * NOTE: selection does not include end, so selection [1,2] of [a,b,c,d] = [b]
+   * @param  {ListElement[]} listElements
+   * @param  {number[]}      ixRange      - A character index range ([startIx, endIx]) in the buffer text.
+   * @return {number[]} An index range ([startIx, endIx]) in the array of ListElements.
+  ###
+  getSelectionForRange: (listElements, ixRange) ->
+    # TODO: Allow empty selection when in whitespace
+    #        Maybe need booleans for distinguishing "one><, two"  "one, ><two" "one>, <two"
+    #       these are empty for paste, but we may interpret them as ">one<, two" "one, >two" and ">one, two<" for select, cut, and copy.
+    #       Not only for empty selections: ">one,< two" may be more intuitive as ">one, two<"
+    #       maybe startIsBeforeSep and endIsBeforeSep? and add an expandListSelection function?
+    #       Not immediately necessary, it may even be possible that letting these selections include the extra element is confusing.
+    [rangeStart,rangeEnd] = ixRange
     i = 0
     while i < listElements.length
       elt = listElements[i]
@@ -249,30 +358,61 @@
       i++
     return [selectionStart, i]

-  showIxRanges: (bufferText, ranges) ->
-    console.log 'showIxRanges:'
+  ###*
+   * @param  {string} bufferText
+   * @param  {number[][]} ranges
+  ###
+  logIxRanges: (bufferText, ranges) ->
+    console.log 'logIxRanges:'
     for ixRange in ranges
       console.log 'ixRange: ['+ ixRange[0] + ',' + ixRange[1] + '>:  >>' + bufferText.substr(ixRange[0], ixRange[1] - ixRange[0]) + '<<'

-  # Convert index-based range array [start, end] to row/column-based Range
+  ###*
+   * Convert index-based range array [startIx, endIx] to row/column-based Range.
+   * @param  {string}   textBuffer
+   * @param  {number[]} ixRange    - Index range: [startIx, endIx]
+   * @return {Range} Row/column based Range
+  ###
   getRangeForIxRange: (textBuffer, ixRange) ->
     new Range (textBuffer.positionForCharacterIndex ixRange[0]),
               (textBuffer.positionForCharacterIndex ixRange[1])

-  # Convert row/column-based Range to index-based range array [start, end]
+  ###*
+   * Convert row/column-based Range to index-based range array [startIx, endIx]
+   * @param  {string}   textBuffer
+   * @param  {number[]} ixRange    - Row/column based Range
+   * @return {Range} Index range: [startIx, endIx]
+  ###
   getIxRangeForRange: (textBuffer, range) ->
     [ (textBuffer.characterIndexForPosition range.start)
     , (textBuffer.characterIndexForPosition range.end) ]

+  # TODO: Create a datastructure for these so we can take them from config.cson
+  # TODO: Not exactly sure if JSdoc's verbose syntax actually makes things clearer here..
+  ###*
+   * @constant {String}
+  ###
   separatorChars: ';,'

-  # TODO: create a datastructure for these so we can take them from config.cson
+  ###*
+   * @constant {String}
+  ###
   openBracketChars:  '[{('
+  ###*
+   * @constant {String}
+  ###
   closeBracketChars: ']})'
+  ###*
+   * @constant {String}
+  ###
   defaultSepChars:   ',;,'

-  # assumes c is character
+  ###*
+   * PRECONDITION: c is a 1-character string.
+   * @param  {string} c
+   * @return {boolean}
+  ###
   isSeparator: (c) ->
     @separatorChars.indexOf(c) != -1
     # ':' is sometimes a separator, but allowing it breaks list-edit for JSON objects
@@ -285,14 +425,27 @@
   # < .. > lists also tricky, as unbalanced < and > are quite common in code.
   # Disabled for now, <> lists are not that prevalent anyway.

-  # assumes c is character
+  ###*
+   * PRECONDITION: c is a 1-character string.
+   * @param  {string} c
+   * @return {boolean}
+  ###
   isOpeningBracket: (c) ->
     @openBracketChars.indexOf(c) != -1

-  # assumes c is character
+  ###*
+   * PRECONDITION: c is a 1-character string.
+   * @param  {string} c
+   * @return {boolean}
+  ###
   isClosingBracket: (c) ->
     @closeBracketChars.indexOf(c) != -1

+  ###*
+   * PRECONDITION: openingBracket is a 1-character string.
+   * @param  {string} openingBracket
+   * @return {string}
+  ###
   getClosingBracketFor: (openingBracket) ->
     ix = @openBracketChars.indexOf(openingBracket)
     if ix >= 0
@@ -301,6 +454,11 @@
       atom.notifications.addFatalError 'List-edit: Unknown opening bracket \'' + openingBracket + '\''
       undefined

+  ###*
+   * PRECONDITION: closingBracket is a 1-character string.
+   * @param  {string} closingBracket
+   * @return {string}
+  ###
   getOpeningBracketFor: (closingBracket) ->
     ix = @closeBracketChars.indexOf(closingBracket)
     if ix >= 0
@@ -309,6 +467,11 @@
       atom.notifications.addFatalError 'List-edit: Unknown closing bracket \'' + closingBracket + '\''
       undefined

+  ###*
+   * PRECONDITION: openingBracket is a 1-character string.
+   * @param  {string} openingBracket
+   * @return {string}
+  ###
   getDefaultSeparatorFor: (openingBracket) ->
     ix = @openBracketChars.indexOf(openingBracket)
     if ix >= 0
@@ -317,8 +480,16 @@
       atom.notifications.addFatalError 'List-edit: Unknown opening bracket \'' + openingBracket + '\''
       undefined

+  ###*
+   * @param  {string} text
+   * @return {string}
+  ###
   getLeadingWhitespace: (text) ->
     (/^\s*/.exec(text) ? [''])[0]

+  ###*
+   * @param  {string} text
+   * @return {string}
+  ###
   getTrailingWhitespace: (text) ->
     (/\s*$/.exec(text) ? [''])[0]


working draft version

working draft version

--- js/main.coffee
+++ js/main.coffee
@@ -3,8 +3,6 @@

 randomCellIndices = ->
   [randomInt(4),randomInt(4)]
-
-

 buildBoard = ->
   # board = []
@@ -24,59 +22,109 @@
 generateTile = (board) ->
   value = getTileValue()
   [row, column] = randomCellIndices()
-  console.log "row: #{row} column: #{column}"
   if board[row][column] is 0
     board[row][column] = value
   else
     generateTile(board)
-  console.log "generated tile with a value of #{value}"

 move = (board, direction) ->
   newBoard = buildBoard()

   for i in [0..3]
-    if direction is 'right' or 'left'
-      row = getRow(i, board)
-      row = mergeCells(row, direction)
-      row = collapseCells(row, direction)
-      setRow(row, i, newBoard)
+    switch direction
+      when 'right', 'left'
+        row = getRow(i, board)
+        row = mergeCells(row, direction)
+        row = collapseCells(row, direction)
+        setRow(row, i, newBoard)
+      when 'up', 'down'
+        column = getColumn(i, board)
+        column = mergeCells(column, direction)
+        column = collapseCells(column, direction)
+        setColumn(column, i, newBoard)
+
+
+    # if direction is 'right' or 'left'
+    #   row = getRow(i, board)
+    #   row = mergeCells(row, direction)
+    #   row = collapseCells(row, direction)
+    #   setRow(row, i, newBoard)
+    # else if direction is 'up' or 'down'
+    #   column = getColumn(i, board)
+    #   column = mergeCells(column, direction)
+    #   column = collapseCells(column, direction)
+    #   setColumn(column, i, newBoard)
+
   newBoard

 getRow = (r, board) ->
   [board[r][0], board[r][1], board[r][2], board[r][3]]

+getColumn = (c, board) ->
+  [board[0][c], board[1][c], board[2][c], board[3][c]]
+
+# getColumn = (c, board) ->
+#   column = []
+#   for row in [0..3]
+#     column[row] = board[row][c]
+#   column
+
+# check = ->
+# a = [[2, 0, 0, 0],
+# [0, 0, 2, 0],
+# [0, 0, 0, 0],
+# [0, 0, 0, 0]]
+# console.log
+
+#harry's version
+# @getColumn = (columnNumber, board) ->
+#   column = []
+#   for row in [0..3]
+#     column[row] = board[row][columnNumber]
+#   column
+
+setColumn = (columnArray, columnNumber, board) -> #column refers to the column array, columnNumber is the column number, board is the original multidimensional array
+  for i in [0..3]
+    board[i][columnNumber] = columnArray[i]
+
+
 setRow = (row, index, board) ->
   board[index] = row

-mergeCells = (row, direction) ->
-  #to merge the cells we need to know the rows and the direction
+mergeCells = (cells, direction) ->
+  #to merge the cells we need to know the cellss and the direction
   #this is a double for loop which iterates over all the possible comparisons within the array
-  merge = (row) ->
+  merge = (cells) ->
     for a in [3...0]
       for b in [a-1..0]
-        if row[a] is 0
+        if cells[a] is 0
           break
-        else if row[a] is row[b]
-          row[a] *= 2
-          row[b] = 0
-        else if row[b] isnt 0
+        else if cells[a] is cells[b]
+          cells[a] *= 2
+          cells[b] = 0
+        else if cells[b] isnt 0
           break
-    row
-
-  if direction = 'right'
-    row = merge(row)
-  else if direction = 'left'
-    row = (merge(row.reverse())).reverse()
-  row
+    cells
+
+  switch direction
+    when 'right', 'down'
+      cells = merge(cells)
+    when 'left', 'up'
+      cells = merge(cells.reverse()).reverse()
+  cells
+
+console.log mergeCells [2,2,4,4], 'up'

 collapseCells = (row, direction) ->
   row = row.filter (x) -> x isnt 0
   zeroesToAdd = 4 - row.length
   for i in [0...zeroesToAdd]
     switch direction
-      when 'right','down' then row.unshift 0
-      when 'left','up' then row.push 0
+      when 'right', 'down'
+        row.unshift 0
+      when 'left','up'
+        row.push 0
   console.log row
   row


Make menu items consisten with sidebar tab labels. PUBDEV-450

--- src/core/components/notebook.coffee
+++ src/core/components/notebook.coffee
@@ -472,8 +472,8 @@
       menuDivider
       createMenuItem 'Toggle Sidebar', toggleSidebar
       createMenuItem 'Outline', showOutline
-      createMenuItem 'Files', showBrowser
-      createMenuItem 'Clipboard', showClipboard
+      createMenuItem 'Flows', showBrowser
+      createMenuItem 'Clips', showClipboard
       menuDivider
       createMenuItem 'Presentation Mode', switchToPresentationMode, yes
     ]


Clean up Operations.

Clean up Operations.

--- src/domain/models/Operations.coffee
+++ src/domain/models/Operations.coffee
@@ -3,15 +3,20 @@
 ItemLookupRequestParameters = require './ItemLookupRequestParameters'

 Operations =
-  ITEM_LOOKUP: 'ItemLookup'
-
   request: (operation) ->
     result = _.find @values, operation: operation
     throw "Invalid operation: #{operation}" if not result
     result

-  values:   [
-    { operation: 'ItemLookup', requestClass: ItemLookupRequest, parametersClass: ItemLookupRequestParameters }
-  ]
+# Define operation constants
+Object.defineProperty(Operations, 'ITEM_LOOKUP', value: 'ItemLookup')
+
+# Define lookup by operation (request and request parameter types)
+Object.defineProperty(Operations, 'values', value: [
+      { operation: Operations.ITEM_LOOKUP, requestClass: ItemLookupRequest, parametersClass: ItemLookupRequestParameters }
+    ]
+)
+
+Object.freeze(Operations)

 module.exports = Operations


ids for untitled editors

ids for untitled editors

--- lib/ink.coffee
+++ lib/ink.coffee
@@ -13,6 +13,11 @@
   activate: ->
     results.activate()
     Console.activate()
+
+    edId = 1
+    atom.workspace.observeTextEditors (ed) ->
+      if not ed.getPath()?
+        ed.getBuffer().inkId ?= edId++

     try
       if id = localStorage.getItem 'metrics.userId'


allow comments

allow comments

--- grammars/subl-json.cson
+++ grammars/subl-json.cson
@@ -17,4 +17,29 @@
     include: "source.json"
   }
 ]
-scopeName: "source.json.subl"+# https://github.com/clintwood/language-json-comments
+repository:
+  'comments':
+    'patterns': [
+      {
+        'match': '/{2}.*'
+        'name': 'comment.single.subl'
+      }
+      {
+        'begin': '/\\*\\*(?!/)'
+        'captures':
+          '0':
+            'name': 'punctuation.definition.comment.subl'
+        'end': '\\*/'
+        'name': 'comment.block.documentation.subl'
+      }
+      {
+        'begin': '/\\*'
+        'captures':
+          '0':
+            'name': 'punctuation.definition.comment.subl'
+        'end': '\\*/'
+        'name': 'comment.block.subl'
+      }
+    ]
+scopeName: "source.json.subl"


Fix duplicate Group Settings link in sidebar

--- components/list/KDListViewController.coffee
+++ components/list/KDListViewController.coffee
@@ -148,7 +148,6 @@

     @getListView().removeItem itemInstance, itemData, index
     @putDefaultItem @getListView().items
-    dataId = itemData.getId?()

   registerItem:(view, index)->


:aries::camel: Updated at https://danielx.net/editor/

--- main.coffee
+++ main.coffee
@@ -5,8 +5,12 @@

 gh = Github(p)

+log = console.log.bind console
+
 setTimeout ->
-  gh.api('user').then console.log.bind console
+  gh.api('orgs/distri/repos').then log
+  .then ->
+    log gh.lastRequest().getResponseHeader("Link")
 , 100

 # We want to get all the repos in distri, STRd6


fixed missing s in exports ...

fixed missing s in exports ...

--- main.coffee
+++ main.coffee
@@ -1,7 +1,7 @@
 clientTM = require './lib/clientTransformManager'
 serverTM = require './lib/serverTransformManager'

-module.export =
+module.exports =

   createServer: (port) ->
     new serverTM false, port


added a view event for when items are initially loaded

--- src/live_document_collection.coffee
+++ src/live_document_collection.coffee
@@ -38,7 +38,11 @@
     handleNotification: (document, method) ->
       console.log(document, method)
       index = @ids[document._id]
-      if method == "update"
+      if method == "load"
+        @items.push(document)
+        @ids[document._id] = @items.length - 1
+        @emit "load", document
+      else if method == "update"
         @items[index] = document
         @emit "update", document
       else if method == "insert"
@@ -50,6 +54,4 @@
         delete @ids[document._id]
         @emit "delete", document

-
-
   return LiveDocumentCollection


remove hrefTargetTop

remove hrefTargetTop

--- client/main.coffee
+++ client/main.coffee
@@ -94,8 +94,8 @@

   constructor: (opts) ->
     super
-    @publicLink = @$('.public_link a').hrefTargetTop()
-    @remoteLink = @$('#remote_link').hrefTargetTop()
+    @publicLink = @$('.public_link a')
+    @remoteLink = @$('#remote_link')
     @model.bind "change:id", => @render()
     @model.bind "initialfetch", => @render()


Check the this value of controller (before) actions.

--- test/spec/dispatcher_spec.coffee
+++ test/spec/dispatcher_spec.coffee
@@ -105,6 +105,7 @@
       loadTest1Controller ->
         for spy in [initialize, action]
           expect(spy).was.calledOnce()
+          expect(spy.firstCall.thisValue).to.be.a Test1Controller
           [passedParams, passedRoute, passedOptions] = spy.firstCall.args
           expect(passedParams).to.eql params
           expect(passedRoute).to.eql create(route1, previous: {})
@@ -452,6 +453,7 @@
         loadController ->
           expect(beforeAction).was.called()
           expect(action).was.called()
+          expect(beforeAction.firstCall.thisValue).to.be.a BeforeActionController
           expect(beforeAction.calledBefore(action)).to.be true

           beforeAction.restore()


rudimentary responsive UI for small video sizes

this code isn’t great! the ui isn’t great either.

--- client/tawk.coffee
+++ client/tawk.coffee
@@ -196,6 +196,8 @@
   gid = @props.gid
   members = tawk['group/' + gid].members or []

+  me = tawk['/connection']
+  me_in_group = me.id in (m.id for m in members)
   divSize = group_size(members.length or 1) # ghost group is size 1

   DIV
@@ -219,6 +221,9 @@

     onMouseLeave: (e) ->
       tawk['/connection'].mouseover = null
+
+    if me_in_group && tawk.dimensions.person_width < 100 # render the AV controls above the group if people are really small
+      AV_CONTROL_BAR above: true

     DIV
       style:
@@ -279,7 +284,7 @@
         width: width + 'px'
         cursor: (if person.id == me.id then 'pointer' else '')
         opacity: (if should_hear_fully(person, me) then 1.0 else 0.5)
-      if person.id == me.id
+      if person.id == me.id && width > 100
         AV_CONTROL_BAR()
       else
         AV_VIEW_BAR
@@ -386,11 +391,11 @@
   me = tawk['/connection']
   DIV
     style:
-      position: 'absolute'
-      bottom: 0
-      right: 0
+      position: if @props.above then 'relative' else 'absolute'
+      bottom: if !@props.above then 0
+      right: if !@props.above then 0
       zIndex: 100
-      textAlign: 'right'
+      textAlign: if @props.above then 'right' else 'center'

     AV_BUTTON
@@ -432,13 +437,15 @@
       right: 0
       zIndex: 100
       textAlign: 'right'
-    if not person.audio
+    if not person.audio
       AV_BUTTON
         disabled: 'disabled'
         danger: true
+        dummy: tawk.dimensions.person_width # needed with react diffing algo, otherwise child component won't get rerendered
         MIC_ICON
           on: false
-          width: 16
+          width: if tawk.dimensions.person_width > 100 then 16 else 4
+

 danger_red = '#d43f3a'
 dom.AV_BUTTON = ->


change gz mode from nap

change gz mode from nap

--- lib/nap.coffee
+++ lib/nap.coffee
@@ -74,7 +74,7 @@
   if @mode is 'production'
     fingerprint = '-' + fingerprintForPkg('js', pkg) if @mode is 'production'
     src = (@cdnUrl ? @_assetsDir) + '/' + "#{pkg}#{fingerprint ? ''}.js"
-    src += '.jgz' if gzip
+    src += '.gz' if gzip
     return "<script src='#{src}' type='text/javascript'></script>"

   expandAssetGlobs()
@@ -96,7 +96,7 @@
   if @mode is 'production'
     fingerprint = '-' + fingerprintForPkg('css', pkg) if @mode is 'production'
     src = (@cdnUrl ? @_assetsDir) + '/' + "#{pkg}#{fingerprint ? ''}.css"
-    src += '.cgz' if gzip
+    src += '.gz' if gzip
     return "<link href='#{src}' rel='stylesheet' type='text/css'>"

   expandAssetGlobs()
@@ -118,7 +118,7 @@
   if @mode is 'production'
     fingerprint = '-' + fingerprintForPkg('jst', pkg) if @mode is 'production'
     src = (@cdnUrl ? @_assetsDir) + '/' + "#{pkg}#{fingerprint ? ''}.jst.js"
-    src += '.jgz' if gzip
+    src += '.gz' if gzip
     return "<script src='#{src}' type='text/javascript'></script>"

   expandAssetGlobs()
@@ -442,7 +442,7 @@

 gzipPkg = (contents, filename, callback) =>
   file = path.join(@_outputDir, filename)
-  ext = if _str.endsWith filename, '.js' then '.jgz' else '.cgz'
+  ext = if _str.endsWith filename, '.js' then '.gz' else '.gz'
   outputFilename = file + ext
   zlib.gzip contents, (err, buf) ->
     fs.writeFile outputFilename, buf, callback


bug fix

bug fix

--- scripts/standup.coffee
+++ scripts/standup.coffee
@@ -80,12 +80,12 @@
       #do some magic here to loop through the standups and find the one for right now
       theStandup = standups.filter(standupShouldFire)
       if theStandup[0].custom_message
-         message = "#{PREPEND_MESSAGE} #{custom_message}"
+         message = "#{PREPEND_MESSAGE} #{theStandup[0].custom_message}"
       else
          message = "#{PREPEND_MESSAGE} #{_.sample(STANDUP_MESSAGES)}"
     else
       if standups[0].custom_message
-          message = "#{PREPEND_MESSAGE} #{custom_message}"
+          message = "#{PREPEND_MESSAGE} #{standups[0].custom_message}"
       else
           message = "#{PREPEND_MESSAGE} #{_.sample(STANDUP_MESSAGES)}"


First pass at random draw.

First pass at random draw.

--- scripts/tickets.coffee
+++ scripts/tickets.coffee
@@ -1,10 +1,13 @@
-# A basic raffle system using points system.
+# A basic raffle system using points system. You can start a raffle
+# at defined cost of points then close and draw a winner before reset.
+# Having a separate reset allows for reopening the raffle to draw again
+# without losing entrants.
 #
 # Command(s):
 #   hubot raffle start <cost> - Start a raffle at <cost> points to enter
-#   hubot raffle draw         - Close raffle, pick winner and reset.
+#   hubot raffle draw         - Close raffle, pick winner.
+#   hubot raffle reset        - Clear entrants for the next raffle.
 #   hubot ticket              - Enters raffle and takes <cost> points from entrant.
-#   hubot entrants            - A temp command for checking entrants in raffle.

 #
 # Author:
@@ -15,7 +18,6 @@
 tickets = []
 raffle = off
 cost = 0
-# entered = {}
 bought = []

 save = (robot) ->
@@ -35,19 +37,6 @@
             return

     # Enter an open raffle if you have enough points
-    # robot.respond /ticket$/i, (msg) ->
-    #     username = "#{msg.envelope.user.name}"
-    #     points[username] ?= 0
-    #
-    #     if points[username] >= cost and raffle is on and entered[username] is false
-    #         entered[username] = true
-    #         points[username] -= cost
-    #         bought.push(username)
-    #         save(robot)
-    #         msg.send "Test: raffle entered."
-    #         return
-
-    # Enter an open raffle if you have enough points
     robot.respond /ticket$/i, (msg) ->
         username = "#{msg.envelope.user.name}"
         points[username] ?= 0
@@ -63,22 +52,20 @@
                 msg.send "Test: Seems you're already in the raffle."
             return

-    # Check entry values for testing
-    robot.respond /entry ([a-zA-Z0-9_]*)/i, (msg) ->
-        if robot.auth.hasRole(msg.envelope.user, ['admin'])
-            username = msg.match[1].toLowerCase()
-            entry = bought.indexOf(username)
-            msg.send entry
-            return
-
     # Close raffle, drawn winner and reset.
     robot.respond /raffle draw$/i, (msg) ->
         if robot.auth.hasRole(msg.envelope.user, ['admin'])
+            winner = bought[Math.floor(Math.random() * bought.length)]
             raffle = off
-            # entered[username] = false
+            save(robot)
+            msg.send "And the winner is..." + winner + "! Congrats!! Masonest will contact you via Twitch message with details on collecting your prize!"
+            return
+
+    # Reset raffle. Can start another a draw including current entrants before reset.
+    robot.respond /raffle reset$/i, (msg) ->
+        if robot.auth.hasRole(msg.envelope.user, ['admin'])
             bought.length = 0
             save(robot)
-            msg.send "The raffle is now closed. A winner will be announced in this message once finished."
             return

     # Check entrants. Temp command for testing


Convert to actual hour count.

Convert to actual hour count.

--- scripts/watched.coffee
+++ scripts/watched.coffee
@@ -89,7 +89,7 @@

             displayScore = (list) ->
               (user, index) ->
-                index + 1 + '. ' + user + ' (' + list[user] + ')'
+                index + 1 + '. ' + user + ' (' + list[user] / 100 + ')'

             # msg.send "The top #{hourcount} users with the most hours are: #{hourscore}"
             msg.send "The top " + hourcount + " people for hours watched: " + topFive


Added another API protection test case.

Added another API protection test case.

--- test/protect_api_spec.coffee
+++ test/protect_api_spec.coffee
@@ -445,3 +445,24 @@
     expect(guys[0].breaths).toEqual 2 * guys[1].first.length
     expect(guys[1].breaths).toEqual 2 * guys[0].first.length
     expect(movies).toEqual 2 * guys[0].last.length + 2 * guys[1].last.length
+
+  it 'should let you mess with your own method return values', ->
+    # https://github.com/codecombat/codecombat/issues/1100
+    code = '''
+      function makePoint (x, y) { return {x: x, y: y}; }
+      var p1 = makePoint(1, 2);
+      p1.d = 3;
+      var p2 = {x: 4, y: 5};
+      p2.d = 6;
+      return {p1: p1, p2: p2, p1s: JSON.stringify(p1), p2s: JSON.stringify(p2), p1d: p1.d, p2d: p2.d}
+    '''
+    aether = new Aether protectAPI: true
+    aether.transpile code
+    result = aether.run()
+    expect(result.p1).toEqual {x: 1, y: 2, d: 3}
+    expect(result.p2).toEqual {x: 4, y: 5, d: 6}
+    expect(result.p1s).toEqual '{"x":1,"y":2,"d":3}'
+    expect(result.p2s).toEqual '{"x":4,"y":5,"d":6}'
+    expect(result.p1d).toEqual 3
+    expect(result.p2d).toEqual 6
+    expect(aether.problems.errors).toEqual []


cistrans: Sort the gene list

cistrans: Sort the gene list

--- cistrans/cistrans.coffee
+++ cistrans/cistrans.coffee
@@ -101,7 +101,7 @@
       else
         probesByGene[gene] = [probe]

-  allgenes = (gene for gene of probesByGene)
+  allgenes = (gene for gene of probesByGene).sort()

   # calculate X and Y scales, using cM positions
   totalChrLength = 0;


mecano: fix issue after proxy update

mecano: fix issue after proxy update

--- lib/run.coffee
+++ lib/run.coffee
@@ -80,6 +80,7 @@
   middlewares = []
   parent = module: parent if typeof parent is 'string'
   plugin = false
+  # load the module unless it has an handler
   if not parent.handler or typeof parent.handler is 'string'
     absname = parent.module
     absname = path.resolve process.cwd(), parent.module if parent.module.substr(0, 1) is '.'
@@ -93,7 +94,8 @@
     middlewares.push module: parent.module, plugin: true
     commands = parent.handler.call ctx
     return unless commands
-    return if commands is ctx
+    return if commands.registry
+    # return if commands is ctx
     for command, children of commands
       # when a plugin reference another plugin, we need to filterout other
       # commands while preserving configure
@@ -112,7 +114,7 @@
         else if typeof child.handler is 'function'
           child.module = parent.module
         else
-          throw Error "Invalid handler: #{child.handler}"
+          throw Error "Invalid handler: #{child.handler}, in module #{parent.module}"
         child.command ?= command
         m = load_module(ctx, child, default_command, command)
         middlewares.push m... if m


working terminal with buffers and throttle

--- client/landing/app/Applications/Shell.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Shell.kdapplication/AppController.coffee
@@ -16,7 +16,8 @@
     shellView.registerListener KDEventTypes:'ViewClosed', listener:@, callback:@closeView
     resetRegexp = /reset\:.*?/
     @dmp = new diff_match_patch()
-    @screenHistory = [""]
+    @lastScreen = ""
+    @bufferedKeyStrokes = []

     shellView.registerListener KDEventTypes:'AdvancedSettingsFunction', listener:@, callback:(pubInst, {functionName})=>
       switch functionName
@@ -43,12 +44,10 @@
       i = _lastMessageProcessed
       for diff in (item while (item = _orderedMessages[i++])?)
         # console.log "updating screen with:",diff
-        lastScreen = @screenHistory[@screenHistory.length-1]
-        currentScreen = (@dmp.patch_apply diff,lastScreen)[0]
-        @screenHistory.push currentScreen
-        @screenHistory.shift() if @screenHistory.length = 50
+        currentScreen = (@dmp.patch_apply diff,@lastScreen)[0]
         @getView().updateScreen(currentScreen)
-      @_lastMessageProcessed = i-1
+        @lastScreen = currentScreen
+        @_lastMessageProcessed = i-1

   resetMessageCounter:->
     console.log 'message counter is reset.'
@@ -211,13 +210,23 @@
       @terminal.resize options.rows, options.cols
     catch e
       console.log "terminal.resize error #{e}"
-
+
+  sendThrottled : _.throttle ->
+    @terminal.write @bufferedKeyStrokes
+    console.log "#{@bufferedKeyStrokes.length} @bufferedKeyStrokes sent at - interval 500msec",new Date if @terminal.log
+    @bufferedKeyStrokes = []
+  ,500
+
   send: (command) ->
     # console.log "sending:"+command
-    try
-      @terminal.write command
-    catch e
-      console.log "terminal.write error : #{e}"
+    @bufferedKeyStrokes.push [command,Date.now()]
+    @sendThrottled()
+    # @terminal.write command
+
+    # try
+    # snd command
+    # catch e
+      # console.log "terminal.write error : #{e}"

 # define ()->


allowing empty bodies in try blocks and in catch blocks

--- grammar.coffee
+++ grammar.coffee
@@ -370,6 +370,7 @@
   # A catch clause names its error and runs a block of code.
   Catch: [
     o "CATCH Identifier Block",                 -> [$2, $3]
+    o "CATCH Identifier TERMINATOR",            -> [$2, new Expressions]
   ]

   # Throw an exception object.


#1026

--- grammar.coffee
+++ grammar.coffee
@@ -483,13 +483,13 @@
   IfBlock: [
     o 'IF Expression Block',                    -> new If $2, $3, type: $1
     o 'IfBlock ELSE IF Expression Block',       -> $1.addElse new If $4, $5, type: $3
-    o 'IfBlock ELSE Block',                     -> $1.addElse $3
   ]

   # The full complement of *if* expressions, including postfix one-liner
   # *if* and *unless*.
   If: [
     o 'IfBlock'
+    o 'IfBlock ELSE Block',                     -> $1.addElse $3
     o 'Statement  POST_IF Expression',          -> new If $3, Expressions.wrap([$1]), type: $2, statement: true
     o 'Expression POST_IF Expression',          -> new If $3, Expressions.wrap([$1]), type: $2, statement: true
   ]


getPortInState test was implemented

getPortInState test was implemented

--- test/unit/common.test.coffee
+++ test/unit/common.test.coffee
@@ -31,7 +31,7 @@
   ###

   it 'should be available for phone number porting', (done) ->
-    check = new bandwidth.LNPAvailabilityCheck("+19193499473")
+    check = new bandwidth.LNPAvailabilityCheck("+19193499479")
     client.checkPortInAvailability check, (err, res) ->
       should.not.exist(err)
       should.exist(res)
@@ -48,6 +48,14 @@
       res.available.should.be.false
       done()

+
+  it 'should get port in state', (done) ->
+    client.getPortInState "lnpin-pwjoyuxvij5xthd3tuxctdi", (err, res) ->
+      should.not.exist(err)
+      should.exist(res)
+      # "state" : "submitted" | "created" | "pending-documents" | "submitted" | "foc" | "exception" | "requested-cancel" | "cancelled" | "complete"
+      res.should.have.property 'state'
+      done()

   it 'should port in phone number', (done) ->
     data = new bandwidth.PortInData "+16882010226",


if user machines doesn't exist on page load, retrieve

--- client/ide/routes.coffee
+++ client/ide/routes.coffee
@@ -57,10 +57,10 @@
       fallback()

-  routeToLatestWorkspace = ->
+  putVMInWorkspace = (machine)->
     localStorage    = KD.getSingleton("localStorageController").storage "IDE"
     latestWorkspace = localStorage.getValue 'LatestWorkspace'
-    machine         = KD.userMachines.first
+
     machineLabel    = machine?.slug or machine?.label or ''
     workspaceSlug   = 'my-workspace'

@@ -69,6 +69,19 @@
         {machineLabel, workspaceSlug} = latestWorkspace

     KD.getSingleton('router').handleRoute "/IDE/#{machineLabel}/#{workspaceSlug}"
+
+
+  routeToLatestWorkspace = ->
+    machine = KD.userMachines.first
+
+    if machine?
+      return putVMInWorkspace machine
+
+    KD.singletons.computeController.fetchMachines (err,  machines)->
+      if err or not machines.length
+        KD.getSingleton('router').handleRoute "/Activity"
+
+      putVMInWorkspace machines.first

   KD.registerRoutes 'IDE',


Suspend /fillon tests due to OpenFisca failure

Until openfisca/openfisca-core#325 is fixed

--- test/unit/hapi/fillon.coffee
+++ test/unit/hapi/fillon.coffee
@@ -12,7 +12,7 @@
 METHOD = 'get'

-describe 'get ' + PATH, ->
+xdescribe 'get ' + PATH, ->
 	this.timeout 10 * 1000	# leave some time for slow connections and slow OpenFisca server

 	server = null


Collaboration: Fixed collaboration settings for host user.

--- client/ide/routes.coffee
+++ client/ide/routes.coffee
@@ -52,7 +52,7 @@
         app.mountedMachineUId   = machineUId
         app.workspaceData       = workspace

-        if username
+        if username and username isnt KD.nick()
           app.isInSession       = yes
           app.amIHost           = no
           app.collaborationHost = username


Also check constructor without params.

Also check constructor without params.

--- src/spec/mapping.spec.coffee
+++ src/spec/mapping.spec.coffee
@@ -11,6 +11,7 @@

   describe '#constructor', ->
     it 'should initialize', ->
+      expect(-> new Mapping()).toBeDefined()
       expect(@map).toBeDefined()

   describe '#mapLocalizedAttrib', ->


VideoStation/rater

VideoStation/rater

--- src/syno/VideoStation.coffee
+++ src/syno/VideoStation.coffee
@@ -257,6 +257,8 @@
     # setimage

     # SYNO.VideoStation.Rating (VideoStation/rater.cgi)
+    # TODO
+    # updateinfo

     getRatingInfo: (params, done)->
         @requestAPI {
@@ -294,10 +296,6 @@
                 path: 'VideoStation/subtitle.cgi'
                 method: 'list'
         }
-
-    # SYNO.VideoStation.Rating (VideoStation/rater.cgi)
-    # TODO
-    # updateinfo

     # SYNO.VideoStation.AudioTrack (VideoStation/audiotrack.cgi)


Remove container on destroy

Remove container on destroy

--- maze.coffee
+++ maze.coffee
@@ -203,12 +203,12 @@
     })

     # make container
-    container = $('

').css({
+    @container = $('

').css({
         position: 'absolute'
         top: (@here.height - @above.height + 71) / 2
         left: (@here.width - @above.width + 71) / 2
     })
-    frame.append(container)
+    frame.append(@container)

     # make invisible grid on which the pawn moves
     grid = $('

').css({
@@ -219,7 +219,7 @@
         left: @here.left
         zIndex: 10
     })
-    container.append(grid)
+    @container.append(grid)

     # make pawn
     @pawn = $('<canvas id=pawn width=70 height=70></canvas>').css({
@@ -295,7 +295,7 @@
             context.lineWidth = 2
             context.stroke()
       @floors[z] = floor
-      container.append(floor)
+      @container.append(floor)
       floor.css({
           position: 'absolute'
       })
@@ -335,6 +335,7 @@

   destroy: ->
     $(document).off('keyup')
+    @container.remove()

   whenIdle: (callback) =>
     @queue.push(callback)


Updates the iis_site  snippet

Updates the iis_site  snippet

--- snippets/iis_site.cson
+++ snippets/iis_site.cson
@@ -1,4 +1,4 @@
 '.source.inspec':
   'Describe iis_site':
     'prefix': 'iis_site'
-    'body': "# http://inspec.io/docs/reference/resources/iis_site/\ndescribe iis_site('${1:name}') do\n  it { should${2:_not} ${3:exist} }${4}\nend"
+    'body': "# http://inspec.io/docs/reference/resources/iis_site/\ndescribe iis_site('${1:Default Web Site}') do\n  it { should${2:_not} ${3:exist} }\n  it { should${4:_not} be_running }\n  it { should have_app_pool('${5:DefaultAppPool}') }\nend"


expose getcustomertoken

expose getcustomertoken

--- src/api.coffee
+++ src/api.coffee
@@ -57,6 +57,9 @@
   setCustomerToken: (key) ->
     @client.setCustomerToken key

+  getCustomerToken: () ->
+    @client.getCustomerToken()
+
   deleteCustomerToken: ->
     @client.deleteCustomerToken()


Add language to html element for accessibility.

--- src/app.coffee
+++ src/app.coffee
@@ -718,6 +718,7 @@

     # We wait for p13n/i18next to finish loading before firing up the UI
     $.when(p13n.deferred).done ->
+        $('html').attr 'lang', p13n.getLanguage()
         app.start()
         if isFrontPage() and p13n.get('first_visit')
             $('body').addClass 'landing'


Implement security input filtering

Implement security input filtering

--- lib/app.coffee
+++ lib/app.coffee
@@ -125,7 +125,11 @@
     parameters.push("--flags-all=" + query.flagsAll)
   if query.additionalParametersEnabled and query.additionalParameters
     parameters.push(query.additionalParameters)
-  parameters.join(" ")
+  string = parameters.join(" ")
+  for excludedParameter in ["--python-expr", "--python-file", "--pmap", "--dynamic-library", "--tuple-file", "--tuple-fields", "--tuple-direction", "--tuple-dilimter", "--all-destination", "--fail-destination", "--pass-destination", "--print-statistics", "--print-volume-statistics", "--xargs"]
+    string = string.replace(excludedParameter, "")
+  string = string.replace(/[^\s\=\-\/\,\.\:0-9a-z]/gi, "")
+  string

 share.isDebug = Meteor.settings.public.isDebug


Adds reset menu for off screen when clicking the document

--- coffee/off-screen.coffee
+++ coffee/off-screen.coffee
@@ -216,6 +216,9 @@
         plugin[_].apply plugin, args

   $('.off-screen-navigation').offScreen()
+  $(document).click ->
+    $('.off-screen-navigation').offScreen('resetMenu')
+

   Furatto.OffScreen.version = "1.0.0"


Check the total

Check the total

--- src/bgg.coffee
+++ src/bgg.coffee
@@ -31,9 +31,8 @@
       type: 'boardgame'
     bgg('search', params)
       .then (results) ->
-        console.log results
-        items = results.items.item
-        if items.length
+        if results.items.total > 0
+          items = results.items.item
           for item in items
             do (item) ->
               msg.send "http://www.boardgamegeek.com/#{item.type}/#{item.id}"


opinion filters for bitcoinfoundation

opinion filters for bitcoinfoundation

--- @client/customizations.coffee
+++ @client/customizations.coffee
@@ -2337,15 +2337,34 @@

 customizations['bitcoinfoundation'] =

-  auth:
-    user_questions : [
-      {
-        tag: 'bitcoin_foundation_member.editable'
-        question: 'I am a member of the Bitcoin Foundation'
-        input: 'dropdown'
-        options:['No', 'Yes']
-        required: true
-      }]
+  opinion_filters: [ {
+      label: 'members'
+      tooltip: 'Verified member of the Second Foundation.'
+      pass: (user) -> passes_tags(user, 'second_foundation_member')
+      icon: "\u2713 member"
+
+    }, {
+      label: 'miners'
+      tooltip: 'Controls > 1% hashrate.'
+      pass: (user) -> passes_tags(user, ['bitcoin_large_miner', 'verified'])
+      icon: "\u26CF miner"
+    }, {
+      label: 'developers'
+      tooltip: 'Self reported in user profile.'
+      pass: (user) -> passes_tags(user, ['bitcoin_developer.editable', 'verified'])
+      icon: " developer"
+    }
+  ]
+
+  # auth:
+  #   user_questions : [
+  #     {
+  #       tag: 'bitcoin_foundation_member.editable'
+  #       question: 'I am a member of the Bitcoin Foundation'
+  #       input: 'dropdown'
+  #       options:['No', 'Yes']
+  #       required: true
+  #     }]
   civility_pledge: true

   # default proposal options


simplify parser also add debugs and some documentation

--- lib/esl.coffee
+++ lib/esl.coffee
@@ -92,6 +92,9 @@
 # the presence and length of the body.

 parse_header_text = (header_text) ->
+  if exports.debug
+    util.log "parse_header_text(#{header_text})"
+
   header_lines = header_text.split("\n")
   headers = {}
   for line in header_lines
@@ -114,25 +117,24 @@
 # "socket" application command).
 class eslParser
   constructor: (@socket) ->
-    @body_left = 0
+    @body_length = 0
     @buffer = ""

   # When capturing the body, buffer contains the current data
-  # (text), and body_left contains how many bytes are left to
+  # (text), and body_length contains how many bytes are expected to
   # be read in the body.
   capture_body: (data) ->
+    @buffer += data
+
     # As long as the whole body hasn't been received, keep
     # adding the new data into the buffer.
-    if data.length < @body_left
-      @buffer    += data
-      @body_left -= data.length
+    if @buffer.length < @body_length
       return

     # Consume the body once it has been fully received.
-    body = @buffer + data.substring(0,@body_left)
-    extra = data.substring(@body_left)
-    @body_left = 0
-    @buffer = ""
+    body = @buffer.substring(0,@body_length)
+    @buffer = @buffer.substring(@body_length)
+    @body_length = 0

     # Process the content
     @process @headers, body
@@ -140,45 +142,46 @@

     # Re-parse whatever data was left after the body was
     # fully consumed.
-    @capture_headers extra
+    @capture_headers ''

   # Capture headers, meaning up to the first blank line.
   capture_headers: (data) ->
-    header_end = data.indexOf("\n\n")
+    @buffer += data

     # Wait until we reach the end of the header.
+    header_end = @buffer.indexOf("\n\n")
     if header_end < 0
-      @buffer += data
       return

     # Consume the headers
-    header_text = @buffer + data.substring(0,header_end)
-    extra = data.substring(header_end+2)
-    @buffer = ""
+    header_text = @buffer.substring(0,header_end)
+    @buffer = @buffer.substring(header_end+2)

     # Parse the header lines
     @headers = parse_header_text(header_text)

     # Figure out whether a body is expected
     if @headers["Content-Length"]
-      @body_left = @headers["Content-Length"]
-      # Re-parse (and eventually process)
-      @capture_body extra
+      @body_length = @headers["Content-Length"]
+      # Parse the body (and eventually process)
+      @capture_body ''

     else
       # Process the (header-only) content
       @process @headers
       @headers = {}

-      # Re-parse whatever data was left after the headers
+      # Re-parse whatever data was left after these headers
       # were fully consumed.
-      @capture_headers extra
+      @capture_headers ''

   # Dispatch incoming data into the header or body parsers.
   on_data: (data) ->
+    if exports.debug
+      util.log "on_data(#{data})"

     # Capture the body as needed
-    if @body_left > 0
+    if @body_length > 0
       return @capture_body data
     else
       return @capture_headers data
@@ -186,6 +189,10 @@
   # For completeness provide an on_end() method.
   # TODO: it probably should make sure the buffer is empty?
   on_end: () ->
+    if exports.debug
+      util.log "Parser: end of stream"
+      if @buffer.length > 0
+        util.log "Buffer is not empty, left over: #{@buffer}"

 #### ESL request
 class eslRequest


Reorder pattern-matching rules by relevance

Bold and italicised strings are unlikely to be referenced by rules which
aren't related to repeating font macros. Similarly, because "#params" is
currently only used by heading-request rules, it feels sensible to group
them together that way.


Async `__wait` helper

Async `__wait` helper

--- helpers.coffee
+++ helpers.coffee
@@ -21,6 +21,6 @@
 @Future = Npm.require 'fibers/future'
 @__wait = (cb) ->
   fut = new Future()
-  cb fut
+  process.nextTick -> bound -> cb fut
   return fut.wait()


Add spec for checking file extension on only the URL path.

--- spec/imgflo-url.coffee
+++ spec/imgflo-url.coffee
@@ -137,6 +137,22 @@
         expect(url).to.equal "https://imgflo.herokuapp.com/graph/#{process.env.IMGFLO_KEY}/#{process.env.IMGFLO_TOKEN_01}/gradientmap.jpg?input=https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FBlM0d2-CcAAT9ic.jpg%3Alarge&color1=%230A2A2F&color2=%23FDE7A0&srgb=true"

+      context 'with a url with query parameters', ->
+
+        it 'should produce the correct URL', ->
+          config =
+            server: 'https://imgflo.herokuapp.com/'
+            key: process.env.IMGFLO_KEY
+            secret: process.env.IMGFLO_SECRET
+
+          params =
+            input: 'https://v.cdn.vine.co/r/videos/B5B06468B91176403722801139712_342c9a1c624.1.5.15775156368984795444.mp4.jpg?versionId=edU_LrAtIFsGvZj.Fgi0Si1bem68tBlk'
+
+          url = imgflo config, 'passthrough', params
+
+          expect(url).to.equal "https://imgflo.herokuapp.com/graph/#{process.env.IMGFLO_KEY}/#{process.env.IMGFLO_TOKEN_03}/passthrough.jpg?input=https%3A%2F%2Fv.cdn.vine.co%2Fr%2Fvideos%2FB5B06468B91176403722801139712_342c9a1c624.1.5.15775156368984795444.mp4.jpg%3FversionId%3DedU_LrAtIFsGvZj.Fgi0Si1bem68tBlk"
+
+
     context 'specifying an image format', ->

       it 'should produce the correct URL', ->


Use atom.workspace.openSync

Use atom.workspace.openSync

--- spec/latex-spec.coffee
+++ spec/latex-spec.coffee
@@ -40,7 +40,7 @@

   describe "build", ->
     it "does nothing for new, unsaved files", ->
-      editor = atom.workspaceView.openSync()
+      editor = atom.workspace.openSync()
       fakeInvoked = false
       fake = -> fakeInvoked = true

@@ -50,7 +50,7 @@
       expect(fakeInvoked).toEqual(false)

     it "runs `latexmk` for existing files", ->
-      editor = atom.workspaceView.openSync("file.tex")
+      editor = atom.workspace.openSync("file.tex")
       [exitCode, done] = []

       spyOn(latex, "showResult").andCallFake -> done = true
@@ -60,7 +60,7 @@
       runs -> expect(latex.showResult).toHaveBeenCalled();

     it "saves the file before building, if modified", ->
-      editor = atom.workspaceView.openSync("file.tex")
+      editor = atom.workspace.openSync("file.tex")

       editor.moveCursorToBottom()
       editor.insertNewline()
@@ -69,7 +69,7 @@
       expect(editor.isModified()).toEqual(false)

     it "supports paths containing spaces", ->
-      editor = atom.workspaceView.openSync("filename with spaces.tex")
+      editor = atom.workspace.openSync("filename with spaces.tex")
       [exitCode, done] = []

       spyOn(latex, "showResult").andCallFake -> done = true


utils: set default async value to true for doXhrRequest function

--- client/Main/utils.coffee
+++ client/Main/utils.coffee
@@ -1030,12 +1030,14 @@
     {type, endPoint, data, async} = options
     type = 'POST'  unless type

+    async or= yes
+
     return callback {message: "endPoint not set"}  unless endPoint

     xhr = new XMLHttpRequest()
     xhr.open type, endPoint, async
     xhr.setRequestHeader "Content-Type", "application/json;"
-    xhr.onreadystatechange = (result) =>
+    xhr.onload = (result) =>
       try
         response = JSON.parse xhr.responseText
       catch e


Basic support for local links (#494)

Basic support for local links (#494)

--- client/messagePDF.coffee
+++ client/messagePDF.coffee
@@ -109,13 +109,34 @@
               for annotation in annotations
                 return unless annotation.subtype == 'Link'
                 return unless annotation.rect
-                return unless annotation.url
+                return unless annotation.url or annotation.dest
                 anchor = document.createElement 'a'
-                anchor.href = annotation.url
                 anchor.style.left = "#{annotation.rect[0]}px"
                 anchor.style.top = "#{annotation.rect[1]}px"
                 anchor.style.width = "#{annotation.rect[2] - annotation.rect[0]}px"
                 anchor.style.height = "#{annotation.rect[3] - annotation.rect[1]}px"
+                anchor.href = annotation.url or '#'
+                if annotation.url  # open URL links in new tab
+                  anchor.target = '_blank'
+                  anchor.rel = 'noopener noreferrer'
+                else if annotation.dest  # local link
+                  ## Refer to https://github.com/mozilla/pdf.js/blob/master/web/pdf_link_service.js goToDestination & _goToDestinationHelper
+                  do (dest = annotation.dest) =>
+                    anchor.addEventListener 'click', (e) =>
+                      e.preventDefault()
+                      pdf.getDestination dest
+                      .then (explicit) =>
+                        unless Array.isArray explicit
+                          return console.error "Invalid link destination #{dest} -> #{explicit}"
+                        if Number.isInteger explicit[0]
+                          @page.set explicit[0] + 1
+                          @renderPage?()
+                        else
+                          pdf.getPageIndex explicit[0]
+                          .then (pageNum) =>
+                            @page.set pageNum + 1
+                            @renderPage?()
+                            ## x,y coordinates given by explicit[1..4]: https://github.com/mozilla/pdf.js/blob/master/web/base_viewer.js scrollPageIntoView
                 annotationsDiv.appendChild anchor
         @renderPage()


draw message time as title. close #22

draw message time as title. close #22

--- src/ui/views/messages.coffee
+++ src/ui/views/messages.coffee
@@ -111,7 +111,8 @@
 drawMessage = (e, entity) ->
     mclz = ['message']
     mclz.push c for c in MESSAGE_CLASSES when e[c]?
-    div id:e.event_id, key:e.event_id, class:mclz.join(' '), ->
+    title = if e.timestamp then moment(e.timestamp / 1000).calendar() else null
+    div id:e.event_id, key:e.event_id, class:mclz.join(' '), title:title, ->
         if e.chat_message
             content = e.chat_message?.message_content
             format content
@@ -174,7 +175,7 @@
                             ifpass(f.strikethrough, s) ->
                                 ifpass(imageUrl, div) ->
                                     # preload returns whether the image
-                                    # has been loaded. redraw when it
+                                    # has been loaded. redraw when it
                                     # loads.
                                     if (imageUrl) and (preload imageUrl)
                                         img src: imageUrl


use i18n

use i18n

--- src/ui/views/messages.coffee
+++ src/ui/views/messages.coffee
@@ -72,7 +72,7 @@

     # Showing message with 3 second delay showing the user that something is happening
     notr {
-      html: "Opening the link in the browser..."
+      html: i18n.__ 'menu.help.about.title:Opening the link in the browser...'
       stay: 3000
     }


Store collection type in request

Store collection type in request

--- coffee/collection.coffee
+++ coffee/collection.coffee
@@ -8,6 +8,8 @@
   if collection not in ['turer', 'steder', 'grupper', 'områder', 'bilder', 'aktiviteter']
     return res.json 404,
       message: 'Objekttype ikke funnet'
+
+  req.type = collection

   if collections[collection]
     req.col = collections[collection]


fix: don't allow negative hide delay

fix: don't allow negative hide delay

--- lib/autohide-tree-view.coffee
+++ lib/autohide-tree-view.coffee
@@ -18,8 +18,7 @@

   calculateHideDuration = (hideDelay) ->
     hideDelay ?= atom.config.get 'autohide-tree-view.hideDelay'
-    openDelay = atom.config.get 'autohide-tree-view.openDelay'
-    hideDuration = if hideDelay is 0 then 0 else .05 + hideDelay - openDelay
+    hideDuration = if hideDelay is 0 then 0 else .05 + Math.max 0, hideDelay - atom.config.get 'autohide-tree-view.openDelay'

   config:
     unfoldSpeed:
@@ -27,7 +26,6 @@
       type: 'number'
       default: 1
       minimum: 1
-      maximum: 50
     hideDelay:
       description: 'Rough estimation of the delay - in seconds - before the menu starts hiding, 0 turns of the animation entirely (0-INFINITY)'
       type: 'number'


more spanish subdomains

more spanish subdomains

Former-commit-id: 43544d32a91cfedc4126ca611020d227c0ca8cf3 [formerly d09182953baa80947c0372dba17fc66776efd572] [formerly a3ea49b230bb1be0e99cc0152a212eeb9ce13e74 [formerly 6378f55d24d0a5a5156158d6bba7d1a540389ac0]] [formerly eee8f4aeb4245fe08cbacd4dbb0d3b11d9e9e55c [formerly 37a9e98a45302a6cbbd922ca0eed9bfad7790b4d] [formerly 8c1f0392a0049e897e59b65cb35b0a2ead866901 [formerly 8c1f0392a0049e897e59b65cb35b0a2ead866901 [formerly d8274e12aebf70638b8fbbc4d933c7367c3b7c4b]]]]
Former-commit-id: 843b5ed67c40aec7d4e3eabd1ce3f9daf655698f [formerly 2ccf94ceb55e186888b74c5008c4a46815b0acc6] [formerly a1c1c5c58c9c268b4bc3dffa8bf4c083978ef8c8 [formerly 2fa81c4dc8112ceb4fd7a71b1cebf2dd1811aaba]]
Former-commit-id: 19b064504f37ec0eab5a1282b6bf7e100320437c [formerly c2ca7ad36fdeeee115ce78a05976d7c87e280734]
Former-commit-id: 3ad86028716a745e025ec069224fbe3b3e4e4e79
Former-commit-id: adece8d8c55016d2dacc34b9ca897a62c160ef59

--- @client/customizations.coffee
+++ @client/customizations.coffee
@@ -558,7 +558,7 @@

 spanish = ['alcala', 'villagb', 'citysens', 'iniciativasciudadanas', \
-           'movilidadcdmx']
+           'movilidadcdmx', 'zonaq', 'valenciaencomu']

 for spa in spanish
   customizations[spa] =