Fixed issue where getUnreservedProps, isLeftClick, and isModifiedEvent were not defined

--- src/RouteToMixin.coffee
+++ src/RouteToMixin.coffee
@@ -14,6 +14,18 @@
   query: true
   children: true # ReactChildren
 }
+
+isLeftClick = (event) -> event.button == 0
+isModifiedEvent = (event) -> !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
+
+getUnreservedProps = (props, additionalReservedProps) ->
+  if (additionalReservedProps)
+    reservedProps = mergeProperties {}, RESERVED_PROPS
+    reservedProps = mergeProperties reservedProps, additionalReservedProps
+  else
+    reservedProps = RESERVED_PROPS
+
+  withoutProperties props, reservedProps

 RouteToMixin =
   getUnreservedProps: getUnreservedProps
@@ -37,16 +49,4 @@
     event.preventDefault()
     transitionTo @props.to, @getParams(), @props.query

-isLeftClick = (event) -> event.button == 0
-isModifiedEvent = (event) -> !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
-
-getUnreservedProps = (props, additionalReservedProps) ->
-  if (additionalReservedProps)
-    reservedProps = mergeProperties {}, RESERVED_PROPS
-    reservedProps = mergeProperties reservedProps, additionalReservedProps
-  else
-    reservedProps = RESERVED_PROPS
-
-  withoutProperties props, reservedProps
-
 module.exports = RouteToMixin


Only listen for changes on the model if the model exists

--- src/scripts/modules/search/search.coffee
+++ src/scripts/modules/search/search.coffee
@@ -17,7 +17,7 @@
       if location.search and location.search isnt '?q='
         @model = searchResults.load(location.search)

-      @listenTo(@model, 'change:error', @displayError)
+      @listenTo(@model, 'change:error', @displayError) if @model

     regions:
       search: '.search'


change verifying quotas are set properly for kucalc to not block on project restart

--- src/smc-hub/kucalc/compute-client.coffee
+++ src/smc-hub/kucalc/compute-client.coffee
@@ -562,9 +562,13 @@
                     dbg("running, but no quotas changed")
                     opts.cb()
                 else
+                    opts.cb()
                     dbg('running and a quota changed; restart')
-                    @restart(cb:opts.cb)
-
-
-
-
+                    # CRITICAL: do NOT wait on this before returning!  The set_all_quotas call must
+                    # complete quickly (in an HTTP requrest), whereas restart can easily take 20s,
+                    # and there is no reason to wait on this.
+                    @restart()
+
+
+
+


Fix #2293

Fix #2293

--- src/smc-webapp/widget-ssh-keys/main.cjsx
+++ src/smc-webapp/widget-ssh-keys/main.cjsx
@@ -58,6 +58,7 @@
         @setState
             key_title  : ""
             key_value  : ""
+            error      : undefined
             show_panel : not @props.toggleable

     trigger_error: (err) ->


- Add missing require.

- Add missing require.

--- debugger.coffee
+++ debugger.coffee
@@ -6,6 +6,7 @@
 log = require 'winston'
 os = require 'os'
 _ = require 'lodash'
+util = require 'util'
 c = new chalk.constructor enabled: true

 exports.init = () ->


Un-F :poop:

Un-F :poop:

--- spec/extensions/tree-view-spec.coffee
+++ spec/extensions/tree-view-spec.coffee
@@ -131,7 +131,7 @@
         expect(treeView).toMatchSelector(':focus')

   describe "when tree-view:unfocus is triggered on the tree view", ->
-    fit "surrenders focus to the root view but remains open", ->
+    it "surrenders focus to the root view but remains open", ->
       rootView.attachToDom()
       treeView.focus()
       treeView.trigger 'tree-view:unfocus'


move focus to card body when content changes

Otherwise the continue button will keep the focus, causing screenreaders to potentially not notice the content has changed

--- shared/src/components/exercise/intro.cjsx
+++ shared/src/components/exercise/intro.cjsx
@@ -1,5 +1,6 @@
 BS = require 'react-bootstrap'
 React = require 'react'
+ReactDOM = require 'react-dom'
 _ = require 'underscore'
 classnames = require 'classnames'

@@ -22,6 +23,13 @@
   propTypes:
     onContinue: React.PropTypes.func.isRequired
     project: React.PropTypes.string.isRequired
+    stepIntroType: React.PropTypes.string.isRequired
+
+  componentWillReceiveProps: (nextProps) ->
+    # focus the cardbody when paging through
+    # Otherwise the continue button may still be focused, causing screenreaders to fail
+    if nextProps.stepIntroType isnt this.props.stepIntroType
+      ReactDOM.findDOMNode(@refs.body).focus()

   render: ->
     {stepIntroType, project, onContinue} = @props
@@ -30,7 +38,7 @@

     classes = classnames 'task-step', "openstax-#{ALIASES[stepGroup]}-intro", project

-    <CardBody className={classes}>
+    <CardBody ref={'body'} className={classes}>

         {TITLES[stepGroup]}


fix(specs): Update spy on `persistModel` to reflect actual behavior

--- spec/tasks/syncback-draft-spec.coffee
+++ spec/tasks/syncback-draft-spec.coffee
@@ -42,8 +42,7 @@
       else if clientId is "missingDraftId" then Promise.resolve()
       else return Promise.resolve()

-    spyOn(DatabaseTransaction.prototype, "persistModel").andCallFake (draft) ->
-      Promise.resolve(draft)
+    spyOn(DatabaseTransaction.prototype, "persistModel").andReturn Promise.resolve()

   describe "queueing multiple tasks", ->
     beforeEach ->


Fix dropping files on an empty editor in FF

--- src/trix/models/selection_manager.coffee
+++ src/trix/models/selection_manager.coffee
@@ -25,8 +25,8 @@
       locationRange

   setLocationRangeFromPoint: (point) ->
-    locationRange = @getLocationRangeAtPoint(point)
-    @setLocationRange(locationRange)
+    if locationRange = @getLocationRangeAtPoint(point)
+      @setLocationRange(locationRange)

   expandSelectionInDirectionWithGranularity: (direction, granularity) ->
     return unless selection = getDOMSelection()


ahora muestra el nombre de usuario cuando no tiene nombres


add class to new buttons

add class to new buttons

--- src/components/app.cjsx
+++ src/components/app.cjsx
@@ -105,14 +105,14 @@
                 onConfirm={_.partial @onNewRecord, 'exercises'}
                 {...guardProps}
               >
-                New Exercise
+                New Exercise
               </SuretyGuard>

               <SuretyGuard
                 onConfirm={_.partial @onNewRecord, 'vocabulary'}
                 {...guardProps}
               >
-                New Vocabulary Term
+                New Vocabulary Term
               </SuretyGuard>

             </BS.ButtonToolbar>


null check on table value

null check on table value

--- src/fields/table.coffee
+++ src/fields/table.coffee
@@ -37,7 +37,7 @@

   numRows: ->
     value = @get('value')
-    if value.length
+    if value? && value.length
       Math.max @minRows(), (value[0].length || 0), 1
     else
       0


Wercker retry

Wercker retry

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -38,7 +38,7 @@
 # 	DeviceScaleDidChange: "change:deviceScale"
 # 	DeviceContentScaleDidChange: "change:contentScale"
 # 	DeviceFullScreenDidChange: ""
-
+
 class exports.DeviceComponent extends BaseClass

 	@define "context", get: -> @_context


Add scrollToClosestLayer options

Add scrollToClosestLayer options

--- framer/Components/ScrollComponent.coffee
+++ framer/Components/ScrollComponent.coffee
@@ -203,8 +203,8 @@

 		@scrollToPoint(scrollPoint, animate, animationOptions)

-	scrollToClosestLayer: (originX=0, originY=0) ->
-		@scrollToLayer(@closestContentLayer(originX, originY), originX, originY)
+	scrollToClosestLayer: (originX=0, originY=0, animate=true, animationOptions={curve:"spring(500,50,0)"}) ->
+		@scrollToLayer(@closestContentLayer(originX, originY, animate, animationOptions), originX, originY)

 	closestContentLayer: (originX=0, originY=0) ->
 		scrollPoint = Utils.framePointForOrigin(@scrollFrame, originX, originY)


sanitize slider component value updates

sanitize slider component value updates

--- framer/Components/SliderComponent.coffee
+++ framer/Components/SliderComponent.coffee
@@ -205,6 +205,11 @@
 			@_updateValue()

 	_updateValue: =>
+
+		if @_lastUpdatedValue is @value
+			return
+
+		@_lastUpdatedValue = @value
 		@emit("change:value", @value)
 		@emit(Events.SliderValueChange, @value)


SliderComponent: Defaults, Momentum, Radius, KnobSize

--- framer/Components/SliderComponent.coffee
+++ framer/Components/SliderComponent.coffee
@@ -1,4 +1,3 @@
-
 Utils = require "../Utils"
 {Layer} = require "../Layer"
 {Events} = require "../Events"
@@ -22,8 +21,9 @@

 	constructor: (options={}) ->
 		options.backgroundColor ?= "#ccc"
+		options.borderRadius ?= 50
 		options.clip ?= false
-		options.width ?= 200
+		options.width ?= 300
 		options.height ?= 10
 		super options

@@ -39,7 +39,7 @@
 		@knob.draggable.overdrag = false
 		@knob.draggable.momentum = true
 		@knob.draggable.momentumOptions =
-			friction: 10
+			friction: 2
 			tolerance: 1/10
 		@knob.draggable.bounce = false
 		@knob.borderRadius = @knob.width
@@ -49,7 +49,8 @@
 			superLayer: @
 			height: @height
 			width: 0
-
+			borderRadius: 50
+
 		@fill.force2d = true
 		@knob.placeBefore(@fill)

@@ -64,7 +65,7 @@
 			@value = @valueForPoint(event.x - @screenFrame.x)
 			@knob.draggable._touchStart(event)

-		@knobSize = options.knobSize or 10
+		@knobSize = options.knobSize or 30

 	_updateFill: =>
 		@fill.width = @knob.midX


Ext/Table: Add support for multidroppers (e.g., ruler) by allowing the dropper function to load a new function.

--- Oghma/Ext/Table.coffee
+++ Oghma/Ext/Table.coffee
@@ -45,6 +45,10 @@
         @apply_dropper( @t( [ e.getX(), e.getY() ] )..., e )
         null
       )
+      @getEl().on( 'mousemove', ( e ) =>
+        @move_dropper( @t( [ e.getX(), e.getY() ] )..., e )
+        null
+      )
     )

     @keymap = Ext.create( 'Ext.util.KeyMap', target: Ext.getDoc() )
@@ -53,10 +57,15 @@

   # Load a function into the dropper.
   #
-  # @param [function(x, y, event)] Event to load.
+  # The click function may call load_dropper() to support multi-dropper
+  # functionality.
+  #
+  # @param [function(x, y, event)] click Function to load for click.
+  # @param [function(x, y, event)] move  Function to load for move.
   # @return [Oghma.App] this
-  load_dropper: ( f ) ->
-    @dropper = f
+  load_dropper: ( click, move = null ) ->
+    @dropper_click = click
+    @dropper_mvoe  = move
     document.body.style.cursor = 'crosshair'
     this

@@ -64,8 +73,19 @@
   #
   # @return [Oghma.App] this
   unload_dropper: ->
-    @dropper = null
+    @dropper_click = null
+    @dropper_move  = null
     document.body.style.cursor = 'default'
+    this
+
+  # Move the dropper.
+  #
+  # @param [numeric] x X stage location.
+  # @param [numeric] y Y stage location.
+  # @param [Object] e Event
+  # @return [Oghma.App] this
+  move_dropper: ( x, y, e = null ) ->
+    @dropper_move?( x, y, e )
     this

   # Apply the dropper.
@@ -75,9 +95,10 @@
   # @param [Object] e Event
   # @return [Oghma.App] this
   apply_dropper: ( x, y, e = null ) ->
-    @dropper?( x, y, e )
+    original_dropper_click = @dropper_click
     if ! e? || ! e.altKey
       @unload_dropper()
+    original_dropper_click?( x, y, e )
     this

   # Add a binding.


Fix double player run in presentation mode

--- frontend/app/scripts/utils/sketch.coffee
+++ frontend/app/scripts/utils/sketch.coffee
@@ -55,7 +55,6 @@
               analyser.onUpdate = ( bands ) => particle.energy = bands[ particle.band ] / 256 for particle in @particles

               # start as soon as the audio is buffered
-              analyser.start()

               # show audio controls
               document.body.appendChild analyser.audio


Fix status code checking

Fix status code checking

--- src/hook-manager.coffee
+++ src/hook-manager.coffee
@@ -35,7 +35,7 @@
       @buildClient(user, repo)
         .post(JSON.stringify(data)) (error, response, body) =>
           throw error if error
-          throw response unless isSuccessful(response.statusCode)
+          throw response unless @isSuccessful(response)
           @robot.logger.info util.inspect(body)

           @message.reply 'I was able to successfully add the GitHub events hook'
@@ -53,7 +53,7 @@
       @buildClient(user, repo)
         .get() (error, response, body) =>
           throw error if error
-          throw response unless isSuccessful(response.statusCode)
+          throw response unless @isSuccessful(response)
           @robot.logger.info util.inspect(body)

           hooks = JSON.parse(body)


Fix message protocol of server adapter

Fix message protocol of server adapter

--- src/coffee/kopi/db/adapters/server.coffee
+++ src/coffee/kopi/db/adapters/server.coffee
@@ -63,11 +63,11 @@
       url = self._url(query)
       params = self._params(query)
       doneFn = (response) ->
