save serialization cache

save serialization cache

--- chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
+++ chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
@@ -1,5 +1,6 @@
 Components.utils.import('resource://gre/modules/Services.jsm')
 Components.utils.import('resource://gre/modules/AddonManager.jsm')
+Components.utils.import('resource://zotero/config.js')

 require('Formatter.js')
 require('lokijs.js')
@@ -138,6 +139,14 @@
       @unregister()
       Zotero.BetterBibTeX.cache.flush()
       Zotero.BetterBibTeX.keymanager.flush()
+
+      try
+        serialized = Zotero.getZoteroDirectory()
+        serialized.append('bbt-serialized-items.json')
+        serialized.remove(false) if serialized.exists()
+        Zotero.File.putContents(serialized, JSON.stringify(Zotero.Translate.ItemGetter::serialized))
+      catch
+
     return
   register: ->
     observerService = Components.classes['@mozilla.org/observer-service;1'].getService(Components.interfaces.nsIObserverService)
@@ -403,11 +412,11 @@
       return original.apply(this, arguments)
     )(Zotero.Debug.setStore)

-  # monkey-patch Zotero.ItemTreeView.prototype.getCellText to replace the 'extra' column with the citekey
+  # monkey-patch Zotero.ItemTreeView::getCellText to replace the 'extra' column with the citekey
   # I wish I didn't have to hijack the extra field, but Zotero has checks in numerous places to make sure it only
   # displays 'genuine' Zotero fields, and monkey-patching around all of those got to be way too invasive (and thus
   # fragile)
-  Zotero.ItemTreeView.prototype.getCellText = ((original) ->
+  Zotero.ItemTreeView::getCellText = ((original) ->
     return (row, column) ->
       if column.id == 'zotero-items-column-extra' && Zotero.BetterBibTeX.pref.get('showCitekeys')
         item = this._getItemAtRow(row)
@@ -419,21 +428,16 @@
           return key.citekey + (if key.citekeyFormat then ' *' else '')

       return original.apply(this, arguments)
-    )(Zotero.ItemTreeView.prototype.getCellText)
+    )(Zotero.ItemTreeView::getCellText)

   # monkey-patch translate to capture export path and auto-export
-  Zotero.Translate.Export.prototype.translate = ((original) ->
+  Zotero.Translate.Export::translate = ((original) ->
     return ->
       # requested translator
       translatorID = @translator?[0]
       translatorID = translatorID.translatorID if translatorID.translatorID
       return original.apply(this, arguments) unless translatorID

-      # detect BBT
-      for own name, bbt of Zotero.BetterBibTeX.translators
-        break if bbt.translatorID == translatorID
-        bbt = null
-
       # convert group into its library items
       if @_collection?.objectType == 'group'
         @_group = @_collection
@@ -441,7 +445,7 @@
         @_items = Zotero.Items.getAll(false, @_group.libraryID)

       # regular behavior for non-BBT translators
-      return original.apply(this, arguments) unless bbt
+      return original.apply(this, arguments) unless Zotero.BetterBibTeX.translators[translatorID]

       # export path for relative exports
       @_displayOptions.exportPath = @location.path.slice(0, -@location.leafName.length) if @location && typeof @location == 'object'
@@ -485,41 +489,60 @@
         Zotero.BetterBibTeX.auto.add(collection, @location.path, @_displayOptions)

       return original.apply(this, arguments)
-    )(Zotero.Translate.Export.prototype.translate)
+    )(Zotero.Translate.Export::translate)

   # monkey-patch _prepareTranslation to add collections for group export
   # and notify itemgetter whether we're doing exportFileData
-  Zotero.Translate.Export.prototype._prepareTranslation = ((original) ->
+  Zotero.Translate.Export::_prepareTranslation = ((original) ->
     return ->
       r = original.apply(this, arguments)
       @_itemGetter._collectionsLeft = @_group.getCollections() if @_group && @_translatorInfo?.configOptions?.getCollections
+
+      # caching shortcut sentinels
+      translatorID = @translator?[0]
+      translatorID = translatorID.translatorID if translatorID.translatorID
+      @_itemGetter._BetterBibTeX = Zotero.BetterBibTeX.translators[translatorID]
       @_itemGetter._exportFileData = @_displayOptions.exportFileData
+
       return r
-    )(Zotero.Translate.Export.prototype._prepareTranslation)
-
-  # replace Zotero.Translate.ItemGetter.prototype.nextItem to fetch from pre-serialization cache.
+    )(Zotero.Translate.Export::_prepareTranslation)
+
+  # monkey-patch Zotero.Translate.ItemGetter::nextItem to fetch from pre-serialization cache.
   # object serialization is the approx 80% of the work being done while translating!
-  Zotero.Translate.ItemGetter::nextItem = ->
-    while @_itemsLeft.length != 0
-      returnItem = @_itemsLeft.shift()
-      # export file data for single files
-      if returnItem.isAttachment()
-        # an independent attachment
-        returnItemArray = @_serialize(returnItem)
-        return returnItemArray if returnItemArray
-      else
-        returnItemArray = @_serialize(returnItem)
-        # get attachments, although only urls will be passed if exportFileData is off
-        returnItemArray.attachments = new Array
-        attachments = returnItem.getAttachments()
-        for attachmentID in attachments
-          attachmentInfo = @_serialize(attachmentID, true) || @_serialize(Zotero.Items.get(attachmentID))
-          returnItemArray.attachments.push(attachmentInfo) if attachmentInfo
-        return returnItemArray
-    return false
+  Zotero.Translate.ItemGetter::nextItem = ((original) ->
+    return ->
+      # don't mess with this unless I know it's in BBT
+      return original.apply(this, arguments) unless @_BetterBibTeX
+
+      while @_itemsLeft.length != 0
+        returnItem = @_itemsLeft.shift()
+        # export file data for single files
+        if returnItem.isAttachment()
+          # an independent attachment
+          returnItemArray = @_serialize(returnItem)
+          return returnItemArray if returnItemArray
+        else
+          returnItemArray = @_serialize(returnItem)
+          # get attachments, although only urls will be passed if exportFileData is off
+          returnItemArray.attachments = new Array
+          attachments = returnItem.getAttachments()
+          for attachmentID in attachments
+            attachmentInfo = @_serialize(attachmentID, true) || @_serialize(Zotero.Items.get(attachmentID))
+            returnItemArray.attachments.push(attachmentInfo) if attachmentInfo
+          return returnItemArray
+      return false
+    )(Zotero.Translate.ItemGetter::nextItem)

   # yes, this is defined on the object prototype, it's a shared cache
-  Zotero.Translate.ItemGetter::serialized = Object.create(null)
+  try
+    serialized = Zotero.getZoteroDirectory()
+    serialized.append('bbt-serialized-items.json')
+    Zotero.File.putContents(serialized, JSON.stringify(Zotero.Translate.ItemGetter::serialized))
+    Zotero.Translate.ItemGetter::serialized = JSON.parse(Zotero.File.getContents(serialized))
+  catch
+    Zotero.Translate.ItemGetter::serialized = {}
+  Zotero.Translate.ItemGetter::serialized = {version: ZOTERO_CONFIG.VERSION} if Zotero.Translate.ItemGetter::serialized.version != ZOTERO_CONFIG.VERSION
+
   Zotero.Translate.ItemGetter::_serialize = (item, isAttachmentID) ->
     if isAttachmentID
       itemID = (if typeof item == 'number' then item else parseInt(item))
@@ -617,7 +640,7 @@
   return

 Zotero.BetterBibTeX.removeTranslators = ->
-  for own name, header of @translators
+  for own id, header of @translators
     @removeTranslator(header)
   @translators = Object.create(null)
   Zotero.Translators.init()
@@ -830,28 +853,28 @@
       Zotero.File.getContentsFromURL("resource://zotero-better-bibtex/translators/#{translator}")
     ].join("\n")

-    @translators[header.label.toLowerCase().replace(/[^a-z]/, '')] = header
+    @translators[header.translatorID] = @translators[header.label.replace(/\s/, '')] = header
     Zotero.Translators.save(header, code)
-
-    #filename = Zotero.Translators.getFileNameFromLabel(header.label, header.translatorID)
-    #file = Zotero.getTranslatorsDirectory()
-    #file.append(filename)
-    #Zotero.File.putContents(file, JSON.stringify(header) + "\n\n" + code)
   catch err
     @log("Loading #{translator} failed", err)
   return

 Zotero.BetterBibTeX.getTranslator = (name) ->
+  return @translators[name.replace(/\s/, '')].translatorID if @translators[name.replace(/\s/, '')]
+
   name = name.toLowerCase().replace(/[^a-z]/, '')
-  translator = @translators[name]
-  translator ||= @translators["better#{name}"]
-  translator ||= @translators["zotero#{name}"]
-  throw "No translator #{name}; available: #{Object.keys(@translators).join(', ')}" unless translator
-  return translator.translatorID
+  translators = {}
+  for id, header of @translators
+    label = header.label.toLowerCase().replace(/[^a-z]/, '')
+    translators[label] = header.translatorID
+    translators[label.replace(/^zotero/, '')] = header.translatorID
+    translators[label.replace(/^better/, '')] = header.translatorID
+  return translators[name] if translators[name]
+  throw "No translator #{name}; available: #{JSON.stringify(translators)} from #{JSON.stringify(@translators)}"

 Zotero.BetterBibTeX.translatorName = (id) ->
-  for own name, tr of @translators
-    return tr.label if tr.translatorID == id
+  for own translatorID, tr of @translators
+    return tr.label if translatorID == id
   return "#{id}"

 Zotero.BetterBibTeX.safeGetAll = ->


SetCurrentUser (for Meteor...)

SetCurrentUser (for Meteor...)

--- src/client/api.coffee
+++ src/client/api.coffee
@@ -84,10 +84,14 @@
         console.error("Api not initialized yet") unless rpc
         promise = core.data.deferred()

-        onSuccess = (res, headers)->
-          setCookies(headers || {})
-          callback(res)
-          promise.resolve(res)
+        onSuccess = (res)->
+          setCurrentUser(res.headers)
+          if _.has(res, 'headers') && _.has(res, 'response')
+            ret = res.response
+          else
+            ret = res
+          callback(ret)
+          promise.resolve(ret)

         onError = (err)->
           errback(err)


Moving lexical checks to a class.

Moving lexical checks to a class.

--- src/coffeelint.coffee
+++ src/coffeelint.coffee
@@ -58,42 +58,52 @@
                 errors.push(error)
     return errors

-# Return a list of errors found by performing "lex"
-# checks on the source
-checkTokens = (source, config) ->
-    tokens = CoffeeScript.tokens(source)
-    errors = []
-    for token in tokens when not token.generated?
+
+#
+# A class that performs checks on the output of CoffeeScript's
+# lexer.
+#
+class LexicalLinter
+
+    constructor : (source, config) ->
+        @tokens = CoffeeScript.tokens(source)
+        @config = config
+        @i = 0
+
+    # Return a list of errors encountered in the given source.
+    lint : () ->
+        errors = []
+        for token in @tokens when not token.generated?
+            error = @lintToken(token)
+            errors.push(error) if error
+        errors
+
+    # Return an error if the given token fails a lint check, false
+    # otherwise.
+    lintToken : (token) ->
         [type, value, line] = token
-        check = lexChecks[type]
-        error = if check then check(config, token) else null
-        if error
-            error.line = line
-            errors.push(error)
-    return errors
+        switch type
+            when "INDENT" then @lintIndentation(token)
+            else null
+
+    # Return an error if the given indentation token is not correct.
+    lintIndentation : (token) ->
+        [type, numIndents, line] = token
+        if @config.indent and numIndents != @config.indent
+            info = "Expected: #{@config.indent} Got: #{numIndents}"
+            error = {reason: MESSAGES.INDENTATION_ERROR + info, line: line}
+        else
+            null

 # Lint the given source text with given user configuration and return a list
 # of any errors encountered.
 coffeelint.lint = (source, userConfig={}) ->
     config = defaults(userConfig)
     config.indent = 1 if config.tabs
-    checkLines(source, config).concat(checkTokens(source, config))

+    lexicalLinter = new LexicalLinter(source, config)
+    checkLines(source, config).concat(lexicalLinter.lint())

-#
-# A set of checks on lex tokens provided by the CoffeeScript lexer.
-#
-lexChecks =
-
-    INDENT : (config, token) ->
-        [type, value, line] = token
-        if config.indent and value != config.indent
-            info = " Expected: #{config.indent} Got: #{value}"
-            error = {reason: MESSAGES.INDENTATION_ERROR + info}
-        else
-            null
-
-#
 # A set of checks that should be performed on every line.
 lineChecks =


adding back ie iframe

adding back ie iframe

--- src/conekta.js.coffee
+++ src/conekta.js.coffee
@@ -27,6 +27,7 @@

       body = document.getElementsByTagName('body')[0]

+      #if ! (location.protocol == 'https:' and (navigator.userAgent.match(/MSIE/) or navigator.userAgent.match(/Trident\/7\./)))
       #fingerprinting png
       iframe = document.createElement('iframe')
       iframe.setAttribute("height", "1")


Fixed: Intermittent node/link display appears related to where you start the zoom level. I backed us out to 16 both at the time the map is created and after we center the map.

