Do not cache avatar unless 200 with image

--- lib/atom-io-client.coffee
+++ lib/atom-io-client.coffee
@@ -137,21 +137,27 @@
     path.join @getCachePath(), "#{login}-*([0-9])"

   fetchAndCacheAvatar: (login, callback) ->
-    if @online()
+    if not @online()
+      callback(null, null)
+    else
       imagePath = @avatarPath login
-      writeStream = fs.createWriteStream imagePath
-      writeStream.on 'finish', -> callback(null, imagePath)
-      writeStream.on 'error', (error) -> callback(error)
-
       request ?= require 'request'
-      readStream = request({
+      requestObject = {
         url: "https://avatars.githubusercontent.com/#{login}"
         headers: DefaultRequestHeaders
-      })
-      readStream.on 'error', (error) -> callback(error)
-      readStream.pipe(writeStream)
-    else
-      callback(null, null)
+      }
+      request.head requestObject, (error, response, body) ->
+        if error? or response.statusCode isnt 200 or not response.headers['content-type'].startsWith('image/')
+          callback(error)
+        else
+          writeStream = fs.createWriteStream imagePath
+          writeStream.on 'finish', -> callback(null, imagePath)
+          writeStream.on 'error', (error) ->
+            writeStream.end()
+            fs.unlinkSync imagePath
+            callback(error)
+          request(requestObject).pipe(writeStream)
+
   # The cache expiry doesn't need to be clever, or even compare dates, it just
   # needs to always keep around the newest item, and that item only. The localStorage
   # cache updates in place, so it doesn't need to be purged.


allow new event from event constructor to bubble

--- view/frontend/web/coffee/addressfinder-magento.coffee
+++ view/frontend/web/coffee/addressfinder-magento.coffee
@@ -152,7 +152,7 @@
         @postcodeElement.value = metadata.postcode

       if typeof Event == "function"
-        event = new Event("change")
+        event = new Event("change", {bubbles: true, cancellable: true})
       else
         event = document.createEvent("Event")
         event.initEvent("change", true, true)


Tweak description

Tweak description

--- lib/incompatible-packages-view.coffee
+++ lib/incompatible-packages-view.coffee
@@ -38,8 +38,8 @@
         """

         @p """
-          These packages should now ship versions of these native modules that
-          are compatible with node 0.11.13.
+          The packages listed should now ship versions of these native modules
+          that are compatible with node 0.11.13.
         """

         @p """


Updated Atom's Configurations

Updated Atom's Configurations

--- copies/atom/atom.copy/init.coffee
+++ copies/atom/atom.copy/init.coffee
@@ -12,3 +12,8 @@
 # Close the tree at Startup
 #workspaceView = atom.views.getView(atom.workspace)
 #atom.commands.dispatch workspaceView, 'tree-view:toggle'
+
+# --------------------------------------------- #
+# | Set dimensions
+# --------------------------------------------- #
+atom.setWindowDimensions({"width": 1300, "height": 700})


renamed easing -> ease for homogeneity  with d3 easing functions

--- client/Plane.coffee
+++ client/Plane.coffee
@@ -11,10 +11,10 @@
             set: (newParent) ->
                 # Steps I'm taking to animate the change of parent CS (Plane):
                 # 1: Compute our current real world coordinates (the absolute transform oldM3d)
-                # 2: Change the parent CS
+                # 2: Change the parent Plane
                 #      (note that now our real world coordinates changed newM3d)
                 # 3: Find the transform (newM3d_to_oldM3d) that leaves us in exactly the same position
-                #    we were before even though our CS is now relative to a different parent (newParent)
+                #    we were before even though our Plane is now relative to a different parent (newParent)
                 #    and apply it instantaneously.
                 #    Now we are exactly were we started, but our transform is relative to the new parent
                 # 4: Compute oldM3d_to_newM3d (by inverting newM3d_to_oldM3d), decompose it in its primitive
@@ -34,7 +34,7 @@

                 oldM3d_to_newM3d = newM3d_to_oldM3d.inverse()
                 t = unmatrix(oldM3d_to_newM3d)
-                t.duration = 10000;
+                t.duration = 1000;
                 this.do t

                 @_parent
@@ -49,7 +49,7 @@
         m3d = new WebKitCSSMatrix(@ownMatrix)
         discardUpTo = 0
         for i, o of @transformQueue
-            factor = o.easing (now - o.start - o.delay) / o.duration
+            factor = o.ease (now - o.start - o.delay) / o.duration
             if o.translate?
                 # todo: how to put every func parameter in its own line?
                 m3d = m3d.translate (o.translate.x ? 0) * factor, (o.translate.y ? 0) * factor, (o.translate.z ? 0) * factor
@@ -85,9 +85,9 @@
         # o.rotate
         # o.around

-        o.duration ?= 10000
+        o.duration ?= 1000
         o.delay ?= 0
-        o.easing ?= d3.ease("cubic-in-out")
+        o.ease ?= d3.ease("cubic-in-out")
         o.start ?= +new Date()

         @transformQueue.push o


event better tab stuff

event better tab stuff

--- client/front.coffee
+++ client/front.coffee
@@ -475,7 +475,7 @@
         @con = new Connection()
         @filename = "foo"

-        @tabwidth = 4
+        @tab_width = 4

         # grab common elements
         @$doc = $(document)
@@ -589,7 +589,7 @@
     save: =>
         text = @$pad.val()
         # replace tabs by spaces
-        space = (" " for _ in [0...@tabwidth]).join("")
+        space = (" " for _ in [0...@tab_width]).join("")
         text = text.replace(/\t/g, space)
         # strip trailing white space onlines
         text = text.replace(/[ \r]*\n/g,"\n").replace(/\s*$/g, "\n")
@@ -622,11 +622,14 @@

     autocomplete: =>
         [text, [start, end], s] = @get_text_state()
+        # if some thing is selected don't auto complete
+        if start != end
+            return false
         string = text.substr(0, start)
         at = text[start]
         string = string.match(/\w+$/)
-        print "ac", at, string
-        if (not at or at.match("\s")) and string
+        # only when at char is splace and there is a string under curser
+        if (not at or at.match(/\s/)) and string
             options = {}
             words = text.split(/\W+/).sort()
             if words
@@ -641,9 +644,8 @@
         return false

     insert_text: (add) =>
-        print "insert", add
         [text, [start, end], s] = @get_text_state()
-        text = text[..start] + add + text[end..]
+        text = text[..start-1] + add + text[end..]
         start += add.length
         end += add.length
         print [text, [start, end], s]
@@ -662,26 +664,46 @@
         @set_text(text)

         if just_tab
-            @$pad[0].selectionStart = real + 4
+            @$pad[0].selectionStart = real + @tab_width
             @$pad[0].selectionEnd = @$pad[0].selectionStart
         else
             @$pad[0].selectionStart = @lines[start][1]
-            @$pad[0].selectionEnd = @lines[end][2] + 4 * (end-start+1)
+            @$pad[0].selectionEnd = @lines[end][2] + @tab_width * (end-start+1)

     deindent: =>
         [start, end] = @selected_line_range()
+        if start == end
+           real = @$pad[0].selectionStart
+           just_tab = true
+
         lines = (l[3] for l in @lines)
+        old_length = 0
         for n in [start..end]
-            for t in [0...4]
+            old_length += lines[n].length + 1
+            for t in [0...@tab_width]
                 if lines[n][0] == " "
                     lines[n] = lines[n][1..]
         text = (l for l in lines).join("\n")
         @set_text(text)
-        @$pad[0].selectionStart = @lines[start][1]
-        full_length = 0
+
+        new_length = 0
         for n in [start..end]
-            full_length += lines[n].length
-        @$pad[0].selectionEnd = @$pad[0].selectionStart + full_length
+            new_length += lines[n].length + 1
+
+        print "o/n", old_length, new_length
+
+        if just_tab
+            if real - @tab_width > @lines[start][1]
+                @$pad[0].selectionStart = real - @tab_width
+            else
+                @$pad[0].selectionStart = @lines[start][1]
+            @$pad[0].selectionEnd = @$pad[0].selectionStart
+        else
+            @$pad[0].selectionStart = @lines[start][1]
+
+            @$pad[0].selectionEnd = @$pad[0].selectionStart + new_length
+
+

     set_text: (text) ->
         @$pad.val(text)


alignment

alignment

--- app/generator.coffee
+++ app/generator.coffee
@@ -127,7 +127,7 @@
       ].map(copyFile)

       @template "_robots.txt"                      , "src/robots.txt"
-      @template "_README.md"                      , "README.md"
+      @template "_README.md"                       , "README.md"
       @template "_.env.example"                    , ".env.example"
       @template "_.env.example"                    , ".env"
       @template "docker/apache/_local.apache.conf" , "docker/apache/local.apache.conf"


Display terminal icon on tab

Display terminal icon on tab

This makes terminal more easy to find. :P

--- lib/TermView.coffee
+++ lib/TermView.coffee
@@ -83,6 +83,9 @@
     titleTemplate = @opts.titleTemplate or "({{ bashName }})"
     renderTemplate titleTemplate, @vars

+  getIconName: ->
+    "terminal"
+
   attachEvents: ->
     @resizeToPane = @resizeToPane.bind this
     @attachResizeEvents()


activeitems: sort tags by followers count

--- workers/social/lib/social/models/activeitems.coffee
+++ workers/social/lib/social/models/activeitems.coffee
@@ -65,7 +65,7 @@
     group = options.group or "koding"
     {select: selector} = nameMapping.topic
     selector.group = group
-    JTag.some selector, {limit:10}, callback
+    JTag.some selector, {limit:10, "counts.followers":-1}, callback

   @_fetchUsers = (options={}, callback)->
     {client}   = options


Social: check if the incoming account is allowed to be added into @group

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -959,17 +959,21 @@
     [callback, roles] = [roles, callback]  unless callback
     roles ?= ['member']

-    kallback = =>
-      callback()
-      @updateCounts()
-      @emit 'MemberAdded', member  if 'member' in roles
-
-    queue = roles.map (role) => =>
-      @addMember member, role, queue.fin.bind queue
-
-    # We were creating group member VMs here before
-    # I've deleted them, ask me if you need more information ~ GG
-    dash queue, -> kallback()
+    @fetchBlockedAccount { targetId: member.getId() }, (err, account_) =>
+      return callback err if err
+      return callback new KodingError 'This account is blocked'  if account_
+
+      kallback = =>
+        callback()
+        @updateCounts()
+        @emit 'MemberAdded', member  if 'member' in roles
+
+      queue = roles.map (role) => =>
+        @addMember member, role, queue.fin.bind queue
+
+      # We were creating group member VMs here before
+      # I've deleted them, ask me if you need more information ~ GG
+      dash queue, -> kallback()

   each:(selector, rest...) ->
     selector.visibility = 'visible'


markdowndoc: added a more comprehensible/extensible markdown generation/syntax hl'ing to markdowndoc

--- workers/social/lib/social/models/markdowndoc.coffee
+++ workers/social/lib/social/models/markdowndoc.coffee
@@ -10,11 +10,44 @@
       html      : String
       checksum  : String

+  @generateHTML=(content)->
+    options =
+      gfm : yes
+      sanitize : yes
+      highlight : (code, lang)->
+        hljs = require('highlight.js')
+        try
+          hljs.highlight(lang, code).value
+        catch e
+          try
+            hljs.highlightAuto(code).value
+          catch _e
+            code
+      breaks : yes
+      langPrefix : 'lang-'
+    marked = require('marked')
+    marked.setOptions options
+    marked content
+
+  @generateChecksum=(content)->
+    require('crypto')
+      .createHash('sha1')
+      .update(content)
+      .digest 'hex'
+
   update:(atomically)->
     setOp = atomically.$set ?= {}
-    setOp.html = require('marked') setOp.content
-    setOp.checksum = require('crypto')
-      .createHash('sha1')
-      .update(@content)
-      .digest 'hex'
-    Module::update.apply this, arguments+    setOp.html = JMarkdownDoc.generateHTML setOp.content
+    setOp.checksum = JMarkdownDoc.generateChecksum setOp.content
+    Module::update.apply this, arguments
+
+  @create = (formData, callback)->
+
+    data = formData
+    data.html = @generateHTML data.content
+    data.checksum = @generateChecksum data.content
+
+    markdownDoc = new @ data
+    markdownDoc.save (err)->
+      if err then callback err
+      else callback null, markdownDoc


client: remove recaptcha

client: remove recaptcha

--- workers/social/lib/social/render/scriptblock.coffee
+++ workers/social/lib/social/render/scriptblock.coffee
@@ -80,8 +80,6 @@
     <!-- End Google Analytics -->

     #{if argv.t then "<script src=\"/a/js/tests.js\"></script>" else ''}