-        message = self._parse(response)
-        fn(null, message)
+        args = self._parse(response)
+        fn(args[0], args[1])
       failFn = (xhr, text, error) ->
         error = self._parseError(xhr, text, error)
-        fn(error)
+        fn(error[0])

       logger.info "Request URL: #{url}"
       # TODO Use some wrapper of $.ajax to queue delayed requests or retry failed requests
@@ -88,9 +88,9 @@

     _parseJSON: (json) ->
       # TODO Verify if response JSON is formatted correctly
-      json
+      if json and json.ok then [null, json.result] else [json.error or true]

     _parseErrorJSON: (xhr, text, error) ->
-      {error: error or true, message: text}
+      [error or true]

   ServerAdapter: ServerAdapter


Extract block.parseToken from block.parse

--- src/liquid/block.coffee
+++ src/liquid/block.coffee
@@ -23,32 +23,35 @@
     token = tokens.shift()

     Promise.try =>
-      if Block.IsTag.test(token.value)
-        match = Block.FullToken.exec(token.value)
-
-        unless match
-          throw new Liquid.SyntaxError("Tag '#{token.value}' was not properly terminated with regexp: #{Liquid.TagEnd.inspect}")
-
-        return @endTag() if @blockDelimiter() is match[1]
-
-        Tag = @template.tags[match[1]]
-        return @unknownTag match[1], match[2], tokens unless Tag
-
-        tag = new Tag @template, match[1], match[2]
-        @nodelist.push tag
-        tag.parseWithCallbacks tokens
-      else if Block.IsVariable.test(token.value)
-        @nodelist.push @createVariable(token)
-      else if token.value.length is 0
-        # skip empty tokens
-      else
-        @nodelist.push token.value
+      @parseToken token, tokens
     .catch (e) =>
       e.message = "#{e.message}\n    at #{token.value} (#{token.filename}:#{token.line}:#{token.col})"
       e.location ?= { col: token.col, line: token.line, filename: token.filename }
       throw e
     .then =>
       @parse tokens
+
+  parseToken: (token, tokens) ->
+    if Block.IsTag.test(token.value)
+      match = Block.FullToken.exec(token.value)
+
+      unless match
+        throw new Liquid.SyntaxError("Tag '#{token.value}' was not properly terminated with regexp: #{Liquid.TagEnd.inspect}")
+
+      return @endTag() if @blockDelimiter() is match[1]
+
+      Tag = @template.tags[match[1]]
+      return @unknownTag match[1], match[2], tokens unless Tag
+
+      tag = new Tag @template, match[1], match[2]
+      @nodelist.push tag
+      tag.parseWithCallbacks tokens
+    else if Block.IsVariable.test(token.value)
+      @nodelist.push @createVariable(token)
+    else if token.value.length is 0
+      # skip empty tokens
+    else
+      @nodelist.push token.value

   endTag: ->
     @ended = true


:art: Clean up MarkerIndex::insert

:art: Clean up MarkerIndex::insert

Signed-off-by: Nathan Sobo <2e8aa918660411855c6d44d5bb2da677aa033255@github.com>

--- src/marker-index.coffee
+++ src/marker-index.coffee
@@ -1,6 +1,6 @@
 Point = require "./point"
 Range = require "./range"
-{setEqual, subtractSet, addSet, intersectSet} = require "./set-helpers"
+{addSet, subtractSet, intersectSet, setEqual} = require "./set-helpers"

 BRANCHING_FACTOR = 3

@@ -13,9 +13,6 @@
       addSet(@ids, child.ids)

   insert: (ids, start, end) ->
-    # Insert the given id into all children that intersect the given range.
-    # Take the intersection of the given range and the child's range when
-    # inserting into each child.
     rangeIsEmpty = start.compare(end) is 0
     childEnd = Point.zero()
     i = 0
@@ -24,23 +21,31 @@
       childStart = childEnd
       childEnd = childStart.traverse(child.extent)

+      switch childEnd.compare(start)
+        when -1 then childPrecedesRange = true
+        when 1  then childPrecedesRange = false
+        when 0
+          if child.hasEmptyRightmostLeaf()
+            childPrecedesRange = false
+          else
+            childPrecedesRange = true
+            if rangeIsEmpty
+              ids = new Set(ids)
+              child.findContaining(child.extent, ids)
+      continue if childPrecedesRange
+
       switch childStart.compare(end)
         when -1 then childFollowsRange = false
-        when 0  then childFollowsRange = not child.hasEmptyLeftmostLeaf() and not rangeIsEmpty
         when 1  then childFollowsRange = true
+        when 0  then childFollowsRange = not (child.hasEmptyLeftmostLeaf() or rangeIsEmpty)
       break if childFollowsRange
-
-      switch childEnd.compare(start)
-        when -1 then childPrecedesRange = true
-        when 0  then childPrecedesRange = not child.hasEmptyRightmostLeaf()
-        when 1  then childPrecedesRange = false
-      continue if childPrecedesRange

       relativeStart = Point.max(Point.zero(), start.traversalFrom(childStart))
       relativeEnd = Point.min(child.extent, end.traversalFrom(childStart))
       if newChildren = child.insert(ids, relativeStart, relativeEnd)
         @children.splice(i - 1, 1, newChildren...)
         i += newChildren.length - 1
+      break if rangeIsEmpty

     if @children.length > BRANCHING_FACTOR
       splitIndex = Math.ceil(@children.length / BRANCHING_FACTOR)
@@ -50,94 +55,99 @@
       return

   delete: (id) ->
-    if @ids.has(id)
-      @ids.delete(id)
-      i = 0
-      while i < @children.length
-        @children[i].delete(id)
-        if @children[i - 1]?.shouldMergeWith(@children[i])
-          @children.splice(i - 1, 2, @children[i - 1].merge(@children[i]))
-        else
-          i++
-
-  hasEmptyRightmostLeaf: ->
-    @children[@children.length - 1].hasEmptyRightmostLeaf()
-
-  hasEmptyLeftmostLeaf: ->
-    @children[0].hasEmptyLeftmostLeaf()
+    return unless @ids.delete(id)
+    i = 0
+    while i < @children.length
+      @children[i].delete(id)
+      if @children[i - 1]?.shouldMergeWith(@children[i])
+        @children.splice(i - 1, 2, @children[i - 1].merge(@children[i]))
+      else
+        i++

   splice: (position, oldExtent, newExtent, excludedIds) ->
-    childStart = Point.zero()
-    oldExtentEmpty = oldExtent.isZero()
-    for child in @children
+    oldRangeIsEmpty = oldExtent.isZero()
+    childEnd = Point.zero()
+    for child in @children
+      childStart = childEnd
       childEnd = childStart.traverse(child.extent)

       if remainderToDelete?
-        break unless remainderToDelete.isPositive()
         remainderToDelete = child.splice(Point.zero(), remainderToDelete, Point.zero())
         continue

-      comparison = childEnd.compare(position)
-      if comparison > 0 or (comparison is 0 and oldExtentEmpty and child.hasEmptyRightmostLeaf())
-        remainderToDelete = child.splice(position.traversalFrom(childStart), oldExtent, newExtent, excludedIds)
-
-      childStart = childEnd
+      switch childEnd.compare(position)
+        when -1 then childPrecedesRange = true
+        when 0  then childPrecedesRange = not (child.hasEmptyRightmostLeaf() and oldRangeIsEmpty)
+        when 1  then childPrecedesRange = false
+      continue if childPrecedesRange
+
+      relativeStart = position.traversalFrom(childStart)
+      remainderToDelete = child.splice(relativeStart, oldExtent, newExtent, excludedIds)

     @extent = @extent
       .traverse(newExtent.traversalFrom(oldExtent))
       .traverse(remainderToDelete)
     remainderToDelete

-  shouldMergeWith: (other) ->
-    @children.length + other.children.length <= BRANCHING_FACTOR
-
-  merge: (other) ->
-    new Node(@children.concat(other.children))
-
   getStart: (id) ->
     return unless @ids.has(id)
-    childStart = Point.zero()
-    for child in @children
+    childEnd = Point.zero()
+    for child in @children
+      childStart = childEnd
+      childEnd = childStart.traverse(child.extent)
       if startRelativeToChild = child.getStart(id)
         return childStart.traverse(startRelativeToChild)
-      childStart = childStart.traverse(child.extent)
     return

   getEnd: (id) ->
     return unless @ids.has(id)
-    childStart = Point.zero()
-    for child in @children
+    childEnd = Point.zero()
+    for child in @children
+      childStart = childEnd
+      childEnd = childStart.traverse(child.extent)
       if endRelativeToChild = child.getEnd(id)
         end = childStart.traverse(endRelativeToChild)
       else if end?
         break
-      childStart = childStart.traverse(child.extent)
     end

   findContaining: (point, set) ->
-    childStart = Point.zero()
-    for child in @children
-      childEnd = childStart.traverse(child.extent)
-      if point.compare(childEnd) <= 0
-        child.findContaining(point.traversalFrom(childStart), set)
-      break if childEnd.compare(point) > 0
-      childStart = childEnd
+    childEnd = Point.zero()
+    for child in @children
+      childStart = childEnd
+      childEnd = childStart.traverse(child.extent)
+      continue if childEnd.compare(point) < 0
+      break if childStart.compare(point) > 0
+      child.findContaining(point.traversalFrom(childStart), set)

   findIntersecting: (start, end, set) ->
     if start.isZero() and end.compare(@extent) is 0
       addSet(set, @ids)
-    else
-      childEnd = Point.zero()
-      for child in @children
-        childStart = childEnd
-        childEnd = childStart.traverse(child.extent)
-        continue if childEnd.compare(start) < 0
-        break if childStart.compare(end) > 0
-        child.findIntersecting(
-          Point.max(Point.zero(), start.traversalFrom(childStart)),
-          Point.min(child.extent, end.traversalFrom(childStart)),
-          set
-        )
+      return
+
+    childEnd = Point.zero()
+    for child in @children
+      childStart = childEnd
+      childEnd = childStart.traverse(child.extent)
+      continue if childEnd.compare(start) < 0
+      break if childStart.compare(end) > 0
+      child.findIntersecting(
+        Point.max(Point.zero(), start.traversalFrom(childStart)),
+        Point.min(child.extent, end.traversalFrom(childStart)),
+        set
+      )
+
+  hasEmptyRightmostLeaf: ->
+    @children[@children.length - 1].hasEmptyRightmostLeaf()
+
+  hasEmptyLeftmostLeaf: ->
+    @children[0].hasEmptyLeftmostLeaf()
+
+  shouldMergeWith: (other) ->
+    @children.length + other.children.length <= BRANCHING_FACTOR
+
+  merge: (other) ->
+    new Node(@children.concat(other.children))

   toString: (indentLevel=0) ->
     indent = ""
@@ -196,29 +206,29 @@
     # shrink enough, we may need to shrink subsequent leaves.
     @extent.traversalFrom(myOldExtent).traversalFrom(spliceDelta)

+  getStart: (id) ->
+    Point.zero() if @ids.has(id)
+
+  getEnd: (id) ->
+    @extent if @ids.has(id)
+
+  findContaining: (point, set) ->
+    addSet(set, @ids)
+
+  findIntersecting: (start, end, set) ->
+    addSet(set, @ids)
+
+  hasEmptyRightmostLeaf: ->
+    @extent.isZero()
+
+  hasEmptyLeftmostLeaf: ->
+    @extent.isZero()
+
   shouldMergeWith: (other) ->
     setEqual(@ids, other.ids)

   merge: (other) ->
     new Leaf(@extent.traverse(other.extent), new Set(@ids))
-
-  getStart: (id) ->
-    Point.zero() if @ids.has(id)
-
-  getEnd: (id) ->
-    @extent if @ids.has(id)
-
-  hasEmptyRightmostLeaf: ->
-    @extent.isZero()
-
-  hasEmptyLeftmostLeaf: ->
-    @extent.isZero()
-
-  findContaining: (point, set) ->
-    addSet(set, @ids)
-
-  findIntersecting: (start, end, set) ->
-    addSet(set, @ids)

   toString: (indentLevel=0) ->
     indent = ""
@@ -238,10 +248,7 @@
     @rootNode = new Leaf(Point.infinity(), new Set)

   insert: (id, start, end) ->
-    ids = new Set
-    ids.add(id)
-    @rootNode.findContaining(start, ids) if start.compare(end) is 0
-    if splitNodes = @rootNode.insert(ids, start, end)
+    if splitNodes = @rootNode.insert(new Set().add(id), start, end)
       @rootNode = new Node(splitNodes)

   delete: (id) ->


:memo:

:memo:

[ci skip]

--- src/marker-layer.coffee
+++ src/marker-layer.coffee
@@ -251,7 +251,7 @@
   # * `callback` A {Function} that will be called with a {Marker} whenever a
   #   new marker is created.
   #
-  # You should prefer {onDidUpdate} when synchronous notifications aren't
+  # You should prefer {::onDidUpdate} when synchronous notifications aren't
   # absolutely necessary.
   #
   # Returns a {Disposable}.


fixed wrong purposes.csv url in purpose-tree view

--- frontend/purpose-tree/purpose-tree.coffee
+++ frontend/purpose-tree/purpose-tree.coffee
@@ -16,7 +16,7 @@
     .attr("transform", "translate(40, 0)")

-d3.csv "/aiddata-purposes.csv", (csv) ->
+d3.csv "aiddata-purposes.csv", (csv) ->

   purposesNested = d3.nest()


Store main path of module

Store main path of module

This is the key in Module._cache

--- src/module-cache.coffee
+++ src/module-cache.coffee
@@ -17,11 +17,17 @@

     childMetadata = JSON.parse(fs.readFileSync(childMetadataPath))
     if childMetadata?.version
