Add comments explaining the parser and utility methods. This might be useful for some.

--- lib/message.coffee
+++ lib/message.coffee
@@ -6,65 +6,104 @@

 class IRCMessage
   constructor: (line) ->
+    # Object containing any message tags.
     @tags = {}
+    # Strings containing the message's prefix and command.
     @prefix = ""
     @command = ""
+    # Any params (middle or trailing) will be pushed to this array.
     @params = []
+    # position and nextspace are used by the parser as a reference.
     position = 0
     nextspace = 0
+
+    # The first thing we check for is IRCv3.2 message tags.
+    # http://ircv3.atheme.org/specification/message-tags-3.2

     if line.charAt(0) is "@"
       nextspace = line.indexOf " "
       return new Object if nextspace is -1
+      # Tags are split by a semi colon.
       rawTags = line.slice(1, nextspace).split ";"

       for tag in rawTags
+        # Tags delimited by an equals sign are key=value tags.
+        # If there's no equals, we assign the tag a value of true.
         pair = tag.split "="
         @tags[pair[0]] = pair[1] or yes

       position = nextspace + 1

+    # Skip any trailing whitespace.
+
     position++ while line.charAt(position) is " "
+
+    # Extract the message's prefix if present. Prefixes are prepended
+    # with a colon.

     if line.charAt(position) is ":"
       nextspace = line.indexOf " ", position
+
+      # If there's nothing after the prefix, deem this message to be
+      # invalid.
       return new Object if nextspace is -1
       @prefix = line.slice position + 1, nextspace
       position = nextspace + 1
+
+      # Skip any trailing whitespace.
       position++ while line.charAt(position) is " "

     nextspace = line.indexOf " ", position

+    # If there's no more whitespace left, extract everything from the
+    # current position to the end of the string as the command.
     if nextspace is -1
       if line.length > position
-        @command = line.slice(position)
+        @command = line.slice position
         return
-      else return
+      else
+        return
+
+    # Else, the command is the current position up to the next space. After
+    # that, we expect some parameters.

     @command = line.slice(position, nextspace)

     position = nextspace + 1
+
+    # Skip any trailing whitespace.
     position++ while line.charAt(position) is " "

     while position < line.length
       nextspace = line.indexOf " ", position
+
+      # If the character is a colon, we've got a trailing parameter.
+      # At this point, there are no extra params, so we push everything
+      # from after the colon to the end of the string, to the params array
+      # and break out of the loop.
       if line.charAt(position) is ":"
         @params.push line.slice position + 1
         break

+      # If we still have some whitespace...
       if nextspace isnt -1
+        # Push whatever's between the current position and the next
+        # space to the params array.
         @params.push line.slice position, nextspace
         position = nextspace + 1
+        # Skip any trailing whitespace and continue looping.
         position++ while line.charAt(position) is " "
         continue

+      # If we don't have any more whitespace and the param isn't trailing,
+      # push everything remaining to the params array.
       if nextspace is -1
         @params.push line.slice position
         break
-    return

   toString: ->
     string = ""
+    # Append any message tags if present.
     if Object.keys(@tags).length isnt 0
       string += "@"
       for tag, value of @tags
@@ -72,7 +111,7 @@
           string += "#{tag}=#{value};"
         else string += "#{tag};"
       string = string.slice(0, -1) + " "
-
+
     if @prefix.length isnt 0
       string += ":#{@prefix} "

@@ -89,9 +128,28 @@
     string = string.slice 0, -1
     return string

-  prefixIsHostmask: -> (@prefix.indexOf("@") isnt -1 and @prefix.indexOf("!") isnt -1)
-  prefixIsServer: -> (@prefix.indexOf("@") is -1 and @prefix.indexOf("!") is -1 and @prefix.indexOf(".") isnt -1)
+  prefixIsHostmask: ->
+    # This method determines whether or not the prefix is a hostmask.
+    # A regular expression might be a better choice for this; at the
+    # moment, it checks to see if the prefix contains an @ and an !,
+    # so it's very lax.
+
+    @prefix.indexOf("@") isnt -1 and
+    @prefix.indexOf("!") isnt -1
+
+  prefixIsServer: ->
+    # This method determines whether or not the prefix is a server host.
+    # Similarly to #prefixIsHostmask(), a regular expression would be a
+    # wiser choice here.
+
+    @prefix.indexOf("@") is -1 and
+    @prefix.indexOf("!") is -1 and
+    @prefix.indexOf(".") isnt -1
+
   parseHostmaskFromPrefix: ->
+    # Simple method to parse the hostmask of the current prefix into an
+    # object with nickname, username and hostname. Returns null if the
+    # return value of #prefixIsHostmask() is false.
     if @prefixIsHostmask()
       [nickname, username, hostname] = @prefix.split /[!@]/
       return (
@@ -103,6 +161,13 @@
       return null

 Message = (line) ->
+  # This was a little bit of a hack. This function takes in the raw
+  # line and creates a new IRCMessage object with it. If the message
+  # could not be successfully parsed, the constructor returns an empty
+  # object. If the returned object has no keys, this function returns
+  # null for error handling convenience. Else, the IRCMessage object
+  # is returned.
+
   message = new IRCMessage line
   if Object.getOwnPropertyNames(message).length == 0
     return null


Fix issue with multiple projects open

Fix issue with multiple projects open

--- lib/psc-ide.coffee
+++ lib/psc-ide.coffee
@@ -42,9 +42,9 @@
     @pscIdePort = atom.config.get("ide-purescript.pscIdePort")
     pscIdeServer = atom.config.get("ide-purescript.pscIdeServerExe")
     dirs = atom.project.rootDirectories
+    path = dirs[0].path
     if dirs.length > 1
-      atom.notifications.addWarning "Multiple project roots - using #{dir}"
-    path = dirs[0].path
+      atom.notifications.addWarning "Multiple project roots - using #{path}"

     exit = (code) =>
       if code isnt 0


Pass current attribute values to vparam scripts

--- lib/session.coffee
+++ lib/session.coffee
@@ -1022,7 +1022,13 @@
   for declaration in virtualParameterDeclarations
     if declaration[1]
       provisions ?= []
-      provisions.push([declaration[0][1], declaration[1], undefined])
+      currentTimestamps = {}
+      currentValues = {}
+      attrs = sessionContext.deviceData.attributes.get(declaration[0])
+      for k, v of attrs
+        currentTimestamps[k] = v[0]
+        currentValues[k] = v[1]
+      provisions.push([declaration[0][1], declaration[1], {}, currentTimestamps, currentValues])
       delete declaration[1]

   return provisions
@@ -1040,7 +1046,12 @@

         if val[0] != curVal[0] or val[1] != curVal[1]
           provisions ?= []
-          provisions.push([declaration[0][1], undefined, {value: val}])
+          currentTimestamps = {}
+          currentValues = {}
+          for k, v of attrs
+            currentTimestamps[k] = v[0]
+            currentValues[k] = v[1]
+          provisions.push([declaration[0][1], {}, {value: val}, currentTimestamps, currentValues])

   return provisions


Fix typo

Fix typo

--- index.cjsx
+++ index.cjsx
@@ -182,7 +182,7 @@
           enemyShips = body.api_deck.api_ships
           baseExp = exp[enemyShips[0].api_level] / 100 + exp[enemyShips[1].api_level ? 0] / 300
           baseExp = if baseExp <= 500 then baseExp else Math.floor 500 + Math.sqrt baseExp - 500
-          window.success "#{__("Expect base experience")}: #{basicExp}",
+          window.success "#{__("Expected base experience")}: #{baseExp}",
             stickyFor: 1000
     componentDidMount: ->
       window.addEventListener 'game.response', @handleResponse


fix matrix

fix matrix

--- src/project/services/projectUtils.coffee
+++ src/project/services/projectUtils.coffee
@@ -35,7 +35,7 @@
   getDevTeam: (team) ->
     return [] unless _.isArray team
     _.filter team, (member) ->
-      member?.role?.value in ['dev', 'archi']
+      member?.role in ['dev', 'archi']
   getRoleLabel: (key) ->
     result = _.find roles, (role) ->
       role.value is key


Update locale.coffee

Update locale.coffee

巫师吴语->吴语
巫師吳語->吳語

--- app/locale/locale.coffee
+++ app/locale/locale.coffee
@@ -19,8 +19,8 @@
   zh: require './zh'             # 中文, Chinese
   'zh-HANS': require './zh-HANS' # 简体中文, Chinese (Simplified)
   'zh-HANT': require './zh-HANT' # 繁体中文, Chinese (Traditional)
-  'zh-WUU-HANS': require './zh-WUU-HANS'   # 巫师吴语, Wuu (Simplified)
-  'zh-WUU-HANT': require './zh-WUU-HANT'   # 巫師吳語, Wuu (Traditional)
+  'zh-WUU-HANS': require './zh-WUU-HANS'   # 吴语, Wuu (Simplified)
+  'zh-WUU-HANT': require './zh-WUU-HANT'   # 吳語, Wuu (Traditional)
   fr: require './fr'             # français, French
   ja: require './ja'             # 日本語, Japanese
   ar: require './ar'             # العربية, Arabic


Update to new css package name

Update to new css package name

--- spec/autocomplete-spec.coffee
+++ spec/autocomplete-spec.coffee
@@ -449,7 +449,7 @@
     expect(autocomplete.list.prop('scrollWidth')).toBe autocomplete.list.width()

   it "includes completions for the scope's completion preferences", ->
-    atom.activatePackage('css-tmbundle', sync: true)
+    atom.activatePackage('language-css', sync: true)
     cssEditor = new Editor(editSession: project.openSync('css.css'))
     autocomplete = new AutocompleteView(cssEditor)


tests to check branches related to using event.orginalEvent or just event.

--- spec/fme-sortable-spec.coffee
+++ spec/fme-sortable-spec.coffee
@@ -44,7 +44,7 @@

         mockEvent = $.Event('dragstart')

-        mockEvent.originalEvent.dataTransfer = {setData: (type,data)-> true}
+        mockEvent.originalEvent = {dataTransfer:{setData: (type,data)-> true}}
         sinon.stub(mockEvent.dataTransfer,'setData')

         @element.find('li:first').triggerHandler(mockEvent)


Prefer cursor targets or the last matching node in #findNodeForLocation

--- src/trix/models/selection_manager.coffee
+++ src/trix/models/selection_manager.coffee
@@ -169,19 +169,25 @@
   findNodeForLocation: (location) ->
     walker = DOM.createTreeWalker(@element, null, trixNodeFilter)
     node = walker.currentNode
+    match = null

     while walker.nextNode()
-      currentNode = walker.currentNode
-
       if walker.currentNode.trixIndex is location.index
         startPosition = walker.currentNode.trixPosition
         endPosition = startPosition + walker.currentNode.trixLength

         if startPosition <= location.offset <= endPosition
-          node = walker.currentNode
+          match = walker.currentNode
+
+        if match?.trixCursorTarget
           break

-    node
+        if match and startPosition > location.offset
+          break
+
+      previousNode = walker.currentNode
+
+    match ? node

   trixNodeFilter = (node) ->
     if node.trixPosition? and node.trixLength?


Revert back file accidentally included in 84cef31

--- spec/results-view-spec.coffee
+++ spec/results-view-spec.coffee
@@ -272,7 +272,7 @@

         expect(resultsView.find(".path-details").length).toBe 3

-    fdescribe "core:page-up and core:page-down", ->
+    describe "core:page-up and core:page-down", ->
       beforeEach ->
         workspaceElement.style.height = '300px'
         projectFindView.findEditor.setText(' ')
@@ -316,14 +316,13 @@

         expect(resultsView.find("li:last")).toHaveClass 'selected'

-      fit "selects the first result on the next page when core:page-up is triggered", ->
+      it "selects the first result on the next page when core:page-up is triggered", ->
         atom.commands.dispatch resultsView.element, 'core:move-to-bottom'

         itemHeight = resultsView.find('.selected').outerHeight()
         pageHeight = Math.round(resultsView.innerHeight() / itemHeight) * itemHeight
         initialScrollTop = resultsView.scrollTop()
         itemsPerPage = Math.floor(pageHeight / itemHeight)
-        console.log("#{itemHeight} #{pageHeight} #{initialScrollTop} #{itemsPerPage} #{resultsView.innerHeight()}")

         initiallySelectedIndex = Math.floor(initialScrollTop / itemHeight) + itemsPerPage
         expect(resultsView.find("li:eq(#{initiallySelectedIndex})")).toHaveClass 'selected'


Builder: disable liveReload

Builder: disable liveReload

--- Builder.coffee
+++ Builder.coffee
@@ -179,16 +179,6 @@
     if @config.client.watch is yes
       if initial
         log.info "\n All done. Watching for changes... \n"
-        @livereloads = []
-        wss = new WebSocketServer port: 35729, path: "/livereload"
-        wss.on 'connection', (ws)=>
-          ws.send "!!ver:1.6"
-          @livereloads.push ws
-
-      if includesChanged or scriptsChanged or stylesChanged
-        for ws in @livereloads
-          if ws.readyState == WebSocket.OPEN
-            ws.send JSON.stringify ['refresh', { path: "" }]

       setTimeout =>
         @compileChanged options, false


router: reverted seperate-history wip (now in branch arvid-history)

--- client/Framework/core/KDRouter.coffee
+++ client/Framework/core/KDRouter.coffee
@@ -1,8 +1,6 @@
 class KDRouter extends KDObject

   {history} = window
-
-  seperateHistories = {}

   listenerKey = 'ಠ_ಠ'

@@ -117,41 +115,15 @@
     path += "?#{qs}"  if qs.length

     if not suppressListeners and shouldPushState and not replaceState and path is @currentPath
-      if @nextCallWillDeleteHistory is frag[0]
-        seperateHistories[frag[0]]= []
-        delete @nextCallWillDeleteHistory
-
-        new KDNotificationView
-          title : 'History for this Application was cleared'
-      else
-        @emit 'AlreadyHere', path
-        return
+      @emit 'AlreadyHere', path
+      return

     @currentPath = path

     if shouldPushState
-      if frag.length is 1 and seperateHistories[frag[0]]?.length
-        @nextCallWillDeleteHistory = frag[0]
-
-        seperateItem = seperateHistories[frag[0]].pop()
-
-        {node,query,objRef,path,replaceState,frag,state} = seperateItem
-
       method = if replaceState then 'replaceState' else 'pushState'
       history[method] objRef, path, "/#{path}"
-
-      seperateHistories[frag[0]] ?= []
-      seperateHistories[frag[0]].push {
-        objRef
-        path
-        fullpath: "/#{path}"
-        replaceState
-        query
-        node
-        frag
-        state
-      }
-
+
     for edge in frag
       if node[edge]
         node = node[edge]


Client: Removed redundant code

Client: Removed redundant code

--- client/app/js/collections/team.coffee
+++ client/app/js/collections/team.coffee
@@ -34,12 +34,10 @@
   # Converts JSON to a team object.
   # Usage: team = Team.fromJSON(jsonObject, teambuilder: true)
   @fromJSON: (json, options={}) =>
-    {name, generation, pokemon} = json
+    {pokemon} = json
     p.teambuilder = true  for p in pokemon
     attributes = _.clone(json)
     attributes.teambuilder = true  if options.teambuilder
-    attributes.name = name
-    attributes.generation = generation
     delete attributes.pokemon
     return new Team(pokemon, attributes)


browser-plus on package list

browser-plus on package list

--- atom/.atom/packages.cson
+++ atom/.atom/packages.cson
@@ -3,5 +3,6 @@
   "term2",
   "merge-conflicts",
   "git-log",
-  "git-history"
+  "git-history",
+  "browser-plus"
 ]