--- src/main/webapp/lib/views/MapNetworkView.coffee
+++ src/main/webapp/lib/views/MapNetworkView.coffee
@@ -70,6 +70,7 @@
   # instantiate the various elements of the network
   _drawNetwork: (network)->
     $a.map.setCenter($a.Util.getLatLng(network))
+    $a.map.setZoom($a.AppView.INITIAL_ZOOM_LEVEL)
     @_drawLinks(network)
     if network.nodes()?
       @_drawNodes network.nodes(), network


Fix Maximum delegate pay per block

Fix Maximum delegate pay per block

--- app/js/services/info.coffee
+++ app/js/services/info.coffee
@@ -28,7 +28,8 @@
                     @info.blockchain_head_block_age = data.blockchain_head_block_age
                     @info.income_per_block = data.blockchain_delegate_pay_rate
                     @info.share_supply = data.blockchain_share_supply
-                    @info.blockchain_delegate_pay_rate = data.blockchain_delegate_pay_rate
+                    @blockchain.get_asset(0).then (v)=>
+                        @info.blockchain_delegate_pay_rate = @utils.formatAsset(@utils.asset(data.blockchain_delegate_pay_rate, v))
                     @info.wallet_scan_progress = data.wallet_scan_progress
                 else
                     @info.wallet_unlocked = data.wallet_unlocked
@@ -53,10 +54,10 @@
                 @refresh_info()
         ), 2500

-    constructor: (@q, @log, @location, @growl, @common_api, @blockchain, @blockchain_api, @interval) ->
+    constructor: (@q, @log, @location, @growl, @common_api, @blockchain, @blockchain_api, @interval, @utils) ->
         @watch_for_updates()
         @blockchain.get_info().then (config) =>
             # TODO: using this have the risk of defferred object not init yet, need to make sure it is inited
             @symbol = config.symbol

-angular.module("app").service("Info", ["$q", "$log", "$location", "Growl", "CommonAPI", "Blockchain", "BlockchainAPI", "$interval", Info])
+angular.module("app").service("Info", ["$q", "$log", "$location", "Growl", "CommonAPI", "Blockchain", "BlockchainAPI", "$interval", "Utils", Info])


ignore points if they go past left margin

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -192,6 +192,7 @@
     # plot points
     for i in [0...@dataLength]
       x = ((+@data.x[i]-xMin)/(xMax-xMin)) * (@canvas.width-@leftPadding)
+      continue if x < 0
       y = ((+@data.y[i]-yMin)/(yMax-yMin)) * @canvas.height
       y = -y + @canvas.height # flip y-values
       @ctx.fillStyle = "#fff" #fc4541"


add (temporary) debug code

add (temporary) debug code

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -9,11 +9,15 @@
     @largestX  = Math.max @data.x...
     @largestY  = Math.max @data.y...

+    console.log 'ORIGINAL MIN: ', @smallestX
+
     @dataLength = Math.min @data.x.length, @data.y.length

     @originalMin = @smallestX
     for xValue, idx in [@data.x...]
       @data.x[idx] = xValue - @smallestX
+
+    console.log 'CURRENT MIN:', @data.x[0]

     # remove outliers and normalize
     @removeOutliers(nsigma=3)
@@ -199,6 +203,17 @@
       @ctx.fillRect(x+@leftPadding,y,2,2)
     if @marks
       for mark in @marks.all
+        console.log """
+                      -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+                      ----------------- DEBUG REPORT ----------------
+                                          xMin: #{xMin}
+                                          xMax: #{xMax}
+                              mark.dataXMinRel: #{mark.dataXMinRel}
+                              mark.dataXMaxRel: #{mark.dataXMaxRel}
+                                   leftPadding: #{@leftPadding}
+                     toDataXCoord(leftPadding): #{@toDataXCoord(@leftPadding)}
+                      -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+                    """
         scaledMin = ((mark.dataXMinRel + @toDataXCoord(@leftPadding) - xMin) / (xMax - xMin)) * (@canvas.width-@leftPadding)
         scaledMax = ((mark.dataXMaxRel + @toDataXCoord(@leftPadding) - xMin) / (xMax - xMin)) * (@canvas.width-@leftPadding)
         mark.element.style.width = (scaledMax-scaledMin) + "px"


bring back zoom animations

bring back zoom animations

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -275,7 +275,7 @@
     # update slider position
     classifier.el.find('#ui-slider').val(@graphCenter-@zoomRanges[@zoomLevel]/2)

-    @plotPoints(@smallestX, @largestX)
+    # @plotPoints(@smallestX, @largestX)

     [cMin, cMax] = [@xMin, @xMax]
     [wMin, wMax] = [@smallestX, @largestX]
@@ -285,31 +285,31 @@
     classifier.el.find('#ui-slider').attr('disabled',true)
     classifier.el.find('.noUi-handle').fadeOut(150)

-    # # TODO: broken (a major pain in my ass)
-    # step = 1.5
-    # zoom = setInterval (=>
-
-    #   # gradually expand zooming bounds
-    #   cMin -= step
-    #   if cMin < @smallestX then cMin = @smallestX
-    #   cMax += step
-    #   if cMax > @largestX then cMax = @largestX
-
-    #   # console.log "[cMin,cMax] = [#{cMin},#{cMax}]"
-    #   @plotPoints(cMin,cMax)
-
-    #   if cMin <= @smallestX and cMax >= @largestX  # finished zooming
-    #     clearInterval zoom
-    #     classifier.el.find('#graph').removeClass('is-zooming')
-    #     @plotPoints(@smallestX, @largestX)
-    #     unless callback is undefined
-    #       callback.apply()
-    #       classifier.el.find("#zoom-button").removeClass("zoomed")
-    #       classifier.el.find("#zoom-button").removeClass("allowZoomOut") # for last zoom level
-    #       classifier.el.find('#ui-slider').attr('disabled',true)
-    #       classifier.el.find('.noUi-handle').fadeOut(150)
-    # ), 30
-    # return
+    # TODO: broken (a major pain in my ass)
+    step = 1.5
+    zoom = setInterval (=>
+
+      # gradually expand zooming bounds
+      cMin -= step
+      if cMin < @smallestX then cMin = @smallestX
+      cMax += step
+      if cMax > @largestX then cMax = @largestX
+
+      # console.log "[cMin,cMax] = [#{cMin},#{cMax}]"
+      @plotPoints(cMin,cMax)
+
+      if cMin <= @smallestX and cMax >= @largestX  # finished zooming
+        clearInterval zoom
+        classifier.el.find('#graph').removeClass('is-zooming')
+        @plotPoints(@smallestX, @largestX)
+        unless callback is undefined
+          callback.apply()
+          classifier.el.find("#zoom-button").removeClass("zoomed")
+          classifier.el.find("#zoom-button").removeClass("allowZoomOut") # for last zoom level
+          classifier.el.find('#ui-slider').attr('disabled',true)
+          classifier.el.find('.noUi-handle').fadeOut(150)
+    ), 30
+    return

   zoomToCenter: (center) ->
     console.log 'zoomToCenter, CENTER = ', center
@@ -332,21 +332,22 @@
     classifier.el.find('#ui-slider').val(boundL)
     console.log "ZOOMING TO: [#{boundL},#{boundR}] (exceeded right bound)"

-    @plotPoints(boundL,boundR) # SKIP ANIMATION
-
-    # [cMin, cMax] = [@xMin, @xMax]
-    # zoom = setInterval (=>
-    #   @plotPoints(cMin,cMax)
-    #   @rescaleMarks(cMin,cMax)
-    #   cMin += 1.5 unless cMin >= boundL
-    #   cMax -= 1.5 unless cMax <= boundR
-    #   if cMin >= boundL and cMax <= boundR # when 'animation' is done...
-    #     clearInterval zoom
-    #     classifier.el.find('#graph').removeClass('is-zooming')
-    #     @plotPoints(boundL,boundR)
-    #     #@rescaleMarks(boundL,boundR)
-    # ), 30
-
+    # @plotPoints(boundL,boundR) # SKIP ANIMATION
+
+    [cMin, cMax] = [@xMin, @xMax]
+    zoom = setInterval (=>
+      @plotPoints(cMin,cMax)
+      @rescaleMarks(cMin,cMax)
+      cMin += 1.5 unless cMin >= boundL
+      cMax -= 1.5 unless cMax <= boundR
+      if cMin >= boundL and cMax <= boundR # when 'animation' is done...
+        clearInterval zoom
+        classifier.el.find('#graph').removeClass('is-zooming')
+        @plotPoints(boundL,boundR)
+        #@rescaleMarks(boundL,boundR)
+    ), 30
+
+  # NOT REALLY USED ANYMORE
   zoomInTo: (wMin, wMax) ->
     classifier.el.find('#graph').addClass('is-zooming')
     [cMin, cMax] = [@xMin, @xMax]


also fix armory show browse links

also fix armory show browse links

--- apps/shortcuts/index.coffee
+++ apps/shortcuts/index.coffee
@@ -7,5 +7,5 @@

 app = module.exports = express()
 app.get '/:short', routes.index
-# Handle shortcuts with additional params like: /vip-preview-the-armory-show-2014/browse/artist/matthias-meyer
-app.get '/:short/*', routes.index
+# # Handle shortcuts with additional params like: /vip-preview-the-armory-show-2014/browse/artist/matthias-meyer
+app.get '/:short/browse/*', routes.index


Client: Activity: Updated sidebarModalList component for more readable coding

--- client/activity/lib/components/sidebarmodallist/index.coffee
+++ client/activity/lib/components/sidebarmodallist/index.coffee
@@ -34,8 +34,8 @@
     return  if isSearching

     { channel } = ActivityFlux.actions
-
-    channel[@props.onThresholdAction] skip: @props.threads.size
+    loadFollowedChannels = channel[@props.onThresholdAction]
+    loadFollowedChannels skip: @props.threads.size

     @setThreads()

@@ -82,9 +82,10 @@
     isSearching = yes

     value = value.slice(1)  if value[0] is '#'
+    loadFollowedChannels = channel[@props.onThresholdAction]

     channel.loadChannelsByQuery value
-    channel[@props.onThresholdAction] skip: @props.threads.size
+    loadFollowedChannels skip: @props.threads.size

     @filter()

@@ -123,7 +124,9 @@

   renderNoResultText: ->

-
Sorry, your search did not have any results

+

+      Sorry, your search did not have any results
+

   renderChannelList: ->


Check whether the socket file exists before connecting.

By removing the socket file when the browser process of Atom quits and
checking it's existence before trying to connect it, we can greatly
reduce the startup time of the first instance of Atom.

--- src/atom-application.coffee
+++ src/atom-application.coffee
@@ -82,11 +82,15 @@
     process.env['NODE_PATH'] = resourcePaths.join path.delimiter

   sendArgumentsToExistingProcess: (pidToKillWhenClosed, callback) ->
+    if not fs.existsSync(@socketPath)
+      callback(false)
+      return
+
     client = net.connect {path: @socketPath}, (args...) =>
       client.write(JSON.stringify({@pathsToOpen, pidToKillWhenClosed}))
       callback(true)

-    client.on 'error', (args...) -> callback(false)
+    client.on 'error', (error) -> console.log(error); callback(false)

   listenForArgumentsFromNewProcess: ->
     fs.unlinkSync @socketPath if fs.existsSync(@socketPath)
@@ -154,6 +158,10 @@
     # Quit when all windows are closed.
     app.on 'window-all-closed', ->
       app.quit()
+
+    # Clean the socket file when quit normally.
+    app.on 'will-quit', =>
+      fs.unlinkSync @socketPath if fs.existsSync(@socketPath)

     ipc.on 'close-without-confirm', (processId, routingId) ->
       window = BrowserWindow.fromProcessIdAndRoutingId processId, routingId


fix onClickNextFrame for 4th frame case

fix onClickNextFrame for 4th frame case

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -60,9 +60,9 @@
       else
         console.log("Error: unknown classifier-type")

-    'change input[name="selected-artifact"]': ->
-      @artifactSubtype = @selectedArtifactRadios.filter(':checked').val()
-
+    'change input[name="selected-artifact"]': ->
+      @artifactSubtype = @selectedArtifactRadios.filter(':checked').val()
+
     'click button[name="delete"]': ->
       @tool.mark.destroy()

@@ -82,7 +82,7 @@
     'button[name="asteroid-done"]'   : 'doneButton'
     'button[name="next-frame"]'      : 'nextFrame'
     'button[name="cancel"]'          : 'cancel'
-    'input[name="selected-artifact"]': 'selectedArtifactRadios'
+    'input[name="selected-artifact"]': 'selectedArtifactRadios'
     'input[name="classifier-type"]'  : 'classifierTypeRadios'
     'input[name="frame-slider"]'     : 'frameSlider'
     '.asteroid-not-visible'          : 'asteroidVisibilityCheckboxes'
@@ -95,7 +95,7 @@
         # reset asteroid/artifact selector
         for e in @el.find('input[name="classifier-type"]')
           e.checked = false
-        @el.find('button[name="to-select"]').addClass 'hidden'
+        @el.find('button[name="to-select"]').addClass 'hidden'
         @el.find('.what-kind').show()

       exit: ->