-
-    <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>
     """

   kallback = ->


updated config to allow packaging as a library.

--- mimosa-config.coffee
+++ mimosa-config.coffee
@@ -1,5 +1,5 @@
 exports.config =
-  modules: ['lint', 'server', 'require', 'minify', 'live-reload', 'web-package']
+  modules: ['lint', 'server', 'require', 'minify', 'live-reload', 'web-package','require-library-package']
   lint:
     compiled:
       css: true
@@ -24,4 +24,22 @@
         #'overqualified-elements': false
         #'floats': false
         #'qualified-headings': false
-        #'errors': false+        #'errors': false
+  webPackage:
+    exclude: ['build','README.md','node_modules','mimosa-config.coffee','mimosa-config.js','assets','.git','.gitignore']
+  libraryPackage:
+    packaging:
+      shimmedWithDependencies: true
+      noShimNoDependencies: true
+      noShimWithDependencies: true
+    overrides:
+      shimmedWithDependencies: {}
+      noShimNoDependencies: {}
+      noShimWithDependencies: {}
+    outFolder: 'build'
+    cleanOutFolder: true
+    globalName: 'BericoTimeline'
+    name: 'berico-timeline.js'
+    main: 'app/timeline/timeline'
+    mainConfigFile: 'javascripts/main.js'
+    removeDependencies: ['d3']


admins: add myself & geraint to dummy admins

--- workers/social/lib/social/models/account/index.coffee
+++ workers/social/lib/social/models/account/index.coffee
@@ -542,9 +542,8 @@
     , (err, count)=>
       @update ($set: 'counts.topics': count), ->

-  dummyAdmins = [ "sinan", "devrim", "aleksey-m", "gokmen", "chris",
-                  "arvidkahl", "testdude", "blum", "neelance", "halk",
-                  "fatihacet", "chrisblum" ]
+  dummyAdmins = [ "sinan", "devrim","gokmen", "chris", "testdude", "blum", "neelance", "halk",
+                  "fatihacet", "chrisblum", "sent-hil", "kiwigeraint"]

   flagAccount: secure (client, flag, callback)->
     {delegate} = client.connection


Share collaboration user add method

Share collaboration user add method

--- workers/social/lib/social/models/collaboration.coffee
+++ workers/social/lib/social/models/collaboration.coffee
@@ -14,6 +14,7 @@
     sharedMethods :
       static      :
         stop      : (signature String, Object, Function)
+        add       : (signature String, Object, Function)

   drive = null


Added reset functionality.

Added reset functionality.

--- chip-8.coffee
+++ chip-8.coffee
@@ -458,20 +458,24 @@

 # # Main

+chip8 = () ->
+  reset()
+
 reset = () ->
+  I = 0 # "I" register
+  DT = 0 # Delay timer
+  ST = 0 # Sound timer
+  PC = 0 # Program counter
+  SP = 0 # Stack pointer
+
   memory = (0 for i in [0...memorySize])
   registers = (0 for i in [0..registerCount])
   stack = (0 for i in [0...stackSize])
   inst00E0CLS()
   addSpritesToMemory()
-  I = 0
-  DT = 0
-  ST = 0
-  PC = 0
-  SP = 0

 #
 main = () ->
   console.log "Start"
-  reset()
+  chip8()
   console.log "Stop"


Partially rewritten to work with max-height values instead of explicit heights

--- app/assets/javascripts/lib/components/section_toggle.js.coffee
+++ app/assets/javascripts/lib/components/section_toggle.js.coffee
@@ -16,7 +16,7 @@
 #  args =
 #    selector: 'div.partner-review-facilities'
 #    text: ['more facilities', 'less facilities']
-#    maxheight: maxheight
+#    maxHeight: maxheight
 #    delegate:
 #      onupdate: (section, state) =>
 #        console.log(state)
@@ -30,60 +30,61 @@

   class SectionToggle

-    @version: '0.0.1'
+    @version: '0.0.2'

     constructor: (@args={}) ->
       @target = $(@args.selector)
+      @target.toggleClass('is-open')
       @wrapper = $(@target).find('.js-read-more-wrapper')
-      @baseHeight = @wrapper.height()
-      @addHandler()
+      @wrapper.css({'overflow': 'hidden'})
+      @includeHandler = @findHeight(@args.maxHeight)
+      @addHandler() if @includeHandler

-    findheight: () ->
+    findHeight: (maxHeight) ->
       nodes = @wrapper.children()
       height = 0
-      for (i = 0; i< nodes.length; i++ )
+      i = 0
+      while ( height < maxHeight )
         height += $(nodes[i]).height()
-      height
-
+        i++
+      return height >= maxHeight
+
     addHandler: ->
-      if @findHeight() > @args.maxHeight
-        @template = "
#{(@args.text)[0]}
"
-        if @args.shadow
-          @template = "
" + @template + "
"
-        @wrapper.append(@template)
-        @bindEvent()
-        @close()
+      @template = "
#{(@args.text)[0]}
"
+      if @args.shadow
+        @template = "
" + @template + "
"
+      @wrapper.append(@template)
+      @handler = @target.find 'div.js-handler'
+      @bindEvent()
+      @close()

     bindEvent: ->
-      $(@target).find('div.js-handler').on('click', (e) =>
+      @handler.on 'click', (e) =>
         e.preventDefault()
         if @state is 'close' then @open() else @close()
         @onUpdate()
-      )

     open: ->
-      @wrapper.height(@baseHeight)
+      @wrapper.css({'max-height': '5000px'})
       # Wait for the transition to finish before taking away the gradient mask
+      # TODO: check for transitionend and use that instead if it's available
       setTimeout(=>
-        $(@target).addClass('is-open').removeClass('is-closed')
+        @toggleClasses()
       , 500)
       @setHandlerText(@args.text[1])
       @state = 'open'

     close: ->
-      @wrapper.css({'overflow': 'hidden'})
-      if @args.shadow is true
-        @wrapper.height(@args.maxHeight-(@args.maxHeight%18)-2)
-      else
-        @wrapper.height(@args.maxHeight)
-      $(@target).addClass('is-closed').removeClass('is-open')
+      @wrapper.css({'max-height': @args.maxHeight + 'px'})
+      @toggleClasses()
       @setHandlerText(@args.text[0])
       @state = 'close'

     setHandlerText: (_text) ->
-      $(@target).find('div.js-handler').text(_text)
+      @handler.text _text

     onUpdate: ->
       @args.delegate.onUpdate(@,@state, @args.selector) if @args.delegate && @args.delegate.onUpdate

-
+    toggleClasses: ->
+      @target.toggleClass('is-open is-closed')


Add removeClassPrefix method

Add removeClassPrefix method

--- drop.coffee
+++ drop.coffee
@@ -37,6 +37,11 @@
             ).eq(0)

         return (/fixed/).test(this.css('position')) or (if not scrollParent.length then $('html') else scrollParent)
+
+
+$.fn.removeClassPrefix = (prefix) ->
+    $(@).attr 'class', (index, className) ->
+        className.replace(new RegExp("\\b#{ prefix }\\S+", 'g'), '')

 $ ->


correct hostname depending on env

correct hostname depending on env

--- models/person.coffee
+++ models/person.coffee
@@ -9,7 +9,7 @@
     everyauth: { User: () -> Person }
   github:
     everyauth:
-      myHostname: 'http://nodeknockout.com'
+      myHostname: if env.production then 'http://nodeknockout.com' else 'http://localhost:8000'
       appId: env.secrets.github_app_id
       appSecret: env.secrets.github_secret
       redirectPath: '/'


Add skeleton for the script

Add skeleton for the script


Reduce verbosity of programmer excuses

Reduce verbosity of programmer excuses

--- scripts/programmer-excuses.coffee
+++ scripts/programmer-excuses.coffee
@@ -27,6 +27,6 @@
       msg.send dialog

 module.exports = (robot) ->
-  robot.hear /\sbug[\s?.!]/i, action
-  robot.hear /\sexcuse\s/i, action
-  robot.hear /what (is|was) the (problem|bug|issue)/i, action
+  robot.respond /\sbug[\s?.!]/i, action
+  robot.respond /\sexcuse\s/i, action
+  robot.respond /what (is|was) the (problem|bug|issue)/i, action


more matches for excuses

more matches for excuses

--- scripts/programmer-excuses.coffee
+++ scripts/programmer-excuses.coffee
@@ -15,7 +15,7 @@
 scraper = require 'scraper'

 module.exports = (robot) ->
-  robot.hear /bug/i, (msg) ->
+  robot.hear /(bug|excuse|what was (is|was) the problem|what (is|was) wrong)/i, (msg) ->
     options = {
       'uri': 'http://programmerexcuses.com',
       'headers': {


fix syntax error

fix syntax error

--- scripts/programmer-excuses.coffee
+++ scripts/programmer-excuses.coffee
@@ -1,14 +1,29 @@
+# Description:
+#   Hubot will handle your bug reports now
+#
+# Dependencies:
+#   "scraper": "0.0.9"
+#
+# Configuration:
+#   None
+#
+# Commands:
+#
+# Author:
+#   capoferro (shamelessly copied from rrix's archer.coffee)
+
 scraper = require 'scraper'

 module.exports = (robot) ->
-        robot.hear /bug/i, (msg) ->
-                options = {
-                        'uri': 'http://programmerexcuses.com',
-                        'headers': {
-                                'User-Agent': 'User-Agent: Programmer Excuses Bot for Hubot'
-                        }
-                scraper options, (err, jQuery) ->
-                        throw err if err
-                        quote = jQuery('.wrapper a').toArray()[0]
-                        dialog = '' + jQuery(quote).text().trim()
-                        msg.send dialog+  robot.hear /bug/i, (msg) ->
+    options = {
+      'uri': 'http://programmerexcuses.com',
+      'headers': {
+        'User-Agent': 'User-Agent: Programmer Excuses Bot for Hubot'
+      }
+    }
+    scraper options, (err, jQuery) ->
+      throw err if err
+      quote = jQuery('.wrapper a').toArray()[0]
+      dialog = '' + jQuery(quote).text().trim()
+      msg.send dialog


Change buttons and improve client updating

--- client.coffee
+++ client.coffee
@@ -22,6 +22,10 @@
 	Dom.br()

 renderUpdateInfo = !->
+	# Cause full update when databases get or statistics updated
+	Db.shared.get('latestUpdate')
+	Db.shared.get('lastStatisticUpdate')
+
 	Obs.observe ->
 		Dom.h2 "Data info and updates"
 		Dom.div !->
@@ -29,7 +33,13 @@
 			Dom.style marginLeft: '-4px', display: 'inline-block'
 			if Db.shared.get('updating')? and Db.shared.get('updating') == 'true'
 				doneCount = Db.shared.get('doneCount')
-				Ui.button "Updating... (" + doneCount + "/" + numberOfRegisters + ")", !->
+				buttonText = ""
+				if Db.shared.get('doneSendingRequests') == 'no'
+					buttonText = "Updating"
+				else
+					buttonText = "Waiting"
+				buttonText += "... (" + doneCount + "/" + numberOfRegisters + ")"
+				Ui.button buttonText, !->
 					Modal.show "Databases already updating", !->
 						Dom.div !->
 							Dom.text "The databases are already updating, wait a while until the update is finished or force update them now."
@@ -40,8 +50,6 @@
 								Db.shared.set 'updating', 'true'
 								Db.shared.set 'doneCount', 0
 					,['cancel', "Cancel", 'do', "Force update"]
-				Dom.text "Done sending requests: "+Db.shared.get('doneSendingRequests')
-				Dom.br()
 			else
 				Ui.button "Update database", !->
 					Server.sync 'gatherHistory', !->


Adding all Hammer gestures strings

Adding all Hammer gestures strings

--- framer/Events.coffee
+++ framer/Events.coffee
@@ -35,17 +35,49 @@

 # Gesture events
 _gestures = []
+
+# Pan
+_gestures.push Events.Pan = "pan" # This event includes all the other Pan events
+_gestures.push Events.PanStart = "panstart"
+_gestures.push Events.PanMove = "panmove"
+_gestures.push Events.PanEnd = "panend"
+_gestures.push Events.PanCancel = "pancancel"
+_gestures.push Events.PanLeft = "panleft"
+_gestures.push Events.PanRight = "panright"
+_gestures.push Events.PanUp = "panup"
+_gestures.push Events.PanDown = "pandown"
+
+# Pinch
+_gestures.push Events.Pinch = "pinch" # This event includes all the other Pinch events
+_gestures.push Events.PinchStart = "pinchstart"
+_gestures.push Events.PinchMove = "pinchmove"
+_gestures.push Events.PinchEnd = "pinchend"
+_gestures.push Events.PinchCancel = "pinchcancel"
+_gestures.push Events.PinchIn = "pinchin"
+_gestures.push Events.PinchOut = "pinchout"
+
+# Press
 _gestures.push Events.Press = "press"
 _gestures.push Events.PressUp = "pressup"
-_gestures.push Events.Pinch = "pinch"
-_gestures.push Events.PinchStart = "pinchstart"
-_gestures.push Events.PinchEnd = "pinchend"
-_gestures.push Events.Pan = "pan"
+
+# Rotate
+_gestures.push Events.Rotate = "rotate" # This event includes all the other Rotate events
+_gestures.push Events.RotateStart = "rotatestart"
+_gestures.push Events.RotateMove = "rotatemove"
+_gestures.push Events.RotateEnd = "rotateend"
+_gestures.push Events.RotateCancel = "rotatecancel"
+
+# Swipe
 _gestures.push Events.Swipe = "swipe"
 _gestures.push Events.SwipeLeft = "swipeleft"
 _gestures.push Events.SwipeRight = "swiperight"
 _gestures.push Events.SwipeUp = "swipeup"
 _gestures.push Events.SwipeDown = "swipedown"
+
+# Tap
+_gestures.push Events.Tap = "tap"
+_gestures.push Events.SingleTap = "singletap" # Helper. Has no event string correspondance in HammerJS
+_gestures.push Events.DoubleTap = "doubletap" # Helper. Has no event string correspondance in HammerJS

 # Scroll events
 Events.Scroll = "scroll"


newbbb!

newbbb!


Fix regex grouping

Fix regex grouping

--- scripts/nostalgia.coffee
+++ scripts/nostalgia.coffee
@@ -258,7 +258,7 @@
     res.send 'You rang?'

 module.exports = (robot) ->
-    robot.respond /Remember (?:that )?(.*) said "?([^"]+)"?/i, rememberPerson
+    robot.respond /Remember (?:that )?(.+) said "([^"]+)"$/i, rememberPerson

     robot.respond /Remind me of (.*)/i, msgRespond
     robot.respond /Quote (.*)/i, msgRespond


Compile error

Compile error

--- modules/cache.coffee
+++ modules/cache.coffee
@@ -3,7 +3,7 @@
 util = require('./util')

 resVersionPath = 'data/static_res.json'
-resVersionRealPath = AppDataPath + '/' + resVersionPath
+resVersionRealPath = global.appDataPath + '/' + resVersionPath

 cacheData = {}


small typo

small typo

--- bokehjs/src/coffee/renderer/tile/dynamic_image_renderer.coffee
+++ bokehjs/src/coffee/renderer/tile/dynamic_image_renderer.coffee
@@ -64,16 +64,14 @@
       clearTimeout(@render_timer)

     image_obj = @mget('image_source').images[extent.join(':')]
-    if image_obj? and image_obj.image_data.loaded
+    if image_obj? and image_obj.loaded
       @_draw_image(extent.join(':'))
       return

     if @lastImage?
       @_draw_image(@lastImage.cache_key)

-    else
-      if @lastImage?
-        @_draw_image(@lastImage.cache_key)
+    if not image_obj?
       @render_timer = setTimeout((=> @_create_image(extent)), 65)

   _draw_image: (image_key) ->


get/set example (#4990)

get/set example (#4990)

--- documentation/examples/get_set.coffee
+++ documentation/examples/get_set.coffee
@@ -6,4 +6,4 @@
   get: ->
     this.width / this.ratio
   set: (val) ->
-    this.width = val / this.ratio
+    this.width = val * this.ratio


changes from considerit lab repo, copied over blindly

--- earl.coffee
+++ earl.coffee
@@ -1,5 +1,7 @@
 #########
 # Earl
+# The MIT License (MIT)
+# Copyright (c) Travis Kriplean, Consider.it LLC
 #
 # Hire Earl to handle the browser history & location bar.
 #
@@ -56,6 +58,9 @@
 onload = ->

   Earl.root = get_script_attr('earl', 'root') or '/'
+  if Earl.root == true # earl attribute just set with no value
+    Earl.root = '/'
+
   if window.location.pathname.match('.html')
     Earl.root += location.pathname.match(/\/([\w-_]+\.html)/)[1] + '/'
   if Earl.root[0] != '/'
@@ -76,6 +81,7 @@

 window.Earl =
+  seek_to_hash: false

   # Updating the browser window location.
   load_page: (url, query_params) ->
@@ -83,29 +89,30 @@
     loc.host = window.location.host
     loc.query_params = query_params or {}

+
+    parser = document.createElement('a');
+    parser.href = url
+
     # if the url has query parameters, parse and merge them into params
-    if url.indexOf('?') > -1
-      [url, query_params] = url.split('?')
-
+
+    if parser.search
+      query_params = parser.search.replace('?', '')
       for query_param in query_params.split('&')
         query_param = query_param.split('=')
         if query_param.length == 2
           loc.query_params[query_param[0]] = query_param[1]

     # ...and parse anchors
-    hash = ''
-    if url.indexOf('#') > -1
-      [url, hash] = url.split('#')
-      url = '/' if url == ''
-
-      # When loading a page with a hash, we need to scroll the page
-      # to proper element represented by that id. This is hard to
-      # represent in Statebus, as it is more of an event than state.
-      # We'll set seek_to_hash here, then it will get set to null
-      # after it is processed.
-      seek_to_hash = true
-
-    url ?= '/'
+    hash = parser.hash.replace('#', '')
+    # When loading a page with a hash, we need to scroll the page
+    # to proper element represented by that id. This is hard to
+    # represent in Statebus, as it is more of an event than state.
+    # We'll set seek_to_hash here, then it will get set to null
+    # after it is processed.
+    Earl.seek_to_hash = hash?.length > 0
+
+    url = parser.pathname
+    url = '/' if !url || url == ''
     path = url
     if path.substring(0, Earl.root.length) != Earl.root
       path = Earl.root + '/' + path
@@ -124,11 +131,12 @@
 # Enables history aware link. Wraps basic dom.A.

 if hist_aware
-
   window.dom = window.dom || {}

   dom.A = ->
     props = @props
+    loc = fetch 'location'
+
     if @props.href
       href = @props.href

@@ -148,7 +156,6 @@
           href = @props.href = href.replace('//', '/')

-
       handle_click = (event) =>
         opened_in_new_tab = event.altKey  || \
                             event.ctrlKey || \
@@ -206,11 +213,14 @@

     # Respond to a location change
     new_location = url_from_statebus()
+
     if @last_location != new_location

       # update browser history if it hasn't already been updated
       if url_from_browser_location() != new_location
-        history.pushState loc.query_params, title, new_location.replace(/(\/){2,}/, '/').replace(/(\/)$/, '')
+        l = new_location.replace(/(\/){2,}/, '/').replace(/(\/)$/, '')
+        l = '/' if l == ''
+        history.pushState loc.query_params, title, l

       @last_location = new_location

@@ -219,14 +229,26 @@
     # enough to deal with the mightly Webkit browser's imposition of
     # a remembered scroll position for a return visitor upon initial
     # page load!
-    if seek_to_hash
-      seek_to_hash = false
-      el = document.querySelector("##{loc.hash}")
-      if el
-        $(window).scrollTop getCoords(el).top - 50
+    if Earl.seek_to_hash && !@int
+
+      @int = setInterval ->
+        Earl.seek_to_hash = false
+        el = document.getElementById("#{loc.hash}") or document.querySelector("[name='#{loc.hash}']")
+        if el
+          window.scrollTo 0, getCoords(el).top - 50
+
+          if loc.query_params.c
+            delete loc.query_params.c
+            delete loc.hash
+            save loc
+
+          clearInterval @int
+          @int = null
+
+      , 50
+
   monitor()

-seek_to_hash = false

 url_from_browser_location = ->
   # location.search returns the query parameters
@@ -241,7 +263,6 @@
   loc = fetch 'location'

   url = loc.path or '/'
-
   if loc.query_params && Object.keys(loc.query_params).length > 0
     query_params = ("#{k}=#{v}" for own k,v of loc.query_params)
     url += "?#{query_params.join('&')}"


'v0.0.0'

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -77,9 +77,9 @@
   grunt.registerTask 'git-tag', 'Tags a release in git', ->
     exec = require('child_process').exec
     done = this.async()
-    releaseVersion = grunt.template.process('<%= pkg.version %>')
+    releaseVersion = grunt.template.process("<%= pkg.version %>")

-    child = exec 'git commit -am \'v#{releaseVersion}\' && git tag v#{releaseVersion}', (error, stdout, stderr) ->
+    child = exec "git commit -am 'v#{releaseVersion}' && git tag v#{releaseVersion}", (error, stdout, stderr) ->
       console.log('Error running git tag: ' + error) if error?
       done(!error?)


grunt-image-embed cant handle nospawn

grunt-image-embed cant handle nospawn

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -129,8 +129,6 @@
         src: ['demo/styles/*.styl', 'tests/mocha/*.styl']

     watch:
-      options:
-        nospawn: true
       coffee_demo:
         files: ['demo/scripts/*.coffee']
         tasks: ['coffee:demo']


fix grunt file

fix grunt file

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -110,5 +110,5 @@
   @loadNpmTasks 'grunt-express'

-  @registerTask 'server', ['grunt', 'express', 'watch']
+  @registerTask 'server', ['default', 'express', 'watch']
   @registerTask 'default', ['clean', 'jshint', 'uglify', 'cssmin', 'processhtml', 'htmlmin', 'copy']


testing work with grunt task

testing work with grunt task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -151,4 +151,4 @@
   grunt.registerTask 'client', ['jshint:client', 'copy:client', 'concat', 'stylus']
   grunt.registerTask 'client-prod', ['client', 'uglify']
   grunt.registerTask 'test', ['karma:unit:start', 'watch:test']
-  grunt.registerTask 'default', ['server', 'client','concurrent', 'test']
+  grunt.registerTask 'default', ['server', 'client', 'karma:unit:start', 'concurrent']


Increase depth of minimatch paths for JS watch task

Signed-off-by: Byron Ruth <e9d71f5ee7c92d6dc9e92ffdad17b8bd49418f98@devel.io>


feat(grunt): add output-coverage task that runs after unit-tests

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -107,6 +107,7 @@
           'coffeelint:tests'
           'coffee:tests'
           'karma'
+          'output-coverage'
         ]
       ruby:
         files: [
@@ -121,6 +122,7 @@
         tasks: [
           'bgShell:pumaRestart'
           'bgShell:rake'
+          'output-coverage'
         ]
     bgShell:
       pumaRestart:
@@ -148,4 +150,17 @@
   grunt.registerTask 'build', ['htmlhint','html2js','sass','coffeelint','coffee']
   grunt.registerTask 'test', ['build','bgShell:rake','karma']
   grunt.registerTask 'dist', ['build']
+  grunt.registerTask 'output-coverage', 'output coverage data', () ->
+    cover_out = path.join __dirname, 'coverage/.last_run.json'
+    done = @async()
+    fs.readFile cover_out, 'utf8', (err, data) ->
+      unless err
+        try
+          coverage = JSON.parse(data)?.result?.covered_percent
+          if coverage
+            grunt.log.writeln "coverage: #{coverage}%"
+            done()
+            return
+      grunt.log.writeln "coverage: not found"
+      done()
   grunt.registerTask 'default', ['bgShell:shotgun','watch']


update gruntfile to only see coffee

update gruntfile to only see coffee

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -4,7 +4,7 @@

     mochaTest:
       server:
-        src: ['test/server/**/*']
+        src: ['test/server/**/*.coffee']

     coffee:
       options:
@@ -23,13 +23,13 @@
     watch:
       test:
         files: [
-          'src/**/*'
-          'test/**/*'
+          'src/**/*.coffee'
+          'test/**/*.coffee'
         ]
         tasks: ['build', 'test']
       build:
         files: [
-          'src/**/*'
+          'src/**/*.coffee'
         ]
         tasks: ['build']


fixed grunt file added ttf fonts

fixed grunt file added ttf fonts

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -76,7 +76,7 @@
 						dest:          DEST_INDEX_HTML
 					}
           {
-            src:          "#{STAGE_DIR}/lib/bootstrap/fonts/fontawesome-webfont.ttf"
+            src:          "#{STAGE_DIR}/lib/font-awesome/fonts/fontawesome-webfont.ttf"
             dest:         "#{BUILD_MAIN_DIR}/fonts/fontawesome-webfont.ttf"
           }
           {


Server task for testing

Server task for testing

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -98,5 +98,8 @@
   # Release meta-task
   grunt.registerTask "release", ["jshint", "concat", "uglify", "git-tag", "s3"]

+  # Run a webserver for testing
+  grunt.registerTask "server", ["connect:server:keepalive"]
+
   # Default meta-task
   grunt.registerTask "default", ["jshint", "concat", "uglify"]


Added newlines

Added newlines

Readability ftw!

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -12,7 +12,13 @@
     coffee:
       compile:
         files: [
-          { src: ['**/*.coffee'], cwd: 'src/js', ext: '.js', dest: 'tmp/compiled_coffee/', expand: true }
+          {
+            src: ['**/*.coffee']
+            cwd: 'src/js'
+            ext: '.js'
+            dest: 'dist/js/'
+            expand: true
+          }
         ]
     concat:
       wrapped_files:


fix gruntfile to make Jasmine tests run off-line.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -91,7 +91,7 @@
               specs: 'spec/*-spec.js'
               helpers: 'spec/*-helper.js'
               styles: 'c3.css'
-              vendor: 'https://raw.githubusercontent.com/mbostock/d3/v3.5.5/d3.min.js'
+              vendor: 'htdocs/js/d3.latest.js'

         uglify:
           c3:


Get coverage back.

Get coverage back.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -77,7 +77,7 @@
       coverage:
         command: "istanbul cover jasmine-node --captureExceptions test && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage"
       jasmine:
-        command: "jasmine-node --captureExceptions test"
+        command: "istanbul cover jasmine-node --captureExceptions test && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage"
       publish:
         command: "npm publish"


Add grunt-notify

Add grunt-notify

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -4,6 +4,7 @@
   # Project configuration.
   grunt.initConfig
     pkg: grunt.file.readJSON('package.json')
+
     jshint:
       options:
         jshintrc: '.jshintrc'
@@ -90,6 +91,7 @@
   grunt.loadNpmTasks 'grunt-contrib-copy'
   grunt.loadNpmTasks 'grunt-contrib-jshint'
   grunt.loadNpmTasks 'grunt-contrib-watch'
+  grunt.loadNpmTasks 'grunt-notify'

   # tasks.
   grunt.registerTask 'compile', [


Grunt: Use pkill

Grunt: Use pkill

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -99,7 +99,7 @@
       portallights_linux_gen: microflo_gen 'PortalLights'
       portallights_linux_comp: microflo_compile 'PortalLights'
       portallights_run: './spec/microflo-linux.sh PortalLights 5555 & sleep 5'
-      kill_microflo_linux: 'kill $(pgrep microflo-linux) || exit 0'
+      kill_microflo_linux: 'pkill microflo-linux || echo no processes to kill'

   # Grunt plugins used for building
   @loadNpmTasks 'grunt-exec'


Split unit and integration tests (mocha)

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -44,7 +44,10 @@
     'test:acceptance'
   ]
   grunt.registerTask 'test:unit', [
-    'mochaTest'
+    'mochaTest:unit'
+  ]
+  grunt.registerTask 'test:integration', [
+    'mochaTest:integration'
   ]
   grunt.registerTask 'test:acceptance', [
     'cucumberjs'
@@ -72,10 +75,15 @@
         files: [ 'src/**/*.pegjs' ]
         tasks: [ 'peg:build' ]
       # Run the unit tests when .js sources change
-      testing:
+      unit:
         files: [ 'src/**/*.js',   'src/**/*.coffee',
-                 'specs/**/*.js', 'specs/**/*.coffee' ]
+                 'specs/unit/**/*', 'specs/unit/**/*' ]
         tasks: [ 'test:unit' ]
+      # Run the integration tests when .js sources change
+      integration:
+        files: [ 'src/**/*.js',   'src/**/*.coffee',
+                 'specs/integration/**/*', 'specs/integration/**/*' ]
+        tasks: [ 'test:integration' ]

     #################################################################### Build

@@ -170,10 +178,13 @@

     # Unit testing using mocha
     mochaTest:
-      test:
-        src: ['specs/**/*.coffee']
-        options:
-          #reporter: 'progress'
+      unit:
+        src: ['specs/unit/**/*.coffee']
+        options:
+          require: 'coffee-script/register'
+      integration:
+        src: ['specs/integration/**/*.coffee']
+        options:
           require: 'coffee-script/register'

     # Acceptance testing with cucumber


dont publish to npm automatically

dont publish to npm automatically

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -68,7 +68,7 @@
         file: 'package.json'
         bump: false
         commit: false
-        # npm: false
+        npm: false # never automatically publish to npm

     # shell tasks
     shell:


Update: Added wildcard for html / php files and directories

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -22,7 +22,7 @@
                     livereload: true,

             template:
-                files: ['<%= pkg.path %>*.<%= pkg.fileType %>']
+                files: ['<%= pkg.path %>**/*.<%= pkg.fileType %>']
                 tasks: ['htmlmin:dev', 'replace:dev']
                 options:
                     livereload: true,
@@ -34,7 +34,7 @@
                     '<%= pkg.path %>assets/js/*.js',
                     '<%= pkg.path %>assets/css/*.css',
                     '<%= pkg.path %>assets/img/{,**/}*.{png,jpg,jpeg,gif,webp,svg}',
-                    '<%= pkg.devPath %>*.<%= pkg.fileType %>'
+                    '<%= pkg.devPath %>**/*.<%= pkg.fileType %>'
                 ]

         coffee:
@@ -44,6 +44,7 @@
                 files:
                     '<%= pkg.devPath %>assets/js/app.js': [
                         '<%= pkg.path %>coffee/**/Config.coffee',
+                        '<%= pkg.path %>coffee/**/Routes.coffee',
                         '<%= pkg.path %>coffee/**/App.coffee',
                         '<%= pkg.path %>coffee/**/*.coffee'
                     ]
@@ -53,6 +54,7 @@
                 files:
                     '<%= pkg.prodPath %>assets/js/app.js': [
                         '<%= pkg.path %>coffee/**/Config.coffee',
+                        '<%= pkg.path %>coffee/**/Routes.coffee',
                         '<%= pkg.path %>coffee/**/App.coffee',
                         '<%= pkg.path %>coffee/**/*.coffee'
                     ]
@@ -69,7 +71,14 @@
                               ' * Copyright <%= pkg.copyright %>. <%= pkg.license %> licensed.\n' +
                               ' */\n'
                 files:
-                    '<%= pkg.devPath %>assets/js/libs.min.js': ['<%= pkg.path %>coffee/libs/*.js']
+                    '<%= pkg.devPath %>assets/js/libs.min.js': [
+                        '<%= pkg.path %>coffee/libs/jquery.js',
+                        '<%= pkg.path %>coffee/libs/modernizr.js',
+                        '<%= pkg.path %>coffee/libs/handlebars.js',
+                        '<%= pkg.path %>coffee/libs/ember.js',
+                        '<%= pkg.path %>coffee/libs/zepto.animate.js',
+                        '<%= pkg.path %>coffee/libs/*.js'
+                    ]

             prod:
                 options:
@@ -82,7 +91,14 @@
                               ' * Copyright <%= pkg.copyright %>. <%= pkg.license %> licensed.\n' +
                               ' */\n'
                 files:
-                    '<%= pkg.prodPath %>assets/js/libs.min.js': ['<%= pkg.path %>coffee/libs/*.js']
+                    '<%= pkg.prodPath %>assets/js/libs.min.js': [
+                        '<%= pkg.path %>coffee/libs/jquery.js',
+                        '<%= pkg.path %>coffee/libs/modernizr.js',
+                        '<%= pkg.path %>coffee/libs/zepto.animate.js',
+                        '<%= pkg.path %>coffee/libs/handlebars.js',
+                        '<%= pkg.path %>coffee/libs/ember.js',
+                        '<%= pkg.path %>coffee/libs/*.js'
+                    ]

         compass:
             dev:
@@ -125,7 +141,7 @@
                 files: [{
                     expand: true,
                     cwd: '<%= pkg.path %>',
-                    src: '*.html',
+                    src: '**/*.<%= pkg.fileType %>',
                     dest: '<%= pkg.devPath %>'
                 }]

@@ -142,7 +158,7 @@
                 files: [{
                     expand: true,
                     cwd: '<%= pkg.path %>',
-                    src: '*.html',
+                    src: '**/*.<%= pkg.fileType %>',
                     dest: '<%= pkg.prodPath %>'
                 }]

@@ -219,7 +235,7 @@
                     prefix: '<!-- @@'
                 },
                 files: [
-                    {expand: true, flatten: true, src: ['dev/*.html'], dest: 'dev/'}
+                    {expand: true, flatten: true, src: ['dev/**/*.<%= pkg.fileType %>'], dest: 'dev/'}
                 ]
             },
             prod: {
@@ -247,7 +263,7 @@
                     prefix: '<!-- @@'
                 },
                 files: [
-                    {expand: true, flatten: true, src: ['prod/*.html'], dest: 'prod/'}
+                    {expand: true, flatten: true, src: ['prod/**/*.<%= pkg.fileType %>'], dest: 'prod/'}
                 ]
             }
         }
@@ -261,7 +277,7 @@
                     '<%= pkg.prodPath %>assets/img/**.png',
                     '<%= pkg.path %>compass/.sass-cache',
                     '<%= pkg.prodPath %>assets/css/main.css',
-                    '<%= pkg.prodPath %>/*.<%= pkg.fileType %>'
+                    '<%= pkg.prodPath %>/**/*.<%= pkg.fileType %>'
                 ]

         imagemin:


Add existing tags to editor

Add existing tags to editor

--- client/publication.coffee
+++ client/publication.coffee
@@ -736,7 +736,13 @@
   $(@findAll '.annotation-tags-list').tagit
     readOnly: true

+Template.annotationEditor.created = ->
+  @_rendered = false
+
 Template.annotationEditor.rendered = ->
+  return if @_rendered
+  @_rendered = true
+
   # Load MediumEditor
   editor = new MediumEditor @findAll('.annotation-content-editor'),
     buttons: ['bold', 'italic', 'quote', 'unorderedlist', 'orderedlist', 'pre']
@@ -745,7 +751,14 @@
   # Load tag-it
   $(@findAll '.annotation-tags-editor').tagit()

+  # Create tags
+  _.each @data.tags, (item) =>
+    $(@findAll '.annotation-tags-editor').tagit 'createTag', item.tag?.name?.en
+
   return # Make sure CoffeeScript does not return anything
+
+Template.annotationEditor.destroyed = ->
+  @_rendered = false

 Template.annotationEditor.events
   'click .publish': (e, template) ->


起動直後にmarkdownの変換がされていなかったのを修正した

起動直後にmarkdownの変換がされていなかったのを修正した

修正すると2つのブラウザ間でメッセージを無限ループするようになってしまったのでそちらも修正している。具体的には、websocket経由でmarkdownがアップデートされた場合にもそれをwebsocket経由でお繰り返していたのをやめた

--- coffee/index.coffee
+++ coffee/index.coffee
@@ -22,13 +22,11 @@
 $ = React.createElement

 Editor = React.createClass
-  contentUpdateFromMarkdown: ->
-    editor = @refs.editor.getDOMNode()
-    @sendMarkdown(editor.value)
+  contentUpdateFromMarkdown: (markdown)->
     @setState
-      markdown: editor.value
+      markdown: markdown
     try
-      content = md2react editor.value,
+      content = md2react markdown,
         gfm: true
         breaks: true
         tables: true
@@ -51,7 +49,7 @@
           # 触っているところより前が書き換わると維持されないという…
           () =>
             editor.setSelectionRange(caretStart, caretEnd)
-          @contentUpdateFromMarkdown
+         @contentUpdateFromMarkdown data.markdown
     @syncMarkdownFromServer()

   sendMarkdown: (markdown)->
@@ -68,6 +66,11 @@
     content: null
     markdown: null

+  onChangeTextarea: ->
+    editor = @refs.editor.getDOMNode()
+    @sendMarkdown(editor.value)
+    @contentUpdateFromMarkdown(editor.value)
+
   render: ->
     $ 'div', {key: 'root'}, [
       $ 'h1', {style: {textAlign: 'center', fontFamily: '"Poiret One", cursive', fontSize: '25px', height: '50px', lineHeight: '50px'}}, 'erlang editor'
@@ -77,7 +80,7 @@
         }, [
           $ 'textarea', {
             ref:'editor'
-            onChange: @contentUpdateFromMarkdown
+            onChange: @onChangeTextarea
             style: {height: '100%', width: '100%', border: 0, outline: 0, fontSize: '14px', padding: '5px', overflow: 'auto', fontFamily:'Consolas, Menlo, monospace', resize: 'none', background: 'transparent'}
             value: @state.markdown
           }


update homedata modal

update homedata modal

--- client/src/sidebar.coffee
+++ client/src/sidebar.coffee
@@ -64,6 +64,8 @@
             miles: @props.homedata.miles
             celsius: @props.homedata.celsius
             fahrenheit: @props.homedata.fahrenheit
+            hectare: @props.homedata.hectare
+            acre: @props.homedata.acre
         }

     onOkClicked: ->
@@ -74,6 +76,8 @@
         miles = @refs.milesInput.getDOMNode().value
         celsius = @refs.celsiusInput.getDOMNode().value
         fahrenheit = @refs.fahrenheit.getDOMNode().value
+        hectare = @refs.hectare.getDOMNode().value
+        acre = @refs.acre.getDOMNode().value

         homedata =
             id: @props.homedata.id
@@ -83,6 +87,8 @@
             miles: miles
             celsius: celsius
             fahrenheit: fahrenheit
+            hectare: hectare
+            acre: acre


Fixed typo: newerTh{e => a}n

Fixed typo: newerTh{e => a}n

--- lib/browsers.coffee
+++ lib/browsers.coffee
@@ -78,7 +78,7 @@
           else
             data.versions.filter (version, i) -> data.popularity[i] > popularity

-    newerThen:
+    newerThan:
       regexp: /^(\w+) (>=?)\s*([\d\.]+)/
       select: (browser, sign, version) ->
         data    = @byName(browser)


Updated Atom config

Updated Atom config

--- home/.atom/config.cson
+++ home/.atom/config.cson
@@ -93,3 +93,4 @@
     style: "semi-standard"
     honorStyleSettings: true
     checkStyleDevDependencies: true
+  "merge-conflicts": {}


blegh

blegh

--- home/.atom/keymap.cson
+++ home/.atom/keymap.cson
@@ -198,7 +198,9 @@
   'ctrl-t': 'fuzzy-finder:toggle-file-finder'

   # Tree View Toggle
-  '\\':     'tree-view:toggle-focus'
+  'ctrl+\\':     'tree-view:toggle-focus'
+  '\' \\':   'tree-view:toggle'
+
   # Show the current file in the tree
   '\' l': 'tree-view:reveal-active-file'


Save resource swf files to cache.

Save resource swf files to cache.

--- modules/proxy.coffee
+++ modules/proxy.coffee
@@ -70,6 +70,7 @@
               res.writeHead result.statusCode, result.headers
               res.write data
               res.end()
+              saveCacheSwfFile req, data

 serverWithSocksProxy = (req, res) ->
   console.log "Get Request #{req.url} using Socks Proxy"
@@ -115,6 +116,7 @@
               res.writeHead result.statusCode, result.headers
               res.write data
               res.end()
+              saveCacheSwfFile req, data

 serverWithHttpProxy = (req, res) ->
   console.log "Get Request #{req.url} using HTTP Proxy"
@@ -153,6 +155,7 @@
               res.writeHead result.statusCode, result.headers
               res.write data
               res.end()
+              saveCacheSwfFile req, data

 serverWithoutProxy = (req, res) ->
   console.log "Get Request #{req.url}"
@@ -189,6 +192,7 @@
               res.writeHead result.statusCode, result.headers
               res.write data
               res.end()
+              saveCacheSwfFile req, data

 sendSocksProxyRequest = (options, counter, callback) ->
   request = http.request options, (result) ->
@@ -248,22 +252,13 @@
 loadCacheSwfFile = (req, res, callback) ->
   # These two swf files are source code files, not resource files
   # Caching these files may cause illegal logic error I guess?
-  if req.url.indexOf('/kcs/Core.swf') != -1
-    callback {err: true}
-    return
-  if req.url.indexOf('/kcs/mainD2.swf') != -1
-    callback {err: true}
+  if (req.url.indexOf('/kcs/Core.swf') != -1) || (req.url.indexOf('/kcs/mainD2.swf') != -1)
+    callback true
     return

   if req.url.indexOf('/kcs/') != -1
     # Get FilePath
-    filePath = ''
-    beginIndex = req.url.indexOf('/kcs/') + 1
-    endIndex = req.url.indexOf('?')
-    if endIndex != -1
-      filePath = req.url.substr beginIndex, (endIndex - beginIndex)
-    else
-      filePath = req.url.substr beginIndex
+    filePath = url.parse(req.url).pathname.substr 1

     # Get FileSize
     fs.stat filePath, (err, stat) ->
@@ -272,27 +267,38 @@
         # Read File
         fs.readFile filePath, (err, data) ->
           if !err
-              console.log "Load File From Cache: #{filePath}, Size: #{fileSize}"
-              res.writeHead 200, "{
-                                    \"date\":\"Mon, 10 Nov 2014 03:08:22 GMT\",
-                                    \"server\":\"Apache\",
-                                    \"last-modified\":\"Wed, 23 Apr 2014 05:46:42 GMT\",
-                                    \"accept-ranges\":\"bytes\",
-                                    \"content-length\":\"#{fileSize}\",
-                                    \"cache-control\":\"max-age=2592000, public\",
-                                    \"connection\":\"close\",
-                                    \"content-type\":\"application/x-shockwave-flash\"
-                                }"
-              res.write data
-              res.end()
-              callback {err: false}
+            console.log "Load File From Cache: #{filePath}, Size: #{fileSize}"
+            res.writeHead 200, "{
+                                  \"date\":\"Mon, 10 Nov 2014 03:08:22 GMT\",
+                                  \"server\":\"Apache\",
+                                  \"last-modified\":\"Wed, 23 Apr 2014 05:46:42 GMT\",
+                                  \"accept-ranges\":\"bytes\",
+                                  \"content-length\":\"#{fileSize}\",
+                                  \"cache-control\":\"max-age=2592000, public\",
+                                  \"connection\":\"close\",
+                                  \"content-type\":\"application/x-shockwave-flash\"
+                              }"
+            res.write data
+            res.end()
+            callback false
           else
             console.log err
-            callback {err: true}
+            callback true
       else
         console.log err
-        callback {err: true}
+        callback true
   else
-    callback {err: true}
-
-
+    callback true
+
+# Save File To Cache
+saveCacheSwfFile = (req, data) ->
+  if (req.url.indexOf('/kcs/Core.swf') != -1) || (req.url.indexOf('/kcs/mainD2.swf') != -1)
+    return
+
+  if req.url.indexOf('/kcs/') != -1
+    # Get FilePath
+    filePath = url.parse(req.url).pathname.substr 1
+    # Save File
+    fs.writeFile filePath, data, (err) ->
+      console.log err if err
+      console.log "Save Cache File: #{filePath}" if !err


bufferedPages support

bufferedPages support

--- lib/document.coffee
+++ lib/document.coffee
@@ -19,6 +19,9 @@
     # Whether streams should be compressed
     @compress = @options.compress ? yes

+    @_pageBuffer = []
+    @_pageBufferStart = 0
+
     # The PDF object store
     @_offsets = []
     @_waiting = 0
@@ -76,11 +79,12 @@

   addPage: (options = @options) ->
     # end the current page if needed
-    @page?.end()
-
+    @flushPages() unless @options.bufferPages
+
     # create a page object
     @page = new PDFPage(this, options)
-
+    @_pageBuffer.push(@page)
+
     # add the page to the object store
     pages = @_root.data.Pages.data
     pages.Kids.push @page.dictionary
@@ -96,7 +100,24 @@
     @transform 1, 0, 0, -1, 0, @page.height

     return this
-
+
+  bufferedPageRange: -> { start: @_pageBufferStart, count: @_pageBuffer.length }
+
+  switchToPage: (n) ->
+    unless page = @_pageBuffer[n-@_pageBufferStart]
+      throw new Error("switchToPage(#{n}) out of bounds, current buffer covers pages #{@_pageBufferStart} to #{@_pageBufferStart + @_pageBuffer.length - 1}")
+    @page = page
+
+  flushPages: ->
+    # this local variable exists so we're future-proof against
+    # reentrant calls to flushPages.
+    pages = @_pageBuffer
+    @_pageBuffer = []
+    @_pageBufferStart += pages.length
+    for page in pages
+      page.end()
+    null
+
   ref: (data) ->
     ref = new PDFReference(this, @_offsets.length + 1, data)
     @_offsets.push null # placeholder for this object's offset once it is finalized
@@ -144,8 +165,7 @@
     '

   end: ->
-    @page.end()
-
+    @flushPages()
     @_info = @ref()
     for key, val of @info
       if typeof val is 'string'


moar logging

moar logging

--- lib/eslogger.coffee
+++ lib/eslogger.coffee
@@ -51,11 +51,17 @@
                 @createIndex index, () =>
                   console.log "re-launch"
                   @logMessageES log, room, msg
+              else
+                @robot.logger.warning "logMessageES / res.statusCode == 400 and json_body.error.type != index_not_found_exception"
+                @robot.logger.warning res.statusCode
+                @robot.logger.warning body
             else
               if res.statusCode > 299
                 @robot.logger.warning "logMessageES / res.statusCode > 299"
                 @robot.logger.warning res.statusCode
                 @robot.logger.warning body
+              else
+                @robot.logger.debug "logged."

   createIndex: (index, cb) ->
     mapping = {


removed unused create stops methods

removed unused create stops methods

--- lib/models/vehicle.coffee
+++ lib/models/vehicle.coffee
@@ -27,15 +27,6 @@
         @staged = message
     ), this)
   stops: ( ->
-    # Need to account for the fact that a journey may not have
-    # had an ignition off and hence become 'stopped' due to
-    # timing out.  If this has happened then we will be one stop
-    # short and we need to create the last stop
-    # _stops = @get('_stops')
-    # if lastJourney = @getPath('_journeys.lastObject')
-    #   if (lastJourney.state() == 'finished') && (lastJourney.getPath('endTime.milliseconds') != @getPath('_stops.lastObject.arriveTime.milliseconds'))
-    #     @createStop(lastJourney.getPath('messages.lastObject'))
-    # _stops
     console.log "calculating stops"
     journeys = @getPath('journeys.content').slice(0)
     if journeys
@@ -65,13 +56,8 @@
     journey.get('messages').pushObject(message)
     @get('_journeys').pushObject(journey)
     journey
-  createStop: (message) ->
-    stop = Sensor.Stop.create arriveMessage: message
-    @get('_stops').pushObject stop
-    stop
   finishJourney: (journey) ->
     journey.finish()
-    @createStop(journey.getPath('messages.lastObject'))
   state: SC.computed( ->
     console.log "calculating state"
     lastMessage = @getPath('messages.lastObject')


use white background

use white background

--- coffee/application.coffee
+++ coffee/application.coffee
@@ -424,8 +424,9 @@

 drawEverything = ->
   return false unless observer.canDraw()
-
-  context.clearRect 0, 0, canvas.width, canvas.height
+  context.fillStyle = "white"
+  #context.clearRect 0, 0, canvas.width, canvas.height
+  context.fillRect 0, 0, canvas.width, canvas.height
   context.save()
   s = Math.min( canvas.width, canvas.height ) / 2 #
   context.scale s, s


oops. added @hash. See last commit

oops. added @hash. See last commit


add missing double-quotes

add missing double-quotes

--- lib/grammars.coffee
+++ lib/grammars.coffee
@@ -658,7 +658,7 @@
       command: if GrammarUtils.OperatingSystem.isWindows() then "cmd" else "bash"
       args: (context) ->
         if GrammarUtils.OperatingSystem.isWindows()
-          ['/c cd ' + context.filepath.replace(/[^\/]*$/, '') + ' && pulp run']
+          ['/c cd "' + context.filepath.replace(/[^\/]*$/, '') + '" && pulp run']
         else
           ['-c', 'cd "' + context.filepath.replace(/[^\/]*$/, '') + '" && pulp run']


Fix _.template usage

Fix _.template usage

--- lib/styles/default.coffee
+++ lib/styles/default.coffee
@@ -61,7 +61,7 @@
         return callback error

       try
-        scriptSource = _.template data, @
+        scriptSource = _.template(data)(@)
       catch error
         @log.error 'Failed to interpolate %s: %s', scriptPath, error.message
         return callback error


Center view on tap

Center view on tap

--- lib/todo-item-view.coffee
+++ lib/todo-item-view.coffee
@@ -46,8 +46,6 @@
     position = [@todo.position[0][0], @todo.position[0][1]]

     atom.workspace.open(@todo.loc, {
-      initialLine: position[0]
-      initialColumn: position[1]
       split: @getSplitDirection()
       pending: atom.config.get('core.allowPendingPaneItems') or false
     }).then ->


Rescue from alt key disaster on windows

Rescue from alt key disaster on windows

--- lib/views/terminal.coffee
+++ lib/views/terminal.coffee
@@ -70,6 +70,8 @@
             Clipboard.writeText(getSelection().toString())
           else if event.which == 86 && event.shiftKey && event.ctrlKey
             ipc.send 'terminal-data', Clipboard.readText()
+          else if event.altKey
+            console.log 'Saved from alt key disaster!'
           else
             ipc.send 'terminal-data', data
         else


Namespacing is a thing

Namespacing is a thing

--- server/s3manager/s3manager.coffee
+++ server/s3manager/s3manager.coffee
@@ -16,7 +16,7 @@
       bucketFound = false
       for bucket, i in data.Buckets
         console.log "  --> "+bucket.Name
-generateModDownloadURL = (mod)->
+@generateModDownloadURL = (mod)->
   response = Async.runSync (done)->
     done null, s3.getSignedUrl 'getObject', {Bucket: bucket, Key: mod.bundlepath}
   response.result


doc: task instead of web worker

doc: task instead of web worker

--- spec/app/git-spec.coffee
+++ spec/app/git-spec.coffee
@@ -214,7 +214,7 @@
         expect(repo.isStatusNew(statuses[newPath])).toBeTruthy()
         expect(repo.isStatusModified(statuses[modifiedPath])).toBeTruthy()

-    it "only starts a single web worker at a time and schedules a restart if one is already running", =>
+    it "only starts a single task at a time and schedules a restart if one is already running", =>
       fsUtils.write(modifiedPath, 'making this path modified')
       statusHandler = jasmine.createSpy('statusHandler')
       repo.on 'statuses-changed', statusHandler


`Iterator` bug with reverting fixed

`Iterator` bug with reverting fixed

--- iterator.coffee
+++ iterator.coffee
@@ -35,6 +35,7 @@
 			@update()

 	revert: ->
+		@clearData()
 		@update()
 		@node.visible = true

@@ -46,9 +47,9 @@
 		# clear all if data changed
 		if data and data isnt each
 			if data instanceof List
-				array.changed.disconnect @updateItem
-				array.inserted.disconnect @insertItem
-				array.popped.disconnect @popItem
+				data.changed.disconnect @updateItem
+				data.inserted.disconnect @insertItem
+				data.popped.disconnect @popItem

 			@clearData()


Delete error.coffee

Delete error.coffee

--- libs/reserve/error.coffee
+++ libs/reserve/error.coffee
@@ -1 +0,0 @@
-exports.bsod = require './error/bsod.js'


:art: Removed duplicate lines

:art: Removed duplicate lines

--- spec/grammar-spec.coffee
+++ spec/grammar-spec.coffee
@@ -75,7 +75,6 @@
       expect(tokens[3]).toEqual value: "{", scopes: ["source.perl"]
       expect(tokens[4]).toEqual value: "m", scopes: ["source.perl", "constant.other.bareword.perl"]
       expect(tokens[5]).toEqual value: "}", scopes: ["source.perl"]
-

   describe "when a regexp find tokenizes", ->
     it "works with all bracket/seperator variations", ->
@@ -166,7 +165,6 @@
       expect(tokens[9]).toEqual value: "i", scopes: ["source.perl", "string.regexp.find.perl", "punctuation.definition.string.perl", "keyword.control.regexp-option.perl"]
       expect(tokens[10]).toEqual value: ") {}", scopes: ["source.perl"]

-
   describe "when a regexp replace tokenizes", ->
     it "works with all bracket/seperator variations", ->
       {tokens} = grammar.tokenizeLine("s/text/test/acdegilmoprsux")
@@ -253,7 +251,6 @@
       expect(lines[1][0]).toEqual value: "/", scopes: ["source.perl", "string.regexp.replace.extended.simple_delimiter.perl", "punctuation.definition.string.perl"]
       expect(lines[1][5]).toEqual value: "/", scopes: ["source.perl", "string.regexp.replace.extended.simple_delimiter.perl", "punctuation.definition.string.perl"]
       expect(lines[1][7]).toEqual value: ";", scopes: ["source.perl"]
-

   describe "tokenizes constant variables", ->
     it "highlights constants", ->
@@ -526,7 +523,6 @@
       expect(lines[25][0]).toEqual value: "~                                    ^<<<<<<<<<<<<<<<<<<<<<<<...", scopes: ["source.perl", "meta.format.perl"]
       expect(lines[27][0]).toEqual value: ".", scopes: ["source.perl", "meta.format.perl"]

-
   describe "when a heredoc tokenizes", ->
     it "does not highlight the whole line", ->
       lines = grammar.tokenizeLines("""$asd->foo(<<TEST, $bar, s/foo/bar/g);