-      relativePath = path.relative(rootPath, childPath)
-      moduleCache.dependencies.push
-        name: childMetadata.name
-        version: childMetadata.version
-        path: relativePath
+      try
+        mainPath = require.resolve(childPath)
+      catch error
+        console.log "Skipping #{childPath}, no main module"
+
+      if mainPath
+        moduleCache.dependencies.push
+          name: childMetadata.name
+          version: childMetadata.version
+          path: path.relative(rootPath, mainPath)
+
       loadDependencies(childPath, rootPath, rootMetadata, moduleCache)

 loadFolderCompatibility = (modulePath, rootPath, rootMetadata, moduleCache) ->


Check cache before atom require short-circuit

--- src/module-cache.coffee
+++ src/module-cache.coffee
@@ -102,8 +102,6 @@
   return

 resolveModulePath = (relativePath, parentModule) ->
-  return cache.atomExportsPath if relativePath is 'atom'
-
   return unless relativePath
   return unless parentModule?.id

@@ -115,7 +113,11 @@
   folderPath = path.dirname(parentModule.id)

   range = cache.folders[folderPath]?[relativePath]
-  return unless range?
+  unless range?
+    if relativePath is 'atom'
+      return cache.atomExportsPath
+    else
+      return

   candidates = cache.dependencies[relativePath]
   return unless candidates?


* [bug] many bugs fixed.

* [bug] many bugs fixed.

--- src/nosql-subkey.coffee
+++ src/nosql-subkey.coffee
@@ -77,11 +77,15 @@
       else
         throw new InvalidArgumentError("class should be inherited from EncodingNoSQL or AbstractNoSQL")
     @Subkey = subkey(@)
-    AbstractNoSQL.constructor.apply(this, arguments)
-  ###
+    AbstractNoSQL::constructor.apply(this, arguments)
+  final: ->
+    @Subkey = undefined
+    super
+  ###
     first check preHooks if operationType
     return falsse if prehooks disallow this operation
     or return encoded string.
+    ops: only for batch(trasaction) operations
   ###
   encodeKey: (aPathArray, aKey, op, operationType, ops)->
     result = prepareOperation(@preHooks, operationType, op, aPathArray, aKey)
@@ -111,17 +115,15 @@
       @cache = null
     super(isOpened, options)
   getPath: (options) ->
-    result = if options and options.path then getPathArray(options.path) else "."
-    vParentPath = if @_options and @_options.path then @_options.path else PATH_SEP
-    result = resolvePathArray vParentPath, result
-    result.shift(0,1)
+    vParentPath = if @_options and @_options.path then @_options.path else []
+    result = if options and options.path then getPathArray(options.path, vParentPath) else vParentPath
     result
   isExistsSync: (key, options) ->
     path = @getPath(options)
     options = extend {}, @_options, options
     key = @encodeKey(path, key, options)
     return false if key is false
-    AbstractNoSQL.isExistsAsync.call @, key, options
+    AbstractNoSQL::isExistsSync.call @, key, options
   isExistsAsync: (key, options, callback) ->
     path = @getPath(options)
     if isFunction options
@@ -130,13 +132,13 @@
     options = extend {}, @_options, options
     key = @encodeKey(path, key, options)
     return false if key is false
-    AbstractNoSQL.isExistsAsync.call @, key, options, callback
+    AbstractNoSQL::isExistsAsync.call @, key, options, callback
   getSync: (key, options) ->
     path = @getPath(options)
     options = extend {}, @_options, options
     key = @encodeKey path, key, options, GET_OP
     return false if key is false
-    result = AbstractNoSQL.getSync.call(@, key, options)
+    result = AbstractNoSQL::getSync.call(@, key, options)
     encoding = @valueEncoding options
     result = encoding.decode(result) if encoding
     result
@@ -149,7 +151,7 @@
     key = @encodeKey(path, key, options, GET_OP)
     return false if key is false
     encoding = @valueEncoding options
-    AbstractNoSQL.getAsync.call @, key, options, (err, value)=>
+    AbstractNoSQL::getAsync.call @, key, options, (err, value)=>
       return @dispatchError err, callback if err
       value = encoding.decode(value) if encoding
       callback null, value
@@ -158,7 +160,7 @@
     options = extend {}, @_options, options
     key = @encodeKey path, key, options, GET_OP
     return false if key is false
-    AbstractNoSQL.getBufferSync.call(@, key, destBuffer, options)
+    AbstractNoSQL::getBufferSync.call(@, key, destBuffer, options)
   getBufferAsync: (key, destBuffer, options, callback) ->
     path = @getPath(options)
     if isFunction options
@@ -167,7 +169,7 @@
     options = extend {}, @_options, options
     key = @encodeKey(path, key, options, GET_OP)
     return false if key is false
-    AbstractNoSQL.getBufferAsync.call(@, key, destBuffer, options, callback)
+    AbstractNoSQL::getBufferAsync.call(@, key, destBuffer, options, callback)
   mGetSync: (keys, options) ->
     path = @getPath(options)
     options = extend {}, @_options, options
@@ -179,7 +181,7 @@
       vOptions.path = options.path
       result.push k if k isnt false

-    result = AbstractNoSQL.mGetSync.call(@, result, options)
+    result = AbstractNoSQL::mGetSync.call(@, result, options)
     if options.keys isnt false
       result.map (item)=>
         item.key = @decodeKey item.key, options
@@ -201,7 +203,7 @@
       vOptions.path = options.path
       vKeys.push k if k isnt false
     that = @
-    AbstractNoSQL.mGetAsync.call @, vKeys, options, (err, result)->
+    AbstractNoSQL::mGetAsync.call @, vKeys, options, (err, result)->
       return that.dispatchError err, callback if err
       if options.keys isnt false
         result.map (item)->
@@ -217,7 +219,7 @@
     return false if key is false
     valueEncoding = @valueEncoding options
     value = valueEncoding.encode(value) if valueEncoding
-    result = AbstractNoSQL.putSync.call(@, key, value, options)
+    result = AbstractNoSQL::putSync.call(@, key, value, options)
     if result
       vKeyPath = options._keyPath
       options.path = vKeyPath[0]
@@ -235,7 +237,7 @@
     return false if key is false
     valueEncoding = @valueEncoding options
     value = valueEncoding.encode(value) if valueEncoding
-    AbstractNoSQL.putAsync.call @, key, value, options, (err, result)=>
+    AbstractNoSQL::putAsync.call @, key, value, options, (err, result)=>
       return @dispatchError err, callback if err
       vKeyPath = options._keyPath
       options.path = vKeyPath[0]
@@ -248,7 +250,7 @@
     options = extend {}, @_options, options
     key = @encodeKey key, path, options, DEL_OP
     return false if key is false
-    result = AbstractNoSQL.delSync.call(@, key, options)
+    result = AbstractNoSQL::delSync.call(@, key, options)
     if result
       vKeyPath = options._keyPath
       options.path = vKeyPath[0]
@@ -264,7 +266,7 @@
     options = extend {}, @_options, options
     key = @encodeKey key, path, options, DEL_OP
     return false if key is false
-    AbstractNoSQL.delAsync.call @, key, options, (err, result)=>
+    AbstractNoSQL::delAsync.call @, key, options, (err, result)=>
       return @dispatchError err, callback if err
       vKeyPath = options._keyPath
       options.path = vKeyPath[0]
@@ -330,7 +332,7 @@
     start = encodeKey(path, start, keyEncoding, options) if start isnt undefined
     vOptions.path = options.path
     end = encodeKey(path, end, keyEncoding, options) if end isnt undefined
-    AbstractNoSQL.approximateSizeSync.call(@, start, end)
+    AbstractNoSQL::approximateSizeSync.call(@, start, end)
   #TODO: approximateSizeAsync should not be here.
   approximateSizeAsync:(start, end, callback) ->
     path = @getPath(options)
@@ -340,7 +342,7 @@
     start = encodeKey(path, start, keyEncoding, options) if start isnt undefined
     vOptions.path = options.path
     end = encodeKey(path, end, keyEncoding, options) if end isnt undefined
-    AbstractNoSQL.approximateSizeAsync.call(@, start, end, callback)
+    AbstractNoSQL::approximateSizeAsync.call(@, start, end, callback)
   iterator: (options) ->
     options = extend {}, @_options, options
     super(options)


fix some things

fix some things

--- src/server/middleware/request_info.coffee
+++ src/server/middleware/request_info.coffee
@@ -5,22 +5,23 @@
 middleware = (req, res, next) ->
   res.locals.REQUEST = null
   return next() unless req.session.user_id
-  Cache.get "request:#{req.session.user_id}", (cache_request) ->
-    if cache_request
-      res.locals.REQUEST = cache_request
-      console.log "hay cache, seteo res locals", res.locals.REQUEST
-      next()
-    else
-      Request.connect (client) ->
-        Request.get_request_by_user_id client, req.session.user_id, (request) ->
-          if request
-            request_data = request
-            Cache.set "request:#{req.session.user_id}", request_data, (cache_request) ->
-              res.locals.REQUEST = request_data
-              console.log "no hay cache, seteo res locals", res.locals.REQUEST
-              next()
-          else
-            Cache.del "request:#{req.session.user_id}", () ->
-              next()
+  Cache.del "request:#{req.session.user_id}", () ->
+    Cache.get "request:#{req.session.user_id}", (cache_request) ->
+      if cache_request
+        res.locals.REQUEST = cache_request
+        console.log "hay cache, seteo res locals", res.locals.REQUEST
+        next()
+      else
+        Request.connect (client) ->
+          Request.get_request_by_user_id client, req.session.user_id, (request) ->
+            if request
+              request_data = request
+              Cache.set "request:#{req.session.user_id}", request_data, (cache_request) ->
+                res.locals.REQUEST = request_data
+                console.log "no hay cache, seteo res locals", res.locals.REQUEST
+                next()
+            else
+              Cache.del "request:#{req.session.user_id}", () ->
+                next()

 module.exports = middleware


Removing obsolete code.

Removing obsolete code.

--- engine/framebuffer.coffee
+++ engine/framebuffer.coffee
@@ -24,9 +24,7 @@
         if tex_type == @render_manager.gl.FLOAT
             if not @render_manager.extensions['texture_float']
                 tex_type == @render_manager.gl.UNSIGNED_BYTE
-            else if @context.MYOU_PARAMS.nodejs
-                # TODO: improve node-webgl to do this
-                internal_format = 0x8814 # RGBA32F_ARB
+
         gl.texImage2D gl.TEXTURE_2D, 0, internal_format, size_x, size_y, 0, tex_format, tex_type, null

         @render_buffer= rb = gl.createRenderbuffer()


HLS Index: Change how we generate short index to test an IOS issue

On the KPCC iPhone app we're seeing some seek behavior changes that may have
coincided with 01b9525adaa1785ccd1229a30b45e616ce1f2d18. Removing the
DATE-TIME from the full index to see if that changes what we're seeing.

--- src/streammachine/rewind/hls_index.coffee
+++ src/streammachine/rewind/hls_index.coffee
@@ -87,6 +87,7 @@
         # segment #2 for our next ts

         idx_segs = []
+        short_segs = []

         # -- loop through remaining segments -- #

@@ -94,11 +95,11 @@

         _short_start = _short_start - 2

-        for seg,i in segs[2..-1]
+        for seg,i in segs[2..]
             # is the segment where we expect it in the timeline?
             has_disc = !(seg.discontinuitySeq == dseq)