Fixed counter

Fixed counter

--- server/src/Component/Builtin/Database/CouchMuffin.coffee
+++ server/src/Component/Builtin/Database/CouchMuffin.coffee
@@ -9,7 +9,6 @@
         @_keyPrefix = '' || options?.keyPrefix
         @_autoId = '' || options?.autoId
         @_trackDates = options?.trackDates
-        @_counterKey = "#{@_keyPrefix}counter"

     init: ->
         @_dataSource = @component 'DataSource.Couchbase', @_dataSourceName
@@ -33,21 +32,21 @@

     _createCounter: (callback) ->
         data =
-            id: @_counterKey
+            id: 'counter'
             data: 1
             options:
                 validate: false
                 match: false
         @insert data, (error) ->
             return callback error if error
-            return callback null, '1'
+            return callback null, 1

     _uuid: ->
         uid = uuid.v4()
         return "#{@_keyPrefix}#{uid}"

     _counter: (callback) ->
-        @_dataSource.bucket.counter @_keyPrefix + @_counterKey, 1, (error, result) =>
+        @_dataSource.bucket.counter "#{@_keyPrefix}counter", 1, (error, result) =>
             return @_createCounter callback if error and error.code is 13
             return callback error if error
             return callback null, result.value
@@ -150,7 +149,6 @@
     # @param {function} callback Called when the operation is completed (error, result)
     insert: (params, callback) ->
         id = params.id || null
-        id = "#{@_keyPrefix}#{id}" if id?
         data = params.data || {}
         options = params.options || {}
         validate = options.validate ? true
@@ -158,6 +156,7 @@

         afterId = (error, newId) =>
             return callback error if error
+            newIdWithPrefix = "#{@_keyPrefix}#{newId}"

             afterValidate = (error = null) =>
                 return callback error if error?
@@ -169,7 +168,7 @@
                 @_addType data
                 @_addCreatedAt data

-                @_dataSource.bucket.insert newId, data, options, (error, result) ->
+                @_dataSource.bucket.insert newIdWithPrefix, data, options, (error, result) ->
                     return callback error if error?
                     return callback null, result


client: landing: made welcome stacks modal default route

--- client/stacks/lib/routehandler.coffee
+++ client/stacks/lib/routehandler.coffee
@@ -6,6 +6,6 @@
   options =
     name      : 'stacks'
     title     : 'Stacks'
-    homeRoute : '/Stacks/My-Stacks'
+    homeRoute : '/Stacks/Welcome'

   routeHandler options


Fix missing reference to cilantro namespace

--- coffee/cilantro/models/concept.coffee
+++ coffee/cilantro/models/concept.coffee
@@ -1,6 +1,6 @@
 define ['../core', './field'], (c, field) ->

-    class ConceptModel extends Backbone.Model
+    class ConceptModel extends c.Backbone.Model
         parse: (resp) ->
             @fields = []
             # Parse and attach field model instances to concept
@@ -9,7 +9,7 @@
             return resp

-    class ConceptCollection extends Backbone.Collection
+    class ConceptCollection extends c.Backbone.Collection
         model: ConceptModel

         url: ->


controlViews: add play and pause controls

--- coffee/src/views/controls_view.coffee
+++ coffee/src/views/controls_view.coffee
@@ -150,6 +150,24 @@

+class PlayControlView extends IconControlView
+  controlName: => 'PlayControl'
+  className: @classNameExtend 'play'
+  tooltip: => title: 'Play', delay: show: 1500
+  defaults: => _.extend super,
+    icon: 'play'
+
+
+
+class PauseControlView extends IconControlView
+  controlName: => 'PauseControl'
+  className: @classNameExtend 'pause'
+  tooltip: => title: 'Pause', delay: show: 1500
+  defaults: => _.extend super,
+    icon: 'pause'
+
+
+
 class ImageControlView extends ControlView

@@ -198,6 +216,8 @@
 acorn.player.controls.NextControlView = NextControlView
 acorn.player.controls.PreviousControlView = PreviousControlView
 acorn.player.controls.GridControlView = GridControlView
+acorn.player.controls.PlayControlView = PlayControlView
+acorn.player.controls.PauseControlView = PauseControlView

 acorn.player.controls.ImageControlView = ImageControlView
 acorn.player.controls.AcornControlView = AcornControlView


make a function, use value-store

make a function, use value-store

--- bin/command.coffee
+++ bin/command.coffee
@@ -1,53 +1,56 @@
-corepath = require 'path'
-nuc = require 'nuc'
-buildValueStore = require('value-store')
+module.exports = (args, input, output) ->

-# ouptput the version from the package.json file
-if '-v' in process.argv or '--version' in process.argv
-  pkg = require corepath.join __dirname, '..', 'package.json'
-  console.log "kevas v#{pkg.version}\n"
-  return process.exit()
+  corepath = require 'path'
+  nuc      = require 'nuc'
+  buildValueStore = require('value-store')

-# see if there's an app id for `nuc` to use
-id = nuc.findId()
+  # ouptput the version from the package.json file
+  if '-v' in args or '--version' in args
+    pkg = require corepath.join __dirname, '..', 'package.json'
+    console.log "kevas v#{pkg.version}\n"
+    return process.exit(0)

-# now build a value store either:
-values =
-  # 1. directly, when there's no id
-  if id?.__error? then buildValueStore [{}]
-  # 2. via `nuc` when there is an id
-  else nuc id:id, collapse:false, stack:true
+  # see if there's an app id for `nuc` to use
+  id = nuc.findId()

-# now, try to load each cli arg into the value store
-for arg in process.argv
+  # now build a value store either:
+  values =
+    # 1. directly, when there's no id
+    if id?.__error? then buildValueStore [{}]
+    # 2. via `nuc` when there is an id
+    else nuc id:id, collapse:false, stack:true

-  # try it as a file path by having value store attempt to read it
-  result = values.append arg
+  # now, try to load each cli arg into the value store
+  for arg in args

-  # when there's no error then it was loaded so move on to the next one
-  unless result?.error? then continue
+    # try it as a file path by having value store attempt to read it
+    result = values.append arg

-  # otherwise, try to convert it to a key or key=value to put into values
-  index = arg.indexOf '='
-  if index < 0 then values.set arg, true
-  else values.set arg[...index], arg[(index + 1)...]
+    # when there's no error then it was loaded so move on to the next one
+    unless result?.error? then continue
+
+    # otherwise, try to convert it to a key or key=value to put into values
+    index = arg.indexOf '='
+    if index < 0 then values.set arg, true
+    else values.set arg[...index], arg[(index + 1)...]

-# build our kevas transform
-kevas = require('../lib')()
+  # build our kevas transform
+  kevas = require('../lib')()

-# add a key to value translator using our values
-kevas.on 'key', ->
-  # get value from values store
-  value = values.get @key
+  # add a key to value translator using our values
+  kevas.on 'key', ->

-  # if there's a value and it's not a string then convert it to one
-  if value? and typeof value isnt 'string' then value = '' + value
+    # get value from values store
+    value = values.get @key

-  # push the value or maintain the key in the content
-  @values.push value ? ('{{' + @key + '}}')
+    # if there's a value and it's not a string then convert it to one
+    if value? and typeof value isnt 'string' then value = '' + value

-  return
+    # push the value or maintain the key in the content
+    @values.push value ? ('{{' + @key + '}}')

-# setup pipeline
-process.stdin.pipe(kevas).pipe process.stdout
+    return
+
+  # setup pipeline
+  input.pipe(kevas).pipe output


updating tests to remove save button from plan footer

--- test/components/reading-plan.spec.coffee
+++ test/components/reading-plan.spec.coffee
@@ -54,20 +54,17 @@
       id: '0'

     node = helper(model, false)
-    expect(node.querySelector('.-save.disabled')).to.not.be.null
     expect(node.querySelector('.-publish.disabled')).to.not.be.null
     expect(node.querySelector('.-delete')).to.not.be.null

   it 'should have the right buttons when valid but not changed', ->
     node = helper(VALID_MODEL, false)
-    expect(node.querySelector('.-save.disabled')).to.not.be.null
     expect(node.querySelector('.-publish.disabled')).to.be.null
     expect(node.querySelector('.-delete')).to.not.be.null

-  it 'should allow save when the title is changed', ->
+  it 'should allow publish/save when the title is changed', ->
     node = helper(VALID_MODEL, true)
-    expect(node.querySelector('.-save.disabled')).to.be.null
-    expect(node.querySelector('.-publish.disabled')).to.not.be.null
+    expect(node.querySelector('.-publish.disabled')).to.be.null
     expect(node.querySelector('.-delete')).to.not.be.null


updated to look for index.htm as well as index.html

--- lib/http.coffee
+++ lib/http.coffee
@@ -44,8 +44,18 @@
       res.end()
       return
     if fs.statSync(filename).isDirectory()
-      filename += '/index.html'
-
+      ext = ".html"
+      path.exists (filename+'/index'+ext), (exists) ->
+        unless exists
+          path.exists (filename+'/index.htm'), (exists) ->
+            unless exists
+              res.writeHead 404, 'Content-Type': 'text/plain'
+              res.end "your client '"+filename+"/index"+ext+"' was not found"
+              return
+            ext = ".htm"
+
+      filename += '/index'+ext
+
     fs.readFile filename, 'binary', (err, file) ->
       if err
         res.writeHead 500, 'Content-Type': 'text/plain'


Test the FileHandler layer.

Test the FileHandler layer.

--- test/unit/coffee/FileHandlerTests.coffee
+++ test/unit/coffee/FileHandlerTests.coffee
@@ -93,6 +93,13 @@
 				@handler._getConvertedFile.called.should.equal false
 				done()

+		it "should pass options to _getStandardFile", (done) ->
+			options = {start: 0, end: 8}
+			@handler.getFile @bucket, @key, options, =>
+				expect(@handler._getStandardFile.lastCall.args[2].start).to.equal 0
+				expect(@handler._getStandardFile.lastCall.args[2].end).to.equal 8
+				done()
+
 		it "should call _getConvertedFile if a format is defined", (done)->
 			@handler.getFile @bucket, @key, format:"png", =>
 				@handler._getStandardFile.called.should.equal false
@@ -116,6 +123,13 @@
 				err.should.equal "err"
 				stream.should.equal @fileStream
 				done()
+
+		it "should pass options to PersistorManager", (done) ->
+			@handler.getFile @bucket, @key, {start: 0, end: 8}, =>
+				expect(@PersistorManager.getFileStream.lastCall.args[2].start).to.equal 0
+				expect(@PersistorManager.getFileStream.lastCall.args[2].end).to.equal 8
+				done()
+

 	describe "_getConvertedFile", ->