@@ -114,7 +114,7 @@

       exit: ->
         @disableMarkingSurfaces()
-        @el.find('.asteroid-classifier').hide()
+        @el.find('.asteroid-classifier').hide()
         @doneButton.hide()
         @finishButton.show()

@@ -124,7 +124,7 @@
         @el.find('.artifact-classifier').show()
       exit: ->
         @disableMarkingSurfaces()
-        @el.find('.artifact-classifier').hide()
+        @el.find('.artifact-classifier').hide()

   constructor: ->
     super
@@ -240,7 +240,7 @@

   disableMarkingSurfaces: =>
     surface.disable() for surface in @markingSurfaceList
-
+
   enableMarkingSurfaces: =>
     surface.enable() for surface in @markingSurfaceList

@@ -381,10 +381,8 @@
       @el.find(".asteroid-visible").show()

   onClickNextFrame: ->
-    sliderValue = document.getElementById('frame-slider').value
-    return if sliderValue is 3
-    sliderValue++
-    @activateFrame(sliderValue)
+    nextFrame = parseInt(document.getElementById('frame-slider').value) + 1
+    @activateFrame(nextFrame) unless nextFrame is 4

   onClickCancel: ->
     @resetMarkingSurfaces() if @state is 'asteroidTool'
@@ -457,4 +455,4 @@
     console?.log JSON.stringify @classification
     @classification.send()

-module.exports = Classifier+module.exports = Classifier


update onClickNextFrame

update onClickNextFrame

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -405,7 +405,7 @@

   onClickNextFrame: ->
     return if @currFrameIdx is 3
-    @setCurrentFrameIdx(@currFrameIdx+1)
+    @currFrameIdx++
     @activateFrame(@currFrameIdx)

   onClickCancel: ->


Initial WebGL setup

Initial WebGL setup

--- app/controllers/fitsviewer.coffee
+++ app/controllers/fitsviewer.coffee
@@ -4,6 +4,9 @@

 class FITSViewer extends Spine.Controller
   @bins = 500
+
+  events:
+    "click .band": "selectBand"

   constructor: ->
     super
@@ -16,13 +19,13 @@
     @means = {}
     @stds = {}

-    @container = $("#examine .subject")
+    @container = document.querySelector("#examine .subject")
     @createBandButtons()
+    @setupWebGL()

-
   createBandButtons: ->
     for band in @bands
-      $("#examine .content-block").append("<button id='band-#{band}' class='band' disabled='disabled'>#{band}</button>")
+      $("#examine .content-block").append("<button id='band-#{band}' class='band' value='#{band}' disabled='disabled'>#{band}</button>")

   addImage: (obj) ->
     band = obj.band
@@ -36,11 +39,10 @@
     dataunit.getFrame()
     dataunit.getExtremes()

-    @setupHistogram(band)
-
+    @computeStatistics(band)

   # Compute histogram using inline worker
-  setupHistogram: (band) ->
+  computeStatistics: (band) ->
     dataunit = @images[band].getDataUnit()

     # Set up message to pass to worker
@@ -62,9 +64,27 @@
       @histograms[band] = data.histogram
       @means[band]      = data.mean
       @stds[band]       = data.std
+
+      # Enable associated button
+      $("#band-#{band}").removeAttr('disabled')
+
     ), false
     worker.postMessage(msg)
+
+  setupWebGL: =>
+    # TODO: Set this dynamically
+    [@width, @height] = [424, 424]
+    canvas  = WebGL.setupCanvas(@container, @width, @height)
+    @gl     = WebGL.create3DContext(canvas)
+    @ext    = @gl.getExtension('OES_texture_float')

-    console.log @histograms, @means, @stds
+    unless @ext
+      alert "No OES_texture_float"
+      return null
+
+    @vertexShader = WebGL.loadShader(@gl, WebGL.vertexShader, @gl.VERTEX_SHADER)
+
+  selectBand: (e) =>
+    console.log e.currentTarget.value

 module.exports = FITSViewer


Indentation bug fix.

Indentation bug fix.

--- visualiser/scripts/init.coffee
+++ visualiser/scripts/init.coffee
@@ -53,4 +53,4 @@
     val = stream.get(i)
   [{min: lowest.x, max: highest.x}, {min: lowest.y, max: highest.y}]

-  eventBlacklist = ["start", "finish", "formdata"]+eventBlacklist = ["start", "finish", "formdata"]


workaround deprecation in CodeMirror 4 that broke saving options in latex editor

--- salvus/page/editor.coffee
+++ salvus/page/editor.coffee
@@ -2630,7 +2630,7 @@
                 if z.indexOf('%sagemathcloud=') != -1
                     m = n
                     break
-            cm.setLine(m, line)
+            cm.replaceRange(line+'\n', {line:m,ch:0}, {line:m+1,ch:0})
         else
             cm.replaceRange('\n'+line, {line:cm.doc.lastLine()+1,ch:0})
         @latex_editor.syncdoc.sync()


Allow underscore and dashes in extensions.


Add mobile poke

Add mobile poke

--- src/public/scripts/app.controller.coffee
+++ src/public/scripts/app.controller.coffee
@@ -39,6 +39,7 @@
 			@boost = null
 			@fire = null
 			@poke = {}
+			@pokeCanvas = null

 			@drawControllers()

@@ -63,29 +64,29 @@
 				@boost.bind("touchend", () => @sendBoostEvent(false))
 				@boost.bind("touchcancel", () => @sendBoostEvent(false))

-				@fire.bind("touchstart", () => @sendFireEvent(true))
+				@fire.bind("touchenter", () => @sendFireEvent(true))
 				@fire.bind("touchend", () => @sendFireEvent(false))
 				@fire.bind("touchcancel", () => @sendFireEvent(false))

 			)