@@ -548,7 +544,6 @@
 $asd\\n
 ;""")
       expect(lines[1][0]).toEqual value: "$asd\\n", scopes: ["source.perl", "string.unquoted.heredoc.quote.perl"]
-

   describe "when a hash variable tokenizes", ->
     it "does not highlight whitespace beside a key as a constant", ->
@@ -563,7 +558,6 @@
       expect(lines[2][2]).toEqual value: "'", scopes: ["source.perl", "string.quoted.single.perl", "punctuation.definition.string.end.perl"]
       expect(lines[2][3]).toEqual value: " ", scopes: ["source.perl"]

-
   describe "when package to tokenizes", ->
     it "does not highlight semicolon in package name", ->
       {tokens} = grammar.tokenizeLine("package Test::ASD; #this is my new class")


rename to more explanatory name

rename to more explanatory name

--- lib/operator.coffee
+++ lib/operator.coffee
@@ -165,7 +165,7 @@
     # we have to return to normal-mode from operator-pending or visual
     @activateMode('normal')

-  selectOccurrence: (fn) ->
+  reselectOccurrence: (fn) ->
     scanRanges = null
     cursorPositionManager = new CursorPositionManager(@editor)

@@ -212,7 +212,7 @@
     @emitWillSelectTarget()

     if @isOccurrence()
-      @selectOccurrence =>
+      @reselectOccurrence =>
         @target.select()
     else
       @target.select()


fix, after-occurrence selection is required for flashing occurrence

--- lib/operator.coffee
+++ lib/operator.coffee
@@ -188,6 +188,7 @@
     @target.select()
     @mutationManager.setCheckPoint('did-select')
     @selectOccurrence() if @isOccurrence()
+    @mutationManager.setCheckPoint('did-select-occurrence')

     if haveSomeNonEmptySelection(@editor) or @target.getName() is "Empty"
       @emitDidSelectTarget()


:art: simplify

:art: simplify

--- lib/operator.coffee
+++ lib/operator.coffee
@@ -283,10 +283,10 @@
     @onDidFinishOperation =>
       @collected = false

-    @collectResults()
+    @collect()
     super

-  collectResults: ->
+  collect: ->
     return if @collected
     @collected = true
     @suspendExecuteOperation()


the watch configuration was also explicitly listing the vanilla handlebars task. fixed that

--- config/application.coffee
+++ config/application.coffee
@@ -9,9 +9,11 @@
 _ = require("underscore")
 config = require('lineman').config

-#replace vanilla handlebars task with an ember handlebars task
-commonTasks = config.application.appTasks.common
-commonTasks[_(commonTasks).indexOf("handlebars")] = "ember_handlebars"
+
+replaceVanillaHandlebarsTasks = ->
+  _([config.application.appTasks.common, config.application.watch.handlebars.tasks]).each (taskList) ->
+    taskList[_(taskList).indexOf("handlebars")] = "ember_handlebars"
+replaceVanillaHandlebarsTasks()

 module.exports = config.extend 'application',
   # Override application configuration here. Common examples follow in the comments.


Cleanup debounce time

Cleanup debounce time

--- drop.coffee
+++ drop.coffee
@@ -17,6 +17,7 @@

 $ = jQuery
 isIE = not not /msie [\w.]+/.exec navigator.userAgent.toLowerCase()
+debounce = if isIE then 100 else 0

 # Extracted from jQuery UI Core (to remove dependency)
@@ -37,9 +38,6 @@

         return (/fixed/).test(this.css('position')) or (if not scrollParent.length then $('html') else scrollParent)

-debounce = 0
-if isIE
-    debounce = 100

 $ ->


Fix reconfigure for new installs (attempt 3)

--- cli/prompts.coffee
+++ cli/prompts.coffee
@@ -29,7 +29,7 @@
       if config.env.GOOGLE_KEY or config.env.IMGUR_KEY or config.env.TUMBLR_CONSUMER_KEY
         choices.push { name: 'image (Image search)', value: 'image', checked: false }
       if config.env.OSU_KEY
-      choices.push { name: 'osu (osu! player statics)', value: 'osu', checked: false }
+        choices.push { name: 'osu (osu! player statics)', value: 'osu', checked: false }
     if advanced
       choices.push { name: 'eval (Evaluate CoffeeScript)', value: 'eval', checked: false }
     if config.modules? then choices.forEach (module)->


Close login menu when moving between pages. Fixes #210

--- client/base.coffee
+++ client/base.coffee
@@ -58,6 +58,9 @@

   # Overlays are special and we do not clear them while routing

+  # Close login buttons dialog box when moving between pages
+  Accounts._loginButtonsSession.closeDropdown()
+
 # TODO: Use real parser (arguments can be listed multiple times, arguments can be delimited by ";")
 parseQuery = (qs) ->
   query = {}


JavaScript driver support for noreplyWait

--- drivers/javascript/net.coffee
+++ drivers/javascript/net.coffee
@@ -180,8 +180,49 @@
             # Unexpected token
             @emit 'error', new err.RqlDriverError "Unexpected token #{token}."

-    close: ar () ->
-        @open = false
+    close: (varar 0, 2, (noreply_wait=true, callback=null) ->
+        unless typeof noreply_wait is 'boolean'
+            throw new err.RqlDriverError "First argument to close must be the boolean noreply_wait flag."
+        unless not callback? or typeof callback is 'function'
+            throw new err.RqlDriverError "Second argument to close must be a callback function."
+
+        cb = (args...) =>
+            @open = false
+            if callback?
+                callback(args...)
+
+        if noreply_wait
+            @noreplyWait(cb)
+        else
+            cb()
+    )
+
+    noreplyWait: ar (callback) ->
+        unless typeof callback is 'function'
+            throw new err.RqlDriverError "First argument to noreplyWait must be a callback function."
+        unless @open then throw new err.RqlDriverError "Connection is closed."
+
+        # Assign token
+        token = @nextToken++
+
+        # Construct query
+        query = {'global_optargs':[]}
+        query.type = "NOREPLY_WAIT"
+        query.token = token
+
+        # Construct callback that checks response message
+        cb = (e, data) =>
+            if e?
+                callback(e)
+            else if data?
+                callback(new err.RqlDriverError "Unexpected response to noreplyWait from server: " + cursor)
+            else
+                callback(null)
+
+        # Save callback
+        @outstandingCallbacks[token] = {cb:cb, root:null, opts:null}
+
+        @_sendQuery(query)

     cancel: ar () ->
         @outstandingCallbacks = {}
@@ -281,7 +322,7 @@
         super(host, callback)

         if @rawSocket?
-            @close()
+            @close(false)

         @rawSocket = net.connect @port, @host
         @rawSocket.setNoDelay()
@@ -329,11 +370,18 @@

         @rawSocket.on 'error', (args...) => @emit 'error', args...

-        @rawSocket.on 'close', => @open = false; @emit 'close'
-
-    close: () ->
-        super()
-        @rawSocket.end()
+        @rawSocket.on 'close', => @open = false; @emit 'close', false
+
+    close: (noreply_wait=true, callback=null, args...) ->
+        unless not callback? or typeof callback is 'function'
+            throw new err.RqlDriverError "Second argument to close must be a callback function."
+
+        cb = (args...) =>
+            @rawSocket.end()
+            if callback?
+                callback(args...)
+
+        super(noreply_wait, cb, args...)

     cancel: () ->
         @rawSocket.destroy()


Add tests for + and enter, mirroring the tests for -

As requested [in the pull request discussion](https://github.com/atom/vim-mode/pull/277#issuecomment-47765173), I am adding the tests for + even though some of the copied tests are disabled.

These tests are all the same as the ones for -, except that the starting line numbers are X from the bottom instead of X from the top, and in the “abc” sample text the order of the lines is reversed.

I also changed a previous test for - so that its cursor column is less remarkable, and won’t fail due to edge cases that that particular test is not aiming to test. The cursor column in that test really is now in the middle of the line, instead of on the last character.

--- spec/motions-spec.coffee
+++ spec/motions-spec.coffee
@@ -574,7 +574,7 @@
       editor.setText("abcdefg\n  abc\n  abc\n")

     describe "from the middle of a line", ->
-      beforeEach -> editor.setCursorScreenPosition([1, 4])
+      beforeEach -> editor.setCursorScreenPosition([1, 3])

       describe "as a motion", ->
         beforeEach -> keydown('-')
@@ -590,7 +590,7 @@

         it 'deletes the current and previous line', ->
           expect(editor.getText()).toBe "  abc\n"
-          expect(editor.getCursorScreenPosition()).toEqual [0, 4]
+          expect(editor.getCursorScreenPosition()).toEqual [0, 3]

     describe "from the first character of a line indented the same as the previous one", ->
       beforeEach -> editor.setCursorScreenPosition([2, 2])
@@ -655,9 +655,89 @@
           expect(editor.getCursorScreenPosition()).toEqual [1, 0]

   describe "the + keybinding (which should also be equivalent to the enter keybinding)", ->
-    # TODO copy the tests for the - keybinding into here, and then
-    #  change the values to expect downwards instead of upwards movement
-    # to ease editing, copy the tests only after enabling all disabled tests for -
+    beforeEach ->
+      editor.setText("  abc\n  abc\nabcdefg\n")
+
+    describe "from the middle of a line", ->
+      beforeEach -> editor.setCursorScreenPosition([1, 3])
+
+      describe "as a motion", ->
+        beforeEach -> keydown('+')
+
+        it "moves the cursor to the first character of the next line", ->
+          expect(editor.getCursorScreenPosition()).toEqual [2, 0]
+
+      # commented out because this fails due to a bug in `j`; re-enable when `j` is fixed
+      xdescribe "as a selection", ->
+        beforeEach ->
+          keydown('d')
+          keydown('+')
+
+        it 'deletes the current and next line', ->
+          expect(editor.getText()).toBe "  abc\n"
+          expect(editor.getCursorScreenPosition()).toEqual [0, 3]
+
+    describe "from the first character of a line indented the same as the next one", ->
+      beforeEach -> editor.setCursorScreenPosition([0, 2])
+
+      describe "as a motion", ->
+        beforeEach -> keydown('+')
+
+        it "moves to the first character of the next line (directly below)", ->
+          expect(editor.getCursorScreenPosition()).toEqual [1, 2]
+
+      # commented out because this fails due to a bug in `j`; re-enable when `j` is fixed
+      xdescribe "as a selection", ->
+        beforeEach ->
+          keydown('d')
+          keydown('+')
+
+        it "selects to the first character of the next line (directly below)", ->
+          expect(editor.getText()).toBe "abcdefg\n"
+          expect(editor.getCursorScreenPosition()).toEqual [0, 2]
+
+    describe "from the beginning of a line followed by an indented line", ->
+      beforeEach -> editor.setCursorScreenPosition([0, 0])
+
+      describe "as a motion", ->
+        beforeEach -> keydown('+')
+
+        it "moves the cursor to the first character of the next line", ->
+          expect(editor.getCursorScreenPosition()).toEqual [1, 2]
+
+      # commented out because this fails due to a bug in `j`; re-enable when `j` is fixed
+      xdescribe "as a selection", ->
+        beforeEach ->
+          keydown('d')
+          keydown('+')
+
+        it 'selects to the first character of the next line', ->
+          expect(editor.getText()).toBe "abcdefg\n"
+          expect(editor.getCursorScreenPosition()).toEqual [0, 0]
+
+    describe "with a count", ->
+      beforeEach ->
+        editor.setText("1\n2\n3\n4\n5\n6\n")
+        editor.setCursorScreenPosition([1, 0])
+
+      describe "as a motion", ->
+        beforeEach ->
+          keydown('3')
+          keydown('+')
+
+        it "moves the cursor to the first character of that many lines following", ->
+          expect(editor.getCursorScreenPosition()).toEqual [4, 0]
+
+      # commented out because this fails due to a bug in `j`; re-enable when `j` is fixed
+      xdescribe "as a selection", ->
+        beforeEach ->
+          keydown('d')
+          keydown('3')
+          keydown('+')
+
+        it 'deletes the current line plus that many following lines', ->
+          expect(editor.getText()).toBe "1\n6\n"
+          expect(editor.getCursorScreenPosition()).toEqual [1, 0]

   describe "the gg keybinding", ->
     beforeEach ->


Fix input height for css frameworks like bootstrap

--- app/assets/javascripts/anjlab/uploads/button.js.coffee
+++ app/assets/javascripts/anjlab/uploads/button.js.coffee
@@ -43,6 +43,7 @@
       # the right side of the input
       right: 0
       top: 0
+      height: 'auto' # bootstrap input[file] height workaround
       fontFamily: 'Arial'
       # 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
       fontSize: '118px'


remember previous settings for new event

--- app/assets/javascripts/controllers/bill_form.js.coffee
+++ app/assets/javascripts/controllers/bill_form.js.coffee
@@ -62,7 +62,7 @@
       $scope.billItem.client = $scope.clientHash[billItem.clientId]
       $scope.billItem.category = $scope.eventCategoryHash[billItem.eventCategoryId]
     else
-      $scope.billItem = {}
+      $scope.billItem ||= {}
     modal = $modal.open
       templateUrl: 'billItemTemplate.html'
       scope: $scope


Allow options to be passed to TokenField#setToken and TokenField#deleteAll

--- app/assets/javascripts/exo/views/token_field.js.coffee
+++ app/assets/javascripts/exo/views/token_field.js.coffee
@@ -138,8 +138,8 @@
           @deleteTokenAtIndex(index, true)

     # Clears any existing tokens and inserts passed in tokens
-    setToken: (tokens) ->
-      @deleteAll()
+    setToken: (tokens, options = {}) ->
+      @deleteAll(options)
       @insertToken(tokens)

     selectPreviousToken: ->
@@ -208,11 +208,11 @@
         @_updateInput()
         @_toggleInputVisibility()

-    deleteAll: ->
+    deleteAll: (options = {}) ->
       @selectTokenAtIndex(TokenField.TokenIndexes.Input)
       $(@tokenContainer).children().remove(".#{@tokenClassName}")

-      @_collection.reset()
+      @_collection.reset([], options)
       @tokens = []

       @_updateInput()


fix colspan in js

fix colspan in js

--- app/assets/javascripts/objects/requests_list.js.coffee
+++ app/assets/javascripts/objects/requests_list.js.coffee
@@ -15,4 +15,4 @@
     $("#requester#{requesterId}").fadeOut 'slow', ->
       $(this).remove()
       unless $('.requester').length
-        $('#requesters-table').append('
You have no students needing help!
')+        $('#requesters-table').append('
You have no students needing help!
')


fix cancel in file promise

fix cancel in file promise

--- app/assets/javascripts/uploadcare/files/base.js.coffee
+++ app/assets/javascripts/uploadcare/files/base.js.coffee
@@ -137,14 +137,15 @@
         img.src = @previewUrl

     __extendPromise: (p) =>
-      p.cancel = @__cancel
-      p.preview = (selector) => @__preview(p, selector)
-
-      __pipe = p.pipe
-      p.pipe = => @__extendPromise __pipe.apply(p, arguments)
+      p.cancel = =>
+        @__cancel()
+
+      p.preview = (selector) =>
+        @__preview(p, selector)

       __then = p.then
-      p.then = => @__extendPromise __then.apply(p, arguments)
+      p.pipe = p.then = =>  # 'pipe' is alias to 'then' from jQuery 1.8
+        @__extendPromise __then.apply(p, arguments)

       p # extended promise


no-op.

no-op.

--- scope.coffee
+++ scope.coffee
@@ -59,7 +59,7 @@

   # Gets the type of a variable.
   type: (name) ->
-    for v in @variables when v.name is name then return v.type
+    return v.type for v in @variables when v.name is name
     null

   # If we need to store an intermediate result, find an available name for a


added shortcut for 'Find definition'

added shortcut for 'Find definition'

--- keymaps/atom-ternjs.cson
+++ keymaps/atom-ternjs.cson
@@ -7,3 +7,5 @@

 # For more detailed documentation see
 # https://atom.io/docs/latest/advanced/keymaps
+'atom-workspace':
+  'ctrl-alt-d': 'tern:definition'


check for pinned on update

check for pinned on update

--- content/keymanager.coffee
+++ content/keymanager.coffee
@@ -229,9 +229,8 @@
     debug('KeyManager.propose: getting existing key from extra field,if any')
     citekey = Citekey.get(item.getField('extra'))
     debug('KeyManager.propose: found key', citekey)
-    citekey.pinned = !!citekey.pinned
-
-    return citekey if citekey.pinned
+
+    return { citekey: citekey.citekey, pinned: true } if citekey.pinned

     debug('KeyManager.propose: formatting...', citekey)
     proposed = Formatter.format(item)
@@ -244,7 +243,7 @@
         re = (proposed.postfix == '0' && @postfixRE.numeric) || @postfixRE.alphabetic
         if citekey.citekey.slice(proposed.citekey.length).match(re)                                           # rest matches proposed postfix
           if !(other = @keys.findOne({ libraryID: item.libraryID, citekey: citekey.citekey, itemID: { $ne: item.id } })) # noone else is using it
-            return citekey
+            return { citekey: citekey.citekey, pinned: false }
 #          else
 #            debug('KeyManager.propose: no, because', other, 'is using it')
 #        else


Replace naive sort with Intl.Collator

Replace naive sort with Intl.Collator

--- lib/display-table.coffee
+++ lib/display-table.coffee
@@ -484,15 +484,9 @@
     @modelToScreenRowsMap = (@screenRows.indexOf(row) for row in rows)
     @computeRowOffsets()

-  compareRows: (order, direction) -> (a,b) ->
-    a = a[order]
-    b = b[order]
-    if a > b
-      direction
-    else if a < b
-      -direction
-    else
-      0
+  compareRows: (order, direction=1) ->
+    collator = new Intl.Collator("en-US", numeric: true)
+    return (a,b) -> collator.compare(a[order],b[order]) * direction

   ##     ######  ######## ##       ##        ######
   ##    ##    ## ##       ##       ##       ##    ##


Implement word cache around fontkit

Implement word cache around fontkit

Caches the results of fontkit layout calls, which can be expensive depending on the opentype features in the font. Splits up text by words and shapes each individually to increase cache efficiency.

--- lib/font/embedded.coffee
+++ lib/font/embedded.coffee
@@ -14,8 +14,57 @@
     @lineGap = @font.lineGap * @scale
     @bbox = @font.bbox

+    @layoutCache = Object.create(null)
+
+  layoutRun: (text, features) ->
+    run = @font.layout text, features
+
+    # Normalize position values
+    for position, i in run.positions
+      for key of position
+        position[key] *= @scale
+
+      position.advanceWidth = run.glyphs[i].advanceWidth * @scale
+
+    return run
+
+  layoutCached: (text) ->
+    if cached = @layoutCache[text]
+      return cached
+
+    run = @layoutRun text
+    @layoutCache[text] = run
+    return run
+
+  layout: (text, features, onlyWidth = false) ->
+    # Skip the cache if any user defined features are applied
+    if features
+      return @layoutRun text, features
+
+    glyphs = if onlyWidth then null else []
+    positions = if onlyWidth then null else []
+    advanceWidth = 0
+
+    # Split the string by words to increase cache efficiency.
+    # For this purpose, spaces and tabs are a good enough delimeter.
+    last = 0
+    index = 0
+    while index <= text.length
+      if (index is text.length and last < index) or text.charAt(index) in [' ', '\t']
+        run = @layoutCached text.slice(last, ++index)
+        if not onlyWidth
+          glyphs.push run.glyphs...
+          positions.push run.positions...
+
+        advanceWidth += run.advanceWidth
+        last = index
+      else
+        index++
+
+    return {glyphs, positions, advanceWidth}
+
   encode: (text, features) ->
-    {glyphs, positions} = @font.layout text, features
+    {glyphs, positions} = @layout text, features

     res = []
     for glyph, i in glyphs
@@ -25,16 +74,11 @@
       @widths[gid] ?= glyph.advanceWidth * @scale
       @unicode[gid] ?= glyph.codePoints

-      for key of positions[i]
-        positions[i][key] *= @scale
-
-      positions[i].advanceWidth = glyph.advanceWidth * @scale
-
     return [res, positions]

   widthOfString: (string, size, features) ->
-    width = @font.layout(string, features).advanceWidth
-    scale = size / @font.unitsPerEm
+    width = @layout(string, features, true).advanceWidth
+    scale = size / 1000
     return width * scale

   embed: ->


Add back root section

Add back root section

--- lib/general-panel.coffee
+++ lib/general-panel.coffee
@@ -27,9 +27,10 @@
     return if _.isEmpty(settings)

     @append $$ ->
-      @h1 class: 'section-heading', "#{_.undasherize(_.uncamelcase(namespace))} Settings"
-      for name in _.keys(settings).sort()
-        appendSetting.call(this, namespace, name, settings[name])
+      @section class: 'bordered', =>
+        @h1 class: 'section-heading', "#{_.undasherize(_.uncamelcase(namespace))} Settings"
+        for name in _.keys(settings).sort()
+          appendSetting.call(this, namespace, name, settings[name])

   bindFormFields: ->
     for input in @find('input[id]').toArray()


Change heading to say themes when theme button is pressed

--- lib/install-panel.coffee
+++ lib/install-panel.coffee
@@ -18,7 +18,7 @@
     @div =>
       @div class: 'section packages', =>
         @div class: 'section-container', =>
-          @h1 class: 'section-heading icon icon-cloud-download', 'Install Packages'
+          @h1 outlet: 'installHeading', class: 'section-heading icon icon-cloud-download', 'Install Packages'

           @div class: 'text native-key-bindings', tabindex: -1, =>
             @span class: 'icon icon-question'
@@ -39,9 +39,9 @@

       @div class: 'section packages', =>
         @div class: 'section-container', =>
-          @div outlet: 'featuredHeading', class: 'section-heading icon icon-star', 'Featured Packages'
+          @div outlet: 'featuredHeading', class: 'section-heading icon icon-star'
           @div outlet: 'featuredErrors'
-          @div outlet: 'loadingMessage', class: 'alert alert-info featured-message icon icon-hourglass', 'Loading featured packages\u2026'
+          @div outlet: 'loadingMessage', class: 'alert alert-info featured-message icon icon-hourglass'
           @div outlet: 'featuredContainer', class: 'container package-container'

   initialize: (@packageManager) ->
@@ -133,9 +133,11 @@
     @featuredContainer.empty()

     if loadThemes
+      @installHeading.text 'Install Themes'
+      @featuredHeading.text 'Featured Themes'
       @loadingMessage.text('Loading featured themes\u2026')
-      @featuredHeading.text 'Featured Themes'
     else
+      @installHeading.text 'Install Packages'
       @featuredHeading.text 'Featured Packages'
       @loadingMessage.text('Loading featured packages\u2026')


Replaced unobserve with dispose Fixes #74 Fixes #65 Fixes #52

--- lib/linter-jshint.coffee
+++ lib/linter-jshint.coffee
@@ -52,6 +52,6 @@
     "#{match.message} (#{match.type}#{match.code})"

   destroy: ->
-    atom.config.unobserve 'linter-jshint.jshintExecutablePath'
+    atom.config.dispose 'linter-jshint.jshintExecutablePath'

 module.exports = LinterJshint


Fix radio id

Fix radio id

--- modules/player/hud.coffee
+++ modules/player/hud.coffee
@@ -26,7 +26,7 @@
     #{@generateProgressOuter item}
     Now playing in **#{item.voiceChannel.name}**:
     >`#{item.title}` (#{@util.displayTime item.duration}) #{@util.displayFilters item.filters}
-    #{if item.radioStream then '\n\n' + @radioInfo(item) + '\n' else ''}
+    #{if item.radioStream then '\n\n' + (await @radioInfo(item)) + '\n' else ''}
     Requested by: **#{item.requestedBy.name}**
     """