-            datetime = if i == 0 || i == _short_start || has_disc
+            datetime = if i == 0 || has_disc
                 "#EXT-X-PROGRAM-DATE-TIME:#{@tz(seg.ts,"%FT%T.%3N%:z")}"

             idx_segs.push new Buffer """
@@ -120,7 +121,17 @@
         @_index = idx_segs

         @_short_header  = short_head
-        @_short_index   = idx_segs[ _short_start.. ]
+
+
+        @_short_index   = idx_segs[ (_short_start+1).. ]
+
+        # FIXME: special case while we sort out an ios issue
+        _ss_seg = segs[_short_start+2]
+        @_short_index.unshift new Buffer """
+        #EXTINF:#{_ss_seg.duration / 1000},
+        #EXT-X-PROGRAM-DATE-TIME:#{@tz(_ss_seg.ts,"%FT%T.%3N%:z")}
+        /#{@stream.key}/ts/#{_ss_seg.id}.#{@stream.opts.format}
+        """

         @_segment_idx = seg_map


Added more methods to PowerCanvas

Added more methods to PowerCanvas

--- src/power_canvas.coffee
+++ src/power_canvas.coffee
@@ -40,7 +40,6 @@
           block(this)
         finally
           context.restore()
-

         return this

@@ -48,19 +47,46 @@
         context.clearRect(0, 0, canvas.width, canvas.height)

         return this
-
+
+      clearRect: (x, y, width, height) ->
+        context.clearRect(x, y, width, height)
+
+        return this
+
       context: ->
         context

       element: ->
         canvas
+
+      globalAlpha: (newVal) ->
+        if newVal?
+          context.globalAlpha = newVal
+          return this
+        else
+          context.globalAlpha
+
+      compositeOperation: (newVal) ->
+        if newVal?
+          context.globalCompositeOperation = newVal
+          return this
+        else
+          context.globalCompositeOperation

       createLinearGradient: (x0, y0, x1, y1) ->
         context.createLinearGradient(x0, y0, x1, y1)

       createRadialGradient: (x0, y0, r0, x1, y1, r1) ->
         context.createRadialGradient(x0, y0, r0, x1, y1, r1)
-
+
+      buildRadialGradient: (c1, c2, stops) ->
+        gradient = context.createRadialGradient(c1.x, c1.y, c1.radius, c2.x, c2.y, c2.radius)
+
+        for position, color of stops
+          gradient.addColorStop(position, color)
+
+        return gradient
+
       createPattern: (image, repitition) ->
         context.createPattern(image, repitition)

@@ -138,6 +164,16 @@
         context.fillRect(x, y, width, height)

         return this
+
+      fillShape: (points...) ->
+        context.beginPath()
+        points.each (point, i) ->
+          if i == 0
+            context.moveTo(point.x, point.y)
+          else
+            context.lineTo(point.x, point.y)
+        context.lineTo points[0].x, points[0].y
+        context.fill()

       ###*
       * Adapted from http://js-bits.blogspot.com/2010/07/canvas-rounded-corner-rectangles.html
@@ -196,7 +232,6 @@
         if color
           if color.channels
             context.fillStyle = color.toString()
-            log color
           else
             context.fillStyle = color

@@ -256,4 +291,3 @@
       return $canvas

 )(jQuery)
-


final commit

final commit

--- lib/collections/footballers.coffee
+++ lib/collections/footballers.coffee
@@ -1,14 +1,12 @@
 @Footballers = new Mongo.Collection 'footballers'
 @Current = new Mongo.Collection 'current'
-
-# TODO: add credits when game starts
-# TODO: fix UX

 Accounts.onCreateUser((options, user)->
   user.profile = {} if user.profile is undefined
   _.extend(user.profile, {credits: 300})
   user
 )
+
 Meteor.users.allow
   update: ->
     true


x_range and y_range are references that need to be resolved

--- static/coffee/renderers/guide/axis.coffee
+++ static/coffee/renderers/guide/axis.coffee
@@ -100,7 +100,8 @@
     i = @get('guidespec').dimension
     j = (i + 1) % 2

-    ranges = [@get_obj('plot').get('x_range'), @get_obj('plot').get('y_range')]
+    ranges = [@get_obj('plot').get_obj('x_range'),
+      @get_obj('plot').get_obj('y_range')]

     user_bounds = @get('guidespec').bounds ? 'auto'
     range_bounds = [ranges[i].get('min'), ranges[i].get('max')]
@@ -125,7 +126,7 @@
     i = @get('guidespec').dimension
     j = (i + 1) % 2

-    ranges = [@get_obj('plot').get('x_range'), @get_obj('plot').get('y_range')]
+    ranges = [@get_obj('plot').get_obj('x_range'), @get_obj('plot').get_obj('y_range')]
     range = ranges[i]
     cross_range = ranges[j]

@@ -154,7 +155,7 @@
     i = @get('guidespec').dimension
     j = (i + 1) % 2

-    ranges = [@get_obj('plot').get('x_range'), @get_obj('plot').get('y_range')]
+    ranges = [@get_obj('plot').get_obj('x_range'), @get_obj('plot').get_obj('y_range')]
     range = ranges[i]
     cross_range = ranges[j]

@@ -189,7 +190,7 @@
     i = @get('guidespec').dimension
     j = (i + 1) % 2

-    ranges = [@get_obj('plot').get('x_range'), @get_obj('plot').get('y_range')]
+    ranges = [@get_obj('plot').get_obj('x_range'), @get_obj('plot').get_obj('y_range')]
     range = ranges[i]
     cross_range = ranges[j]


make default label font a little smaller

--- static/coffee/renderers/guide/axis.coffee
+++ static/coffee/renderers/guide/axis.coffee
@@ -212,7 +212,7 @@

     major_label_standoff: 15
     major_label_text_font: "helvetica"
-    major_label_text_font_size: "12pt"
+    major_label_text_font_size: "10pt"
     major_label_text_font_style: "normal"
     major_label_text_color: "#444444"
     major_label_text_alpha: 1.0


DiaScene: addContainer: to return container and create canvas first

--- lib/components/dia/diascene.coffee
+++ lib/components/dia/diascene.coffee
@@ -37,6 +37,9 @@

   addContainer: (container, pos = {}) ->
+
+    @createCanvas()
+
     @addSubView container

     container.on 'NewDiaObjectAdded', @bound 'diaAdded'
@@ -54,7 +57,7 @@
     container.setX pos.x  if pos.x?
     container.setY pos.y  if pos.y?

-    @createCanvas()
+    return container

   drawFakeLine: (options = {}) ->


Make the test assertions a bit easier to digest


use robot.hear to listen for anything that looks like a jira issue

--- src/scripts/jira.coffee
+++ src/scripts/jira.coffee
@@ -246,7 +246,7 @@
     comment msg, msg.match[1], msg.match[2], (text) ->
       msg.reply text

-  robot.respond /([^\w\-]|^)(\w+-[0-9]+)(?=[^\w]|$)/ig, (msg) ->
+  robot.hear /([^\w\-]|^)(\w+-[0-9]+)(?=[^\w]|$)/ig, (msg) ->
     if msg.message.user.id is robot.name
       return

@@ -259,6 +259,4 @@
       if !recentissues.contains msg.message.user.room+ticket
         info msg, ticket, (text) ->
           msg.send text
-        recentissues.add msg.message.user.room+ticket
-
-
+        recentissues.add msg.message.user.room+ticket


Last commit had no effect, this one actually uses the changes

--- src/scripts/time.coffee
+++ src/scripts/time.coffee
@@ -212,7 +212,7 @@
         message: {
           user: {
             name: body.user_name,
-            room: body.channel_name,
+            room: channel_name,
             slack: {
               tz: TIMEZONE
             }


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/title_screen.coffee
+++ src/title_screen.coffee
@@ -1,3 +1,13 @@
+###*
+The Text Effect class provides a method to display moving text onscreen, fading out the text over the effect duration.
+
+By default, images are loaded asynchronously. A proxy object is
+returned immediately. Even though it has a draw method it will not
+draw anything to the screen until the image has been loaded.
+
+@name TextEffect
+@constructor
+###
 TitleScreen = (I={}) ->
   self = TextScreen(I)


bug repair

bug repair

--- lib/autocompletion/member-provider.coffee
+++ lib/autocompletion/member-provider.coffee
@@ -118,7 +118,7 @@
                     snippet     : snippet
                     displayText : displayText
                     leftLabel   : returnValue
-                    description : if ele.args.descriptions.short? then ele.args.descriptions.short else ''
+                    description : if ele.args.descriptions && ele.args.descriptions.short? then ele.args.descriptions.short else ''
                     className   : if ele.args.deprecated then 'php-atom-autocomplete-strike' else ''

         return suggestions


Remove listener on unmount

Remove listener on unmount

--- resources/assets/lib/popup-menu.coffee
+++ resources/assets/lib/popup-menu.coffee
@@ -77,6 +77,7 @@

   componentWillUnmount: =>
     $(document).off ".#{@uuid}"
+    $(window).off ".#{@uuid}"

   dismiss: =>


sagews: ensure codes don't show in other window on copy/paste

--- salvus/page/syncdoc.coffee
+++ salvus/page/syncdoc.coffee
@@ -978,7 +978,7 @@
                     # since we just canceled the change.
                     @remove_cell_flags_from_changeObj(changeObj, ACTION_FLAGS)
                     @_apply_changeObj(changeObj)
-                    @process_sage_updates(cm:instance, caller:"paste")
+                    @process_sage_updates(caller:"paste")
                     @sync()

             cm.sage_update_queue = []


worksheet padding -- oops, this felt very bad for empty worksheets!

--- salvus/page/syncdoc.coffee
+++ salvus/page/syncdoc.coffee
@@ -816,6 +816,8 @@
     pad_bottom_with_newlines: (n) =>
         cm = @codemirror
         m = cm.lineCount()
+        if m <= 13  # don't bother until worksheet gets big
+            return
         j = m-1
         while j >= 0 and j >= m-n and cm.getLine(j).length == 0
             j -= 1


fixing space bar [4ab8085149e9de1d9541fcbacbdce90abc64c382]

--- review_front/tests/mocks/server.coffee
+++ review_front/tests/mocks/server.coffee
@@ -2,7 +2,7 @@
 `import Pretender from 'pretender'`

 Server = Ember.Object.extend
-  init:->
+  init: ->
     @set('server', @_makePretender())

   shutdown: ->


Revert "temp disable cpp (#6236)"

Revert "temp disable cpp (#6236)"

This reverts commit 6f38ee66e8cf1946ce9c03cefb25264a130f1cda.

--- app/models/User.coffee
+++ app/models/User.coffee
@@ -589,7 +589,7 @@
   showChinaResourceInfo: -> features?.china ? false
   useChinaHomeView: -> features?.china ? false
   showChinaRegistration: -> features?.china ? false
-  enableCpp: -> false # me.hasSubscription() || me.isStudent() || me.isTeacher()
+  enableCpp: -> me.hasSubscription() || me.isStudent() || me.isTeacher()
   useQiyukf: -> features?.china ? false
   useChinaServices: -> features?.china ? false
   useGeneralArticle: -> not (features?.china ? false)


fix for section[id]>h1.self-link highlighting problem

--- main/static/src/script/common/util.coffee
+++ main/static/src/script/common/util.coffee
@@ -55,6 +55,5 @@
     id = id.replace('.', '\\.').replace('@', '\\@')
     ($ '.hash').removeClass 'hash'
     if id.length > 0
-      if ($ "##{id}").hasClass 'self-link'
-        ($ "##{id}").addClass 'hash'
+      ($ "##{id}.self-link, ##{id} .self-link").first().addClass('hash')


upgrades to deploy hooks

upgrades to deploy hooks

--- scripts/github-hook.coffee
+++ scripts/github-hook.coffee
@@ -121,7 +121,7 @@
     {creator, sha, environment} = deployment

     """