@@ -178,7 +192,3 @@
 				@FileConverter.convert.calledWith(@stubbedPath, @format).should.equal true
 				@LocalFileWriter.deleteFile.calledWith(@stubbedPath).should.equal true
 				done()
-
-
-
-


set refresh interval to 800 ms

set refresh interval to 800 ms

--- lib/init.coffee
+++ lib/init.coffee
@@ -361,7 +361,7 @@
     autoRefreshEl = document.getElementById('lupa-refresh')
     if value
         autoRefreshEl.style.display = 'none'
-        refreshInterval = setInterval(refresh, 1500)
+        refreshInterval = setInterval(refresh, 800)
     else
         clearInterval(refreshInterval)
         autoRefreshEl.style.display = 'block'


Restore old .txt extension command

Restore old .txt extension command

--- lib/main.coffee
+++ lib/main.coffee
@@ -53,7 +53,7 @@
     atom.commands.add '.tree-view .file .name[data-name$=\\.mkd]', 'markdown-preview:preview-file', previewFile
     atom.commands.add '.tree-view .file .name[data-name$=\\.mkdown]', 'markdown-preview:preview-file', previewFile
     atom.commands.add '.tree-view .file .name[data-name$=\\.ron]', 'markdown-preview:preview-file', previewFile
-    atom.commands.add '.tree-view .file .name[data-name$=\\.text]', 'markdown-preview:preview-file', previewFile
+    atom.commands.add '.tree-view .file .name[data-name$=\\.txt]', 'markdown-preview:preview-file', previewFile

     atom.workspace.addOpener (uriToOpen) ->
       try


still WIP #3

still WIP #3

--- lib/main.coffee
+++ lib/main.coffee
@@ -1,4 +1,4 @@
-{CompositeDisposable, TextEditor} = require 'atom'
+{CompositeDisposable, TextEditor, Emitter} = require 'atom'
 path = require 'path'

 History    = null
@@ -24,13 +24,21 @@
   subscriptions: null
   editorSubscriptions: null

+  onWillJumpToHistory: (callback) ->
+    @emitter.on 'will-jump-to-history', callback
+
+  onDidJumpToHistory:  (callback) ->
+    @emitter.on 'did-jump-to-history', callback
+
   activate: ->
     History = require './history'
     LastEditor = require './last-editor'

+
     @subscriptions = new CompositeDisposable
     @editorSubscriptions = {}
-    @history = new History settings.get('max')
+    @emitter       = new Emitter
+    @history       = new History settings.get('max')

     @rowDeltaToRemember = settings.get('rowDeltaToRemember')
     settings.onDidChange 'rowDeltaToRemember', ({newValue}) =>
@@ -47,6 +55,14 @@
     @subscriptions.add @observeTextEditors()
     @subscriptions.add @observeActivePaneItem()
     @subscriptions.add @observeOnWillDestroyPaneItem()
+    @subscriptions.add @onWillJumpToHistory (direction) =>
+      @lock()
+
+    @subscriptions.add @onDidJumpToHistory (direction) =>
+      @unLock()
+      Flasher ?= require './flasher'
+      Flasher.flash() if settings.get('flashOnLand')
+      @history.dump direction

   deactivate: ->
     for editorID, disposables of @editorSubscriptions
@@ -56,6 +72,7 @@
     settings.dispose()
     @history?.destroy()
     @history = null
+    @emitter = null

   # For better handling symbols-view.
   # -------------------------
@@ -113,7 +130,6 @@
     atom.workspace.panelContainers['modal'].onDidAddPanel ({panel, index}) =>
       # [CAUTION] Simply checking constructor name is not enough.
       # e.g. ProjectView is also used in `fuzzy-finder`.
-      # return unless name in ['GoToView', 'GoBackView', 'FileView', 'ProjectView']
       item = panel.getItem()
       name = item.constructor.name
       if name in ['FileView', 'ProjectView'] and typeof(item.openTag) is 'function'
@@ -141,7 +157,7 @@
           # So we need lock state again here after some delay.
           return if @isLocked()

-          # Ignore Rapid movement, dirty workaround for GoToView, GoBackView.
+          # Ignore Rapid movement, dirty workaround for symbols-view's GoToView, GoBackView.
           unless newBufferPosition.isEqual @getPosition(editor)
             debug "Rapid movement ignore!"
             return
@@ -192,6 +208,7 @@
   isLocked: -> @locked

   jump: (direction) ->
+    # console.log "Jump! #{direction}"
     # Settings tab is not TextEditor instance.
     return unless editor = @getEditor()
     return unless entry  = @history["get#{direction}"]()
@@ -201,7 +218,8 @@
       URI   = editor.getURI()
       @history.pushToHead editor, point, URI

-    @lock()
+    @emitter.emit 'will-jump-to-history', direction
+    # @lock()

     {URI, point} = entry
     if editor.getURI() is URI # Same pane.
@@ -210,7 +228,8 @@
       editor.setCursorBufferPosition point, autoscroll: false
       # Adjust cursor position to middle of screen.
       editor.scrollToCursorPosition()
-      @land direction
+      # @land direction
+      @emitter.emit 'did-jump-to-history', direction
     else
       # Jump to different pane
       options =
@@ -219,7 +238,8 @@
       atom.workspace.open(URI, options).done (editor) =>
         editor.scrollToBufferPosition(point, center: true)
         editor.setCursorBufferPosition(point)
-        @land direction
+        # @land direction
+        @emitter.emit 'did-jump-to-history', direction

   land: (direction) ->
     @unLock()