@@ -156,7 +156,7 @@
     if item.filters and item.filters.length
       r.fields.push { inline: true, name: 'Filters', value: @util.displayFilters item.filters }
     if item.radioStream
-      r.description += "\n#{@getRadioTrack(item)}"
+      r.description += "\n#{await @radioInfo(item)}"
     r

   queue: (q, page=1)=>


Keep instance dropdown sorted

Keep instance dropdown sorted

--- SingularityUI/app/components/aggregateTail/Header.cjsx
+++ SingularityUI/app/components/aggregateTail/Header.cjsx
@@ -21,15 +21,14 @@
         )

   renderListItems: ->
-    tasks = @props.activeTasks.map (task) =>
-      taskId = task.id
-

-         @props.toggleViewingInstance(taskId)}>
-
-           Instance {task.taskId.instanceNo}
-
-

-
+    tasks = _.sortBy(@props.activeTasks, (t) => t.taskId.instanceNo).map (task, i) =>
+        taskId = task.id
+

+           @props.toggleViewingInstance(taskId)}>
+
+             Instance {task.taskId.instanceNo}
+
+

     if tasks.length > 0
       return tasks
     else


profile: added logo back in, removed avatar drop

--- client/landing/app/MainApp/lazy/staticprofilecontroller.coffee
+++ client/landing/app/MainApp/lazy/staticprofilecontroller.coffee
@@ -409,10 +409,10 @@
     @landingView._windowDidResize = =>
       @landingView.setHeight window.innerHeight
       @profileContentView.setHeight window.innerHeight-@profileTitleView.getHeight()