-    [#{creator.login}](#{creator.html_url}) started a deployment of #{sha[0..8]} in [#{repository.name}](#{repository.html_url}) to #{environment}
+    [#{creator.login}](#{creator.html_url}) started a deployment of [#{sha[0..8]}](#{repository.html_url}/commits/#{sha}) in [#{repository.name}](#{repository.html_url}) to #{environment}
     """

   formatDeploymentStatus = (event) ->


Start the timer in the registered event

Start the timer in the registered event

--- scripts/poo-tracker.coffee
+++ scripts/poo-tracker.coffee
@@ -28,6 +28,7 @@
       return if reply == null
       return res.send "Latest poo: #{reply}"

-  setInterval ->
-    checkRedisForShit()
-  , 1000
+  robot.on 'registered', (message) ->
+    setInterval ->
+      checkRedisForShit()
+    , 1000


fix checkboxes for boolean preferences

fix checkboxes for boolean preferences

--- scripts/preferences.coffee
+++ scripts/preferences.coffee
@@ -63,7 +63,10 @@
       panel.find('input').bind 'click', (event) ->
         input = $(this)
         key = input.attr('name')
-        exports.set key, input.val()
+        if input.is(':checkbox')
+          exports.set key, input.is(':checked')
+        else
+          exports.set key, input.val()

     panel.cssFadeIn()


Task sorting

Task sorting

--- scripts/views/tasks.coffee
+++ scripts/views/tasks.coffee
@@ -412,9 +412,33 @@
                 @$taskPreviewsList.empty()
                 @$taskPreviewsList.html $('

').addClass('lead').text 'No tasks yet.'
             else
+                getSortResultByKey = (a, b, getValueFunc) ->
+                    valA = getValueFunc a
+                    valB = getValueFunc b
+                    if a  b
+                        return 1
+                    else
+                        return 0
+
+                sortTaskPreviewsFunc = (a, b) ->
+                    if a.state == b.state
+                        return getSortResultByKey a, b, (v) -> v.updatedAt.getTime()
+                    else
+                        if a.isOpened()
+                            return -1
+                        if b.isOpened()
+                            return 1
+                        if a.isClosed() and b.isInitial()
+                            return -1
+                        if a.isInitial() and b.isClosed()
+                            return 1
+                        return 0
+
                 @$taskPreviewsList.empty()
-                sortedTaskPreviews = _.sortBy @taskPreviews, 'createdAt'
-                for taskPreview in sortedTaskPreviews
+                @taskPreviews.sort sortTaskPreviewsFunc
+                for taskPreview in @taskPreviews
                     categoriesList = ''
                     for categoryId in taskPreview.categories
                         taskCategory = _.findWhere @taskCategories, id: categoryId


Achievements: Updated ids to match the site

--- server/achievements.coffee
+++ server/achievements.coffee
@@ -26,43 +26,43 @@

 ACHIEVEMENTS = [
   {
-    id: 1,
+    id: 1
     name: "Taste of Victory"
     condition: "Win on PokeBattle for the first time"
     conditionFn: winCount(1)
   },
   {
-    id: 2,
+    id: 2
     name: "Centurian"
     condition: "Win 100 times"
     conditionFn: winCount(100)
   },
   {
-    id: 3,
+    id: 3
     name: "Jackpot!"
     condition: "Lucky 777 victories"
     conditionFn: winCount(777)
   },
   {
-    id: 4,
+    id: 4
     name: "Ladder Monster"
     condition: "Win a beastly 2500 matches"
     conditionFn: winCount(2500)
   },
   {
-    id: 5,
+    id: 5
     name: "Mile High Club"
     condition: "Win 52"
     conditionFn: winCount(5280)
   },
   {
-    id: 6,
+    id: 6
     name: "Better than Phil"
     condition: "More than 7086 victories"
     conditionFn: winCount(7087)
   },
   {
-    id: 7,
+    id: 7
     name: "KAKAROT!"
     condition: "What? More than 9000 victories?!"
     conditionFn: winCount(9001)
@@ -71,37 +71,37 @@
   # ladder ranking achievements go here

   {
-    id: 8,
+    id: 26
     name: "On a Roll!"
     condition: "Five in a row"
     conditionFn: streak(5)
   },
   {
-    id: 9,
+    id: 27
     name: "Perfect Ten!"
     condition: "10 in a row, very nice"
     conditionFn: streak(10)
   },
   {
-    id: 10,
+    id: 28
     name: "Incredible!"
     condition: "15 victories!"
     conditionFn: streak(15)
   },
   {
-    id: 11,
+    id: 29
     name: "Unreal!"
     condition: "20 victories in a row"
     conditionFn: streak(20)
   },
   {
-    id: 12,
+    id: 30
     name: "Impossible!"
     condition: "30 victories! Hopefully counting"
     conditionFn: streak(30)
   },
   {
-    id: 13,
+    id: 31
     name: "The One"
     condition: "50 in a row! PS: I love you <3"
     conditionFn: streak(50)


Fix resetYear

Fix resetYear

--- server/api/setupApi.coffee
+++ server/api/setupApi.coffee
@@ -352,7 +352,8 @@
             await checkin.upsert()
         res.json({ok: "OK"})

-    app.post '/api/moveUserToGroup/:userId/:groupName', ensureLoggedIn, wrap (req, res) ->
+    app.post '/api/
+/:userId/:groupName', ensureLoggedIn, wrap (req, res) ->
         if not req.user?.admin
             res.status(403).send('No permissions')
             return
@@ -388,7 +389,7 @@

         runForUser = (user) ->
             await groups.moveUserToGroup(adminUser, user._id, "unknown")
-            #await user.setUserList("unknown")
+            await user.setUserList("unknown")
             logger.info("Moved user #{user._id} to unknown group")

         users = await User.findAll()


Reset year api

Reset year api

--- server/api/setupApi.coffee
+++ server/api/setupApi.coffee
@@ -270,6 +270,25 @@
             await user.setUserList(req.params.groupName)
         res.send('OK')

+    app.post '/api/resetYear', ensureLoggedIn, wrap (req, res) ->
+        if not req.user?.admin
+            res.status(403).send('No permissions')
+            return
+        adminUser = await InformaticsUser.findAdmin()
+
+        runForUser = (userId) ->
+            await groups.moveUserToGroup(adminUser, userId, "unknown")
+            if req.params.groupName != "none"
+                await user.setUserList("unknown")
+            logger.info("Moved user #{user._id} to unknown group")
+
+        users = await User.findAll()
+        for user in users
+            if user.userList == "stud" or user.userList == "unknown"
+                continue
+            runForUser(user._id)
+        res.send('OK')
+
     app.get '/api/updateResults/:user', ensureLoggedIn, wrap (req, res) ->
         if not req.user?.admin
             res.status(403).send('No permissions')


Small improvements to #entries tag

Small improvements to #entries tag

* Support passing bucket, eg. `bucket="articles"`. TODO: Pass multiple buckets with |, negate with -, etc.
* Supports passing a where clause. TODO: Create a custom syntax that allows them to use variables here

--- server/lib/renderer.coffee
+++ server/lib/renderer.coffee
@@ -4,6 +4,7 @@
 config = require '../config'

 Entry = require '../models/entry'
+Bucket = require '../models/bucket'

 module.exports = (hbs) ->

@@ -11,36 +12,47 @@
   Swag.registerHelpers hbs.handlebars
   hbs.registerPartials config.buckets.templatePath

-  # Add the entries helper
-  hbs.registerAsyncHelper 'entries', (options, cb) ->
-    _.defaults options.hash,
-      bucket: ''
-      until: Date.now()
-      since: ''
-      limit: 10
-      skip: 0
-      sort_by: 'date_posted'
-      sort_dir: 'desc'
-      status: 'live'
+  Bucket.find {}, (err, buckets) ->

-    searchQuery = {}
-    searchQuery['bucket.slug'] = options.hash.bucket if options.hash.bucket
+    # Add the entries helper
+    hbs.registerAsyncHelper 'entries', (options, cb) ->
+      _.defaults options.hash,
+        bucket: ''
+        until: Date.now()
+        since: ''
+        limit: 10
+        skip: 0
+        sort: ''
+        sort_by: 'date_posted'
+        sort_dir: 'desc'
+        status: 'live'
+        find: ''

-    Entry.find()
-      .populate('bucket')
-      .populate('author')
-      .limit(options.hash.limit)
-      .skip(options.hash.skip)
-      .exec (err, pages) ->
-        console.log err if err
+      searchQuery = {}
+      bucketPath = path: 'bucket'

-        return cb options.inverse @ if pages.length is 0 or err
+      if options.hash.bucket
+        bucket = _.findWhere(buckets, slug: options.hash.bucket)
+        searchQuery['bucket'] = bucket._id if bucket

-        ret = []
-        for page in pages
-          try
-            ret.push options.fn page.toJSON()
-          catch e
-            console.log e.stack, arguments
+      if options.hash.where
+        searchQuery.$where = options.hash.where

-        cb ret.join('')
+      Entry.find(searchQuery)
+        .populate('bucket')
+        .populate('author')
+        .limit(options.hash.limit)
+        .skip(options.hash.skip)
+        .exec (err, pages) ->
+          console.log err if err
+
+          return cb options.inverse @ if pages?.length is 0 or err
+
+          ret = []
+          for page in pages
+            try
+              ret.push options.fn page.toJSON()
+            catch e
+              console.log e.stack, arguments
+
+          cb ret.join('')


Fix trailing space

Fix trailing space

--- server/models/event.coffee
+++ server/models/event.coffee
@@ -68,7 +68,7 @@
         date = momentTz(@start)

     date.tz User.timezone
-    dStr = date.format dateFormat
+    dStr = date.format dateFormat
     dStr += ' ' + User.timezone

     return dStr


Fix the bug uncovered by 7923268

Fix the bug uncovered by 7923268

--- server/publications.coffee
+++ server/publications.coffee
@@ -35,7 +35,7 @@
   ids = _.uniq(_.pluck(Post.all(fields: userId: 1), 'userId'))

   Author.find
-    id: $in: ids
+    _id: $in: ids
   ,
     fields:
       profile: 1


NEW: FullScreenApp now Configures ConfigRegistry

--- source/Art/Engine/FullScreenApp.coffee
+++ source/Art/Engine/FullScreenApp.coffee
@@ -1,6 +1,6 @@
 Foundation = require 'art-foundation'

-{merge, Promise, parseQuery, log} = Foundation
+{merge, Promise, parseQuery, log, ConfigRegistry} = Foundation
 {Meta, Link} = Foundation.Browser.DomElementFactories

 module.exports = class FullScreenApp
@@ -32,14 +32,17 @@

     log "Art.Engine.FullScreenApp: app ready"

-  @init: (config = {})->
+  @getDomReadyPromise: ->
     new Promise (resolve) =>
       document.onreadystatechange = =>
         if document.readyState == "interactive"
           @_domReady()
           resolve()

-      @writeDom config
+  @init: (config = {})=>
+    ConfigRegistry.configure config
+    @writeDom config
+    @getDomReadyPromise()

   @writeDom: ({title, styleSheets, scripts, fontFamilies, meta, link, manifest})->


Improve Grunt watch configuration

Improve Grunt watch configuration

--- grunt/config/watch.coffee
+++ grunt/config/watch.coffee
@@ -10,10 +10,16 @@
         spawn: false
       tasks: ['copy:app', 'coffee:server', 'express:dev']
     client:
-      files: ['<%= src %>/*.coffee', '<%= src %>/views/*.coffee']
+      files: [
+        '<%= src %>/*.coffee'
+        '<%= src %>/views/*.coffee'
+      ]
       tasks: ['copy:app']
     coffee2css:
-      files: ['<%= src %>/color.coffee']
+      files: [
+        'grunt/tasks/color2css.coffee'
+        '<%= src %>/color.coffee'
+      ]
       tasks: 'coffee2css'
     less:
       files: ['<%= styles %>/**/*.less']


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/game_object.coffee
+++ test/game_object.coffee
@@ -34,6 +34,7 @@
   equals o.closest([other, other2]), other

 test "#sendOrApply", ->
+  o = GameObject()

 test "[event] create", 2, ->


Fix JSON keys test, split into two tests

include() only looks at the first argument, so we were only testing the namespace of the response.

--- test/integration.coffee
+++ test/integration.coffee
@@ -60,8 +60,11 @@
     it 'responds with json', (done) ->
       request.get('/avatars/list')
         .expect('Content-Type', /json/)
+        .end(done)
+
+    it 'responds with a list of possible face parts', (done) ->
+      request.get('/avatars/list')
         .end (err, res) ->
-          jsonResponse = JSON.parse(res.text)
-          responseKeys = Object.keys(jsonResponse)
-          expect(responseKeys).to.include('face', 'eyes', 'mouth', 'nose')
+          faceParts = res.body.face
+          expect(faceParts).to.have.keys('eyes', 'mouth', 'nose')
           done()


Formatting / whitespace

Formatting / whitespace

--- test/listnr-test.coffee
+++ test/listnr-test.coffee
@@ -4,8 +4,10 @@
 assert = buster.assert
 refute = buster.refute

+
 createEl = (tag) ->
   document.createElement(tag)
+

 triggerKeypress = (el, keyCode) ->
   event = document.createEvent('Event')
@@ -13,9 +15,11 @@
   event.keyCode = keyCode
   el.dispatchEvent(event)

+
 triggerCombo = (el, combo) ->
   for key in combo.split('+')
     triggerKeypress(el, key.charCodeAt(0))
+

 buster.testCase 'Listnr',


Add test for https protocol

Add test for https protocol

--- test/reader_spec.coffee
+++ test/reader_spec.coffee
@@ -1,6 +1,8 @@
 expect = require('chai').expect
 fs     = require('fs')
+url    = require('url')
 http   = require('http')
+https  = require('https')
 Stream = require('stream')

 Reader = require('../lib/reader')
@@ -46,7 +48,7 @@
           expect(fullData).to.be.eql @dummy.toString()
           done()

-  context 'when reading from a server', ->
+  context 'when reading from an http server', ->
     before ->
       @address = '127.0.0.1'
       @port    = 1338
@@ -95,3 +97,45 @@
           expect(err).to.be.instanceof Error
           expect(err.message).to.be.eql 'http status 404: Not Found'
           done()
+
+  context 'when reading from a secure http server', ->
+    before ->
+      @address = '127.0.0.1'
+      @port    = 1338
+      @url     = "https://#{@address}:#{@port}"
+
+      @server = https.createServer
+        key: fs.readFileSync './test/fixtures/server.key'
+        cert: fs.readFileSync './test/fixtures/server.crt'
+      , (req, res) =>
+        if req.url.indexOf('404') > -1
+          res.writeHead 404
+          res.end()
+        else
+          res.writeHead 200, 'Content-Type': 'text/html'
+          res.end @dummy
+
+      @server.listen @port, @address
+
+    after ->
+      @server.close()
+
+    describe '#getUrlStream', ->
+      beforeEach (done) ->
+        options = url.parse @url
+        options.ca = fs.readFileSync './test/fixtures/server.crt'
+        options.rejectUnauthorized = false
+
+        @reader.getUrlStream options, (err, @stream) =>
+          done()
+
+      it 'returns a stream', ->
+        expect(@stream).to.be.instanceof Stream
+
+      it 'contains dummy data', (done) ->
+        fullData = ''
+        @stream.on 'data', (data) ->
+          fullData += data.toString()
+        @stream.on 'end', =>
+          expect(fullData).to.be.eql @dummy.toString()
+          done()


Get media resources directly, add text about image params

--- app/pages/settings/profile.cjsx
+++ app/pages/settings/profile.cjsx
@@ -10,39 +10,50 @@
 module.exports = React.createClass
   displayName: 'CustomizeProfilePage'

+  getDefaultProps: ->
+    user: null
+
   getInitialState: ->
     avatarError: null
     headerError: null

   render: ->
-    @getAvatarSrc ?= @props.user.get 'avatar'
+    @avatarGet ?= @props.user.get 'avatar'
       .then ([avatar]) ->
-        avatar.src
+        avatar
       .catch ->
-        ''
+        null

-    @getHeaderSrc ?= @props.user.get 'profile_header'
+    @headerGet ?= @props.user.get 'profile_header'
       .then ([header]) ->
-        header.src
+        header
       .catch ->
-        ''
+        null

-

-

-
Change avatar

-        <PromiseRenderer promise={@getAvatarSrc}>{(avatarSrc) =>
-          placeholder =
Drop an image here

-          <ImageSelector maxSize={MAX_AVATAR_SIZE} ratio={1} defaultValue={avatarSrc} placeholder={placeholder} onChange={@handleMediaChange.bind(this, 'avatar')} />
+

+

+
Change avatar

+        <PromiseRenderer promise={@avatarGet}>{(avatar) =>
+

+
Drop an image here (square, less than {Math.floor MAX_AVATAR_SIZE / 1000} KB)

+

+              <ImageSelector maxSize={MAX_AVATAR_SIZE} ratio={1} defaultValue={avatar?.src} onChange={@handleMediaChange.bind(this, 'avatar')} />
+

+

         }</PromiseRenderer>
         {if @state.avatarError

{@state.avatarError.toString()}
}

-
-

-
Change profile header

-        <PromiseRenderer promise={@getHeaderSrc}>{(headerSrc) =>
-          placeholder =
Drop an image here

-          <ImageSelector maxSize={MAX_HEADER_SIZE} defaultValue={headerSrc} placeholder={placeholder} onChange={@handleMediaChange.bind(this, 'profile_header')} />
+

+

+
Change profile header

+        <PromiseRenderer promise={@headerGet}>{(header) =>
+

+
Drop an image here (any dimensions, less than {Math.floor MAX_HEADER_SIZE / 1000} KB)

+

+              <ImageSelector maxSize={MAX_HEADER_SIZE} defaultValue={header?.src} onChange={@handleMediaChange.bind(this, 'profile_header')} />
+

+

         }</PromiseRenderer>
         {if @state.headerError

{@state.headerError.toString()}
}
@@ -61,7 +72,7 @@
         putFile resource.src, file
       .then =>
         @props.user.uncacheLink type
-        @["#{type}SrcGet"] = null # Uncache the local request so that rerendering makes it again.
+        @["#{type}Get"] = null # Uncache the local request so that rerendering makes it again.
         @props.user.emit 'change'
       .catch (error) =>
         newState = {}


Make sure selectors only select selectable reactives

--- fetching.coffee
+++ fetching.coffee
@@ -542,8 +542,8 @@

             else if fieldOrReactiveName of modelClass.reactiveSpecs
                 reactiveSpec = modelClass.reactiveSpecs[fieldOrReactiveName]
-                if not reactiveSpec.denorm
-                    throw new Error "Can't fetch with a selector on a non-denorm
+                if not reactiveSpec.selectable
+                    throw new Error "Can't fetch with a selector on a non-selectable
                         reactive: #{modelClass.name}.#{fieldOrReactiveName}"
                 reactiveSelectorKey = ["_reactives.#{fieldOrReactiveName}.val"].concat(
                     selectorKeyParts[1...]


Use building sprites as tiles

Use building sprites as tiles

--- app/scripts/scenery/City.coffee
+++ app/scripts/scenery/City.coffee
@@ -186,10 +186,16 @@
   @addBackground(0, @level.visibleHeight - 310, c, .5)
   @addBackground(0, @level.visibleHeight - 310, d, .5)

-generator.defineElement 'cityFront', ->
-  bb = Crafty.e('2D, WebGL, bigBuilding, ColorEffects, RiggedExplosion').attr(z: -20).crop(1, 1, 446, 510)
+generator.defineElement 'cityFrontTop', ->
+  bb = Crafty.e('2D, WebGL, bigBuildingTop, ColorEffects, RiggedExplosion').attr(z: -20).crop(1, 1, 446, 8 * 32)
   bb.colorOverride('#001fff', 'partial')
   @add(0, @level.visibleHeight - 700, bb)
+
+  for i in [0...3]
+    floor = Crafty.e('2D, WebGL, bigBuildingLayer, ColorEffects').attr(z: -20).crop(1, 1, 446, 2 * 32)
+    floor.attr(x: bb.x, y: bb.y + bb.h + (i * 64))
+    bb.attach(floor)
+
   bb.bind('BigExplosion', ->
     return if @buildingExploded
     if @x + @w > -Crafty.viewport.x and @x < -Crafty.viewport.x + Crafty.viewport.width
@@ -215,12 +221,28 @@
         .tween({ y: @y + 500}, 3000, 'easeInQuad')

       @sprite(30, 13)
+      for e in @_children
+        e.sprite(30, 21) if e.has('bigBuildingLayer')
       @buildingExploded = yes
   )

+generator.defineElement 'cityFront', ->
+  bb = Crafty.e('2D, WebGL, bigBuildingTop, ColorEffects').attr(z: -20).crop(1, 1, 446, 8 * 32)
+  bb.colorOverride('#001fff', 'partial')
+  @add(0, @level.visibleHeight - 700, bb)
+
+  height = 5
+  for i in [0...height]
+    floor = Crafty.e('2D, WebGL, bigBuildingLayer, ColorEffects').attr(z: -20).crop(1, 1, 446, 2 * 32)
+    floor.attr(x: bb.x, y: bb.y + bb.h + (i * 64))
+    bb.attach(floor)
+
+  bottom = Crafty.e('2D, WebGL, bigBuildingBottom, ColorEffects').attr(z: -20).crop(1, 1, 446, 120)
+  bottom.attr(x: bb.x, y: bb.y + bb.h + (height * 64))
+  bb.attach(bottom)
+
 generator.defineElement 'cityFrontBlur', ->
-  h = 768
-  @addBackground(200, @level.visibleHeight - 550, Crafty.e('2D, WebGL, bigBuilding').crop(1, 1, 446, 510).attr(w: 768, h: h, z: 50, lightness: .4), 1.5)
+  @addBackground(200, @level.visibleHeight - 550, Crafty.e('2D, WebGL, bigBuildingTop').crop(1, 1, 446, 8 * 32).attr(w: 768, h: 384, z: 50, lightness: .4), 1.5)

 generator.defineElement 'city-bridge', ->
   bg = Crafty.e('2D, WebGL, cityLayer2, Collision, SunBlock, Horizon')
@@ -267,8 +289,13 @@
           cityLayer2: [0, 9, 12, 8]
           cityDistance: [32, 6, 8, 7]
           cityDistanceBase: [38, 6, 8, 7]
-          bigBuilding: [16, 13, 16, 14]
-          bigBuildingBroken: [30, 13, 16, 14]
+
+          bigBuildingTop: [16, 13, 16, 8]
+          bigBuildingBrokenTop: [30, 13, 16, 8]
+
+          bigBuildingLayer: [16, 21, 16, 2]
+          bigBuildingBottom: [16, 25, 16, 4]
+
           glass: [12, 9, 4, 3]
           cloud: [16, 9, 8, 3]
           shadow: [16, 12, 2, 1]
@@ -321,7 +348,7 @@
       lightness: 0.5
       #blur: 6.0
     ).crop(0, 1, 512, 95)
-    @addBackground(0, @level.visibleHeight - 8, frontWave, 1.25)
+    @addBackground(0, @level.visibleHeight - 18, frontWave, 1.25)
     frontWave.originalY = frontWave.y

     @addElement 'cloud'
@@ -605,7 +632,7 @@
   generate: ->
     super

-    @addElement 'cityFront'
+    @addElement 'cityFrontTop'
     @addElement 'cityFrontBlur'
     @addElement 'water'

@@ -656,11 +683,11 @@
     @addElement 'city'

     h = 150
-    @add(0, @level.visibleHeight - 100, Crafty.e('2D, WebGL, Color, SunBlock').attr(w: @delta.x, h: h, z: -10).color('#505050'))
+    @add(0, @level.visibleHeight - 100, Crafty.e('2D, WebGL, Color, SunBlock').attr(w: @delta.x, h: h, z: -25).color('#505050'))
     h2 = 400
-    @add(0, @level.visibleHeight - 100 + h, Crafty.e('2D, WebGL, Color, SunBlock').attr(w: @delta.x, h: h2, z: -10).color('#202020'))
+    @add(0, @level.visibleHeight - 100 + h, Crafty.e('2D, WebGL, Color, SunBlock').attr(w: @delta.x, h: h2, z: -25).color('#202020'))
     h = 150
-    @add(0, @level.visibleHeight - 100 + h + h2, Crafty.e('2D, WebGL, Color, Solid, SunBlock').attr(w: @delta.x, h: h + h2, z: -10).color('#505050'))
+    @add(0, @level.visibleHeight - 100 + h + h2, Crafty.e('2D, WebGL, Color, Solid, SunBlock').attr(w: @delta.x, h: h + h2, z: -25).color('#505050'))

 generator.defineBlock class extends @Game.LevelScenery


Fixed bug in hightlight function.

Fixed bug in hightlight function.

--- jquery.immybox.coffee
+++ jquery.immybox.coffee
@@ -13,8 +13,8 @@
       i = text.toLowerCase().indexOf query.toLowerCase()
       if i >= 0 # should always be since we only attempt to highlight things that passed the filterFn
         matchedText = text[i...i+query.length]
-        [head, tail] = text.split(matchedText)
-        "#{head}#{matchedText}#{tail}"
+        [head, tail...] = text.split(matchedText)
+        "#{head}#{matchedText}#{tail.join matchedText}"
       else
         text


Syntax Highlighting: Apply recent changes from DartCode.

- Remove obsolete "interface" keyword.
- Remove incorrect support.constant regex. (Dart-Code/Dart-Code#309)
- Recognize $ character in identifiers. (Dart-Code/Dart-Code#309)
- Highlight more characters within interpolated strings. (Dart-Code/Dart-Code#310)


Include trailing quote-marks in matched substrings

--- grammars/roff.cson
+++ grammars/roff.cson
@@ -199,7 +199,7 @@
 		beginCaptures:
 			0: {name: "entity.function.name.roff"}
 		patterns: [{
-			begin: "\"[^\"]*|\\S+"
+			begin: "\"[^\"]*\"|\\S+"
 			end: "[ \t]+(\"[^\"]*\"|\\S+)"
 			beginCaptures:
 				0: {name: "markup.bold.roff"}


First CoffeeScript version of emulator

First CoffeeScript version of emulator

--- emulation/atom-emulator.coffee
+++ emulation/atom-emulator.coffee
@@ -1,256 +1,238 @@
-var Buffer = function() {
-  this.$textArea = null;
-}
-Buffer.prototype.setText = function(txt) {
-  return this.$textArea.val(txt);
-};
-
-Buffer.prototype.getText = function() {
-  return this.$textArea.val();
-};
-Buffer.prototype.getSelectionRange = function() {
-  return new atom.Range( this.positionForCharacterIndex( this.$textArea.get(0).selectionStart )
-                       , this.positionForCharacterIndex( this.$textArea.get(0).selectionEnd )
-                       )
-};
-
-Buffer.prototype.setSelectionRange = function(range) {
-  range = Range.fromObj(range);
-  this.$textArea.get(0).selectionStart = this.characterIndexForPosition( range.start );
-  this.$textArea.get(0).selectionEnd = this.characterIndexForPosition( range.end );
-};
-
-Buffer.prototype.delete = function(range) {
-  range = Range.fromObj(range);
-  const rangeStartIx = this.characterIndexForPosition( range.start );
-  const rangeEndIx = this.characterIndexForPosition( range.end );
-  const rangeLength = rangeEndIx - rangeStartIx;
-  var selStartIx = this.$textArea.get(0).selectionStart;
-  var selEndIx = this.$textArea.get(0).selectionEnd;
-  selStartIx = selStartIx <= rangeStartIx ? selStartIx
-                                          : selStartIx < rangeEndIx ? rangeStartIx : selStartIx - rangeLength
-  selEndIx = selEndIx <= rangeStartIx ? selEndIx
-                                      : selEndIx < rangeEndIx ? rangeStartIx : selEndIx - rangeLength
-
-  this.setTextInRange(range, '');
-  this.$textArea.get(0).selectionStart = selStartIx
-  this.$textArea.get(0).selectionEnd = selEndIx
-};
-
-Buffer.prototype.setTextInRange = function(range, text) {
-  range = Range.fromObj(range);
-  const rangeStartIx = this.characterIndexForPosition( range.start );
-  const rangeEndIx = this.characterIndexForPosition( range.end );
-  const bufferText = this.$textArea.val();
-  this.setText(bufferText.slice(0,rangeStartIx)+text+bufferText.slice(rangeEndIx,bufferText.length));
-};
-
-Buffer.prototype.positionForCharacterIndex = function(ix) {
-  const bufferText = this.$textArea.val();
-  var row = 0;
-  var col = 0;
-  for (var i=0; i<ix; i++) {
-    if (bufferText[i]==='\n') {
-      row++;
-      col = 0;
-    } else {
-      col++;
-    }
-  }
-  return new atom.Point(row,col);
-};
-
-// Note: does note handle column position past line end (returned ix is on following lines)
-Buffer.prototype.characterIndexForPosition = function(pos) {
-  pos = Point.fromObj(pos)
-  const bufferText = this.$textArea.val();
-  var ix = 0;
-  const row = pos.row;
-  const col = pos.column;
-  for (var r=0; r<row; r++) {
-    const nextNewline = bufferText.indexOf('\n', ix);
-    if (nextNewline < 0) {
-      return bufferText.length;
-    } else {
-      ix = nextNewline+1;
-    }
-  }
-  return Math.min(ix+col, bufferText.length);
-}
-
-var Range = function(start, end) {
-  this.start = start;
-  this.end = end;
-}
-
-// Not in prototype, because fromObj is a static method
-Range.fromObj = function(obj) {
-  if (obj instanceof this) {
-    return obj;
-  } else if (Array.isArray(obj)) {
-    return new atom.Range( obj[0], obj[1] );
-  } else {
-   console.error('rangeFromObj parameter is Range nor Array: ');
-   console.dir(obj);
-  }
-}
-
-var Point = function(row, column) {
-  this.row = row;
-  this.column = column;
-};
-Point.fromObj = function(obj) {
-  if (obj instanceof atom.Point) {
-    return obj;
-  } else if (Array.isArray(obj)) {
-    return new atom.Point( obj[0], obj[1] );
-  } else {
-   console.error('fromObj parameter is Point nor Array: ');
-   console.dir(obj);
-  }
-}
-
-atom = {
-  Range: Range,
-  Point: Point,
-
-  notifications: {
-    hideTimer: null,
-    addNotification: function(msg, bgColor, duration) {
-      $notification = $("#atom-Emulator .notification");
-      $notification.text(msg);
-      $notification.css('background-color', bgColor);
-      $notification.hide();
-      $notification.fadeIn(50);
-      if (this.hideTimer)
-        clearTimeout(this.hideTimer);
-      this.hideTimer = setTimeout(function() {
-        $notification.fadeOut(100);
-      }, duration);
-    },
-    addInfo: function(msg) {
-      this.addNotification(msg, '9ed2ff', 3000);
-    },
-    addSuccess: function(msg) {
-      this.addNotification(msg, '99dba6', 3000);
-    },
-    addWarning: function(msg) {
-      this.addNotification(msg, 'e8d0a1', 3000);
-    },
-    addError: function(msg) {
-      this.addNotification(msg, 'ecb1b1', 4000);
-    }
-  },
-  clipboard: {
-    clipboard: {text: null, meta: null},
-
-    readWithMetadata: function() {
-      return this.clipboard;
-    },
-    write: function(text, metadata) {
-      this.clipboard = {text: text, metadata: metadata ? metadata : null};
-    }
-  },
-
-  workspace: {
-    getActiveTextEditor: function()  {
-      return this.editor;
-    },
-
-    editor: {
-      getGrammar: function() {
-        return this.grammar;
-      },
-      grammar: {
-        tokenizeLine: function() {
-          return {tags: [], ruleStack: []}
-        },
-
-        registry: {
-          idsByScope: {}
-        }
-      },
-
-      getBuffer: function() {
-        return this.buffer;
-      },
-      setCursorBufferPosition: function(pos) {
-        this.setSelectedBufferRange([pos,pos])
-      },
-      getSelectedBufferRange: function() {
-        return this.buffer.getSelectionRange();
-      },
-      setSelectedBufferRange: function(range) {
-        this.buffer.setSelectionRange(range);
-      },
-      getText: function() {
-        return this.buffer.getText();
-      },
-      setText: function(txt) {
-        this.buffer.setText(txt);
-      },
-      buffer: new Buffer()
-    }
-  },
-
-  init: function($atomEmulator) {
-    var $textArea = $atomEmulator.find('textarea');
-    atom.workspace.editor.buffer.$textArea = $textArea;
-    $textArea.attr('spellcheck', false);
-    $textArea.keydown((this.keyHandler).bind(this));
-    setTimeout(function () {
-      atom.notifications.addSuccess('Atom emulator initialized')
-    }, 300); // Small delay to see it appear after page has loaded
-  },
-
-  keyHandler: function(event) {
-    // console.log(event.charCode);
-    // console.log(event.keyCode);
-    // console.log(event.altKey);
-    // console.log(event.metaKey);
-    // console.log(event.ctrlKey);
-    if (event.ctrlKey && event.charCode == 115) { // CTRL-S, just for testing
-      this.listSelect();
+class Range
+  start: null
+  end: null
+
+  constructor: (start, end) ->
+    @start = start
+    @end = end
+
+# Not in prototype, because fromObj is a static method
+Range.fromObj = (obj) ->
+  if obj instanceof this
+    return obj
+  else if Array.isArray(obj)
+    return new (atom.Range)(obj[0], obj[1])
+  else
+    console.error 'rangeFromObj parameter is Range nor Array: '
+    console.dir obj
+
+class Point
+  row: null
+  column: null
+
+  constructor: (row, column) ->
+    @row = row
+    @column = column
+
+# Not in prototype, because fromObj is a static method
+Point.fromObj = (obj) ->
+  if obj instanceof atom.Point
+    return obj
+  else if Array.isArray(obj)
+    return new (atom.Point)(obj[0], obj[1])
+  else
+    console.error 'fromObj parameter is Point nor Array: '
+    console.dir obj
+
+class Buffer
+  $textArea = null
+  constructor: ($textArea) ->
+    @$textArea = $textArea
+
+  setText: (txt) ->
+    @$textArea.val(txt)
+
+  getText: ->
+    @$textArea.val();
+
+  getSelectionRange: ->
+      new Range( @positionForCharacterIndex(@$textArea.get(0).selectionStart)
+               , @positionForCharacterIndex(@$textArea.get(0).selectionEnd)
+               )
+
+  setSelectionRange: (range) ->
+    range = Range.fromObj(range)
+    @$textArea.get(0).selectionStart = @characterIndexForPosition range.start
+    @$textArea.get(0).selectionEnd = @characterIndexForPosition range.end
+
+  delete: (range) ->
+    range = Range.fromObj range
+    rangeStartIx = @characterIndexForPosition range.start
+    rangeEndIx = @characterIndexForPosition range.end
+    rangeLength = rangeEndIx - rangeStartIx
+    selStartIx = @$textArea.get(0).selectionStart
+    selEndIx = @$textArea.get(0).selectionEnd
+    # TODO: fix layout
+    selStartIx = if selStartIx <= rangeStartIx then selStartIx else
+                                                    if selStartIx < rangeEndIx then rangeStartIx else
+                                                                                selStartIx - rangeLength
+    selEndIx = if selEndIx <= rangeStartIx then selEndIx else if selEndIx < rangeEndIx then rangeStartIx else selEndIx - rangeLength
+    @setTextInRange range, ''
+    @$textArea.get(0).selectionStart = selStartIx
+    @$textArea.get(0).selectionEnd = selEndIx
+
+  setTextInRange: (range, text) ->
+    range = Range.fromObj range
+    rangeStartIx = @characterIndexForPosition range.start
+    rangeEndIx = @characterIndexForPosition range.end
+    bufferText = @$textArea.val()
+    @setText (bufferText.slice 0, rangeStartIx) + text + (bufferText.slice rangeEndIx, bufferText.length)
+
+  positionForCharacterIndex: (ix) ->
+    bufferText = @$textArea.val()
+    row = 0
+    col = 0
+    i = 0
+    while i < ix
+      if bufferText[i] == '\n'
+        row++
+        col = 0
+      else
+        col++
+      i++
+    new Point(row, col)
+
+  # Note: does note handle column position past line end (returned ix is on following lines)
+  characterIndexForPosition: (pos) ->
+    pos = Point.fromObj(pos)
+    bufferText = @$textArea.val()
+    ix = 0
+    row = pos.row
+    col = pos.column
+    r = 0
+    while r < row
+      nextNewline = bufferText.indexOf '\n', ix
+      if nextNewline < 0
+        return bufferText.length
+      else
+        ix = nextNewline + 1
+      r++
+    Math.min ix + col, bufferText.length
+
+
+module.exports = atom =
+  Range: Range
+  Point: Point
+  Buffer: Buffer
+
+  notifications:
+    hideTimer: null
+    addNotification: (msg, bgColor, duration) ->
+      $notification = $('#atom-Emulator .notification')
+      $notification.text msg
+      $notification.css 'background-color', bgColor
+      $notification.hide()
+      $notification.fadeIn 50
+      if @hideTimer
+        clearTimeout @hideTimer
+      @hideTimer = setTimeout((->
+        $notification.fadeOut 100
+        return
+      ), duration)
+      return
+    addInfo: (msg) ->
+      @addNotification msg, '9ed2ff', 3000
+      return
+    addSuccess: (msg) ->
+      @addNotification msg, '99dba6', 3000
+      return
+    addWarning: (msg) ->
+      @addNotification msg, 'e8d0a1', 3000
+      return
+    addError: (msg) ->
+      @addNotification msg, 'ecb1b1', 4000
+      return
+  clipboard:
+    clipboard:
+      text: null
+      meta: null
+    readWithMetadata: ->
+      @clipboard
+    write: (text, metadata) ->
+      @clipboard =
+        text: text
+        metadata: if metadata then metadata else null
+      return
+  workspace:
+    getActiveTextEditor: ->
+      @editor
+    editor:
+      getGrammar: ->
+        @grammar
+      grammar:
+        tokenizeLine: ->
+          {
+            tags: []
+            ruleStack: []
+          }
+        registry: idsByScope: {}
+      getBuffer: ->
+        @buffer
+      setCursorBufferPosition: (pos) ->
+        @setSelectedBufferRange [
+          pos
+          pos
+        ]
+        return
+      getSelectedBufferRange: ->
+        @buffer.getSelectionRange()
+      setSelectedBufferRange: (range) ->
+        @buffer.setSelectionRange range
+        return
+      getText: ->
+        @buffer.getText()
+      setText: (txt) ->
+        @buffer.setText txt
+        return
+      buffer: new Buffer
+  init: ($atomEmulator) ->
+    $textArea = $atomEmulator.find('textarea')
+    atom.workspace.editor.buffer.$textArea = $textArea
+    $textArea.attr 'spellcheck', false
+    $textArea.keydown @keyHandler.bind(this)
+    setTimeout (->
+      atom.notifications.addSuccess 'Atom emulator initialized'
+    ), 300
+    # Small delay to see it appear after page has loaded
+    return
+  keyHandler: (event) ->
+    # console.log(event.charCode);
+    # console.log(event.keyCode);
+    # console.log(event.altKey);
+    # console.log(event.metaKey);
+    # console.log(event.ctrlKey);
+    if event.ctrlKey and event.charCode == 115
+      # CTRL-S, just for testing
+      @listSelect()
       return false
-    }
-    if (event.altKey && !event.shiftKey &&
-        (event.metaKey && !event.ctrlKey) || (event.ctrlKey && !event.metaKey) ) {
-      switch (event.keyCode) {
-        case 83:
-          this.listSelect();
-          break;
-        case 88:
-          this.listCut();
-          break;
-        case 67:
-          this.listCopy();
-          break;
-        case 86:
-          this.listPaste();
-        default:
-          return true;
-      }
-      return false;
-    }
-  },
-
-  listSelect: function() {
-    console.log('list-select');
-    ListEdit.selectCmd();
-  },
-
-  listCut: function() {
-    console.log('list-cut');
-    ListEdit.cutCmd();
-  },
-
-  listCopy: function() {
-    console.log('list-copy');
-    ListEdit.copyCmd();
-  },
-
-  listPaste: function() {
-    console.log('list-paste');
-    ListEdit.pasteCmd();
-  }
-}
+    if event.altKey and !event.shiftKey and event.metaKey and !event.ctrlKey or event.ctrlKey and !event.metaKey
+      switch event.keyCode
+        when 83
+          @listSelect()
+        when 88
+          @listCut()
+        when 67
+          @listCopy()
+        when 86
+          @listPaste()
+        else
+          return true
+      return false
+    return
+  listSelect: ->
+    console.log 'list-select'
+    ListEdit.selectCmd()
+    return
+  listCut: ->
+    console.log 'list-cut'
+    ListEdit.cutCmd()
+    return
+  listCopy: ->
+    console.log 'list-copy'
+    ListEdit.copyCmd()
+    return
+  listPaste: ->
+    console.log 'list-paste'
+    ListEdit.pasteCmd()
+    return
+###


show current interpretation of programs

show current interpretation of programs

Needs to do a better job for the current program

--- graphics.coffee
+++ graphics.coffee
@@ -22,9 +22,9 @@
       x: 10
       y: @mode_dims.y+@mode_dims.height
       width: @block*14
-      height: @block*8
+      height: @block*6
       Wi: 14
-      Hi: 8
+      Hi: 6

     @message_pos =
       x: @board_dims.x + @board_dims.width/2
@@ -447,23 +447,41 @@
     for label in @program_labels
       program = @game_state.programs[label.id]
       if xi + program.code.length + 2 >= @clues_dims.Wi
-        @ctx.translate(-xi*@block, @block*2)
+        @ctx.translate(-xi*@block, @block*1.5)
         xi = 0
-        yi += 2
+        yi += 1.5
+
+      @ctx.strokeStyle = 'white'

       @ctx.save()
-      @ctx.strokeStyle = 'white'
       @ctx.translate(.5, .5+@block*.125)
       @renderLetter(label.letter, @block*.75)
       @ctx.restore()

       @ctx.translate(@block, 0)
       xi += 1
+
+      mode = @game_state.current_mode

       for idx in [0...program.code.length]
         command = program.code.charAt(idx)
         @ctx.fillStyle = @instruction_colors[command]
         @ctx.fillRect(0,0,bs,bs)
+
+        if mode != 'unknown'
+          action = mode.lookup[command]
+
+          @ctx.translate(bs/2+.5, bs/2+.5)
+          if action?
+            @renderCommand(mode.lookup[command], @block)
+
+            if mode.lookup[command].type == 'mode'
+              mode = @game_state.modes[mode.lookup[command].idx]
+          else
+            @renderShape('question', @block/2)
+            mode = 'unknown'
+          @ctx.translate(-bs/2-.5, -bs/2-.5)
+
         @ctx.translate(@block, 0)

         xi += 1


fix selector

fix selector

--- keymaps/refactor.cson
+++ keymaps/refactor.cson
@@ -1,4 +1,4 @@
-'.atom-text-editor:not(.mini)':
+'atom-text-editor:not(.mini)':
   'ctrl-alt-r': 'refactor:rename'
   'enter': 'refactor:done'
   # "'": 'refactor:singlequote'


Add ctrl-[ keymapping to leave visual mode.

Sorry, missed this one yesterday.

--- keymaps/vim-mode.cson
+++ keymaps/vim-mode.cson
@@ -6,6 +6,7 @@
 '.vim-mode.visual-mode:not(.mini)':
   'escape': 'vim-mode:activate-command-mode'
   'ctrl-c': 'vim-mode:activate-command-mode'
+  'ctrl-[': 'vim-mode:activate-command-mode'
   'h': 'vim-mode:select-left'
   'l': 'vim-mode:select-right'
   'j': 'vim-mode:move-down'


Add a Framer logo in the center of the loading circle.

--- framer/Extras/Preloader.coffee
+++ framer/Extras/Preloader.coffee
@@ -16,6 +16,7 @@
 		@context.run =>

 			@cover = new Layer
+				size: 200
 				backgroundColor: "white"

 			@progressIndicator = new CircularProgressComponent
@@ -25,9 +26,17 @@
 			@progressIndicator.railsColor = Color.grey(0, 0.1)
 			@progressIndicator.progressColor = Color.grey(0, 0.8)

+			@brand = new Layer
+				width: 48
+				height: 72
+				parent: @cover
+				backgroundColor: null
+			@brand.style["background-image"] = "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAACQCAYAAAD3Cm4hAAAABGdBTUEAALGPC/xhBQAAA5xJREFUeAHt3O9t2zAQh2EpyBbtUp2k3SDOBu2nbpHu0FmaOVzRDv0ntmSJPJJ3vJeAQcIOeNLvCekPMjj+ft/vhmF4mV60BgmMoSYIDZL/KHkAAEEBAAhtEE4rIJZnO4pJ1OlvAEJZEOqEH6rcBQgfgBBSKN9mAUJpEBoDgKAAAISyCItb0GVptqPLNOTGqwFCSRDkgo8zbQIAIcYm128GAEEu/DBTEgAIcgjJACDIIGQBgJCPkA0AQh6CCAAI6QhiACCkIYgCgLAdQRwAhG0IRQBAWI9QDACEdQhFAUB4jFAcAIRlhCoAIMwjVAMA4T5CVQAQbhGqA4BwjdAEAIQzQjMAEI4ITQFAyHgkeV5E+SPPv7ZovgIin1cENQBetyNVAB4R1AF4Q1AJ4AlBLYAXBNUAHhDUA/SOYAKgZwQzAL0imALoEcEcQG8IJgF6QjAL0AvC+OVtvw83Y7WN4/D679u4s3r9T1YvPF739O/z8vXP4dCp+Jap3jxASNsyQhcAlhG6AbCK0BWARYTuAKwhdAlgCaFbACsIXQNYQOgeQDuCCwDNCG4AtCK4AtCI4A5AG4JLAE0IbgG0ILgG0IDgHqA1AgBBYGqtHuoAcMy/GQIAFwBhWHslAPAJoDYCAHcAaiIAMANQCwGABYAaCAA8ACiNAMAKgJIIAKwEKIUAwAaAEggAbASQRgAgAUASAYBEACkEADIAJBAAyATIRQBAACAHAQAhgFQEAAQBUhAAEAbYigBAAYAtCAAUAliLAEBBgDUIABQGeIQAQAWAJQQAKgHMIQBQEeAeAgCVAT4jPDeoT8kpgY9f4A3P73+H12E68sVsKr+mE4MMt6fh53TY0XTokeF7MH3px+8AEJohnr+EQWiCcAYI5UGojnANAIICABCqItyugFie7SgmUbSfBwhlQSgafph8GSD8BQghhWLtMUAoDUJjABAUAIBQBGHdFnRZmu3oMo3s8XaAUBKE7ODjBGkAIMT8svt0ABCyww8T5AGAkI2QDwBCFoIMAAjJCHIAICQhyAKAsBlBHgCETQhlAEBYjVAOAIRVCGUBQHiIUB4AhEWEOgAgzCLUAwDhLkJdABBuEOoDgHCF0AYAhBNCOwAQDghtAUAQeCBzWkwZA8fPmNuvgOjmFEEPgNPtSBeAQwR9AM4QdAI4QtAL4ARBN4ADBP0AnSPYAOgYwQ5Apwi2ADpFCLdlr/3Y74bv06EvtIYJBATj7T+6xt5DGDRVPAAAAABJRU5ErkJggg==')"
+
 			do layout = =>
 				@cover.frame = Canvas
 				@progressIndicator.point = Align.center
+				@brand.point = Align.center

 			Canvas.onResize(layout)


nannka iroiro

nannka iroiro

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -6,7 +6,6 @@
 	ts: './src/**/*.ts'
 	webRes: './src/web/statics/resources/**/**'
 	webViews: './src/web/views/**/*.jade'
-	webCommon: './src/web/common/**'

 gulp.task 'clean', del.bind(null, ['./bin/**'])

@@ -29,15 +28,9 @@
 	gulp.src paths.webViews
 		.pipe gulp.dest './bin/web/views'

-gulp.task 'build-web-common', ->
-	gulp.src paths.webCommon
-		.pipe gulp.dest './bin/web/common'
-		.pipe gulp.dest './bin/web/statics/resources/common'
-
-gulp.task 'build', ['build-web-common', 'build-ts', 'build-web-res', 'build-web-views']
+gulp.task 'build', ['build-ts', 'build-web-res', 'build-web-views']

 gulp.task 'watch', ['build'], ->
-	gulp.watch paths.webCommon, ['build-web-common']
 	gulp.watch paths.ts, ['build-ts']
 	gulp.watch paths.webRes, ['build-web-res']
 	gulp.watch paths.webViews, ['build-web-views']


trim trailing whitespace

trim trailing whitespace

reorganize require calls alphabetically

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -1,11 +1,11 @@
 browserify   = require 'browserify'
+browserSync  = require 'browser-sync'
 chalk        = require 'chalk'
 CSSmin       = require 'gulp-minify-css'
 ecstatic     = require 'ecstatic'
 gulp         = require 'gulp'
 gutil        = require 'gulp-util'
 jade         = require 'gulp-jade'
-browserSync  = require 'browser-sync'
 path         = require 'path'
 prefix       = require 'gulp-autoprefixer'
 prettyTime   = require 'pretty-hrtime'
@@ -92,8 +92,8 @@
 gulp.task 'server', ->
   browserSync
     port:      9001
-    server:
-      baseDir: './public'
+    server:
+      baseDir: './public'

 gulp.task 'watch', ->
   gulp.watch config.templates.watch, ['templates']


Update watch file extention

Update watch file extention

`scss` to `sass`

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -45,5 +45,5 @@
   gulp.watch 'src/**/*.coffee', ['build:coffee']
   gulp.watch 'src/**/*.ts', ['build:ts']
   gulp.watch 'src/**/*.jade', ['build:jade']
-  gulp.watch 'src/styles/**/*.scss', ['build:css']
+  gulp.watch 'src/styles/**/*.sass', ['build:css']
   gulp.watch 'lib/**/*', ['build:web']


chore(build): add initial less sourceMap support

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -138,10 +138,16 @@
 		.pipe jade options
 		.pipe gulp.dest tempDirectory

+console.log path.resolve tempDirectory
+
 gulp.task 'less', ['copy:temp'], ->
+	options =
+		sourceMap: true
+		sourceMapBasepath: path.resolve path.join tempDirectory, 'styles'
+
 	gulp
 		.src 'styles/styles.less', cwd: tempDirectory
-		.pipe less()
+		.pipe less options
 		.pipe gulp.dest 'styles/', cwd: tempDirectory

 gulp.task 'markdown', ['copy:temp'], ->


Gulpfile reformat

Gulpfile reformat

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -72,10 +72,12 @@
 gulp.task "test", ["build-debug", "build-test"], ->
 	return gulp
 		.src("test/phantomjs/index.html")
-		.pipe(phantomjs({reporter:"dot", viewportSize: {
-			width: 1024,
-			height: 768
-		}}))
+		.pipe(phantomjs({
+			reporter:"dot",
+			viewportSize: {width: 1024, height: 768},
+			useColors: true,
+			loadImages: false
+		}))

 gulp.task "watch", ->
 	gulp.watch(["./*.coffee", "framer/**", "test/tests/**", "!Version.coffee"], ["test"])


fix errors in build process

fix errors in build process

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -44,7 +44,7 @@
   gulp.watch "./assets/stylus/**/*.styl", ['css:preview']

-gulp.task "clean:build", ->
+gulp.task "build:clean", ->
   return gulp.src('./build',{read:false})
           .pipe(clean())

@@ -121,7 +121,7 @@
       .pipe gulp.dest "./build/js"

 gulp.task "js:sidenav:ref", ->
-  return gulp.src "./templates/includes/js.jade", {base: "./"}
+  return gulp.src "./templates/includes/js.jade"
           .pipe rev_replace( { manifest: gulp.src("assets/rev_manifest/rev-manifest.json"), replaceInExtensions: ['.jade']} )
           .pipe gulp.dest "./"

@@ -143,7 +143,7 @@
       .pipe gulp.dest "./build/js"

 gulp.task "js:video:ref", ->
-  return gulp.src "./templates/vr-ar.jade", {base: "./"}
+  return gulp.src "./templates/vr-ar.jade"
           .pipe rev_replace( { manifest: gulp.src("assets/rev_manifest/rev-manifest.json"), replaceInExtensions: ['.jade']} )
           .pipe gulp.dest "./"

@@ -169,7 +169,7 @@
         .pipe gulp.dest "./build/js"

 gulp.task "js:coffee:ref", ->
-  return gulp.src "./templates/includes/js.jade", {base: "./"}
+  return gulp.src "./templates/includes/js.jade"
           .pipe rev_replace( { manifest: gulp.src("assets/rev_manifest/rev-manifest.json"), replaceInExtensions: ['.jade']} )
           .pipe gulp.dest "./"


default keybinding for goto-symbol

default keybinding for goto-symbol

--- keymaps/julia-client.cson
+++ keymaps/julia-client.cson
@@ -11,6 +11,7 @@
   'cmd-j cmd-d': 'julia-client:toggle-documentation'
   'cmd-shift-a': 'julia:select-block'
   'ctrl-x': 'julia-debug:toggle-breakpoint'
+  'cmd-j cmd-g': 'julia-client:goto-symbol'

 '.platform-darwin .item-views > atom-text-editor[data-grammar="source julia"],
  .platform-darwin ink-console.julia,
@@ -43,6 +44,7 @@
   'ctrl-shift-c': 'julia-client:interrupt-julia'
   'ctrl-j ctrl-d': 'julia-client:toggle-documentation'
   'alt-x': 'julia-debug:toggle-breakpoint'
+  'ctrl-j ctrl-g': 'julia-client:goto-symbol'

 '.platform-win32 ink-console.julia,
  .platform-win32 ink-console.julia atom-text-editor,


Support for custom renderer on series graph

Also moved config vars to node rather than container

--- widgets/series_graph/series_graph.coffee
+++ widgets/series_graph/series_graph.coffee
@@ -11,10 +11,11 @@
     # Gross hacks. Let's fix this.
     width = (Dashing.widget_base_dimensions[0] * container.data("sizex")) + Dashing.widget_margins[0] * 2 * (container.data("sizex") - 1)
     height = (Dashing.widget_base_dimensions[1] * container.data("sizey"))
-    if container.data("colorscheme")
-      scheme = container.data("colorscheme")
-    else if container.data("colors")
-      scheme = container.data("colors").split(' ')
+
+    if $(@node).data("colorscheme")
+      scheme = $(@node).data("colorscheme")
+    else if $(@node).data("colors")
+      scheme = $(@node).data("colors").split(' ')
     else
       scheme = 'spectrum14'
     palette = new Rickshaw.Color.Palette({scheme: scheme})
@@ -25,6 +26,7 @@

     @graph = new Rickshaw.Graph(
       element: @node
+      renderer: $(@node).data("renderer") || 'area'
       width: width
       height: height
       series: seriesCombined
@@ -32,7 +34,7 @@

     x_axis = new Rickshaw.Graph.Axis.Time(graph: @graph)
     y_axis = new Rickshaw.Graph.Axis.Y(graph: @graph, tickFormat: Rickshaw.Fixtures.Number.formatKMBT)
-    @graph.renderer.unstack
+    @graph.renderer.unstack if $(@node).data("unstack")
     @graph.render()

   onData: (data) ->


Updated size limit on sale fetch

Updated size limit on sale fetch

We currently have more than 20 upcoming auctions and this list is reverse sorted on the client so the most upcoming auctions are actually being cut off.

--- mobile/apps/auctions/routes.coffee
+++ mobile/apps/auctions/routes.coffee
@@ -13,7 +13,7 @@
   sales = new Sales
   sales.fetch
     cache: true
-    data: is_auction: true, published: true, size: 20, sort: '-timely_at,name'
+    data: is_auction: true, published: true, size: 100, sort: '-timely_at,name'
     success: (collection, response, options) ->
       # Fetch artworks for the sale
       Q.allSettled(sales.map (sale) ->


Remove redundant path in .desktop for Linux

The install-task has two instructions that add an extra "resources" to the iconName directory and thus the icon doesn't work. The resulting nylus.desktop has an invalid path for the icon.

--- build/tasks/install-task.coffee
+++ build/tasks/install-task.coffee
@@ -33,7 +33,7 @@
     else
       binDir = path.join(installDir, 'bin')
       shareDir = path.join(installDir, 'share', 'nylas')
-      iconName = path.join(shareDir, 'resources', 'app', 'resources', 'nylas.png')
+      iconName = path.join(shareDir, 'resources', 'app', 'nylas.png')

       mkdir binDir
       # Note that `N1.sh` can't be renamed `nylas.sh` because `apm`
@@ -50,7 +50,7 @@
         desktopInstallFile = path.join(installDir, 'share', 'applications', 'nylas.desktop')

         {description} = grunt.file.readJSON('package.json')
-        iconName = path.join(shareDir, 'resources', 'app', 'resources', 'nylas.png')
+        iconName = path.join(shareDir, 'resources', 'app', 'nylas.png')
         installDir = path.join(installDir, '.') # To prevent "Exec=/usr/local//share/nylas/nylas"
         template = _.template(String(fs.readFileSync(desktopFile)))
         filled = template({description, installDir, iconName})


Render out italics at all sizes.

Render out italics at all sizes.

--- grammars/restructuredtext.cson
+++ grammars/restructuredtext.cson
@@ -418,7 +418,7 @@
             'name': 'punctuation.definition.italic.restructuredtext'
           '2':
             'name': 'punctuation.definition.italic.restructuredtext'
-        'match': '(\\*)\\w[^*]+\\w(\\*)'
+        'match': '(\\*)[^*]+(\\*)'
         'name': 'markup.italic.restructuredtext'
       }
     ]