Pass project path of file to the command as cwd (#172)

This adds support to run project-specific PHP version.

--- lib/main.coffee
+++ lib/main.coffee
@@ -1,5 +1,6 @@
 {CompositeDisposable} = require 'atom'
 helpers = require('atom-linter')
+path = require('path')

 module.exports =
   config:
@@ -48,7 +49,9 @@
         parameters.push('--define', 'display_errors=On')
         parameters.push('--define', 'log_errors=Off')
         text = textEditor.getText()
-        return helpers.exec(command, parameters, {stdin: text}).then (output) ->
+        [projectPath] = atom.project.relativizePath(filePath)
+        cwd = if projectPath? then projectPath else path.dirname(filePath)
+        return helpers.exec(command, parameters, {stdin: text, cwd: cwd}).then (output) ->
           regex = /^(?:Parse|Fatal) error:\s+(.+) in .+? on line (\d+)/gm
           messages = []
           while((match = regex.exec(output)) isnt null)


cleanup

cleanup

--- lib/main.coffee
+++ lib/main.coffee
@@ -14,36 +14,25 @@
 isFunction = (object) -> typeof(object) is 'function'
 isInstanceOfTreeView = (target) -> target.constructor.name is 'TreeView'

-getHisotryManager = ->
-  entries = [null, null]
+getHisotryManager = (initialEntry) ->
+  entries = [null, initialEntry]

   save: (entry) ->
+    # Ignore unfocus/re-focus to same pane.
+    #  e.g. focus mini editor and back to original pane..
     return if @getCurrentFocus() is entry
     entries.shift()
     entries.push(entry)
-    # @dump("SAVING")
+
   getLastFocus: -> entries[0]
   getCurrentFocus: -> entries.slice(-1)[0]
-
-  dumpEntry: (entry) ->
-    name = entry?.constructor?.name
-    str = switch
-      when isPane(entry) then path.basename(entry.getActiveEditor()?.getPath?())
-      when isPanel(entry) then 'Panel'
-    "#{name}: #{str}"
-
-  dump: (msg) ->
-    unless entries.length is 2
-      throw "WANNA DIE"
-    console.log "-#{msg}--:", entries.map (e) => @dumpEntry(e)

 module.exports =
   history: null

   activate: ->
-    @history = getHisotryManager()
+    @history = getHisotryManager(atom.workspace.getActivePane())
     @subscriptions = new CompositeDisposable
-    @history.save(atom.workspace.getActivePane())
     @input = new (require './input')

     @subscribe atom.commands.add 'atom-workspace',


change flash folder path #2

change flash folder path #2

--- lib/main.coffee
+++ lib/main.coffee
@@ -23,7 +23,7 @@
 console.log packageFile.flash.version

 # initiate Flash
-app.commandLine.appendSwitch 'ppapi-flash-path', "#{__dirname}/#{packageFile.flash.path}"
+app.commandLine.appendSwitch 'ppapi-flash-path', "#{__dirname}/../#{packageFile.flash.path}"
 app.commandLine.appendSwitch 'ppapi-flash-version', packageFile.flash.version

 # Quit when all windows are closed.


Add deserializers on package activation

Add deserializers on package activation

This is needed because deserializers are reset in between test runs.

--- lib/main.coffee
+++ lib/main.coffee
@@ -11,12 +11,6 @@
 isMarkdownPreviewView = (object) ->
   MarkdownPreviewView ?= require './markdown-preview-view'
   object instanceof MarkdownPreviewView
-
-atom.deserializers.add
-  name: 'MarkdownPreviewView'
-  deserialize: (state) ->
-    if state.editorId or fs.isFileSync(state.filePath)
-      createMarkdownPreviewView(state)

 module.exports =
   config:
@@ -44,6 +38,12 @@
       default: false

   activate: ->
+    atom.deserializers.add
+      name: 'MarkdownPreviewView'
+      deserialize: (state) ->
+        if state.editorId or fs.isFileSync(state.filePath)
+          createMarkdownPreviewView(state)
+
     atom.commands.add 'atom-workspace',
       'markdown-preview:toggle': =>
         @toggle()


Convert configDefaults into config schema

Default jade to removeWhitespace: false

--- lib/main.coffee
+++ lib/main.coffee
@@ -1,11 +1,22 @@
 Whitespace = require './whitespace'

 module.exports =
-  configDefaults:
-    removeTrailingWhitespace: true
-    ignoreWhitespaceOnCurrentLine: true
-    ignoreWhitespaceOnlyLines: false
-    ensureSingleTrailingNewline: true
+  config:
+    removeTrailingWhitespace:
+      type: 'boolean'
+      default: true
+      scopes:
+        '.source.jade':
+          default: false
+    ignoreWhitespaceOnCurrentLine:
+      type: 'boolean'
+      default: true
+    ignoreWhitespaceOnlyLines:
+      type: 'boolean'
+      default: false
+    ensureSingleTrailingNewline:
+      type: 'boolean'
+      default: true

   activate: ->
     @whitespace = new Whitespace()


Remove tag type

Remove tag type

--- lib/tags.coffee
+++ lib/tags.coffee
@@ -74,11 +74,11 @@
         group: group

 Meteor.methods
-  tagNew: (group, key, type) ->
+  tagNew: (group, key) ->
     check Meteor.userId(), String  ## should be done by 'canPost'
     check group, String
     check key, String
-    check type, "boolean"  ## only type supported for now
+    #check type, "boolean"  ## only type supported for now
     unless canPost group, null
       throw new Meteor.Error 'tagNew.unauthorized',
         "Insufficient permissions to tag message in group '#{group}'"
@@ -101,7 +101,7 @@
       Tags.insert
         group: group
         key: key
-        type: type
+        #type: type
         deleted: false
         creator: Meteor.user().username
         created: new Date


Inputs class extends eventable to send global keyboard and mouse events

--- layout.coffee
+++ layout.coffee
@@ -825,15 +825,15 @@

   mouseEvents = ['click', 'mouseover', 'mouseout', 'mousedown', 'mouseup']
   keyboardEvents = ['focus', 'blur', 'select', 'keyup', 'keydown']
-  # singleton that listens for mouse position and holds the most recent left and top coordinates
-  class InputEvents extends Module
+  # singleton that listens for keyboard and mouse events. Holds data about the most recent left and top mouse coordinates
+  class Inputs extends Eventable
     constructor: ->
       @docSelector = $(document)
       @docSelector.on(mouseEvents.join(' '), @handleMouse)
       @docSelector.on(keyboardEvents.join(' '), @handleKeyboard)
     sender: ->
-      trigger("mousemove", left, top)
-    handleKeyboard: (event) ->
+      @trigger("mousemove", left, top)
+    handleKeyboard: (event) =>
       view = event.target.$view
       type = event.type
       if view
@@ -844,16 +844,16 @@
           if (view.text != value)
             view.text = value
             view.sendEvent('text', value);
-      # else
-      #   trigger(type)
+
+      @sendEvent(type, view)
       # console.log 'handleKeyboard', type, view, event
-    handleMouse: (event) ->
+    handleMouse: (event) =>
       view = event.target.$view
       type = event.type
       if view
         view.sendEvent(type, view)
-      # else
-      #   trigger(type)
+
+      @sendEvent(type, view)
       # console.log 'event', event.type, event.target.$view
       if @started
         requestTick 0, sender
@@ -868,7 +868,7 @@
       @started = false
       @docSelector.off("mousemove", @handler).one("mouseover", @start)

-  mouse = new InputEvents()
+  new Inputs()

   exports = {
     view: View,


lineplots now work in webkit, removed bad old codepath

--- src/coffee/bokeh_views.coffee
+++ src/coffee/bokeh_views.coffee
@@ -613,8 +613,6 @@
     @plot_view.ctx.fillStyle = 'blue'
     @plot_view.ctx.strokeStyle = @mget('color')
     @plot_view.ctx.beginPath()
-    if navigator.userAgent.indexOf("WebKit") != -1
-      @ctx.scale(0.5, 0.5)

     @plot_view.ctx.moveTo(@screenx[0], @screeny[0])
     for idx in [1..@screenx.length]


Have student assessment goals able to be 'uncompleted'

The 'goalStates' in LevelSessions generally does not update goals
that go from 'success' to something else, so that achievements are not
lost in this scenario. But the teacher assessments view relies on this
property to show the current student state. Have the property able
to be unset for students, since they aren't using achievements anyway.

--- app/lib/LevelBus.coffee
+++ app/lib/LevelBus.coffee
@@ -200,7 +200,10 @@

     changed = false
     for goalKey, goalState of newGoalStates
-      continue if oldGoalStates[goalKey]?.status is 'success' and goalState.status isnt 'success' # don't undo success, this property is for keying off achievements
+      unless me.isStudent()
+        # don't undo success, this property is for keying off achievements for home users
+        # do undo for students, though, so this property can be used in teacher assessment tabs
+        continue if oldGoalStates[goalKey]?.status is 'success' and goalState.status isnt 'success'
       continue if utils.kindaEqual state.goalStates?[goalKey], goalState # Only save when goals really change
       changed = true
       oldGoalStates[goalKey] = _.cloneDeep newGoalStates[goalKey]


Fix #151 submitting form by CloseIcon

Fix #151 submitting form by CloseIcon

--- wzk/ui/CloseIcon.coffee
+++ wzk/ui/CloseIcon.coffee
@@ -27,7 +27,8 @@
     @override
   ###
   afterRendering: ->
-    goog.events.listen @getElement(), goog.events.EventType.CLICK, =>
+    goog.events.listen @getElement(), goog.events.EventType.CLICK, (e) =>
+      e.preventDefault()
       @dispatchEvent goog.ui.Component.EventType.ACTION
     undefined # because of CoffeeScript


Update pt-PT.coffee

Update pt-PT.coffee

--- app/locale/pt-PT.coffee
+++ app/locale/pt-PT.coffee
@@ -425,13 +425,13 @@
     tip_js_beginning: "O JavaScript é apenas o começo."
     tip_autocast_setting: "Ajuste as definições de auto-lançamento ao clicar na engrenagem no botão lançar."
     think_solution: "Pense na solução, não no problema."
-    tip_theory_practice: "Teoricamente, não há diferença entre a teoria e a práitca. Mas na prática, há. - Yogi Berra"
+    tip_theory_practice: "Teoricamente, não há diferença entre a teoria e a prática. Mas na prática, há. - Yogi Berra"
     tip_error_free: "Há duas formas de escrever programas sem erros; apenas a terceira funciona. - Alan Perlis"
     tip_debugging_program: "Se depurar é o processo de remover erros, então programar deve ser o processo de os adicionar. - Edsger W. Dijkstra"
     tip_forums: "Vá aos fóruns e diga-nos o que pensa!"
     tip_baby_coders: "No futuro, até os bebés serão Arcomagos."
     tip_morale_improves: "O carregamento irá continuar até que a moral melhore."
-    tip_all_species: "Acreditamos em oportunidades iguais para todas as espécies aprenderem a programar."
+    tip_all_species: "Acreditamos em oportunidades iguais para todas as espécies, em relação a aprenderem a programar."
     tip_reticulating: "A reticular espinhas."
     tip_harry: "Você é um Feitiçeiro, "
 #    tip_great_responsibility: "With great coding skill comes great debug responsibility."
@@ -442,7 +442,7 @@
     tip_patience: "Paciência tu deves ter, jovem Padawan. - Yoda"
     tip_documented_bug: "Um erro documentado não é um erro; é uma funcionalidade."
     tip_impossible: "Parece sempre impossível até ser feito. - Nelson Mandela"
-    tip_talk_is_cheap: "Falar é fácil. Mostra-me o código. - Linus Torvalds"
+    tip_talk_is_cheap: "Falar é fácil. Mostre-me o código. - Linus Torvalds"
     tip_first_language: "A coisa mais desastrosa que pode aprender é a sua primeira linguagem de programação. - Alan Kay"
     tip_hardware_problem: "P: Quantos programadores são necessários para mudar uma lâmpada?  R: Nenhum, é um problema de hardware."
     time_current: "Agora:"


Remove annoying console.info

Remove annoying console.info

--- core/app/backbone/models/topic.coffee
+++ core/app/backbone/models/topic.coffee
@@ -16,7 +16,6 @@
     else
       ch = @newChannelForUser(user)
       user.channels.add(ch)
-      console.info "saving channel #{ch.get 'title'} to #{ch.url()}"
       ch.save {},
         success: (m,r)-> options.success?(m,r)
         error:   (m,r)->


mp4: Emit EOF event

mp4: Emit EOF event

--- mp4.coffee
+++ mp4.coffee
@@ -21,6 +21,7 @@
   constructor: (filename) ->
     if filename?
       @open filename
+    @isStopped = false

   open: (filename) ->
     if DEBUG
@@ -32,7 +33,11 @@
       console.log "took #{(diffTime[0] * 1e9 + diffTime[1]) / 1000000} ms to read"

   close: ->
+    if not @isStopped
+      @stop()
     @bits = null
+    @emit 'close'
+    return

   parse: ->
     if DEBUG
@@ -87,6 +92,9 @@
     esdsBox = @audioTrakBox.find 'esds'
     return esdsBox.decoderConfigDescriptor.decoderSpecificInfo.specificInfo

+  stop: ->
+    @isStopped = true
+
   play: ->
     @currentTime = 0
     @consumedAudioSamples = 0
@@ -129,6 +137,10 @@
       # fill buffer
       if @readNextAudioChunk()
         @queueBufferedSamples()
+      else
+        if not @isAudioEOF
+          @isAudioEOF = true
+          @checkEOF()
     else
       @queueBufferedSamples()
     return
@@ -138,6 +150,10 @@
     if timeDiff < READ_BUFFER_TIME
       if @readNextVideoChunk()
         @queueBufferedSamples()
+      else
+        if not @isVideoEOF
+          @isVideoEOF = true
+          @checkEOF()
     else
       @queueBufferedSamples()
     return
@@ -185,14 +201,21 @@
       @queueBufferedSamples()

   queueBufferedSamples: ->
+    if @isStopped
+      return
     @queueBufferedAudioSamples()
     @queueBufferedVideoSamples()
+
+  checkEOF: ->
+    if @isAudioEOF and @isVideoEOF
+      @close()

   bufferAudio: (callback) ->
     # TODO: Use async
     while @bufferedAudioTime < @currentPlayTime + READ_BUFFER_TIME
       if not @readNextAudioChunk()
         @isAudioEOF = true
+        @checkEOF()
     callback?()

   bufferVideo: (callback) ->
@@ -200,6 +223,7 @@
     while @bufferedVideoTime < @currentPlayTime + READ_BUFFER_TIME
       if not @readNextVideoChunk()
         @isVideoEOF = true
+        @checkEOF()
     callback?()

   getNumVideoSamples: ->


Refactor mac-tooltip Updated tooltip attributes to match the naming convention. Updated to use extendAttributes.

--- src/directives/tooltip.coffee
+++ src/directives/tooltip.coffee
@@ -5,84 +5,91 @@
 @description
 Tooltip directive

-@param {String}  mac-tooltip Text to show in tooltip
-@param {String}  direction   Direction of tooltip
-@param {String}  trigger     How tooltip is triggered
-@param {Boolean} inside      Should the tooltip be appended inside element
+@param {String}  mac-tooltip           Text to show in tooltip
+@param {String}  mac-tooltip-direction Direction of tooltip (default 'top')
+@param {String}  mac-tooltip-trigger   How tooltip is triggered (default 'hover')
+@param {Boolean} mac-tooltip-inside    Should the tooltip be appended inside element (default false)
 ###

-angular.module("Mac").directive "macTooltip", ->
-  restrict: "A"
+angular.module("Mac").directive "macTooltip", [
+  "util"
+  (util) ->
+    restrict: "A"

-  link: (scope, element, attrs) ->
-    tooltip = null
-    text    = ""
-    enabled = false
+    link: (scope, element, attrs) ->
+      tooltip = null
+      text    = ""
+      enabled = false

-    showTip = (event) ->
-      inside    = attrs.inside?
-      direction = attrs.direction or "top"
-      tip       = if inside then element else $(document.body)
-      tooltip   = $("""

#{text}

""")
-      tip.append tooltip
+      defaults =
+        direction: "top"
+        trigger:   "hover"
+        inside:    false

-      # Only get element offset when not adding tooltip within the element.
-      offset = if inside then { top: 0, left: 0 } else element.offset()
+      opts = util.extendAttributes "macTooltip", defaults, attrs

-      # Get element height and width.
-      elementSize =
-        width:  element.outerWidth()
-        height: element.outerHeight()
+      showTip = (event) ->
+        tip       = if opts.inside then element else $(document.body)
+        tooltip   = $("""

#{text}

""")
+        tip.append tooltip

-      # Get tooltip width and height.
-      tooltipSize =
-        width:  tooltip.outerWidth()
-        height: tooltip.outerHeight()
+        # Only get element offset when not adding tooltip within the element.
+        offset = if opts.inside then { top: 0, left: 0 } else element.offset()

-      # Adjust offset based on direction.
-      switch direction
-        when "bottom", "top" then offset.left += elementSize.width / 2.0 - tooltipSize.width / 2.0
-        when "left", "right" then offset.top  += elementSize.height / 2.0 - tooltipSize.height / 2.0
+        # Get element height and width.
+        elementSize =
+          width:  element.outerWidth()
+          height: element.outerHeight()

-      switch direction
-        when "bottom" then offset.top  += elementSize.height
-        when "top"    then offset.top  -= tooltipSize.height
-        when "left"   then offset.left -= tooltipSize.width
-        when "right"  then offset.left += elementSize.width
+        # Get tooltip width and height.
+        tooltipSize =
+          width:  tooltip.outerWidth()
+          height: tooltip.outerHeight()

-      # Constrain the position.
-      offset.top  = Math.max 0, offset.top
-      offset.left = Math.max 0, offset.left
+        # Adjust offset based on direction.
+        switch opts.direction
+          when "bottom", "top" then offset.left += elementSize.width / 2.0 - tooltipSize.width / 2.0
+          when "left", "right" then offset.top  += elementSize.height / 2.0 - tooltipSize.height / 2.0

-      # Set the offset.
-      tooltip.css(offset).addClass "visible"
+        switch opts.direction
+          when "bottom" then offset.top  += elementSize.height
+          when "top"    then offset.top  -= tooltipSize.height
+          when "left"   then offset.left -= tooltipSize.width
+          when "right"  then offset.left += elementSize.width

-    removeTip = (event) ->
-      tooltip.removeClass "visible"
-      setTimeout ->
-        tooltip.remove()
-      , 100
+        # Constrain the position.
+        offset.top  = Math.max 0, offset.top
+        offset.left = Math.max 0, offset.left

-    toggle = (event) ->
-      if tooltip? then removeTip(event) else showTip(event)
+        # Set the offset.
+        tooltip.css(offset).addClass "visible"

-    attrs.$observe "macTooltip", (value) ->
-      if value? and value
-        text = value
+      removeTip = (event) ->
+        tooltip.removeClass "visible"
+        setTimeout ->
+          tooltip.remove()
+        , 100

-        unless enabled
-          trigger = attrs.trigger or "hover"
-          unless trigger in ["hover", "click"]
-            return console.error "Invalid trigger"
+      toggle = (event) ->
+        if tooltip? then removeTip(event) else showTip(event)

-          switch trigger
-            when "click"
-              element.on "click", toggle
-            when "hover"
-              element.on "mouseenter", showTip
-              element.on "mouseleave click", removeTip
+      attrs.$observe "macTooltip", (value) ->
+        if value? and value
+          text = value

-          enabled = true
+          unless enabled
+            unless opts.trigger in ["hover", "click"]
+              return console.error "Invalid trigger"

-    scope.$on "$destroy", ->
-      removeTip() if tooltip?
+            switch opts.trigger
+              when "click"
+                element.on "click", toggle
+              when "hover"
+                element.on "mouseenter", showTip
+                element.on "mouseleave click", removeTip
+
+            enabled = true
+
+      scope.$on "$destroy", ->
+        removeTip() if tooltip?
+]


fix(macTooltip): Fixed tooltip possibly watching an undefined variable

--- src/directives/tooltip.coffee
+++ src/directives/tooltip.coffee
@@ -95,8 +95,9 @@

             enabled = true

-      scope.$watch attrs.macTooltipDisabled, (value) ->
-        disabled = value
+      if attrs.macTooltipDisabled?
+        scope.$watch attrs.macTooltipDisabled, (value) ->
+          disabled = value

       scope.$on "$destroy", ->
         removeTip() if tooltip?


Bugfix initial tutorial state

Bugfix initial tutorial state

--- app/pages/classify.cjsx
+++ app/pages/classify.cjsx
@@ -30,6 +30,7 @@
     workflow: workflowStore.data
     subject: subjectStore.data
     showingSummary: classifierStore.data.showingSummary
+    shownTutorial: classifierStore.data.shownTutorial
     tutorialIsOpen: classifierStore.data.tutorialIsOpen

   componentDidMount: ->


Remove unused contextTypes

Remove unused contextTypes

--- app/pages/projects.cjsx
+++ app/pages/projects.cjsx
@@ -16,9 +16,6 @@
   title: 'Projects'

   mixins: [TitleMixin]
-
-  contextTypes:
-    location: React.PropTypes.object

   emptyPromise:
     then: ->


fixed download all

fixed download all

--- src/driver/druidDriver.coffee
+++ src/driver/druidDriver.coffee
@@ -792,7 +792,7 @@
         sort: {
           compare: 'natural'
           prop: condensedCommand.split.name
-          direction: 'descending'
+          direction: condensedCommand.combine.sort.direction
         }
         limit: allDataChunks
       })