-      # @repositionLogoView()
-
-    groupAvatarDrop = new KDView
-      lazyDomId : 'landing-page-avatar-drop'
+
+
+    groupKodingLogo = new KDView
+      lazyDomId : 'landing-page-logo'
       tooltip   :
         title   : "Click here to go to Koding"
       click     : =>
@@ -420,6 +420,16 @@
           @lazyDomController.hideLandingPage()
         else
           @mainController.loginScreen.animateToForm 'login'
+
+    # groupAvatarDrop = new KDView
+    #   lazyDomId : 'landing-page-avatar-drop'
+    #   tooltip   :
+    #     title   : "Click here to go to Koding"
+    #   click     : =>
+    #     if KD.isLoggedIn()
+    #       @lazyDomController.hideLandingPage()
+    #     else
+    #       @mainController.loginScreen.animateToForm 'login'

     @profileTitleView = new KDView
       lazyDomId : 'profile-title'


display loading more in a react manner

display loading more in a react manner

--- core/app/backbone/views/sub_comments/react_sub_comments.coffee
+++ core/app/backbone/views/sub_comments/react_sub_comments.coffee
@@ -36,16 +36,14 @@
         Factlink.Global.t.post_subcomment

 window.ReactSubCommentList = React.createBackboneClass
-  getInitialState: ->
-    loading: true
+  componentDidMount: ->
+    @props.model.on 'sync request', (-> @forceUpdate()), @