-			$(window).on('orientationchange', () =>
+			$(window).on('orientationchange resize', () =>
 				@drawControllers()
 			)

 		drawControllers: () =>

-			canvas = oCanvas.create({
+			@canvas = oCanvas.create({
 				canvas: "#canvas"
 				disableScrolling : true
 			})

-			width = canvas.width  = $(window).width()
-			height = canvas.height = $(window).height()
+			width = @canvas.width  = $(window).width()
+			height = @canvas.height = $(window).height()

 			if width > height

-				@boost = canvas.display.rectangle({
+				@boost = @canvas.display.rectangle({
 					x: width / 4,
 					y: height / 2,
 					origin: { x: "center", y: "center" },
@@ -93,7 +94,7 @@
 					height: height,
 					fill: "#0aa"
 				})
-				@fire = canvas.display.rectangle({
+				@fire = @canvas.display.rectangle({
 					x: width * 3 / 4,
 					y: height / 2,
 					origin: { x: "center", y: "center" },
@@ -104,7 +105,7 @@

 			else

-				@boost = canvas.display.rectangle({
+				@boost = @canvas.display.rectangle({
 					x: width / 2,
 					y: height / 4,
 					origin: { x: "center", y: "center" },
@@ -112,7 +113,7 @@
 					height: height / 2,
 					fill: "#0aa"
 				})
-				@fire = canvas.display.rectangle({
+				@fire = @canvas.display.rectangle({
 					x: width / 2,
 					y: height * 3 / 4,
 					origin: { x: "center", y: "center" },
@@ -121,7 +122,7 @@
 					fill: "#f21"
 				})

-			boostText = canvas.display.text({
+			boostText = @canvas.display.text({
 				x: 0,
 				y: 0,
 				origin: { x: "center", y: "top" },
@@ -130,38 +131,47 @@
 				fill: "#000"
 			})

-			fireText = canvas.display.text({
+			@pokeCanvas = fireText = @canvas.display.ellipse({
 				x: 0,
 				y: 0,
-				origin: { x: "center", y: "top" },
+				origin: { x: "center", y: "center" },
 				font: "bold 25px/1.5 sans-serif",
-				text: "Fire",
+				radius: 48,
 				fill: "#000"
 			})

-			canvas.addChild(@boost)
+			@canvas.addChild(@boost)
 			@boost.addChild(boostText)

-			canvas.addChild(@fire)
+			@canvas.addChild(@fire)
 			@fire.addChild(fireText)

-			fireText.bind("touchenter", @initiatePoke, false )
-			fireText.bind("touchmove", @handlePoke, false )
-			fireText.bind("touchleave", @stopPoke, false )
+			@fire.bind("touchenter", @initiatePoke, false )
+			@fire.bind("touchmove", @handlePoke, false )
+			@fire.bind("touchend", @stopPoke, false )
+			@fire.bind("touchcancel", @stopPoke, false )

 		initiatePoke: (event) =>
+			console.log "start: ", event.x, event.y
 			@poke.x = event.x
 			@poke.y = event.y

 		handlePoke: (event) =>
+			sendPoke = {}
+			sendPoke.x = event.x - @poke.x
+			sendPoke.y = event.y - @poke.y
+			console.log sendPoke
+			@node.getPeers()[0].emit('controller.cannon', sendPoke)
+
 			poke = {}
-			poke.x = event.x - @poke.x
-			poke.y = event.y - @poke.y
-			console.log poke
-			@node.getPeers()[0].emit('controller.canon', poke)
+			poke.x = @fire.x - event.x
+			poke.y = @fire.y - event.y
+			@pokeCanvas.moveTo(-poke.x, -poke.y)
+			@canvas.redraw()

 		stopPoke: (event) =>
-			# asf
+			@pokeCanvas.moveTo(0, 0)
+			@canvas.redraw()

 		# Get values stored in the url of the controller
 		#


home: use generic list component for connected machines

--- client/home/lib/virtualmachines/components/connectedmachineslist.coffee
+++ client/home/lib/virtualmachines/components/connectedmachineslist.coffee
@@ -2,7 +2,8 @@
 React           = require 'kd-react'
 EnvironmentFlux = require 'app/flux/environment'
 KDReactorMixin  = require 'app/flux/base/reactormixin'
-List            = require './machineslist/list'
+List            = require 'app/components/list'
+MachineItem     = require './machineslist/listitem'

 module.exports = class ConnectedMachinesListContainer extends React.Component
@@ -13,14 +14,44 @@
     }

+  numberOfSections: -> 1
+
+
+  numberOfRowsInSection: ->
+
+    @state.stacks
+      .toList()
+      .filter (stack) -> stack.get('title').toLowerCase() is 'managed vms'
+      .size
+
+
+  renderSectionHeaderAtIndex: -> null
+
+
+  renderRowAtIndex: (sectionIndex, rowIndex) ->
+
+    stack = @state.stacks.toList().get rowIndex
+
+    stack.get 'machines'
+      .sort (a, b) -> a.get('label').localeCompare(b.get('label')) # Sorting from a to z
+      .map (machine) ->
+        <MachineItem
+          key={machine.get '_id'} stack={stack} machine={machine}
+          shouldRenderDetails={no} />
+
+
+  renderEmptySectionAtIndex: ->
No connected machines.

+
+
   render: ->

-    stacks = @state.stacks.toList()
-      .filter (stack) -> stack.get('title').toLowerCase() is 'managed vms'
-
     <List
-      stacks={stacks}
-      shouldRenderDetails={no} />
+      numberOfSections={@bound 'numberOfSections'}
+      numberOfRowsInSection={@bound 'numberOfRowsInSection'}
+      renderSectionHeaderAtIndex={@bound 'renderSectionHeaderAtIndex'}
+      renderRowAtIndex={@bound 'renderRowAtIndex'}
+      renderEmptySectionAtIndex={@bound 'renderEmptySectionAtIndex'}
+    />

 ConnectedMachinesListContainer.include [KDReactorMixin]


Some firewall in asia is freaking out, fall back to event streams

--- src/scripts/services/dapiServices.coffee
+++ src/scripts/services/dapiServices.coffee
@@ -4,10 +4,11 @@
 atmosphereOptions =
   contentType : 'application/json'
   transport : 'websocket'
-  reconnectInterval : 5000
+  connectTimeout : 10000 # 10s timeout before fallback to streaming
+  reconnectInterval : 30000
   enableXDR: true
   timeout : 60000
-  fallbackTransport: 'jsonp' # Server might have issues with 'long-polling'
+  fallbackTransport: 'streaming' # Server might has issues with 'long-polling'
   headers:
     api_key: apiKey
   #onError: (resp) =>


add spec

add spec

--- spec/BaconSpec.coffee
+++ spec/BaconSpec.coffee
@@ -1650,6 +1650,14 @@
           [as, bs, cs], (a,b,c) ->  a + b + c,
           [as],         (a)   ->  a)
       ['a', 'ab0', 'a', 'ab1', 'ab2', 'ab3'])
+  describe "doesn't output before properties have values", ->
+    expectStreamEvents(
+      ->
+        p = series(2, ["p"])
+        s = series(1, ["s"])
+        Bacon.when(
+          [s, p], (s, p) -> s + p)
+      ["sp"])
   describe "returns Bacon.never() on the empty list of patterns", ->
     expectStreamEvents(
       ->


Put initialization into its own suite.

Put initialization into its own suite.

--- spec/Heap.spec.coffee
+++ spec/Heap.spec.coffee
@@ -30,8 +30,10 @@
 		output = []
 		removeAllMins(heap, output)
 		expect(output).toEqual [-10, -6, -2, -1, 0, 4, 7, 8, 9, 99, undefined]
+
+describe "Initialization passing an array", ->
 	heap2 = new Heap([undefined, 4, 6, -8, null, 5, -3, 2, 5, 6, -7])
-	it "should initialize a heap if an array is passed", ->
+	it "should heapify the array", ->
 		output = []
 		removeAllMins(heap2, output)
 		expect(output).toEqual [-8, -7, -3, 2, 4, 5, 5, 6, 6, null, undefined]


Silence old logs

Silence old logs

--- spec/SCDDetect.coffee
+++ spec/SCDDetect.coffee
@@ -67,7 +67,6 @@
         ins.send canvas

       it 'should find faces', ->
-        console.log 'data', results
         chai.expect(results).to.be.an 'array'
         chai.expect(results.length).to.gte 0
         chai.expect(grps.length).to.equal 1
@@ -120,7 +119,6 @@
           out.on 'begingroup', (grp) ->
             grps.push grp
           out.once "data", (data) ->
-            console.log 'data', data
             results = data
             done()
           ins.beginGroup 'foo'
@@ -139,7 +137,6 @@
         grps.push grp
       out.once "data", (data) ->
         results = data
-        console.log 'data', data
         chai.expect(results).to.be.an 'array'
         chai.expect(results.length).to.gte 0
         chai.expect(grps.length).to.equal 1


moved default values from config to own function

--- web/scripts/gameManager.coffee
+++ web/scripts/gameManager.coffee
@@ -5,6 +5,79 @@
 class window.GameManager
     constructor: (@environment) ->
         @config = @environment.description
+        @loadDefaultsIfUndefined()
+
+        @editorDiv = 'codeEditor'
+        @visualDiv = 'gameVisual'
+        @setUpGame()
+
+    setUpGame: ->
+        ###
+            Sets up everything for the game to run.
+        ###
+        @gameDiv = jQuery @environment.gamediv
+        @gameDiv.append "

"
+        @gameDiv.append '<button id="compileAndRun">Go</button>'
+        @gameDiv.append '<button id="resetState">Reset</button>'
+        @gameDiv.append "

"
+
+        @codeEditor = new EditorManager @editorDiv, @config.editor, @config.code
+        @interpreter = new CodeInterpreter @config.editor.commands
+
+        @environment.visualMaster.container.id = @visualDiv
+        @visual = new GameVisual @environment.visualMaster, @environment.frameRate
+        @addEventListeners()
+        return
+
+    startGame: ->
+        @config.visual.characters.protagonist.x = @config.game.startpos[0] - 1
+        @config.visual.characters.protagonist.y = @config.game.startpos[1] - 1
+        @visual.startGame @config.visual
+        @gameState = new MapGameState this, @visual, @config.visual.characters
+        @commandMap = new MapGameCommands @gameState
+        return
+
+    gameWon: (score, stars) ->
+        log "Game Won: #{@environment.key}"
+        player = @environment.player
+        if player.games[@environment.key]?
+            if @player.games[@environment.key].hiscore? > score
+                score = @player.games[@environment.key].hiscore
+
+            if @player.games[@environment.key].stars? > stars
+                stars = @player.games[@environment.key].stars
+
+        @environment.codeland.storeGameCompletionData @environment.key, {
+            hiscore : score,
+            stars : stars,
+            passed : true
+        }
+        @finishGame()
+        return
+
+    finishGame: ->
+        @codeEditor = null
+        @interpreter = null
+        @visual = null
+        @gameDiv.empty()
+        return
+
+    addEventListeners: ->
+        jQuery('#compileAndRun').click @runStudentCode
+        jQuery('#resetState').click @reset
+        return
+
+    reset: =>
+        @codeEditor.resetEditor()
+        @visual.startGame @config.visual
+        return
+
+    runStudentCode: =>
+        @interpreter.scanText @codeEditor.getStudentCode()
+        @interpreter.executeCommands @commandMap
+        return
+
+    loadDefaultsIfUndefined: ->
         if not @config.editor?
             @config.editor = {}
         if not @config.code?
@@ -44,74 +117,24 @@
         if not @config.code.postfix?
             @config.code.postfix = '}\n}'

-        @editorDiv = 'codeEditor'
-        @visualDiv = 'gameVisual'
-        @setUpGame()
-
-    setUpGame: ->
-        ###
-            Sets up everything for the game to run.
-        ###
-        @gameDiv = jQuery @environment.gamediv
-        @gameDiv.append "

"
-        @gameDiv.append '<button id="compileAndRun">Go</button>'
-        @gameDiv.append '<button id="resetState">Reset</button>'
-        @gameDiv.append "

"
-
-        @codeEditor = new EditorManager @editorDiv, @config.editor, @config.code
-        @interpreter = new CodeInterpreter @config.editor.commands
-
-        @environment.visualMaster.container.id = @visualDiv
-        @visual = new GameVisual @environment.visualMaster, @environment.frameRate
-        @addEventListeners()
-        return
-
-    startGame: ->
-        @config.visual.characters.protagonist.x = @config.game.startpos[0] - 1
-        @config.visual.characters.protagonist.y = @config.game.startpos[1] - 1
-        @visual.startGame @config.visual
-        @gameState = new MapGameState this, @visual, @config.visual.characters
-        @commandMap = new MapGameCommands @gameState
-        return
-
-    gameWon: (score, stars) ->
-        log "Game Won: #{@environment.key}"
-        player = @environment.player
-        if player.games[@environment.key]?
-            if @player.games[@environment.key].hiscore? > score
-                score = @player.games[@environment.key].hiscore
-
-            if @player.games[@environment.key].stars? > stars
-                stars = @player.games[@environment.key].stars
-
-        @environment.codeland.storeGameCompletionData @environment.key, {
-            hiscore : score,
-            stars : stars,
-            passed : true
-        }
-        @finishGame()
-        return
-
-    finishGame: ->
-        @codeEditor = null
-        @interpreter = null
-        @visual = null
-        @gameDiv.empty()
-        return
-
-    addEventListeners: ->
-        jQuery('#compileAndRun').click @runStudentCode
-        jQuery('#resetState').click @reset
-        return
-
-    reset: =>
-        @codeEditor.resetEditor()
-        @visual.startGame @config.visual
-        return
-
-    runStudentCode: =>
-        @interpreter.scanText @codeEditor.getStudentCode()
-        @interpreter.executeCommands @commandMap
+        if not @environment.visualMaster?
+            @environment.visualMaster = {}
+
+        if not @environment.visualMaster.container?
+            @environment.visualMaster.container = {
+                width: 360,
+                height: 360
+            }
+
+        if not @environment.visualMaster.preLoading?
+            @environment.visualMaster.preLoading = {
+                protagonist: [
+                    "img/wmn1_bk1.gif","img/wmn1_bk2.gif",
+                    "img/wmn1_rt1.gif","img/wmn1_rt2.gif",
+                    "img/wmn1_fr1.gif","img/wmn1_fr2.gif",
+                    "img/wmn1_lf1.gif","img/wmn1_lf2.gif"
+                ]
+            }
         return

 class MapGameState


add comments

add comments

--- src/game/Scene.coffee
+++ src/game/Scene.coffee
@@ -1,13 +1,37 @@
+# An actual Game scene : Everything happens here
+# Can be a config screen, a start screen, or your whole game scene
+#
+# @example How to create a new scene
+#   var scene = new Rotten.Scene();
+#
+# @example How to add stuff to your scene
+#   scene.addExisting({Rotten.Drawables});
+#
 class Rotten.Scene

+
+    # Constructs your scene
+    # @param {Rotten.Game} game Your current game
     constructor: (@game) ->
+
+        # Objects in your scene
         @objects = []

-    setup: () ->
+
+    # All your setup logic should go here (adding objects to scene, ...)
+    setup: ->
         object.setup() for object in @objects

-    update: () ->
+
+    # All your update logic should go here (moving objects, ...)
+    update: ->
         object.update() for object in @objects

-    draw: () ->
+
+    # All your draw logic should be here (draw objects, draw shit, ...)
+    draw: ->
         object.draw() for object in @objects
+
+
+    addExisting: (object, unique) ->
+        @objects.push object if unique and @objects.indexOf object == -1


payment: fetch the child products belonging to plans

TODO: this can be optimized, no doubt

--- client/landing/app/Applications/Dashboard.kdapplication/controllers/productscontroller.coffee
+++ client/landing/app/Applications/Dashboard.kdapplication/controllers/productscontroller.coffee
@@ -8,29 +8,12 @@
     @productsView = @prepareProductView 'product'
     @plansView    = @prepareProductView 'plan'

-    @loadProducts()
-
-  loadProducts:->
-    { view } = @getOptions()
-
-    dash [
-      => @fetchProducts 'product', (err, products) =>
-        return callback err  if err
-        @products = products
-        view.setProducts 'product', products
-
-      => @fetchProducts 'plan', (err, plans) =>
-        return callback err  if err
-        @plans = plans
-        view.setProducts 'plan', plans
-    ]
-
   prepareProductView: (category) ->
     { view } = @getOptions()

     konstructor = getConstructor category

-    reload = =>
+    do reload = =>
       @fetchProducts category, (err, products) ->
         view.setProducts category, products

@@ -52,7 +35,11 @@

     showAddProductsModal = (plan) =>
       modal = new GroupPlanAddProductsModal {}, plan
-      modal.on 'ProductsAdded', reload
+      modal.on 'ProductsAdded', (quantities) ->
+        plan.updateProducts quantities, (err) ->
+          handleResponse err
+
+          modal.destroy()

       @fetchProducts 'product', (err, products) ->
         return  if KD.showError err
@@ -136,4 +123,21 @@
     KD.remote.api["JPayment#{ category.capitalize() }"]

   fetchProducts: (category, callback) ->
-    KD.getGroup().fetchProducts category, callback+    KD.getGroup().fetchProducts category, (err, products) ->
+      return callback err  if err
+
+      queue = products.map (plan) -> ->
+        # recursively fetch nested products, if any
+        if plan.fetchProducts?
+          plan.fetchProducts (err, planProducts) ->
+            return queue.fin err  if err
+
+            plan.childProducts = planProducts
+            queue.fin()
+
+        else queue.fin()
+
+      dash queue, (err) ->
+        return callback err  if err
+
+        callback null, products


Support session as an image resource parent.

--- src/image/xnat.coffee
+++ src/image/xnat.coffee
@@ -49,15 +49,18 @@
     # sequence.
     resource = image.resource
     imageSequence = image.imageSequence
-    scan = scanFor(imageSequence)
-    session = imageSequence.session
+    if imageSequence?
+      scan = scanFor(imageSequence)
+      session = imageSequence.session
+    else
+      session = image.session
+    if not session?
+      throw new Error "The image #{ image.name } does not have a session"
     subject = session.subject
     collection = subject.collection
     project = subject.project
     # The XNAT project directory.
     projectDir = projectLocation(project)
-    # The scan subdirectory.
-    scanDir = "SCANS/#{ scan.number }"
     # The zero-padded subject label number suffix, e.g. "003".
     subjectNumberSuffix = sprintf.sprintf("%03d", subject.number)
     # The subject label, e.g. "Breast003".
@@ -66,8 +69,12 @@
     sessionNumberSuffix = sprintf.sprintf("%02d", session.number)
     # The XNAT experiment label, e.g. "Breast003_Session02".
     expLabel = "#{ subjectLabel }_Session#{ sessionNumberSuffix }"
+    if scan?
+      parentDir = "#{ expLabel }/SCANS/#{ scan.number }"
+    else
+      parentDir = expLabel

     # Return the formatted image file path.
-    "#{ projectDir }/#{ expLabel }/#{ scanDir }/#{ resource }/#{ image.name }"
+    "#{ projectDir }/#{ parentDir }/#{ resource }/#{ image.name }"

 `export { XNAT as default }`


nexts the route if the fair is in open state

--- apps/fair_organizer/routes.coffee
+++ apps/fair_organizer/routes.coffee
@@ -25,6 +25,8 @@
 @fetchFairData = (req, res, next) ->
   data = {}
   data.access_token = req.user.get('accessToken') if req.user
+
+  # manually fetching the profile here, since we don't want to override /the-armory-show just yet
   new Profile(id: 'the-armory-show').fetch
     data: data
     success: (profile) ->
@@ -39,6 +41,9 @@
       fair.fetch
         error: res.backboneError
         success: =>
+          # go to normal fair page when this fair switches to open
+          return next() if fair.get('auctionState') is 'open'
+
           # This is the specific-to-armory part
           # Eventually we will fetch the organizer's past fairs here.
           armory2013 = new Fair id: 'the-armory-show-2013'


start infinite scroll after first feed item loads

--- apps/root/client/feed_view.coffee
+++ apps/root/client/feed_view.coffee
@@ -21,15 +21,15 @@

         @collection.markRead() if sd.FEED_TYPE is 'notifications'

+        if sd.FEED_TYPE is 'primary'
+          # notifications api updates to be paginated :/
+          @infinite_view = new InfiniteView
+            context: @$el
+            collection: @collection
+            itemSelector: @$el
+

     @current_user = new User sd.CURRENT_USER
-
-    if sd.FEED_TYPE is 'primary'
-      # notifications api updates to be paginated :/
-      @infinite_view = new InfiniteView
-        context: @$el
-        collection: @collection
-        itemSelector: @$el

   render: ->
     @$el.html feedTemplate(feed: @collection.models, user: @current_user)


Changed testpie colors

Changed testpie colors


Rename mock()._check_mock_usage() to mock()._check_usage()

--- src/TinyMockJS.coffee
+++ src/TinyMockJS.coffee
@@ -17,7 +17,7 @@
   #
   PUBLISH.mock = (args...) ->
     try
-      _check_mock_usage(args)
+      _check_usage(args)
       [ expects_method_name, mock_count, test_function ] = _parse_args(args)
       # TODO: what is 'all_expectations'?  Why is it being created here?  What purpose does it server?  Need a better name.
       all_expectations = new AllExpectations()    # TODO: use factory method instead of new (same for other classes)
@@ -32,7 +32,7 @@

   # private

-  _check_mock_usage = (args) ->
+  _check_usage = (args) ->
     switch args.length
       when 1
         fail(messages.MockUsage) if not is_function(args[0])


:lipstick:

:lipstick:

--- src/ls-archive.coffee
+++ src/ls-archive.coffee
@@ -33,12 +33,10 @@
       entryPath = entry.path[0...-1]
     else
       entryPath = entry.path
-    if entry.type is 'Directory'
-      entryType = 5
-    else if entry.type is 'File'
-      entryType = 0
-    else
-      entryType = -1
+    switch entry.type
+      when 'Directory' then entryType = 5
+      when 'File' then entryType = 0
+      else entryType = -1
     paths.push(new ArchiveEntry(entryPath, entryType))
     entry.autodrain()


Fix missed permission name in last commit

--- packages/rocketchat-ui-flextab/flex-tab/tabs/membersList.coffee
+++ packages/rocketchat-ui-flextab/flex-tab/tabs/membersList.coffee
@@ -63,8 +63,8 @@
 		roomData = Session.get('roomData' + this._id)
 		return '' unless roomData
 		return switch roomData.t
-			when 'p' then RocketChat.authz.hasAtLeastOnePermission ['add-user-to-any-p-room', 'add-user-to-own-room'], this._id
-			when 'c' then RocketChat.authz.hasAtLeastOnePermission ['add-user-to-any-c-room', 'add-user-to-own-room'], this._id
+			when 'p' then RocketChat.authz.hasAtLeastOnePermission ['add-user-to-any-p-room', 'add-user-to-joined-room'], this._id
+			when 'c' then RocketChat.authz.hasAtLeastOnePermission ['add-user-to-any-c-room', 'add-user-to-joined-room'], this._id
 			else false

 	autocompleteSettingsAddUser: ->


Fixed date types

Fixed date types

--- src/numbersapi.coffee
+++ src/numbersapi.coffee
@@ -23,7 +23,7 @@
         else
           msg.reply body

-  robot.respond /number ([0-9]+)\/([0-9]+)\s?(trivia|math|date|year)?$/i, (msg) ->
+  robot.respond /number ([0-9]+)\/([0-9]+)\s?(date)?$/i, (msg) ->
     msg
       .http("http://numbersapi.com/" + msg.match[2] + "/" + msg.match[1] + "/" + ( msg.match[3] || "" ))
       .get() (err, res, body) ->


bump version

bump version

--- src/git-timetracking.coffee
+++ src/git-timetracking.coffee
@@ -22,7 +22,7 @@
 		defaultPauseTime

 program
-	.version('0.2.0')
+	.version('0.2.1')
 	.option("-d, --directory [dir]", "directory to analyse", ".")
 	.option("-g, --group [regexp]", "group commit times by regexp", null)
 	.option("-u, --user <email>", "user email adress to filter")


__privatized__ some methods on orderedset

--- src/orderedset.coffee
+++ src/orderedset.coffee
@@ -1,9 +1,7 @@
-_some = (fn) ->
+some = Array::some ?  (fn) ->
     return true for i in this when fn(i)

     false
-
-some = Array::some ? _some

 class OrderedSet
     ## A Set of items ordered so that:
@@ -15,22 +13,18 @@
     ##   equal(a, b)
     constructor: (@items = [], unique = no, sorted = no) ->
         if not unique
-            @uniqify()
+            @__uniqify__()
         else if not sorted
-            @sort()
+            @__sort__()

         @length = @items.length

     __constructor__: OrderedSet

-    contains: (item) ->
-        some.call @items, (a) => @equal a, item
+    __sort__: ->
+        @items.sort (a, b) => if @greater(b, a) then 1 else -1

-    sort: ->
-        @items.sort (a, b) => if @greater(b, a) then 1 else -1
-        return this
-
-    uniqify: ->
+    __uniqify__: ->
         # enforce the policy that no two
         # items (a, b) should be @equal(a, b)
         _items = @items
@@ -39,8 +33,10 @@
         for item in _items
             @items.push item unless @contains item

-        @sort()
-        return this
+        @__sort__()
+
+    contains: (item) ->
+        some.call @items, (a) => @equal a, item

     addOne: (item) ->
         if @contains item
@@ -49,6 +45,7 @@
             new @__constructor__ @items.concat([item]), yes

     add: (items...) ->
+        # add each of the arguments to the set
         set = this

         for item in items
@@ -56,19 +53,21 @@

         return set

-    concat: (items) ->
+    concat: (ls) ->
+        # add a collection of items to the set
         if items instanceof OrderedSet
-            @add items.items...
+            @add ls.items...
         else
-            @add items...
+            @add ls...

+    # set.item(i) -> array[i]
     item: (n) -> @items[n]

     toArray: -> @items[0..]

     slice: -> @items.slice arguments...

-    equal:   (a, b) -> a is b
+    equal: (a, b) -> a is b

     greater: (a, b) -> b > a


fix bug where username/email were not editable

--- client/app/Applications/Account.kdapplication/account/accSettingsPersUsername.coffee
+++ client/app/Applications/Account.kdapplication/account/accSettingsPersUsername.coffee
@@ -77,8 +77,11 @@
       views    : [emailInputs,nonEmailInputs]
       cssClass : "clearfix"

-    @listenTo KDEventTypes : "click", listenedToInstance : emailCancel, callback : emailSwappable.swapViews
-    @listenTo KDEventTypes : "click", listenedToInstance : emailEdit,   callback : emailSwappable.swapViews
+    emailEdit.on 'click', ->
+      emailSwappable.swapViews()
+
+    emailCancel.on 'click', ->
+      emailSwappable.swapViews()

     # #
     # ADDING USERNAME FORM
@@ -124,5 +127,8 @@
       views    : [usernameInputs,usernameNonInputs]
       cssClass : "clearfix"

-    @listenTo KDEventTypes : "click", listenedToInstance : usernameCancel, callback : usernameSwappable.swapViews
-    @listenTo KDEventTypes : "click", listenedToInstance : usernameEdit,   callback : usernameSwappable.swapViews
+    usernameEdit.on 'click', ->
+      usernameSwappable.swapViews()
+
+    usernameCancel.on 'click', ->
+      usernameSwappable.swapViews()


removed temporary group visibility control procedure

--- client/landing/app/Applications/Groups.kdapplication/Views/GroupsListItemView.coffee
+++ client/landing/app/Applications/Groups.kdapplication/Views/GroupsListItemView.coffee
@@ -7,11 +7,6 @@

     group = @getData()
     {title, slug, body, visibility} = group
-
-    # temp fix
-    # to hide hidden groups from groups page
-    # ths should be taken to backend ASAP
-    @hide()  if visibility is 'hidden'

     slugLink = if slug is KD.defaultSlug then '/' else "/#{slug}"


Don't autoscroll when updating the display after adjusting scrollTop

--- src/app/editor.coffee
+++ src/app/editor.coffee
@@ -462,7 +462,7 @@
     return if scrollTop == @cachedScrollTop
     @cachedScrollTop = scrollTop

-    @updateDisplay() if @attached
+    @updateDisplay(autoscroll: false) if @attached

     @renderedLines.css('top', -scrollTop)
     @underlayer.css('top', -scrollTop)


fixing apparently untested disasm bug

fixing apparently untested disasm bug

--- src/attributes.coffee
+++ src/attributes.coffee
@@ -94,7 +94,7 @@
     else if frame_type == 247
       {
         frame_type: frame_type
-        frame_name: 'same_locals_1_stack_item_extended'
+        frame_name: 'same_locals_1_stack_item_frame_extended'
         offset_delta: bytes_array.get_uint 2
         stack: [parse_verification_type_info(bytes_array, constant_pool)]
       }


Fix bug where node could not be saved without new images.

--- app/assets/javascripts/controllers/nodes.controller.js.coffee
+++ app/assets/javascripts/controllers/nodes.controller.js.coffee
@@ -103,6 +103,9 @@
       # More about promises: http://emberjs.com/api/classes/Ember.RSVP.Promise.html
       # TODO find a way to have more save listeners, at the moment the first listener calling resolve stops the event
       promise = new Ember.RSVP.Promise (resolve, reject)->
+        that.one 'save', ->
+          resolve()
+
         that.trigger('save', resolve, reject)

       promise.then ->


add validate_with

add validate_with

--- src/base_model.coffee
+++ src/base_model.coffee
@@ -203,12 +203,17 @@
       current_value = @get(key)
       for validator, v of value
         klass = switch validator
-          when "length"       then new LengthValidator()
-          when "exclusion"    then new ExclusionValidator()
-          when "inclusion"    then new InclusionValidator()
-          when "format"       then new FormatValidator()
-          when "numericality" then new NumericalityValidator()
-          when "presence"     then new PresenceValidator()
+          when "length"        then new LengthValidator()
+          when "exclusion"     then new ExclusionValidator()
+          when "inclusion"     then new InclusionValidator()
+          when "format"        then new FormatValidator()
+          when "numericality"  then new NumericalityValidator()
+          when "presence"      then new PresenceValidator()
+          when "validate_with"
+            z = new Validator()
+            z.validate = v
+            z.msg = null
+            z

         r = klass.validate(current_value, v)
         if !r


Minor change in comment.

Minor change in comment.

--- app/assets/javascripts/ng-services/ng-FeedsFoldersSvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-FeedsFoldersSvc.js.coffee
@@ -105,7 +105,7 @@
   # PRIVATE FUNCTION: Load feeds inside a single folder. Receives its id as argument.
   #--------------------------------------------
   load_folder_feeds = (id)->
-    # If this feed is already being loaded, do nothing
+    # If feeds in this folder are already being loaded, do nothing
     $rootScope.loading_single_folder_feeds ||= {}
     return if $rootScope.loading_single_folder_feeds[id]


newline at end of file

newline at end of file


Bing delta feed (returns data for artworks published in the past 7 days as JSON file)

--- apps/sitemaps/routes.coffee
+++ apps/sitemaps/routes.coffee
@@ -161,12 +161,13 @@
       res.end()

 @bingDelta = (req, res, next) ->
+  res.set('Content-Disposition': 'attachment').write('[')
   request
     .get("#{FUSION_URL}/api/v1/artworks")
-    .query(published_at_since: moment().subtract(7, 'days'))
-    .end (err, sres) ->
-      return next err if err
-      res.set('Content-Type', 'application/json').write('[')
+    .query(published_at_since: moment().subtract(7, 'days').format('YYYY-MM-DD'))
+    .end (err, sres) ->
+      return next err if err
+      return callback() if sres.body.results.length is 0
       streamResults sres.body.results, res
       res.write('{}]')
       res.end()
@@ -224,4 +225,4 @@
     when 'production'
       "Sitemap: #{APP_URL}/sitemap.xml"
     else
-      "User-agent: *\nDisallow: /"
+      "User-agent: *\nDisallow: /"


Skip failing env var tests after MC deployment

--- tests/integration/models/environment-variables.spec.coffee
+++ tests/integration/models/environment-variables.spec.coffee
@@ -25,7 +25,7 @@
 			result = resin.models.environmentVariables.isSystemVariable(name: 'RESIN_API_KEY')
 			m.chai.expect(result).to.be.true

-	describe 'when logged in', ->
+	describe.skip 'when logged in', ->

 		givenLoggedInUser()


syntax highlighting

syntax highlighting

--- js/codemirror-syntax.coffee
+++ js/codemirror-syntax.coffee
@@ -21,6 +21,12 @@
    state.htmlState = @CodeMirror.startState @htmlMode
    return OPERATOR

+  match = stream.match /^\|\|\|/
+  if match
+   stack.push indentation: stream.indentation(), type: 'table'
+   stream.skipToEnd()
+   return OPERATOR
+
   match = stream.match /^\+\+\+/
   if match
    stack.push indentation: stream.indentation(), type: 'special'
@@ -99,6 +105,22 @@
   if match
    state.link = false
    return OPERATOR_INLINE
+
+  match = stream.match /^\[\[/
+  if match
+   state.inlineMedia = true
+   return OPERATOR_INLINE
+
+  match = stream.match /^\]\]/
+  if match
+   state.inlineMedia = false
+   return OPERATOR_INLINE
+
+  match = stream.match /^\|/
+  if match
+   for t in state.stack
+    if t.type is 'table'
+     return OPERATOR_INLINE

   return null

@@ -109,6 +131,7 @@
   state.superscript = false
   state.code = false
   state.link = false
+  state.inlineMedia = false

  startState: ->
   stack: []
@@ -121,6 +144,7 @@
   superscript: false
   code: false
   link: false
+  inlineMedia: false

   heading: false
   media: false
@@ -160,6 +184,7 @@
     html: false
     special: false
     code: false
+    table: false

    for t in stack
     types[t.type] = true
@@ -173,6 +198,7 @@
    html: false
    special: false
    code: false
+   table: false

   for t in stack
    types[t.type] = true


fixing

fixing

--- configs.coffee
+++ configs.coffee
@@ -22,6 +22,7 @@
 		configs.cache = true
 		configs.mongoURL = process.env.MONGOLAB_URI
 		configs.url = "http://www.vault.tk"
+		configs.port = "80"
 		configs.host = null
 		configs.clef =
 			app_id: process.env.CLEF_APP_ID


`setTimeout`, not `setInterval`.

`setTimeout`, not `setInterval`.

`setInterval` sets up a recurring callback. Calling `setInterval` from
`setInterval` sets up *more* recurring callbacks, which don't get cleared.

See, @k0d3k1ttn? It was a bug!

--- src/challenger.coffee
+++ src/challenger.coffee
@@ -39,13 +39,13 @@
     fn = =>
       @healInt = null
       @storage.hp = Math.min(@maxHP(), @hp() + @healingRate())
-      @healInt = setInterval(fn, 1000) if @hp() < @maxHP()
-    @healInt = setInterval fn, 1000
+      @healInt = setTimeout(fn, 1000) if @hp() < @maxHP()
+    @healInt = setTimeout fn, 1000

   # Public: Halt the healing process begun with `healOverTime`. Important so you don't heal during
   # a Match!
   #
-  stopHealing: -> clearInterval(@healInt) if @healInt?
+  stopHealing: -> clearTimeout(@healInt) if @healInt?

   displayName: -> "@#{@user.name}"


fixing blank search on library update

fixing blank search on library update

--- src/shared/mpd.coffee
+++ src/shared/mpd.coffee
@@ -544,6 +544,7 @@
     if query.length == 0
       @search_results = @library
       @raiseEvent 'libraryupdate'
+      @last_query = query
       return
     words = query.toLowerCase().split(/\s+/)
     query = words.join(" ")


socket.io is dead!!

socket.io is dead!!

--- core/KD.coffee
+++ core/KD.coffee
@@ -47,9 +47,9 @@
     else
       'https://dev-api.koding.com'

-  whoami:-> KD.getSingleton('mainController').getVisitor().currentDelegate
+  whoami:-> KD.getSingleton('mainController').userAccount

-  isLoggedIn:-> @whoami() instanceof koding.api.JAccount
+  isLoggedIn:-> @whoami() instanceof KD.remote.api.JAccount

   isMine:(account)-> @whoami().profile.nickname is account.profile.nickname

@@ -59,8 +59,7 @@

   requireLogin:(errMsg, callback)->
     [callback, errMsg] = [errMsg, callback] unless callback
-    {currentDelegate} = @getSingleton('mainController').getVisitor()
-    if currentDelegate instanceof koding.api.JGuest
+    if KD.whoami() instanceof KD.remote.api.JGuest
       # KDView::handleEvent {type:"NavigationTrigger",pageName:"Login", appId:"Login"}
       new KDNotificationView
         type     : 'growl'


No longer necessary to track collection contents

--- src/collection.coffee
+++ src/collection.coffee
@@ -7,14 +7,6 @@
   indices = (parseInt(index, 10) for index, val of arr when isArrayIndex(index))
   if indices.length then Math.max(indices...) + 1 else 0

-mark = (collection, item) ->
-  if item?._useDefer
-    item._inCollections or= {}
-    item._inCollections[collection] = collection
-
-unmark = (collection, item) ->
-  delete item._inCollections[collection] if item?._inCollections
-
 class exports.Collection
   extend(@prototype, Events)
   constructor: (list) ->
@@ -22,9 +14,7 @@
     @length = getLength(@)
   get: (index) -> @[index]
   set: (index, value) ->
-    unmark(@, @[index])
     @[index] = value
-    mark(@, value)
     @length = getLength(@)
     @trigger("change:#{index}", value)
     @trigger("set", index, value)
@@ -32,7 +22,6 @@
     value
   push: (element) ->
     @[@length] = element
-    mark(@, element)
     @length = getLength(@)
     @trigger("add", element)
     @trigger("change", @)
@@ -41,18 +30,14 @@
   unshift: (item) -> @insertAt(0, item)
   shift: -> @deleteAt(0)
   update: (list) ->
-    unmark(@, element) for element in @
     delete @[index] for index, _ of @ when isArrayIndex(index)
     @[index] = val for val, index in list
-    mark(@, element) for element in list
     @length = getLength(@)
     @trigger("update", list)
     @trigger("change", @)
     list
   splice: (start, deleteCount, list...) ->
-    mark(@, element) for item in list
     deleted = Array.prototype.splice.apply(@, [start, deleteCount, list...])
-    unmark(@, element) for element in deleted
     @length = getLength(@)
     @trigger("update", list)
     @trigger("change", @)
@@ -96,7 +81,6 @@
   find: (fun) ->
     return item for item in @ when fun(item)
   insertAt: (index, value) ->
-    mark(@, value)
     Array.prototype.splice.call(@, index, 0, value)
     @length = getLength(@)
     @trigger("insert", index, value)
@@ -104,7 +88,6 @@
     value
   deleteAt: (index) ->
     value = @[index]
-    unmark(@, value)
     Array.prototype.splice.call(@, index, 1)
     @length = getLength(@)
     @trigger("delete", index, value)


Changed fullpie font size

Changed fullpie font size

--- widgets/fullpie/fullpie.coffee
+++ widgets/fullpie/fullpie.coffee
@@ -59,7 +59,7 @@
       .attr('x', 0)
       .attr('dy', 15)
       .attr('font-size', '90%')
-      .text((d,i) -> Math.round(data[i].value/sum * 100) + '%').attr('font-size', '24px')
+      .text((d,i) -> Math.round(data[i].value/sum * 100) + '%')


Changed fullpie font size

Changed fullpie font size

--- widgets/fullpie/fullpie.coffee
+++ widgets/fullpie/fullpie.coffee
@@ -59,7 +59,7 @@
       .attr('x', 0)
       .attr('dy', 15)
       .attr('font-size', '70%')
-      .text((d,i) -> Math.round(data[i].value/sum * 100) + '%')
+      .text((d,i) -> Math.round(data[i].value/sum * 100) + '%').attr('font-size', '24px')


Cache user status. We would like to implement this correctly in the future.

--- picoCTF-web/web/coffee/dependencies.coffee
+++ picoCTF-web/web/coffee/dependencies.coffee
@@ -6,6 +6,9 @@
   .fail (jqXHR, text) ->
     ga('send', 'event', 'Error', 'APIOffline', url)
     $.notify "The server is currently down. We will work to fix this error right away.", "error"
+  .success (resp) ->
+    if url == "/api/user/status" and resp.status == 1
+      window.userStatus = resp.data

 @redirectIfNotLoggedIn = ->
   apiCall "GET", "/api/user/status", {}


adding in a few new commands that appear in SL-CLSI analysis

--- services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/CommandManager.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/CommandManager.coffee
@@ -1,10 +1,15 @@
 define [], () ->
 	noArgumentCommands = [
 		'item', 'hline', 'lipsum', 'centering', 'noindent', 'textwidth', 'draw',
-		'maketitle', 'newpage', 'verb', 'bibliography', 'fi', 'hfill', 'par',
-		'in', 'sum', 'cdot', 'alpha', 'ldots', 'else', 'linewidth', 'left',
-		'right', 'today', 'clearpage', 'newline', 'endinput', 'mu',
-		'tableofcontents', 'vfill', 'bigskip', 'fill', 'cleardoublepage'
+		'maketitle', 'newpage', 'verb', 'bibliography', 'hfill', 'par',
+		'in', 'sum', 'cdot', 'ldots', 'linewidth', 'left', 'right', 'today',
+		'clearpage', 'newline', 'endinput', 'tableofcontents', 'vfill',
+		'bigskip', 'fill', 'cleardoublepage', 'infty', 'leq', 'geq', 'times',
+		'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'varepsilon', 'zeta',
+		'eta', 'theta', 'vartheta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi',
+		'pi', 'varpi', 'rho', 'varrho', 'sigma', 'varsigma', 'tau', 'upsilon',
+		'phi', 'varphi', 'chi', 'psi', 'omega', 'Gamma', 'Delta', 'Theta',
+		'Lambda', 'Xi', 'Pi', 'Sigma', 'Upsilon', 'Phi', 'Psi', 'Omega'
 	]
 	singleArgumentCommands = [
 		'chapter', 'usepackage', 'section', 'label', 'textbf', 'subsection',
@@ -13,11 +18,12 @@
 		'hspace', 'bibitem', 'url', 'large', 'subsubsection', 'textsc', 'date',
 		'footnote', 'small', 'thanks', 'underline', 'graphicspath', 'pageref',
 		'section*', 'subsection*', 'subsubsection*', 'sqrt', 'text',
-		'normalsize', 'Large', 'paragraph', 'pagestyle', 'thispagestyle',
-		'bibliographystyle'
+		'normalsize', 'footnotesize', 'Large', 'paragraph', 'pagestyle',
+		'thispagestyle', 'bibliographystyle', 'hat'
 	]
 	doubleArgumentCommands = [
-		'newcommand', 'frac', 'renewcommand', 'setlength', 'href', 'newtheorem'
+		'newcommand', 'frac', 'dfrac', 'renewcommand', 'setlength', 'href',
+		'newtheorem'
 	]
 	tripleArgumentCommands = [
 		'addcontentsline', 'newacronym', 'multicolumn'
@@ -25,12 +31,12 @@
 	special = ['LaTeX', 'TeX']

 	rawCommands = [].concat(
-						noArgumentCommands,
-						singleArgumentCommands,
-						doubleArgumentCommands,
-						tripleArgumentCommands,
-						special
-					)
+		noArgumentCommands,
+		singleArgumentCommands,
+		doubleArgumentCommands,
+		tripleArgumentCommands,
+		special
+	)

 	noArgumentCommands = for cmd in noArgumentCommands
 		{


Update atom packages

Update atom packages

--- atom/dot-atom/packages.cson
+++ atom/dot-atom/packages.cson
@@ -1,10 +1,12 @@
 packages: [
+  "advanced-open-file"
   "atom-beautify"
   "atom-clock"
   "atom-fuzzy-grep"
   "atom-html-preview"
   "atom-ide-ui"
   "atom-python-test"
+  "atom-reverser"
   "atomic-rtags"
   "auto-update-plus"
   "autoclose-html"
@@ -17,15 +19,20 @@
   "busy-signal"
   "clang-format"
   "code-peek"
+  "copy-paste"
+  "ex-mode"
   "expand-region"
+  "expose"
   "file-icons"
   "git-blame"
   "git-time-machine"
   "gpp-compiler"
+  "highlight-line"
   "highlight-selected"
   "Hydrogen"
   "hydrogen-launcher"
   "ide-python"
+  "imdone-atom"
   "intentions"
   "jumpy"
   "language-cuda"
@@ -38,14 +45,16 @@
   "linter-clang"
   "linter-flake8"
   "linter-lintr"
+  "linter-markdown"
+  "linter-pydocstyle"
   "linter-pycodestyle"
   "linter-pylama"
-  "linter-pylint"
   "linter-ui-default"
   "MagicPython"
   "markdown-preview-plus"
   "markdown-preview-plus-opener"
   "markdown-table-editor"
+  "markdown-writer"
   "merge-conflicts"
   "minimap"
   "minimap-highlight-selected"
@@ -56,15 +65,23 @@
   "pigments"
   "platformio-ide-terminal"
   "project-manager"
+  "project-plus"
   "python-indent"
   "python-tools"
   "set-syntax"
+  "solarized-light-ui"
+  "sort-lines"
+  "switch-header-source"
+  "symbols-tree-view"
+  "toggle-quotes"
   "toggler"
   "tree-view-git-branch"
   "tree-view-git-status"
+  "tree-view-open-files"
   "vim-mode-plus"
   "vim-mode-plus-keymaps-for-surround"
   "vim-mode-plus-move-to-symbols"
   "zenburn"
   "zenburn-ui"
+  "zotero-citations"
 ]


[grunt] add note about https://github.com/gruntjs/grunt/issues/1541

--- gruntfile.coffee
+++ gruntfile.coffee
@@ -93,6 +93,7 @@
   localesList = _.map(grunt.config('i18n.locales'), 'locale')

   grunt.config.set 'i18n.gettext', new Gettext({ locales: localesList, cwd: grunt.config('path.source.locales'), src: '{,**/}*.{po,mo}' })
+  # @todo For reasons why it has been set that way refer to https://github.com/gruntjs/grunt/issues/1541
   grunt.config.set 'i18n.locales.list', localesList

   grunt.config.set 'data', require('./' + grunt.config('path.source.data'))(grunt)


KiteCache: deny to cache non existing kite instances

--- client/Main/kite/kitecache.coffee
+++ client/Main/kite/kitecache.coffee
@@ -43,6 +43,9 @@

   @cache = (query, kite) ->

+    unless kite?
+      return warn "[KiteCache] KITE NOT PROVIDED, IGNORING TO CACHE"
+
     queryString = @generateQueryString query
     kite = proxifyTransport kite
     try storage[signed queryString] = JSON.stringify kite


More specs

More specs

--- spec/digester-spec.coffee
+++ spec/digester-spec.coffee
@@ -3,7 +3,77 @@
 Biscotto = require 'biscotto'

 describe 'digest', ->
-  describe 'src generation', ->
+  it 'generates method arguments', ->
+    file = """
+      # Public: Some class
+      class Something
+        # Public: this is a function
+        #
+        # * `argument` arg
+        #   * `argument` arg
+        someFunction: ->
+    """
+    json = Parser.generateDigest(file)
+    expect(json.classes.Something.instanceMethods[0].arguments.list.length).toBe 1
+    expect(json.classes.Something.instanceMethods[0].arguments.list[0].children.length).toBe 1
+
+  it 'generates examples', ->
+    file = """
+      # Public: Some class
+      #
+      # ## Examples
+      #
+      # This is an example
+      #
+      # ```js
+      # a = 1
+      # ```
+      class Something
+        # Public: this is a function
+        #
+        # ## Examples
+        #
+        # Method example
+        #
+        # ```js
+        # a = 1
+        # ```
+        someFunction: ->
+    """
+    json = Parser.generateDigest(file)
+    expect(json.classes.Something.examples.length).toBe 1
+    expect(json.classes.Something.examples[0].description).toBe 'This is an example'
+    expect(json.classes.Something.instanceMethods[0].examples.length).toBe 1
+    expect(json.classes.Something.instanceMethods[0].examples[0].description).toBe 'Method example'
+
+  it 'generates events', ->
+    file = """
+      # Public: Some class
+      #
+      # ## Events
+      #
+      # Class Events
+      #
+      # * `event-one` an event
+      #   * `argument` arg
+      class Something
+        # Public: this is a function
+        #
+        # ## Events
+        #
+        # Method Events
+        #
+        # * `event-method` a method event
+        #   * `argument` arg
+        someFunction: ->
+    """
+    json = Parser.generateDigest(file)
+    expect(json.classes.Something.events.length).toBe 1
+    expect(json.classes.Something.events.description).toBe 'Class Events'
+    expect(json.classes.Something.instanceMethods[0].events.length).toBe 1
+    expect(json.classes.Something.instanceMethods[0].events.description).toBe 'Method Events'
+
+  describe 'src link generation', ->
     it 'generates links to github based on repo and version', ->
       file = """
         # Public: Some class
@@ -38,6 +108,54 @@
               description : 'this is a function '
             }]

+  describe 'class methods', ->
+    it 'generates class level methods', ->
+      file = """
+        # Public: Some class
+        class Something
+          # Public: Some class level function
+          @aClassFunction: ->
+
+          # A private class function
+          @privateClassFunction: ->
+
+          # Public: this is a function
+          someFunction: ->
+      """
+      json = Parser.generateDigest file,
+        filename: 'file1.coffee'
+        packageJson:
+          name: 'somerepo'
+          repository: 'https://github.com/atom/somerepo.git'
+          version: '2.3.4'
+
+      expect(json).toEqualJson
+        classes:
+          Something:
+            visibility : 'Public'
+            name : 'Something'
+            filename : 'file1.coffee'
+            summary : 'Some class '
+            description : 'Some class '
+            srcUrl: 'https://github.com/atom/somerepo/blob/v2.3.4/file1.coffee#L2'
+            sections : []
+            classMethods : [{
+              visibility : 'Public'
+              name : 'aClassFunction'
+              sectionName : null
+              srcUrl : 'https://github.com/atom/somerepo/blob/v2.3.4/file1.coffee#L4'
+              summary : 'Some class level function '
+              description : 'Some class level function '
+            }]
+            instanceMethods: [{
+              visibility : 'Public'
+              name : 'someFunction'
+              sectionName : null
+              srcUrl : 'https://github.com/atom/somerepo/blob/v2.3.4/file1.coffee#L10'
+              summary : 'this is a function '
+              description : 'this is a function '
+            }]
+
   describe 'sections', ->
     it 'correctly splits the sections when there are mutiple classes in the file', ->
       file = """
@@ -256,14 +374,6 @@
         description: 'in section two '
         srcUrl: null
       }]
-
-  describe 'digesting Scandal metadata', ->
-    it 'can digest', ->
-      rootPath = fs.realpathSync("spec/fixtures/scandal-metadata.json")
-      metadata = JSON.parse(fs.readFileSync(rootPath))
-
-      json = digest(metadata)
-      console.log JSON.stringify(json, null, '  ')

 # Yeah, recreating some biscotto stuff here...
 class Parser


Disable button until json is fetched

Disable button until json is fetched


fix test

fix test

--- spec/git-diff-spec.coffee
+++ spec/git-diff-spec.coffee
@@ -53,6 +53,7 @@
       buffer = project.buildBuffer(path)
       buffer.setText("Some different text.")
       rootView.open('sample.txt')
+      editor = rootView.getActiveView()
       nextTick = false
       _.nextTick -> nextTick = true
       waitsFor -> nextTick


Spy on setImmediate to make specs pass with React editor enabled

--- spec/git-diff-spec.coffee
+++ spec/git-diff-spec.coffee
@@ -7,6 +7,8 @@
   [editor, editorView, projectPath] = []

   beforeEach ->
+    spyOn(window, 'setImmediate').andCallFake (fn) -> fn()
+
     projectPath = temp.mkdirSync('git-diff-spec-')
     fs.copySync(path.join(__dirname, 'fixtures', 'working-dir'), projectPath)
     fs.moveSync(path.join(projectPath, 'git.git'), path.join(projectPath, '.git'))


decrease live update interval

decrease live update interval

Former-commit-id: e73f05cc6e59bc6f799fefdd5fc1afeebdcc17bc [formerly 707749ef2ee560c07603997802f066ca0c46e376] [formerly f841483e18705a711b5ec2d230657e9a72198594 [formerly a95e4603de59ea66a6a6840a7371c3f64c680c13]] [formerly 603f4ee66b2f5f9b4f783e1ff88e442409d10bad [formerly 60b6b8ba3eec3828d8356740ed0a97be23b7a2f3] [formerly a12f49e96ac659ffb8491be6ade48fdf6dd9f4ba [formerly a12f49e96ac659ffb8491be6ade48fdf6dd9f4ba [formerly a54df86d66485f2213de59604b7707099bb047c7]]]]
Former-commit-id: 403e84edb3edf5c94a190ce2599646d4a677e4e3 [formerly fe5768a438ce296a467388dc80ce4baaca517a28] [formerly 16fb6094a16668999930557d44824a2437750695 [formerly e6c3ecadb67378ca04649da188dc9ac72e9286c3]]
Former-commit-id: cf2e331be73a955b8950f633426617c005ac8884 [formerly ee182084fbdf7c26c5ecef7f8fc192f389fcf75b]
Former-commit-id: f21c51bab175175eaf5247da5bf5a9b7964e341b
Former-commit-id: 02bc675494a2a55c60fb4fcac8710a69a8a87539

--- @client/shared.coffee
+++ @client/shared.coffee
@@ -10,7 +10,7 @@

 window.TRANSITION_SPEED = 700   # Speed of transition from results to crafting (and vice versa)

-window.LIVE_UPDATE_INTERVAL = 1000 * 60
+window.LIVE_UPDATE_INTERVAL = 3000 * 60

 # live updating
 setInterval ->


Context traslation

Context traslation

--- def/pt-br/context.cson
+++ def/pt-br/context.cson
@@ -1,120 +1,120 @@
 Context:
   "atom-workspace":
-    "application:inspect": "Inspect Element"
+    "application:inspect": "Inspecionar Elemento"
   "atom-text-editor":
-    "color-picker:open": "Color Picker"
-    "minimap:toggle": "Toggle Minimap"
+    "color-picker:open": "Selecionador Da Cores"
+    "minimap:toggle": "Alternar Minimap"
   "atom-text-editor, .overlayer":
-    "core:undo": "Undo"
-    "core:redo": "Redo"
-    "core:cut": "Cut"
-    "core:copy": "Copy"
-    "core:paste": "Paste"
-    "core:delete": "Delete"
-    "core:select-all": "Select All"
-    "pane:split-up-and-copy-active-item": "Split Up"
-    "pane:split-down-and-copy-active-item": "Split Down"
-    "pane:split-left-and-copy-active-item": "Split Left"
-    "pane:split-right-and-copy-active-item": "Split Right"
-    "pane:close": "Close Pane"
+    "core:undo": "Desfazer"
+    "core:redo": "Refazer"
+    "core:cut": "Cortar"
+    "core:copy": "Copiar"
+    "core:paste": "Colar"
+    "core:delete": "Excluir"
+    "core:select-all": "Selecionar Tudo"
+    "pane:split-up-and-copy-active-item": "Dividir Para Cima"
+    "pane:split-down-and-copy-active-item": "Dividir Abaixo"
+    "pane:split-left-and-copy-active-item": "Dividir À Esquerda"
+    "pane:split-right-and-copy-active-item": "Dividir À Direita"
+    "pane:close": "Fechar Painel"
   "atom-pane":
-    "pane:split-up-and-copy-active-item": "Split Up"
-    "pane:split-down-and-copy-active-item": "Split Down"
-    "pane:split-left-and-copy-active-item": "Split Left"
-    "pane:split-right-and-copy-active-item": "Split Right"
-    "pane:close": "Close Pane"
+    "pane:split-up-and-copy-active-item": "Dividir Para Cima"
+    "pane:split-down-and-copy-active-item": "Dividir Abaixo"
+    "pane:split-left-and-copy-active-item": "Dividir À Esquerda"
+    "pane:split-right-and-copy-active-item": "Dividir À Direita"
+    "pane:close": "Fechar Painel"
   "atom-text-editor:not([mini])":
-    "encoding-selector:show": "Change Encoding"
-    "spell-check:correct-misspelling": "Correct Spelling"
-    "symbols-view:go-to-declaration": "Go to Declaration"
+    "encoding-selector:show": "Alterar Codificação"
+    "spell-check:correct-misspelling": "Pronúncia Correta"
+    "symbols-view:go-to-declaration": "Ir Para Declaração"
   ".overlayer":
-    "grammar-selector:show": "Change Grammar"
+    "grammar-selector:show": "Alterar Gramática"
   ".image-view":
-    "image-view:reload": "Reload Image"
+    "image-view:reload": "Recarregar Imagem"
   ".tab":
-    "tabs:close-tab": "Close Tab"
-    "tabs:close-other-tabs": "Close Other Tabs"
-    "tabs:close-tabs-to-right": "Close Tabs to the Right"
-    "tabs:close-tabs-to-left": "Close Tabs to the Left"
-    "tabs:close-saved-tabs": "Close Saved Tabs"
-    "tabs:close-all-tabs": "Close All Tabs"
-    "tabs:split-up": "Split Up"
-    "tabs:split-down": "Split Down"
-    "tabs:split-left": "Split Left"
-    "tabs:split-right": "Split Right"
+    "tabs:close-tab": "Fechar Guia"
+    "tabs:close-other-tabs": "Fechar Outras Guias"
+    "tabs:close-tabs-to-right": "Fechar Guias À Direita"
+    "tabs:close-tabs-to-left": "Fecha Guias À Esquerda"
+    "tabs:close-saved-tabs": "Fechar Guias Guardadas"
+    "tabs:close-all-tabs": "Fechar Todas As Guias"
+    "tabs:split-up": "Dividir Para Cima"
+    "tabs:split-down": "Dividir Abaixo"
+    "tabs:split-left": "Dividir À Esquerda"
+    "tabs:split-right": "Dividir À Direita"
   ".tab.texteditor":
-    "tabs:open-in-new-window": "Open In New Window"
+    "tabs:open-in-new-window": "Abra Em Uma Nova Janela"
   ".tab.pending-tab":
-    "tabs:keep-pending-tab": "Keep Pending Tab"
+    "tabs:keep-pending-tab": "Guia Manter Pendente"
   ".tab-bar":
-    "pane:reopen-closed-item": "Reopen Closed Tab"
+    "pane:reopen-closed-item": "Abrir Novamente A Guia Fechada"
   ".tree-view.full-menu":
-    "tree-view:add-file": "New File"
-    "tree-view:add-folder": "New Folder"
-    "tree-view:move": "Rename"
-    "tree-view:duplicate": "Duplicate"
-    "tree-view:remove": "Delete"
-    "tree-view:copy": "Copy"
-    "tree-view:cut": "Cut"
-    "tree-view:paste": "Paste"
-    "tree-view:open-selected-entry-up": "Split Up"
-    "tree-view:open-selected-entry-down": "Split Down"
-    "tree-view:open-selected-entry-left": "Split Left"
-    "tree-view:open-selected-entry-right": "Split Right"
-    "application:add-project-folder": "Add Project Folder"
-    "tree-view:copy-full-path": "Copy Full Path"
-    "tree-view:copy-project-path": "Copy Project Path"
-    "tree-view:open-in-new-window": "Open In New Window"
+    "tree-view:add-file": "Novo Arquivo"
+    "tree-view:add-folder": "Nova Pasta"
+    "tree-view:move": "Renomear"
+    "tree-view:duplicate": "Duplicar"
+    "tree-view:remove": "Excluir"
+    "tree-view:copy": "Copiar"
+    "tree-view:cut": "Cortar"
+    "tree-view:paste": "Colar"
+    "tree-view:open-selected-entry-up": "Dividir Para Cima"
+    "tree-view:open-selected-entry-down": "Dividir Abaixo"
+    "tree-view:open-selected-entry-left": "Dividir À Esquerda"
+    "tree-view:open-selected-entry-right": "Dividir À Direita"
+    "application:add-project-folder": "Adicionar Pasta De Projeto"
+    "tree-view:copy-full-path": "Copiar Caminho Completo"
+    "tree-view:copy-project-path": "Copiar Caminho Do Projeto"
+    "tree-view:open-in-new-window": "Abra Em Uma Nova Janela"
   '.tree-view.full-menu [is="tree-view-file"]':
-    "tree-view:open-selected-entry-up": "Split Up"
-    "tree-view:open-selected-entry-down": "Split Down"
-    "tree-view:open-selected-entry-left": "Split Left"
-    "tree-view:open-selected-entry-right": "Split Right"
+    "tree-view:open-selected-entry-up": "Dividir Para Cima"
+    "tree-view:open-selected-entry-down": "Dividir Abaixo"
+    "tree-view:open-selected-entry-left": "Dividir À Esquerda"
+    "tree-view:open-selected-entry-right": "Dividir À Direita"
   ".tree-view.full-menu .project-root > .header":
-    "tree-view:add-file": "New File"
-    "tree-view:add-folder": "New Folder"
-    "tree-view:move": "Rename"
-    "tree-view:duplicate": "Duplicate"
-    "tree-view:remove": "Delete"
-    "tree-view:copy": "Copy"
-    "tree-view:cut": "Cut"
-    "tree-view:paste": "Paste"
-    "tree-view:open-selected-entry-up": "Split Up"
-    "tree-view:open-selected-entry-down": "Split Down"
-    "tree-view:open-selected-entry-left": "Split Left"
-    "tree-view:open-selected-entry-right": "Split Right"
-    "application:add-project-folder": "Add Project Folder"
-    "tree-view:remove-project-folder": "Remove Project Folder"
-    "tree-view:copy-full-path": "Copy Full Path"
-    "tree-view:copy-project-path": "Copy Project Path"
-    "tree-view:open-in-new-window": "Open In New Window"
+    "tree-view:add-file": "Novo Arquivo"
+    "tree-view:add-folder": "Nova Pasta"
+    "tree-view:move": "Renomear"
+    "tree-view:duplicate": "Duplicar"
+    "tree-view:remove": "Excluir"
+    "tree-view:copy": "Copiar"
+    "tree-view:cut": "Cortar"
+    "tree-view:paste": "Colar"
+    "tree-view:open-selected-entry-up": "Dividir Para Cima"
+    "tree-view:open-selected-entry-down": "Dividir Abaixo"
+    "tree-view:open-selected-entry-left": "Dividir À Esquerda"
+    "tree-view:open-selected-entry-right": "Dividir À Direita"
+    "application:add-project-folder": "Adicionar Pasta De Projeto"
+    "tree-view:remove-project-folder": "Excluir Pasta De Projeto"
+    "tree-view:copy-full-path": "Copiar Caminho Completo"
+    "tree-view:copy-project-path": "Copiar Caminho Do Projeto"
+    "tree-view:open-in-new-window": "Abra Em Uma Nova Janela"
   ".platform-darwin .tree-view.full-menu":
-    "tree-view:show-in-file-manager": "Show in Finder"
+    "tree-view:show-in-file-manager": "Mostrar No Finder"
   ".platform-win32 .tree-view.full-menu":
-    "tree-view:show-in-file-manager": "Show in Explorer"
+    "tree-view:show-in-file-manager": "Mostrar No Explorer"
   ".platform-linux .tree-view.full-menu":
-    "tree-view:show-in-file-manager": "Show in File Manager"
+    "tree-view:show-in-file-manager": "Mostrar No File Manager"
   ".tree-view > li.directory":
-    "project-find:show-in-current-directory": "Search in Directory"
+    "project-find:show-in-current-directory": "Pesquisar No Diretório"
   ".tree-view.multi-select":
-    "tree-view:remove": "Delete"
-    "tree-view:copy": "Copy"
-    "tree-view:cut": "Cut"
-    "tree-view:paste": "Paste"
+    "tree-view:remove": "Excluir"
+    "tree-view:copy": "Copiar"
+    "tree-view:cut": "Cortar"
+    "tree-view:paste": "Colar"
   "atom-pane[data-active-item-path] .item-views":
-    "tree-view:reveal-active-file": "Reveal in Tree View"
+    "tree-view:reveal-active-file": "Revelar Em Vista De Árvore"
   "atom-pane[data-active-item-path] .tab.active":
-    "tree-view:rename": "Rename"
-    "tree-view:reveal-active-file": "Reveal in Tree View"
+    "tree-view:rename": "Renomear"
+    "tree-view:reveal-active-file": "Revelar Em Vista De Árvore"
   ".platform-darwin atom-pane[data-active-item-path] .tab.active":
-    "tree-view:show-current-file-in-file-manager": "Show In Finder"
+    "tree-view:show-current-file-in-file-manager": "Mostrar No Finder"
   ".platform-win32 atom-pane[data-active-item-path] .tab.active":
-    "tree-view:show-current-file-in-file-manager": "Show In Explorer"
+    "tree-view:show-current-file-in-file-manager": "Mostrar No Explorer"
   ".platform-linux atom-pane[data-active-item-path] .tab.active":
-    "tree-view:show-current-file-in-file-manager": "Show In File Manager"
+    "tree-view:show-current-file-in-file-manager": "Mostrar No File Manager"
   ".platform-darwin atom-text-editor:not([mini])":
-    "tree-view:show-current-file-in-file-manager": "Show In Finder"
+    "tree-view:show-current-file-in-file-manager": "Mostrar No Finder"
   ".platform-win32 atom-text-editor:not([mini])":
-    "tree-view:show-current-file-in-file-manager": "Show In Explorer"
+    "tree-view:show-current-file-in-file-manager": "Mostrar No Explorer"
   ".platform-linux atom-text-editor:not([mini])":
-    "tree-view:show-current-file-in-file-manager": "Show In File Manager"
+    "tree-view:show-current-file-in-file-manager": "Mostrar No File Manager"


New spec for #213

New spec for #213

--- spec/snippets-spec.coffee
+++ spec/snippets-spec.coffee
@@ -122,6 +122,14 @@
             body: """
               line ${1:1}
                 ${2:body...}
+            """
+
+          "multiline starting with tabstop":
+            prefix: "t4b"
+            body: """
+              $1 = line 1 {
+                line 2
+              }
             """

           "nested tab stops":
@@ -403,53 +411,65 @@
             expect(editor.lineTextForBufferRow(3)).toBe "      line 2"
             expect(editor.getCursorBufferPosition()).toEqual [3, 12]

-          it "places tab stops correctly", ->
-            expect(editor.getSoftTabs()).toBeTruthy()
-            editor.setCursorScreenPosition([2, Infinity])
-            editor.insertText ' t3'
-            atom.commands.dispatch editorElement, 'snippets:expand'
-
-            markers = editor.getMarkers()
-            expect(markers.length).toBe 2
-            expect(markers[0].getBufferRange().start).toEqual row: 3, column: 12
-            expect(markers[0].getBufferRange().end).toEqual markers[0].getBufferRange().start
-            expect(markers[1].getBufferRange().start).toEqual row: 4, column: 4
-            expect(markers[1].getBufferRange().end).toEqual markers[1].getBufferRange().start
-
-          it "does not change the relative positioning of the tab stops when inserted multiple times", ->
-            editor.setCursorScreenPosition([2, Infinity])
-            editor.insertNewline()
-            editor.insertText 't4'
-            atom.commands.dispatch editorElement, 'snippets:expand'
-
-            markers = editor.getMarkers()
-            expect(markers.length).toBe 2
-            expect(markers[0].getBufferRange().start).toEqual row: 3, column: 9
-            expect(markers[0].getBufferRange().end).toEqual row: 3, column: 10
-            expect(markers[1].getBufferRange().start).toEqual row: 4, column: 6
-            expect(markers[1].getBufferRange().end).toEqual row: 4, column: 13
-
-            atom.commands.dispatch editorElement, 'snippets:next-tab-stop'
-            editor.insertText 't4'
-            atom.commands.dispatch editorElement, 'snippets:expand'
-
-            markers = editor.getMarkers()
-            expect(markers.length).toBe 4
-            expect(markers[2].getBufferRange().start).toEqual row: 4, column: 11
-            expect(markers[2].getBufferRange().end).toEqual row: 4, column: 12
-            expect(markers[3].getBufferRange().start).toEqual row: 5, column: 8
-            expect(markers[3].getBufferRange().end).toEqual row: 5, column: 15
-
-            editor.setText('') # Clear editor
-            editor.insertText 't4'
-            atom.commands.dispatch editorElement, 'snippets:expand'
-
-            markers = editor.getMarkers()
-            expect(markers.length).toBe 6
-            expect(markers[4].getBufferRange().start).toEqual row: 0, column: 5
-            expect(markers[4].getBufferRange().end).toEqual row: 0, column: 6
-            expect(markers[5].getBufferRange().start).toEqual row: 1, column: 2
-            expect(markers[5].getBufferRange().end).toEqual row: 1, column: 9
+      describe "when the snippet spans multiple lines", ->
+        it "places tab stops correctly", ->
+          expect(editor.getSoftTabs()).toBeTruthy()
+          editor.setCursorScreenPosition([2, Infinity])
+          editor.insertText ' t3'
+          atom.commands.dispatch editorElement, 'snippets:expand'
+
+          markers = editor.getMarkers()
+          expect(markers.length).toBe 2
+          expect(markers[0].getBufferRange().start).toEqual row: 3, column: 12
+          expect(markers[0].getBufferRange().end).toEqual markers[0].getBufferRange().start
+          expect(markers[1].getBufferRange().start).toEqual row: 4, column: 4
+          expect(markers[1].getBufferRange().end).toEqual markers[1].getBufferRange().start
+
+        it "indents the subsequent lines of the snippet based on the indent level before the snippet is inserted", ->
+          editor.setCursorScreenPosition([2, Infinity])
+          editor.insertNewline()
+          editor.insertText 't4b'
+          atom.commands.dispatch editorElement, 'snippets:expand'
+
+          expect(editor.lineTextForBufferRow(3)).toBe "     = line 1 {" # 4 + 1 spaces (because the tab stop is invisible)
+          expect(editor.lineTextForBufferRow(4)).toBe "      line 2"
+          expect(editor.lineTextForBufferRow(5)).toBe "    }"
+          expect(editor.getCursorBufferPosition()).toEqual [3, 4]
+
+        it "does not change the relative positioning of the tab stops when inserted multiple times", ->
+          editor.setCursorScreenPosition([2, Infinity])
+          editor.insertNewline()
+          editor.insertText 't4'
+          atom.commands.dispatch editorElement, 'snippets:expand'
+
+          markers = editor.getMarkers()
+          expect(markers.length).toBe 2
+          expect(markers[0].getBufferRange().start).toEqual row: 3, column: 9
+          expect(markers[0].getBufferRange().end).toEqual row: 3, column: 10
+          expect(markers[1].getBufferRange().start).toEqual row: 4, column: 6
+          expect(markers[1].getBufferRange().end).toEqual row: 4, column: 13
+
+          atom.commands.dispatch editorElement, 'snippets:next-tab-stop'
+          editor.insertText 't4'
+          atom.commands.dispatch editorElement, 'snippets:expand'
+
+          markers = editor.getMarkers()
+          expect(markers.length).toBe 4
+          expect(markers[2].getBufferRange().start).toEqual row: 4, column: 11
+          expect(markers[2].getBufferRange().end).toEqual row: 4, column: 12
+          expect(markers[3].getBufferRange().start).toEqual row: 5, column: 8
+          expect(markers[3].getBufferRange().end).toEqual row: 5, column: 15
+
+          editor.setText('') # Clear editor
+          editor.insertText 't4'
+          atom.commands.dispatch editorElement, 'snippets:expand'
+
+          markers = editor.getMarkers()
+          expect(markers.length).toBe 6
+          expect(markers[4].getBufferRange().start).toEqual row: 0, column: 5
+          expect(markers[4].getBufferRange().end).toEqual row: 0, column: 6
+          expect(markers[5].getBufferRange().start).toEqual row: 1, column: 2
+          expect(markers[5].getBufferRange().end).toEqual row: 1, column: 9

       describe "when multiple snippets match the prefix", ->
         it "expands the snippet that is the longest match for the prefix", ->