New style bindings syntax support added

New style bindings syntax support added

--- cake/styles.coffee
+++ cake/styles.coffee
@@ -4,7 +4,7 @@

 BINDING_RE = ///([a-z0-9_]+)\.(left|top|width|height)///ig
 RESERVED_IDS = ['window']
-HASH_PATTERN = '__{hash}__'
+HASH_PATTERN = 'u{hash}_'
 BINDED_PROPS = ['left', 'top', 'width', 'height']

 result =
@@ -27,16 +27,24 @@
 	if opts.id
 		result.ids[opts.id] = index
 		obj.variables = {}
-		obj.variables.id = opts.id + HASH_PATTERN
+		obj.variables.id = HASH_PATTERN + opts.id
 		opts.id = ''

 	# binded properties
 	for prop in BINDED_PROPS when typeof opts[prop] is 'string'
 		obj.variables ?= {}
-		obj.variables[prop] = opts[prop].replace BINDING_RE, (str, id, prop) ->
-			return str if utils.has RESERVED_IDS, id
-			"#{id}#{HASH_PATTERN}.#{prop}"
-		opts[prop] = ''
+		args = []
+		idsIndexes = {}
+		expr = opts[prop].replace BINDING_RE, (str, id, prop) ->
+			unless utils.has RESERVED_IDS, id
+				id = HASH_PATTERN + id
+			unless idsIndexes[id]
+				idsIndexes[id] = args.length
+				args.push id
+			"$#{idsIndexes[id]}.#{prop}"
+
+		opts[prop] = [expr, []]
+		obj.variables[prop] = args

 	obj


added a verified email toast

added a verified email toast

--- packages/rocketchat-ui/lib/accounts.coffee
+++ packages/rocketchat-ui/lib/accounts.coffee
@@ -1,6 +1,5 @@
 Accounts.onEmailVerificationLink (token, done) ->
 	Accounts.verifyEmail token, (error) ->
 		if not error?
-			alert(t('Email_verified'))
-
-		done()
+			toastr.success t('Email_verified')
+		done()


fix artists on automated gene pages

fix artists on automated gene pages

--- apps/gene/client.coffee
+++ apps/gene/client.coffee
@@ -67,7 +67,8 @@
         @renderArtistFillwidth()

   fetchRelatedArtists: ->
-    @model.fetchArtists 'related', success: (artists) =>
+    data = { exclude_artists_without_artworks: true }
+    @model.fetchArtists 'related', data: data, success: (artists) =>
       @relatedArtists = artists
       @trigger 'relatedArtistsFetched'


ROM importer speed improvements

ROM importer speed improvements

--- plugins/coffeekeep.importer.rom/rom.coffee
+++ plugins/coffeekeep.importer.rom/rom.coffee
@@ -24,7 +24,7 @@
     data

   getLine: ->
-    [@line, @lines...] = @lines
+    @line = @lines[@lineIndex]
     @lineIndex += 1
     @log?.silly "Parsing line %d/%d: %j", @lineIndex, @linesTotal, @line
     if not @line?
@@ -47,8 +47,7 @@
     @lineIndex = 0
     @lines = data.split '\n'
     @linesTotal = @lines.length
-    #while @lines.length > 0
-    async.whilst (=>@lines.length),
+    async.whilst (=>@lineIndex < @linesTotal),
       (cb) =>
         @getLine()
         if not @line or @line.indexOf("#") != 0


remove trailing slash for bank route

remove trailing slash for bank route

--- config/routes.coffee
+++ config/routes.coffee
@@ -22,7 +22,7 @@
     map.delete '/data/index/:id', 'index#remove'

     # Browser connectors
-    map.post '/connectors/bank/:name', 'connector#bank'
+    map.post '/connectors/bank/:name/', 'connector#bank'

     # File management
     map.post '/data/:id/attachments/', 'attachment#addAttachment'


re-applying Karthik's changes which git mishandled