-  componentDidMount: ->
-    @model().fetch
-      success: => @setState loading: false
-    @setState loading: true
+  componentWillMount: ->
+    @model().fetch()

   render: ->
-    if @model().size() == 0 && @state.loading
+    if @model().size() == 0 && @model().loading()
       ReactLoadingIndicator()
     else
       R.div className: 'sub-comments',


Default logging level.

Default logging level.

--- src/Notifications.coffee
+++ src/Notifications.coffee
@@ -26,6 +26,10 @@

   _bindLogger: (bindArgs) ->
     unless Logger? then throw new Error('Logger module not found - cannot bind to notifications.')
+    # By default, only generate notifications for errors but continue to allow logging with Logger
+    # on the existing level.
+    if bindArgs == true
+      bindArgs = {level: 'error'}
     bindLevel = bindArgs.level
     oldMsg = Logger.msg
     Logger.msg = (msg, args, func) ->


add observables on project for getting paying-user and paid-parallelism

--- js/inner.coffee
+++ js/inner.coffee
@@ -569,6 +569,9 @@
     followed: null
     loading_users: false
     users: []
+    paying_user: null
+    parallel: 1 #TODO: Load this from server
+    parallelism_options: [1..10]

   constructor: (json) ->

@@ -595,6 +598,8 @@
     else
       if l.vcs_url().toLowerCase() > r.vcs_url().toLowerCase() then 1 else -1