--- client/tawk.coffee
+++ client/tawk.coffee
@@ -6,10 +6,13 @@

 window.statebus_ready or= []
 window.statebus_ready.push(->
-  window.tawkbus = statebus()
-  tawkbus.sockjs_client("/*", "https://tawk.space")
-  window.tawk = tawkbus.sb
+  # HACK: Remove
+  window.tawkbus = window.bus
+  window.tawk = window.sb
+
   tawk.janus_initialized = false
+  tawk.id = random_string(16)
+  tawk.space = null # Will be filled in dom.TAWK

   unsavable = (obj) ->
     throw new Error("Cannot save #{obj.key}")
@@ -116,10 +119,14 @@
 ###############################################################################

 dom.TAWK = ->
-  space = if @props.space? then @props.space else ''
+  tawk.space = if @props.space? then @props.space else ''
   name = if @props.name? then @props.name else 'Anonymous ' + random_numbers(4)
   video = if @props.video? then @props.video else true
   audio = if @props.audio? then @props.audio else true
+
+  if not tawk.janus_initialized
+    initialize_janus(tawk.id, tawk.space)
+    tawk.janus_initialized = true

   # Have to make sure we get all connections to choose
   # whether to join the first group
@@ -129,18 +136,14 @@

   me = tawk['/connection']
   if not me.id
-    me.id = random_string(16)
+    me.id = tawk.id
     me.name = name
     me.group = tawk.gids[0] or random_string(16)
     me.timeEntered = Date.now()
     me.active = true
-    me.space = space
+    me.space = tawk.space
     me.video = video
     me.audio = audio
-
-  if not tawk.janus_initialized
-    initialize_janus(me.id, me.space)
-    tawk.janus_initialized = true

   DIV
     id: 'tawk'
@@ -587,7 +590,7 @@
                 request: "start"
                 room: 1234

-initialize_janus = (my_id, my_space) ->
+initialize_janus = () ->
   Janus.init
     callback: ->
       if not Janus.isWebrtcSupported()
@@ -601,7 +604,7 @@
           janus.attach
             plugin: "janus.plugin.videoroom"
             error: console.error
-            onlocalstream: (stream) -> recieved_stream(stream, my_id)
+            onlocalstream: (stream) -> recieved_stream(stream, tawk.id)
             success: (ph) ->
               # Join plugin as a publisher (able to both send and receive streams)
               plugin_handle = ph
@@ -611,14 +614,14 @@
                   room: 1234
                   ptype: "publisher"
                   display: JSON.stringify
-                    id: my_id
-                    space: my_space
+                    id: tawk.id
+                    space: tawk.space
             onmessage: (msg, jsep) ->
               # Janus is informing us of publishers we do not know about
               publishers = msg["publishers"] or []
               for feed in publishers
                 {id, space} = JSON.parse feed.display
-                if space == my_space
+                if space == tawk.space
                   new_remote_feed janus, feed

               # The plugin_handle.send call to join as a publisher succeeded.


add whitespace

add whitespace

--- matrix.coffee
+++ matrix.coffee
@@ -68,6 +68,7 @@
       for j in [0...@height] by 1
         T.array[j + i * @height] = @array[i + j * @width]
     T
+
   mult: (B, T = newEmpty(B.width, @height)) ->
     if @width != B.height or T.height != @height or T.width != B.width
       failUnmatchingDimensions()


Update uk.coffee (#5572)

Update uk.coffee (#5572)

--- app/locale/uk.coco.coffee
+++ app/locale/uk.coco.coffee
@@ -2416,7 +2416,7 @@
     sound_on: "Увімкнути звук"
     back_to_map: "Повернутись до карти"
     level_options: "Налаштування рівня"
-    restart_level: "Перезавантажити рівень"
+    restart_level: "Перезапустити рівень"

 #  impact:
 #    hero_heading: "Building A World-Class Computer Science Program"
@@ -2544,7 +2544,7 @@
     continue_statements: "Оператори продовження"
     dom_events: "Події DOM"
     dynamic_styling: "Динамічні стилі"
-#    events: "Events"
+    events: "Події"
     event_concurrency: "Конкурентність подій"
     event_data: "Дані подій"
     event_handlers: "Обробники подій"


update zh-HANS.coffee

update zh-HANS.coffee

two places

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -122,7 +122,7 @@
     edit_settings: "编辑设置"
     profile_for_prefix: "关于TA的基本资料："
     profile_for_suffix: ""
-    profile: "Profile"
+    profile: "基本资料"
     user_not_found: "没有找到用户。检查URL？"
     gravatar_not_found_mine: "我们找不到TA的基本资料："
     gravatar_not_found_email_suffix: "."
@@ -153,7 +153,7 @@
     victory_title_suffix: " 完成"
     victory_sign_up: "保存进度"
     victory_sign_up_poke: "想保存你的代码？创建一个免费账户吧！"
-    victory_rate_the_level: "Rate the level: "
+    victory_rate_the_level: "评估关卡: "
     victory_play_next_level: "下一关"
     victory_go_home: "返回主页"
     victory_review: "给我们反馈！"


only instantiate scraper if it matches the uri

--- scripts/media.coffee
+++ scripts/media.coffee
@@ -9,25 +9,23 @@
     uri       = new Uri link.href

     for scraper in @scrapers
-      break if new scraper(container, link, uri).scrape()
+      if scraper.doesUriMatch uri
+        break if new scraper(container, link, uri).scrape()

 class Media.Scraper
   THROBBER_URI  = 'images/throbber.gif'
   LOADING_TITLE = 'loading\u2026'
+
+  @doesUriMatch: (uri) -> false

   constructor: (container, link, @uri) ->
     @container = $(container)
     @source    = $(link)

   scrape: ->
-    if @doesUriMatch @uri
-      @container.append @createDefaultThumbnail()
-      @loadThumbnail()
-      true
-    else
-      false
-
-  doesUriMatch: (uri) -> false
+    @container.append @createDefaultThumbnail()
+    @loadThumbnail()
+    true

   loadThumbnail: ->
     @loadThumbnailTitle()
@@ -65,7 +63,7 @@
 class YouTubeScraper extends Media.Scraper
   Media.register this

-  doesUriMatch: (uri) ->
+  @doesUriMatch: (uri) ->
     if uri.matchHost 'youtube.com'
       uri.query.v? or
       uri.matchPath '/v/'
@@ -95,7 +93,7 @@
 class ImgurScraper extends Media.Scraper
   Media.register this

-  doesUriMatch: (uri) ->
+  @doesUriMatch: (uri) ->
     if uri.host is 'imgur.com' or uri.host is 'i.imgur.com'
       true
     else
@@ -115,7 +113,7 @@

   PATTERN = /// \. (?: bmp | gif | jp2 | jpe?g | png | tiff? ) $///i

-  doesUriMatch: (uri) ->
+  @doesUriMatch: (uri) ->
     uri.path.match(PATTERN)?

   loadThumbnailImage: ->


remove defaultContent var

remove defaultContent var

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -169,10 +169,9 @@
 		@articleCreateURL = options.articleCreateURL
 		@articleSaveURL = options.articleSaveURL
 		@articleDeleteURL = options.articleDeleteURL
-		@defaultContent = '

'

 	init: ()->
-		$('#editor').html @defaultContent
+		$('#editor').html '

'
 		@update()
 		@checkTitleEmpty()
 		@checkEmpty()


Client: Activity: Added onResize Event handler to chatpanewrapppermixin to set champagne height property when chatInputWidget height update with multi-line text writing

--- client/activity/lib/components/chatpane/chatpanewrappermixin.coffee
+++ client/activity/lib/components/chatpane/chatpanewrappermixin.coffee
@@ -35,3 +35,23 @@

     input.setCommand '/invite @'

+
+  onResize: ->
+
+    ChatPaneBody              = document.querySelector '.ChatPane-body'
+    ChatPaneFooter            = document.querySelector '.PublicChatPane-footer'
+    scrollContainer           = ChatPaneBody.querySelector '.Scrollable'
+    footerHeight              = ChatPaneFooter.offsetHeight
+    ChatPaneBodyHeight        = "calc(100% \- #{footerHeight}px)"
+    ChatPaneBody.style.height = ChatPaneBodyHeight
+
+
+    # we can not catch 0px to scroll to bottom. If scroll near about 20px or less
+    # and when new message received we make scroll to bottom so user can see new messages.
+    # If not probably user is reading old messages and we don't make scroll to bottom.
+
+    { scrollTop, offsetHeight, scrollHeight } = scrollContainer
+
+    if scrollHeight - (scrollTop + offsetHeight) < 20
+      scrollContainer.scrollTop = scrollHeight
+


stop reloading activities over and over

stop reloading activities over and over

--- client/app/Applications/Activity.kdapplication/AppController.coffee
+++ client/app/Applications/Activity.kdapplication/AppController.coffee
@@ -312,10 +312,13 @@

   continueLoadingTeasers:->

+    lastTimeStamp = (new Date @lastFrom).getTime()
+    if @continueLoadingTeasersLastTimeStamp is lastTimeStamp
+      return
+
     # HACK: this gets called multiple times if there's no wait
     KD.utils.wait 1000, =>
       return  unless @lastFrom?
-
       lastTimeStamp = (new Date @lastFrom).getTime()
       @populateActivity {slug : "before/#{lastTimeStamp}", to: lastTimeStamp}


fix chrome app unload listener

fix chrome app unload listener

--- client/app/Applications/WebTerm.kdapplication/webtermappview.coffee
+++ client/app/Applications/WebTerm.kdapplication/webtermappview.coffee
@@ -92,7 +92,6 @@
   chromeAppMode: ->
     windowController = KD.getSingleton("windowController")
     mainController   = KD.getSingleton("mainController")
-    windowController.clearUnloadListeners "window"

     # talking with chrome app
     if window.parent?.postMessage


collaborative-workspace: host pinging and participant time sharing

--- client/landing/CollaborativeWorkspace/collaborativeworkspace.coffee
+++ client/landing/CollaborativeWorkspace/collaborativeworkspace.coffee
@@ -41,31 +41,33 @@
           @showNotActiveView()
           return false

-      isOldSession = keys = snapshot.val()?.keys
-
-      if isOldSession
-        @sessionData  = keys
-        @createPanel()
+      cb = =>
+        isOldSession = keys = snapshot.val()?.keys
+        if isOldSession
+          @sessionData = keys
+          @createPanel()
+        else
+          @createPanel()
+          @workspaceRef.set "keys": @sessionData
+
+        @setPresenceHandlers()
+
+        record = if isOldSession then "$0 joined the session" else "$0 started the session"
+        @addToHistory { message: record, by: KD.nick() }
+        @watchRef.child(@nickname).set "everybody"
+
+        @loader.destroy()
+        @chatView?.show()
+
+        @emit "WorkspaceSyncedWithRemote"
+        @emit "SomeoneJoinedToSession", KD.nick() if isOldSession
+
+      if @amIHost()
+      then cb()
       else
-        @createPanel()
-        @workspaceRef.set "keys": @sessionData
-
-      @userRef = @usersRef.child @nickname
-      @userRef.set "online"
-      @userRef.onDisconnect().set "offline"
-      record = if isOldSession then "$0 joined the session" else "$0 started the session"
-      @addToHistory { message: record, by: KD.nick() }
-      @watchRef.child(@nickname).set "everybody"
-
-      if @amIHost()
-        @workspaceRef.onDisconnect().remove()
-        @userRef.onDisconnect().remove()
-
-      @loader.destroy()
-      @chatView?.show()
-
-      @emit "WorkspaceSyncedWithRemote"
-      @emit "SomeoneJoinedToSession", KD.nick() if isOldSession
+        @pingHost (status) =>
+          return  unless status is "online"
+          cb()

     @usersRef.on "child_added", (snapshot) =>
       @fetchUsers()
@@ -143,24 +145,32 @@
     [sessionOwner] = @sessionKey.split "_"
     return sessionOwner is @nickname

-  showNotActiveView: ->
-    notValid = new KDView
-      cssClass : "not-valid"
-      partial  : "This session is not valid or no longer available."
-
-    notValid.addSubView new KDView
-      cssClass : "description"
-      partial  : "This usually means, the person who is hosting this session is disconnected or closed the session."
-
-    notValid.addSubView new KDButtonView
-      cssClass : "cupid-green"
-      title    : "Start New Session"
-      callback : =>
-        @startNewSession()
-
-    @container.addSubView notValid
-    @sessionNotActive = yes
-    @loader.hide()
+  showNotActiveView: do ->
+    notValid = null
+
+    ->
+      return  if notValid
+
+      notValid = new KDView
+        cssClass : "not-valid"
+        partial  : "This session is not valid or no longer available."
+
+      notValid.addSubView new KDView
+        cssClass : "description"
+        partial  : "This usually means, the person who is hosting this session is disconnected or closed the session."
+
+      notValid.addSubView new KDButtonView
+        cssClass : "cupid-green"
+        title    : "Start New Session"
+        callback : =>
+          @startNewSession()
+          notValid.destroy()
+          notValid = null
+
+      @container.destroySubViews()
+      @container.addSubView notValid
+      @sessionNotActive = yes
+      @loader.hide()

   startNewSession: ->
     @destroy()
@@ -356,3 +366,54 @@
       broadcastItem.hide()
       activePanel.unsetClass "broadcasting"
       @emit "MessageBroadcasted"
+
+  setPresenceHandlers: ->
+    @userRef = @usersRef.child @nickname
+
+    @timestampInterval = @utils.repeat 1000, =>
+      @userRef.child("timestamp").set new Date().getTime(), (err) =>
+        return  unless err
+        @utils.killRepeat interval
+
+    @pingHostInterval = @utils.repeat 2000, @bound "pingHost"  unless @amIHost()
+
+    @once "KDObjectWillBeDestroyed", =>
+      @utils.killRepeat timestampInterval
+      @utils.killRepeat pingHostInterval
+
+  checkHost: (snapshot, callback) ->
+    unless host = snapshot.val()
+      @showNotActiveView()
+      @utils.killRepeat @timestampInterval
+      @utils.killRepeat @pingHostInterval
+      return
+
+    diff = new Date().getTime() - host.timestamp
+
+    if diff <= 5000
+      @hideNotification()
+      @hostStatus = "online"
+    else if diff >= 20000
+      @hideNotification()
+      @showNotActiveView()
+      @hostStatus = "offline"
+    else if diff >= 5000
+      @showNotification "Host is experiencing connectivity issues"
+      @hostStatus = "unknown"
+
+    callback? @hostStatus
+
+  pingHost: (callback) ->
+    @usersRef.child(@getHost()).once "value", (snapshot) =>
+      @checkHost snapshot, callback
+
+  showNotification: (message) ->
+    @hideNotification()
+    @notification = new KDNotificationView
+      title       : message
+      duration    : 20000
+      container   : this
+      overlay     : {}
+
+  hideNotification: ->
+    @notification?.destroy()


webapp: rust .rs files + an icon that looks like the rust logo

--- src/smc-webapp/file-associations.coffee
+++ src/smc-webapp/file-associations.coffee
@@ -63,6 +63,7 @@
     rmd    : 'rmd'
     rnw    : 'rnw'
     rtex   : 'rtex'
+    rs     : 'text/x-rustsrc'
     rst    : 'rst'
     rb     : 'text/x-ruby'
     ru     : 'text/x-ruby'
@@ -107,10 +108,14 @@
             'fab fa-js-square'
         when 'text/typescript-jsx'    # would be nice to have proper TS...
             'fab fa-node-js'
+        when 'text/x-rustsrc'
+            'cog'
         else
             'fa-file-code-o'
+
     if ext in ['r', 'rmd']
         icon = 'cc-icon-r'
+
     file_associations[ext] =
         editor : 'codemirror'
         binary : false


formating the way how solid principles are displayed

--- scripts/solid.coffee
+++ scripts/solid.coffee
@@ -16,9 +16,10 @@
 module.exports = (robot) ->

   robot.respond /define solid/i, (msg)->
-    msg.send "In object-oriented computer programming, the term SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable. The principles are a subset of many principles promoted by Robert C. Martin. Though they apply to any object-oriented design, the SOLID principles can also form a core philosophy for methodologies such as agile development or adaptive software development. The SOLID acronym was introduced by Michael Feathers. They are:
-1) Single responsibility principle
-2) Open/closed principle
-3) Liskov substitution principle
-4) Interface segregation principle
+    msg.send "In object-oriented computer programming, the term SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable. The principles are a subset of many principles promoted by Robert C. Martin. Though they apply to any object-oriented design, the SOLID principles can also form a core philosophy for methodologies such as agile development or adaptive software development. The SOLID acronym was introduced by Michael Feathers.\n
+They are:\n
+1) Single responsibility principle \n
+2) Open/closed principle \n
+3) Liskov substitution principle \n
+4) Interface segregation principle \n
 5) Dependency inversion principle"


If a user logged in with a social button, take them to the ('/') route

--- shared/social.coffee
+++ shared/social.coffee
@@ -23,6 +23,7 @@
     if (Accounts.ui._options.requestOfflineToken[serviceName])
       options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName]
     loginWithService(options, callback)
+    Router.go('/')

 capitalize = (str) ->
   str.charAt(0).toUpperCase() + str.slice(1)


middle click on a tab closes it

middle click on a tab closes it

--- client/ide.coffee
+++ client/ide.coffee
@@ -46,7 +46,7 @@
     $tabs.each -> $t = $ @; if !$('li', $t).length then ['east', 'south'].forEach (d) -> (if $t.is '.ui-layout-' + d then layout.close d); return
          .tabs 'refresh'
     return
-  ($uls = $tabs.find 'ul').each ->
+  $tabs.find('ul').each ->
     $(@).sortable
       cursor: 'move', containment: 'parent', tolerance: 'pointer', axis: 'x', revert: true
       receive: (_, ui) ->
@@ -138,8 +138,9 @@
     .on 'OpenWindow', (ee) -> # "ee" for EditableEntity
       layout.open dir = if ee.debugger then 'south' else 'east'
       w = ee.token