+  paid_parallelism: =>
+    if @paying_user() then @paying_user().parallelism else 1

   checkbox_title: =>
     "Add CI to #{@project_name()}"
@@ -1268,6 +1273,7 @@

     subpage = subpage[0].replace('#', '')
     subpage = subpage || "settings"
+
     $('#main').html(HAML['edit']({project: project_name}))
     $('#subpage').html(HAML['edit_' + subpage]({}))
     ko.applyBindings(VM)


Remove unneeded semicolon

Remove unneeded semicolon

--- js/outer.coffee
+++ js/outer.coffee
@@ -23,7 +23,7 @@

     scroll: (hash) ->
       if hash == '' or hash == '#' then hash = "body"
-      $('html, body').animate({scrollTop: $(hash).offset().top}, 0);
+      $('html, body').animate({scrollTop: $(hash).offset().top}, 0)

     display: ->
       if HAML? and HAML[@name]?


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/active_bounds.coffee
+++ src/active_bounds.coffee
@@ -28,7 +28,7 @@

     # => bullet2 will be removed when it
          goes outside the bounds
-
+         x: 0

 ActiveBounds module
 @name ActiveBounds


its annoying

its annoying

--- src/archer-quotes.coffee
+++ src/archer-quotes.coffee
@@ -27,11 +27,6 @@
     "The zone will be one of danger?"
     "https://www.youtube.com/watch?v=siwpn14IE7E"
   ]
-  hadSomething: [
-    "hold on! I had something good for this",
-    "Pam, wait! I had something… what was it? … Uh, something about stripes and solids.",
-    "Oh, good, you caught the, uh, oh wait, I had something good for this... the... 'Pita Predator'."
-  ]

 module.exports = (robot) ->
   # TODO read this from an environment variable
@@ -40,9 +35,6 @@

   robot.hear /danger|loggin|logins|obsess|life insurance/i, (msg) ->
     msg.send msg.random sayings.dangerZone
-
-  robot.hear /situation|dealing|stress|!/i, (msg) ->
-    msg.send msg.random sayings.hadSomething

   robot.hear /ocelot/i, (msg) ->
     msg.send "I've never seen an ocelot!"


three line long windows explanation of how to deal with Administrator privileges.

--- src/npm.iced
+++ src/npm.iced
@@ -4,6 +4,7 @@
 fs = require 'fs'
 log = require './log'
 {prng} = require 'crypto'
+os = require 'os'

 ##-----------------------------------
 _config = null
@@ -41,7 +42,13 @@
     await fs.writeFile test, (new Buffer []), { mode : 0o600 }, defer err
     if err?
       if err.code in [ 'EACCES', 'EPERM' ]
-        err = new Error "Permission denied installing to #{dirname}: try running `sudo keybase-installer`"
+        if os.platform() is 'win32' # this is actually any version of windows
+          err = new Error "Permission denied - Node was installed as Administrator.\n" +
+            "\n  Windows solution: launch another command window by right-clicking" +
+            "\n  and selecting \"Run as Administrator\"," +
+            "\n  then run `keybase-installer`. (Then you may close the window.)\n"
+        else
+          err = new Error "Permission denied installing to #{dirname}: try running `sudo keybase-installer`"
       else
         err = new Error "Can't write to directory #{dirname}: #{err.code}"
     else


Implemented caching (via brain) for Steam data

--- steam.coffee
+++ steam.coffee
@@ -61,6 +61,9 @@
     if not STEAM_API_KEY?
         return robot.logger.debug 'Missing STEAM_API_KEY in environment. Please set and try again.'

+    steamData = () ->
+        robot.brain.data.steam ?= {}
+
     robot.respond /steam id( me)? (.*)/i, (msg) ->
         getSteamID msg, msg.match[2], (id) ->
             msg.match[2] += if msg.match[2].slice(-1) is "s" then "'" else "'s"
@@ -149,36 +152,54 @@
                     getSteamID msg, msg.match[2], (steamID) ->
                         playerInfo getCommunityID(steamID)

-getSteamID = (msg, customURL, handler) ->
-    steam_request msg, "/ISteamUser/ResolveVanityURL", vanityurl: customURL, (object) ->
-        if object.response.success is 42
-            msg.reply "The custom URL you have entered (\"#{customURL}\") does not exist."
-            return
-
-        handler object.response.steamid
-
-getCommunityID = (steamID) ->
-    # 64 -> 32
-    buffer = new Buffer 8
-    buffer.writeUInt64LE steamID, 0
-    buffer.readUInt32LE 0
-
-getHero = (heroID) ->
-    return hero for hero in heroes when hero.id is heroID
-
-getTowers = (dec) ->
-    for status, tower in "00000000000#{(+dec).toString(2)}".slice(-11).split('')
-        if parseInt(status) then towers[tower] else continue
-
-steam_request = (msg, endpoint, params = {}, handler, version = 1) ->
-    params.key = STEAM_API_KEY
-
-    msg.http("http://api.steampowered.com#{endpoint}/v#{version}/")
-        .query(params)
-        .get() (err, res, body) ->
-            if err or res.statusCode isnt 200
-                err = "Bad request (invalid Steam web API key)" if res.statusCode is 400
-                msg.reply "An error occurred while attempting to process your request."
-                return robot.logger.error err
-
-            handler JSON.parse(body)
+    getSteamID = (msg, customURL, handler) ->
+        for steamID, user of steamData()
+            return handler steamID if user.url is customURL
+
+        steam_request msg, "/ISteamUser/ResolveVanityURL", vanityurl: customURL, (object) ->
+            if object.response.success is 42
+                msg.reply "The custom URL you have entered (\"#{customURL}\") does not exist."
+                return
+
+            if steamData()[object.response.steamid]?
+                steamData()[object.response.steamid].url = customURL
+            else
+                steamData()[object.response.steamid] = url: customURL
+
+            handler object.response.steamid
+
+    getCommunityID = (steamID) ->
+        if steamData()[steamID]?.communityID?
+            return steamData()[steamID].communityID
+
+        # 64 -> 32
+        buffer = new Buffer 8
+        buffer.writeUInt64LE steamID, 0
+        communityID = buffer.readUInt32LE 0
+
+        if steamData()[steamID]?
+            steamData()[steamID].communityID = communityID
+        else
+            steamData()[object.response.steamid] = communityID: communityID
+
+        communityID
+
+    getHero = (heroID) ->
+        return hero for hero in heroes when hero.id is heroID
+
+    getTowers = (dec) ->
+        for status, tower in "00000000000#{(+dec).toString(2)}".slice(-11).split('')
+            if parseInt(status) then towers[tower] else continue
+
+    steam_request = (msg, endpoint, params = {}, handler, version = 1) ->
+        params.key = STEAM_API_KEY
+
+        msg.http("http://api.steampowered.com#{endpoint}/v#{version}/")
+            .query(params)
+            .get() (err, res, body) ->
+                if err or res.statusCode isnt 200
+                    err = "Bad request (invalid Steam web API key)" if res.statusCode is 400
+                    msg.reply "An error occurred while attempting to process your request."
+                    return robot.logger.error err
+
+                handler JSON.parse(body)