-      $("

").appendTo('.ui-layout-' + dir + ' ul').find('a').text ee.name
-      $tabContent = $("

").appendTo('.ui-layout-' + dir)
+      $("

").appendTo(".ui-layout-#{dir} ul")
+        .find('a').text(ee.name).click (e) -> e.which == 2 && wins[w].EP(); return # middle click
+      $tabContent = $("

").appendTo ".ui-layout-#{dir}"
       wins[w] = new Editor $tabContent,
         id: w, name: ee.name, debugger: ee.debugger, emit: emit
         weakInterrupt: WI


afterRender bugfix

afterRender bugfix

--- components.coffee
+++ components.coffee
@@ -349,12 +349,11 @@

         componentSpec.componentDidMount?.apply @

-        return if @_showingLoader
         @_doAfterRender()

     reactSpec._doAfterRender = ->
-        if not @_valid.get()
+        if @_showingLoader or not @_valid.get()
             # Will try again after render
             return

@@ -368,7 +367,6 @@

     reactSpec.componentDidUpdate = (prevProps, prevState) ->
-        return if @_showingLoader
         @_doAfterRender()

         componentSpec.componentDidUpdate?.call @


:clock630::clock11: Updated in browser at strd6.github.io/editor

--- pixie.cson
+++ pixie.cson
@@ -1,6 +1,6 @@
-version: "0.3.2-pre.1"
+version: "0.3.2-pre.2"
 dependencies:
-  composition: "distri/model:v0.1.2"
+  composition: "distri/model:v0.1.3"
   observable: "distri/observable:v0.3.1"
   sha1: "distri/sha1:v0.1.0"
   util: "distri/util:v0.1.0"


No scope found bug fixed

No scope found bug fixed

--- style.coffee
+++ style.coffee
@@ -325,7 +325,7 @@
 			loop
 				scope = parent?.scope or windowStyle
 				@item = scope.ids[id] or scope.mainItem.$?[id]
-				@item ?= scope.styles(id)
+				@item ?= scope.styles?(id)
 				if @item or ((not parent or not (parent = parent.parent)) and scope is windowStyle)
 					break


Acknowledge

Acknowledge

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -92,7 +92,7 @@
       if err?
         robot.emit 'error', err, msg
         return
-
+
       if incidents.length > 0
         buffer = "Triggered:\n----------\n"
         for junk, incident of incidents.reverse()
@@ -181,7 +181,7 @@
                       ]
                     }
                 }
-
+

                 pagerduty.put "/incidents", data , (err, json) ->
                   if err?
@@ -216,26 +216,24 @@
         robot.emit 'error', err, msg
         return

-      ## Determine the email based on the adapter type (v4.0.0+ of the Slack adapter stores it in `profile.email`)
-      email  = msg.message.user.pagerdutyEmail || msg.message.user.email_address || msg.message.user.profile.email
-
-      filteredIncidents = if force
-                            incidents # don't filter at all
-                          else
-                            incidentsForEmail(incidents, email) # filter by email
-
-      if filteredIncidents.length is 0
-        # nothing assigned to the user, but there were others
-        if incidents.length > 0 and not force
-          msg.send "Nothing assigned to you to acknowledge. Acknowledge someone else's incident with `hubot pager ack <nnn>`"
-        else
-          msg.send "Nothing to acknowledge"
-        return
-
-      incidentNumbers = (incident.incident_number for incident in filteredIncidents)
-
-      # only acknowledge triggered things
-      updateIncidents(msg, incidentNumbers, 'triggered,acknowledged', 'acknowledged')
+      campfireUserToPagerDutyUser msg, msg.message.user, (user) ->
+        filteredIncidents = if force
+                              incidents # don't filter at all
+                            else
+                              incidentsByUserId(incidents, user.id) # filter by id
+
+        if filteredIncidents.length is 0
+          # nothing assigned to the user, but there were others
+          if incidents.length > 0 and not force
+            msg.send "Nothing assigned to you to acknowledge. Acknowledge someone else's incident with `hubot pager ack <nnn>`"
+          else
+            msg.send "Nothing to acknowledge"
+          return
+
+        incidentNumbers = (incident.incident_number for incident in filteredIncidents)
+
+        # only acknowledge triggered things
+        updateIncidents(msg, incidentNumbers, 'triggered,acknowledged', 'acknowledged')

   robot.respond /(?:pager|major)(?: me)? res(?:olve)?(?:d)? (.+)$/i, (msg) ->
     msg.finish()
@@ -891,7 +889,8 @@
             console.log res.statusCode
             console.log body

-  incidentsForEmail = (incidents, userEmail) ->
+  incidentsByUserId = (incidents, userId) ->
     incidents.filter (incident) ->
-      incident.assigned_to.some (assignment) ->
-        assignment.object.email is userEmail
+      assignments = incident.assignments.map (item) -> item.assignee.id
+      assignments.some (assignment) ->
+        assignment is userId


Fix URLs in trollicon script

Fix URLs in trollicon script

--- src/scripts/trollicon.coffee
+++ src/scripts/trollicon.coffee
@@ -7,36 +7,36 @@
 # used resources from : https://github.com/sagargp/trollicons Adium extension

 trollicons = {
-  'gasp'            : 'http : //i.imgur.com/tYmuZ.png',
-  'challenge'       : 'http : //i.imgur.com/jbKmr.png',
-  'lol'             : 'http : //i.imgur.com/WjI3L.png',
-  'no'              : 'http : //i.imgur.com/loC5s.png',
-  'yao'             : 'http : //i.imgur.com/wTAP3.png',
-  'kidding'         : 'http : //i.imgur.com/0uCcv.png',
-  'megusta'         : 'http : //i.imgur.com/QfeUB.png',
-  'isee'            : 'http : //i.imgur.com/M4bcv.png',
-  'fuckyeah'        : 'http : //i.imgur.com/m7mEZ.png',
-  'problem'         : 'http : //i.imgur.com/oLlJm.png',
-  'dissapoint'      : 'http : //i.imgur.com/EwBi7.png',
-  'nothing'         : 'http : //i.imgur.com/Nwos9.png',
-  'pokerface'       : 'http : //i.imgur.com/dDjvG.png',
-  'ok'              : 'http : //i.imgur.com/QRCoI.png',
-  'sadtroll'        : 'http : //i.imgur.com/gYsxd.png',
-  'yuno'            : 'http : //i.imgur.com/sZMnV.png',
-  'true'            : 'http : //i.imgur.com/oealL.png',
-  'freddie'         : 'http : //i.imgur.com/zszUl.png',
-  'forever'         : 'http : //i.imgur.com/5MBi2.png',
-  'jackie'          : 'http : //i.imgur.com/63oaA.png',
-  'fu'              : 'http : //i.imgur.com/YHYTg.png',
-  'rage'            : 'http : //i.imgur.com/itXDM.png',
-  'areyoukiddingme' : 'http : //i.imgur.com/0uCcv.png',
-  'nothingtodo'     : 'http : //i.imgur.com/Nwos9.png',
-  'moonshot'        : 'http : //i.imgur.com/E8Dq3.png',
-  'cerealguy'       : 'http : //i.imgur.com/sD2jS.png',
-  'gtfo'            : 'http : //i.imgur.com/kSxyw.png',
-  'youdontsay'      : 'http : //i.imgur.com/xq9Ix.png',
-  'motherofgod'     : 'http : //i.imgur.com/CxL3b.png',
-  'likeasir'        : 'http : //i.imgur.com/CqBdw.png'
+  'gasp'            : 'http://i.imgur.com/tYmuZ.png',
+  'challenge'       : 'http://i.imgur.com/jbKmr.png',
+  'lol'             : 'http://i.imgur.com/WjI3L.png',
+  'no'              : 'http://i.imgur.com/loC5s.png',
+  'yao'             : 'http://i.imgur.com/wTAP3.png',
+  'kidding'         : 'http://i.imgur.com/0uCcv.png',
+  'megusta'         : 'http://i.imgur.com/QfeUB.png',
+  'isee'            : 'http://i.imgur.com/M4bcv.png',
+  'fuckyeah'        : 'http://i.imgur.com/m7mEZ.png',
+  'problem'         : 'http://i.imgur.com/oLlJm.png',
+  'dissapoint'      : 'http://i.imgur.com/EwBi7.png',
+  'nothing'         : 'http://i.imgur.com/Nwos9.png',
+  'pokerface'       : 'http://i.imgur.com/dDjvG.png',
+  'ok'              : 'http://i.imgur.com/QRCoI.png',
+  'sadtroll'        : 'http://i.imgur.com/gYsxd.png',
+  'yuno'            : 'http://i.imgur.com/sZMnV.png',
+  'true'            : 'http://i.imgur.com/oealL.png',
+  'freddie'         : 'http://i.imgur.com/zszUl.png',
+  'forever'         : 'http://i.imgur.com/5MBi2.png',
+  'jackie'          : 'http://i.imgur.com/63oaA.png',
+  'fu'              : 'http://i.imgur.com/YHYTg.png',
+  'rage'            : 'http://i.imgur.com/itXDM.png',
+  'areyoukiddingme' : 'http://i.imgur.com/0uCcv.png',
+  'nothingtodo'     : 'http://i.imgur.com/Nwos9.png',
+  'moonshot'        : 'http://i.imgur.com/E8Dq3.png',
+  'cerealguy'       : 'http://i.imgur.com/sD2jS.png',
+  'gtfo'            : 'http://i.imgur.com/kSxyw.png',
+  'youdontsay'      : 'http://i.imgur.com/xq9Ix.png',
+  'motherofgod'     : 'http://i.imgur.com/CxL3b.png',
+  'likeasir'        : 'http://i.imgur.com/CqBdw.png'
 }

 module.exports = (robot)->


Show redmine commands in hubot help, not full comment.

--- src/scripts/redmine.coffee
+++ src/scripts/redmine.coffee
@@ -6,7 +6,7 @@
 #                                                     *With optional notes
 # update <issue-id> with "<note>"  - Adds a note to the issue
 # add <hours> hours to <issue-id> ["comments"]  - Adds hours to the issue with the optional comments
-#
+
 # Note: <issue-id> can be formatted in the following ways:
 #       1234, #1234, issue 1234, issue #1234
 #
@@ -326,4 +326,4 @@

     request.on "error", (err) ->
       console.error "Redmine request error: #{err}"
-      callback err, null+      callback err, null


version string

version string

--- spec/protocol.coffee
+++ spec/protocol.coffee
@@ -5,7 +5,7 @@

 #   GCP PROTOCOL SPECIFICATION   *StudioLounge Multiplayer Game*
-#                       v0.2.3    RFC                    (Draft)
+#                         v0.3    RFC                    (Draft)
 #   (in)formal description of digital message formats and rules,
 #   for exchanging of theese messages between computing systems,
 #   defines syntax, semantics, synchronization of communication;


Turn off coffee source-maps, bad debug experience.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -78,7 +78,6 @@
     coffee:
       options:
         bare: yes
-        sourceMap: yes
       src:
         expand: yes
         src: src.coffee


deleted Gruntfile - not used right now

deleted Gruntfile - not used right now

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,31 +0,0 @@
-module.exports = (grunt) ->
-	grunt.initConfig
-		coffeeLint:
-			scripts:
-				files: './tasks/**/*.coffee'
-
-				# Use one tab for indentation.
-				indentation:
-					value: 1
-					level: 'error'
-
-				# No maximum line length.
-				max_line_length:
-					level: 'ignore'
-
-				# Using tabs should not result in an error.
-				no_tabs:
-					level: 'ignore'
-
-		watch:
-			src:
-				files: './tasks/**/*.coffee'
-				tasks: 'coffeeLint'
-
-	grunt.loadTasks './tasks/'
-	grunt.loadNpmTasks 'grunt-contrib-watch'
-
-	grunt.registerTask 'default', [
-		'coffeeLint'
-		'watch'
-	]


Added new Grunt tasks for Bower distribution

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,3 +1,5 @@
+path = require 'path'
+
 module.exports = (grunt) ->
   grunt.initConfig
     pkg: grunt.file.readJSON 'package.json'
@@ -12,6 +14,9 @@
     source:
       specs  : '<%= meta.specs %>/*.spec.coffee'

+    bower:
+      dist   : path.normalize(__dirname + '/..') + '/bower-<%= meta.name %>'
+
     coffee:
       specs  : files: '<%= meta.build %>/<%= meta.name %>.spec.js' : '<%= source.specs %>'

@@ -19,11 +24,16 @@
       build  : src: ['build']
       dist   : src: ['dist/**/*']

+    copy:
+      dist   : src: '<%= meta.dist %>/*', dest: '<%= bower.dist %>/', expand: true, flatten: true
+      bower  : src: 'bower.json', dest: '<%= bower.dist %>/'
+      readme : src: 'README.md', dest: '<%= bower.dist %>/'
+      license: src: 'LICENSE.md', dest: '<%= bower.dist %>/'
+
     browserify:
       dist:
         files: 'dist/micro.js' : ['source/micro.coffee']
-        options:
-          transform: ['coffeeify']
+        options: transform: ['coffeeify']

     uglify:
       options: mangle: true, report: 'gzip'
@@ -31,20 +41,16 @@

     jasmine:
       pivotal:
-        src: [
-          '<%= meta.dist %>/<%= meta.name %>.js'
-        ]
+        src: ['<%= meta.dist %>/<%= meta.name %>.js']
         options: specs: '<%= meta.build %>/<%= meta.name %>.spec.js'

     watch:
       specs:
-        files: [
-          '<%= meta.source %>/*.coffee',
-          '<%= meta.specs %>/*.coffee'
-        ]
+        files: ['<%= meta.source %>/*.coffee', '<%= meta.specs %>/*.coffee']
         tasks: ['coffee', 'browserify', 'jasmine']

     grunt.loadNpmTasks 'grunt-contrib-coffee'
+    grunt.loadNpmTasks 'grunt-contrib-copy'
     grunt.loadNpmTasks 'grunt-contrib-clean'
     grunt.loadNpmTasks 'grunt-contrib-jasmine'
     grunt.loadNpmTasks 'grunt-browserify'
@@ -52,4 +58,5 @@
     grunt.loadNpmTasks 'grunt-contrib-watch'

     grunt.registerTask 'test', ['clean', 'coffee', 'browserify', 'jasmine']
+    grunt.registerTask 'bower', ['default', 'copy']
     grunt.registerTask 'default', ['clean', 'coffee', 'browserify', 'uglify', 'jasmine', 'clean:build']


fix gruntfile

fix gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -85,7 +85,7 @@
           port: 8000

     qunit:
-      all: ['test/unit/index.html']
+      all: ['tests/unit/index.html']

   # Load tasks


update grunt file

update grunt file

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,7 +1,9 @@
 module.exports = (grunt) ->

   @initConfig
+    PKG: @file.readJSON 'package.json'
     APP_ID: null
+    APP_NAME: '<%= PKG.name %>'
     JS_DIR: 'js/'
     JS_LIBS_DIR: '<%= JS_DIR %>lib/'
     ASSETS_DIR: 'assets/'
@@ -11,7 +13,6 @@
     DEV_DIR: 'dev/'
     DIST_FILE: '<%= DIST_DIR %><%= PKG.name %>'
     INDEX_FILE: 'index.html'
-    PKG: @file.readJSON 'package.json'

     clean:
       options:
@@ -135,16 +136,19 @@
               meta = i.split(/\./)
               apps[meta[1]] = meta[0]

-            appname = (grunt.config.get 'PKG').name
+            console.log ('APP')
+            appname = grunt.config.get 'APP_NAME'
             appid = apps[appname]
             grunt.config.set 'APP_ID', appid
             cb()

       launch:
         command: ->
+          appname = grunt.config.get 'APP_NAME'
+          console.log appname
           appid = grunt.config.get 'APP_ID'
           if appid
-            "webtizen -r -i #{appid}"
+            "webtizen -r -i #{appid}.#{appname}"

       uninstall:
         command: ->


Support different versions of sharelatex repos

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -11,30 +11,39 @@
 SERVICES = [{
 	name: "web"
 	repo: "https://github.com/sharelatex/web-sharelatex.git"
+	version: "master"
 }, {
 	name: "document-updater"
 	repo: "https://github.com/sharelatex/document-updater-sharelatex.git"
+	version: "master"
 }, {
 	name: "clsi"
 	repo: "https://github.com/sharelatex/clsi-sharelatex.git"
+	version: "master"
 }, {
 	name: "filestore"
 	repo: "https://github.com/sharelatex/filestore-sharelatex.git"
+	version: "master"
 }, {
 	name: "track-changes"
 	repo: "https://github.com/sharelatex/track-changes-sharelatex.git"
+	version: "master"
 }, {
 	name: "docstore"
 	repo: "https://github.com/sharelatex/docstore-sharelatex.git"
+	version: "master"
 }, {
 	name: "chat"
 	repo: "https://github.com/sharelatex/chat-sharelatex.git"
+	version: "master"
 }, {
 	name: "tags"
 	repo: "https://github.com/sharelatex/tags-sharelatex.git"
+	version: "master"
 }, {
 	name: "spelling"
 	repo: "https://github.com/sharelatex/spelling-sharelatex.git"
+	version: "master"
 }]

 module.exports = (grunt) ->
@@ -86,10 +95,10 @@
 		do (service) ->
 			grunt.registerTask "install:#{service.name}", "Download and set up the #{service.name} service", () ->
 				done = @async()
-				Helpers.installService(service.repo, service.name, done)
+				Helpers.installService(service, done)
 			grunt.registerTask "update:#{service.name}", "Checkout and update the #{service.name} service", () ->
 				done = @async()
-				Helpers.updateService(service.name, done)
+				Helpers.updateService(service, done)
 			grunt.registerTask "run:#{service.name}", "Run the ShareLaTeX #{service.name} service", ["bunyan", "execute:#{service.name}"]

 	grunt.registerTask 'install:config', "Copy the example config into the real config", () ->
@@ -133,41 +142,50 @@

 	Helpers =
-		installService: (repo_src, dir, callback = (error) ->) ->
-			Helpers.cloneGitRepo repo_src, dir, (error) ->
+		installService: (service, callback = (error) ->) ->
+			Helpers.cloneGitRepo service, (error) ->
 				return callback(error) if error?
-				Helpers.installNpmModules dir, (error) ->
+				Helpers.installNpmModules service, (error) ->
 					return callback(error) if error?
-					Helpers.runGruntInstall dir, (error) ->
+					Helpers.runGruntInstall service, (error) ->
 						return callback(error) if error?
 						callback()

-		updateService: (dir, callback = (error) ->) ->
-			Helpers.updateGitRepo dir, (error) ->
+		updateService: (service, callback = (error) ->) ->
+			Helpers.updateGitRepo service, (error) ->
 				return callback(error) if error?
-				Helpers.installNpmModules dir, (error) ->
+				Helpers.installNpmModules service, (error) ->
 					return callback(error) if error?
-					Helpers.runGruntInstall dir, (error) ->
+					Helpers.runGruntInstall service, (error) ->
 						return callback(error) if error?
 						callback()

-		cloneGitRepo: (repo_src, dir, callback = (error) ->) ->
+		cloneGitRepo: (service, callback = (error) ->) ->
+			repo_src = service.repo
+			dir = service.name
 			if !fs.existsSync(dir)
-				proc = spawn "git", ["clone", repo_src, dir], stdio: "inherit"
+				proc = spawn "git", [
+					"clone",
+					"-b", service.version,
+					repo_src,
+					dir
+				], stdio: "inherit"
 				proc.on "close", () ->
 					callback()
 			else
 				console.log "#{dir} already installed, skipping."
 				callback()

-		updateGitRepo: (dir, callback = (error) ->) ->
-			proc = spawn "git", ["checkout", "master"], cwd: dir, stdio: "inherit"
+		updateGitRepo: (service, callback = (error) ->) ->
+			dir = service.name
+			proc = spawn "git", ["checkout", service.version], cwd: dir, stdio: "inherit"
 			proc.on "close", () ->
 				proc = spawn "git", ["pull"], cwd: dir, stdio: "inherit"
 				proc.on "close", () ->
 					callback()

-		installNpmModules: (dir, callback = (error) ->) ->
+		installNpmModules: (service, callback = (error) ->) ->
+			dir = service.name
 			proc = spawn "npm", ["install"], stdio: "inherit", cwd: dir
 			proc.on "close", () ->
 				callback()


update grunt bump task

update grunt bump task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -82,12 +82,9 @@

     bump:
       options:
-        files: [
-          'package.json'
-          'bower.json'
-        ]
+        files: ['package.json', 'bower.json']
         updateConfigs: ['pkg']
-        commitFiles: ['-a']
+        commitFiles: ['package.json', 'bower.json']

   grunt.registerTask 'build', [
     'coffeelint'


fix(Grunt): Fixed configuration errors with css watch task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -191,12 +191,8 @@
         tasks: ["coffee", "concat:jqueryui", "concat:appJs", "clean", "copy:public"]
         options: interrupt: true
       css:
-        files: ["src/css/*.styl"]
-        tasks: ["stylus", "concat:appCss", "clean"]
-        options: interrupt: true
-      vendorCss:
-        files: ["vendor/vendor.styl"]
-        tasks: ["stylus:vendor", "concat:css", "clean"]
+        files: ["src/css/*.styl", "vendor/vendor.styl"]
+        tasks: ["stylus", "concat:css", "clean"]
         options: interrupt: true
       jade:
         files: ["src/**/*.jade"]


Ajusta Gruntfile

Ajusta Gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -97,7 +97,7 @@
 				options:
 					livereload: true
 				files: ['src/**/*.html', 'src/**/*.dust', 'src/**/*.coffee', 'spec/**/*.coffee', 'spec/**/*.html', 'src/**/*.js', 'src/**/*.less']
-				tasks: ['clean', 'concurrent:transform', 'string-replace', 'karma:unit:run']
+				tasks: ['clean', 'concurrent:transform', 'concat', 'uglify', 'string-replace']

 		concurrent:
 			transform: ['copy:main', 'copy:mocks', 'coffee', 'less', 'dustjs']
@@ -118,7 +118,7 @@

 	grunt.loadNpmTasks name for name of pkg.dependencies when name[0..5] is 'grunt-'

-	grunt.registerTask 'default', ['clean', 'concurrent:transform', 'concat', 'uglify', 'string-replace', 'server', 'karma:unit', 'watch:main']
+	grunt.registerTask 'default', ['clean', 'concurrent:transform', 'concat', 'uglify', 'string-replace', 'server', 'watch:main']
 	grunt.registerTask 'dist', ['clean', 'concurrent:transform', 'concat', 'uglify', 'string-replace'] # Dist - minifies files
 	grunt.registerTask 'test', ['karma:single']
 	grunt.registerTask 'server', ['connect', 'remote']


Updated grunt task.

Updated grunt task.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -174,7 +174,6 @@
       complete:
         options:
           custom: '.grunt/pop.mp3'
-          volume: 1

     watch:
       options:
@@ -185,7 +184,7 @@
         event: ['all']

       configFiles:
-        files: ['Gruntfile.js']
+        files: ['Gruntfile.coffee']
         tasks: ['notify:grunt', 'wakeup:complete']
         options:
           reload: true
@@ -212,4 +211,4 @@

   grunt.registerTask 'test', ['jshint:all']
   grunt.registerTask 'dev', ['jade:dev', 'htmlmin:dev', 'sass', 'concat:js', 'copy', 'uglify:dist']
-  grunt.registerTask 'default', ['connect:server', 'dev', 'notify:server', 'watch']+  grunt.registerTask 'default', ['connect:server', 'dev', 'notify:server', 'wakeup:complete', 'watch']


Copy docs into admin assets

Copy docs into admin assets

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -113,6 +113,11 @@
         cwd: 'client/assets'
         src: ['**/*']
         dest: 'public/'
+      docs:
+        expand: yes
+        cwd: 'docs/api'
+        src: ['**/*']
+        dest: 'public/docs/api'
       fontastic:
         expand: yes
         cwd: 'client/assets/fontastic/fonts/'


Add test to verify that we can fit in appcache limits

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -169,6 +169,26 @@
   # Grunt plugins used for deploying
   @loadNpmTasks 'grunt-gh-pages'

+  grunt = @
+  @registerTask 'checkSize', ->
+    limit = 5000
+    done = @async()
+    require('child_process').exec 'du -k --total browser/*', (err, stdout) ->
+      grunt.fail.fatal err if err
+      lines = stdout.split "\n"
+      entries = {}
+      for line in lines
+        continue unless line.length
+        [kb, entry] = line.split "\t"
+        entries[entry] = parseInt kb
+      if entries.total < limit
+        grunt.log.writeln "Total size for build is #{entries.total}kb, below #{limit}kb max"
+        return done()
+      for entry, size of entries
+        continue if entry is 'total'
+        grunt.log.writeln "Size of #{entry} is #{size}kb"
+      grunt.fail.warn new Error "Total size for build is #{entries.total}kb (>#{limit}kb)"
+
   # Our local tasks
   @registerTask 'build', 'Build NoFlo for the chosen target platform', (target = 'all') =>
     @task.run 'noflo_browser'
@@ -182,5 +202,6 @@
     @task.run 'connect'
     @task.run 'noflo_browser_mocha'
     @task.run 'mocha_phantomjs'
+    @task.run 'checkSize'

   @registerTask 'default', ['test']


Add watcher to grunt

Add watcher to grunt

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -14,6 +14,7 @@
   grunt.initConfig
     yeoman: yeomanConfig

+
     coffee:
       dist:
         files: [
@@ -24,17 +25,12 @@
           ext: '.js'
         ]

+
     uglify:
       build:
         src: '<%=yeoman.dist %>/forty-datetime.js'
         dest: '<%=yeoman.dist %>/forty-datetime.min.js'

-#    mochaTest:
-#       test:
-#         options:
-#           reporter: 'spec'
-#           compilers: 'coffee:coffee-script'
-#         src: ['test/**/*.coffee']

     karma:
        options:
@@ -43,11 +39,18 @@
          singleRun: true

+    watch:
+      test:
+        files: ['src/*.*','test/spec/{,*/}*.{coffee,litcoffee,coffee.md}']
+        tasks: ['coffee', 'karma']
+
+
     grunt.loadNpmTasks('grunt-karma')
+    grunt.loadNpmTasks('grunt-contrib-watch')

     grunt.registerTask 'default', [
-#      'mochaTest'
       'karma'
       'coffee'
       'uglify'
+      'watch'
     ]


blank lines

blank lines

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -192,6 +192,7 @@
         grunt.config 'remove.name', name
         grunt.config 'remove.label', label
         removeTask.call @, name, label, done
+
   addTask = (name, label, done) ->
     {replaceText} = @data
     if replaceText
@@ -219,6 +220,7 @@
     for src in @data.src
       grunt.file.delete src.replace new RegExp(myUtils.RegExpEscape("{{name}}"), 'g'), name
     done?()
+
   grunt.loadNpmTasks 'grunt-contrib-clean'
   grunt.loadNpmTasks 'grunt-contrib-coffee'
   grunt.loadNpmTasks 'grunt-contrib-copy'


chore(Gruntfile.coffee): FF 22 is most current

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -112,7 +112,7 @@
         {
             browserName: 'firefox'
             platform: 'Windows 7'
-            version: '21'
+            version: '22'
         }
     ]


added pretty to indent HTML

added pretty to indent HTML

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -2,6 +2,8 @@
 	grunt.initConfig

 		jade:
+			options:
+				pretty: true
 			compile:
 				files: [
 					cwd: "src/views",


Grunt: Disable kill

Grunt: Disable kill

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -118,7 +118,6 @@
     'exec:portallights_linux_gen', 'exec:portallights_linux_comp',
   ]
   @registerTask 'run-microflo-linux', [
-    'exec:kill_microflo_linux'
     'exec:tablelights_run'
     'exec:portallights_run'
   ]


Fix coffee watch

Fix coffee watch

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -22,7 +22,7 @@
     files: ['src/templates/**/*.dust']
     tasks: ['dust', 'concat']

-  config.watch.coffee.tasks.push 'copy:js', 'concat'
+  config.watch.coffee.tasks = ['coffee', 'copy:js', 'concat']

   config.clean.deploy = 'build/<%= relativePath %>/script/'

@@ -79,4 +79,4 @@
     # Turn off LiveReload in development mode
     grunt.config 'watch.options.livereload', false
     return true
-  grunt.registerTask taskName, taskArray for taskName, taskArray of tasks+  grunt.registerTask taskName, taskArray for taskName, taskArray of tasks


removed stray watch

removed stray watch

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -108,7 +108,7 @@
       dev:
         options:
           file: 'app/app.coffee'
-          watchedFolders: ['app.coffee', 'app', 'node_modules']
+          watchedFolders: ['app', 'node_modules']

     forever:
       options:


Added test unit for the controller

Added test unit for the controller

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -17,6 +17,7 @@
                     'tests/units/lib/bootstrapper.js': 'dev/tests/units/lib/bootstrapper.coffee'
                     'tests/units/lib/app.js': 'dev/tests/units/lib/app.coffee'
                     'tests/units/lib/helper.js': 'dev/tests/units/lib/helper.coffee'
+                    'tests/units/helpers/controller.js': 'dev/tests/units/helpers/controller.coffee'
         copy:
             dist:
                 files: [